简介:张正友标定法(Zhang's Calibration Method)资源包面向计算机视觉初学者与研究人员,系统讲解摄像机标定的核心原理与MATLAB实现,涵盖针孔相机模型、棋盘格标定板设计、角点检测、内参外参求解及畸变校正等关键环节。压缩包共27个文件,包括10个m脚本、7个asv自动保存文件、9个txt说明文档及1个caj论文原文,大小仅85KB,便于快速下载与学习;其中m脚本用于实现标定与测试算法,txt提供说明与数据,caj为张正友原始论文,便于对照理论。已有501人学习浏览。资源提供的MATLAB脚本覆盖同形矩阵计算、旋转测试、归一化点处理等具体功能,配合文本说明和论文,可帮助读者从理论推导到代码实践完整掌握该标定方法,适合用于机器人导航、自动驾驶、工业检测等领域的项目入门与算法复现。
1. 张正友标定法:让单目相机从像素走向物理尺度的第一步
做过视觉测量或者三维重建的朋友应该都有这种体会:从相机拿到的图像只是一堆像素,像素本身不知道自己对应现实世界里的多少毫米。真正让单目相机从“看得到”变成“量得准”的,就是相机内参标定。张正友标定法是目前工业界和学术界最主流的棋盘格标定方案,OpenCV 里那套 calibrateCamera 流程,底层用的就是它。这篇文章我不打算复述教科书推导,而是把它拆成一份能照着跑的实战笔记——标定板怎么选、图像怎么拍、参数怎么解读、哪些坑会导致你标出来的内参完全不能用。适合正在做视觉定位、SLAM 初始化、机械臂手眼标定或者简单单目测距的工程师,照着做一遍能省下好几个晚上的试错时间。
2. 标定板与图像采集:为什么 80% 的误差出在拍照环节
很多人的标定流程是拿一块棋盘格随便拍十几张,然后丢给 calibrateCamera 就算完事。但内参标定这件事,输入图像的质量基本决定了输出结果的命运。拍得不行,后面算法调得再花哨也是白搭。这一章先解决“拍什么、怎么拍”的问题。
2.1 棋盘格、圆点阵列还是 ChArUco:三种靶标怎么选
张正友标定法最初用的就是棋盘格,因为棋盘格的角点检测简单、稳定,两个相邻黑白格交界的角点位置可以用亚像素精度提取。OpenCV 里findChessboardCorners到cornerSubPix就是标准流程。棋盘格的主要限制是必须整块可见,如果有遮挡或者边缘裁切,检测会直接失败或者返回错误的角点排序。
圆点阵列的优点是每个圆心的提取精度可以做得很高,因为圆的几何中心可以通过椭圆拟合得到,对标定板打印质量和光照变化更鲁棒。但圆点阵列存在一个经典问题:偏心误差。相机透视下,圆在图像里变成椭圆,椭圆中心并不是圆心的真实投影,这在标定板倾斜角度大时尤其明显。如果选用圆点阵列,建议拍照时标定板尽量正对相机,倾斜角度控制在 30 度以内,并且配套使用findCirclesGrid时要确保圆点数量设置正确。
ChArUco 是 ArUco 码和棋盘格的结合体,每个格子角落都带唯一编码。它最大的好处是允许标定板部分遮挡,哪怕只有一部分格子可见也能完成角点检测和识别。对于不方便把整个板子完整露出来的场景——比如机械臂末端带着相机去拍桌面上的板子——ChArUco 是更稳的选择。但也有代价:编码区域会占用一些格子面积,在图像分辨率不高时,小格子的 ArUco 码识别容易出错,反而导致检测失败。如果是固定视野的视觉测量,棋盘格就行;如果是移动平台或者遮挡严重的场景,优先 ChArUco。下表是我在实际项目里的选型参考:
| 靶标类型 | 角点/圆心精度 | 部分遮挡支持 | 倾斜鲁棒性 | 推荐场景 |
|---|---|---|---|---|
| 棋盘格 | 高 | 不支持 | 较好 | 固定视野、批量标定 |
| 圆点阵列 | 最高 | 不支持 | 一般 | 高精度测量、低速运动 |
| ChArUco | 中高 | 支持 | 好 | 移动平台、遮挡场景 |
2.2 采集规范:姿态、光照、数量与顺序
标定板图像的质量不只是“看得清楚”这么简单。张正友标定法要求标定板在至少三个不同姿态下成像,才能解出内参矩阵的闭式解。实际经验是:每个相机拍 15 到 20 张不同姿态的图,并且姿态要尽量差异化。什么叫“差异化”?就是不要在一个位置上微调角度拍 20 张,而是要覆盖整个视野空间。
我一般会按这样的套路来:
- 标定板放在画面中央,正对相机拍 2 张;
- 标定板左右平移,贴着画面左右边缘各拍 2 张,注意板子边缘不能出画;
- 标定板上下平移,贴近画面上边缘和下边缘各拍 2 张;
- 标定板倾斜 30 到 45 度,绕水平轴和垂直轴分别旋转,各拍 3 到 4 张;
- 标定板在景深方向前后移动,靠近相机和远离相机各拍 2 到 3 张。
光照方面,均匀的漫反射光是最理想的。棋盘格表面不能有高光反射,否则高光区域的角点检测精度会严重下降。我在室内一般用补光灯打在天花板或者白色墙面,让光漫反射下来;室外标定就尽量选阴天,或者用背光面。
还有一个容易被忽略的点:采集顺序要可回溯。拍完一组图,当场就重投影误差看一眼,不合格的删掉重拍,不要拖到晚上统一处理,到时候你已经想不起来哪张图当时是什么姿态了。这个习惯能帮你省掉很多无效的调参时间。
2.3 一张不合格的图像长什么样
先看角点检测是否成功。findChessboardCorners如果返回 False,那就说明这张图在算法层面已经“不可用”了。还有一种更隐蔽的情况:检测返回 True,但角点排序是乱的,或者部分角点位置明显偏离真实交点。这种情况在处理后一定要做可视化,把检测结果画在图上逐张看。
所有可用的检测结果,角点要清晰地落在棋盘格的黑色和白色交界处,角点之间间距在图像里至少要占 5 到 8 个像素,否则亚像素细化没有意义。如果图像里棋盘格占的面积太小,角点提取的像素误差会被放大很多倍。反之,棋盘格充满整个画面也不一定好,因为边缘区域的畸变信息虽然在,但网格过密会导致角点间距太近,cornerSubPix的搜索窗口很容易跑到邻近角点上去。经验值是棋盘格成像后,单个格子边长占 12 到 25 像素左右比较舒服。
如果发现角点检测结果不稳定,先把标定板放平、正对相机拍一张测试图,检测通过后再开始正式采集。这一张测试图就是你后面所有采集的“基准样张”,光照和焦距都以此为参考。
3. 内参矩阵、畸变系数与单应性矩阵:标定到底在解什么
这一章把张正友标定法的数学模型讲清楚。不要被公式吓到,核心逻辑其实只有几条链:世界坐标怎么一步步变成像素坐标,单应性矩阵怎么对内参施加约束,以及为什么最后还要做一次非线性优化。
3.1 从世界坐标到像素坐标的映射链路
一个三维空间点 $P_w = (X_w, Y_w, Z_w)$ 要变成图像上的像素坐标,中间经过四个变换:
- 从世界坐标系变换到相机坐标系,这是刚体变换,由旋转矩阵 $R$ 和平移向量 $t$ 决定;
- 从相机坐标系投影到归一化平面,取 $x = X_c / Z_c$,$y = Y_c / Z_c$;
- 通过内参矩阵 $K$ 把归一化坐标映射到像素坐标;
- 考虑到镜头畸变,真实像素坐标还要在第三步之前先做畸变补偿。
内参矩阵 $K$ 长得是这样:
[ K = \begin{bmatrix} f_x & \gamma & c_x \ 0 & f_y & c_y \ 0 & 0 & 1 \end{bmatrix} ]
其中 $f_x$ 和 $f_y$ 是归一化焦距,单位是像素,它们和物理焦距 $f$(毫米)的关系是 $f_x = f / d_x$,$d_x$ 是单个像素的宽度;$c_x$ 和 $c_y$ 是主点坐标,理想情况下是图像中心,实际会因为镜头安装偏差偏移几十个像素;$\gamma$ 是倾斜系数,对大多数工业相机来说接近 0,OpenCV 默认把它当作 0 处理,你不需要单独去估计它。
张正友标定法的一个聪明之处在于:它利用平面标定板,把世界坐标里的 $Z_w$ 固定为 0,于是世界坐标到像素坐标的映射退化成单应性变换,这样每张标定板图像只能给出一组单应性矩阵,而内参矩阵 $K$ 可以被这个单应性矩阵约束住。你不需要知道标定板在相机坐标系下的精确位姿,就能先解出内参。
3.2 单应性矩阵与闭式解:为什么最少只要三张图
标定板平面上所有点的 $Z_w = 0$,于是外参里的旋转矩阵 $R$ 的第三列就消失了,整个映射变成了:
[ s \cdot \begin{bmatrix} u \ v \ 1 \end{bmatrix} = K \cdot \begin{bmatrix} r_1 & r_2 & t \end{bmatrix} \cdot \begin{bmatrix} X_w \ Y_w \ 1 \end{bmatrix} ]
中间的 $H = K \cdot [r_1, r_2, t]$ 就是单应性矩阵,它是一个 3x3 的矩阵,有 8 个自由度(最后一个元素归一化)。每张标定板图像提供一组 $H$,而 $H$ 和 $K$ 之间能建立两个约束方程。为什么说至少三张图?因为内参矩阵 $K$ 有 4 个未知数($f_x$, $f_y$, $c_x$, $c_y$),两个约束一张图,4 个未知数就需要至少 3 张图。这是理论下界,实际中噪声很大,3 张图解出来的内参几乎不可用,所以至少要拍十几张。
闭式解的思路是构造一个关于内参矩阵元素的齐次线性方程组,然后用最小二乘或者 SVD 求解。它得到的解在噪声存在时不是最优的,但这没关系,因为它只是给后面的非线性优化提供了一个好的初值。张正友标定法的精妙之处就在这:先用线性解法求闭式解,再用极大似然估计去精修。
3.3 畸变模型与极大似然优化:线性解为什么不够
线性解没有考虑镜头畸变。真实的镜头,尤其是工业相机上那种几十块钱的广角镜头,畸变非常明显。最常用的畸变模型是 Brown-Conrady 模型,它把畸变分成径向畸变和切向畸变:
[ x_{dist} = x (1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + 2 p_1 x y + p_2 (r^2 + 2x^2) ]
[ y_{dist} = y (1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + p_1 (r^2 + 2y^2) + 2 p_2 x y ]
其中 $r^2 = x^2 + y^2$,$(x, y)$ 是归一化平面上的坐标,$k_1, k_2, k_3$ 是径向畸变系数,$p_1, p_2$ 是切向畸变系数。注意 $r$ 是归一化半径,不是像素半径,所以畸变系数和图像分辨率没有直接关系。
径向畸变是主要原因。摄像头模组里那种焦距很短的镜头,边缘区域桶形畸变相当严重。切向畸变主要来自镜头和 sensor 的装配误差,对高精度测量来说不能忽略,但对普通视觉应用,把 $p_1, p_2$ 固定为 0 也是常见做法。
非线性优化这一步做的事情是:给定所有标定板图像的角点观测值,同时优化内参矩阵、所有图像的外参、畸变系数,让重投影误差的平方和最小。重投影误差的定义是:把 3D 标定板角点通过当前估计的内外参和畸变模型投影到图像平面,得到的像素坐标和实际提取到的角点坐标之间的欧氏距离。这个误差以像素为单位,是衡量标定质量最直接的指标。
4. 用 OpenCV 把张正友标定跑通:标定脚本与参数解读
这一章是真正的实操环节。下面的脚本可以在 OpenCV + Python 环境下直接跑,读取一个标定板图像文件夹,输出内参矩阵、畸变系数和每张图的外参。
4.1 预处理:角点提取与亚像素细化
先写图像预处理和角点提取的代码,这是整个流程里最耗时的部分,也是最需要可视化检查的部分。以棋盘格为例,假设标定板内角点是 9x6(也就是横向 9 个内角点、纵向 6 个内角点,对应的格子数是 10x7)。
import cv2 import numpy as np import glob import os # 棋盘格内角点数量,横向和纵向,不含最外面的格子边界 pattern_size = (9, 6) # 亚像素细化窗口大小,一般取 (11, 11) win_size = (11, 11) # 零区域,设成 (-1, -1) 表示不使用 zero_zone = (-1, -1) # 亚像素细化的迭代终止条件 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) def extract_corners(image_path): img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 返回是否检测成功,以及角点坐标,shape 为 (N, 1, 2) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if not ret: return None, None, img # 亚像素精化:在原始灰度图上迭代搜索角点最优位置 corners_refined = cv2.cornerSubPix( gray, corners, win_size, zero_zone, criteria ) # 可视化并保存检测结果,便于后续人工确认 img_debug = img.copy() cv2.drawChessboardCorners(img_debug, pattern_size, corners_refined, ret) cv2.imwrite(image_path.replace('.jpg', '_corners.jpg'), img_debug) return corners_refined, ret, img这段代码里,findChessboardCorners的角点是整数像素精度,它是先用灰度梯度检测棋盘格的大致位置,再在局部窗口内精确求解角点。cornerSubPix是在这个整数像素初值附近,用灰度梯度加权最小二乘迭代出亚像素坐标。
criteria里三个参数分别表示:终止条件类型是“达到精度或达到迭代次数就停”,最大迭代次数 30,角点移动精度阈值 0.001 像素。这个阈值一般不用改,如果你发现角点在相邻两帧之间抖动明显,可以把迭代次数提到 50。
注意findChessboardCorners的第三个参数是None,表示使用默认算法标志;如果棋盘格的角点检测不稳定,可以考虑加cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE标志,帮助算法在光照不均时更好地提取角点。
4.2 calibrateCamera 参数与内参输出解读
角点提取完成后,进入核心标定。输入是标定板的世界坐标(因为标定板平面 $Z=0$,世界坐标就是格子的平面坐标)和对应的图像角点坐标。棋盘格的格子尺寸必须提前用卡尺或者游标卡尺量准,单位用毫米。
def calibrate_from_images(image_paths, pattern_size, square_size_mm): # 构造世界坐标:以棋盘格左上角内角点为原点,x 向右,y 向下 obj_points = [] # 每张图的世界坐标点 img_points = [] # 每张图的图像角点 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) # mgrid 生成网格坐标,再乘以物理尺寸得到实际世界坐标 objp[:, :2] = np.mgrid[ 0:pattern_size[0], 0:pattern_size[1] ].T.reshape(-1, 2) * square_size_mm for path in image_paths: corners, ret, _ = extract_corners(path) if corners is None: print(f"跳过无法检测的图片: {path}") continue # 每个角点的世界坐标和图像坐标必须一一对应 obj_points.append(objp) img_points.append(corners) flags = cv2.CALIB_ZERO_TANGENT_DIST # 返回标定误差、内参矩阵、畸变系数、旋转向量、平移向量 rms, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, (img_width, img_height), None, None, flags=flags ) return rms, camera_matrix, dist_coeffs, rvecs, tvecscalibrateCamera的参数不多,坑都在细节里。
flags参数是很多人忽略的地方。CALIB_ZERO_TANGENT_DIST表示切向畸变系数固定为 0,只优化径向畸变。对大多数普通镜头这是合理的选择,因为装配误差引入的切向畸变很难被稳定估计,强行估计反而会拉低径向畸变和外参的精度。如果你的镜头安装精度很高,或者你明确知道切向畸变很小,这个 flag 可以一直开。反过来,如果重投影误差一直降不到 0.1 像素以下,可以试一下去掉这个 flag,让 $p_1$ 和 $p_2$ 也参与优化,看误差有没有明显变化。
camera_matrix和dist_coeffs在最开始传入None,表示由算法自动估计。你也可以传入一个初值矩阵,比如把camera_matrix设成图像中心为 $c_x, c_y$、焦距设为图像宽度的矩阵,这样优化更容易收敛到好的局部极小值,但大多数情况下默认的零初值就够用。
另一个有用的 flag 是cv2.CALIB_FIX_K3。如果你用的是普通工业镜头而不是超广角镜头,$k_3$ 这 3 阶径向畸变项通常很小,让它参与优化反而可能产生过拟合。建议第一次跑时不开这个 flag,等看到 $k_3$ 的值,如果它的绝对值比 $k_1$ 小 2 个数量级以上,就加上CALIB_FIX_K3重新跑一次,对比两次的重投影误差。
下面是一个典型的内参输出:
| 参数 | 典型输出 | 解读 |
|---|---|---|
| fx | 1432.5 | 沿 x 方向的归一化焦距,单位像素 |
| fy | 1435.8 | 沿 y 方向的归一化焦距,单位像素 |
| cx | 961.2 | 主点 x 坐标,接近图像中心 960 |
| cy | 541.7 | 主点 y 坐标,接近图像中心 540 |
| k1 | -0.083 | 二阶径向畸变,负值对应桶形畸变 |
| k2 | 0.102 | 四阶径向畸变 |
| p1/p2 | 0(固定) | 切向畸变,被 CALIB_ZERO_TANGENT_DIST 固定 |
如果 $c_x$ 和 $c_y$ 离图像中心偏离超过 50 像素,先不要慌,这不一定说明标定有问题。很多镜头模组的 sensor 安装本身就有 1~2% 的偏移,对 1920x1080 的 sensor 来说,偏移几十个像素是正常范围。真正要警惕的是复标定结果是否稳定。
4.3 重投影误差与结果保存
标定完成后,用cv2.projectPoints把世界坐标点投影回图像平面,计算和提取角点的距离,得到逐张图的 RMS 误差。这是最直接的质量反馈。
def evaluate_calibration(obj_points, img_points, camera_matrix, dist_coeffs, rvecs, tvecs): total_error = 0.0 per_image_errors = [] for i in range(len(obj_points)): # 用当前内外参 + 畸变模型把世界坐标投影回像素坐标系 proj_points, _ = cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], camera_matrix, dist_coeffs ) # 求每个角点的投影误差,然后取平均 error = cv2.norm(img_points[i], proj_points, cv2.NORM_L2) / len(proj_points) per_image_errors.append(error) total_error += error * error rms = np.sqrt(total_error / len(obj_points)) return rms, per_image_errors # 重投影误差超过 0.3 像素的图,建议直接删除并重新标定 for idx, err in enumerate(per_image_errors): if err > 0.3: print(f"图像 {idx} 重投影误差 {err:.4f} 像素,建议检查")这里有一个重要的细节:cv2.norm计算的是两个点集之间的 L2 范数,然后除以角点数量,得到的是单张图的平均像素误差。这个值的单位是像素,数值大小和图像分辨率直接相关。同样的物理误差,在 500 万像素相机上对应的像素误差会比 200 万像素相机更小,所以不要跨相机比较这个数值。
标定结果保存时,建议把内参矩阵、畸变系数、图像分辨率、标定日期一起存成 YAML 文件。畸变系数要注明是 OpenCV 的 5 参数模型还是 8 参数模型,这关系到后续调用undistort时传给它的参数类型。我用的是 5 参数模型,保存格式如下:
image_width: 1920 image_height: 1080 camera_matrix: [1432.5, 0.0, 961.2, 0.0, 1435.8, 541.7, 0.0, 0.0, 1.0] dist_coeffs: [-0.083, 0.102, 0.0, 0.0, 0.0] rms_error: 0.082dist_coeffs里的五个值顺序固定:$k_1, k_2, p_1, p_2, k_3$。如果以后要在 OpenCV 之外的环境使用,比如 Unity 或者自己的 C++ 引擎,一定先确认它对畸变模型的定义是否和 OpenCV 一致,否则去畸变出来的图像就会失真。
5. 标定避坑指南:五个让标定结果翻车的真实案例
这一章把我在实际项目里踩过的坑、以及帮别人排查标定问题时撞见的典型问题整理出来。每一条都是先看到现象,再定位原因,最后给出解决路径,你可以按图索骥。
5.1 拍了 20 张图,重投影误差死活降不到 0.3 以下
现象:calibrateCamera的 RMS 在 0.4~0.6 像素徘徊,删掉误差大的图也没用。
原因:这是最常见的情况——标定板不平整。很多人用 A4 纸打印棋盘格直接贴墙上或者压在桌面上,纸张微小的波浪起伏在近景拍摄时会被放大。标定板平面假设被打破后,单应性矩阵本身就不准确,内参自然无法收敛。
解决:把标定板裱在平直的铝板或亚克力板上,用双面胶贴紧,确保表面没有气泡和褶皱。如果是临时用纸板,拍照时尽量把板子放平,不要手持悬空。用手指轻弹板子中央,听声音是实的还是空鼓的——这一步可以帮你快速判断板子是否完全贴合。从那以后我每拿到一块新标定板,第一件事就是在灯光下斜着看表面平整度。
5.2 标定完成,去畸变后图像边缘出现奇怪的波浪
现象:undistort之后,图像边缘的直线变弯了,而且弯曲方向不是简单的桶形或枕形,而是波浪状的。
原因:畸变系数估计过度。常见于用了过多的畸变项,比如同时让 $k_1, k_2, k_3$ 和 $p_1, p_2$ 全部自由优化,且拍摄的图像姿态分布不均,导致高维畸变空间被噪声主导。另一个常见原因是用鱼眼镜头参数模型去标定普通镜头,或者反过来,模型与镜头实际畸变形态不匹配。
解决:先固定切向畸变为 0(CALIB_ZERO_TANGENT_DIST),再固定 $k_3$ 为 0(CALIB_FIX_K3),只优化 $k_1, k_2$ 两个径向项。如果误差还偏高,再逐个放开。放开的顺序是:先放开 $k_3$,再放开 $p_1, p_2$,每放开一个就重新标定一次,看误差改善多少。这么做可以避免高维参数被噪声带偏。另外,检查拍摄的图像集合里,标定板是否覆盖了画面的四角和边缘——畸变信息主要来自这些区域,如果只拍了中央区域的图,边缘畸变就完全靠外推,给出来的结果当然不靠谱。
5.3 换了一次分辨率,内参矩阵没换,测距全都偏
现象:同一个相机,原来 1920x1080 下标定过,后来为了提升帧率改成 1280x720,直接用原来的内参去做测距,结果误差放大好几倍。
原因:这是新手最容易翻车的地方。内参矩阵里的 $f_x$ 和 $f_y$ 是像素单位的归一化焦距,分辨率变了,像素的物理尺寸不变,但像素坐标范围变了,$f_x, f_y, c_x, c_y$ 全部要按比例缩放。比如从 1920x1080 缩到 1280x720,缩放因子是 2/3,新的 $f_x = 1432.5 \times 2/3 \approx 955$,主点 $c_x = 961.2 \times 2/3 \approx 640.8$。如果不缩放直接沿用,相当于把相机模型套用在错误的坐标系上,测距误差会随距离线性放大。
解决:新分辨率必须重新标定。如果实在懒得拍,可以用等比缩放公式折算,但只适合精度要求不高的场景,且镜头的光学畸变在感光区域裁剪后不变,$k_1, k_2$ 这些畸变系数可以完全沿用,不需要缩放。我自己的习惯是:每切换一次分辨率,就花十分钟重新标定一遍,然后把它写进相机的配置文件管理流程里。等你有过两三次因为忘换内参导致整个测量系统报废的经历,就会理解这个习惯有多值钱。
5.4 角点检测总是失败,或者检出来的角点是歪的
现象:findChessboardCorners返回 False,或者返回的角点看起来像是每行错开了一格。
原因:棋盘格的行列数设置错了。OpenCV 的pattern_size指的是内角点数量,而不是格子数量。比如一张 10x7 格子的棋盘格,内角点是 9x6。如果你把pattern_size设成了 10x7,算法就会试图找一个 10x7 的内角点网格,自然对不上。还有一种情况是标定板边缘被截断,算法只能检测到部分格子,返回失败。错开一格的角点往往是因为标定板方向翻转后,检测算法从右侧开始计数,排序方向不一致。
解决:首先核对pattern_size。然后看标定板的格子数是不是标准的偶数排列。如果角点排序不稳定,在findChessboardCorners里试试cv2.CALIB_CB_EXHAUSTIVE标志,它会用更彻底的搜索策略,代价是耗时增加不少。另一个保险做法是:在drawChessboardCorners的可视化结果里逐张检查,用脚本把每张图的检测结果拼成大图扫一遍。网格错位这种问题,靠肉眼扫一眼比靠任何算法排查都快。
5.5 光照一变,重投影误差就变了
现象:上午在窗边拍的图,标定 RMS 0.08 像素;下午在灯光下补拍的几张图,加进去之后 RMS 涨到 0.25。
原因:光照不均导致角点提取的亚像素位置偏移。cornerSubPix本质上用的是局部灰度梯度加权,如果棋盘格表面光照分布严重不均匀——一边亮一边暗,或者有高光斑块——角点位置会被拉向亮度更高的方向。另外,如果标定板表面的颜色不是纯黑白,而是偏灰或者发黄,也会影响检测精度。
解决:保持光照均匀,避免阳光直射和点光源造成的硬阴影。如果必须在室外标定,优先选择阴天,或者用白色柔光布把标定板上方挡住。拍摄时让标定板表面亮度尽量和相机曝光设置匹配,不要出现大面积过曝或者欠曝。还有一点:不同时间拍的图放进同一个标定集合时,尽量保证它们的光照状态一致。不一致的话,不如分开做两套标定,分别验证精度。
6. 标定质量验证与进阶技巧:从能用走向好用
标定完并不算结束,验证标定结果是否可靠,以及在特定场景下怎么调整标定策略,是工程师之间拉开差距的地方。这一章给出两种验证方法,以及三个可以直接上手的进阶技巧。
第一个验证方法是残差图检查。把每张标定图像角点的重投影误差画成矢量图,观察误差的方向是否随机。如果误差方向呈现出明显的规律性——比如都朝向画面中心、或者都沿某一方向偏移——说明畸变模型没有完全覆盖镜头的真实畸变形态,可能是模型阶数不够,也可能是初始值陷入局部极小值。随机分布的误差则说明优化已经收敛到了比较理想的状态。这个检查用 OpenCV 的projectPoints配合 matplotlib 就能画。
第二个验证方法是三维重建验证。拿一个已知物理尺寸的物体,比如一块 100 毫米的标准量块,放在相机前方不同距离,用标定好的内参做单目位姿估计,然后反算量块的三维尺寸,看误差是否在合理范围内。这一步能直接检验内外参联合精度,比单独看重投影误差更有说服力。我在一个视觉引导的抓取项目里,就是用这个验证方法发现标定结果在远距离处误差飙升——后来排查发现是有一张图的棋盘格角点提取错误混进了标定集,重投影误差虽然不高,但那张图的外参带偏了整体优化。
进阶技巧方面,第一个是分阶段标定。对高精度测量场景,我一般先拍 20 张图做一次初始标定,扔掉误差最大的 3 到 5 张,用剩下的重新标定,然后再补拍一些覆盖之前空白区域的图,再做一次联合标定。两轮下来结果通常比一次性拍完直接标定要稳。
第二个技巧是合理使用 flags 组合。如果你确认镜头质量很好,可以尝试cv2.CALIB_ZERO_TANGENT_DIST + cv2.CALIB_FIX_K3,重投影误差会稍微变差一点点,但畸变场会更平滑,去畸变后图像的边缘质量更好。如果你的镜头是广角镜头,畸变很大,需要把CALIB_FIX_K3去掉,让高阶径向项参与拟合。
第三个技巧是变焦镜头的处理。变焦镜头在不同焦距下内参完全不同,没有任何一套标定参数可以覆盖整个变焦范围。我的做法是把变焦环用胶带固定在最常用的焦距位置,然后只针对这个位置标定。如果变焦是自动的,那就只能做多组标定,通过焦距反馈实时切换内参,工程复杂度会高很多。
从那以后,我每次标定完都会强制走一遍“残差图检查 + 实物验证”这个流程,确认内参真的能还原物理尺度,才敢把它写进算法里。这个习惯救过我很多次,希望你也能用上。希望帮到你。
本文还有配套的精品资源,点击获取