干双目视觉的,没有人能绕开极线校正这一步。不管你是做深度估计、三维重建还是目标测距,左右图像对不齐,后面的匹配算法再强也是白搭。我在做双目测距项目时,一开始直接拿原始图像跑SGBM,结果视差图上一片噪声,后来才发现问题出在极线没有校正干净,而不是匹配算法本身不够好。换用Bouguet算法做极线校正之后,左右图像同一行就是同一条极线,搜索维度直接从二维降到一维,匹配速度和准确性都上了一个台阶。
这篇文章我打算把Bouguet算法从数学原理到OpenCV落地实现完整过一遍,核心是代码解析和实操参数调优。适合正在做双目视觉项目、被极线对齐问题折磨过的读者,也适合刚入门双目视觉、想搞清楚stereoRectify背后到底在干什么的新手。文章里所有代码都是我在实际项目中跑通的,你可以直接拿去改。
1. 先把极线校正这事儿说清楚
1.1 为什么双目视觉绕不开极线校正
双目视觉的核心任务是通过两个相机观察同一个场景,利用视差来恢复深度。理想情况下,两个相机完美平行放置,光轴完全对齐,那么空间中的某个点在左图和右图上的投影只会存在水平方向的位移,不会有垂直方向的偏差。这个水平位移就是视差,有了视差和相机参数就能算出深度。
但现实很骨感。即便你用再精密的机械结构固定两个相机,镜头畸变、装配误差、温度变形这些因素都会让两个相机不可能完全平行。实际拍摄出来的左右图像,同一个特征点在两幅图上的位置既会有水平差异,也会有垂直差异。如果不做任何处理,立体匹配就得在一个二维区域内搜索对应点,计算量爆炸不说,还特别容易匹配错误。
极线校正要解决的就是这个问题。它的本质是把左右图像重新投影到一个公共的虚拟平面上,让两个虚拟相机的光轴平行、图像平面共面,最终使得任意一个三维点在左右图像上的投影落在同一行上。这样一来,立体匹配就被简化为在一维水平线上搜索,计算复杂度从O(n²)降到O(n),匹配稳定性也大幅提升。
我在项目里最直观的感受是,未校正的图像跑SGBM,视差图边缘全是毛刺,无效像素比例接近40%;校正之后重新跑,无效像素比例能降到5%以内。这个差距是肉眼可见的,也是为什么我坚持认为极线校正是双目视觉流程里最不能省的一步。
1.2 Bouguet算法的设计思路和数学原理
我们常说的Bouguet算法,指的是Jean-Yves Bouguet在OpenCV中实现的一种双目视觉校正方法。它最早源自他在加州理工做的Camera Calibration Toolbox,后来被OpenCV吸收为stereoRectify函数的默认实现逻辑。核心思路可以拆成两步。
第一步,把右相机相对于左相机的旋转矩阵R分解成两部分:左相机旋转一半,右相机旋转一半。目标是把两个相机的光轴旋转到互相平行的方向。用Ri表示左相机的旋转矩阵,Rr表示右相机的旋转矩阵,那么左相机的作用相当于R的平方根(更准确地说,是让R = Rrᵀ * Ri成立且Ri和Rr各承担一半旋转量)。这样做的妙处在于,两个相机各自只旋转原来角度的一半,图像的畸变程度最小,公共视野区域最大。
第二步,构造一个旋转矩阵Rrect,把极点映射到无穷远处。啥意思呢?两个相机光轴平行之后,极线还是斜着的,需要再旋转一下图像平面,让极线变成水平方向。这个旋转矩阵的构造方式很讲究,它利用平移向量T来定义新的坐标轴。其中:
- e1方向取平移向量T的单位方向,也就是两个相机光心连线的方向;
- e2方向取e1与主光轴方向的叉积再归一化,保证与e1正交;
- e3方向取e1与e2的叉积,构成完整的三维正交坐标系。
这三个向量组成Rrect,作用是把图像平面旋转到与基线(两个光心连线)平行,这样极线就完全水平了。
光看公式可能有点抽象,我用生活中的场景类比一下。假设你站在一条笔直的马路中间,路的尽头是远方。你左眼和右眼看到的路面纹理是横向错开的,但你感觉不到垂直错位,因为你的大脑已经把两眼图像"校正"到同一水平面了。Bouguet算法干的活就是在大脑层面手动完成这个对齐操作,只不过换成了数学矩阵来实现。
1.3 校正之后能得到什么
完成Bouguet校正之后,你会得到一组映射关系:原始左图像素到校正后左图像的坐标映射,原始右图像素到校正后右图像的坐标映射。把原始图像通过这个映射重新采样一遍,就能得到校正后的图像。
OpenCV里这条流水线由三个函数协同完成:stereoRectify负责计算旋转矩阵和投影矩阵,initUndistortRectifyMap负责把校正后的虚拟相机投影过程转换成查表映射,remap负责实际执行像素重采样。后面的代码解析部分,我会逐个函数讲清楚每个参数该怎么填、有什么坑。
2. 工程落地前的准备工作
2.1 立体标定:校正质量的命门
Bouguet算法本身是确定的几何变换,但它的输入——左右相机的内参、畸变系数、相对旋转和平移——必须来自立体标定。标定结果直接决定校正质量,这一步做不好,后面再调参数都是白搭。
我见过不少同行图省事,直接把单个相机标定的内参拿来用,跳过立体标定,自己猜测R和T近似为单位矩阵和零平移。结果校正出来的图像看起来好像还行,但一算视差就露馅,精度根本达不到要求。正确做法是用棋盘格标定板同时拍摄左右相机,检测角点后用stereoCalibrate同时求解内外参和双目标定参数。
标定采图时有一个容易被忽视的细节:标定板要在不同距离、不同角度、不同位置各拍20到30组。只看单一距离的标定板会让外参求解退化,R和T的置信度极低。另外,标定板一定要足够平整,打印到普通A4纸上再用双面胶贴在硬纸板上,表面一旦起皱,角点检测精度直接下降,最终影响的是极线校正的效果。
立体标定输出的参数包括:
- 左右相机的相机矩阵K1、K2,包含焦距fx、fy和主点cx、cy;
- 左右相机的畸变系数D1、D2,包含k1、k2、p1、p2、k3;
- 右相机相对左相机的旋转向量R和平移向量T;
- 本征矩阵E和基础矩阵F,主要用于对极几何验证。
R和T尤其重要,stereoRectify里传的R、T就是这个,它描述的是在左相机坐标系下,右相机光心相对左相机光心的旋转和平移。
2.2 相机参数与数据结构检查
拿到标定结果后,不要急着写校正代码,先把参数撸一遍,确认数值在合理范围内。怎么判断合理?我给你几个经验值参考。
左相机的fx和fy一般接近且等于传感器焦距乘以像素尺寸的倒数,数量级在几百到几千之间,取决于相机分辨率。如果fx和fy相差超过5%,说明相机传感器像素不是正方形,或者标定过程有问题,需要复查。主点cx、cy理论上应该在图像中心附近,如果偏差超过图像宽高的10%,就要怀疑标定板采集是否覆盖了图像各个角落。
畸变系数k1、k2一般是小量,数量级在0.01到0.1之间。如果标定出来的k1超过0.5甚至接近1,大概率是标定板图片太少或者某组图片的角点检测跳了,得重新标定。R应该是接近单位矩阵的,因为双目标定时左右相机的位置关系基本不会大变。T向量的模长就是基线长度,单位是标定时用的尺度单位(比如毫米)。如果你的双目基线是120毫米,标定出来的|T|应该在120附近,差太远说明标定尺度不对。
在代码层面,我习惯把标定结果保存成JSON或YAML文件,方便后续读取。数据结构上定义一个简单的字典或者类,把K1、D1、K2、D2、R、T、图像尺寸都封装好。这样校正和后续的匹配模块都能复用,不用每跑一次就重新标定一次。
2.3 OpenCV环境配置要点
代码依赖OpenCV的calib3d、imgproc、core几个模块,用pip安装的opencv-python包一般自带。需要注意版本问题,OpenCV 4.x系列中stereoRectify的API签名没有大的变化,但如果你用的老版本OpenCV 2.x,部分参数的默认值会有差异,建议直接用OpenCV 4.5以上版本。
如果要用CUDA加速remap,还得装opencv-contrib-python并编译CUDA支持。不过日常调试阶段CPU版本就够用,后面做性能优化时再考虑CUDA。另外,推荐装一个matplotlib用于绘制校正前后的对比图,方便验证效果。环境依赖很轻,不需要额外安装其他重型库。
3. 核心代码实现:Bouguet校正全流程
3.1 关键API选型:stereoRectify参数逐项拆解
先看核心函数stereoRectify的调用方式。我直接贴一个实际项目里的实现,然后逐行拆解每个参数的用意。
import cv2 import numpy as np def bouguet_rectify(K1, D1, K2, D2, R, T, image_size, alpha=0.0, new_image_size=None, flags=0): """ 基于Bouguet算法的双目极线校正 :param K1: 左相机内参矩阵 (3x3) :param D1: 左相机畸变系数 (1x5 或 1x4) :param K2: 右相机内参矩阵 (3x3) :param D2: 右相机畸变系数 :param R: 右相机相对左相机的旋转矩阵 (3x3) :param T: 右相机相对左相机的平移向量 (3x1) :param image_size: 原始图像尺寸 (width, height) :param alpha: 有效区域保留系数,0~1 :param new_image_size: 校正后图像尺寸,默认与原始一致 :param flags: 校正标志位 :return: rectification results """ # 校正映射矩阵 R1 = np.zeros((3, 3)) R2 = np.zeros((3, 3)) P1 = np.zeros((3, 4)) P2 = np.zeros((3, 4)) Q = np.zeros((4, 4)) # 有效ROI valid_roi1 = (0, 0, 0, 0) valid_roi2 = (0, 0, 0, 0) R1, R2, P1, P2, Q, valid_roi1, valid_roi2 = cv2.stereoRectify( K1, D1, K2, D2, image_size, R, T, R1, R2, P1, P2, Q, flags=flags, alpha=alpha, newImageSize=(new_image_size if new_image_size is not None else image_size) ) return R1, R2, P1, P2, Q, valid_roi1, valid_roi2这个函数里每个参数都不白给。flags参数最常用的取值是cv2.CALIB_ZERO_DISPARITY,它让两个校正相机的光轴严格平行而且主点行号一致。什么意思呢?就是左右校正后的图像,主点在同一水平线上,这能保证后续立体匹配时左右视差图对齐。如果不用这个标志,两个相机虽然极线水平了,但主点的垂直位置可能不一致,视差图像素坐标对不上,计算出来的深度会有系统偏差。
alpha参数是关键中的关键。它控制校正后图像中保留的有效像素范围。alpha=0表示只保留两个相机都能看到的公共区域,黑色边框最少,但图像四周会被裁剪掉一部分;alpha=1表示保留原始图像的所有像素,每个像素都会映射到校正后的图像上,但边缘会出现大量无效的黑色区域。实际项目中我做深度估计时alpha取0,因为裁剪掉边缘区域换来的是更高的有效像素比例,计算视差时不需要额外判断边界;做可视化展示时alpha取0.5左右,既保留大部分图像内容,又没有太明显的黑边。
newImageSize参数可以让你把校正后的图像缩放到任意尺寸。这个功能很实用,当你要把校正结果直接送进深度学习网络,而网络要求固定输入尺寸时,可以在这里一次性完成尺寸调整。不过我建议缩放系数保持和原始图像接近,过度缩放的图像会让后续视差计算精度下降。
3.2 initUndistortRectifyMap与remap的配合
stereoRectify算出的R1、R2、P1、P2是校正后的虚拟相机模型,还不能直接拿来对像素做变换。真正干活的是initUndistortRectifyMap加remap这个组合。我拆开讲一下这两个函数各自干了什么。
initUndistortRectifyMap做的事情是:对于校正后目标图像上的每个像素,计算它在原始图像上对应的像素坐标。这个计算考虑了原始相机的内参、畸变系数、左右相机的相对位置关系,一次性把去畸变、极线校正、坐标系映射全部融合在一起。输出是两个float32类型的映射表map_x和map_y,map_x[u, v]表示目标图像上(u, v)位置应该取原始图像的哪一列,map_y[u, v]表示取哪一行。
这两个映射表的精度必须是CV_32FC1,很多新手在这里踩坑:用默认的CV_32F还是CV_16SC2搞不清楚,导致remap时报类型错误或结果图像花掉。我建议直接指定为cv2.CV_32FC1,这个类型在remap里支持最好的双线性插值。
remap则是真正执行像素搬运的函数。它遍历目标图像上的每个像素位置,根据映射表找到原始图像的对应位置,然后插值出最终像素值。插值方法用cv2.INTER_LINEAR就行,没必要用INTER_CUBIC,后者计算量大一倍但视觉差异几乎看不出来,纯属浪费算力。
值得提醒的是,这两个映射表在相机固定不动的场景下只需要计算一次。很多项目的性能瓶颈就出在这,每帧图像都调用一遍initUndistortRectifyMap,白白浪费了大量CPU时间。正确做法是启动时算好map,之后的每一帧只跑remap。
3.3 完整可运行代码示例
我把上面这些零散的片段拼成一个完整的类,方便直接集成到你的项目里。
import cv2 import numpy as np import json class BOUGUET_RECTIFIER: def __init__(self, stereo_params_path, image_size, alpha=0.0): """ 加载标定参数并初始化校正映射 :param stereo_params_path: 标定结果文件路径(JSON格式) :param image_size: 原始图像尺寸 (width, height) :param alpha: 校正有效区域系数 """ self.image_size = tuple(image_size) self.alpha = alpha # 加载标定参数 with open(stereo_params_path, 'r') as f: params = json.load(f) self.K1 = np.array(params['K1'], dtype=np.float32).reshape(3, 3) self.D1 = np.array(params['D1'], dtype=np.float32) self.K2 = np.array(params['K2'], dtype=np.float32).reshape(3, 3) self.D2 = np.array(params['D2'], dtype=np.float32) self.R = np.array(params['R'], dtype=np.float32).reshape(3, 3) self.T = np.array(params['T'], dtype=np.float32).reshape(3, 1) # 计算校正参数 self.R1, self.R2, self.P1, self.P2, self.Q, \ self.valid_roi1, self.valid_roi2 = \ self._compute_rectification_params() # 计算映射表(只需要计算一次) self.map1_x, self.map1_y = self._compute_maps( self.K1, self.D1, self.R1, self.P1 ) self.map2_x, self.map2_y = self._compute_maps( self.K2, self.D2, self.R2, self.P2 ) def _compute_rectification_params(self): R1 = np.zeros((3, 3)) R2 = np.zeros((3, 3)) P1 = np.zeros((3, 4)) P2 = np.zeros((3, 4)) Q = np.zeros((4, 4)) R1, R2, P1, P2, Q, valid_roi1, valid_roi2 = \ cv2.stereoRectify( self.K1, self.D1, self.K2, self.D2, self.image_size, self.R, self.T, R1, R2, P1, P2, Q, flags=cv2.CALIB_ZERO_DISPARITY, alpha=self.alpha, newImageSize=self.image_size ) return R1, R2, P1, P2, Q, valid_roi1, valid_roi2 def _compute_maps(self, K, D, R_rect, P_rect): """ 计算映射表 :param K: 相机内参 :param D: 畸变系数 :param R_rect: 校正旋转矩阵 :param P_rect: 校正投影矩阵 :return: map_x, map_y """ map_x, map_y = cv2.initUndistortRectifyMap( K, D, R_rect, P_rect, self.image_size, cv2.CV_32FC1 ) return map_x, map_y def rectify(self, img_left, img_right): """ 对左右图像执行极线校正 :param img_left: 原始左图 :param img_right: 原始右图 :return: 校正后的左右图像,有效ROI """ rect_left = cv2.remap( img_left, self.map1_x, self.map1_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT ) rect_right = cv2.remap( img_right, self.map2_x, self.map2_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT ) return rect_left, rect_right, self.valid_roi1, self.valid_roi2 # 使用示例 if __name__ == "__main__": # 图像尺寸必须与标定时一致 img_size = (1280, 720) rectifier = BOUGUET_RECTIFIER( stereo_params_path='stereo_params.json', image_size=img_size, alpha=0.0 ) # 读取双目图像 left = cv2.imread('left.png') right = cv2.imread('right.png') # 执行校正 rect_left, rect_right, roi1, roi2 = rectifier.rectify(left, right) # 保存结果 cv2.imwrite('rectified_left.png', rect_left) cv2.imwrite('rectified_right.png', rect_right)这代码我实际用过很多次,可以直接拿来用。这里有个地方需要注意,JSON文件里的R、T必须是stereoCalibrate直接输出的原始结果,不要做任何归一化或修改。R和T的坐标系基准是左相机坐标系,很多人在这里容易搞错方向,导致校正后左右图像完全对不上。
3.4 Q矩阵在深度恢复中的用途
stereoRectify还会额外输出一个4x4的Q矩阵,它是视差到三维坐标的转换矩阵。Q矩阵长这样:
Q = [[1, 0, 0, -cx1], [0, 1, 0, -cy1], [0, 0, 0, f ], [0, 0, 1/b, 0]]这里cx1和cy1是校正后左相机的主点坐标,f是焦距,b是基线长度。当你通过立体匹配得到视差图disp之后,可以用cv2.reprojectImageTo3D把每个像素的视差转换成三维坐标:
points_3d = cv2.reprojectImageTo3D(disp, Q)这个函数返回的三维点坐标的单位和你的标定尺度一致。如果你的基线是毫米,那恢复出的三维坐标单位就是毫米。很多人的深度恢复结果看起来不对,往往是因为没有搞清楚Q矩阵里的参数单位,或者没有使用stereoRectify输出的Q矩阵,而是自己手搓了一个近似的Q,导致结果在数值上差了一截。
4. 校正效果验证与结果分析
4.1 肉眼检查与定量指标
代码写完,怎么判断校正到底成不成功?这里我分享一个三层次的验证思路。
第一层:肉眼观察。把左右校正图像拼到同一张图上,左右并排显示,然后画几条水平辅助线。如果校正成功,会发现左右图中对应的特征点(比如棋盘格的角点、建筑物的边缘、桌子的边角)都落在同一水平线上,也就是y坐标基本一致。如果发现明显的高低错位,说明R、T参数或者校正流程有问题。
第二层:特征点匹配验证。用ORB或SIFT在左右校正图上提取特征点并做匹配,然后统计所有匹配点的y坐标差。校正成功后,这个y坐标差的均值应该在1像素以内。如果均方差在3像素以上,说明校正效果不理想。我在项目中一般以左右图特征点匹配的垂直方向残差作为标准,残差均值控制在0.5像素以内才算达标,这样后续的立体匹配才有意义。
第三层:立体匹配验证。用校正后的图像跑SGBM或者BM算法生成视差图,观察视差图边缘是否干净、无效像素是否少。如果校正前视差图一片噪声,校正后明显改善,说明校正有效。反之,如果校正后视差图依然很差,就要回头检查标定参数。
下面给一个简单的验证脚本,可以自动计算匹配点垂直方向残差:
def verify_rectification(left_rect, right_rect): """ 通过特征匹配计算校正后的垂直残差 :param left_rect: 校正后的左图 :param right_rect: 校正后的右图 :return: 平均垂直残差(像素) """ # 转为灰度图 gray_l = cv2.cvtColor(left_rect, cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(right_rect, cv2.COLOR_BGR2GRAY) # 使用ORB特征 orb = cv2.ORB_create(nfeatures=2000) kp1, des1 = orb.detectAndCompute(gray_l, None) kp2, des2 = orb.detectAndCompute(gray_r, None) # 暴力匹配 bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) matches = bf.match(des1, des2) matches = sorted(matches, key=lambda x: x.distance)[:100] # 计算垂直方向残差 y_errors = [] for m in matches: pt1 = kp1[m.queryIdx].pt pt2 = kp2[m.trainIdx].pt y_errors.append(abs(pt1[1] - pt2[1])) mean_err = np.mean(y_errors) print(f"有效匹配对: {len(y_errors)}") print(f"平均垂直残差: {mean_err:.3f} 像素") return mean_err这个脚本我每次换相机或者调整标定参数后都会跑一遍,用来量化校正质量。如果你跑出来的平均垂直残差超过1像素,建议先重新标定再调代码,而不要在错误的参数上做无用功。
4.2 校正结果对后续任务的影响
校正质量直接影响后续立体匹配的复杂度和准确性。校正前,我们匹配左图特征点时需要在右图的二维区域(比如30x30的窗口)搜索;校正后,只需要在一条水平线上搜索(搜索范围缩减到一维,比如宽度为50的线段),搜索点数从900个降到50个,计算量下降了一个数量级。
更重要的是,一维搜索比二维搜索鲁棒得多。二维搜索容易在纹理重复区域产生误匹配,而一维搜索天然受极线约束,误匹配概率大幅下降。我做目标测距时,校正后SGBM的误匹配率从12.7%降到了2.1%,测距误差从厘米级降到了毫米级。没有极线校正,双目视觉的精度根本不可能达到实用水平。
在深度学习方案里同样如此。现在很多立体匹配网络(比如PSMNet、RAFT-Stereo)的输入是水平拼接的左右图像对,它们隐含假设输入已经完成了极线校正。不校正就输入网络,预测的视差图会明显劣化,因为网络内部的代价计算假设对应点在相同行。所以不管你是传统算法还是深度学习,极线校正都是前置条件。
5. 常见问题排查与避坑经验
5.1 校正后图像扭曲严重
遇到这种情况,第一个要检查的就是alpha参数。alpha取1.0时,保留全部像素,图像边缘会出现明显的拉伸变形和黑边,看起来像鱼眼镜头效果。这不一定是代码bug,而是alpha参数配置的预期行为。调小alpha到0或0.5,图像变形会明显缓解。
如果alpha=0时图像仍然严重扭曲,那八成是标定参数对不上。检查K1、D1、K2、D2的单位和坐标系是否与原始图像匹配。有个隐蔽的坑:stereoCalibrate输出的相机内参是基于标定板图像分辨率,而如果你在initUndistortRectifyMap里用的image_size与标定时一致,不会出问题;但如果你在中间加过resize操作(比如为了调试把图像缩小了一半),那么K矩阵中的fx、fy、cx、cy都要相应缩放,否则映射关系就全乱了。
5.2 上下视差没有完全消除
校正之后左右图特征点仍然有1到3像素的垂直偏移,这多半是标定过程中的随机误差。我的经验是,先检查标定板采图数量。少于15组图像的外参解算极不稳定,R和T的估计方差很大,自然导致校正不彻底。补拍更多角度、更多距离的标定板图像,重新标定后垂直残差通常会显著下降。
另外,CALIB_ZERO_DISPARITY标志位也会影响结果。如果你没有设置这个标志,函数允许左右校正图像的主点存在垂直位置差异,虽然极线仍然水平,但两幅图的y坐标起点可能不同。极线水平不代表两幅图在垂直方向完全对齐,立体匹配需要的是行号严格对应,所以必须设置这个标志位。
5.3 remap结果出现黑色波状条纹
这个问题的典型原因是映射表类型错误。initUndistortRectifyMap返回的map_x和map_y必须是CV_32FC1类型,如果被强制转换成CV_16SC2,remap时会丢失小数精度,导致边缘出现锯齿和波状条纹。检查一下你的映射表dtype,确认是np.float32。
还有一种可能性是输入图像是灰度图但映射表基于彩色图计算,或者反过来。映射表只和相机参数有关,把彩色图和灰度图混用不会改变映射关系,但要注意图像的通道数要和输入一致,否则remap会报错或者输出意外的结果。
5.4 性能优化建议
校正的全流程里,remap是最耗时的部分。在嵌入式设备或者实时性要求高的场景中,我建议做三件事。
第一,预先计算映射表。前面代码示例里已经把initUndistortRectifyMap放在构造函数里了,运行时只执行remap,这一步能节省大约60%的校正耗时。
第二,插值算法用INTER_LINEAR,不要用INTER_CUBIC。实测下来两者在结果上的差异微乎其微,但耗时差了一倍。
第三,对图像ROI区域做裁剪。有时候你只关心画面中心区域的目标测距,那么可以在校正后先把ROI切出来再做remap,或者直接用valid_roi1和valid_roi2裁剪掉无效黑边,减少需要处理的像素数量。
5.5 常见问题速查表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 校正后图像严重扭曲 | alpha参数过大 | 调小alpha到0或0.5 |
| 校正后边缘有黑色波浪 | 映射表类型错误 | 确认map_x/map_y为CV_32FC1 |
| 特征点垂直残差大 | 标定采图不充分 | 增加标定板图像到20组以上 |
| 视差图边缘噪声大 | 未设置CALIB_ZERO_DISPARITY | 在stereoRectify中设置该标志 |
| remap结果全黑 | 图像分辨率与标定参数不匹配 | 统一image_size参数 |
| 运行卡顿严重 | 每帧重复计算映射表 | 将initUndistortRectifyMap移到初始化阶段 |
6. 一点个人的实操体会
做双目视觉这段时间,我最大的感悟是:Bouguet算法本身并不复杂,OpenCV也把实现封装得很彻底,真正拉开效果差距的往往是标定环节的把控和对参数语义的理解。alpha取多少、要不要CALIB_ZERO_DISPARITY、映射表用哪种精度、R和T的方向对不对,这些细节单独拿出来都不起眼,但组合起来决定了你的极线校正到底是一秒出图还是反复返工。
最后再分享一个我反复用的小技巧:每次标定并校正完之后,一定保存一组校正前后的对比图和一张特征匹配验证图。下次调试算法或者换环境时,拿出来一眼就能确认校正流程没被改动搞坏,省去很多重复排查的时间。如果你也在做双目项目,建议你把这套流程固化下来,形成自己团队的标定和校正常态化检查机制,收益非常大。