內(nèi)參標(biāo)定全解析:從針孔模型到重投影誤差)
相機(jī)內(nèi)參標(biāo)定這件事做視覺的人早晚都要碰一次。不管你做的是SLAM、三維重建、工業(yè)測量還是機(jī)器人抓取只要涉及“從圖像反推空間信息”內(nèi)參不準(zhǔn)就是繞不過去的坎。我見過不少朋友拿著一套隨手跑出來的內(nèi)參用了大半年項(xiàng)目出了奇怪問題才回頭查標(biāo)定結(jié)果發(fā)現(xiàn)主點(diǎn)偏移了幾十個(gè)像素——這種坑一旦踩進(jìn)去排查成本非常高。這篇內(nèi)容從相機(jī)成像模型出發(fā)把內(nèi)參標(biāo)定涉及的針孔模型、畸變模型、標(biāo)定實(shí)操全流程都拆開講包括重投影誤差怎么判斷、標(biāo)定結(jié)果不穩(wěn)定怎么排查以及我在實(shí)際項(xiàng)目里反復(fù)踩過的坑和總結(jié)的經(jīng)驗(yàn)。適合剛開始接觸視覺測量、正在搭SLAM或三維重建的開發(fā)者也適合那些標(biāo)定流程跑得很熟但對底層原理說不清的老手重新梳理一遍底層邏輯。1. 內(nèi)參標(biāo)定到底在解決什么問題1.1 標(biāo)定的本質(zhì)建立像素坐標(biāo)與空間方向的對應(yīng)對相機(jī)做內(nèi)參標(biāo)定本質(zhì)上就是在做一件事確定圖像像素坐標(biāo)和相機(jī)坐標(biāo)系下空間方向之間的換算關(guān)系。你可以把相機(jī)想象成一個(gè)“像素映射器”三維世界里的物體通過鏡頭最終落在感光元件上變成一個(gè)個(gè)像素。但這個(gè)映射不是隨意的它由鏡頭的光學(xué)結(jié)構(gòu)、感光元件的物理尺寸和裝配工藝共同決定。內(nèi)參標(biāo)定就是用一個(gè)數(shù)學(xué)模型去擬合這套映射關(guān)系把這套關(guān)系中的全部參數(shù)估計(jì)出來。很多初學(xué)者容易把內(nèi)參和外參搞混。簡單說內(nèi)參描述的是“相機(jī)自己內(nèi)部”的映射屬性跟相機(jī)擺在什么位置、朝哪個(gè)方向無關(guān)外參描述的是相機(jī)坐標(biāo)系相對于某個(gè)世界坐標(biāo)系的姿態(tài)和位置。每次你移動相機(jī)外參都會變但內(nèi)參是不變的前提是焦距、分辨率這些硬件屬性沒動過。平時(shí)標(biāo)定得到的K矩陣和畸變系數(shù)就是內(nèi)參的核心內(nèi)容它們一旦標(biāo)錯后面做空間變換的所有環(huán)節(jié)都會跟著錯。1.2 內(nèi)參不準(zhǔn)會帶來什么后果內(nèi)參不準(zhǔn)的影響在不同應(yīng)用里表現(xiàn)不一樣。在單目測距系統(tǒng)里內(nèi)參偏差會讓測得的距離整體偏移而且這種偏移往往不是線性的——近處誤差小、遠(yuǎn)處誤差大很難通過簡單加個(gè)補(bǔ)償值解決。在三維重建里內(nèi)參不準(zhǔn)會造成重建物體的表面扭曲比如平面變成曲面、直線變成弧線。在視覺定位里內(nèi)參不準(zhǔn)會讓特征點(diǎn)的投影誤差增大導(dǎo)致位姿估計(jì)抖動甚至發(fā)散。一個(gè)典型的例子如果內(nèi)參矩陣中的主點(diǎn)cx, cy標(biāo)錯了相當(dāng)于你用錯誤的幾何中心來反解空間方向那么圖像邊緣的區(qū)域受影響最明顯因?yàn)殡x主點(diǎn)越遠(yuǎn)角度偏差越大。這種誤差在“圖像中心區(qū)域”看起來還行一到視野邊緣就露餡。所以做高精度應(yīng)用時(shí)尤其是要看圖像邊緣區(qū)域的應(yīng)用內(nèi)參標(biāo)定的質(zhì)量會直接決定系統(tǒng)能不能用。這也是為什么很多工業(yè)視覺項(xiàng)目對標(biāo)定流程要求那么嚴(yán)格因?yàn)樗麄円幚淼木褪钦麄€(gè)視野范圍里的測量邊緣精度一點(diǎn)都不能含糊。1.3 什么時(shí)候需要重新標(biāo)定不少人不清楚相機(jī)在什么情況下必須重新標(biāo)定。我的經(jīng)驗(yàn)是動了鏡頭、換了相機(jī)分辨率、調(diào)過對焦環(huán)這三個(gè)動作任何一個(gè)發(fā)生內(nèi)參都可能變化最好重新標(biāo)一遍。尤其是工業(yè)相機(jī)配定焦鏡頭很多人覺得“鏡頭裝上去就固定了”其實(shí)運(yùn)輸震動、溫度變化都可能讓鏡頭和相機(jī)法蘭之間的位置發(fā)生微小改變內(nèi)參也會跟著漂移。所以我一般在項(xiàng)目啟動時(shí)標(biāo)一次設(shè)備搬運(yùn)之后再看一眼如果重投影誤差明顯變大就重標(biāo)。2. 相機(jī)成像模型針孔模型與四坐標(biāo)系變換2.1 針孔模型的核心公式針孔模型是相機(jī)成像最基礎(chǔ)的抽象它假設(shè)光線通過一個(gè)小孔在后面的成像平面上形成倒像。雖然真實(shí)鏡頭用的是透鏡組但針孔模型的數(shù)學(xué)框架依然是理解相機(jī)映射的起點(diǎn)。在不考慮畸變的情況下三維空間點(diǎn)P[X, Y, Z]在相機(jī)坐標(biāo)系下投影到圖像物理平面上的坐標(biāo)是x f * X / Zy f * Y / Z其中Z就是物距f是焦距。這個(gè)公式的本質(zhì)是相似三角形物體離相機(jī)越遠(yuǎn)成像越小焦距越長成像越大。可以拿投影儀做類比——投影儀離墻越遠(yuǎn)畫面越大、亮度越暗這就是同一個(gè)幾何關(guān)系在反向工作。把這個(gè)物理坐標(biāo)換算到像素坐標(biāo)系考慮到像素在x和y方向上的物理尺寸不同dx、dy以及主點(diǎn)的偏移cx、cy就得到u fx * X / Z cxv fy * Y / Z cy其中fx f / dxfy f / dy。寫成齊次坐標(biāo)形式更簡潔λ·[u, v, 1]? K·[X, Y, Z]?這里的K就是內(nèi)參矩陣。2.2 四個(gè)坐標(biāo)系的變換鏈路理解成像模型最怕的就是坐標(biāo)系混亂。我建議畫一條鏈?zhǔn)澜缱鴺?biāo)系3D你定義的基準(zhǔn)→ 相機(jī)坐標(biāo)系3D光心為原點(diǎn)→ 圖像坐標(biāo)系2D物理單位毫米→ 像素坐標(biāo)系2D像素單位世界坐標(biāo)系到相機(jī)坐標(biāo)系由外參[R|t]完成也就是旋轉(zhuǎn)加平移相機(jī)坐標(biāo)系到圖像坐標(biāo)系由焦距f完成這一步是透視投影圖像坐標(biāo)系到像素坐標(biāo)系由像素尺寸和主點(diǎn)完成這一步相當(dāng)于做了一次平移和縮放。這條鏈路在數(shù)學(xué)上可以一次性寫成λ·[u, v, 1]? K·[R|t]·[X, Y, Z, 1]?很多朋友看公式覺得抽象我建議動手拿一個(gè)真實(shí)點(diǎn)算一遍假設(shè)世界坐標(biāo)系下有個(gè)點(diǎn)P[0, 0, 1000]單位毫米相機(jī)正好對著它外參是單位旋轉(zhuǎn)、零平移那么相機(jī)坐標(biāo)也是[0, 0, 1000]代入公式再除以λ就能算出像素坐標(biāo)。手算一次之后四個(gè)坐標(biāo)系之間的關(guān)系就再也不會忘了。2.3 內(nèi)參矩陣K中的每個(gè)參數(shù)逐個(gè)說清內(nèi)參矩陣K [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]fx表示焦距在像素列方向上的度量單位是像素。它的物理意義可以理解為“焦距長度內(nèi)含多少個(gè)像素寬度”。fy同理。fx和fy的比值通常接近1如果相差太遠(yuǎn)說明鏡頭在橫縱方向的放大率不一致或者圖像被做過非等比縮放。這種情況在某些工業(yè)相機(jī)和熱成像相機(jī)里比較常見如果不做處理測量時(shí)會出現(xiàn)橫縱比例失真的問題。cx和cy是主點(diǎn)坐標(biāo)理想情況下應(yīng)該在圖像正中心但實(shí)際裝配中感光元件不可能精確位于鏡頭光軸中心所以主點(diǎn)通常會有幾個(gè)像素到幾十個(gè)像素的偏移。有些朋友標(biāo)定后發(fā)現(xiàn)cx、cy不在中心就慌其實(shí)完全沒必要這正是標(biāo)定要糾正的偏差。只要標(biāo)定本身可靠cx、cy偏離中心幾十像素也是正?,F(xiàn)象。3. 畸變模型成像不聽話的那部分3.1 徑向畸變鏡頭形狀帶來的偏差針孔模型是理想情況真實(shí)鏡頭是透鏡組透鏡的曲面形狀決定了光線并不會精確聚到理想位置這就是畸變的來源。徑向畸變是其中最顯著的一類表現(xiàn)為直線在圖像里變成彎曲的弧線。桶形畸變最典型圖像邊緣的直線向外彎像一個(gè)木桶的輪廓枕形畸變則相反邊緣向內(nèi)收縮。數(shù)學(xué)上徑向畸變可以表示為x_distorted x·(1 k1·r2 k2·r? k3·r?)y_distorted y·(1 k1·r2 k2·r? k3·r?)這里的x、y是歸一化平面坐標(biāo)r2 x2 y2。k1影響畸變的主要量k2、k3用于糾正大視場或魚眼鏡頭的邊緣畸變。普通鏡頭通常只需要k1和k2魚眼類鏡頭才需要考慮k3。你可以這樣理解r的冪次越高這項(xiàng)對遠(yuǎn)離中心的區(qū)域影響越大所以高階項(xiàng)管的就是圖像邊角那一片。3.2 切向畸變裝配誤差帶來的偏差切向畸變比徑向畸變更難直觀理解它是鏡頭與感光元件不完全平行導(dǎo)致的。想象一下鏡頭光軸和圖像傳感器平面之間存在一個(gè)微小的夾角那么圖像看起來就像被“斜著切了一刀”的感覺直線會變成斜線但這和透視關(guān)系無關(guān)。數(shù)學(xué)模型為x_corrected x [2·p1·x·y p2·(r2 2·x2)]y_corrected y [p1·(r2 2·y2) 2·p2·x·y]p1和p2是切向畸變系數(shù)通常數(shù)值很小。一般情況下切向畸變比徑向畸變小一個(gè)數(shù)量級但在高精度標(biāo)定里不能忽略。如果你標(biāo)定出來的p1、p2特別大比如超過0.1就要懷疑是不是硬件裝配出了問題而不是單純把畸變模型擬合完就完事。3.3 畸變校正的完整鏈路畸變校正這個(gè)概念容易把人繞暈我?guī)湍憷硪幌路较?。?biāo)定做的是“從畸變圖像反推理想圖像”的逆過程也就是給定一個(gè)畸變的像素坐標(biāo)尋找它對應(yīng)的理想坐標(biāo)。由于畸變方程是非線性的直接反解需要迭代OpenCV的內(nèi)部實(shí)現(xiàn)就是做這件事。實(shí)際使用中有兩種常用方式一種是直接調(diào)用undistort函數(shù)簡單方便適合離線處理單張圖另一種是用initUndistortRectifyMap先生成映射表mapx、mapy再用remap做查表變換適合視頻流實(shí)時(shí)處理因?yàn)橛成浔碇恍枰?jì)算一次后面每幀就是兩次查表加插值速度非??臁N以趯?shí)時(shí)視覺里程計(jì)項(xiàng)目里用的就是第二種方式在嵌入式平臺上處理1080p圖像也能保持實(shí)時(shí)。4. 標(biāo)定實(shí)操流程從拍照到出參數(shù)4.1 標(biāo)定板的選擇與準(zhǔn)備標(biāo)定板的選擇直接影響標(biāo)定質(zhì)量。棋盤格是最常用的選擇角點(diǎn)檢測算法成熟、子像素定位精度高適合大多數(shù)場景。圓形標(biāo)定板acircles在某些運(yùn)動模糊或大角度拍攝場景下表現(xiàn)更好因?yàn)閳A形質(zhì)心的檢測在透視變形下依然穩(wěn)定但依賴不規(guī)則間距的同心圓圖案檢測魯棒性取決于制造商的設(shè)計(jì)質(zhì)量。打印棋盤格時(shí)三個(gè)細(xì)節(jié)必須注意一是用啞光紙或啞光覆膜避免反光造成角點(diǎn)附近過曝二是把紙貼在一塊平整的硬板上我用過鋁板和亞克力板效果都不錯千萬別直接拿在手里手部微小的抖動都會讓圖片模糊三是確認(rèn)棋盤格圖案的內(nèi)角點(diǎn)數(shù)比如一個(gè)10×7格的棋盤內(nèi)角點(diǎn)數(shù)是9×6這是findChessboardCorners函數(shù)需要的參數(shù)寫錯一個(gè)數(shù)程序就直接報(bào)錯了。另外每個(gè)格子的實(shí)際邊長要量準(zhǔn)我習(xí)慣用游標(biāo)卡尺量多個(gè)格子取平均而不是只量一個(gè)格子再乘以數(shù)量因?yàn)榇蛴C(jī)可能存在累積誤差。4.2 圖像采集的姿勢與數(shù)量采集標(biāo)定圖像是整個(gè)流程中最耗時(shí)、也最影響結(jié)果的環(huán)節(jié)。我的建議是拍攝15到30張左右寧多勿少。角度分布要覆蓋正視、左右傾斜、上下傾斜、旋轉(zhuǎn)讓標(biāo)定板在畫面四個(gè)角落和中心都出現(xiàn)過。拍攝距離也要有變化近處、遠(yuǎn)處各來幾張這樣標(biāo)定出的焦距對不同物距都適用。實(shí)際操作中有幾個(gè)容易忽略的細(xì)節(jié)。第一不要只在一個(gè)固定距離下拍攝那樣標(biāo)定出的參數(shù)只對這個(gè)距離有效換到其他距離誤差會變大。第二標(biāo)定板在畫面中不要太小否則角點(diǎn)檢測不穩(wěn)定一般讓標(biāo)定板占據(jù)圖像的1/3到1/2比較合適。第三光照要均勻避免標(biāo)定板上有高光斑或者陰影覆蓋。第四持機(jī)要穩(wěn)如果手抖導(dǎo)致圖像模糊這組數(shù)據(jù)就直接作廢我會在采集完一批后快速檢查一遍凡是模糊的都刪掉。4.3 OpenCV標(biāo)定代碼與參數(shù)解讀下面這段是OpenCV標(biāo)定的核心流程我用它跑過不少項(xiàng)目整理成一個(gè)可以直接參考的版本import cv2 import numpy as np import glob # 棋盤格內(nèi)角點(diǎn)數(shù)量比如10×7格的棋盤就是 (9, 6) CHECKERBOARD (9, 6) # 每個(gè)格子的實(shí)際邊長單位mm SQUARE_SIZE 30.0 # 角點(diǎn)細(xì)化迭代條件 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) # 世界坐標(biāo)系下的棋盤格角點(diǎn)坐標(biāo) objp np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp * SQUARE_SIZE objpoints [] # 世界坐標(biāo)點(diǎn) imgpoints [] # 圖像角點(diǎn)坐標(biāo) images glob.glob(calib_images/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHECKERBOARD, None) if ret: # 角點(diǎn)細(xì)化到子像素精度這一步非常關(guān)鍵 corners_refined cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria ) objpoints.append(objp) imgpoints.append(corners_refined) # 標(biāo)定主函數(shù) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print(重投影RMS誤差:, ret) print(內(nèi)參矩陣:\n, mtx) print(畸變系數(shù) (k1, k2, p1, p2, k3):\n, dist)cornerSubPix這一步很多人會省掉我建議一定保留它能把角點(diǎn)定位精度從像素級提升到亞像素級對標(biāo)定結(jié)果的穩(wěn)定性影響很大。另外要注意calibrateCamera返回的第一個(gè)值ret是RMS重投影誤差單位是像素不是“準(zhǔn)確率”這個(gè)值后面專門講。標(biāo)定完成后必須做一步驗(yàn)證單獨(dú)計(jì)算每張圖的平均重投影誤差找出誤差特別大的“壞圖”。total_error 0 for i in range(len(objpoints)): imgpoints_proj, _ cv2.projectPoints( objpoints[i], rvecs[i], tvecs[i], mtx, dist ) error cv2.norm(imgpoints[i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj) total_error error print(f第{i1}張圖重投影誤差: {error:.4f} px) print(f平均誤差: {total_error / len(objpoints):.4f} px)如果某張圖的誤差明顯大于整體水平比如其他都是0.2像素它突然到0.8像素那張圖大概率有問題刪掉重新標(biāo)定。4.4 去畸變與映射表生成標(biāo)定拿到參數(shù)后實(shí)際使用中要做去畸變處理。這里我推薦用映射表方式h, w gray.shape[:2] # 計(jì)算最優(yōu)新內(nèi)參矩陣alpha控制摳圖范圍 newcameramtx, roi cv2.getOptimalNewCameraMatrix( mtx, dist, (w, h), 1, (w, h) ) # 生成映射表 mapx, mapy cv2.initUndistortRectifyMap( mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1 ) # 每幀用remap查表 dst cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)alpha參數(shù)值得解釋一下。alpha1表示保留所有原始像素輸出圖像四角會出現(xiàn)黑色區(qū)域alpha0表示裁剪掉黑色區(qū)域同時(shí)會讓圖像稍微放大、損失一部分邊緣視野。實(shí)際項(xiàng)目中如果邊緣區(qū)域也要用alpha設(shè)成0.5左右是個(gè)不錯的折中如果只關(guān)心中心區(qū)域直接alpha0就行。4.5 關(guān)于畸變模型選擇的建議OpenCV默認(rèn)的5參數(shù)模型k1, k2, p1, p2, k3覆蓋了絕大多數(shù)普通鏡頭。但有些場景需要調(diào)整工業(yè)遠(yuǎn)心鏡頭畸變通常極小一兩個(gè)參數(shù)就夠了廣角鏡頭或魚眼鏡頭普通模型擬合不準(zhǔn)確應(yīng)該用OpenCV的fisheye模塊它的模型里包含k1、k2、k3、k4四個(gè)徑向畸變參數(shù)專門為大視場設(shè)計(jì)。在標(biāo)定之前先弄清楚你的鏡頭屬于哪種類型比埋頭調(diào)參數(shù)更重要。5. 常見問題與排查技巧實(shí)錄5.1 重投影誤差到底看什么重投影誤差是標(biāo)定質(zhì)量的直接指標(biāo)但很多人對這個(gè)數(shù)字的理解有偏差。它的含義是把世界坐標(biāo)點(diǎn)通過標(biāo)定出的內(nèi)外參投影到圖像上得到投影點(diǎn)和實(shí)際檢測角點(diǎn)之間的距離差單位是像素。RMS誤差在0.3像素以下說明標(biāo)定質(zhì)量很好0.5像素以內(nèi)大多數(shù)應(yīng)用都能接受超過1像素就說明標(biāo)定數(shù)據(jù)里有問題需要排查。我之前做過一個(gè)工業(yè)測量項(xiàng)目標(biāo)定RMS一直在1.5像素左右怎么調(diào)都下不來。后來把每張圖的誤差打出來發(fā)現(xiàn)有一張圖誤差特別大仔細(xì)一看那張照片標(biāo)定板邊緣有一小塊反光角點(diǎn)檢測被帶偏了。刪掉那張重標(biāo)RMS直接降到0.25。這個(gè)經(jīng)驗(yàn)讓我養(yǎng)成了習(xí)慣標(biāo)定完第一件事不是看RMS而是檢查每張圖的單項(xiàng)誤差分布。5.2 結(jié)果不穩(wěn)定的幾個(gè)典型原因標(biāo)定結(jié)果不穩(wěn)定指的是連續(xù)標(biāo)定幾次內(nèi)參和畸變系數(shù)差異很大。這個(gè)問題常見原因有五個(gè)第一標(biāo)定板不平整。紙貼在彎曲的紙箱上、板子有折痕都會讓空間點(diǎn)坐標(biāo)和實(shí)際不符。第二圖像數(shù)量太少或者角度覆蓋太單一。只拍正面圖畸變參數(shù)約束不足標(biāo)定結(jié)果會漂移。第三標(biāo)定板在圖像里太小角點(diǎn)定位精度下降。第四圖片里有運(yùn)動模糊。這一點(diǎn)在手持標(biāo)定板拍攝時(shí)特別常見尤其是快門速度不夠時(shí)。第五不同批次的圖像分辨率或縮放不一致。如果你從視頻里抽幀標(biāo)定注意別用不同的縮放尺寸混在一起。5.3 快速排查清單我把常遇到的問題整理成一張表方便對照排查現(xiàn)象可能原因排查對策RMS大于1像素部分圖像模糊、反光、角點(diǎn)檢測錯誤剔除單圖誤差大的圖片后重標(biāo)fx和fy差異超過5%圖像被非等比縮放或傳感器/鏡頭異常檢查圖像處理流程確認(rèn)未做拉伸cx、cy偏移非常大鏡頭裝配偏差大或標(biāo)定板只拍了偏移角度確保圖像覆蓋四角和中心去畸變后邊緣仍明顯彎曲畸變模型階數(shù)不足或標(biāo)定板覆蓋不夠增加邊緣區(qū)域的拍攝角度換高階模型連續(xù)標(biāo)定結(jié)果差異大標(biāo)定板不平整、圖像數(shù)量不足、光照不均檢查標(biāo)定板狀態(tài)增加圖像數(shù)量改善光照標(biāo)定結(jié)果在不同距離下測量的精度差采集距離太單一在近、中、遠(yuǎn)多個(gè)距離下采集圖像排查時(shí)我建議每次只改一個(gè)變量。比如先增加圖像數(shù)量看RMS有沒有降再換更平整的標(biāo)定板不要同時(shí)換標(biāo)定板、換光照、換算法不然出了問題你根本不知道是哪一步引起的。6. 最后分享一點(diǎn)個(gè)人體會很多朋友標(biāo)定完了內(nèi)參矩陣和畸變系數(shù)一存就再不管了。我的習(xí)慣是每次標(biāo)定完順便做一次靜態(tài)驗(yàn)證打印一張A4棋盤格放在已知距離的位置用標(biāo)定出的參數(shù)去反測距離看看跟真實(shí)距離差多少。這個(gè)驗(yàn)證成本很低但能幫你建立對整套參數(shù)的信心也能在后續(xù)系統(tǒng)出問題時(shí)快速確認(rèn)標(biāo)定到底有沒有問題。還有一個(gè)小技巧把標(biāo)定的中間結(jié)果——每張圖的角點(diǎn)檢測圖、重投影誤差列表——保存下來放到項(xiàng)目目錄里。這樣過幾個(gè)月回來看如果設(shè)備或算法出了問題你還能知道當(dāng)初采集的圖像是什么樣的重投影誤差是多少排查起來會方便很多。好的標(biāo)定不只是拿一組參數(shù)而是留一條可以追溯的完整記錄。