標(biāo)定與3D坐標(biāo)轉(zhuǎn)換:從像素到真實(shí)世界的OpenCV工程實(shí)現(xiàn))
簡介面向計(jì)算機(jī)視覺開發(fā)者與科研人員這套資源提供從相機(jī)標(biāo)定到2D/3D坐標(biāo)轉(zhuǎn)換的完整實(shí)現(xiàn)方案。系統(tǒng)基于Python開發(fā)支持RGB相機(jī)與紅外相機(jī)標(biāo)定涵蓋圖像去畸變、平面直線算法、PnP算法、8點(diǎn)算法等核心算法適用于機(jī)器人導(dǎo)航、自動(dòng)駕駛、增強(qiáng)現(xiàn)實(shí)、工業(yè)測量等需要精確空間映射的場景。壓縮包共210個(gè)文件總大小71.12MB其中28個(gè)Python源碼和2個(gè)C文件構(gòu)成程序主體87張JPG與71張PNG標(biāo)定圖像可用于棋盤格與特征點(diǎn)提取8個(gè)NPZ文件保存標(biāo)定結(jié)果或中間參數(shù)XML/JSON配置便于調(diào)整相機(jī)參數(shù)DOCX文檔提供使用說明整體結(jié)構(gòu)清晰便于按需學(xué)習(xí)。已有141人學(xué)習(xí)下載資源還附帶Docker封裝可跨平臺(tái)快速部署免去繁瑣環(huán)境配置內(nèi)含Python工程模塊劃分明確方便二次開發(fā)或移植到自有視覺流程中。無論用于課程設(shè)計(jì)、畢業(yè)課題還是企業(yè)級項(xiàng)目驗(yàn)證這套資料都能幫助減少從理論到落地的調(diào)試成本適合中高級開發(fā)者參考。1. 相機(jī)標(biāo)定與3D坐標(biāo)轉(zhuǎn)換從像素到真實(shí)世界的完整工程實(shí)現(xiàn)做視覺項(xiàng)目的人大概都經(jīng)歷過這種尷尬算法在仿真數(shù)據(jù)上跑得漂漂亮亮一旦換成真實(shí)相機(jī)坐標(biāo)全部偏移單目測距徹底不可信。這套基于計(jì)算機(jī)視覺的相機(jī)標(biāo)定與3D坐標(biāo)轉(zhuǎn)換系統(tǒng)解決的就是“從像素到真實(shí)世界”的問題。它把完整的標(biāo)定流程打包成一套可復(fù)用的Python工程覆蓋RGB相機(jī)標(biāo)定、紅外相機(jī)標(biāo)定、圖像去畸變以及平面直線算法、PnP算法、8點(diǎn)算法六條主線適合正在做機(jī)器人導(dǎo)航、工業(yè)檢測、安防監(jiān)控或AR定位的開發(fā)者。與純公式堆砌的標(biāo)定Demo不同這套代碼從標(biāo)定板采集到2D到3D坐標(biāo)換算都有對應(yīng)實(shí)現(xiàn)解壓后改改路徑就能跑通。2. 系統(tǒng)架構(gòu)與技術(shù)選型從標(biāo)定板到3D坐標(biāo)的完整鏈路2.1 目錄結(jié)構(gòu)與模塊劃分源碼包解壓后是一個(gè)典型的模塊化工程目錄結(jié)構(gòu)大致如下calib_system/ ├── config/ │ ├── rgb_camera.yaml # RGB相機(jī)內(nèi)參、畸變系數(shù)、分辨率的配置文件 │ ├── ir_camera.yaml # 紅外相機(jī)標(biāo)定參數(shù) │ └── system.yaml # 相機(jī)外參、平面單應(yīng)性映射參數(shù)、坐標(biāo)系定義 ├── data/ │ ├── rgb/ # RGB標(biāo)定板采樣圖建議30張以上 │ └── ir/ # 紅外標(biāo)定板采樣圖同時(shí)段對齊采集 ├── src/ │ ├── calibration.py # 單目相機(jī)標(biāo)定主流程角點(diǎn)檢測→內(nèi)參求解→重投影驗(yàn)證 │ ├── undistort.py # 圖像去畸變模塊基于remap映射表 │ ├── homography.py # 平面直線算法像素平面→地平面坐標(biāo)的單應(yīng)性變換 │ ├── pnp_solver.py # PnP位姿估計(jì)算法封裝支持RANSAC │ ├── eight_point.py # 8點(diǎn)算法基礎(chǔ)矩陣F與本質(zhì)矩陣E的求解與分解 │ └── utils.py # 公共工具文件讀取、YAML參數(shù)加載、角點(diǎn)可視化 ├── examples/ │ ├── demo_rgb_calib.py # RGB相機(jī)標(biāo)定Demo │ ├── demo_undistort.py # 去畸變效果對比Demo │ ├── demo_pnp_pose.py # PnP位姿解算Demo │ └── demo_3d_convert.py # 2D像素點(diǎn)到3D地面坐標(biāo)轉(zhuǎn)換Demo └── requirements.txt這個(gè)結(jié)構(gòu)把“配置”和“代碼”徹底分開換相機(jī)參數(shù)時(shí)不需要?jiǎng)尤魏卧创a邏輯。config目錄下的YAML文件是整個(gè)系統(tǒng)的“黑匣子”鑰匙標(biāo)定結(jié)果會(huì)寫回這里所有下游模塊啟動(dòng)時(shí)只認(rèn)這些參數(shù)。data目錄按傳感器類型分文件夾組織標(biāo)定圖像紅外和RGB圖像需要在相近時(shí)間采集否則后續(xù)聯(lián)合使用時(shí)對不上。2.2 技術(shù)選型Python OpenCV為什么不再用MATLAB整套系統(tǒng)基于Python 3.8、OpenCV 4.x和NumPy構(gòu)建requirements.txt里的核心依賴就四個(gè)opencv-python、numpy、pyyaml、scipy。選擇OpenCV而不是MATLAB的原因很直接OpenCV的calibrateCamera、findChessboardCorners、solvePnP這些函數(shù)本身就是工業(yè)級實(shí)現(xiàn)經(jīng)過了海量攝像機(jī)標(biāo)定場景的驗(yàn)證直接調(diào)用比自己從頭實(shí)現(xiàn)張正友標(biāo)定法要穩(wěn)定得多。標(biāo)定這種場景精度瓶頸往往不在算法本身而在數(shù)據(jù)采集和參數(shù)配置。OpenCV的優(yōu)勢在于它把畸變模型、優(yōu)化求解、RANSAC這些底層細(xì)節(jié)都封裝好了開發(fā)者只需要管好標(biāo)定板、角點(diǎn)數(shù)量、像素尺寸這些“上層變量”。NumPy在矩陣運(yùn)算上足夠快處理幾千個(gè)標(biāo)定點(diǎn)的線性代數(shù)求解完全夠用。PyYAML負(fù)責(zé)參數(shù)序列化標(biāo)定一次之后所有模塊從YAML讀參數(shù)不需要重新計(jì)算。這套系統(tǒng)里沒有用深度學(xué)習(xí)框架因?yàn)橄鄼C(jī)標(biāo)定本質(zhì)上是一個(gè)幾何優(yōu)化問題傳統(tǒng)方法在無紋理、低分辨率的紅外圖像上反而比CNN更魯棒。紅外相機(jī)的圖像通常是單通道灰度圖熱成像下棋盤格的角點(diǎn)對比度可能很低傳統(tǒng)方法配合亞像素精化仍然能拿到亞像素級的角點(diǎn)坐標(biāo)。2.3 數(shù)據(jù)流設(shè)計(jì)標(biāo)定、去畸變、坐標(biāo)轉(zhuǎn)換三步走整個(gè)系統(tǒng)的數(shù)據(jù)流可以拆成三個(gè)階段這也是多數(shù)視覺測量項(xiàng)目的標(biāo)準(zhǔn)流水線階段一離線標(biāo)定 RGB標(biāo)定板圖像 → 角點(diǎn)檢測 → calibrateCamera → 內(nèi)參K 畸變dist 紅外標(biāo)定板圖像 → 角點(diǎn)檢測 → calibrateCamera → 內(nèi)參K_ir dist_ir 階段二在線預(yù)處理 原始幀 → initUndistortRectifyMap → remap → 去畸變圖 階段三坐標(biāo)換算 去畸變圖 → 特征點(diǎn)像素坐標(biāo) → PnP/單應(yīng)性 → 相機(jī)坐標(biāo)系/地平面坐標(biāo)系離線標(biāo)定只跑一次產(chǎn)出的內(nèi)參和畸變系數(shù)固化成YAML文件。在線階段每幀圖像都走一遍去畸變和坐標(biāo)轉(zhuǎn)換這兩步的耗時(shí)決定系統(tǒng)能否做到實(shí)時(shí)。OpenCV的initUndistortRectifyMap會(huì)預(yù)先計(jì)算好像素映射表之后每幀只執(zhí)行一次remap雙線性插值在1080p圖像上大約耗時(shí)2-5毫秒完全不影響實(shí)時(shí)性。設(shè)計(jì)上的一個(gè)關(guān)鍵點(diǎn)是所有坐標(biāo)轉(zhuǎn)換模塊都默認(rèn)輸入已經(jīng)去畸變的像素坐標(biāo)。如果跳過去畸變直接做PnP在圖像邊緣區(qū)域的誤差可以達(dá)到幾十個(gè)像素?fù)Q算成真實(shí)距離偏差非??鋸?。這個(gè)約束在代碼里通過斷言和注釋反復(fù)強(qiáng)調(diào)我在二次開發(fā)時(shí)也踩過這個(gè)坑后面避坑章節(jié)會(huì)細(xì)說。3. 核心模塊拆解RGB相機(jī)標(biāo)定與圖像去畸變的實(shí)現(xiàn)細(xì)節(jié)3.1 標(biāo)定板采集與角點(diǎn)檢測標(biāo)定流程的第一步是準(zhǔn)備標(biāo)定板圖片。這套系統(tǒng)默認(rèn)支持棋盤格標(biāo)定板內(nèi)角點(diǎn)數(shù)量通過config/rgb_camera.yaml指定常用的參數(shù)是(9, 6)或(11, 8)。采集時(shí)讓標(biāo)定板出現(xiàn)在畫面的不同位置中心、四個(gè)角落、傾斜角度、遠(yuǎn)近各一組每張圖之間的位姿差異越大標(biāo)定出的內(nèi)參越穩(wěn)定。角點(diǎn)檢測是整套標(biāo)定流程的地基如果這一步拿到的角點(diǎn)坐標(biāo)有誤后面求解出的內(nèi)參全是錯(cuò)的。核心代碼如下import cv2 import numpy as np import yaml # 從配置文件讀取棋盤格參數(shù) with open(config/rgb_camera.yaml, r) as f: cfg yaml.safe_load(f) pattern_size tuple(cfg[calib][pattern_size]) # (9, 6) square_size cfg[calib][square_size] # 每個(gè)棋盤格的邊長單位米 # 構(gòu)造世界坐標(biāo)系下的棋盤格角點(diǎn)三維坐標(biāo) # 假設(shè)棋盤格放置在z0平面上所以第三維全是0 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size obj_points [] # 每張圖對應(yīng)的世界坐標(biāo)點(diǎn)集 img_points [] # 每張圖對應(yīng)的像素坐標(biāo)點(diǎn)集 # 遍歷采集到的標(biāo)定板圖片 for fname in image_files: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if not ret: print(f角點(diǎn)檢測失敗跳過{fname}) continue # 亞像素級角點(diǎn)精化提高角點(diǎn)坐標(biāo)精度 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_sub cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_sub)這段代碼的邏輯分三步先把棋盤格的世界坐標(biāo)構(gòu)造出來這里默認(rèn)棋盤格落在z0平面上然后逐張檢測角點(diǎn)檢測失敗的直接跳過不混入標(biāo)定數(shù)據(jù)最后用cornerSubPix做亞像素精化把角點(diǎn)定位精度從像素級提升到亞像素級。注意cornerSubPix的窗口大小(11, 11)是經(jīng)驗(yàn)值窗口太小噪聲抑制不足窗口太大會(huì)讓角點(diǎn)響應(yīng)變鈍死區(qū)設(shè)為(-1, -1)意味著沒有死角區(qū)全部像素參與計(jì)算。還有一個(gè)容易忽略的點(diǎn)采集的圖片數(shù)量少于10張時(shí)標(biāo)定結(jié)果的重投影誤差會(huì)很大。這套系統(tǒng)的示例腳本里給了一個(gè)校驗(yàn)邏輯obj_points長度少于15張就直接報(bào)錯(cuò)退出這個(gè)閾值在實(shí)際項(xiàng)目中是合理的下限。3.2 內(nèi)參矩陣與畸變系數(shù)求解拿到足夠多的角點(diǎn)對之后調(diào)用OpenCV的calibrateCamera一次求解內(nèi)參矩陣K、畸變系數(shù)dist、每張圖的旋轉(zhuǎn)向量rvec和平移向量tvec# 求解相機(jī)內(nèi)參和畸變系數(shù) # gray.shape[::-1] 傳的是 (寬, 高)順序一定不要寫反 ret, K, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) # 重投影誤差越小說明標(biāo)定越可靠通常低于0.3像素 mean_error 0.0 for i in range(len(obj_points)): imgpoints2, _ cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], K, dist ) error cv2.norm(img_points[i], imgpoints2, cv2.NORM_L2) mean_error error print(f平均重投影誤差{mean_error / len(obj_points):.4f} 像素)calibrateCamera內(nèi)部用的是張正友標(biāo)定法的優(yōu)化實(shí)現(xiàn)先通過單應(yīng)性矩陣估計(jì)初始內(nèi)參再用Levenberg-Marquardt算法做非線性優(yōu)化。K矩陣的形式是K [[fx, 0, cx], [ 0, fy, cy], [ 0, 0, 1]]fx和fy分別是x、y方向上的焦距單位是像素cx和cy是主點(diǎn)坐標(biāo)。如果一個(gè)鏡頭的fx和fy相差超過5%通常意味著傳感器像素不是正方形這在一些低成本工業(yè)相機(jī)里確實(shí)會(huì)出現(xiàn)?;兿禂?shù)dist是(k1, k2, p1, p2, k3)五參數(shù)模型前兩個(gè)是徑向畸變p1、p2是切向畸變k3是高階徑向畸變項(xiàng)。每次標(biāo)定完我都會(huì)打印重投影誤差這個(gè)數(shù)字是判斷標(biāo)定品質(zhì)的唯一硬指標(biāo)。誤差在0.1-0.3像素之間說明標(biāo)定數(shù)據(jù)質(zhì)量很好超過0.5像素就要檢查是否有標(biāo)定板彎曲、圖片模糊或者角點(diǎn)誤檢。這里沒有玄學(xué)空間誤差大就是數(shù)據(jù)有問題重新采集比調(diào)參更有效。3.3 去畸變圖像的重映射實(shí)現(xiàn)去畸變的原理是畸變圖上的像素點(diǎn)(u, v)通過反查畸變模型找到它在理想無畸變圖上的位置(u, v)然后做插值采樣。OpenCV把這一步拆成initUndistortRectifyMap和remap兩個(gè)函數(shù)前者只計(jì)算一次映射表后者每幀執(zhí)行查表插值def load_undistort_maps(K, dist, image_size): 預(yù)計(jì)算去畸變映射表供每幀圖像使用 # initUndistortRectifyMap要求傳內(nèi)參矩陣和畸變系數(shù) # 第三個(gè)參數(shù)設(shè)None表示單位旋轉(zhuǎn)矩陣不做雙目校正 mapx, mapy cv2.initUndistortRectifyMap( K, dist, None, K, image_size, cv2.CV_32FC1 ) return mapx, mapy def undistort_frame(frame, mapx, mapy): 對單幀圖像執(zhí)行去畸變基于預(yù)計(jì)算的映射表 return cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR)initUndistortRectifyMap的第六個(gè)參數(shù)是映射表精度cv2.CV_32FC1即單通道浮點(diǎn)兼顧精度和內(nèi)存如果你用的是640x480的實(shí)時(shí)視頻流這個(gè)精度足夠。remap的插值方式建議用cv2.INTER_LINEAR雙線性插值速度和質(zhì)量比較均衡如果對邊緣質(zhì)量要求高可以換INTER_CUBIC但每幀耗時(shí)大約會(huì)增加一倍。去畸變有個(gè)容易忽略的副作用畸變圖邊緣的像素被拉伸后圖像四個(gè)角會(huì)出現(xiàn)黑邊而且視場角會(huì)輕微變小。在3D坐標(biāo)轉(zhuǎn)換場景里黑邊本身無所謂但如果你把去畸變后的圖送去給PnP算法當(dāng)輸入一定要同步更新特征點(diǎn)的像素坐標(biāo)因?yàn)閞emap改變了所有點(diǎn)的位置。最常見的翻車操作是只用去畸變圖做可視化但特征點(diǎn)還從原始畸變圖里提取導(dǎo)致坐標(biāo)全部偏移。這套工程里把去畸變和特征提取寫在同一個(gè)流程里就是在代碼層面強(qiáng)制避免這種錯(cuò)位。4. 核心模塊拆解平面直線算法、PnP與8點(diǎn)算法的坐標(biāo)轉(zhuǎn)換實(shí)現(xiàn)4.1 平面直線算法像素平面到地面坐標(biāo)系的單應(yīng)性變換“平面直線算法”這個(gè)名字聽起來抽象落到工程里就是一個(gè)單應(yīng)性矩陣H的求解與應(yīng)用。假設(shè)目標(biāo)物體在一個(gè)平面上運(yùn)動(dòng)比如地面上的AGV小車、傳送帶上的工件那么像素坐標(biāo)和平面世界坐標(biāo)之間存在一個(gè)3×3的齊次變換矩陣把4組已知對應(yīng)點(diǎn)代進(jìn)去就能解出H。這是2D到3D坐標(biāo)轉(zhuǎn)換中最“輕量”的方案因?yàn)椴恍枰疃刃畔?。import cv2 import numpy as np class HomographyMapper: 基于平面單應(yīng)性的像素坐標(biāo)到地面坐標(biāo)轉(zhuǎn)換器 def __init__(self, src_points, dst_points): src_points: 圖像上的4個(gè)像素坐標(biāo)點(diǎn)N×2 dst_points: 地面坐標(biāo)系對應(yīng)的4個(gè)點(diǎn)N×2單位通常為毫米 # RANSAC魯棒估計(jì)單應(yīng)性矩陣 self.H, _ cv2.findHomography(src_points, dst_points, cv2.RANSAC) # 同時(shí)計(jì)算反向映射用于驗(yàn)證 self.H_inv, _ cv2.findHomography(dst_points, src_points, cv2.RANSAC) def pixel_to_ground(self, u, v): 將像素坐標(biāo)轉(zhuǎn)換為地面平面坐標(biāo) pt np.array([u, v, 1.0], dtypenp.float64) ground self.H pt # 齊次坐標(biāo)歸一化除以第三個(gè)分量 ground ground / ground[2] return ground[0], ground[1] def ground_to_pixel(self, x, y): 反向映射地面坐標(biāo)轉(zhuǎn)回像素坐標(biāo)用于驗(yàn)證和可視化 pt np.array([x, y, 1.0], dtypenp.float64) pixel self.H_inv pt pixel pixel / pixel[2] return pixel[0], pixel[1]求H的過程中必須做齊次歸一化這一步漏了結(jié)果就是亂碼。所謂“平面直線算法”本質(zhì)上是把三維空間問題降維成平面問題用一個(gè)3×3的矩陣表達(dá)旋轉(zhuǎn)、平移和縮放。選4個(gè)點(diǎn)時(shí)要注意四個(gè)點(diǎn)不能有三點(diǎn)共線或近共線的情況否則H矩陣的求解會(huì)退化這是典型的秩虧問題。標(biāo)定平面點(diǎn)的時(shí)候盡量選場地四角的大范圍分布點(diǎn)而不是在畫面中央擠一小塊區(qū)域。這類映射的精度上限受兩個(gè)因素限制一是標(biāo)定參照點(diǎn)的測量誤差二是鏡頭畸變在去畸變后殘留的誤差。第一個(gè)靠激光測距儀或標(biāo)定布上的精確網(wǎng)格來壓第二個(gè)靠前面章節(jié)的標(biāo)定質(zhì)量來壓。用這個(gè)模塊做地面導(dǎo)航定位精度做到±2厘米是可以實(shí)現(xiàn)的前提是參照點(diǎn)測準(zhǔn)、去畸變做到位。4.2 PnP算法單目相機(jī)的位姿估計(jì)與測距PnPPerspective-n-Point解決的是另一類問題已知物體上N個(gè)3D點(diǎn)在世界坐標(biāo)系中的位置以及它們在圖像上的2D投影反推相機(jī)在世界坐標(biāo)系中的位置和姿態(tài)也就是位姿。應(yīng)用到單目測距上一條常用路徑是在目標(biāo)物體上貼幾個(gè)已知尺寸和相對位置的特征標(biāo)記檢測到標(biāo)記的像素坐標(biāo)后solvePnP直接輸出相機(jī)到標(biāo)記的距離。def solve_camera_pose(object_points, image_points, K, dist): object_points: N×3世界坐標(biāo)系下的3D點(diǎn)坐標(biāo)單位米 image_points: N×2對應(yīng)的像素坐標(biāo)注意必須是去畸變后的坐標(biāo) K: 相機(jī)內(nèi)參矩陣 dist: 畸變系數(shù)通常傳零矩陣因?yàn)閕mage_points已去畸變 # 使用RANSAC魯棒估計(jì)位姿剔除誤匹配點(diǎn) success, rvec, tvec, inliers cv2.solvePnPRansac( object_points, image_points, K, dist, iterationsCount200, # RANSAC迭代次數(shù)越大越魯棒但越慢 reprojectionError2.0, # 重投影誤差閾值單位像素 confidence0.99, # 置信度 flagscv2.SOLVEPNP_ITERATIVE ) if not success: return None, None # 旋轉(zhuǎn)向量轉(zhuǎn)旋轉(zhuǎn)矩陣 R, _ cv2.Rodrigues(rvec) # tvec表示相機(jī)光心在世界坐標(biāo)系中的位置向量從世界系看相機(jī) # 嚴(yán)格來說相機(jī)在世界系的坐標(biāo)是 -R.T tvec camera_pos -R.T tvec # 目標(biāo)點(diǎn)到相機(jī)的距離 camera_pos的歐幾里得距離 distance np.linalg.norm(camera_pos) return camera_pos, distancesolvePnPRansac比普通solvePnP多了一個(gè)RANSAC步驟在特征點(diǎn)有誤匹配或遮擋的情況下普通solvePnP會(huì)被外點(diǎn)拉偏RANSAC會(huì)在迭代中隨機(jī)抽取子集估計(jì)位姿然后用全部點(diǎn)驗(yàn)證把不符合的點(diǎn)踢出去。iterationsCount設(shè)200、reprojectionError設(shè)2.0是我的習(xí)慣起始值特征點(diǎn)質(zhì)量高時(shí)可以收斂到100次以內(nèi)。PnP有個(gè)常見的翻車場景當(dāng)特征點(diǎn)近乎共面時(shí)解算出的位姿會(huì)出現(xiàn)兩個(gè)可能的解——一個(gè)在平面前方一個(gè)在平面后方張正友標(biāo)定法也受這個(gè)問題影響。解決方法是增加非共面的特征點(diǎn)或者在solvePnPRansac之后做一次左右手坐標(biāo)系驗(yàn)證確保旋轉(zhuǎn)矩陣的行列式接近1。很多人剛用PnP時(shí)會(huì)把rvec直接當(dāng)?shù)匦D(zhuǎn)向量用忘了它代表的是“世界坐標(biāo)系到相機(jī)坐標(biāo)系的旋轉(zhuǎn)”方向弄反之后測距結(jié)果差得很離譜。4.3 8點(diǎn)算法基礎(chǔ)矩陣與本質(zhì)矩陣的對極幾何估計(jì)8點(diǎn)算法用于多視角幾何中的對極幾何估計(jì)是雙目視覺和結(jié)構(gòu)恢復(fù)的基礎(chǔ)。給定兩幀圖像之間至少8組匹配點(diǎn)對8點(diǎn)算法可以估計(jì)出基礎(chǔ)矩陣F它描述了左右圖像對應(yīng)點(diǎn)之間的極線約束。如果系統(tǒng)己知相機(jī)的內(nèi)參K可以把F升級成本質(zhì)矩陣E進(jìn)一步分解出兩幀之間的相對旋轉(zhuǎn)R和平移t。def estimate_essential_matrix(pts1, pts2, K): pts1: 第一幀的特征點(diǎn)像素坐標(biāo)N×2 pts2: 第二幀的對應(yīng)特征點(diǎn)像素坐標(biāo)N×2 K: 相機(jī)內(nèi)參矩陣 # 第一步像素坐標(biāo)歸一化到相機(jī)坐標(biāo)系 # 這一步本質(zhì)上是去除內(nèi)參的影響讓算法在“歸一化相機(jī)系”下工作 pts1_norm cv2.undistortPoints( pts1, K, np.zeros(5), RNone, PNone ) pts2_norm cv2.undistortPoints( pts2, K, np.zeros(5), RNone, PNone ) # 用RANSAC估計(jì)基礎(chǔ)矩陣F歸一化坐標(biāo)下等價(jià)于本質(zhì)矩陣E的初始估計(jì) F, mask cv2.findFundamentalMat( pts1_norm, pts2_norm, methodcv2.FM_RANSAC, ransacReprojThreshold0.01, confidence0.999 ) # 通過已知內(nèi)參升級為本質(zhì)矩陣E E K.T F K # 對E做奇異值分解強(qiáng)制秩為2且兩個(gè)非零奇異值相等 U, S, Vt np.linalg.svd(E) S[0] (S[0] S[1]) / 2 S[1] S[0] S[2] 0 E_refined U np.diag(S) Vt # 分解出旋轉(zhuǎn)矩陣R和平移向量t _, R, t, _ cv2.recoverPose(E_refined, pts1_norm, pts2_norm) return R, t, mask8點(diǎn)算法的核心坑有兩個(gè)。第一個(gè)坑直接對原始像素坐標(biāo)構(gòu)造線性方程會(huì)導(dǎo)致矩陣條件數(shù)極差數(shù)值不穩(wěn)定所以必須先把坐標(biāo)歸一化OpenCV的undistortPoints其實(shí)同時(shí)做了“去畸變”和“歸一化到相機(jī)系”兩件事傳零畸變系數(shù)就只做歸一化。第二個(gè)坑求出的本質(zhì)矩陣在數(shù)學(xué)上不一定滿足“只含旋轉(zhuǎn)且秩為2”的約束所以要做SVD強(qiáng)制修正代碼里第18-21行干的就是這件事。如果跳過這一步recoverPose的分解結(jié)果往往是錯(cuò)的。這部分的實(shí)際應(yīng)用場景是拿一臺(tái)手持單目相機(jī)拍兩張不同角度的照片通過8點(diǎn)算法恢復(fù)兩幀之間的相對位姿再結(jié)合三角測量算出目標(biāo)3D坐標(biāo)。做相對位姿估計(jì)時(shí)要注意scale ambiguity——單目相機(jī)無法從兩張圖恢復(fù)絕對尺度平移向量t只有方向沒有大小。如果后續(xù)要拿到米制距離必須引入一個(gè)已知長度的參照物來恢復(fù)尺度這是我覺得整套系統(tǒng)里最容易被忽略的設(shè)計(jì)點(diǎn)。5. 避坑指南標(biāo)定與坐標(biāo)轉(zhuǎn)換中常見的5類問題排查5.1 角點(diǎn)檢測失敗光線和標(biāo)定板反差是頭號殺手現(xiàn)象findChessboardCorners對一部分圖像返回retFalse程序自動(dòng)跳過但跳過的圖太多導(dǎo)致標(biāo)定數(shù)據(jù)不足最后內(nèi)參誤差很大。原因標(biāo)定板表面反光、光照不均勻、棋盤格印刷不平整都會(huì)讓灰度梯度信噪比下降。尤其是紅外相機(jī)的標(biāo)定熱成像圖中棋盤格的黑白格溫差不明顯角點(diǎn)響應(yīng)非常弱。解決把標(biāo)定板貼在剛性平板上避免紙張彎曲調(diào)整光源方向避免正對鏡頭產(chǎn)生鏡面反射紅外場景下建議使用加熱后的標(biāo)定板或者直接用“冷熱交替”的棋盤格靶標(biāo)。采集時(shí)把相機(jī)曝光固定住不要開啟自動(dòng)曝光否則每幀圖像的亮度基準(zhǔn)不一致角點(diǎn)檢測的穩(wěn)定性會(huì)很差。這套系統(tǒng)的示例腳本里可以加一步“檢測率統(tǒng)計(jì)”如果超過30%的圖失敗就打印提醒重新采集。5.2 去畸變后圖像邊緣出現(xiàn)異常拉伸現(xiàn)象remap輸出的圖邊緣出現(xiàn)波浪狀扭曲原本筆直的墻面線條變成弧形靠近畫面四角的區(qū)域拉伸尤其嚴(yán)重。原因畸變系數(shù)估計(jì)不準(zhǔn)或者去畸變時(shí)用了錯(cuò)誤的內(nèi)參矩陣。還有一種常見誤用把initUndistortRectifyMap的第三、四參數(shù)填了非None的變換矩陣導(dǎo)致輸出圖被整體重采樣成另一個(gè)視角。解決回到標(biāo)定環(huán)節(jié)檢查重投影誤差不要急著調(diào)denoising或插值參數(shù)。重投影誤差在0.5像素以上時(shí)先去補(bǔ)拍標(biāo)定板圖像而不是去后處理。曾見過有同學(xué)在undistort之后又手動(dòng)裁切邊緣黑邊裁切時(shí)沒有同步更新后續(xù)PnP模塊的像素坐標(biāo)結(jié)果坐標(biāo)整體偏移折騰了一下午才定位到原因。5.3 紅外相機(jī)與RGB相機(jī)標(biāo)定結(jié)果差異過大現(xiàn)象RGB相機(jī)標(biāo)定出fx≈850紅外相機(jī)標(biāo)定出fx≈1200兩個(gè)相機(jī)的視場角按理應(yīng)當(dāng)接近結(jié)果卻差異懸殊。原因紅外相機(jī)的傳感器分辨率往往和RGB相機(jī)不同且紅外鏡頭通常有更嚴(yán)重的熱漂移——快門加熱導(dǎo)致鏡筒膨脹焦距發(fā)生微小變化。另外紅外圖像對比度低角點(diǎn)亞像素精化的效果不如RGB標(biāo)定噪聲更大。解決建議對紅外相機(jī)做兩次標(biāo)定一次在冷啟動(dòng)狀態(tài)一次在工作半小時(shí)后的熱穩(wěn)定狀態(tài)比較兩次內(nèi)參差異。如果fx差距超過2%要么給鏡頭加恒溫套要么在算法里做一個(gè)溫度補(bǔ)償?shù)牟逯的P透鶕?jù)相機(jī)工作時(shí)間切換內(nèi)參。這套系統(tǒng)在config目錄下留了ir_camera.yaml的冗余字段就是為這種場景準(zhǔn)備的。5.4 PnP位姿解算結(jié)果抖動(dòng)或“翻轉(zhuǎn)”現(xiàn)象相機(jī)靜止不動(dòng)但PnP輸出的distance在小數(shù)點(diǎn)后幾位跳個(gè)不停更嚴(yán)重的是姿態(tài)忽然從正面翻轉(zhuǎn)到背面旋轉(zhuǎn)矩陣行列式變負(fù)。原因特征點(diǎn)數(shù)量太少或者共面程度太高位姿求解存在多解。另一個(gè)常見原因是image_points未做去畸變邊緣殘差在RANSAC里被當(dāng)成外點(diǎn)導(dǎo)致解算結(jié)果不穩(wěn)定。解決特征點(diǎn)數(shù)量至少6-8個(gè)且盡量分布在目標(biāo)物體的不同深度平面上。對翻轉(zhuǎn)問題可以在recoverPose或solvePnP之后校驗(yàn)旋轉(zhuǎn)矩陣的行列式正常情況下應(yīng)該接近1如果接近-1就說明坐標(biāo)軸方向定義反了。我遇到的更多情況是誤把tvec當(dāng)成了目標(biāo)距離實(shí)際上tvec是相機(jī)坐標(biāo)系下目標(biāo)原點(diǎn)的距離方向取決于你定義的世界坐標(biāo)系朝向需要先確認(rèn)坐標(biāo)系定義再做距離換算。5.5 8點(diǎn)算法基礎(chǔ)矩陣估計(jì)失敗或退化現(xiàn)象findFundamentalMat返回的inlier數(shù)量很少或者R和t的分解結(jié)果在相鄰幀之間跳躍劇烈。原因特征點(diǎn)集中在圖像的一塊小區(qū)域內(nèi)對極幾何約束退化或者兩張圖的視差太小基線過短導(dǎo)致三角測量不穩(wěn)定。另一個(gè)高頻原因是用ORB這類特征在重復(fù)紋理場景下產(chǎn)生了大量誤匹配RANSAC的種子就被污染了。解決加長兩幀之間的基線距離讓視差變大特征匹配階段用ratio test最近鄰距離比次近鄰小于0.75篩掉一批誤匹配。如果還是沒有足夠inlier降低ransacReprojThreshold從0.01調(diào)到0.005這個(gè)參數(shù)決定內(nèi)點(diǎn)判定閾值太嚴(yán)格會(huì)導(dǎo)致inlier過少太寬松會(huì)把錯(cuò)誤匹配放進(jìn)來。這部分的調(diào)參有點(diǎn)“玄學(xué)”但本質(zhì)是平衡精度與召回多試幾組參數(shù)看inlier分布就清楚了。6. 二次開發(fā)把標(biāo)定系統(tǒng)接進(jìn)自己的視覺項(xiàng)目6.1 多相機(jī)聯(lián)合標(biāo)定擴(kuò)展這套系統(tǒng)的單目標(biāo)定和坐標(biāo)轉(zhuǎn)換邏輯是模塊化的擴(kuò)展成多相機(jī)聯(lián)合標(biāo)定并不復(fù)雜。我的建議是在config/system.yaml里增加一個(gè)cameras列表每臺(tái)相機(jī)維護(hù)自己的一組K和dist再額外維護(hù)一個(gè)外參矩陣T_cn_c0表示第n臺(tái)相機(jī)到主相機(jī)坐標(biāo)系的變換。標(biāo)定時(shí)讓所有相機(jī)同時(shí)采集同一塊標(biāo)定板以主相機(jī)的棋盤格坐標(biāo)系為公共參考系聯(lián)立求解每臺(tái)相機(jī)的外參。代碼層面只需在calibration.py之后追加一個(gè)pairwise的solvePnP把從標(biāo)定板坐標(biāo)系到各相機(jī)坐標(biāo)系的變換求出來再通過矩陣鏈換算成相機(jī)間的外參。6.2 把標(biāo)定參數(shù)導(dǎo)出為JSON配置工程里用YAML存參數(shù)是好事但很多部署環(huán)境只有JSON配置中心。我在二次開發(fā)時(shí)加了一個(gè)export_json.py腳本把K、dist、H矩陣、PnP結(jié)果統(tǒng)一序列化成JSON文件輸出格式大致是{ camera: { fx: 845.23, fy: 841.08, cx: 642.51, cy: 384.67, dist: [0.113, -0.221, 0.001, 0.0009, 0.032] }, homography: { H: [[...]], H_inv: [[...]] } }序列化時(shí)有一個(gè)細(xì)節(jié)dist數(shù)組的順序必須和OpenCV約定一致畸變系數(shù)在OpenCV里的排列是k1, k2, p1, p2, k3如果你輸出成其他順序下游讀取時(shí)就會(huì)張冠李戴。拿JSON做配置還有一個(gè)好處是可以直接拿去Web端做可視化給現(xiàn)場調(diào)試用的管理后臺(tái)讀這份JSON就能畫出標(biāo)定板的角點(diǎn)投影圖。6.3 坐標(biāo)轉(zhuǎn)換精度驗(yàn)證的通用方法每次改完標(biāo)定參數(shù)我都會(huì)做一遍“閉環(huán)驗(yàn)證”用第21張之前沒參與標(biāo)定的圖片手動(dòng)點(diǎn)擊幾個(gè)已知地面坐標(biāo)的參照點(diǎn)走pixel_to_ground轉(zhuǎn)換對比坐標(biāo)差距。這個(gè)方法不用額外硬件也不依賴激光測距但能快速暴露標(biāo)定參數(shù)是否過擬合到標(biāo)定數(shù)據(jù)集上。驗(yàn)證結(jié)果打印成表格誤差控制在±2厘米以內(nèi)就算合格。如果誤差超了優(yōu)先懷疑參照點(diǎn)在地面坐標(biāo)系中的實(shí)測坐標(biāo)是不是測錯(cuò)了——這種情況下重新測量參照點(diǎn)遠(yuǎn)比重新標(biāo)定更快。這類工程問題的排查規(guī)律九成是數(shù)據(jù)問題只有一成是代碼問題。從那以后我每次換相機(jī)或換鏡頭都會(huì)強(qiáng)制把整套標(biāo)定流程重跑一遍哪怕只是擰了一下光圈環(huán)也不放過。拿一張未參與標(biāo)定的圖像做隨機(jī)抽查已經(jīng)成了肌肉記憶這個(gè)習(xí)慣幫我躲過了不止一次交付前翻車。希望幫你少踩幾個(gè)坑。本文還有配套的精品資源點(diǎn)擊獲取