1. 共视区到底决定什么——先从一个看着很近却测不到的距离讲起
之前调试某双目光学测量装置,左右相机装在120毫米的机械滑块上,标定重投影误差压到了0.08像素以内,视差算法跑起来也很正常。但现场测距时碰到一个很诡异的情况:一个贴着镜头的目标物,看着明明在两个画面里都占了不小面积,深度图却始终是一团噪声,反复调参数都救不回来。换了几个距离测试,发现大概在6到7厘米以内就会出现这种失效,而一旦把目标物往后挪到十几厘米,深度值一下又稳了。
当时第一反应是算法问题,怀疑立体匹配的近邻参数没调好。后来把左右图像一帧一帧导出来对比才发现:在极近距离下,左图能看到目标物完整轮廓,右图里目标物已经有一部分出了画面边界,两边画面里根本没有同一块完整的物理表面,立体匹配自然找不到对应点。这就是双目系统里常说的共视区问题——两个相机同时能观察到的空间范围。超出这个范围,哪怕像素面积再大,也无法通过视差计算出有效深度。
很多人做双目项目的第一反应是看基线多长、分辨率多高、算法是SGBM还是半全局,却很少在方案设计阶段认真算一算两个相机的视野到底重叠了多少。实际上这个参数决定了系统的天然量程边界:最近能测到多近,远场横向覆盖能有多宽,安装高度和俯仰角稍微动一下,共视区边界会怎么偏,全是从这一层几何关系里推出来的。对于机器人避障、手爪抓取、人体姿态识别这些需要明确量程的场景,先把共视区算明白,比后期调算法省力得多。
共视区的本质是两个相机视锥体的交集,不是简单叠加。把每个相机理解成从一个光学中心向传感器四边发射出的棱锥空间,两个棱锥在空中相遇的部分才是立体匹配可用的区域。既然有两个棱锥,必然存在一个近处盲区:两个锥体在离相机很近的位置还没来得及交叉,目标离得太近时只有一个相机能看到它。再往远处走,两个锥体交叠的范围会变宽,但这个宽度不是无限扩展的,它取决于水平视场角、基线和光轴夹角三者的组合。
这个几何关系看着简单,真正算起来却有好几层坑。以下用一个普通针孔模型下的双目系统为例,把计算路线完整拆开。
2. 针孔模型下的几何推导——用一张纸就能算出来的边界
2.1 先算水平视场角,别把内参单位搞混
双目相机的共视区计算,最核心的起点是相机的水平视场角(HFOV)和垂直视场角(VFOV)。这两个值不需要看镜头厂商的手册,直接拿标定出的内参矩阵就能算出来。
针孔模型内参矩阵一般写成:
K = [fx s cx 0 fy cy 0 0 1]fx、fy是以像素为单位的等效焦距,cx、cy是主点坐标。水平视场角公式:
θh = 2 * arctan(W / (2 * fx))同理,垂直视场角:
θv = 2 * arctan(H / (2 * fy))举个例子,图像分辨率1280×720,标定得到fx=fy=700,主点基本居中。那么水平视场角就是2×arctan(640/700),约84.8度;垂直视场角是2×arctan(360/700),约54.4度。
这里最容易翻车的是单位。很多人习惯拿着镜头焦距的毫米数直接代进公式,比如标称焦距3.6毫米,再把1280像素当作宽度直接除,算出来的视场角大得离谱。fx和fy本质上是焦距的毫米值除以单个像素的物理尺寸,所以它已经是“像素单位”。除非你同时知道传感器物理尺寸和单个像素尺寸,否则不要拿镜头焦距毫米数硬套,直接用标定出的fx最稳妥。
我平时会先列一张小表,方便快速评估不同镜头方案的影响:
| fx(像素) | 图像宽度 | 水平视场角(度) | 直观感受 |
|---|---|---|---|
| 300 | 1280 | 约130.4 | 接近鱼眼,畸变大 |
| 500 | 1280 | 约104.3 | 广角,视野开阔 |
| 700 | 1280 | 约84.8 | 普通视觉方案 |
| 1000 | 1280 | 约65.2 | 长焦,视野窄但细节好 |
2.2 平行光轴模型的Z_min公式
计算出水平视场角之后,先假设两个相机的光轴严格平行、主点都居中、不存在俯仰和偏航差。把两个相机分别放在一条基线上的两个端点,基线距离记为B。
在距离相机平面Z处做一个水平切面。左侧相机能看到的横向半宽是Z×tan(θh/2),右侧相机同理。两个相机视野中心之间错开了B的距离,于是该距离处两个视野在水平方向的重叠宽度可以写成:
W_overlap(Z) = 2 * Z * tan(θh / 2) - B从这个公式能直接读出一个关键结论:重叠宽度和距离Z成正比,但会被基线B削减一个固定量。当Z足够小时,计算结果是负数,说明该距离下左右视野根本不相交。重叠宽度恰好为0的临界距离,就是最近可测距离:
Z_min = B / (2 * tan(θh / 2))这个Z_min是我做双目系统最先算的一个数,它直接告诉你这个方案在近端能干什么。
沿用前面的参数再算一遍:B=0.12米,θh=84.8度,tan(θh/2)=0.914,于是Z_min=0.12/(2×0.914)=0.0656米,约6.6厘米。也就是说,这套相机对6.6厘米以内的物体完全没有共视区,深度必然失效。开头提到的那个调试现象恰好就发生在这个边界上,可见这个数不是纸上谈兵。
如果换个更短的基线,比如B=0.06米,Z_min就变成3.3厘米,近端能力明显提升。这也是人眼基线大约6.5厘米而能看到非常近物体的原因——人眼水平视场角超过120度,算出来的Z_min很小。
2.3 垂直方向并没有想象中那么省心
很多人算完水平方向就停了,觉得垂直方向反正两个相机上下视野一样,重叠区必然存在。这个直觉在理想安装条件下是对的:如果左右相机高度一致、俯仰角一致,垂直方向上确实没有基线位移,垂直视野完全重叠,重叠高度就是2×Z×tan(θv/2)。
但工程安装几乎做不到这种理想状态。左右相机之间存在一个垂直方向的高度差ΔY时,垂直重叠公式就变成了:
W_overlap_v(Z) = 2 * Z * tan(θv / 2) - ΔY只要ΔY大于0,垂直方向同样会存在一个近端盲区。更麻烦的是俯仰角差异,哪怕只有1度的俯仰偏差,在10米外就会让两个画面的垂直内容差出0.17米以上。用我们这套参数算一下:θv=54.4度,若两个相机高度差1厘米,垂直Z_min=0.01/(2×tan(27.2度))≈0.0098米,看起来影响不大;但如果高度差到3厘米,垂直Z_min就成了约3厘米,已经不可忽略了。
所以我会把水平和垂直方向分开算,分别记录各自的Z_min,取其中较大者作为系统整体近端极限。很多双目模组看着参数漂亮,装到支架上之后近端表现差,就是因为垂直装配误差把理论Z_min拉大了。
3. 不靠手算——用OpenCV和三维仿真把共视区画出来
3.1 用相机内参直接反推视场角
如果系统已经标定好了,直接用OpenCV读出内参即可。最简单的代码如下:
import numpy as np fx, fy = 700.0, 700.0 width, height = 1280, 720 theta_h = 2 * np.degrees(np.arctan(width / (2 * fx))) theta_v = 2 * np.degrees(np.arctan(height / (2 * fy))) print(f"水平视场角: {theta_h:.2f} 度") print(f"垂直视场角: {theta_v:.2f} 度")运行结果就是上面提到的84.8度和54.4度。拿到视场角后,Z_min和W_overlap都可以直接算:
baseline = 0.12 # 单位:米 z_min = baseline / (2 * np.tan(np.radians(theta_h / 2))) Z = 2.0 # 目标距离 overlap_width = 2 * Z * np.tan(np.radians(theta_h / 2)) - baseline print(f"最近可测距离: {z_min:.3f} 米") print(f"Z={Z}m 处水平重叠宽度: {overlap_width:.3f} 米")这段代码在整个方案评估阶段很有用,换一个基线或者换一个镜头,立刻就能看到近端极限和远端覆盖的变化。
3.2 一个Python脚本:逐点判断空间点是否落在共视区
前面的公式只适用于理想平行光轴模型。实际系统经过立体标定之后,左右相机之间存在旋转矩阵R和平移向量t,光轴不一定平行。更稳妥的做法是直接用针孔投影逐点判断:把一个空间点分别投影到左右图像平面,如果它同时落在左右图像的边界内,就认为它处于共视区。
import numpy as np def project_point_to_image(K, R, t, point_world): """将世界坐标点投影到相机坐标系并换算到像素坐标""" point_cam = R @ point_world + t X, Y, Z = point_cam u = K[0, 0] * X / Z + K[0, 2] v = K[1, 1] * Y / Z + K[1, 2] return np.array([u, v]) def is_in_common_view(point_world, K_left, K_right, R_left, t_left, R_right, t_right, width, height): """判断空间点是否同时落在左右相机的有效画面内""" u_left, v_left = project_point_to_image(K_left, R_left, t_left, point_world) u_right, v_right = project_point_to_image(K_right, R_right, t_right, point_world) in_left = (0 <= u_left < width) and (0 <= v_left < height) in_right = (0 <= u_right < width) and (0 <= v_right < height) return in_left and in_right这个函数可以被循环调用,在三维空间里生成网格采样点,找出所有同时可见的点,即可近似还原共视区的三维轮廓。对精度要求高的场景,可以配合立体校正之后的映射参数来做,因为校正之后左右图像处于同一虚拟平面上,边界判断更接近真实匹配可用的范围。
用这套逐点法算出来的共视区边界,和前面公式法算出来的结果在大部分区域会非常接近,但在视场边缘有差异。原因在于逐点法考虑了旋转矩阵R造成的视野错位,而理想公式把R默认成了单位矩阵。
3.3 三维软件里验证,让几何边界可视化
如果手头有三维建模软件,还可以做一步更直观的验证:在每个相机的物理位置上建立一个从镜头中心出发的四棱锥,底面形状和尺寸由传感器宽高比、视场角决定,然后对两个四棱锥做布尔相交,直接看到共视区的三维形状。
这个办法特别适合方案评审时给非技术同事看。把两个不同颜色的半透明锥体叠加在一起,相交部分高亮出来,Z_min和远端覆盖形状一目了然。另一个好处是它能快速评估障碍物遮挡的影响——如果车体前方的支架或外壳伸进了相交区域,布尔模型里直接能看到共视区被切掉了一块。
我自己常用的流程是:先用公式算Z_min,再用逐点法验证标定后的真实边界,最后用三维模型做整体确认。三步走完,基本不会踩到设计阶段的几何暗坑。
4. 标定误差、畸变和机械公差——纸上几何为什么总差一截
4.1 内参不准时,边界会“飘”
如果你拿一套还没标定的相机,直接套用公式,算出来的共视区几乎肯定是错的。fx如果有百分之几的偏差,水平视场角就会偏出好几度,Z_min跟着成比例变化。很多模组出厂时声称焦距多少毫米,但实际镜头安装公差会让等效fx偏离标称值。所以我的习惯是:凡是涉及毫米级量程判断的时候,一律先标定,再算共视区。
标定自身的质量同样要小心。棋盘格照片如果没拍满画面边角,边缘区域的内参约束不足,fx、fy其实并不准。重投影误差小不表示视场角一定准,因为棋盘格只覆盖了画面中心区域,它对畸变系数的约束很弱。边缘边界算出来的角度差哪怕只有0.5度,在Z=10米的位置横向边界也会偏离快9厘米了。
4.2 畸变模型让视锥边界不再是直线
针孔模型假定光线沿直线进入传感器,但实际镜头尤其是广角镜头存在明显的桶形畸变。畸变会导致靠近画面边缘的等效放大率和中心区域不同,视场角不再是简单的一条直线边界。
对常规工业镜头,畸变系数普遍在几毫米的范围内,直线模型在测试共视区时误差能控制在厘米级,对选型阶段的判断够用。但如果是鱼眼镜头或者视场角超过100度的超广角镜头,直接套直线圆锥模型会明显低估边缘区域的可见性,在图像边缘看到的物体被畸变拉得很挤压,立体匹配效果本身就差,共视区边界也不好精确定义。
这时候我建议转换思路:不再追求一条几何直线,而是用标定后的畸变模型做逐点投影判断。也就是把第3节的project_point_to_image函数里加上cv2.projectPoints处理畸变,先对三维点做畸变校正,再判断是否落在有效画面内。这样算出来的共视区边界虽然是不规则的弯曲边界,但更贴合实际去畸变流程之后的可使用区域。
4.3 基线别用尺子量,光学中心才是真正的B
双目系统中B的取值很容易让人栽跟头。机械设计图纸上标注的是镜头外壳中心之间的距离,但真正决定共视区的是两个相机光学中心的距离,两者之间可能存在好几毫米的偏差。
最直白的例子是镜头光轴不在镜筒正中央,拧紧的时候镜头筒会整体偏一点点。此时用卡尺量两颗镜头金属筒正中心的距离,量出来的B并不是立体几何里真正使用的基线长度。最靠谱的做法是用立体标定输出结果里的平移向量t的欧氏范数来反向确认基线,这个值是重投影解算出来的实际光学中心距离,比机械量测可靠得多。
同样的道理也适用于主点偏移。传感器芯片封装时中心不一定和镜头光轴完全对齐,标定后的cx、cy可能偏离画面中心几十个像素。如果计算共视区时把cx、cy当作理想中心值对待,左右相机视野的真实朝向就和假设不一致,近处重叠区域会向一侧收缩。偏差大时,原本理论Z_min是6.6厘米,实际可能要到8厘米甚至9厘米才会完整重叠。
很多标定软件只报告重投影误差和帧率,不会提醒你共视区变化了。所以我建议把标定得到的完整内参矩阵、畸变系数、相对旋转和平移都存成固定配置,每次做集成测试时重新计算一次共视区边界,而不是沿用初始设计文档里的假设参数。
5. 共视区现场测量的土办法
5.1 用一块标定板把边界“投”到地面上
器材清单很简单:一块黑白方格标定板或者一张高对比度棋盘纸,一个能固定标定板的支架,一台激光测距仪或者卷尺。不需要额外买昂贵的测量设备,关键是让标记物在不同距离和横向位置都能被明确识别。
做法是这样的:把标定板固定在支架上,正面对着双目相机,从Z_min附近的位置开始,沿着相机光轴方向每次移动5厘米拍一帧图像。每一帧里分别查看左右相机的原始图像,逐一看标定板的四条边是不是完整落在画面内。如果左右两幅画面里标定板都完整无缺,这个位置就处于共视区内;只要有一侧画面裁掉了标定板的边角,就说明已经超出了共视区边界。
5.2 操作步骤和读数细节
横向边界的测量稍微麻烦一些。保持标定板到相机的距离Z不变,让标定板沿水平方向左右移动,直到它在一侧画面中刚好看不见完整图案。这个横向极限位置用激光测距仪或者预先画好的地面网格记录。每隔0.5米或1米的Z距离重复一次,就能得到一系列点,把这些点连起来就是共视区的实测轮廓。
操作时有几个细节值得注意。第一,不要只看目标物的质心是否在画面内,要看完整特征区域的边缘是否都在画面内,因为立体匹配需要左右两视图中有足够的同名特征,目标物被画面切掉一半时匹配质量已经不可用了。第二,标定板图案要足够密集,让边缘是否被裁切容易判断,稀疏的大间距格子会让判断模糊。第三,测试时相机白平衡和曝光固定,不要在移动过程中频繁调整参数,否则图像亮度和对比度变化会干扰边缘判读。
我通常会把每个Z位置的实测重叠宽度和理论公式计算值放在一张表里:
| 距离Z(米) | 理论重叠宽度(米) | 实测重叠宽度(米) | 偏差 |
|---|---|---|---|
| 0.5 | 0.914*1 - 0.12 = 0.794 | 0.78 | 约 -1.8% |
| 1.0 | 1.708 | 1.67 | 约 -2.2% |
| 2.0 | 3.536 | 3.44 | 约 -2.7% |
5.3 边界验证数据与理论值的偏差处理
实测值比理论值小是常见现象。一般来自失真校正、机械公差、探头镜头距等因素。偏差小于5%都在合理范围,继续使用理论公式做方案设计问题不大。但如果观察到偏差随Z增大而明显扩大,那很可能是光轴不平行导致的系统旋转;如果偏差集中出现在某一侧,优先怀疑主点偏移和相机安装偏航。
有一个非常容易忽略的点:如果算法流水线里用了OpenCV的stereoRectify对图像做了重投影,左右图像实际上会被裁掉一部分边缘无效区域。此时现场用原始图像测出来的共视区,和算法实际可用的共视区并不是一回事。建议现场测量时直接用校正后的图像做判断,而不是用原始未校正图像。
6. 三个边缘案例把我从理想公式拉回现实,以及我现在选型时的习惯
6.1 把fx当成镜头焦距毫米数,算出来的视场角直接翻车
第一次做双目方案时,我拿到一份镜头参数表,写的是焦距3.6毫米,传感器尺寸1/2.5英寸。顺手就用3.6毫米算水平视场角,代入1280像素图像宽度,结果得出一百多度的离谱结论,还认真照这个数据设计了视场覆盖。后来拿标定内参一算,fx大约是560,水平视场角其实只有97度左右,差了整整三四十度。
这个错误在项目评审时被当场指出来,非常尴尬。后来所有涉及视场角、Z_min、共视区宽度的计算,全部以标定出来的fx、fy为准。如果需要从标称焦距估算方案可行性,也严格用焦距毫米数除以像素物理尺寸,而不是直接把毫米数当像素数用。
这里也可以给一个快速反推fx的经验技巧:拿一把垂直放置的直尺放在已知距离Z处,拍一张图,量出直尺在图像中占据的像素数量N和直尺的实际长度W_real,那么fx约等于Z×N/W_real。没有标定条件时,这个方法能很准确地估算等效焦距。
6.2 只算水平FOV,忽略了主点偏移带来的不对称
另一个让我记忆深刻的坑是只按画面中心对称来算视野,完全没管主点偏移。当时有一套左下相机标定出来后cx偏离中心将近40像素,整个视野的实际有效中心线偏了一大截。用对称公式算出Z_min是6.6厘米,实际测试却发现左侧画面先丢目标,直到更远的距离才能完整重合。后来重新用逐点法一算,真实边界果然不对称:一侧的重叠起始距离明显更远,另一侧则近一些。
从那以后,我对所有系统的共视区计算都不再做对称假设。cx、cy直接作为内参代入,计算左右两个边界各自相对相机基线的角度,再把两个相机的左右边界分别配对求交点。虽然多写了几行代码,但得到的是实打实的非对称真实边界。
6.3 畸变小镜头未必可以不处理边缘
还有一种常见的省略是:镜头畸变不大,就完全忽略畸变对共视区的影响。小畸变在画面中心确实看不出问题,但共视区边界往往落在画面边缘10%的区域,筒形畸变把这里的光线压变形之后,边界位置实测会偏离理论值几毫米到几厘米。对近距离抓取场景来说,几厘米的误差已经能导致一个零件无法稳定匹配。
所以我现在的默认做法是:凡是要做工程交付的系统,哪怕畸变系数只有0.01,边界判断一律走完整标定模型加逐点投影。公式法只用来做初期的快速估算和方案对比,真正落到量产结构上的设计值,一定以逐点计算结果和现场实测共同确认。
6.4 选基线和视场角的个人取舍
做完这么多共视区计算之后,我最大的体会是基线并不是越长越好。基线长确实能提升远距离视差分辨率,但Z_min会线性变大,近场覆盖被牺牲。不要只盯着参数表上的分辨率指标,要结合具体安装高度和最近工作距离来反推合理的B和θh组合。
做近距离机械臂抓取,工作范围0.3米以内,那就选短基线加广角镜头,让Z_min压到10厘米以下;做车辆前方感知,工作距离10米以上,基线才值得加长到0.15米甚至更多。共视区计算应该和任务量程放在一起联合设计,而不是等机械结构定死了再拿一个相机过来配合。
最后分享一个我已经形成习惯的做法:每次拿到新的双目系统配置,先花十分钟把fx、fy、幅面分辨率、机械基线四个数填进一个脚本,输出Z_min、Z=2米和Z=10米的重叠宽度,再把结果贴到项目文档第一页。这个参数表比任何标定报告都能更快地告诉团队:这套系统到底能在什么范围里干活,不能干什么。很多后期避坑,都是从这个简单的表格开始的。