構(gòu)光三維重建:12步相移與互補格雷碼全流程)
簡介這份資源是一套基于雙目視覺與結(jié)構(gòu)光投影的三維重建完整項目面向計算機視覺學(xué)習(xí)者、課程設(shè)計或科研入門者解決從相機標(biāo)定到點云生成的工程落地問題。項目采用12步相移法與互補格雷碼實現(xiàn)高精度相位解包裹通過雙目標(biāo)定獲取內(nèi)外參數(shù)并完成極線校正再結(jié)合相位匹配與視差計算輸出三維點云模型覆蓋結(jié)構(gòu)光與立體視覺兩條技術(shù)路線。壓縮包共89個文件約104.39MB以66張bmp采集圖像和6張png說明圖為主輔以xml標(biāo)定參數(shù)、py與ipynb源碼腳本、docx與md說明文檔便于對照代碼與數(shù)據(jù)復(fù)現(xiàn)流程。已有149人學(xué)習(xí)下載。讀者可獲得可運行的main.py主流程、explain.ipynb逐步講解、標(biāo)定與相位計算模塊及結(jié)果圖適合按目錄結(jié)構(gòu)逐模塊調(diào)試快速理解相位解包裹、極線校正與視差計算的實現(xiàn)細(xì)節(jié)。1. 雙目結(jié)構(gòu)光三維重建從12步相移到點云落地的完整路徑雙目視覺加結(jié)構(gòu)光投影做三維重建是工業(yè)檢測、逆向工程和文物數(shù)字化里非常成熟的一條路線。它解決的核心問題是單目相機只能拍到紋理拍不到深度純雙目立體匹配在弱紋理或高反光表面會大面積翻車而結(jié)構(gòu)光投影主動往物體表面打編碼條紋讓每個像素都有可解碼的相位信息再配合雙目標(biāo)定得到的極線約束就能把亞毫米級的深度算出來。12步相移法負(fù)責(zé)把相位精度推到極致互補格雷碼負(fù)責(zé)把相位從[-π, π]展開到全局唯一兩者配合是目前高精度面結(jié)構(gòu)光系統(tǒng)里最穩(wěn)的組合之一。這套方案適合做精密零件檢測、葉片型面測量、PCB焊點高度分析的工程師也適合想從零搭一套結(jié)構(gòu)光系統(tǒng)的研究生。整條鏈路的關(guān)鍵不在算法多新而在標(biāo)定、解碼、匹配三個環(huán)節(jié)的誤差控制。2. 雙目標(biāo)定與極線校正把兩個相機變成一臺虛擬相機2.1 為什么標(biāo)定精度直接決定重建上限雙目標(biāo)定的本質(zhì)是求兩個相機各自的內(nèi)參矩陣和兩者之間的外參關(guān)系。內(nèi)參包括焦距、主點、畸變系數(shù)外參是右相機相對左相機的旋轉(zhuǎn)矩陣R和平移向量T。標(biāo)定誤差會以乘性方式傳播到最終點云如果重投影誤差在0.3像素在1米工作距離、5毫米基線條件下深度誤差大約在0.5到1毫米量級。很多人標(biāo)定完只看重投影誤差就收工忽略了極線校正后的殘差結(jié)果后面視差匹配怎么調(diào)都不對。常見做法是用棋盤格或圓點標(biāo)定板采集15到25對圖像覆蓋畫面中心和四角。標(biāo)定板姿態(tài)要多樣傾斜角度至少覆蓋正負(fù)30度否則外參中的旋轉(zhuǎn)分量約束不足。我一般會額外檢查校正后左右圖像的極線對齊程度用一張帶水平紋理的板子拍一張校正后左右圖同一特征點應(yīng)該在同一行偏差超過0.5像素就要重新標(biāo)。2.2 用OpenCV做雙目標(biāo)定與校正的完整命令import cv2 import numpy as np import glob # 棋盤格參數(shù)內(nèi)角點數(shù)量根據(jù)實際標(biāo)定板修改 chessboard_size (11, 8) square_size 3.0 # 毫米 # 生成棋盤格世界坐標(biāo) objp np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp * square_size objpoints [] imgpoints_left [] imgpoints_right [] left_images sorted(glob.glob(calib/left/*.png)) right_images sorted(glob.glob(calib/right/*.png)) for lpath, rpath in zip(left_images, right_images): img_l cv2.imread(lpath, cv2.IMREAD_GRAYSCALE) img_r cv2.imread(rpath, cv2.IMREAD_GRAYSCALE) ret_l, corners_l cv2.findChessboardCorners(img_l, chessboard_size, None) ret_r, corners_r cv2.findChessboardCorners(img_r, chessboard_size, None) if ret_l and ret_r: criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_l cv2.cornerSubPix(img_l, corners_l, (5, 5), (-1, -1), criteria) corners_r cv2.cornerSubPix(img_r, corners_r, (5, 5), (-1, -1), criteria) objpoints.append(objp) imgpoints_left.append(corners_l) imgpoints_right.append(corners_r) # 單目標(biāo)定獲取初始內(nèi)參 ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera(objpoints, imgpoints_left, img_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera(objpoints, imgpoints_right, img_r.shape[::-1], None, None) # 雙目標(biāo)定 flags cv2.CALIB_FIX_INTRINSIC criteria_stereo (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_l, dist_l, mtx_r, dist_r, img_l.shape[::-1], criteriacriteria_stereo, flagsflags ) # 極線校正 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, img_l.shape[::-1], R, T, alpha0, # alpha0裁剪黑邊alpha1保留全部像素 newImageSizeimg_l.shape[::-1] ) # 保存映射表用于后續(xù)實時校正 map1_l, map2_l cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, img_l.shape[::-1], cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, img_r.shape[::-1], cv2.CV_32FC1) np.savez(stereo_params.npz, mtx_lmtx_l, dist_ldist_l, mtx_rmtx_r, dist_rdist_r, RR, TT, R1R1, R2R2, P1P1, P2P2, QQ, map1_lmap1_l, map2_lmap2_l, map1_rmap1_r, map2_rmap2_r)這段代碼先做單目標(biāo)定拿到初始內(nèi)參再用CALIB_FIX_INTRINSIC固定內(nèi)參只優(yōu)化外參避免雙目標(biāo)定在圖像對不足時把內(nèi)參也帶偏。alpha0會裁掉校正后的無效區(qū)域適合后續(xù)做稠密匹配如果要做全視場重建就設(shè)alpha1但邊緣會有黑邊需要掩膜。Q矩陣是后面從視差圖轉(zhuǎn)三維點云的關(guān)鍵必須保存。2.3 校正效果怎么驗證校正完不要直接往下走先做兩件事。第一用cv2.remap把左右原圖校正疊加顯示看同一物理點在左右圖中是否在同一水平線。第二用cv2.stereoRectify返回的roi1和roi2裁剪有效區(qū)域記錄裁剪后的圖像尺寸和主點偏移后面視差轉(zhuǎn)深度時要用到。如果校正后極線偏差大于1像素檢查標(biāo)定板圖像是否有運動模糊或者標(biāo)定板平面度是否夠。3. 12步相移與互補格雷碼把相位解包裹做扎實3.1 相移法為什么選12步而不是4步相移法的基本原理是投影N幅相位間隔2π/N的正弦條紋通過N幅圖的灰度值解出包裹相位。4步相移的相位誤差對灰度噪聲敏感信噪比低的時候相位波動能到0.1弧度以上。12步相移把相位誤差降到4步的約三分之一代價是采集時間變成3倍。在靜態(tài)測量場景下這個代價完全值得。12步的相位解算公式是φ atan2( Σ I_k · sin(2πk/12), Σ I_k · cos(2πk/12) )其中I_k是第k幅圖的灰度值。這個公式對二次諧波有很好的抑制投影儀非線性帶來的誤差會明顯小于4步法。3.2 互補格雷碼怎么編解碼格雷碼負(fù)責(zé)把包裹相位展開成全局相位。普通二進(jìn)制碼在碼字邊界處容易因為噪聲產(chǎn)生跳變錯誤格雷碼相鄰碼字只差一位邊界容錯更好?;パa格雷碼是同時投影正碼和反碼解碼時用正反碼的差值做閾值判斷能進(jìn)一步壓低誤碼率。具體做法對投影儀的每一列根據(jù)條紋周期數(shù)分配格雷碼字。假設(shè)用8位格雷碼可以區(qū)分256個周期。每個周期對應(yīng)包裹相位從-π到π。解碼時先解出包裹相位再根據(jù)格雷碼值確定周期序號k全局相位Φ φ 2πk。import numpy as np def decode_phase(images): images: 12幅相移圖shape(12, H, W) H, W images.shape[1], images.shape[2] I images.astype(np.float64) sin_sum np.zeros((H, W)) cos_sum np.zeros((H, W)) for k in range(12): angle 2 * np.pi * k / 12 sin_sum I[k] * np.sin(angle) cos_sum I[k] * np.cos(angle) phase np.arctan2(sin_sum, cos_sum) # 包裹相位范圍[-pi, pi] return phase def decode_graycode(gray_images, threshold0.5): gray_images: 正反格雷碼圖shape(2*N, H, W)前N張正碼后N張反碼 N len(gray_images) // 2 H, W gray_images.shape[1], gray_images.shape[2] code np.zeros((H, W), dtypenp.int32) for i in range(N): pos gray_images[i].astype(np.float64) neg gray_images[i N].astype(np.float64) # 正反碼差值判斷比單閾值更穩(wěn) bit (pos - neg) 0 code (code 1) | bit.astype(np.int32) # 格雷碼轉(zhuǎn)二進(jìn)制 binary code.copy() for shift in range(1, N): binary ^ (code shift) return binary def unwrap_phase(wrapped_phase, binary_code, periods256): 全局相位 包裹相位 2*pi*k k binary_code.astype(np.float64) / periods global_phase wrapped_phase 2 * np.pi * k return global_phasedecode_phase里用atan2解包裹相位注意sin_sum和cos_sum同時接近零的區(qū)域是無效點要加掩膜。decode_graycode用正反碼差值做判斷比直接對單幅圖取閾值抗噪能力強很多。unwrap_phase里的periods要和投影條紋總周期數(shù)一致這個參數(shù)設(shè)錯會導(dǎo)致全局相位整體偏移點云直接變形。3.3 相位解包裹的邊界處理格雷碼解碼在物體邊緣和陰影區(qū)域容易出錯表現(xiàn)為相位跳變或噪點。常見做法是加一個置信度掩膜正反碼差值絕對值小于某個閾值的像素標(biāo)記為不可靠不參與后續(xù)匹配。另外格雷碼的碼字邊界和相移條紋的周期邊界要對齊否則在周期切換處會出現(xiàn)半周期錯位。我一般會在投影圖案生成階段就把兩者的相位零點對齊省去后面調(diào)。4. 相位匹配與視差計算從全局相位到三維點云4.1 相位匹配為什么比灰度匹配穩(wěn)傳統(tǒng)雙目立體匹配用灰度或梯度做代價在弱紋理區(qū)域基本失效。結(jié)構(gòu)光把每個像素編碼成全局相位值左右圖中同一物點的全局相位理論上相等匹配就變成在極線上找相位最接近的點。這個約束比灰度強得多因為相位是連續(xù)量對光照變化和表面顏色不敏感。匹配策略一般有兩種一是對左圖每個像素在右圖極線上搜索相位差最小的點二是先做粗匹配用格雷碼碼字定位周期再在周期內(nèi)用包裹相位精定位。第二種效率高很多因為搜索范圍從整條極線縮小到一個周期內(nèi)。4.2 視差轉(zhuǎn)三維坐標(biāo)的完整實現(xiàn)import numpy as np import cv2 def phase_to_disparity(global_phase_left, global_phase_right, mask): 基于全局相位的視差計算 H, W global_phase_left.shape disparity np.zeros((H, W), dtypenp.float32) for y in range(H): valid_x np.where(mask[y])[0] if len(valid_x) 0: continue phase_row global_phase_right[y] for x in valid_x: target global_phase_left[y, x] # 在右圖同一行搜索相位最接近的點 diff np.abs(phase_row - target) diff[~mask[y]] np.inf best_x np.argmin(diff) if diff[best_x] 0.1: # 相位差閾值 disparity[y, x] x - best_x return disparity def disparity_to_pointcloud(disparity, Q): 用Q矩陣把視差圖轉(zhuǎn)成三維點云 points_3d cv2.reprojectImageTo3D(disparity, Q) mask disparity 0 cloud points_3d[mask] colors None # 如果有紋理圖可以在這里取顏色 return cloud, maskphase_to_disparity里逐行搜索相位最接近的點0.1弧度的閾值是經(jīng)驗值相位噪聲大的系統(tǒng)可以放寬到0.2。disparity_to_pointcloud直接用OpenCV的reprojectImageTo3D它內(nèi)部用Q矩陣做齊次變換。注意視差圖里無效區(qū)域要置零或負(fù)值否則會生成飛點。4.3 點云后處理濾波與平滑原始點云一定有飛點和噪聲。常見做法是先用統(tǒng)計濾波去掉離群點再用半徑濾波平滑。PCL里對應(yīng)StatisticalOutlierRemoval和RadiusOutlierRemoval。如果做精密測量還要做平面擬合和基準(zhǔn)對齊把點云轉(zhuǎn)到工件坐標(biāo)系。這一步的精度直接決定最終測量結(jié)果不要跳過。5. 避坑與排查結(jié)構(gòu)光三維重建最常見的5個翻車點5.1 標(biāo)定重投影誤差很小但重建出來是彎的現(xiàn)象標(biāo)定報告重投影誤差0.2像素但重建一個標(biāo)準(zhǔn)球點云明顯橢化。原因通常是標(biāo)定板圖像覆蓋范圍不夠外參中的旋轉(zhuǎn)分量約束不足或者標(biāo)定板本身平面度差。解決重新采集標(biāo)定圖確保標(biāo)定板出現(xiàn)在畫面中心和四個角傾斜角度覆蓋正負(fù)30度以上換一塊高精度陶瓷標(biāo)定板。5.2 相位解包裹在物體邊緣出現(xiàn)大面積跳變現(xiàn)象格雷碼解碼后物體輪廓附近全局相位出現(xiàn)2π跳變點云在邊緣撕裂。原因是格雷碼碼字邊界和相移周期邊界沒對齊或者邊緣處正反碼差值太小導(dǎo)致誤判。解決在投影圖案生成時把格雷碼跳變沿對齊到相移條紋的相位零點解碼時對正反碼差值小于閾值的像素加掩膜。5.3 視差圖在弱紋理區(qū)域出現(xiàn)空洞現(xiàn)象白色陶瓷或金屬表面相位匹配找不到對應(yīng)點視差圖大片空洞。原因是這些表面反射率高相移圖過曝相位解算失效。解決降低投影亮度或縮短曝光時間讓相移圖灰度落在相機線性響應(yīng)區(qū)間也可以噴顯影劑或啞光粉但會引入厚度誤差。5.4 點云整體偏移或尺度不對現(xiàn)象重建的點云和實際工件尺寸差一個固定比例或者整體平移。原因是Q矩陣用的校正后主點和圖像尺寸不匹配或者標(biāo)定板方格尺寸設(shè)錯。解決檢查stereoRectify返回的roi是否被正確應(yīng)用確認(rèn)標(biāo)定板實際方格尺寸和代碼里square_size一致重新生成Q矩陣。5.5 12步相移采集時間太長導(dǎo)致運動模糊現(xiàn)象靜態(tài)測量沒問題但產(chǎn)線上工件有微小振動重建結(jié)果出現(xiàn)條紋狀噪聲。原因是12步相移需要采集12幅圖總曝光時間乘以12。解決用全局快門相機縮短單幅曝光時間提高投影亮度補償或者改用6步相移加互補格雷碼犧牲一點相位精度換速度。6. 進(jìn)階技巧用多頻外差和GPU加速把精度和速度同時拉滿12步相移加互補格雷碼已經(jīng)能覆蓋大部分高精度靜態(tài)測量場景但如果要兼顧速度和精度有兩個方向值得投入。第一個是多頻外差投影三種不同頻率的條紋用外差原理逐級展開相位可以只用較少步數(shù)達(dá)到和12步接近的精度。常見組合是3頻3步總共9幅圖比12步少3幅相位精度損失在可接受范圍內(nèi)。第二個是GPU加速相位解算和視差匹配都是逐像素操作天然適合并行。用CUDA或OpenCL把decode_phase和phase_to_disparity重寫在1080p分辨率下能把處理時間從秒級壓到毫秒級。我自己在產(chǎn)線項目里的習(xí)慣是先用12步相移把精度基準(zhǔn)打出來確認(rèn)整個鏈路誤差在指標(biāo)內(nèi)再逐步減步數(shù)或換多頻方案做加速。每次改完都要用同一個標(biāo)準(zhǔn)件復(fù)測看點云偏差是否還在公差帶內(nèi)。標(biāo)定參數(shù)和Q矩陣一定要版本化管理換鏡頭或動相機之后必須重新標(biāo)定不要偷懶復(fù)用舊參數(shù)。這套系統(tǒng)沒有黑匣子每個環(huán)節(jié)的誤差都能追溯到具體參數(shù)把標(biāo)定、解碼、匹配三個環(huán)節(jié)的邊界條件摸清楚后面就是調(diào)參和驗證的體力活。希望幫到你。本文還有配套的精品資源點擊獲取