简介:面向VC++与OpenCV开发者的张正友标定实现资源包,适合需要处理镜头畸变、求解相机内参外参的初学者与相关工程人员。资源核心为一份完整的Calibrate.cpp源码,覆盖棋盘格图像采集、角点检测与亚像素细化、calibrateCamera参数求解,以及undistort畸变校正等关键步骤,代码简洁直观,可运行、可改造,非常适合对照学习;同时包含对图像质量、拍摄角度和数据处理的注意事项,有助于减少标定失败。rar压缩包内共13个文件,包括cpp源码、可执行exe、pdb/ilk调试符号、dsp/dsw工程配置等多种类型,能在Visual C++环境中打开调试,工程结构不臃肿,压缩后仅1.34MB,便于快速部署或教学演示,也可在此基础上继续扩展标定工具。已有253人浏览学习,对想理解张正友标定法在OpenCV中实际落地方式的开发者,这是一个极具参考价值的轻量实例。
1. 什么是张正友标定法,为什么要用它
1.1 先弄清你标定的到底是什么问题
做机器视觉、双目立体匹配、三维重建、SLAM或者机器人抓取,绕不开一件事:相机拍到的二维像素点,怎么换算成真实世界里的三维坐标。这个“换算规则”就是相机的内外参数。内参是焦距、主点、畸变系数,外参是相机在世界坐标系中的旋转和平移。张正友标定法就是目前最主流、最通用的相机标定方法,1998年由张正友提出,用一块平面棋盘格搞定内外参求解,速度快、精度高、操作门槛低。
拿个最朴素的例子来说——你想用机械臂去抓一个零件,相机看到的是某个像素坐标 (u, v),但这个点到底在机器人坐标系里是 (x, y, z) 多少,必须靠标定把相机内参定准,再配合外参做手眼标定,才能把像素坐标换到机器人坐标系。内参不准,后面任何一步都是白搭。张正友法的核心能力就是:给你一组带畸变的棋盘格照片,通过角点检测加单应性矩阵求解,算出相机的焦距、主点、畸变系数,顺带还能算出每张图对应的外参。
如果你是学生或者刚入门的工程师,第一次用OpenCV调calibrateCamera就能跑通流程,这很正常,但真正理解每一步背后的几何含义,才能在你换相机、换镜头、项目精度要求高的时候不慌。这篇文章就把张正友法的原理拆开,把实操细节和踩坑点讲清楚。
1.2 张正友巧妙在哪里
在张正友法出现之前,传统标定多用三维标定块,需要精密的立体靶标,一块金属箱子带多个已知三维坐标的方块,制作成本高且不能有丝毫变形。自标定(Self Calibration)又不依赖标定物,但鲁棒性差,用FaK波段等封闭约束求解,在实际场景里经常发散。张正友的办法是折中:只用一块平面棋盘格,通过多个视角拍摄,利用平面靶标上的共面点建立每张图的单应性矩阵,再线性求解内参初值,之后用最大似然估计做非线性优化,把畸变系数一起迭代出来。
它的巧妙之处在于把“平面”这件事用到了极致。棋盘格上所有角点都在同一平面上,于是Z坐标为0,这样单应性矩阵H就天然退化成一个 3×3 的矩阵,关键信息都被压缩在里面。张正友从H矩阵出发推导出一组约束方程,直接就能解出内参矩阵的闭式解。不是靠猜,不是靠迭代乱跑,而是先有解析解再把精度提上去。
2. 标定前准备:棋盘格和拍摄规范
2.1 棋盘格的选择与制作
棋盘格听起来简单,实际选型和制作有讲究。最常见的棋盘格是黑白交替方格,但需要注意两个关键词:内角点(interior corners)和格子数量。OpenCV的findChessboardCorners需要的尺寸是内角点的数量,比如你打印的棋盘格有 9×7 个格子,内角点就是 8×6。这是一个特别容易踩的坑——很多人直接把格子数填进去,结果角点检测就是找不对。
格子尺寸要结合你的场景来定。做近距离的机械臂抓取,格子边长 20mm 到 30mm 比较合适;如果用在工业测量或者远距离场景,边长可能需要 50mm 甚至更大。精度要求高的场景,不要把棋盘格打印在普通A4纸上,纸会变形、会反光、会褶皱,我都见过。最好用胶版纸打印后贴到硬质铝板或者亚克力板上,让靶标尽量平。你要是只是做着玩,打印在硬卡纸上也行,但心里要有数,精度上限就摆在那里。
镜头焦距和棋盘格的视场占比也有关系。标定时棋盘格在画面里至少要占三分之一以上,太小了角点检测容易糊,角点亚像素提取的精度也会下降。
2.2 一张合格标定图的拍摄要领
拍标定图看起来很简单:拿着棋盘格转一转就行。但拍得规不规范直接决定标定结果。我总结下来这几个要素缺一不可:
- 多角度:必须让棋盘格在相机前方呈现不同姿态,倾斜、旋转、俯仰都要有,这个角度变化至少要在20度以上,最好有45度左右的倾斜,不要只在一个平面上平移。
- 多距离:要有近景、中景、远景,让靶标尺寸在画面中占的比例有变化,这样畸变参数才约束得住。
- 多位置:要覆盖画面的中心和边缘,尤其要尽量靠近视野的四角,因为镜头的径向畸变在边缘最明显,如果你只把棋盘格放在画面中央,畸变系数基本就是靠猜。
- 光照均匀:避免强反光、局部过曝、阴影遮挡,棋盘格上的角点才会黑白分明。
平面靶标能不能有一点倾斜?可以,但不要太大,过度倾斜会让角点检测变得极其不稳定。一般视角不超过60度为好。
还有一个我踩过很多次的坑:拍的时候相机或者棋盘格在运动导致画面模糊。虽然findChessboardCorners对轻微模糊还有一定容忍度,但运动模糊会让cornerSubPix的亚像素定位精度大打折扣。我建议每拍一张就快速检查一下图像清晰度,模糊的直接删掉。
拍摄数量多少合适?我个人的标准是15到20张。少于10张,畸变模型容易过拟合;多于30张,带来的是边际收益递减,除非你要标定超大广角镜头。这里有一个常见的误区:拍得越多越好,其实关键是多角度、多距离,而不是同一角度多拍。
3. 实操步骤与代码实现
3.1 环境与核心流程概览
我用Python加OpenCV来演示。OpenCV从3.0开始集成了完整的张正友标定流程,接口是cv2.calibrateCamera。整个流程无非就是这几步:
- 准备棋盘格的物理尺寸,定义三维点世界坐标,Z全部设为0。
- 读取标定图像,转成灰度图。
findChessboardCorners找到棋盘格上的内角点,再用cornerSubPix做亚像素精化。calibrateCamera求解内参、畸变系数和每一帧的外参。- 用重投影误差评估标定质量,必要时剔除误差大的图像重标定。
这套流程你可能在无数教程里看到过,但很多人不知道每一步的默认参数和结果格式容易踩坑。比如findChessboardCorners返回的角点顺序,是从图像的左上角开始,按行排列的,但如果你旋转了棋盘格或者从后面拍摄,顺序可能不一致,这会直接影响标定结果,所以拍摄时尽量保持棋盘格在一个大致统一的朝向。
3.2 核心代码走一遍
先定义世界坐标系下的棋盘格角点坐标。这里的关键是单位要统一,我习惯用毫米。
import cv2 import numpy as np import glob # 棋盘格内角点尺寸:内角点 8x6,格子边长 25mm CHECKERBOARD = (8, 6) square_size = 25.0 objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp *= square_size objpoints = [] # 世界坐标系中的角点(每张图一个副本) imgpoints = [] # 图像坐标系中的角点(每张图一个副本) images = glob.glob('./calib_images/*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners( gray, CHECKERBOARD, cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_FAST_CHECK + cv2.CALIB_CB_NORMALIZE_IMAGE ) if ret: # 亚像素精化,这一步直接影响最终精度 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners)随后调用标定接口:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print("重投影误差 RMS:", ret) print("内参矩阵:\n", mtx) print("畸变系数:", dist.ravel())标定完后,如果你要来做去畸变,用cv2.undistort即可,但高性能应用里最好还是先算映射表:
h, w = img.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1) dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)initUndistortRectifyMap这一步的作用是预先把整幅图像的映射关系算出来,之后逐帧remap时速度会快很多,不会每帧都去解畸变方程。
这里有三个容易搞混的点:
gray.shape[::-1]是图像宽度和高度,注意OpenCV的习惯是 (width, height)。calibrateCamera返回的mtx顺序是 fx, fy, cx, cy,排列成 3×3 矩阵。dist的默认顺序是 [k1, k2, p1, p2, k3],只有当使用CALIB_RATIONAL_MODEL标志时才会返回更多畸变参数。
3.3 关键结果参数怎么判读
ret是重投影误差(RMS),单位是像素,它表示所有角点投影回世界坐标后与原始图像检测到的角点之间的平均偏差。我这几年标定过不少相机,经验是普通网络摄像头RMS小于0.5就算正常,工业相机和高质量镜头能做到0.1以内。
内参矩阵长这样:
[[fx 0 cx], [0 fy cy], [0 0 1]]fx和fy是归一化焦距(像素单位),cx和cy是主点坐标。如果你的相机宽和高分别是1280和1024,主点通常应该在(640, 512)附近。如果偏离很离谱,比如跑到了(900, 300),基本可以断定标定图数量不够或者多张图像质量有问题。
畸变系数里,k1、k2是径向畸变,p1、p2是切向畸变。普通镜头用4个参数就够;鱼眼镜头得用fisheye模块或者最多解到k6;如果你用了带大畸变的广角镜头,记得在calibrateCamera里加cv2.CALIB_RATIONAL_MODEL标志,把k3也解出来。
4. 张正友标定法和几种常见标定法对比
4.1 传统标定、自标定、张正友法怎么选
张正友法不是唯一的标定方式,但它的综合性价比在绝大多数场景里是最优的。横向对比几种主流方法,你可以更清楚什么时候该选它,什么时候该换别的:
| 方法 | 标定物 | 精度 | 灵活性 | 典型应用 |
|---|---|---|---|---|
| 传统标定(如Tsai两步法) | 三维立体靶标 | 高 | 低,需要精密靶标 | 实验室、高精度视觉测量 |
| 张正友法 | 平面棋盘格 | 高 | 高,靶标易制作 | 机器视觉、SLAM、三维重建 |
| 自标定 | 无 | 一般 | 高,无需靶标 | 未知场景、纯旋转采集 |
| DLT直接线性变换 | 6个以上3D-2D点对 | 中等 | 中等,需要空间点坐标 | 单应估计、标定初值 |
传统标定里的Tsai两步法很经典,第一步解线性参数,第二步考虑畸变再做非线性优化。但它依赖三维立体靶标,靶标加工精度不够的话,误差会直接传递给标定结果。很多实验室用亚克力加工的立体标定块,看上去很精致,实际上每个面的垂直度和间距精度也不一定比A4纸打印的棋盘格高到哪里去,这就是张正友法的明显优势——你只需要保证平面度,平面精度好控制得多,这也是它后来大规模取代传统立体靶标的原因。
自标定不需要标定物,通过移动相机采集多帧图像,利用绝对二次曲线/绝对二次曲面的约束来求解内参。听着很先进,但方程约束往往不够稳,容易出现数值发散,标定结果经常飘得离谱。我在三维重建项目里试过几次自标定,结论是:除非你完全没有标定条件,否则不要指望它给你高精度结果。
4.2 什么场景必须用张正友法
如果项目里涉及视觉测量,比如机械臂引导、尺寸检测、AGV定位、双目深度估计,张正友法几乎就是标配解法。原因在于它能在保证精度的情况下只要求一块平面靶标,适合工程现场灵活部署。
SfM和视觉SLAM场景中,张正友法也常用来先离线标定好内参,后续运行算法时固定内参,减少在线优化的自由度,提高系统稳定性。还有一个容易被忽视的点:即使你已经买了出厂带标定文件的工业相机,镜头一换,内参就变了,甚至同一颗镜头你动了调焦环,焦距和主点都会漂移,这时候还是得老老实实重新用张正友法标一遍。
5. 常见问题与排错实录
5.1 重投影误差太大怎么办
标定完一看RMS,超过了0.8甚至1.0,别急着改代码,先检查是哪些图像拖后腿。我会在标定结束后把每一张图的重投影误差单独打印出来,找出误差明显偏大的个别图,检查原因并剔除。
# 计算每张图的平均重投影误差 mean_error = 0 for i in range(len(objpoints)): imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) print(f"图像 {i} 重投影误差: {error:.4f}")我遇到过很多次,误差大的那张图,要么是拍摄时手抖导致棋盘格边缘模糊,要么是棋盘格反光导致角点检测偏移了几个像素。直接剔除后重新标定,效果立竿见影。
如果多张图误差都大,那就要复盘拍摄过程了。常见的拍摄问题包括:镜头对焦没有锁死、棋盘格不在同一个平面(纸弯了)、所有图像视角单一导致外参约束不够。特别是视角单一这个问题,我见过有人拍了30张图全是正对棋盘格的,误差照样大。
提示:标定前先检查每张图的角点检测质量,可以在图上画出角点保存下来人工抽检。这一步花不了两分钟,但能省掉后期一小时的排查时间。
5.2 角点检测频繁失败
findChessboardCorners找不到角点,是新手碰到最多的报错。原因主要有三类:
第一,棋盘格尺寸填错。注意是内角点数量,不是格子数量。如果你的棋盘格是 9×9 个格子,内角点就是 8×8。这里我建议一个自检方法:先在纸上数清楚横向和纵向的内角点,再填代码。
第二,图像和板的对比度不够。在光照很强的室外,白色格子反光变白,黑色格子受环境光影响变灰,角点检测容易崩溃。解决办法是拍摄时把CALIB_CB_NORMALIZE_IMAGE和CALIB_CB_ADAPTIVE_THRESH都加上,让OpenCV在检测前做图像归一化和自适应阈值分割。
第三,畸变太严重。广角镜头边缘的棋盘格会被扭曲得厉害,OpenCV自带的findChessboardCorners在大畸变情况下经常失败。解决办法有两个:一是把棋盘格尽量放在画面中心区域,边缘部分适当保留但不要勉强;二是可以用CALIB_CB_EXHAUSTIVE参数(8.0版本)让检测器多尝试一些排列组合,但速度会慢很多。
还有一个提升检测精度的技巧:对低分辨率图像,可以先放大再检测角点。比如640×480的图,用cv2.resize放大到1280×960,检测率会有明显提升,检测到的角点坐标缩小回来即可。
5.3 标定板拍摄的进阶技巧与避坑
拍标定图时,很多人喜欢把棋盘格拿在手里晃来晃去。实际拍摄时手部微动会导致图像轻微模糊,尤其在光照不足、快门速度低的情况下。我现在的标准做法是:把棋盘格贴在硬质板上,拍摄时让板靠住任意稳定支撑物,或者让另一只手托住板底。
还有一个容易忽略的点:标定过程中千万不能改变镜头的焦距。自动对焦镜头必须先锁定对焦,或者直接切换到手动对焦模式。工业相机有的可以固定光圈和对焦环,标定前一定要检查一遍。如果中途相机对焦发生了变化,那所有图像的焦距不一致,标定出来就是一团浆糊。
拍摄时棋盘格的方向也需要考虑。如果所有图像里棋盘格的长边都在水平方向,那图像的水平方向约束很好,垂直方向可能约束不足,导致fy的精度远差于fx。所以拍摄时要刻意旋转棋盘格,保证90度方向、45度对角线方向都有覆盖,让xy两个方向的内参都得到充分约束。
最后聊一个很多项目里实际遇到的问题:内参已知的情况下,怎么验证标定准不准。你可以随便拍一张棋盘格,用solvePnP解出外参,然后检测这张棋盘格角点的重投影误差,再看棋盘格的边缘在图像里是否明显弯曲。这个效果最直观,也最适合向不懂视觉的同事证明你的标定结果靠不靠谱。
个人体会是,张正友法你跑通一遍并不难,难的是在真实项目里让它稳定可靠。标定不是跑完calibrateCamera就结束了,而是要去理解每一张图、每一个参数、每一项误差指标背后的物理意义。多拍、多试、多对比,标定手感这个东西只能靠踩坑踩出来。如果你刚接触,别贪快,按文章里的规范拍20张图,标定完画出角点图检查一遍,再观察去畸变效果,走完这一轮,后面换相机、换镜头、上项目,你都会心里有底。
本文还有配套的精品资源,点击获取