標(biāo)定實(shí)戰(zhàn):從原理到深度圖生成的完整指南)
簡(jiǎn)介相機(jī)標(biāo)定是計(jì)算機(jī)視覺(jué)中建立二維圖像像素與三維世界坐標(biāo)對(duì)應(yīng)關(guān)系的核心步驟其本質(zhì)是求解相機(jī)的內(nèi)參、外參及畸變系數(shù)。通過(guò)精確的數(shù)學(xué)模型如針孔模型和優(yōu)化算法標(biāo)定能夠矯正鏡頭畸變?yōu)楹罄m(xù)的視覺(jué)任務(wù)提供準(zhǔn)確的幾何基礎(chǔ)。在三維重建、機(jī)器人導(dǎo)航和自動(dòng)駕駛等領(lǐng)域精確的標(biāo)定是實(shí)現(xiàn)深度感知和空間理解的技術(shù)前提。雙目視覺(jué)系統(tǒng)通過(guò)模擬人眼利用兩個(gè)相機(jī)之間的視差計(jì)算深度而雙目相機(jī)標(biāo)定正是精確測(cè)定兩個(gè)相機(jī)之間相對(duì)位置關(guān)系外參的關(guān)鍵。本文以O(shè)penCV-Python為工具詳細(xì)解析了使用棋盤(pán)格進(jìn)行雙目標(biāo)定的完整流程涵蓋了從相機(jī)模型、極線(xiàn)約束原理到立體校正和SGBM立體匹配生成深度圖的工程實(shí)踐并針對(duì)角點(diǎn)檢測(cè)、重投影誤差優(yōu)化等常見(jiàn)問(wèn)題提供了系統(tǒng)的解決方案。1. 項(xiàng)目概述為什么雙目相機(jī)標(biāo)定是三維視覺(jué)的基石在三維重建、機(jī)器人導(dǎo)航、自動(dòng)駕駛這些領(lǐng)域我們常常需要讓機(jī)器“看見(jiàn)”并理解三維世界。單目相機(jī)雖然便宜好用但它有一個(gè)致命的缺陷它拍下的是一張二維照片丟失了至關(guān)重要的深度信息。就像我們閉上一只眼睛很難準(zhǔn)確判斷一個(gè)物體離我們有多遠(yuǎn)。而雙目視覺(jué)模仿了人眼的原理通過(guò)兩個(gè)并排的相機(jī)從不同角度觀察同一場(chǎng)景利用視差來(lái)計(jì)算深度從而恢復(fù)三維信息。但這里有個(gè)前提你得知道這兩個(gè)“眼睛”的精確幾何關(guān)系。它們的光心距離基線(xiàn)是多少兩個(gè)成像平面是否完全平行各自的鏡頭有沒(méi)有畸變?nèi)绻@些參數(shù)都是未知的那么計(jì)算出的深度就會(huì)謬以千里。雙目相機(jī)標(biāo)定要解決的就是這個(gè)核心問(wèn)題——精確地測(cè)定每只“眼睛”的內(nèi)部參數(shù)內(nèi)參和它們之間的相對(duì)位置關(guān)系外參同時(shí)矯正鏡頭畸變。OpenCV-Python是實(shí)現(xiàn)這一過(guò)程的絕佳工具鏈。OpenCV提供了強(qiáng)大且久經(jīng)考驗(yàn)的計(jì)算機(jī)視覺(jué)算法庫(kù)而Python則以其簡(jiǎn)潔的語(yǔ)法和豐富的科學(xué)計(jì)算生態(tài)如NumPy讓我們能快速搭建原型、處理數(shù)據(jù)。整個(gè)標(biāo)定流程從采集棋盤(pán)格圖像、角點(diǎn)檢測(cè)、參數(shù)計(jì)算到重投影誤差評(píng)估都可以用這一組合高效完成。對(duì)于剛?cè)腴T(mén)的開(kāi)發(fā)者、機(jī)器人愛(ài)好者或是需要快速驗(yàn)證算法的研究人員來(lái)說(shuō)掌握這套方法就等于拿到了開(kāi)啟三維視覺(jué)世界大門(mén)的鑰匙。2. 核心原理拆解從二維圖像到三維關(guān)系的數(shù)學(xué)橋梁要理解標(biāo)定我們不能只停留在調(diào)用API的層面必須搞清楚背后的數(shù)學(xué)原理。這能幫助我們?cè)诮Y(jié)果不理想時(shí)知道該從何處排查。2.1 相機(jī)模型小孔成像與它的不完美我們通常用針孔相機(jī)模型來(lái)近似描述相機(jī)成像。在這個(gè)理想模型中三維空間點(diǎn)[X, Y, Z]通過(guò)透視投影變換到二維圖像平面[u, v]上。這個(gè)變換關(guān)系可以用一個(gè)3x4的投影矩陣P來(lái)表示s * [u, v, 1]^T P * [X, Y, Z, 1]^T。其中s是一個(gè)縮放因子。投影矩陣P可以進(jìn)一步分解為兩個(gè)更直觀的部分P K * [R | t]。內(nèi)參矩陣K描述了相機(jī)自身的屬性與它的位置和朝向無(wú)關(guān)。K [ fx 0 cx ] [ 0 fy cy ] [ 0 0 1 ]fx,fy焦距以像素為單位。fx F / dx其中F是物理焦距dx是傳感器上一個(gè)像素的物理寬度。fx和fy通常接近相等如果相機(jī)傳感器像素是正方形的。cx,cy主點(diǎn)坐標(biāo)通常是圖像的中心即光軸與成像平面的交點(diǎn)。外參[R | t]描述了世界坐標(biāo)系到相機(jī)坐標(biāo)系的剛體變換旋轉(zhuǎn)和平移。然而真實(shí)的鏡頭并非理想的小孔光線(xiàn)穿過(guò)透鏡會(huì)產(chǎn)生畸變主要是徑向畸變和切向畸變。徑向畸變由透鏡形狀導(dǎo)致成像點(diǎn)會(huì)沿著徑向發(fā)生偏移。表現(xiàn)為“桶形畸變”圖像邊緣向內(nèi)彎曲和“枕形畸變”圖像邊緣向外膨脹。通常用參數(shù)k1, k2, k3, ...來(lái)校正。切向畸變由透鏡安裝與成像平面不平行導(dǎo)致。用參數(shù)p1, p2來(lái)校正。所以一個(gè)完整的相機(jī)模型是從三維世界點(diǎn)經(jīng)過(guò)旋轉(zhuǎn)平移外參再經(jīng)過(guò)透視投影內(nèi)參最后經(jīng)過(guò)畸變校正才得到最終的圖像像素坐標(biāo)。標(biāo)定的目標(biāo)就是求出K、畸變系數(shù)(k1, k2, p1, p2, k3...)以及雙目之間的R和t。2.2 雙目幾何極線(xiàn)約束與立體校正標(biāo)定出單個(gè)相機(jī)的參數(shù)后對(duì)于雙目系統(tǒng)我們更關(guān)心兩個(gè)相機(jī)之間的相對(duì)關(guān)系。這個(gè)關(guān)系同樣用一個(gè)旋轉(zhuǎn)矩陣R和一個(gè)平移向量t來(lái)描述。這里的t特別重要它的模長(zhǎng)就是兩個(gè)相機(jī)光心之間的距離稱(chēng)為基線(xiàn)Baseline?;€(xiàn)越長(zhǎng)理論上深度測(cè)量的范圍越遠(yuǎn)、精度越高但視野重疊區(qū)域會(huì)變小。得到雙目標(biāo)定參數(shù)后一個(gè)至關(guān)重要的后續(xù)步驟是立體校正。理想的雙目系統(tǒng)是兩個(gè)相機(jī)完全平行放置這樣匹配點(diǎn)搜索只需要在水平線(xiàn)上進(jìn)行極大簡(jiǎn)化了計(jì)算。但實(shí)際安裝總有偏差。立體校正的目的就是通過(guò)圖像變換將兩個(gè)非共面行對(duì)準(zhǔn)的成像平面重投影到同一個(gè)平面上并且讓兩個(gè)相機(jī)的光軸平行。這個(gè)過(guò)程依賴(lài)于我們標(biāo)定得到的R和t。校正后一個(gè)三維空間點(diǎn)在兩個(gè)校正后的圖像上的投影點(diǎn)將具有相同的縱坐標(biāo)行對(duì)齊。這個(gè)特性就是極線(xiàn)約束在左圖上的一個(gè)點(diǎn)其在右圖中的對(duì)應(yīng)點(diǎn)必然位于同一水平掃描線(xiàn)上。這將對(duì)后續(xù)的立體匹配尋找對(duì)應(yīng)點(diǎn)帶來(lái)革命性的簡(jiǎn)化從二維搜索降為一維搜索。2.3 標(biāo)定板為什么棋盤(pán)格是“標(biāo)尺”我們需要一個(gè)已知精確尺寸的物體作為“標(biāo)尺”來(lái)建立圖像點(diǎn)像素坐標(biāo)和世界點(diǎn)物理坐標(biāo)之間的對(duì)應(yīng)關(guān)系。棋盤(pán)格圖案因其角點(diǎn)黑白方格的交點(diǎn)易于被計(jì)算機(jī)高精度檢測(cè)而成為最常用的標(biāo)定板。在標(biāo)定時(shí)我們定義標(biāo)定板所在平面為世界坐標(biāo)系的Z0平面。這樣棋盤(pán)格上每個(gè)角點(diǎn)的世界坐標(biāo)(X, Y, 0)是已知的根據(jù)方格尺寸計(jì)算得出而它在圖像中的像素坐標(biāo)(u, v)可以通過(guò)圖像處理算法檢測(cè)出來(lái)。我們通過(guò)多張不同角度、不同位置的棋盤(pán)格圖像收集大量的(世界點(diǎn) - 圖像點(diǎn))對(duì)應(yīng)關(guān)系然后利用最小二乘法等優(yōu)化算法求解出使得所有投影點(diǎn)與檢測(cè)點(diǎn)誤差最小的相機(jī)參數(shù)。注意棋盤(pán)格方格的物理尺寸必須精確測(cè)量并輸入程序這個(gè)值是所有計(jì)算的尺度基準(zhǔn)。一個(gè)微小的測(cè)量誤差比如0.1mm會(huì)在最終的深度計(jì)算中被放大。建議使用高精度加工的標(biāo)定板或使用游標(biāo)卡尺多次測(cè)量取平均值。3. 環(huán)境準(zhǔn)備與工具選型工欲善其事必先利其器。一個(gè)穩(wěn)定、一致的開(kāi)發(fā)環(huán)境是成功標(biāo)定的第一步。3.1 Python與OpenCV安裝避坑指南對(duì)于新手我最推薦使用Anaconda來(lái)管理Python環(huán)境。它能很好地解決包依賴(lài)沖突的問(wèn)題。你可以創(chuàng)建一個(gè)獨(dú)立的虛擬環(huán)境來(lái)進(jìn)行視覺(jué)項(xiàng)目。# 創(chuàng)建名為 stereo_calib 的虛擬環(huán)境指定Python版本如3.9 conda create -n stereo_calib python3.9 # 激活環(huán)境 conda activate stereo_calib接下來(lái)安裝OpenCV。千萬(wàn)不要只用pip install opencv-python因?yàn)檫@個(gè)包只包含主要模塊不包含contrib模塊而一些高級(jí)功能如更穩(wěn)定的角點(diǎn)檢測(cè)器在里面。應(yīng)該安裝pip install opencv-contrib-python這個(gè)命令會(huì)安裝完整的OpenCV。安裝完成后可以在Python中驗(yàn)證import cv2 print(cv2.__version__)如果遇到網(wǎng)絡(luò)問(wèn)題可以使用國(guó)內(nèi)鏡像源例如清華源pip install opencv-contrib-python -i https://pypi.tuna.tsinghua.edu.cn/simple。3.2 硬件準(zhǔn)備與標(biāo)定板制作雙目相機(jī)市面上從幾十元的USB雙目攝像頭到幾千元的工業(yè)級(jí)雙目模組如ZED, Intel RealSense D455都有。對(duì)于入門(mén)一個(gè)普通的USB雙目攝像頭完全足夠。關(guān)鍵是兩個(gè)相機(jī)需要?jiǎng)傂怨潭ㄔ跇?biāo)定和后續(xù)使用中它們的相對(duì)位置不能有絲毫變化。你可以用3D打印一個(gè)支架或者用膠水、螺絲將它們牢牢固定在一塊亞克力板或鋁型材上。標(biāo)定板你可以選擇購(gòu)買(mǎi)專(zhuān)業(yè)的陶瓷或玻璃標(biāo)定板精度最高。但對(duì)于大多數(shù)實(shí)驗(yàn)和項(xiàng)目自己打印一張足夠。設(shè)計(jì)使用LaTeXtexlive包的tikz或者Python的opencv自己生成一張棋盤(pán)格圖像。確保棋盤(pán)格是奇數(shù)行 x 偶數(shù)列或者偶數(shù)列 x 奇數(shù)行如9x610x7這樣角點(diǎn)數(shù)量是內(nèi)角點(diǎn)不包含最外邊框的交點(diǎn)OpenCV的檢測(cè)函數(shù)默認(rèn)尋找內(nèi)角點(diǎn)。打印用高精度打印機(jī)如激光打印機(jī)在光面銅版紙上打印。關(guān)鍵步驟打印后將它平整地貼在一塊堅(jiān)硬的平板上比如亞克力板、鋁板或者厚重的卡紙上。目的是保證標(biāo)定板在拍攝時(shí)自身是平整的不會(huì)彎曲。測(cè)量用精度至少為0.1mm的游標(biāo)卡尺多次測(cè)量一個(gè)方格的邊長(zhǎng)并記錄平均值。例如測(cè)量值為24.5mm。這個(gè)值將作為程序的輸入。3.3 圖像采集軟件與技巧你需要編寫(xiě)或使用一個(gè)簡(jiǎn)單的程序來(lái)同時(shí)捕獲左右相機(jī)的圖像。OpenCV的VideoCapture可以做到。如果你的雙目攝像頭是獨(dú)立的兩個(gè)USB設(shè)備你需要?jiǎng)?chuàng)建兩個(gè)VideoCapture對(duì)象通常傳入設(shè)備索引0和1。采集黃金法則數(shù)量至少準(zhǔn)備15-20組有效的圖像對(duì)。越多越好但超過(guò)25組后收益遞減。姿態(tài)讓標(biāo)定板覆蓋圖像的各個(gè)區(qū)域中心、四個(gè)角、邊緣。改變標(biāo)定板相對(duì)于相機(jī)的距離、傾斜角度俯仰、偏航、滾轉(zhuǎn)、左右位置。想象用標(biāo)定板“填充”相機(jī)的整個(gè)視野。清晰度確保圖像中棋盤(pán)格清晰不模糊。光照要均勻避免反光和高光區(qū)域遮蓋角點(diǎn)。同步性對(duì)于非硬件同步的USB相機(jī)盡量在標(biāo)定板靜止時(shí)同時(shí)觸發(fā)左右相機(jī)的捕獲以減少運(yùn)動(dòng)模糊和不同步帶來(lái)的誤差。可以在程序里先緩存左右?guī)缓髱缀跬瑫r(shí)保存。我通常會(huì)寫(xiě)一個(gè)簡(jiǎn)單的Python腳本按空格鍵同時(shí)保存左右?guī)⒆詣?dòng)命名如left_01.jpg,right_01.jpg方便后續(xù)處理。4. 分步實(shí)操?gòu)膱D像到參數(shù)的全過(guò)程假設(shè)我們已經(jīng)采集好了20組圖像對(duì)存放在./calib_imgs/文件夾下左右圖像分別以left_和right_開(kāi)頭。方格物理尺寸為24.5 mm。4.1 單目標(biāo)定奠定基礎(chǔ)雙目標(biāo)定建立在兩個(gè)相機(jī)各自參數(shù)準(zhǔn)確的基礎(chǔ)上。我們需要分別對(duì)左相機(jī)和右相機(jī)進(jìn)行單目標(biāo)定。import numpy as np import cv2 import glob # 定義棋盤(pán)格尺寸內(nèi)角點(diǎn)數(shù)量例如9行6列 pattern_size (9, 6) # 定義每個(gè)方格的實(shí)際物理尺寸單位毫米 square_size 24.5 # 為世界坐標(biāo)系中的角點(diǎn)準(zhǔn)備坐標(biāo) (0,0,0), (1,0,0), (2,0,0) ....,(8,5,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 # 用于存儲(chǔ)所有圖像的對(duì)象點(diǎn)和圖像點(diǎn) objpoints [] # 3d point in real world space imgpoints_left [] # 2d points in left image plane. imgpoints_right [] # 2d points in right image plane. # 獲取左右圖像路徑 images_left sorted(glob.glob(./calib_imgs/left_*.jpg)) images_right sorted(glob.glob(./calib_imgs/right_*.jpg)) # 角點(diǎn)檢測(cè)的迭代終止條件 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for i, (fname_left, fname_right) in enumerate(zip(images_left, images_right)): img_l cv2.imread(fname_left) img_r cv2.imread(fname_right) gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) # 在左右圖中查找棋盤(pán)格角點(diǎn) ret_l, corners_l cv2.findChessboardCorners(gray_l, pattern_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, pattern_size, None) # 如果左右圖都成功找到角點(diǎn) if ret_l and ret_r: objpoints.append(objp) # 同一張標(biāo)定板世界坐標(biāo)相同添加一次即可 # 亞像素級(jí)角點(diǎn)精確化 corners_l_refined cv2.cornerSubPix(gray_l, corners_l, (11,11), (-1,-1), criteria) corners_r_refined cv2.cornerSubPix(gray_r, corners_r, (11,11), (-1,-1), criteria) imgpoints_left.append(corners_l_refined) imgpoints_right.append(corners_r_refined) # 可視化角點(diǎn)可選用于調(diào)試 cv2.drawChessboardCorners(img_l, pattern_size, corners_l_refined, ret_l) cv2.drawChessboardCorners(img_r, pattern_size, corners_r_refined, ret_r) # 可以在這里顯示圖像檢查角點(diǎn)是否檢測(cè)正確 # cv2.imshow(Left Corners, img_l) # cv2.imshow(Right Corners, img_r) # cv2.waitKey(500) print(fImage pair {i1}: Corners found.) else: print(fImage pair {i1}: Failed to find corners in one or both images. Skipping.) cv2.destroyAllWindows() # 檢查是否有足夠的數(shù)據(jù)進(jìn)行標(biāo)定 if len(objpoints) 10: print(fError: Only {len(objpoints)} valid image pairs. Need at least 10.) exit() # 分別進(jìn)行左相機(jī)和右相機(jī)的單目標(biāo)定 print(\n--- Calibrating Left Camera ---) ret_l, mtx_l, dist_l, rvecs_l, tvecs_l cv2.calibrateCamera(objpoints, imgpoints_left, gray_l.shape[::-1], None, None) print(fLeft Camera Reprojection Error: {ret_l}) print(\n--- Calibrating Right Camera ---) ret_r, mtx_r, dist_r, rvecs_l, tvecs_l cv2.calibrateCamera(objpoints, imgpoints_right, gray_r.shape[::-1], None, None) print(fRight Camera Reprojection Error: {ret_r}) # 保存單目標(biāo)定結(jié)果 np.savez(mono_calib_results.npz, mtx_leftmtx_l, dist_leftdist_l, mtx_rightmtx_r, dist_rightdist_r) print(Mono calibration results saved.)關(guān)鍵點(diǎn)解析findChessboardCorners這個(gè)函數(shù)是自動(dòng)檢測(cè)角點(diǎn)的核心。如果檢測(cè)失敗可能是棋盤(pán)格沒(méi)有被完整拍到、光照不均、或者pattern_size設(shè)置錯(cuò)誤。cornerSubPix將整數(shù)像素級(jí)的角點(diǎn)位置優(yōu)化到亞像素精度這是提高標(biāo)定精度的關(guān)鍵一步。calibrateCamera執(zhí)行核心的標(biāo)定計(jì)算。返回的重投影誤差ret是一個(gè)重要的質(zhì)量指標(biāo)。一般來(lái)說(shuō)誤差小于0.5像素可以認(rèn)為是很好的結(jié)果0.5到1.0像素是可接受的大于1.0則需要檢查圖像質(zhì)量和角點(diǎn)檢測(cè)。4.2 雙目標(biāo)定求解“雙眼”關(guān)系在獲得兩個(gè)相機(jī)的內(nèi)參和畸變系數(shù)后我們可以進(jìn)行雙目標(biāo)定求解它們之間的旋轉(zhuǎn)和平移關(guān)系。print(\n--- Stereo Calibration ---) # 使用 cv2.stereoCalibrate 進(jìn)行雙目標(biāo)定 # 注意這里傳入的是之前分別標(biāo)定得到的內(nèi)參和畸變作為初始值可以提高優(yōu)化速度和精度。 criteria_stereo (cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 100, 1e-5) flags cv2.CALIB_FIX_INTRINSIC # 使用我們已標(biāo)定的內(nèi)參只優(yōu)化外參和可能的畸變微調(diào) # 也可以使用 CALIB_USE_INTRINSIC_GUESS讓算法在初始值附近優(yōu)化內(nèi)參可能更精確但需要更多數(shù)據(jù)。 retval, _, _, _, _, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteriacriteria_stereo, flagsflags ) print(fStereo Calibration Reprojection Error: {retval}) print(fRotation Matrix (R):\n{R}) print(fTranslation Vector (T):\n{T}) print(fBaseline (norm of T): {np.linalg.norm(T):.2f} mm) # 計(jì)算基線(xiàn)長(zhǎng)度 # 保存雙目標(biāo)定結(jié)果 np.savez(stereo_calib_results.npz, RR, TT, EE, FF, mtx_leftmtx_l, dist_leftdist_l, mtx_rightmtx_r, dist_rightdist_r) print(Stereo calibration results saved.)R和T這就是我們最需要的雙目外參。T通常是一個(gè)3x1的向量其數(shù)值表示右相機(jī)相對(duì)于左相機(jī)的空間位置偏移單位與你提供的square_size一致這里是毫米。E本質(zhì)矩陣和F基礎(chǔ)矩陣包含了雙目對(duì)極幾何的信息在后續(xù)的立體匹配和三維重建中會(huì)用到?;€(xiàn)np.linalg.norm(T)計(jì)算出的就是基線(xiàn)長(zhǎng)度。這個(gè)值對(duì)于深度計(jì)算Z f * B / df焦距B基線(xiàn)d視差至關(guān)重要。4.3 立體校正與極線(xiàn)對(duì)齊得到標(biāo)定參數(shù)后下一步是計(jì)算用于立體校正的映射表并將左右圖像校正到共面行對(duì)準(zhǔn)的狀態(tài)。print(\n--- Computing Rectification Maps ---) # 計(jì)算立體校正的映射矩陣 R1, R2, P1, P2, Q, validPixROI1, validPixROI2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, alpha0 # alpha參數(shù)控制校正后圖像的縮放和裁剪0表示裁剪掉所有無(wú)效像素黑邊 ) # 計(jì)算用于remap的映射表 mapL1, mapL2 cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_16SC2) mapR1, mapR2 cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_16SC2) # 保存校正映射和投影矩陣Q用于后續(xù)的重投影 np.savez(rectify_maps.npz, mapL1mapL1, mapL2mapL2, mapR1mapR1, mapR2mapR2, QQ) print(Rectification maps saved.) # 實(shí)時(shí)校正演示 cap_l cv2.VideoCapture(0) # 假設(shè)左相機(jī)索引0 cap_r cv2.VideoCapture(1) # 假設(shè)右相機(jī)索引1 while True: ret_l, frame_l cap_l.read() ret_r, frame_r cap_r.read() if not (ret_l and ret_r): break # 應(yīng)用校正映射 rectified_l cv2.remap(frame_l, mapL1, mapL2, cv2.INTER_LINEAR) rectified_r cv2.remap(frame_r, mapR1, mapR2, cv2.INTER_LINEAR) # 繪制極線(xiàn)用于檢查在左圖畫(huà)一些點(diǎn)在右圖對(duì)應(yīng)行畫(huà)水平線(xiàn) height, width rectified_l.shape[:2] line_interval 50 for y in range(0, height, line_interval): cv2.line(rectified_l, (0, y), (width, y), (0, 255, 0), 1) cv2.line(rectified_r, (0, y), (width, y), (0, 255, 0), 1) # 并排顯示 display np.hstack((rectified_l, rectified_r)) cv2.imshow(Rectified Stereo Pair (Green lines for epipolar check), display) if cv2.waitKey(1) 0xFF ord(q): break cap_l.release() cap_r.release() cv2.destroyAllWindows()stereoRectify參數(shù)alpha詳解 這是校正過(guò)程中一個(gè)非常實(shí)用且容易困惑的參數(shù)。它控制著校正后圖像的“有效區(qū)域”。alpha -1OpenCV會(huì)自動(dòng)縮放和移動(dòng)圖像使得所有原始像素都可見(jiàn)即原圖沒(méi)有丟失任何信息但會(huì)產(chǎn)生較大的黑色未定義區(qū)域黑邊。alpha 0OpenCV會(huì)進(jìn)行最大化裁剪剔除所有因校正變換而產(chǎn)生的無(wú)效像素黑邊得到的圖像沒(méi)有黑邊但可能會(huì)損失一部分視野。alpha 1保留所有原始像素且不產(chǎn)生任何黑邊通過(guò)只保留兩個(gè)圖像共有的有效區(qū)域來(lái)實(shí)現(xiàn)但圖像會(huì)被顯著縮放和裁剪。0 alpha 1一種折中方案。alpha0.5意味著保留一部分黑邊在視野保留和圖像有效區(qū)域之間取得平衡。對(duì)于后續(xù)的立體匹配我通常推薦使用alpha0。因?yàn)楹谶厖^(qū)域無(wú)效像素在進(jìn)行匹配時(shí)會(huì)產(chǎn)生噪聲或錯(cuò)誤值直接裁剪掉可以簡(jiǎn)化處理并提高匹配質(zhì)量。雖然損失了一些視野但換來(lái)了更干凈的數(shù)據(jù)。5. 標(biāo)定質(zhì)量評(píng)估與深度圖生成實(shí)戰(zhàn)標(biāo)定完了參數(shù)也有了但效果到底怎么樣我們需要用客觀和主觀的方法來(lái)評(píng)估。5.1 重投影誤差分析在單目標(biāo)和雙目標(biāo)定函數(shù)中返回的retval就是平均重投影誤差。這個(gè)值我們已經(jīng)打印出來(lái)了。但更細(xì)致的做法是我們可以手動(dòng)計(jì)算每一幅圖像的重投影誤差找出那些誤差特別大的“壞圖”。def compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist): mean_error 0 error_per_view [] for i in range(len(objpoints)): imgpoints2, _ cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) mean_error error error_per_view.append(error) mean_error / len(objpoints) return mean_error, error_per_view # 計(jì)算左相機(jī)的每幅圖誤差 _, errors_left compute_reprojection_error(objpoints, imgpoints_left, rvecs_l, tvecs_l, mtx_l, dist_l) # 計(jì)算右相機(jī)的每幅圖誤差需要右相機(jī)對(duì)應(yīng)的旋轉(zhuǎn)平移向量這里需要從雙目標(biāo)定結(jié)果或重新計(jì)算略復(fù)雜 # 一個(gè)簡(jiǎn)單的整體評(píng)估是看 stereoCalibrate 返回的誤差。 print(Left camera reprojection errors per image:) for i, err in enumerate(errors_left): print(f Image {i1}: {err:.3f} pixels)如果發(fā)現(xiàn)某幾張圖的誤差顯著高于其他圖例如平均0.3像素但某張圖1.5像素就應(yīng)該檢查這些圖像是否角點(diǎn)檢測(cè)不準(zhǔn)標(biāo)定板是否模糊是否離鏡頭太近導(dǎo)致畸變模型失效考慮剔除這些“壞圖”后重新標(biāo)定往往能提升整體精度。5.2 極線(xiàn)對(duì)齊視覺(jué)檢查這是最直觀的檢查方法。運(yùn)行上面4.3節(jié)的校正演示代碼觀察綠色的水平線(xiàn)。在理想情況下左圖和右圖中同一個(gè)物理特征如標(biāo)定板的角點(diǎn)、桌角、筆帽應(yīng)該位于同一行綠色水平線(xiàn)上。你可以拿一個(gè)邊緣清晰的物體如一支筆在鏡頭前移動(dòng)觀察它在左右校正圖中的行坐標(biāo)是否始終保持一致。如果偏差在1-2個(gè)像素內(nèi)通常可以接受。如果偏差很大說(shuō)明標(biāo)定結(jié)果不準(zhǔn)或者立體校正的映射計(jì)算有誤。5.3 生成深度圖驗(yàn)證標(biāo)定成果的終極測(cè)試標(biāo)定的最終目的是為了三維感知。我們可以用校正后的圖像通過(guò)立體匹配算法計(jì)算視差圖進(jìn)而生成深度圖。這里我們用OpenCV實(shí)現(xiàn)的Semi-Global Block Matching (SGBM)算法為例。# 加載校正映射和參數(shù) rectify_data np.load(rectify_maps.npz) mapL1, mapL2 rectify_data[mapL1], rectify_data[mapL2] mapR1, mapR2 rectify_data[mapR1], rectify_data[mapR2] Q rectify_data[Q] # 重投影矩陣用于將視差圖轉(zhuǎn)換為深度圖 # 讀取一對(duì)校正后的圖像或?qū)崟r(shí)捕獲并校正 img_l cv2.imread(rectified_left.jpg, cv2.IMREAD_GRAYSCALE) img_r cv2.imread(rectified_right.jpg, cv2.IMREAD_GRAYSCALE) # 配置SGBM參數(shù)這些參數(shù)需要根據(jù)你的場(chǎng)景微調(diào) window_size 5 min_disp 0 num_disp 16 * 5 # 必須是16的整數(shù)倍。數(shù)值越大能檢測(cè)的深度范圍越遠(yuǎn)但計(jì)算量也越大。 # 創(chuàng)建SGBM對(duì)象 left_matcher cv2.StereoSGBM_create( minDisparitymin_disp, numDisparitiesnum_disp, blockSizewindow_size, P18 * 3 * window_size ** 2, # 控制視差平滑度的參數(shù) P232 * 3 * window_size ** 2, disp12MaxDiff1, uniquenessRatio15, speckleWindowSize0, speckleRange2, preFilterCap63, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) # 計(jì)算視差圖 disparity left_matcher.compute(img_l, img_r).astype(np.float32) / 16.0 # SGBM返回的視差值是16倍整數(shù) # 將視差圖轉(zhuǎn)換為深度圖 # 公式depth (f * Baseline) / disparity # 其中 f 是焦距像素Baseline是基線(xiàn)毫米disparity是視差像素 # 實(shí)際上重投影矩陣Q已經(jīng)包含了這些信息使用reprojectImageTo3D更簡(jiǎn)單。 points_3d cv2.reprojectImageTo3D(disparity, Q) # 得到的是與視差圖同尺寸的3通道圖像每個(gè)像素是(X,Y,Z) depth_map points_3d[:, :, 2] # 取出Z通道即深度值 # 可視化 cv2.imshow(Left Image, img_l) cv2.imshow(Disparity, (disparity - min_disp) / num_disp) # 歸一化顯示 # 深度圖通常范圍很大需要?dú)w一化或截?cái)囡@示 depth_vis cv2.normalize(depth_map, None, 0, 255, cv2.NORM_MINMAX, dtypecv2.CV_8U) cv2.imshow(Depth Map, depth_vis) cv2.waitKey(0) cv2.destroyAllWindows() # 保存深度數(shù)據(jù)可選 np.save(depth_map.npy, depth_map)SGBM參數(shù)調(diào)優(yōu)心得numDisparities這是最重要的參數(shù)之一。它定義了搜索的視差范圍[minDisparity, minDisparity numDisparities)。設(shè)置太小遠(yuǎn)處的物體無(wú)法匹配設(shè)置太大計(jì)算量增加且可能引入噪聲??梢韵裙浪阋幌耼umDisparities (預(yù)計(jì)最大視差 - 預(yù)計(jì)最小視差)并向上取整到16的倍數(shù)。對(duì)于基線(xiàn)較短幾厘米的桌面級(jí)雙目numDisparities64或80可能是個(gè)不錯(cuò)的起點(diǎn)。blockSize匹配塊的大小。奇數(shù)通常在3~11之間。較小的塊對(duì)細(xì)節(jié)更敏感但噪聲也多較大的塊更平滑但會(huì)損失邊緣細(xì)節(jié)。通常從5或7開(kāi)始嘗試。P1,P2控制視差平滑度的能量函數(shù)參數(shù)。P2應(yīng)大于P1。一個(gè)經(jīng)驗(yàn)法則是P1 8*通道數(shù)*blockSize^2P2 32*通道數(shù)*blockSize^2。對(duì)于灰度圖通道數(shù)為1。uniquenessRatio通常設(shè)置在5~15之間。值越大匹配要求越嚴(yán)格能過(guò)濾掉一些模糊的匹配但也可能增加無(wú)效區(qū)域。運(yùn)行這段代碼后觀察生成的視差圖和深度圖。一個(gè)成功的標(biāo)定應(yīng)該能產(chǎn)生輪廓相對(duì)清晰、噪聲較少的深度圖并且前景物體的深度值小于背景。如果深度圖一片混亂或充滿(mǎn)噪聲首先返回去檢查極線(xiàn)對(duì)齊是否完美其次調(diào)整SGBM的參數(shù)。6. 常見(jiàn)問(wèn)題排查與性能優(yōu)化技巧在實(shí)際操作中你幾乎一定會(huì)遇到各種問(wèn)題。下面是我踩過(guò)坑后總結(jié)的一些排查思路和優(yōu)化技巧。6.1 角點(diǎn)檢測(cè)失敗或不穩(wěn)定癥狀findChessboardCorners經(jīng)常返回False或者檢測(cè)到的角點(diǎn)位置亂跳。排查圖像模糊確保相機(jī)對(duì)焦準(zhǔn)確標(biāo)定板紋理清晰。可以嘗試手動(dòng)對(duì)焦或使用更小的光圈增加景深。光照不均或反光避免強(qiáng)光直射標(biāo)定板產(chǎn)生高光。使用柔和的漫射光源。如果標(biāo)定板是光面打印的可以考慮噴涂亞光清漆。棋盤(pán)格圖案問(wèn)題確認(rèn)pattern_size設(shè)置的是內(nèi)角點(diǎn)數(shù)量行數(shù)-1列數(shù)-1。例如你打印的格子是10x7那么內(nèi)角點(diǎn)就是9x6。對(duì)比度不足調(diào)整相機(jī)曝光或增益使黑白方格對(duì)比鮮明??梢栽诖a中先對(duì)圖像進(jìn)行直方圖均衡化cv2.equalizeHist(gray)試試。嘗試其他角點(diǎn)檢測(cè)方法OpenCV的findChessboardCornersSB位于contrib模塊比傳統(tǒng)的findChessboardCorners更魯棒尤其對(duì)模糊或部分遮擋的圖像。用法類(lèi)似但需要OpenCV編譯時(shí)啟用了contrib模塊我們安裝的opencv-contrib-python包含它。6.2 重投影誤差過(guò)大1.0像素癥狀標(biāo)定程序能跑通但輸出的重投影誤差很大。排查標(biāo)定板平整度這是最常見(jiàn)的原因之一。打印的紙如果翹曲世界坐標(biāo)Z0的假設(shè)就不成立。務(wù)必貼在硬質(zhì)平板上。物理尺寸測(cè)量錯(cuò)誤用游標(biāo)卡尺反復(fù)測(cè)量多個(gè)方格取平均值。確認(rèn)輸入到程序的square_size單位毫米和數(shù)值正確。圖像質(zhì)量差同6.1檢查模糊、畸變過(guò)大特別是圖像邊緣的圖像。畸變太大的區(qū)域角點(diǎn)檢測(cè)本身就不準(zhǔn)。姿態(tài)多樣性不足所有圖像中標(biāo)定板都集中在視野中心或者傾斜角度變化不大。需要確保標(biāo)定板覆蓋整個(gè)視野并且有各種旋轉(zhuǎn)角度。剔除壞圖如5.1所述計(jì)算每張圖的誤差剔除誤差明顯高于平均值的圖像例如誤差大于平均誤差2倍然后重新標(biāo)定。嘗試更多圖像如果只有10-12張圖增加到20-25張。6.3 立體校正后極線(xiàn)仍不對(duì)齊癥狀校正后的圖像左右圖的對(duì)應(yīng)特征點(diǎn)不在同一水平線(xiàn)上。排查標(biāo)定參數(shù)錯(cuò)誤首先懷疑雙目標(biāo)定得到的R和T不準(zhǔn)。重新進(jìn)行雙目標(biāo)定確保使用了足夠多且高質(zhì)量的圖像對(duì)。相機(jī)在標(biāo)定后發(fā)生移動(dòng)這是硬件問(wèn)題。雙目相機(jī)必須剛性固定。如果在采集標(biāo)定圖像和后續(xù)使用之間相機(jī)被碰了一下所有參數(shù)就都失效了。必須重新標(biāo)定。stereoRectify參數(shù)使用錯(cuò)誤檢查傳入stereoRectify的內(nèi)參、畸變、圖像尺寸、R、T是否與雙目標(biāo)定結(jié)果一致。鏡頭畸變模型不適用OpenCV默認(rèn)使用(k1, k2, p1, p2, k3)的畸變模型。對(duì)于魚(yú)眼鏡頭或畸變特別大的廣角鏡頭需要使用魚(yú)眼相機(jī)模型cv2.fisheye命名空間下的函數(shù)進(jìn)行標(biāo)定和校正。6.4 深度圖噪聲大、空洞多癥狀計(jì)算出的深度圖充滿(mǎn)雪花點(diǎn)噪聲或者物體內(nèi)部有很多空洞無(wú)效點(diǎn)。優(yōu)化后處理濾波空洞填充使用cv2.ximgproc.createDisparityWLSFilter進(jìn)行左右一致性檢查濾波可以顯著減少誤匹配和空洞。平滑濾波對(duì)視差圖進(jìn)行中值濾波cv2.medianBlur或雙邊濾波cv2.bilateralFilter可以在平滑噪聲的同時(shí)保留邊緣。注意濾波會(huì)損失一些細(xì)節(jié)和精度。調(diào)整立體匹配參數(shù)仔細(xì)調(diào)整5.3節(jié)中SGBM的參數(shù)。特別是P1/P2控制平滑度和uniquenessRatio控制匹配唯一性閾值。圖像預(yù)處理在立體匹配前對(duì)左右校正圖進(jìn)行直方圖均衡化或使用cv2.xphoto.createGrayworldWB()進(jìn)行白平衡可以提高紋理不明顯區(qū)域的匹配效果。外極線(xiàn)約束檢查理論上校正后對(duì)應(yīng)點(diǎn)行坐標(biāo)一致。可以在匹配后對(duì)匹配點(diǎn)對(duì)進(jìn)行行坐標(biāo)檢查剔除行坐標(biāo)差超過(guò)1-2個(gè)像素的誤匹配。6.5 性能優(yōu)化技巧校正映射表復(fù)用initUndistortRectifyMap計(jì)算量較大但一旦算出mapL1, mapL2, mapR1, mapR2就可以保存下來(lái)如用np.savez。在后續(xù)實(shí)時(shí)處理中直接加載這些映射表并使用cv2.remap速度極快。降低分辨率如果對(duì)深度圖實(shí)時(shí)性要求高可以先將捕獲的圖像縮放到較小的尺寸如VGA 640x480再進(jìn)行校正和匹配能大幅提升幀率。使用更快的匹配算法SGBM精度不錯(cuò)但較慢。如果場(chǎng)景紋理豐富可以嘗試cv2.StereoBM塊匹配它速度更快但精度和抗噪性稍差。對(duì)于非常追求速度的場(chǎng)景可以考慮CUDA加速的立體匹配算法如cv2.cuda.createStereoBM或cv2.cuda.createStereoSGM但這需要編譯帶CUDA支持的OpenCV。整個(gè)雙目相機(jī)標(biāo)定的流程從原理理解、環(huán)境搭建、數(shù)據(jù)采集、代碼實(shí)現(xiàn)到問(wèn)題排查是一個(gè)典型的“理論指導(dǎo)實(shí)踐實(shí)踐反饋理論”的過(guò)程。第一次做可能會(huì)遇到各種問(wèn)題但每解決一個(gè)你對(duì)相機(jī)模型和立體視覺(jué)的理解就會(huì)加深一層。最關(guān)鍵的還是耐心和細(xì)致仔細(xì)測(cè)量標(biāo)定板、認(rèn)真采集多樣化的數(shù)據(jù)、嚴(yán)謹(jǐn)?shù)胤治稣`差。當(dāng)你看到第一張由自己標(biāo)定的雙目系統(tǒng)生成的、輪廓分明的深度圖時(shí)那種成就感會(huì)讓你覺(jué)得所有的折騰都是值得的。本文還有配套的精品資源點(diǎn)擊獲取