物體尺寸測(cè)量:從標(biāo)定到三維坐標(biāo)計(jì)算)
簡(jiǎn)介這套基于 Python 與 OpenCV 的雙目視覺(jué)尺寸測(cè)量項(xiàng)目源碼及配套文檔面向需要完成畢業(yè)設(shè)計(jì)、期末大作業(yè)或課程設(shè)計(jì)的計(jì)算機(jī)視覺(jué)方向讀者旨在幫助快速搭建物體尺寸測(cè)量系統(tǒng)理解雙目視差、相機(jī)標(biāo)定與三角測(cè)量等核心原理。資源包共 23 個(gè)文件壓縮后約 2.27MB內(nèi)含 21 張左右目測(cè)試圖像、1 個(gè)帶詳細(xì)注釋的 Python 主程序以及 1 份 Markdown 說(shuō)明文檔代碼覆蓋圖像預(yù)處理、特征提取、立體匹配和尺寸換算等關(guān)鍵環(huán)節(jié)文檔則配合源碼講解項(xiàng)目結(jié)構(gòu)、運(yùn)行方式與算法思路。整套項(xiàng)目目前已有 83 人學(xué)習(xí)界面直觀、功能完整可在本地直接部署運(yùn)行項(xiàng)目經(jīng)過(guò)嚴(yán)格調(diào)試整體流程清晰適合作為高分離線項(xiàng)目參考。對(duì)初學(xué)者而言清晰的注釋能降低入門(mén)門(mén)檻對(duì)進(jìn)階者也能提供從圖像標(biāo)定到實(shí)際測(cè)距的完整工程參考。1. 雙目視覺(jué)測(cè)尺寸為什么說(shuō)它比單目靠譜又容易在哪里翻車(chē)期末大作業(yè)要測(cè)一個(gè)紙盒的長(zhǎng)寬高你首先想到的是用 OpenCV 加單目攝像頭把畫(huà)面里像素?fù)Q算成毫米。但拍過(guò)就知道物體離鏡頭近一點(diǎn)、遠(yuǎn)一點(diǎn)同樣一個(gè)盒子的像素高度變了而單目圖像里根本沒(méi)有深度信息比例尺是猜的?;?OpenCV 的雙目視覺(jué)測(cè)量被攝物體尺寸本質(zhì)就是用兩個(gè)固定間距的攝像頭模擬人眼通過(guò)左右圖像的視差把每個(gè)像素的深度算出來(lái)再在三維空間里量?jī)牲c(diǎn)距離。這個(gè)方案能解決的正是“物體不在同一平面、相機(jī)距離未知”時(shí)的尺寸測(cè)量問(wèn)題適合期末大作業(yè)、畢設(shè)里需要展示完整技術(shù)鏈路的場(chǎng)景。它能同時(shí)拿到深度圖和三維坐標(biāo)但前提是標(biāo)定、立體匹配、測(cè)量三環(huán)都別出錯(cuò)任何一個(gè)環(huán)節(jié)失守測(cè)出來(lái)的數(shù)字比單目還離譜。2. 從相機(jī)標(biāo)定到極線校正先把雙目系統(tǒng)的“尺子”校準(zhǔn)2.1 雙目測(cè)距的原理與坐標(biāo)系視差、基線、焦距的關(guān)系雙目測(cè)尺寸的核心不是“拍兩張圖”而是知道左右兩個(gè)相機(jī)之間的空間關(guān)系。一個(gè)理想的雙目系統(tǒng)里左右相機(jī)光心距離叫基線b焦距是f空間中某一點(diǎn) P 在左右相機(jī)成像平面上的水平坐標(biāo)分別是xl和xr視差d xl - xr。根據(jù)相似三角形深度z f * b / d。有了 z再加上相機(jī)內(nèi)參就能把像素坐標(biāo)(u, v)換算成相機(jī)坐標(biāo)系下的三維坐標(biāo)X (u - cx) * z / fx Y (v - cy) * z / fy Z z所以整個(gè)測(cè)量流程可以拆成三步先把左右相機(jī)各自的內(nèi)參和畸變算出來(lái)這叫單目標(biāo)定再算兩個(gè)相機(jī)之間的旋轉(zhuǎn)矩陣 R 和平移向量 T這叫雙目標(biāo)定最后把左右圖像校正成嚴(yán)格的“左右對(duì)齊只差視差”的形態(tài)這叫立體校正。校準(zhǔn)這一步是后面所有數(shù)字的尺子尺子歪了測(cè)出來(lái)的尺寸全是錯(cuò)的。2.2 標(biāo)定板采集與 calibrateCamera 參數(shù)棋盤(pán)格數(shù)量與角點(diǎn)提取我一般建議用 OpenCV 的經(jīng)典標(biāo)定流程不需要額外下載標(biāo)定工具。首先打印一張棋盤(pán)格貼在硬紙板上。內(nèi)角點(diǎn)數(shù)量建議用 9×6 或者 8×6不要用太少角點(diǎn)太少解出來(lái)的內(nèi)參不穩(wěn)定。采集時(shí)左右相機(jī)各拍一組同一時(shí)刻拍同一個(gè)棋盤(pán)格姿勢(shì)至少拍 15 組角度要覆蓋傾斜、平移、遠(yuǎn)近不同。采集代碼如下import cv2 import numpy as np # 棋盤(pán)格內(nèi)角點(diǎn)數(shù)比如橫向9個(gè)縱向6個(gè) pattern_size (9, 6) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) def collect_chessboard(cap_left, cap_right, save_dir, count20): for i in range(count): ret_l, frame_l cap_left.read() ret_r, frame_r cap_right.read() gray_l cv2.cvtColor(frame_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(frame_r, cv2.COLOR_BGR2GRAY) found_l, corners_l cv2.findChessboardCorners(gray_l, pattern_size, None) found_r, corners_r cv2.findChessboardCorners(gray_r, pattern_size, None) if found_l and found_r: corners_l cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) cv2.imwrite(f{save_dir}/left_{i:02d}.jpg, frame_l) cv2.imwrite(f{save_dir}/right_{i:02d}.jpg, frame_r) print(f采集第 {i1} 組左右同時(shí)檢測(cè)到角點(diǎn))cornerSubPix是把角點(diǎn)精度從像素級(jí)提升到亞像素級(jí)的關(guān)鍵窗口大小取(11, 11)即可。采集時(shí)要注意左右兩張圖必須同時(shí)按下快門(mén)或者用支持同步的雙目攝像頭否則棋盤(pán)格在運(yùn)動(dòng)中拍出來(lái)的左右位置不匹配后面的立體標(biāo)定會(huì)出現(xiàn)無(wú)法收斂的畸變。保存圖像后再統(tǒng)一讀取文件做標(biāo)定避免實(shí)時(shí)標(biāo)定時(shí)相機(jī)還在自動(dòng)調(diào)節(jié)曝光。這里有一個(gè)細(xì)節(jié)OpenCV 標(biāo)定需要的是“世界坐標(biāo)”的棋盤(pán)格角點(diǎn)如果用 9×6 棋盤(pán)每個(gè)格子邊長(zhǎng)記得量準(zhǔn)確單位用毫米。標(biāo)定主體代碼def calibrate_camera(image_paths, pattern_size, square_size): # 生成棋盤(pán)格角點(diǎn)的三維世界坐標(biāo)假設(shè)棋盤(pán)格在Z0平面 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 [] img_points [] for path in image_paths: img cv2.imread(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: corners cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) return mtx, distsquare_size是實(shí)際標(biāo)定板的格子邊長(zhǎng)單位必須一致比如 25 毫米后面測(cè)尺寸的結(jié)果單位就是毫米。如果這里寫(xiě)錯(cuò)內(nèi)參里的焦距會(huì)等比例錯(cuò)最終測(cè)出來(lái)的長(zhǎng)寬高全部偏移。標(biāo)定完成之后用cv2.projectPoints把角點(diǎn)重新投影回圖像計(jì)算重投影誤差一般小于 0.1 像素算合格。如果誤差在 0.5 像素以上多半是采集圖像模糊、標(biāo)定板不平整或者圖片數(shù)量太少。2.3 立體校正與 stereoRectify 參數(shù)alpha 和平衡單目標(biāo)定之后還需要雙目標(biāo)定來(lái)求兩臺(tái)相機(jī)的相對(duì)位置關(guān)系。這里用stereoCalibrate輸入左右相機(jī)各自的內(nèi)參和畸變系數(shù)以及每一對(duì)圖像的角點(diǎn)坐標(biāo)輸出旋轉(zhuǎn)矩陣R、平移向量T。拿到R和T后用stereoRectify生成校正映射def stereo_rectify(mtx_l, dist_l, mtx_r, dist_r, R, T, image_size): R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, image_size, R, T, alpha0, flagscv2.CALIB_ZERO_DISPARITY) map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, image_size, cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, image_size, cv2.CV_32FC1) return map1_l, map2_l, map1_r, map2_r, QstereoRectify的alpha參數(shù)控制校正后圖像保留多少原始像素。alpha0會(huì)裁剪掉圖像邊緣的非公共區(qū)域?qū)е乱暡顖D有效區(qū)域變小alpha1會(huì)保留全部像素但邊緣會(huì)出現(xiàn)黑色條帶。我一般取alpha0讓有效區(qū)域最大化同時(shí)配合CALIB_ZERO_DISPARITY讓主點(diǎn)保持一致這樣后續(xù) SGBM 搜索范圍更整齊。Q是重投影矩陣后面從視差圖轉(zhuǎn)三維坐標(biāo)全靠它。校正之后可以用cv2.remap生成校正圖再用鼠標(biāo)點(diǎn)同一物理點(diǎn)在左右圖上的像素坐標(biāo)看是否在同一水平線上明顯不在就說(shuō)明標(biāo)定失敗。3. 用 OpenCV 實(shí)現(xiàn)視差圖與深度圖SGBM 參數(shù)是唯一玄學(xué)3.1 視差計(jì)算的最小閉環(huán)SGBM 創(chuàng)建與參數(shù)初始化標(biāo)定和校正都通過(guò)后下一步是立體匹配。OpenCV 里有兩種常見(jiàn)的匹配器StereoBM和SGBM。BM 快但噪聲大SGBM 慢但更平滑實(shí)測(cè)中 SGBM 配合 WLS 濾波是期末作業(yè)和畢設(shè)里最穩(wěn)妥的組合。最小閉環(huán)代碼def create_sgbm(min_disparity0, num_disparities64, block_size11): left_matcher cv2.StereoSGBM_create( minDisparitymin_disparity, numDisparitiesnum_disparities, blockSizeblock_size, P18 * 3 * block_size ** 2, P232 * 3 * block_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) return left_matchernumDisparities必須能被 16 整除它決定了搜索的視差范圍。如果被測(cè)物體離相機(jī)較近視差范圍大取 96 或 128物體在 1 米以外64 就夠。blockSize必須是奇數(shù)值越大對(duì)噪聲越不敏感但也會(huì)抹掉邊緣細(xì)節(jié)。P1和P2是平滑懲罰項(xiàng)控制視差圖的平滑程度。經(jīng)驗(yàn)值是小 blockSize 時(shí)用8 * 3 * blockSize**2和32 * 3 * blockSize**2當(dāng) blockSize 增大時(shí)P1、P2 按比例調(diào)大。uniquenessRatio用于排除低置信度的匹配點(diǎn)10 到 15 之間比較合適。這些參數(shù)沒(méi)有萬(wàn)能取值我用一組默認(rèn)值跑完再根據(jù)視差圖里的空洞和橫條紋調(diào)。3.2 從視差到深度重投影矩陣 Q 與 point cloud 的換算得到原始視差圖disp后需要用reprojectImageTo3D把它轉(zhuǎn)換成三維點(diǎn)集。這個(gè)函數(shù)用到雙目標(biāo)定時(shí)輸出的Q矩陣def compute_depth(disp, Q, min_depth100, max_depth2000): # disp 是 int16 格式值表示實(shí)際視差 * 16 disp_float disp.astype(np.float32) / 16.0 disp_float[disp_float 0] 0.1 # 避免除零 points_3d cv2.reprojectImageTo3D(disp_float, Q, handleMissingValuesTrue) # 取出每個(gè)像素的深度 Z depth_map points_3d[:, :, 2] depth_map[depth_map min_depth] 0 depth_map[depth_map max_depth] 0 return points_3d, depth_map一個(gè)常見(jiàn)的坑SGBM 輸出的disp是定點(diǎn)數(shù)真實(shí)視差必須除以 16。如果漏掉這一步深度會(huì)小 16 倍測(cè)出來(lái)的尺寸也小 16 倍。另外handleMissingValuesTrue會(huì)把無(wú)效點(diǎn)置成 0但處理時(shí)會(huì)引入邊緣的異常值所以后面必須做深度裁剪。min_depth和max_depth根據(jù)你的拍攝距離設(shè)置比如物體離相機(jī) 50 到 150 厘米就設(shè) 500 和 1500 毫米能過(guò)濾掉大部分噪聲。如果你只想看深度可以用cv2.normalize把深度圖映射到 0-255 顯示但測(cè)量尺寸時(shí)一定要用三維坐標(biāo)點(diǎn)而不是直接用視差換算的近似深度。3.3 深度圖后處理WLS 濾波與空洞填充的參數(shù)選擇原始視差圖通常有大量黑色空洞尤其是紋理少的區(qū)域比如紙箱的純色表面。OpenCV 的ximgproc模塊提供了 WLS 濾波需要左右兩個(gè)方向的一致性檢查。做法是先創(chuàng)建右匹配器算出右視差圖再傳給createDisparityWLSFilterdef wls_filter(left_matcher, left_img, right_img, disp_l, disp_r, sigma1.5, lmbda8000): right_matcher cv2.ximgproc.createRightMatcher(left_matcher) disp_r right_matcher.compute(right_img, left_img) wls_filter cv2.ximgproc.createDisparityWLSFilter(left_matcher) wls_filter.setLambda(lmbda) wls_filter.setSigmaColor(sigma) filtered_disp wls_filter.filter(disp_l, left_img, disparity_map_rightdisp_r) return filtered_displmbda是平滑權(quán)重值越大視差圖越平滑但實(shí)際物體的棱角也會(huì)被抹平導(dǎo)致邊緣向外擴(kuò)測(cè)出來(lái)的尺寸偏大。我用 8000 到 10000 之間sigma用 1.5。濾波后仍然會(huì)有小塊空洞可以再做一步中值濾波或者形態(tài)學(xué)閉合。注意createRightMatcher使用的參數(shù)必須與左匹配器完全一致否則左右一致性檢查會(huì)失效。這一章做完你已經(jīng)能從一對(duì)圖像中得到每個(gè)像素的三維坐標(biāo)接下來(lái)該怎么量尺寸取決于被測(cè)物體放在哪里、你要量的是哪個(gè)部位。4. 測(cè)量被攝物體的尺寸像素坐標(biāo)到世界坐標(biāo)的換算與 ROI 操作4.1 目標(biāo)分割與基準(zhǔn)平面擬合在最簡(jiǎn)單場(chǎng)景下先跑通測(cè)量尺寸時(shí)最直接的思路是鼠標(biāo)在左圖上框出被測(cè)區(qū)域然后取這一區(qū)域內(nèi)的三維點(diǎn)云計(jì)算兩點(diǎn)間距離。但要保證精度必須知道物體放置的平面。如果物體放在桌面上我們可以先用標(biāo)定板或者一個(gè)已知平面擬合出桌面方程這樣物體表面即使不平整也能用平面投影來(lái)消除視角誤差。一個(gè)更簡(jiǎn)單的方案適合期末大作業(yè)把被測(cè)物體放在一個(gè)棋盤(pán)格標(biāo)定板上用標(biāo)定板所在的平面作為基準(zhǔn)面。檢測(cè)棋盤(pán)格角點(diǎn)得到該平面的位姿然后把物體上的像素點(diǎn)投影到這個(gè)平面。代碼思路如下def segment_object_by_roi(image, depth_map, roi): x, y, w, h roi roi_depth depth_map[y:yh, x:xw] # 用ROI內(nèi)深度中值作為物體表面深度 valid_depth roi_depth[roi_depth 0] if len(valid_depth) 0: return None surface_depth np.median(valid_depth) # 得到ROI中心像素的三維坐標(biāo) cx x w // 2 cy y h // 2 # 根據(jù)深度和相機(jī)內(nèi)參反算三維坐標(biāo) fx mtx_l[0, 0] fy mtx_l[1, 1] cx_intr mtx_l[0, 2] cy_intr mtx_l[1, 2] X (cx - cx_intr) * surface_depth / fx Y (cy - cy_intr) * surface_depth / fy Z surface_depth return (X, Y, Z)這個(gè)簡(jiǎn)化模型假設(shè)物體表面是一個(gè)平面并且深度取中值。實(shí)測(cè)時(shí)如果物體的厚度不能忽略比如一個(gè)盒子要分別測(cè)頂面和放置平面的距離來(lái)得到高度而不是直接取表面深度。roi可以用cv2.selectROI讓用戶手動(dòng)框選這比自動(dòng)分割更穩(wěn)因?yàn)閺?fù)雜背景下自動(dòng)分割經(jīng)常把陰影和反光也算進(jìn)去。4.2 尺寸計(jì)算兩點(diǎn)間歐氏距離與尺度因子有了兩點(diǎn)的三維坐標(biāo)長(zhǎng)寬就是三維空間里的歐氏距離。但如果你已經(jīng)讓物體貼著基準(zhǔn)平面放置測(cè)量頂面兩個(gè)邊緣點(diǎn)之間的距離時(shí)需要把這兩個(gè)點(diǎn)投影到同一平面上否則因?yàn)橥敢晝A斜算出來(lái)的距離比真實(shí)值偏大。我一般先擬合一個(gè)平面到點(diǎn)云數(shù)據(jù)然后把待測(cè)點(diǎn)垂直投影到這個(gè)平面再算投影后的距離。擬合平面可以用最小二乘OpenCV 沒(méi)有直接封裝但可以用np.linalg.lstsqdef fit_plane(points): # points: Nx3 矩陣 centroid points.mean(axis0) centered points - centroid _, _, vt np.linalg.svd(centered) normal vt[-1] d -np.dot(normal, centroid) return normal, d def project_to_plane(point, normal, d): # 點(diǎn)到平面投影 dist np.dot(normal, point) d return point - dist * normal這個(gè)函數(shù)可以復(fù)用標(biāo)定板的角點(diǎn)把檢測(cè)到的棋盤(pán)格角點(diǎn)三維坐標(biāo)喂進(jìn)去得到標(biāo)定板平面方程。測(cè)量物體輪廓上的任意點(diǎn)時(shí)候先投影到該平面再計(jì)算兩點(diǎn)距離能顯著減少相機(jī)傾斜帶來(lái)的誤差。實(shí)際操作中我會(huì)在左圖上用鼠標(biāo)點(diǎn)擊物體的兩個(gè)角點(diǎn)然后去三維點(diǎn)云里取這兩個(gè)像素坐標(biāo)對(duì)應(yīng)的 XYZ。4.3 把測(cè)量程序封裝成函數(shù)輸入輸出與可視化為了讓代碼看起來(lái)像一個(gè)完整的“項(xiàng)目源碼”最好把整個(gè)流程封裝成一個(gè)類(lèi)或者一組函數(shù)方便期末答辯時(shí)演示。下面是一個(gè)最小封裝class StereoMeasurer: def __init__(self, mtx_l, dist_l, mtx_r, dist_r, R, T): self.mtx_l mtx_l self.dist_l dist_l self.dist_r dist_r self.R R self.T T self.map1_l, self.map2_l, self.map1_r, self.map2_r, self.Q None, None, None, None, None def rectify(self, image_size, alpha0): self.R1, self.R2, self.P1, self.P2, self.Q, _, _ cv2.stereoRectify( self.mtx_l, self.dist_l, self.mtx_r, self.dist_r, image_size, self.R, self.T, alphaalpha) self.map1_l, self.map2_l cv2.initUndistortRectifyMap( self.mtx_l, self.dist_l, self.R1, self.P1, image_size, cv2.CV_32FC1) self.map1_r, self.map2_r cv2.initUndistortRectifyMap( self.mtx_r, self.dist_r, self.R2, self.P2, image_size, cv2.CV_32FC1) def compute_disparity(self, left, right, sgbm_params): left_rect cv2.remap(left, self.map1_l, self.map2_l, cv2.INTER_LINEAR) right_rect cv2.remap(right, self.map1_r, self.map2_r, cv2.INTER_LINEAR) matcher create_sgbm(**sgbm_params) disp matcher.compute(left_rect, right_rect) return disp def measure_distance(self, left, right, p1, p2, sgbm_params): disp self.compute_disparity(left, right, sgbm_params) _, depth_map compute_depth(disp, self.Q) # p1, p2 是左圖上的像素坐標(biāo) d1 depth_map[p1[1], p1[0]] d2 depth_map[p2[1], p2[0]] if d1 0 or d2 0: return None # 根據(jù)深度和相機(jī)內(nèi)參計(jì)算3D點(diǎn) pt1 pixel_to_3d(p1, d1, self.mtx_l) pt2 pixel_to_3d(p2, d2, self.mtx_l) return np.linalg.norm(pt1 - pt2)pixel_to_3d函數(shù)就是前面提到的反投影公式。可視化部分可以用cv2.imshow把視差圖和深度圖并排顯示再畫(huà)上你點(diǎn)擊的測(cè)量點(diǎn)和連線。所有這些代碼合在一起就是一個(gè)可供答辯演示的“基于 OpenCV 的雙目視覺(jué)尺寸測(cè)量”項(xiàng)目。5. 雙目測(cè)尺寸的常見(jiàn)坑與排查從標(biāo)定到測(cè)量的 5 條血淚經(jīng)驗(yàn)5.1 重投影誤差看著低測(cè)出來(lái)卻差一截現(xiàn)象calibrateCamera返回的重投影誤差只有 0.08 像素但測(cè)量一個(gè) 100 毫米的標(biāo)準(zhǔn)塊結(jié)果偏差超過(guò) 10 毫米。原因單目標(biāo)定的重投影誤差只代表單目?jī)?nèi)參自洽不代表雙目標(biāo)定里的R、T準(zhǔn)確。常見(jiàn)的是左右相機(jī)采集棋盤(pán)格時(shí)兩組圖像沒(méi)有嚴(yán)格同步或者棋盤(pán)格一半被遮擋角點(diǎn)檢測(cè)到的位置有偏移。另一個(gè)隱藏原因是標(biāo)定板不平整打印紙貼在彎曲的紙板上角點(diǎn)世界坐標(biāo)和真實(shí)位置有偏差。解決把左右相機(jī)固定在一個(gè)剛性支架上用外觸發(fā)或者同時(shí)拍照每張圖像檢查棋盤(pán)格所有內(nèi)角點(diǎn)都被完整檢測(cè)到標(biāo)定板用玻璃貼片或硬塑料板。我習(xí)慣把采集的圖像數(shù)量從 15 組增加到 25 組并保證棋盤(pán)格在畫(huà)面四個(gè)角和中心都出現(xiàn)過(guò)。5.2 視差圖全是橫紋或者“鬼影”現(xiàn)象SGBM 計(jì)算出的視差圖有一條條的水平條紋物體邊緣像帶鋸齒的重影深度圖看起來(lái)是臟的色塊。原因左右圖像校正不徹底極線沒(méi)有嚴(yán)格對(duì)齊。stereoRectify之后沒(méi)有驗(yàn)收直接丟給 SGBM。SGBM 的搜索窗口默認(rèn)只做水平搜索如果極線有哪怕 1-2 個(gè)像素的偏差匹配就全亂。解決用cv2.remap校正后在左右圖上畫(huà)一條水平線觀察同一個(gè)角點(diǎn)是否落在同一條水平線上。如果有偏差檢查stereoRectify的入?yún)⑹遣皇亲笥覂?nèi)參寫(xiě)反了或者R、T方向不對(duì)。另一個(gè)常見(jiàn)原因是相機(jī)分辨率不小但標(biāo)定時(shí)沒(méi)有傳入正確的imageSize導(dǎo)致映射矩陣尺寸和原圖對(duì)不上。5.3 純色物體測(cè)出來(lái)的尺寸忽大忽小現(xiàn)象一個(gè)白色紙箱放在淺色桌面上視差圖里紙箱表面有大片空洞邊緣被噪聲覆蓋連續(xù)測(cè)量?jī)纱谓Y(jié)果差了 20 毫米。原因SGBM 依賴(lài)紋理來(lái)匹配左右圖像。純色區(qū)域沒(méi)有梯度匹配置信度低OpenCV 只能靠平滑項(xiàng)去猜猜出來(lái)的視差自然不穩(wěn)。解決最省事的辦法是在物體表面貼上帶紋理的貼紙或撒一點(diǎn)細(xì)小顆粒物如果不想破壞外觀就把blockSize調(diào)大讓平滑窗口覆蓋更大區(qū)域或者用 WLS 濾波把空洞補(bǔ)上。但要記住blockSize也不能無(wú)腦大它會(huì)把邊緣往里收窄導(dǎo)致測(cè)出來(lái)的尺寸偏小需要拿標(biāo)準(zhǔn)塊標(biāo)定一次補(bǔ)償值。5.4 測(cè)量時(shí)相機(jī)被碰一下所有數(shù)據(jù)全廢現(xiàn)象標(biāo)定和測(cè)量都正常但某次測(cè)量前相機(jī)支架被碰歪了之后測(cè)出的尺寸直接飄了。原因雙目測(cè)量的前提是兩個(gè)相機(jī)的相對(duì)位姿不變。只要左右相機(jī)之間有任何微小的旋轉(zhuǎn)和平移R、T和Q矩陣全部失效視差和深度跟著錯(cuò)。解決相機(jī)支架要用螺絲鎖死不要用夾子或雙面膠。記錄每個(gè)標(biāo)定板角點(diǎn)的三維坐標(biāo)如果發(fā)現(xiàn)前后兩次標(biāo)定得到的R、T變化超過(guò) 0.1 毫米量級(jí)就重新標(biāo)定。另一個(gè)實(shí)用方案是做“在線校驗(yàn)”每次開(kāi)機(jī)拍一張固定位置的棋盤(pán)格計(jì)算當(dāng)前的R、T如果和標(biāo)定結(jié)果偏差大就報(bào)錯(cuò)提醒。5.5 手動(dòng)點(diǎn)擊測(cè)量點(diǎn)時(shí)亞像素精度不夠現(xiàn)象用鼠標(biāo)點(diǎn)擊角點(diǎn)時(shí)的像素坐標(biāo)是整數(shù)取深度時(shí)也是取單點(diǎn)像素的值導(dǎo)致兩三次測(cè)量重復(fù)性差。原因整數(shù)值像素對(duì)邊緣而言有 0.5 像素的量化誤差深度圖的單點(diǎn)噪聲又會(huì)被放大測(cè)出來(lái)的距離自然抖。解決測(cè)量點(diǎn)不要在 ROI 里取單個(gè)像素而是取該點(diǎn)周?chē)?3×3 或 5×5 窗口的深度中值。如果物體邊緣有清晰的角點(diǎn)用cv2.cornerSubPix先求亞像素角點(diǎn)再測(cè)。另外深度圖本身要先做中值濾波cv2.medianBlur(depth, 5)能有效抑制單點(diǎn)噪聲但要注意濾波核太大會(huì)把邊緣弄圓。6. 進(jìn)階用自動(dòng)標(biāo)定與單應(yīng)性簡(jiǎn)化測(cè)量附驗(yàn)證方法和驗(yàn)收習(xí)慣如果你的項(xiàng)目時(shí)間不夠不想走完整雙目標(biāo)定流程還有一種適合固定場(chǎng)景的簡(jiǎn)化方案把相機(jī)固定在一個(gè)已知高度的支架上拍攝一個(gè)貼著棋盤(pán)格的平面然后用單應(yīng)性矩陣建立像素到真實(shí)平面坐標(biāo)的映射。這種方法只適合物體完全貼合這個(gè)平面、且沒(méi)有明顯厚度的場(chǎng)景比如測(cè)量一張紙的長(zhǎng)度、一個(gè) PCB 板的尺寸。它不需要 SGBM直接以棋盤(pán)格角點(diǎn)為基準(zhǔn)計(jì)算單應(yīng)性矩陣H然后測(cè)量任意兩點(diǎn)在平面上的距離。這個(gè)方案速度更快精度取決于標(biāo)定板到相機(jī)的距離和棋盤(pán)的角點(diǎn)檢測(cè)精度通常能達(dá)到 0.5 毫米以內(nèi)前提是物體高度差不超過(guò) 2 毫米。它的局限是量不了高度量不了圓柱和斜面。驗(yàn)證測(cè)量結(jié)果時(shí)不要只測(cè)一個(gè)標(biāo)準(zhǔn)塊就宣布成功。我建議準(zhǔn)備三組參照物硬幣直徑已知 25 毫米、A4 紙寬 210 毫米和一個(gè)手機(jī)長(zhǎng)度可查。每個(gè)物體測(cè) 10 次記錄每次的像素坐標(biāo)和計(jì)算結(jié)果計(jì)算均值和標(biāo)準(zhǔn)差。如果標(biāo)準(zhǔn)差超過(guò) 1 毫米說(shuō)明圖像采集或者 SGBM 的穩(wěn)定性不夠檢查光線是否變化、物體是否反光、相機(jī)是否用了自動(dòng)對(duì)焦——自動(dòng)對(duì)焦是測(cè)量的大敵必須手動(dòng)鎖焦。另外每次測(cè)量前都先測(cè)一次固定位置的棋盤(pán)格用重投影誤差作為當(dāng)日系統(tǒng)的“健康指標(biāo)”。我最后的習(xí)慣是把每一組測(cè)量數(shù)據(jù)連同當(dāng)時(shí)的視差圖、深度圖一起存成一個(gè)文件夾命名帶上日期和工況。這樣答辯時(shí)老師問(wèn)“這個(gè)結(jié)果怎么來(lái)的”我能直接調(diào)出原始數(shù)據(jù)復(fù)現(xiàn)而不是只給一張截圖。這個(gè)習(xí)慣幫我挽回過(guò)好多次因?yàn)榄h(huán)境光變化導(dǎo)致結(jié)果無(wú)法復(fù)現(xiàn)的局面。雙目測(cè)尺寸這條路真正難的不是算法是你有沒(méi)有把所有會(huì)導(dǎo)致誤差的細(xì)節(jié)都看住。希望這篇筆記能幫你在期末或畢設(shè)里少走幾步彎路。本文還有配套的精品資源點(diǎn)擊獲取