☰
双目相机标定实战:USB摄像头做实时人脸测距的关键一步
2026/10/16 5:08:51 网站建设 项目流程

做双目测距最容易被低估的坑,是很多人直接套一行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就完事。正规做法是先分别对左右相机做单目标定,得到各自的初始内参和畸变系数,然后再做立体标定,在公共约束下优化外参。

单目标定的流程是:对每一张标定图检测棋盘格角点,然后根据棋格的物理尺寸生成对应的世界坐标。假设棋盘格平面是z=0,它的世界坐标点可以认为是:

(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), criteria=criteria, flags=flags )

立体标定之后,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, alpha=0 )

alpha这个参数值得多说两句。alpha=0意味着校正后会裁掉边缘不可用的区域,图像损失最多,但后续计算视差的有效区域非常干净;alpha=1表示保留全部原始像素,图像完整,但边缘会有黑边,立体匹配时这些没用的区域还会干扰视差图。我一般先用alpha=0验证算法流程,确认一切正常后,再看是否需要增大alpha保留更多视野。实际做人脸测距时,目标集中在画面中部,alpha=0带来的视野损失往往可以接受。

接着生成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矩阵换出的三维坐标对准在人脸框上,距离值就在眼前了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询