双目相机标定实战:USB摄像头做实时人脸测距的关键一步

发布时间:2026/10/16 5:08:56
双目相机标定实战:USB摄像头做实时人脸测距的关键一步
做双目测距最容易被低估的坑是很多人直接套一行z f * b / d然后发现测出来的距离完全不对。这里大概率不是公式错了而是公式里的f、b、d你一个都没有真正拿到手。f不是标称的镜头焦距而是经过畸变和主点偏移之后的等效焦距b是两个相机光心之间的真实平移量必须通过标定估计d是左右图像上同一个物理点的视差而这要求两张图在数学上严格行对齐行对齐又依赖极线校正。这一串问题全都要从双目相机标定开始解决。这个系列的目标很明确用两个普通USB摄像头实现对真实人脸距离的实时检测。上一篇我们已经把双路USB摄像头的采集通路跑通现在进入第二篇专门解决双目相机标定。这篇做完之后你能得到两个摄像头各自的内参、畸变系数以及两相机之间的旋转平移关系后续再做立体校正、视差计算、人脸测距就是水到渠成的事。1. 为什么做双目测距必须先标定1.1 测距公式背后藏着两个“暗坑”先还原一下双目测距的基本模型。左右两个相机平行放置光心距离为基线b两个相机焦距同为f。空间里的点P被左右相机分别成像在左图上落在x_l在右图上落在x_r视差d x_l - x_r。根据相似三角形深度满足z f * b / d这里面有三个量任何一个是错的测距结果就废了。现实中的USB摄像头镜头不是完美透镜传感器也不是绝对平面f必须通过标定得到。更关键的是左右相机的内参几乎不可能完全一致两个镜头的光轴也不可能理想平行。b也不是简单量一量两个USB外壳之间的距离就能确定的光心实际位置可能在镜头后方的某个点并且你很难从外壳尺寸推断光心坐标。所以双目相机标定要解决的问题其实就是把每个相机的“内在底数”和两个相机之间的“相对关系”全部精确算出来。另一个暗坑是视差。视差要求左右相机对空间点的观测存在同名点匹配而匹配的前提是图像已经完成极线校正。校正之前同名点可能在左图某个位置在右图另一条斜线上直接用原始图像搜索会产生很多误匹配。标定这一步不做扎实后面的立体匹配基本就是在错误的地基上盖楼。1.2 双目标定到底标定什么双目标定输出的东西可以分成两层。第一层是两个相机的单目标定结果。对于每个相机我们要拿到相机内参矩阵K和畸变系数向量D。内参矩阵大概长这样K [fx 0 cx 0 fy cy 0 0 1]其中fx、fy是分别在 x、y 方向上的等效焦距单位是像素cx、cy是主点坐标也就是光轴穿过成像平面的位置。畸变系数通常包括径向畸变k1 k2 k3和切向畸变p1 p2。这些都是单目学里面的内容但在双目标定里不能各自独立完事还需要第二层。第二层是双相机之间的外参也就是右相机相对于左相机的旋转矩阵R和平移向量T。这组参数决定了两个相机光轴之间的角度差和光心之间的真实位移。有了R、T才能把左右两幅图像校正成共面且行对齐的状态。所以双目标定不是简单“各标一次再拼起来”它要在一个公共世界坐标系下把两套相机模型统一起来。1.3 USB摄像头比工业相机更吃标定如果手里是工业相机镜头是C口手动定焦光圈固定标定一次之后长期使用基本没问题。但用USB摄像头做双目问题就来了很多USB摄像头带自动曝光、自动白平衡、自动对焦甚至还有宽动态、降噪这些后处理功能。自动对焦会让焦距实时变化等于内参矩阵里的fx一直漂移标定结果一拿到手就过期。自动曝光会造成同一场景下左右两幅图的亮度差异很大影响后续匹配。所以我的建议是硬件到手之后先做两件事。第一关闭一切能关的自动功能把曝光、白平衡、增益设置成固定值或者至少保证左右镜头在同样的环境光下工作。第二如果条件允许把镜头用胶固定在对焦位置防止晃动。有些人会在代码里每次启动摄像头时设置CAP_PROP_AUTO_EXPOSURE 0这已经是标配操作。没有这一步标定结果再好都白搭。2. 动手前的准备2.1 硬件怎么固定更合理双目相机的目标用途是测人脸距离测量范围一般在0.5米到3米之间。这时候基线长度不要拍脑袋定。基线太短比如只有2厘米近距离精度可能还行但到了1米之外视差极小距离稍微一换算就是巨大误差基线太长左右视图的公共视野会变小稍远一点的目标可能只出现在一侧画面里。人脸测距这种场景基线放在6到12厘米之间比较合理手感类似人眼间距可以兼顾近距离和中等距离。固定方式上两个相机必须保证相对位置固定不能每次上电都变。我用过很多种固定方案最稳的是把两个相机粘在一条铝合金型材上或者用两个滑块锁死在一个底座上。关键是底座刚性要足够不要用那种会弯的塑料支架。即使相机固定住光心之间的平移也不会自动等于外壳中心距这部分未知量就交给标定去估计。另外两个相机的朝向不要差太多尽量保持平行否则标定出的旋转矩阵会非常大校正之后图像边缘的裁切也会很厉害。2.2 标定板怎么选怎么用标定板是双目标定里最直接的参照物。不需要买很贵的玻璃板用A4纸打印一张棋盘格贴在硬纸板或亚克力板上就可以。关键是尺寸要准确并且打印之后不要折皱。OpenCV里用的棋格角点数是内部角点数比如棋盘是 9x6 格内部角点就是 8x5。标定板的物理格子边长比如square_size 0.024米角点数量chessboard_size (8, 5)这两个值必须一致地传给后面的代码。有人会问标定板是大一点好还是小一点好。标定板太小远了看不清角点近了又只能覆盖镜头的局部视野标定出的畸变参数不全面。建议A4纸上打印8x6或9x6个格子尺寸在20到30厘米见方最合适。格子边长必须统一我用卡尺量过手残裁剪不均匀会引入误差最省心的做法是去广告打印店用激光打印纸张平整度更好。只要板子不弯、图案不变形、格子尺寸准确标定的基础就稳了。2.3 采集标定图的正确姿势采集这一步直接影响标定精度新手最容易在这里翻车。常见错误是拿着标定板在相机前同一个位置转来转去画面里板子只是角度稍微变化覆盖的视野范围极窄这样标定出来的畸变是不可信的。正确做法是让左右相机同时拍摄棋盘格每拍一组就把标定板移动到视野内的不同位置保持板子相对于相机有不同距离、不同姿态。要覆盖四角、边缘和中心区域距离从很近到较远都要有。总组数建议至少15到20组如果画面分辨率高30组更好。拍摄过程中棋盘格要尽量完全可见不要让一条边出画面。对带自动对焦的USB摄像头标定时一定要把对焦调到固定值然后保持标定板距镜头的距离不超过或低于那个对焦工作距离。否则一旦失焦角点检测就不稳定标定误差会直接上升。另外左右图像必须同步采集。如果两个摄像头没有硬件同步哪怕只是几毫秒的时间差标定板移动中也会造成左右棋盘格姿态不同立体标定就会算出一组错误的R、T。稳妥的做法是把标定板放在静止的桌面上移动时先停稳再采集或者用软触发同时读取两路视频帧。实际项目中我宁可用慢速移动并在画面中人工挑选清晰帧也不会让板子一直抖着录。3. 双目标定的完整流程3.1 单目标定双目标定不是直接跳进stereoCalibrate就完事。正规做法是先分别对左右相机做单目标定得到各自的初始内参和畸变系数然后再做立体标定在公共约束下优化外参。单目标定的流程是对每一张标定图检测棋盘格角点然后根据棋格的物理尺寸生成对应的世界坐标。假设棋盘格平面是z0它的世界坐标点可以认为是(0,0,0), (square_size,0,0), (2*square_size,0,0), ...每一个角点在图像上有一个像素坐标在同一张图里又对应一个世界坐标多张图形成多组对应关系调用cv2.calibrateCamera就能解出内参和畸变系数。这一步输出的重投影误差是一个很重要的质量指标。如果单目标定的重投影误差都超过1个像素说明角点检测或者图片质量有问题后面双目标定也不会好到哪去。左右两台相机都要单独做这一步然后得到K1, D1, K2, D2。很多人会在这里直接把这些参数当成最终结果但严格来说这些只是初始值。因为双目标定要求左右内参在同一个立体约束下达到最优尤其是两个相机的相对姿态未知时误差会在立体匹配里被放大所以还需要下一步。3.2 立体标定立体标定的核心是估计两个相机之间的R和T。OpenCV里的cv2.stereoCalibrate接收的是左右相机在各自图像中检测到的同一组棋盘格角点以及这些角点在棋盘平面坐标系中的世界坐标。在调用时可以指定flags让它在优化外参的同时是否允许调整内参。工程里常见做法是先做完整单目标定得到K1 D1 K2 D2然后立体标定时使用CALIB_FIX_INTRINSIC只求解外参。这样做速度最快结果稳定因为内参已经在足够多的样本上优化过了。如果认为单目标定的内参不够准可以在立体标定时去掉CALIB_FIX_INTRINSIC让stereoCalibrate去微调所有参数但这会大幅增加优化参数的数量稍不注意就会陷入局部最优。实操时我倾向于采集了很多高质量标定图的前提下使用CALIB_USE_INTRINSIC_GUESS让算法在外参求解时对两个相机的内参做轻微调整效果通常比完全固定好一点。立体标定输出的R是右相机到左相机的旋转矩阵T是右相机光心在左相机坐标系下的平移向量。到这里我们已经拥有了一套完整的相机相对位姿数据。下一步就可以做立体校正。3.3 立体校正与质量验证立体校正的目标是把左右相机坐标系下的图像重投影到同一个虚拟平面上让左右两幅图变成前排共面且行对齐。OpenCV 的cv2.stereoRectify会读入单目标定结果和双目标定的R, T输出两个新的校正旋转矩阵R1, R2两个新的投影矩阵P1, P2以及重投影矩阵Q。Q矩阵在OpenCV的双目模式里承担了深度恢复的任务。给出左右图的同名点像素坐标(x, y)和视差d通过Q矩阵可以直接换算出该点的三维坐标。校正之后要立即验证左右图是否真正行对齐。最简单的验证方法是把左右校正后的图像横向拼在一起然后在图像上画一条水平线找到同一个物理点看它的投影是否落在同一条水平线上。如果左右两图同名点之间有明显的高度差说明标定和校正流程中有一处参数不对要先回头排查。我用这个验证方法抓过很多次问题。有时候stereoCalibrate标定出的R和T方向反了或者左右图的传入顺序乱了校正后行不对齐视差计算出来的深度全是负的。这里强烈建议在标定完成后把左右校正结果可视化一遍不要直接跳到深度图。否则错误会被后续一连串操作掩盖排查起来非常头疼。4. 用OpenCV实现核心流程4.1 角点检测OpenCV中检测棋盘格角点用cv2.findChessboardCorners。假设棋盘格是8x6个格子内部角点是(7,5)。下面是读取左右图像并检测角点的核心逻辑import numpy as np import cv2 chessboard_size (7, 5) # 内部角点数宽7高5 square_size 0.024 # 格子边长24mm criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp np.zeros((np.prod(chessboard_size), 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp * square_size obj_points [] # 世界坐标 left_points [] # 左图角点 right_points [] # 右图角点 for left_img, right_img in zip(left_images, right_images): left_gray cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY) right_gray cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY) ret_l, corners_l cv2.findChessboardCorners(left_gray, chessboard_size, None) ret_r, corners_r cv2.findChessboardCorners(right_gray, chessboard_size, None) if ret_l and ret_r: corners_l cv2.cornerSubPix(left_gray, corners_l, (5, 5), (-1, -1), criteria) corners_r cv2.cornerSubPix(right_gray, corners_r, (5, 5), (-1, -1), criteria) obj_points.append(objp) left_points.append(corners_l) right_points.append(corners_r)注意findChessboardCorners返回的角点坐标类型必须转成float32调用cornerSubPix之前要传入灰度图。这一步看似简单却很容易出错。如果标定板在图像里占比太小或者板子上有反光检测结果会出现跳变该剔除的图像组一定要剔除不要留着当脏数据。4.2 内外参数估计有了左右两组角点先做单目标定h, w left_gray.shape ret_l, K1, D1, rvecs_l, tvecs_l cv2.calibrateCamera(obj_points, left_points, (w, h), None, None) ret_r, K2, D2, rvecs_r, tvecs_r cv2.calibrateCamera(obj_points, right_points, (w, h), None, None)ret_l和ret_r是重投影误差单位是像素。新手上路统一要求低于0.5到0.8左右如果到1.0以上不要往下走先排查采集图片质量。然后做立体标定flags cv2.CALIB_USE_INTRINSIC_GUESS ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( obj_points, left_points, right_points, K1, D1, K2, D2, (w, h), criteriacriteria, flagsflags )立体标定之后K1 D1 K2 D2可能被微调过所以后续要用的是立体标定输出的这套内参而不是单目标定的结果。这里最关键的输出是R和T。R是右相机相对左相机的旋转矩阵T是右相机光心在左相机坐标系下的平移向量。你可以打印出来看看如果平移向量的量级和真实基线的量级差很远比如你预估基线10厘米结果T的模算出来只有0.001米那大概率是标定板格子边长单位写错了。4.3 立体校正参数生成标定完R和T之后用cv2.stereoRectify生成校正映射R1, R2, P1, P2, Q, validPixROI1, validPixROI2 cv2.stereoRectify( K1, D1, K2, D2, (w, h), R, T, alpha0 )alpha这个参数值得多说两句。alpha0意味着校正后会裁掉边缘不可用的区域图像损失最多但后续计算视差的有效区域非常干净alpha1表示保留全部原始像素图像完整但边缘会有黑边立体匹配时这些没用的区域还会干扰视差图。我一般先用alpha0验证算法流程确认一切正常后再看是否需要增大alpha保留更多视野。实际做人脸测距时目标集中在画面中部alpha0带来的视野损失往往可以接受。接着生成remap需要的映射表map1_l, map2_l cv2.initUndistortRectifyMap(K1, D1, R1, P1, (w, h), cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap(K2, D2, R2, P2, (w, h), cv2.CV_32FC1)以后每来一帧左右图都执行left_rect cv2.remap(left_img, map1_l, map2_l, cv2.INTER_LINEAR) right_rect cv2.remap(right_img, map1_r, map2_r, cv2.INTER_LINEAR)这基本上就是一个可复用的标定与校正全景。只要硬件固定位置不变这套映射表可以长时间使用不需要每次启动都重新标定。5. 标定结果怎么用5.1 重投影误差重投影误差是标定质量最直观的指标。单目标定的ret_l、ret_r以及立体标定返回的ret都代表三维世界点投影到图像平面后与实际检测角点之间的平均像素偏差。如果这个偏差小于0.3像素说明标定结果非常好小于0.7像素基本可用超过1.0像素深度误差会被放大得很厉害。但要注意重投影误差小不代表一切都没问题。它只说明在当前这些标定图片上模型拟合得不错。如果采集图片只覆盖画面中心附近那么边缘畸变没有被充分约束重投影误差也可能很低可一旦真实场景里物体跑到画面边缘校正结果就不准。所以误差指标要和采集覆盖度一起看缺一不可。5.2 校正后的图像校正后的图像质量需要人眼判断。把左右校正图拼在一起并排显示重点看桌面边缘、墙面直线等结构在跨左右图时是否保持水平。如果看到一条本来很直的桌面边缘在左图朝下弯、在右图还朝上弯那么立体校正基本失败。这通常不是因为代码写错而是标定板图片组数不够或者棋盘格物理尺寸和实际打印尺寸不一致。还有一种情况校正后左右图边缘出现大面积黑区说明两个相机相对姿态差异很大或者基线段旋转过大。这时候检查R矩阵的旋转角如果转角超过5度说明两个镜头朝向角度差太多最好重新调整物理固定方式让两镜头尽量平行而不是硬靠标定去补偿。5.3 输出 Q 矩阵Q矩阵是双目深度恢复的桥梁。OpenCV经典的深度公式是[ x, y, z, w ] Q * [ x_l, y_l, disparity, 1 ] Z z / w把左右校正图中匹配到的同名点代入左图坐标(x_l, y_l)和视差disparity即可算出三维坐标。这里很多人会搞混数据顺序因为Q矩阵里的符号排布和左手、右手坐标系相关。OpenCV默认双目输出的是右相机校正坐标系下的三维坐标实际使用中我建议先拿一组已知距离的目标做一次验证。比如在1米处放一个标定板算出来的Z应该在1米左右偏差超过5%就要先检查标定和校正流程。有了校正映射和Q矩阵后续接立体匹配算法比如cv2.StereoSGBM_create得到的视差图经过cv2.reprojectImageTo3D就能得到整张深度图。面对人脸测距这个场景还可以进一步结合人脸检测框取框内有效视差的中值或均值把对应位置的三维坐标提取出来距离值自然就有了。6. 常见问题与排查技巧实录6.1 角点检测不稳定findChessboardCorners明明对着标准棋盘格却总是检测不到角点。这是新手最常见的问题。通常有几种原因。第一标定板纹理过于复杂或者格子比例不标准棋盘格必须是黑色和白色交替的矩形格子比例偏差太大会导致角点特征不明显。第二图像里面有反光亮点覆盖了角点区域拍出来的图像部分过曝可以换哑光打印纸或调整光源角度。第三棋盘格在图像中的尺寸过小角点间距只有几个像素解决方法是把标定板拿近一点或者换更高分辨率的UVC模式。如果检测结果时好时坏要写一个可视化预览把检测到的角点画出来逐帧确认。千万不能闭着眼睛批量跑完整个数据集进去一堆坏数据出来一堆好参数这是不对劲的。6.2 标定误差大标定误差大的时候可以先检查一个容易被忽略的地方square_size的单位。OpenCV里世界坐标通常用米如果你把24mm写成0.024那没问题但如果你写成0.24那T的单位会整体扩大十倍深度算出来直接乱了。建议打印T的模和实际测量的两台相机光心距离做一个量级对比。另一个常见原因是标定板不够平整。A4纸直接贴桌上桌面有纹理或者纸面隆起角点的物理坐标就不是理想的平面模型。尤其用硬纸板纸板翘边投影误差会偏高。我试过把打印纸贴在平底透明玻璃上效果反而不如贴在实心木板平面来得好因为玻璃反光会干扰检测。最后是标定板姿态覆盖不够。如果全是正对相机的正面图没有大角度倾斜畸变参数中的切向分量几乎无法被约束。补拍时让标定板形成30度、45度的倾斜角度同时保证所有角点都在画面内。倾斜过头导致角点间距过小也可能检测失败所以需要把握尺度。6.3 左右时间不同步USB摄像头没有硬件同步是先天条件限制。标定过程中只要棋盘格静止左右图时间差就不会引入误差但如果标定板是运动中采集的左右帧不是同一时刻的物理状态立体标定就会算错外参。做双目标定一定要保持标定对象静止或者从视频流里手动挑选左右画面中棋盘格位置几乎重合的帧。人脸测距最终出现在实时的场景里如果人轻微晃动可能带来几毫米到几厘米的深度抖动属于USB方案本身的上限问题。不要指望标定能解决它。6.4 标定一次能用多久很多人标定一次之后发现过几天深度误差变大了第一反应是标定库被系统删了或者参数读错了。实际更可能是摄像头被碰过或者自动对焦被恢复开启了。USB摄像头的外壳很软轻微挤压就会让内参变化。建议把标定结果保存成npz文件连同对应的图像分辨率一起保存写代码时先校验当前分辨率与标定分辨率是否一致。如果重新插拔USB摄像头后系统给它分配了不同的帧尺寸映射表也会失效。平时尽量不要动硬件结构非动不可就重新标定一次代价并不大。6.5 给后续人脸测距留一手底牌跨到下一篇之前建议把标定阶段的可视化脚本全部保存好。以后只要发现测距不准先打开极线校正图像看左图的某个特征点是否落在右图的同一水平线上。如果行对不齐问题出在标定和校正流程如果行对齐了但深度不准问题大概率在立体匹配或人脸框区域筛选上。这样能快速定位问题不至于每次都要从零排查。我每做完一套相机标定都会保留一份校正前后的对比截图既方便调参也方便跟别人讲清楚问题发生在哪一环。这套流程下来双目标定就不再是黑盒了。对我自己来说真正把标定原理吃透之后再回头调立体匹配参数视野完全不同。下一步就可以把立体校正后的左右图交给SGBM或者更轻量的匹配器拿到视差图后把从Q矩阵换出的三维坐标对准在人脸框上距离值就在眼前了。