:從相機標定到距離計算完整鏈路)
做視覺測距這個需求十個里有八個是從“檢測到框”開始到“算出距離”結(jié)束。YOLO系列模型發(fā)展到Ultralytics YOLO11檢測精度和推理速度已經(jīng)非常成熟但很多人把模型跑通了才發(fā)現(xiàn)框出來容易距離難算。這篇博文就圍繞Ultralytics YOLO11做距離計算這件事把像素坐標到物理距離的完整鏈路拆開講清楚從相機成像原理、焦距標定到檢測結(jié)果換算、誤差排查給你一條可以直接落地的實現(xiàn)路徑。1. 距離計算的整體思路與方案選型先說結(jié)論YOLO11本身只負責“在圖像里找到目標并畫出邊界框”它輸出的是像素坐標不包含任何深度信息。想從二維像素算出三維物理距離必須在模型之外再搭一套幾何換算邏輯整套系統(tǒng)才算閉環(huán)。1.1 為什么不能直接使用檢測框的像素大小算距離很多人第一反應是“物體在畫面里越小距離越遠”然后試圖用一個線性函數(shù)擬合邊框高度和距離的關(guān)系。這個思路在固定場景、固定相機角度下勉強能用換到別的場景就徹底失效。根本原因在于相機成像是一個透視投影過程物體在圖像上的大小與距離之間是倒數(shù)關(guān)系不是線性關(guān)系。距離10米和距離20米時物體在畫面里縮小的幅度完全不同。真正可靠的做法是走針孔相機模型加相似三角形換算。這個方案的前提是你知道目標的實際物理尺寸比如人的平均身高、車牌的寬度然后利用相機焦距這個內(nèi)部參數(shù)把像素高度換算成物理距離。1.2 三種主流測距方案的取舍方案一單目視覺測距本項目采用的核心方案只用一個普通攝像頭借助幾何模型和已知尺寸先驗來估算距離。優(yōu)點是成本低、部署簡單、算力要求小適合大部分安防和工業(yè)場景。缺點是精度依賴目標尺寸先驗的準確性且無法應對目標遮擋。方案二雙目立體視覺測距通過兩個相機拍攝同一場景利用左右視圖的視差計算深度。精度高不受目標尺寸先驗限制但需要嚴格的相機同步和標定流程硬件成本和計算開銷都翻倍。適合對精度要求極高、場景可控的室內(nèi)環(huán)境。方案三深度相機直接測距用深度傳感器直接獲取每個像素的深度值原理最簡單精度也穩(wěn)定。但深度相機普遍視場角有限戶外強光下性能急劇下降設(shè)備成本也高于普通網(wǎng)絡(luò)攝像頭。綜合來看單目方案在“快速落地驗證 中遠距離估算”這個需求上性價比最高。本項目選它不是因為它能做到毫米級精度而是因為它能在普通攝像頭和YOLO11的基礎(chǔ)上用最小的改動換來可用的距離估計能力。1.3 完整技術(shù)鏈路整個系統(tǒng)分為四個模塊串起來就是一條完整鏈路目標檢測模塊基于Ultralytics YOLO11加載預訓練權(quán)重對視頻幀執(zhí)行推理輸出邊界框、類別和置信度。相機標定模塊計算相機內(nèi)參矩陣和畸變系數(shù)核心是獲取焦距像素值。幾何換算模塊基于三角形相似原理將檢測框高度轉(zhuǎn)換為物理距離。結(jié)果可視化與輸出模塊在幀上疊加距離標注并輸出結(jié)構(gòu)化數(shù)據(jù)。2. 相機成像模型與焦距標定實操測距的精度上限一半取決于相機標定做得好不好。這一節(jié)不講虛的直接給標定思路和關(guān)鍵參數(shù)。2.1 針孔成像模型的核心關(guān)系在針孔模型下相機拍攝目標時物理世界中的目標高度、相機焦距、目標到相機的距離、目標在圖像傳感器上成像的像素高度四者滿足一個極其簡潔的關(guān)系物理高度 / 像素高度 物理距離 / 焦距這個式子整理之后就能得到距離的計算公式距離 目標物理高度 × 焦距 / 目標像素高度這里的焦距不是鏡頭包裝盒上寫的毫米數(shù)而是以像素為單位的等效焦距。這個數(shù)值會隨相機分辨率變化所以在實際項目中必須通過標定來確定不能靠猜也不能直接用傳感器尺寸手工推算因為鏡頭實際視角往往和標稱值有偏差。2.2 基于棋盤格的標定流程棋盤格標定是目前最穩(wěn)定、最常用的方法核心原理是讓相機從不同角度拍攝一個圖案尺寸已知的棋盤格通過檢測角點來求解相機內(nèi)參和畸變系數(shù)。操作步驟如下打印一張A4紙大小的棋盤格建議使用7x10的角點布局格邊長打印后用游標卡尺測量精確值別輕信打印設(shè)置里的理論值。固定好相機手持棋盤格在相機前變換姿態(tài)拍攝15到20張照片。要覆蓋不同距離、不同傾斜角度、畫面四個角落確保棋盤格在每張圖里完整可見。對每張照片用OpenCV的cv2.findChessboardCorners提取角點坐標并配合cv2.cornerSubPix做亞像素級精化。import cv2 import numpy as np CHECKERBOARD (7, 10) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) 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 objp * square_size # square_size為實測格邊長單位米 objpoints [] imgpoints [] images [fcalib_{i}.jpg for i in range(1, 21)] for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHECKERBOARD, None) if ret: corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) fx mtx[0, 0] # x軸等效焦距像素單位 fy mtx[1, 1] # y軸等效焦距標定完成后fx和fy就是換算公式里的焦距。注意在測距公式中水平方向和垂直方向的像素尺寸通常不同具體用哪個焦距取決于你測量目標是靠水平寬度還是垂直高度。2.3 標定實操中的關(guān)鍵注意事項棋盤格不要與相機平面平行要故意傾斜一個角度否則角點匹配會退化標定結(jié)果會虛高。標定照片要覆蓋變焦范圍。定焦鏡頭可以一次標定長期使用變焦鏡頭如果改變了焦距必須重新標定?;冃U龑h距離測距影響巨大。實測中未校正畸變的畫面邊緣誤差能到15%以上靠近畫面中心時誤差相對小。如果只測畫面中心區(qū)域目標可以忽略畸變校正否則務必對每幀先做cv2.undistort再送入檢測模型。3. 基于YOLO11的檢測結(jié)果提取與坐標處理標定拿到焦距后下一步就是讓YOLO11在視頻流或圖片中輸出邊界框并從輸出里提取出測距所需的像素高度和類別信息。3.1 模型加載與推理流程Ultralytics YOLO11的使用已經(jīng)非常工程化幾行代碼就能跑通。以下代碼基于實測穩(wěn)定版本from ultralytics import YOLO model YOLO(yolo11n.pt) # 可以用n/s/m/l/x不同尺寸 results model.predict( sourcecamera_in.mp4, imgsz640, conf0.4, streamTrue, classes[0], # 0對應COCO數(shù)據(jù)集中的person類 verboseFalse, )這里的streamTrue很關(guān)鍵它讓模型對視頻流逐幀處理而不是一次性加載整段視頻能顯著降低內(nèi)存占用。imgsz640是常見的平衡點尺寸越大精度越高但推理延遲也隨之上升邊緣設(shè)備上建議保持640服務端有GPU可以嘗試768或更高。3.2 提取目標的高度與類別信息YOLO11的返回對象中核心信息封裝在result.boxes里。手動遍歷每個檢測框拿到歸一化的坐標和類別import cv2 import numpy as np KNOWN_HEIGHTS { 0: 1.7, # person單位米 2: 1.5, # car典型轎車高度 7: 0.6, # truck按小型貨車估算 } results model.predict(sourcecamera_in.mp4, streamTrue, verboseFalse) for result in results: frame result.orig_img.copy() if result.boxes is None: continue for box in result.boxes: cls int(box.cls[0]) conf float(box.conf[0]) xyxy box.xyxy[0].tolist() y_min, y_max int(xyxy[1]), int(xyxy[3]) pixel_height y_max - y_min if cls in KNOWN_HEIGHTS: real_height KNOWN_HEIGHTS[cls] distance (real_height * fx) / pixel_height cv2.rectangle(frame, (int(xyxy[0]), y_min), (int(xyxy[2]), y_max), (0, 255, 0), 2) label fcls{cls} conf{conf:.2f} dist{distance:.2f}m cv2.putText(frame, label, (int(xyxy[0]), y_min - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)這段代碼會把每個目標的類別、置信度和計算出的距離實時標注在對應框上方。實測下來在普通筆記本CPU上yolo11n模型配合640輸入分辨率幀率能穩(wěn)定在25FPS以上完全能滿足實時預覽。3.3 為什么不直接用邊界框的寬度很多人問為什么不用邊界框的寬度算距離寬度不是也能反映目標大小嗎核心原因是對于直立的人、車輛這類目標檢測框的高度與目標實際高度之間存在相對穩(wěn)定的對應關(guān)系受目標姿態(tài)變化影響較小。而寬度方向一個側(cè)身的人和一個正面的人邊界框?qū)挾炔町悩O大直接算距離誤差會非常離譜。用高度做換算本質(zhì)上是因為高度先驗更穩(wěn)定。3.4 檢測框抖動處理YOLO11單幀檢測會受運動模糊、目標姿態(tài)變化、遮擋等因素影響導致檢測框高度在相鄰幀之間抖動進而讓測距結(jié)果忽遠忽近。處理手段不是改模型而是在后處理環(huán)節(jié)引入平滑算法。我實測最有效的是簡單的一階低通濾波smoothed_distance alpha * current_distance (1 - alpha) * previous_distancealpha取0.3到0.5之間比較合適。太小會讓反應變遲鈍目標急停時距離變化顯示有拖影太又完全濾不掉抖動。另外人的行走速度大約在每秒1.2到1.5米可以根據(jù)這個參考值做目標跟蹤時的速度合理性校驗連續(xù)多幀距離跳變超過這個范圍就可以判定為檢測異常。4. 距離公式的推導與誤差邊界分析看到這里公式已經(jīng)用起來了但你還是要知道這個公式到底在算什么誤差從哪里來這樣在結(jié)果異常時才知道該往哪個方向排查。4.1 三角形相似的完整推導設(shè)相機光心為O感光元件平面在焦距f處目標頂部與底部分別為A和B對應成像點A和B。則三角形OAB與三角形OAB相似于是AB / AB OA / OA其中AB是目標物理高度HAB是成像像素高度h經(jīng)過畸變校正后OA是目標到光心的實際距離D近似為水平距離OA等于焦距f。于是D H × f / h這個推導成立的前提是目標底部與成像平面近似的垂直關(guān)系目標必須直立且相機光軸與地面平行。如果相機俯仰角度過大底部落點與目標實際垂直腳點發(fā)生偏移公式就會系統(tǒng)性地偏向某一側(cè)。4.2 誤差來源的定量分析實際誤差來自三個主要方面目標物理高度先驗誤差如果把人高先驗設(shè)為1.7米而檢測對象實際身高是1.9米那么求出的距離會系統(tǒng)性偏大約11.8%。在實際場景中固定先驗值不可避免會引入誤差這是單目測距的天然局限。檢測框像素高度的量化誤差圖像在y方向的分辨率越高相同物理距離對應的像素高度變化越敏感。以1080P視頻為例一個60米外的人像素高度可能只有幾十像素而此時檢測框上下邊緣差1個像素距離計算值就會變化大約2%。非線性的透視失真相機邊緣區(qū)域畸變顯著即使校正后依然存在殘差。當目標位于畫面邊緣時誤差比中心區(qū)域高出很多。實測結(jié)論是目標靠近畫面中心誤差約5%到8%目標移到畫面邊緣誤差可飆升至15%以上。4.3 影響距離計算精度的核心參數(shù)焦距f對測距結(jié)果的影響是線性的。焦距標定誤差5%測距結(jié)果就會跟著偏差5%。而像素高度h對測距結(jié)果的影響是非線性的目標越遠、像素高度越小同樣的檢測框誤差導致的距離誤差越大。所以在遠距離場景下提高網(wǎng)絡(luò)輸入分辨率比優(yōu)化標定參數(shù)更有性價比。5. 實操過程從YOLO11檢測到距離輸出的完整代碼前面是理論與模塊拆解這里給一套可以直接運行的完整實現(xiàn)。環(huán)境依賴很簡單主要就是Ultralytics、OpenCV和NumPy。5.1 完整代碼框架import cv2 import numpy as np from collections import deque from ultralytics import YOLO # 焦距通過相機標定獲得這里是示例值 FX 980.0 # 目標類別對應的物理高度單位為米 KNOWN_HEIGHTS { 0: 1.7, # person 2: 1.5, # car } # 距離平滑緩存 dist_history deque(maxlen3) def calculate_distance(cls, pixel_height): real_height KNOWN_HEIGHTS.get(cls) if real_height is None: return None return (real_height * FX) / pixel_height def smooth_distance(raw_dist): if abs(raw_dist - dist_history[-1]) 2.0: dist_history.clear() dist_history.append(raw_dist) return float(np.mean(dist_history)) def main(): model YOLO(yolo11n.pt) cap cv2.VideoCapture(0) # 或本地視頻文件路徑 while cap.isOpened(): ret, frame cap.read() if not ret: break # 去畸變處理需要傳入標定得到的dist系數(shù) # frame cv2.undistort(frame, mtx, dist) results model.predict(frame, imgsz640, conf0.4, verboseFalse) for result in results: if result.boxes is None: continue for box in result.boxes: cls int(box.cls[0]) conf float(box.conf[0]) x1, y1, x2, y2 box.xyxy[0].tolist() pixel_height y2 - y1 raw_dist calculate_distance(cls, pixel_height) if raw_dist is None: continue final_dist smooth_distance(raw_dist) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) info f{model.names[cls]} {conf:.2f} D{final_dist:.1f}m cv2.putText(frame, info, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(YOLO11 Distance, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() if __name__ __main__: main()5.2 參數(shù)調(diào)整建議conf建議不低于0.3低于這個值檢測框抖動會很嚴重測距結(jié)果也會跟著劇烈跳動。要識別小目標時可以適當降到0.25但要做好誤差增大的心理準備。輸入分辨率imgsz設(shè)640時人體最小可靠檢測距離約40米超過這個距離檢測框本身就開始不穩(wěn)定。需要更遠測距時把輸入分辨率升到1280代價是推理時間增加大約2.5倍。距離平滑的deque長度設(shè)為3即可太大會造成測距延遲感明顯目標快速移動時距離顯示明顯滯后。5.3 部署時的視頻流接入建議這套系統(tǒng)不只是能處理本地攝像頭實際項目里更常見的是接入RTSP網(wǎng)絡(luò)攝像頭。只需要把cv2.VideoCapture(0)替換成cv2.VideoCapture(rtsp://your_stream_address)。注意網(wǎng)絡(luò)攝像頭存在斷流重連問題穩(wěn)妥的做法是在主循環(huán)里檢測cap.isOpened()為False時自動重建連接避免程序跑一會兒就死掉。6. 常見問題與排查技巧實錄這段時間我前后泡在這個項目里踩了不少坑整理下來大概有五大類基本覆蓋了新手會遇到的所有問題。6.1 距離數(shù)值不穩(wěn)定前后幀跳動劇烈這個問題最常出現(xiàn)。檢查順序依次是檢測框是否抖動可以加跟蹤或平滑、目標是否在畫面邊緣往中心靠、當前幀是否出現(xiàn)運動模糊降低快門或提高幀率。最終手段是加低通濾波把短時跳變削平。6.2 計算距離整體偏大或偏小如果誤差方向一致、幅度穩(wěn)定基本是焦距標定不準或者目標物理高度先驗和實際不符。排查方法是找一個已知距離的位置放置標定物反推當前焦距的實際有效值然后調(diào)整代碼中的FX常數(shù)。千萬別改檢測閾值改閾值只會讓框變多或變少不會修正系統(tǒng)性距離偏差。6.3 目標在畫面邊緣時距離誤差明顯變大這是畸變未有效校正的典型癥狀。解決方法是確保送入模型前對每幀執(zhí)行cv2.undistort。實測1080P圖像全幀去畸變單幀耗時約2到3毫秒對整體幀率影響不大可以放心開啟。6.4 多目標場景下距離標記串位當畫面中有多個人重疊時YOLO11可能會把多個目標合并在一個框里導致輸出的像素高度比真實高度大距離就被算近了。這種情況沒有完美的檢測后處理能拯救較優(yōu)的策略是加載精度更高的模型yolo11m或yolo11l并調(diào)高conf過濾置信度低的重疊框。要對同一行人做穩(wěn)定測距建議在檢測之上再套一層ByteTrack等多目標跟蹤讓每個目標的距離信息隨時間關(guān)聯(lián)起來。6.5 標定時角點檢測失敗通常是棋盤格圖片邊緣被裁切、光照不均勻?qū)е聦Ρ榷忍突蛘咂灞P格反光嚴重。解決辦法是保證棋盤格完整出現(xiàn)在畫面中適當調(diào)整曝光補償避免強光直射打印紙。另外別用太厚的塑封膜反光會讓角點響應質(zhì)量驟減。7. 進階優(yōu)化像素高度修正與參考物等寬法基礎(chǔ)項目跑通之后想讓精度再上一個臺階可以考慮下面幾個優(yōu)化方向改動量都不大但收益明顯。7.1 基于檢測框底邊位置的修正一個容易被忽略的事實是當目標在畫面中的位置不同時同樣的像素高度對應的物理距離并不相同。嚴格修正需要用到標定得到的旋轉(zhuǎn)矩陣和平移向量將檢測框底邊中心點的像素坐標反投影到地面平面。這個方法改動復雜一些但在固定相機場景下能做得很準。簡化版做法先離線采集不同畫面位置與實測距離的對應關(guān)系擬合一個修正系數(shù)再乘到原始距離上。這個方法是我實測性價比最高的精度提升手段。7.2 參考物等寬測距法當目標物理高度不可靠或類別太多時可以換一種思路在場景中放置一個物理尺寸已知的參考標志物用“參考物在畫面里的像素寬度 對比 目標的像素寬度”的比例關(guān)系配合兩者與相機的三角關(guān)系推導距離。這種方法特別適合固定機位的施工區(qū)域或停車場入口精度受目標姿態(tài)影響較小。7.3 多幀置信度融合對靜態(tài)或低速目標可以收集連續(xù)N幀的距離計算結(jié)果剔除明顯離群值后取中位數(shù)。這個方法特別適合閘機、安防門禁這類目標停留時間長的場景能夠把測距誤差穩(wěn)在3%以內(nèi)。實時性要求高的場景慎用延遲會疊加。我個人在實際項目中的感受是視覺測距做到最后拼的不是模型多強而是對誤差來源的理解和對場景約束的把握。YOLO11負責把目標穩(wěn)定找出來剩下的精度全靠相機標定、幾何修正和工程細節(jié)堆出來。這個項目后續(xù)如果要擴展可以考慮往端側(cè)設(shè)備遷移把模型導出為ONNX再轉(zhuǎn)TensorRT或OpenVINO格式在Jetson系列設(shè)備和工業(yè)相機方案上同樣的邏輯可以跑出接近實時的多路測距能力。踩過幾次坑之后最大的體會是任何看似高大上的視覺算法落地時都要回歸到“標定準不準、先驗對不對、場景穩(wěn)不穩(wěn)”這三件事上來。