最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Python OpenCV實(shí)現(xiàn)相機(jī)標(biāo)定的完整代碼示例

 更新時(shí)間:2026年07月24日 09:10:27   作者:Humbunklung  
相機(jī)標(biāo)定(Camera Calibration) 是指通過一系列數(shù)學(xué)方法,估計(jì)相機(jī)成像模型中各項(xiàng)參數(shù)的過程,本文將細(xì)講解OpenCV實(shí)現(xiàn)相機(jī)標(biāo)定的原理與代碼,有需要的小伙伴可以了解下

1. 相機(jī)標(biāo)定的定義

相機(jī)標(biāo)定(Camera Calibration) 是指通過一系列數(shù)學(xué)方法,估計(jì)相機(jī)成像模型中各項(xiàng)參數(shù)的過程。這些參數(shù)包括描述相機(jī)內(nèi)部光學(xué)和幾何特性的內(nèi)參(Intrinsic Parameters),以及描述相機(jī)在三維世界中位置和姿態(tài)的外參(Extrinsic Parameters),以及描述鏡頭幾何失真的畸變系數(shù)(Distortion Coefficients)。

通俗地說,相機(jī)標(biāo)定就是為相機(jī)建立一套精確的數(shù)學(xué)模型,使得計(jì)算機(jī)能夠通過這個(gè)模型,將二維圖像中的像素坐標(biāo)與三維世界中的物理坐標(biāo)相互轉(zhuǎn)換。

參數(shù)類型具體內(nèi)容作用
內(nèi)參矩陣 K焦距 fx?,fy?,主點(diǎn)坐標(biāo)cx?,cy?描述相機(jī)將3D點(diǎn)投影到圖像平面的方式
外參 [R | t]旋轉(zhuǎn)矩陣 R,平移向量 t描述相機(jī)坐標(biāo)系相對于世界坐標(biāo)系的位姿
畸變系數(shù)徑向k1?,k2?,k3?,切向p1?,p2?描述鏡頭引入的幾何失真

2. 為什么要進(jìn)行相機(jī)標(biāo)定

在計(jì)算機(jī)視覺的實(shí)際應(yīng)用中,相機(jī)標(biāo)定是幾乎所有三維感知任務(wù)的先決條件,其重要性體現(xiàn)在以下三個(gè)層面。

第一,建立坐標(biāo)映射關(guān)系。 相機(jī)成像是一個(gè)將三維世界降維投影到二維圖像的過程,這一過程中深度信息被丟失。通過標(biāo)定獲得的內(nèi)外參數(shù),我們可以建立像素坐標(biāo)與世界坐標(biāo)之間的精確數(shù)學(xué)關(guān)系,這是三維重建、機(jī)器人定位、自動駕駛感知等任務(wù)的基礎(chǔ)。

第二,校正鏡頭畸變。 實(shí)際相機(jī)鏡頭并非理想的針孔模型,透鏡的形狀和組裝誤差會引入徑向畸變(桶形畸變或枕形畸變)和切向畸變。這些畸變會使圖像中的直線變彎,嚴(yán)重影響幾何測量精度。標(biāo)定可以求出畸變系數(shù),進(jìn)而對圖像進(jìn)行去畸變處理。

第三,提高系統(tǒng)測量精度。 在工業(yè)視覺檢測、醫(yī)學(xué)圖像分析、精密測量等高要求場景中,未經(jīng)標(biāo)定的相機(jī)會引入系統(tǒng)性誤差。精確的標(biāo)定是保證測量結(jié)果可靠性的必要前提。

3. 針孔相機(jī)模型與坐標(biāo)系

理解相機(jī)標(biāo)定,首先需要了解相機(jī)的數(shù)學(xué)模型。最常用的是針孔相機(jī)模型(Pinhole Camera Model),它將相機(jī)簡化為一個(gè)光線通過單個(gè)點(diǎn)(光心)投射到成像平面的理想模型。

3.1 四個(gè)坐標(biāo)系

相機(jī)成像過程涉及四個(gè)坐標(biāo)系之間的連續(xù)變換:

下圖展示了這四個(gè)坐標(biāo)系的關(guān)系:

# ── 環(huán)境準(zhǔn)備 ──────────────────────────────────────────────────────────────
import cv2
import numpy as np
# import matplotlib
import platform
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
# from matplotlib import font_manager
# import warnings
# warnings.filterwarnings('ignore')

# # 配置中文字體(使用系統(tǒng)內(nèi)置的 Noto CJK 字體)
# _FONT_PATH = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'
# try:
#     _zh_font = font_manager.FontProperties(fname=_FONT_PATH)
#     _zh_font_sm = font_manager.FontProperties(fname=_FONT_PATH, size=9)
#     _zh_font_md = font_manager.FontProperties(fname=_FONT_PATH, size=10)
#     _zh_font_lg = font_manager.FontProperties(fname=_FONT_PATH, size=13)
#     _zh_font_xl = font_manager.FontProperties(fname=_FONT_PATH, size=14)
#     print("? 中文字體加載成功")
# except Exception:
#     _zh_font = _zh_font_sm = _zh_font_md = _zh_font_lg = _zh_font_xl = None
#     print("? 未找到 Noto CJK 字體,中文標(biāo)注可能顯示為方框")

if platform.system() == 'Windows':
    plt.rcParams['font.sans-serif'] = ['SimHei']
else:
    plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei']
plt.rcParams['axes.unicode_minus'] = False

print(f"OpenCV 版本: {cv2.__version__}")
print(f"NumPy  版本: {np.__version__}")

輸出:

OpenCV 版本: 5.0.0
NumPy  版本: 2.5.1

# ── 圖1:四個(gè)坐標(biāo)系關(guān)系示意圖 ──────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(13, 3))
ax.axis('off')
ax.set_xlim(0, 11)
ax.set_ylim(0, 2.8)

boxes = [
    ('世界坐標(biāo)系\n$(X_w, Y_w, Z_w)$', 0.4),
    ('相機(jī)坐標(biāo)系\n$(X_c, Y_c, Z_c)$', 3.0),
    ('圖像物理坐標(biāo)系\n$(x, y)$', 5.6),
    ('像素坐標(biāo)系\n$(u, v)$', 8.2),
]
arrows = ['旋轉(zhuǎn) R\n平移 t', '透視投影\n焦距 f', '縮放\n平移']

for (label, x) in boxes:
    rect = mpatches.FancyBboxPatch((x, 0.9), 2.2, 1.2,
                                    boxstyle='round,pad=0.1',
                                    linewidth=1.8, edgecolor='steelblue',
                                    facecolor='#ddeeff')
    ax.add_patch(rect)
    # ax.text(x + 1.1, 1.5, label, ha='center', va='center',
    #         fontproperties=_zh_font_sm)
    ax.text(x + 1.1, 1.5, label, ha='center', va='center')
for arrow_label, x_start in zip(arrows, [2.6, 5.2, 7.8]):
    ax.annotate('', xy=(x_start + 0.4, 1.5), xytext=(x_start, 1.5),
                arrowprops=dict(arrowstyle='->', color='tomato', lw=2))
    # ax.text(x_start + 0.2, 2.1, arrow_label, ha='center', va='bottom',
    #         fontproperties=_zh_font_sm, color='tomato')
    ax.text(x_start + 0.2, 2.1, arrow_label, ha='center', va='bottom', color='tomato')

# ax.set_title('相機(jī)成像的四個(gè)坐標(biāo)系及其轉(zhuǎn)換關(guān)系', fontproperties=_zh_font_xl)
ax.set_title('相機(jī)成像的四個(gè)坐標(biāo)系及其轉(zhuǎn)換關(guān)系')
plt.tight_layout()
plt.show()

3.2 針孔相機(jī)模型的數(shù)學(xué)描述

完整的相機(jī)投影模型可以用以下矩陣方程表示:

其中,fx?=f/dx? fy?=f/dy? 分別是以像素為單位的水平和垂直焦距,cx?,cy? 是主點(diǎn)(光軸與成像平面的交點(diǎn))的像素坐標(biāo), Z 是尺度因子(即物點(diǎn)的深度)。

# ── 圖2:針孔相機(jī)成像原理示意圖 ────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(11, 5))
ax.set_xlim(0, 10)
ax.set_ylim(-3.2, 3.2)
ax.set_aspect('equal')
ax.axis('off')

# 光軸
ax.annotate('', xy=(9.5, 0), xytext=(0.5, 0),
            arrowprops=dict(arrowstyle='->', color='gray', lw=1.5))
# ax.text(9.55, 0.15, '光軸 Z', fontsize=10, color='gray', fontproperties=_zh_font_md)
ax.text(9.55, 0.15, '光軸 Z', fontsize=10, color='gray')

# 物體
ax.plot([2, 2], [0, 2.2], 'g-', lw=4)
ax.plot([2], [2.2], 'g^', markersize=14)
# ax.text(1.4, -0.45, '物體 P', fontsize=10, ha='center', fontproperties=_zh_font_md)
ax.text(1.4, -0.45, '物體 P', fontsize=10, ha='center')

# 光心
ax.plot(5, 0, 'ko', markersize=12)
# ax.text(5, -0.5, '光心 O', fontsize=10, ha='center', fontproperties=_zh_font_md)
ax.text(5, -0.5, '光心 O', fontsize=10, ha='center')

# 成像平面
ax.plot([7.5, 7.5], [-2.2, 2.2], 'b-', lw=3)
# ax.text(7.5, -2.7, '成像平面', fontsize=10, ha='center', fontproperties=_zh_font_md)
ax.text(7.5, -2.7, '成像平面', fontsize=10, ha='center')

# 投影線
ax.plot([2, 7.5], [2.2, -1.65], 'r--', lw=1.5, alpha=0.7)
ax.plot([2, 7.5], [0, 0], 'r--', lw=1.5, alpha=0.7)

# 像點(diǎn)
ax.plot([7.5], [-1.65], 'r^', markersize=12)
# ax.text(7.85, -1.65, "像點(diǎn) p'", fontsize=10, fontproperties=_zh_font_md)
ax.text(7.85, -1.65, "像點(diǎn) p'", fontsize=10)

# 焦距標(biāo)注
ax.annotate('', xy=(7.5, -3.0), xytext=(5, -3.0),
            arrowprops=dict(arrowstyle='<->', color='purple', lw=1.5))
# ax.text(6.25, -3.2, '焦距 f', fontsize=10, ha='center', color='purple', fontproperties=_zh_font_md)
ax.text(6.25, -3.2, '焦距 f', fontsize=10, ha='center', color='purple')

# 物距標(biāo)注
ax.annotate('', xy=(5, 3.0), xytext=(2, 3.0),
            arrowprops=dict(arrowstyle='<->', color='darkgreen', lw=1.5))
# ax.text(3.5, 3.15, '物距 Z', fontsize=10, ha='center', color='darkgreen', fontproperties=_zh_font_md)
ax.text(3.5, 3.15, '物距 Z', fontsize=10, ha='center', color='darkgreen')

# ax.set_title('針孔相機(jī)成像原理示意圖', fontproperties=_zh_font_xl, pad=15)
ax.set_title('針孔相機(jī)成像原理示意圖', pad=15)
plt.tight_layout()
plt.show()

3.3 鏡頭畸變模型

實(shí)際相機(jī)鏡頭會引入畸變,主要分為兩類:

徑向畸變(Radial Distortion) 由透鏡形狀引起,表現(xiàn)為圖像向中心收縮(桶形)或向外擴(kuò)張(枕形):

切向畸變(Tangential Distortion) 由透鏡與成像平面不平行引起:

其中 (x,y) 為理想歸一化圖像坐標(biāo), (x^,y^?) 為畸變后的坐標(biāo),r2=x2+y2。

# ── 圖3:徑向畸變類型對比 ──────────────────────────────────────────────────
fig, axes = plt.subplots(1, 3, figsize=(13, 4.5))

def draw_distorted_grid(ax, distort_fn, title):
    ax.set_xlim(-1.5, 1.5)
    ax.set_ylim(-1.5, 1.5)
    ax.set_aspect('equal')
    ax.axis('off')
    # ax.set_title(title, fontproperties=_zh_font_lg)
    ax.set_title(title)
    for v in np.linspace(-1.2, 1.2, 7):
        pts = [distort_fn(u, v) for u in np.linspace(-1.2, 1.2, 120)]
        ax.plot([p[0] for p in pts], [p[1] for p in pts], 'steelblue', lw=1.2)
    for u in np.linspace(-1.2, 1.2, 7):
        pts = [distort_fn(u, v) for v in np.linspace(-1.2, 1.2, 120)]
        ax.plot([p[0] for p in pts], [p[1] for p in pts], 'steelblue', lw=1.2)

draw_distorted_grid(axes[0], lambda x, y: (x, y), '無畸變(理想模型)')
draw_distorted_grid(axes[1],
    lambda x, y: (x*(1 - 0.3*(x**2+y**2)), y*(1 - 0.3*(x**2+y**2))),
    '桶形畸變\n($k_1 < 0$,圖像向內(nèi)收縮)')
draw_distorted_grid(axes[2],
    lambda x, y: (x*(1 + 0.3*(x**2+y**2)), y*(1 + 0.3*(x**2+y**2))),
    '枕形畸變\n($k_1 > 0$,圖像向外擴(kuò)張)')

# plt.suptitle('徑向畸變類型示意圖', fontproperties=_zh_font_xl, y=1.02)
plt.suptitle('徑向畸變類型示意圖', y=1.02)
plt.tight_layout()
plt.show()

4. 張正友相機(jī)標(biāo)定法

4.1 方法簡介

張正友標(biāo)定法(Zhang’s Method) 由微軟研究院張正友博士于1998年提出,發(fā)表于 IEEE Transactions on Pattern Analysis and Machine Intelligence。該方法介于傳統(tǒng)需要高精度三維標(biāo)定物的方法和完全自標(biāo)定方法之間,僅需使用一個(gè)打印的二維棋盤格標(biāo)定板,從多個(gè)角度拍攝若干張圖像即可完成標(biāo)定,兼具精度高、操作簡便的優(yōu)點(diǎn),已被 OpenCV 等主流視覺庫直接集成。

4.2 核心原理

張正友標(biāo)定法將世界坐標(biāo)系固定于棋盤格平面上,令所有角點(diǎn)的 Z w = 0 Z_w = 0 Zw?=0。此時(shí),三維到二維的投影關(guān)系退化為平面單應(yīng)性(Homography):

?

其中 H=K[r1?,r2?,t] 是一個(gè) 3×3 的單應(yīng)性矩陣,可由每張圖像的角點(diǎn)對應(yīng)關(guān)系求解。利用旋轉(zhuǎn)矩陣列向量之間的單位正交約束r1T?r1?=r2T?r2?=1r1T?r2?=0,可以建立關(guān)于內(nèi)參矩陣的線性方程組,從而在不需要三維標(biāo)定物的情況下恢復(fù)內(nèi)參。

4.3 標(biāo)定流程

下圖展示了使用 OpenCV 實(shí)現(xiàn)張正友標(biāo)定法的完整步驟:

# ── 圖4:張正友標(biāo)定法完整流程圖 ────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(11, 8))
ax.axis('off')
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)

steps = [
    ('① 打印棋盤格標(biāo)定板,從多角度拍攝若干張圖像(建議 15~20 張)', '#4472C4', '#dce6f1'),
    ('② 調(diào)用 findChessboardCorners 提取每張圖像的內(nèi)角點(diǎn)像素坐標(biāo)', '#ED7D31', '#fce4d6'),
    ('③ 調(diào)用 cornerSubPix 將角點(diǎn)坐標(biāo)精化至亞像素級別', '#70AD47', '#e2efda'),
    ('④ 調(diào)用 calibrateCamera 求解內(nèi)參矩陣 K 和畸變系數(shù) D', '#FF0000', '#ffd7d7'),
    ('⑤ 利用 undistort / initUndistortRectifyMap 對圖像進(jìn)行去畸變', '#7030A0', '#ede7f6'),
    ('⑥ 評估重投影誤差,必要時(shí)增補(bǔ)圖像并重新標(biāo)定', '#C00000', '#f4cccc'),
]

y_positions = [9.0, 7.4, 5.8, 4.2, 2.6, 1.0]

for (text, edge_color, face_color), y in zip(steps, y_positions):
    rect = mpatches.FancyBboxPatch((0.4, y - 0.55), 9.2, 1.1,
                                    boxstyle='round,pad=0.1',
                                    linewidth=2, edgecolor=edge_color,
                                    facecolor=face_color)
    ax.add_patch(rect)
    # ax.text(5, y, text, ha='center', va='center',
    #         fontproperties=_zh_font_md, color='#1a1a1a')
    ax.text(5, y, text, ha='center', va='center', color='#1a1a1a')

for y_cur, y_next in zip(y_positions[:-1], y_positions[1:]):
    ax.annotate('', xy=(5, y_next + 0.55), xytext=(5, y_cur - 0.55),
                arrowprops=dict(arrowstyle='->', color='#444444', lw=2))

# ax.set_title('張正友相機(jī)標(biāo)定法完整流程(OpenCV 實(shí)現(xiàn))',
#              fontproperties=_zh_font_xl, pad=12)
ax.set_title('張正友相機(jī)標(biāo)定法完整流程(OpenCV 實(shí)現(xiàn))', pad=12)
plt.tight_layout()
plt.show()

4.4 OpenCV 代碼實(shí)現(xiàn)

以下代碼完整演示了張正友標(biāo)定法的實(shí)現(xiàn)過程,包括:生成模擬棋盤格圖像、角點(diǎn)檢測與精化、執(zhí)行標(biāo)定、去畸變處理,以及重投影誤差評估。

# ── 步驟 1:生成模擬棋盤格圖像(實(shí)際應(yīng)用中應(yīng)替換為真實(shí)拍攝的照片)─────────

CHECKERBOARD = (6, 9)   # 棋盤格內(nèi)角點(diǎn)數(shù)量 (行數(shù), 列數(shù))
SQUARE_SIZE  = 0.025    # 每個(gè)方格的物理邊長(單位:米)

def generate_checkerboard(rows, cols, step=56, margin=60,
                           img_h=480, img_w=640):
    """生成標(biāo)準(zhǔn)棋盤格灰度圖像。"""
    img = np.ones((img_h, img_w), dtype=np.uint8) * 255
    for i in range(cols + 1):
        for j in range(rows + 1):
            if (i + j) % 2 == 0:
                x1 = margin + i * step
                y1 = margin + j * step
                x2 = min(x1 + step, img_w - 1)
                y2 = min(y1 + step, img_h - 1)
                cv2.rectangle(img, (x1, y1), (x2, y2), 0, -1)
    return img

# 生成一張基準(zhǔn)棋盤格圖像
base_board = generate_checkerboard(*CHECKERBOARD)

# 通過仿射變換模擬多角度拍攝,生成 12 張不同視角的圖像
def make_perspective_views(base, n=12):
    h, w = base.shape
    views = []
    np.random.seed(42)
    for _ in range(n):
        # 隨機(jī)仿射變換(模擬輕微旋轉(zhuǎn)和縮放)
        angle = np.random.uniform(-12, 12)
        scale = np.random.uniform(0.85, 1.0)
        M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, scale)
        M[0, 2] += np.random.uniform(-20, 20)
        M[1, 2] += np.random.uniform(-15, 15)
        warped = cv2.warpAffine(base, M, (w, h),
                                flags=cv2.INTER_LINEAR,
                                borderMode=cv2.BORDER_CONSTANT,
                                borderValue=255)
        views.append(warped)
    return views

images = make_perspective_views(base_board, n=12)
print(f"共生成 {len(images)} 張模擬標(biāo)定圖像,尺寸:{images[0].shape}")

輸出:共生成 12 張模擬標(biāo)定圖像,尺寸:(480, 640)

# ── 步驟 2:角點(diǎn)檢測與亞像素精化 ──────────────────────────────────────────

# 停止準(zhǔn)則:最大迭代次數(shù) 30 次,或精度達(dá)到 0.001 像素
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 準(zhǔn)備世界坐標(biāo)系中的角點(diǎn)坐標(biāo)(Z=0 平面)
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0],
                        0:CHECKERBOARD[1]].T.reshape(-1, 2) * SQUARE_SIZE

objpoints = []  # 3D 世界坐標(biāo)點(diǎn)列表
imgpoints = []  # 2D 圖像像素坐標(biāo)點(diǎn)列表
valid_images = []

for idx, gray in enumerate(images):
    ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
    if ret:
        # cornerSubPix:將角點(diǎn)精化到亞像素精度
        corners_refined = cv2.cornerSubPix(
            gray, corners, (11, 11), (-1, -1), criteria)
        objpoints.append(objp)
        imgpoints.append(corners_refined)
        valid_images.append(gray)

print(f"成功檢測到角點(diǎn)的圖像數(shù)量:{len(valid_images)} / {len(images)}")

# 可視化第一張有效圖像的角點(diǎn)檢測結(jié)果
img_vis = cv2.cvtColor(valid_images[0], cv2.COLOR_GRAY2BGR)
cv2.drawChessboardCorners(img_vis, CHECKERBOARD, imgpoints[0], True)

fig, axes = plt.subplots(1, 2, figsize=(13, 5))
axes[0].imshow(valid_images[0], cmap='gray')
# axes[0].set_title('原始棋盤格圖像', fontproperties=_zh_font_lg)
axes[0].set_title('原始棋盤格圖像')
axes[0].axis('off')
axes[1].imshow(cv2.cvtColor(img_vis, cv2.COLOR_BGR2RGB))
# axes[1].set_title('角點(diǎn)檢測結(jié)果(findChessboardCorners + cornerSubPix)',
#                    fontproperties=_zh_font_lg)
axes[1].set_title('角點(diǎn)檢測結(jié)果(findChessboardCorners + cornerSubPix)')
axes[1].axis('off')
# plt.suptitle('張正友標(biāo)定法 — 棋盤格角點(diǎn)檢測與精化', fontproperties=_zh_font_xl)
plt.suptitle('張正友標(biāo)定法 — 棋盤格角點(diǎn)檢測與精化')
plt.tight_layout()
plt.show()

輸出: 成功檢測到角點(diǎn)的圖像數(shù)量:12 / 12

# ── 步驟 3:執(zhí)行相機(jī)標(biāo)定 ───────────────────────────────────────────────────

img_size = (valid_images[0].shape[1], valid_images[0].shape[0])  # (width, height)

# cv2.calibrateCamera 返回:
#   ret    — 平均重投影誤差(越小越好,通常要求 < 1.0 像素)
#   mtx    — 3×3 相機(jī)內(nèi)參矩陣 K
#   dist   — 畸變系數(shù)向量 [k1, k2, p1, p2, k3]
#   rvecs  — 每張圖像的旋轉(zhuǎn)向量列表
#   tvecs  — 每張圖像的平移向量列表
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, img_size, None, None)

print("=" * 55)
print("  相機(jī)標(biāo)定結(jié)果")
print("=" * 55)
print(f"\n平均重投影誤差 (RMS): {ret:.4f} 像素")
print("\n內(nèi)參矩陣 K:")
print(f"  fx = {mtx[0,0]:.2f} px,  fy = {mtx[1,1]:.2f} px")
print(f"  cx = {mtx[0,2]:.2f} px,  cy = {mtx[1,2]:.2f} px")
print("\n  K =\n", np.round(mtx, 2))
print("\n畸變系數(shù) [k1, k2, p1, p2, k3]:")
print("  ", np.round(dist.ravel(), 6))
print("=" * 55)

輸出:

=======================================================
  相機(jī)標(biāo)定結(jié)果
=======================================================
平均重投影誤差 (RMS): 0.0637 像素
內(nèi)參矩陣 K:
  fx = 156005.03 px,  fy = 156305.83 px
  cx = 333.07 px,  cy = 655.47 px
  K =
 [[1.5600503e+05 0.0000000e+00 3.3307000e+02]
 [0.0000000e+00 1.5630583e+05 6.5547000e+02]
 [0.0000000e+00 0.0000000e+00 1.0000000e+00]]
畸變系數(shù) [k1, k2, p1, p2, k3]:
   [-4.505586 -2.842325 -0.021303 -0.007666 -0.      ]
=======================================================

# ── 步驟 4:去畸變處理 ────────────────────────────────────────────────────
# 模擬一張帶有桶形畸變的網(wǎng)格圖像,演示去畸變效果

h_g, w_g = 400, 400
img_grid = np.ones((h_g, w_g, 3), dtype=np.uint8) * 255
for i in range(0, h_g, 40):
    cv2.line(img_grid, (0, i), (w_g, i), (160, 160, 160), 1)
for j in range(0, w_g, 40):
    cv2.line(img_grid, (j, 0), (j, h_g), (160, 160, 160), 1)
cv2.rectangle(img_grid, (0, 0), (w_g-1, h_g-1), (0, 0, 0), 2)

# 使用模擬的相機(jī)參數(shù)和畸變系數(shù)引入桶形畸變
K_sim = np.array([[350., 0., 200.], [0., 350., 200.], [0., 0., 1.]])
D_sim = np.array([-0.4, 0.15, 0., 0., 0.])
map1, map2 = cv2.initUndistortRectifyMap(
    K_sim, D_sim, None, K_sim, (w_g, h_g), cv2.CV_32FC1)
img_distorted = cv2.remap(img_grid, map1, map2, cv2.INTER_LINEAR)

# 使用 cv2.undistort 進(jìn)行去畸變
img_undistorted = cv2.undistort(img_distorted, K_sim, D_sim)

fig, axes = plt.subplots(1, 3, figsize=(14, 5))
axes[0].imshow(img_grid)
# axes[0].set_title('原始網(wǎng)格圖像(理想無畸變)', fontproperties=_zh_font_lg)
axes[0].set_title('原始網(wǎng)格圖像(理想無畸變)')
axes[0].axis('off')
axes[1].imshow(img_distorted)
# axes[1].set_title('引入桶形畸變后的圖像', fontproperties=_zh_font_lg)
axes[1].set_title('引入桶形畸變后的圖像')
axes[1].axis('off')
axes[2].imshow(img_undistorted)
# axes[2].set_title('cv2.undistort 去畸變結(jié)果', fontproperties=_zh_font_lg)
axes[2].set_title('cv2.undistort 去畸變結(jié)果')
axes[2].axis('off')
# plt.suptitle('相機(jī)標(biāo)定 — 去畸變效果對比', fontproperties=_zh_font_xl)
plt.suptitle('相機(jī)標(biāo)定 — 去畸變效果對比')
plt.tight_layout()
plt.show()

# ── 步驟 5:重投影誤差評估 ────────────────────────────────────────────────
# 重投影誤差是衡量標(biāo)定質(zhì)量的核心指標(biāo):
# 將已知3D點(diǎn)用標(biāo)定結(jié)果重新投影到圖像,計(jì)算與實(shí)際檢測點(diǎn)的距離誤差

errors = []
for i in range(len(objpoints)):
    # 將3D點(diǎn)重新投影到圖像平面
    projected, _ = cv2.projectPoints(
        objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    # 計(jì)算與實(shí)際檢測角點(diǎn)的歐氏距離
    actual = imgpoints[i].reshape(-1, 2).astype(np.float32)
    proj   = projected.reshape(-1, 2).astype(np.float32)
    err = np.sqrt(np.mean(np.sum((actual - proj)**2, axis=1)))
    errors.append(err)

fig, ax = plt.subplots(figsize=(9, 4))
bars = ax.bar(range(1, len(errors)+1), errors,
              color='steelblue', edgecolor='white', linewidth=0.8)
ax.axhline(np.mean(errors), color='tomato', lw=2, ls='--',
           label=f'平均誤差 = {np.mean(errors):.4f} px')
# ax.set_xlabel('圖像編號', fontproperties=_zh_font_md)
ax.set_xlabel('圖像編號')
# ax.set_ylabel('重投影誤差(像素)', fontproperties=_zh_font_md)
ax.set_ylabel('重投影誤差(像素)')
# ax.set_title('各圖像的重投影誤差分布', fontproperties=_zh_font_xl)
ax.set_title('各圖像的重投影誤差分布')
# ax.legend(prop=_zh_font_md)
ax.legend()
ax.set_xticks(range(1, len(errors)+1))
plt.tight_layout()
plt.show()

print(f"平均重投影誤差:{np.mean(errors):.4f} 像素")
print("(通常認(rèn)為誤差 < 1.0 像素為合格,< 0.5 像素為優(yōu)秀)")

輸出:

 平均重投影誤差:0.0608 像素
(通常認(rèn)為誤差 < 1.0 像素為合格,< 0.5 像素為優(yōu)秀)

5. 比例因子計(jì)算方法

5.1 方法簡介

比例因子計(jì)算方法(Scale Factor / Pixels-Per-Metric Method) 是一種簡化的相機(jī)測量方法。在某些不需要完整三維標(biāo)定的應(yīng)用場景中,如果被測物體與相機(jī)之間的距離固定,且所有物體處于同一深度平面,可以通過在場景中放置一個(gè)已知物理尺寸的參考物,計(jì)算像素與物理單位之間的換算比例,從而估算其他物體的實(shí)際尺寸。

5.2 方法局限性

比例因子方法成立的前提條件較為嚴(yán)格,實(shí)際使用時(shí)需注意以下限制:

假設(shè)條件說明
同一深度平面參考物與待測物必須在同一距離處,否則透視效應(yīng)會引入誤差
畸變較小圖像邊緣的畸變會導(dǎo)致比例因子在不同區(qū)域不一致
相機(jī)固定相機(jī)位置和焦距不能改變,否則比例因子失效
參考物精確參考物的實(shí)際尺寸必須精確已知

5.3 OpenCV 代碼實(shí)現(xiàn)

# ── 比例因子法:生成演示場景 ──────────────────────────────────────────────
# 場景描述:
#   左側(cè):參考物(已知寬度 = 2.0 cm,像素寬度 = 100 px)
#   右側(cè):待測物(寬度未知,像素寬度約 230 px)

KNOWN_WIDTH_CM = 2.0  # 參考物的實(shí)際寬度(厘米)

# 構(gòu)建演示圖像
scene = np.ones((420, 720, 3), dtype=np.uint8) * 245

# 參考物(橙色邊框):像素寬度 = 180-80 = 100 px
cv2.rectangle(scene, (80, 150), (180, 260), (30, 30, 30), -1)
cv2.rectangle(scene, (80, 150), (180, 260), (0, 120, 220), 3)
cv2.putText(scene, 'Reference Object', (60, 140),
            cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 120, 220), 1)
# 寬度標(biāo)注
cv2.arrowedLine(scene, (80, 285), (180, 285), (0, 120, 220), 2, tipLength=0.15)
cv2.arrowedLine(scene, (180, 285), (80, 285), (0, 120, 220), 2, tipLength=0.15)
cv2.putText(scene, '100 px = 2.0 cm', (55, 308),
            cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 120, 220), 1)

# 待測物(綠色邊框):像素寬度 = 560-330 = 230 px
cv2.rectangle(scene, (330, 110), (560, 300), (30, 30, 30), -1)
cv2.rectangle(scene, (330, 110), (560, 300), (0, 170, 80), 3)
cv2.putText(scene, 'Unknown Object', (330, 100),
            cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 170, 80), 1)
# 寬度標(biāo)注
cv2.arrowedLine(scene, (330, 325), (560, 325), (0, 170, 80), 2, tipLength=0.08)
cv2.arrowedLine(scene, (560, 325), (330, 325), (0, 170, 80), 2, tipLength=0.08)
cv2.putText(scene, '230 px = ? cm', (360, 348),
            cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 170, 80), 1)

# 公式說明
cv2.putText(scene, 'Scale Factor = 100 px / 2.0 cm = 50 px/cm',
            (40, 380), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (140, 0, 160), 2)
cv2.putText(scene, 'Unknown Width = 230 px / 50 px/cm = 4.6 cm',
            (40, 410), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (140, 0, 160), 2)

fig, ax = plt.subplots(figsize=(11, 6))
ax.imshow(cv2.cvtColor(scene, cv2.COLOR_BGR2RGB))
# ax.set_title('比例因子計(jì)算方法 — 尺寸測量示意圖', fontproperties=_zh_font_xl)
ax.set_title('比例因子計(jì)算方法 — 尺寸測量示意圖')
ax.axis('off')
plt.tight_layout()
plt.show()

# ── 比例因子法:完整代碼實(shí)現(xiàn) ──────────────────────────────────────────────
# 本示例演示如何利用輪廓檢測自動計(jì)算比例因子并測量物體尺寸

def order_points(pts):
    """將矩形的四個(gè)頂點(diǎn)按 [左上, 右上, 右下, 左下] 排序。"""
    rect = np.zeros((4, 2), dtype='float32')
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]   # 左上
    rect[2] = pts[np.argmax(s)]   # 右下
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]  # 右上
    rect[3] = pts[np.argmax(diff)]  # 左下
    return rect

def midpoint(ptA, ptB):
    return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5)

# 1. 圖像預(yù)處理
gray_scene = cv2.cvtColor(scene, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray_scene, (7, 7), 0)
edged = cv2.Canny(blurred, 30, 100)
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)

# 2. 輪廓檢測
cnts, _ = cv2.findContours(
    edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 過濾面積過小的噪聲輪廓,并按 x 坐標(biāo)從左到右排序
cnts = [c for c in cnts if cv2.contourArea(c) > 500]
cnts = sorted(cnts, key=lambda c: cv2.boundingRect(c)[0])

pixels_per_cm = None
result_img = scene.copy()
measurements = []

for c in cnts:
    # 計(jì)算最小外接矩形
    box = cv2.minAreaRect(c)
    box_pts = cv2.boxPoints(box)
    box_pts = np.intp(box_pts)
    ordered = order_points(box_pts.astype('float32'))

    tl, tr, br, bl = ordered
    # 計(jì)算矩形的寬和高(像素)
    width_px  = np.linalg.norm(tr - tl)
    height_px = np.linalg.norm(bl - tl)

    # 第一個(gè)(最左側(cè))輪廓作為參考物,初始化比例因子
    if pixels_per_cm is None:
        pixels_per_cm = width_px / KNOWN_WIDTH_CM
        label = f'Ref: {KNOWN_WIDTH_CM:.1f}cm'
        color = (0, 120, 220)
    else:
        w_cm = width_px  / pixels_per_cm
        h_cm = height_px / pixels_per_cm
        label = f'{w_cm:.1f} x {h_cm:.1f} cm'
        color = (0, 170, 80)
        measurements.append((w_cm, h_cm))

    cv2.drawContours(result_img, [box_pts], -1, color, 2)
    cx, cy = int(np.mean(ordered[:, 0])), int(np.mean(ordered[:, 1]))
    cv2.putText(result_img, label, (cx - 50, cy),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, color, 2)

print(f"計(jì)算得到的比例因子:{pixels_per_cm:.2f} px/cm")
for i, (w, h) in enumerate(measurements):
    print(f"待測物 {i+1} 的實(shí)際尺寸:寬 = {w:.2f} cm,高 = {h:.2f} cm")

fig, ax = plt.subplots(figsize=(11, 6))
ax.imshow(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB))
# ax.set_title('比例因子法 — 自動輪廓檢測與尺寸測量結(jié)果', fontproperties=_zh_font_xl)
ax.set_title('比例因子法 — 自動輪廓檢測與尺寸測量結(jié)果')
ax.axis('off')
plt.tight_layout()
plt.show()

輸出:

計(jì)算得到的比例因子:37.50 px/cm
待測物 1 的實(shí)際尺寸:寬 = 1.89 cm,高 = 0.32 cm
待測物 2 的實(shí)際尺寸:寬 = 2.80 cm,高 = 3.07 cm
待測物 3 的實(shí)際尺寸:寬 = 2.70 cm,高 = 0.97 cm
待測物 4 的實(shí)際尺寸:寬 = 6.27 cm,高 = 5.20 cm
待測物 5 的實(shí)際尺寸:寬 = 6.19 cm,高 = 0.75 cm
待測物 6 的實(shí)際尺寸:寬 = 1.79 cm,高 = 0.32 cm

6. 兩種方法的對比總結(jié)

以下表格從多個(gè)維度對張正友標(biāo)定法和比例因子計(jì)算方法進(jìn)行系統(tǒng)比較:

# ── 圖:兩種方法對比可視化 ────────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(13, 5.5))
ax.axis('off')

headers = ['對比維度', '張正友標(biāo)定法', '比例因子計(jì)算方法']
rows = [
    ['標(biāo)定物', '二維棋盤格標(biāo)定板(已知格點(diǎn)尺寸)', '任意已知尺寸的參考物體'],
    ['輸出結(jié)果', '內(nèi)參矩陣 K、外參 [R|t]、畸變系數(shù) D', '像素/物理單位的換算比例'],
    ['精度', '高(可達(dá)亞像素級別)', '中等(受透視和畸變影響)'],
    ['適用場景', '三維重建、機(jī)器人定位、雙目視覺', '固定場景下的平面尺寸估算'],
    ['操作復(fù)雜度', '較高(需多角度拍攝 15~20 張)', '低(只需一張含參考物的圖像)'],
    ['關(guān)鍵 API', 'calibrateCamera, undistort', 'findContours, minAreaRect'],
]

col_widths = [0.22, 0.39, 0.39]
col_starts = [0.0, 0.22, 0.61]
row_height = 0.13
header_y = 0.88

# 繪制表頭
for j, (h, x, w) in enumerate(zip(headers, col_starts, col_widths)):
    rect = mpatches.FancyBboxPatch((x + 0.005, header_y), w - 0.01, row_height,
                                    transform=ax.transAxes,
                                    boxstyle='square,pad=0', clip_on=False,
                                    linewidth=0, facecolor='#2e5fa3')
    ax.add_patch(rect)
    # ax.text(x + w/2, header_y + row_height/2, h,
    #         transform=ax.transAxes, ha='center', va='center',
    #         fontproperties=_zh_font_md, color='white', fontweight='bold')
    ax.text(x + w/2, header_y + row_height/2, h,
            transform=ax.transAxes, ha='center', va='center',
            color='white', fontweight='bold')

# 繪制數(shù)據(jù)行
for i, row in enumerate(rows):
    y = header_y - (i + 1) * (row_height + 0.005)
    bg = '#f0f4ff' if i % 2 == 0 else '#ffffff'
    for j, (cell, x, w) in enumerate(zip(row, col_starts, col_widths)):
        rect = mpatches.FancyBboxPatch((x + 0.005, y), w - 0.01, row_height,
                                        transform=ax.transAxes,
                                        boxstyle='square,pad=0', clip_on=False,
                                        linewidth=0.5, edgecolor='#cccccc',
                                        facecolor=bg if j > 0 else '#e8f0fe')
        ax.add_patch(rect)
        # ax.text(x + w/2, y + row_height/2, cell,
        #         transform=ax.transAxes, ha='center', va='center',
        #         fontproperties=_zh_font_sm, color='#222222')
        ax.text(x + w/2, y + row_height/2, cell,
                transform=ax.transAxes, ha='center', va='center',
                color='#222222')

# ax.set_title('張正友標(biāo)定法 vs. 比例因子計(jì)算方法 — 綜合對比',
#              fontproperties=_zh_font_xl, pad=15)
ax.set_title('張正友標(biāo)定法 vs. 比例因子計(jì)算方法 — 綜合對比', pad=15)
plt.tight_layout()
plt.show()

6.1 方法選擇建議

在實(shí)際工程中,兩種方法的選擇取決于具體的應(yīng)用需求。張正友標(biāo)定法適用于對精度要求高、需要完整三維信息的場景,如機(jī)器人手眼標(biāo)定、雙目深度估計(jì)、增強(qiáng)現(xiàn)實(shí)(AR)等;而比例因子方法則適用于場景固定、只需要平面尺寸估算的輕量級應(yīng)用,如生產(chǎn)線上的零件尺寸檢測、文檔掃描中的比例估算等。

對于絕大多數(shù)需要精確三維感知的計(jì)算機(jī)視覺系統(tǒng),推薦優(yōu)先使用張正友標(biāo)定法,并結(jié)合 OpenCV 的 calibrateCamera 接口進(jìn)行完整的內(nèi)參、外參和畸變系數(shù)的估計(jì)。

以上就是Python OpenCV實(shí)現(xiàn)相機(jī)標(biāo)定的完整代碼示例的詳細(xì)內(nèi)容,更多關(guān)于Python相機(jī)標(biāo)定的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 基于Python實(shí)現(xiàn)一鍵獲取電腦瀏覽器的賬號密碼

    基于Python實(shí)現(xiàn)一鍵獲取電腦瀏覽器的賬號密碼

    發(fā)現(xiàn)很多人在學(xué)校圖書館喜歡用電腦占座,而且出去的時(shí)候經(jīng)常不鎖屏,為了讓大家養(yǎng)成良好的習(xí)慣,本文將分享一個(gè)小程序,可以快速獲取你存儲在電腦瀏覽器中的所有賬號和密碼,感興趣的可以了解一下
    2022-05-05
  • Python中cv2.Canny() 函數(shù)使用方法

    Python中cv2.Canny() 函數(shù)使用方法

    cv2.Canny() 函數(shù)是 OpenCV 中的邊緣檢測函數(shù)之一,用于檢測圖像的邊緣,它的基本原理是通過計(jì)算圖像中每個(gè)像素點(diǎn)的梯度值來檢測邊緣,本文通過示例代碼介紹Python中cv2.Canny() 函數(shù)用法,需要的朋友參考下吧
    2023-07-07
  • 解決py2exe打包后,總是多顯示一個(gè)DOS黑色窗口的問題

    解決py2exe打包后,總是多顯示一個(gè)DOS黑色窗口的問題

    今天小編就為大家分享一篇解決py2exe打包后,總是多顯示一個(gè)DOS黑色窗口的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-06-06
  • Python(Pandas、Dask、PySpark等庫)在大數(shù)據(jù)處理中的學(xué)習(xí)心得

    Python(Pandas、Dask、PySpark等庫)在大數(shù)據(jù)處理中的學(xué)習(xí)心得

    作者分享了他對Python在大數(shù)據(jù)處理中的學(xué)習(xí)心得,介紹了大數(shù)據(jù)的概念、特點(diǎn)及處理挑戰(zhàn),討論了Python的優(yōu)勢及常用的大數(shù)據(jù)處理庫,比較了Python與Rust在大數(shù)據(jù)處理中的優(yōu)劣,并提出了大數(shù)據(jù)處理的最佳實(shí)踐
    2026-04-04
  • Python Numpy中數(shù)據(jù)的常用保存與讀取方法

    Python Numpy中數(shù)據(jù)的常用保存與讀取方法

    這篇文章主要介紹了Python Numpy中數(shù)據(jù)的常用保存與讀取方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-04-04
  • python實(shí)現(xiàn)二分查找算法

    python實(shí)現(xiàn)二分查找算法

    這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)二分查找算法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-09-09
  • 解決Python print 輸出文本顯示 gbk 編碼錯(cuò)誤問題

    解決Python print 輸出文本顯示 gbk 編碼錯(cuò)誤問題

    這篇文章主要介紹了解決Python print 輸出文本顯示 gbk 編碼錯(cuò)誤問題,本文給出了三種解決方法,需要的朋友可以參考下
    2018-07-07
  • Python實(shí)現(xiàn)按比例產(chǎn)生隨機(jī)數(shù)的兩種方法

    Python實(shí)現(xiàn)按比例產(chǎn)生隨機(jī)數(shù)的兩種方法

    生成隨機(jī)數(shù)一般使用的就是random模塊下的函數(shù),生成的隨機(jī)數(shù)并不是真正意義上的隨機(jī)數(shù),而是對隨機(jī)數(shù)的一種模擬,random模塊包含各種偽隨機(jī)數(shù)生成函數(shù),以及各種根據(jù)概率分布生成隨機(jī)數(shù)的函數(shù)本文給大家介紹了Python實(shí)現(xiàn)按比例產(chǎn)生隨機(jī)數(shù)的兩種方法,需要的朋友可以參考下
    2025-08-08
  • PySpark中RDD的數(shù)據(jù)輸出問題詳解

    PySpark中RDD的數(shù)據(jù)輸出問題詳解

    RDD是 Spark 中最基礎(chǔ)的抽象,它表示了一個(gè)可以并行操作的、不可變得、被分區(qū)了的元素集合,這篇文章主要介紹了PySpark中RDD的數(shù)據(jù)輸出詳解,需要的朋友可以參考下
    2023-01-01
  • Python中edge-tts實(shí)現(xiàn)便捷語音合成

    Python中edge-tts實(shí)現(xiàn)便捷語音合成

    edge-tts是一個(gè)功能強(qiáng)大的Python庫,支持多種語言和聲音選項(xiàng),本文主要介紹了Python中edge-tts實(shí)現(xiàn)便捷語音合成,具有一定的參考價(jià)值,感興趣的可以了解一下
    2025-05-05

最新評論

莲花县| 印江| 临澧县| 商丘市| 安国市| 海丰县| 抚宁县| 昂仁县| 神农架林区| 池州市| 枝江市| 同仁县| 明星| 新营市| 伊吾县| 灵丘县| 毕节市| 晋中市| 栖霞市| 许昌县| 马公市| 咸阳市| 阿尔山市| 进贤县| 修武县| 自贡市| 南丹县| 涪陵区| 茌平县| 新余市| 建平县| 五峰| 阿克陶县| 临猗县| 四会市| 安远县| 墨江| 得荣县| 清流县| 筠连县| 小金县|