news 2026/9/7 9:40:40

相机标定实战:张正友标定法配合OpenCV从棋盘格到内参全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
相机标定实战:张正友标定法配合OpenCV从棋盘格到内参全解

简介:一份基于OpenCV实现张正友相机标定的完整工程资源,面向需要学习相机标定原理与工程实现的开发者、研究人员,解决从标定板制作、图像采集到相机内外参计算与畸变矫正的全流程问题。压缩包共107个文件,大小约95.67MB,内容涵盖VS2015工程源码(cpp、vcxproj、sln、bat自动化脚本等)、棋盘格打印PDF、10张手机拍摄与20张摄像头拍摄的标定测试图,以及标定得到的mat矩阵与txt文本结果、运行日志等,结构清晰便于按需取用。工程代码注释详细,支持一键运行,能够输出相机内参、外参、旋转和平移矩阵,并附带标定效果评价模块;同时可使用标定结果对原始棋盘图进行矫正,直观展示算法效果,且标定结果与Matlab非常接近。资源还提供了pdb、ilk等调试文件,方便读者跟踪执行流程;测试图像覆盖不同拍摄设备,增强了标定的泛化参考价值。目前已有6805人浏览学习,适合计算机视觉方向的初学者与进阶者参考。 搞视觉的人基本都逃不过相机标定这一关,尤其是做机器人抓取、SLAM、三维重建或者自动驾驶感知的,拿到的相机如果内参不准,后面所有计算都像用一把没校零的尺子量东西。张正友标定法配合OpenCV实现,是目前最常用、也最适合入门的一套方案——你只需要一块棋盘格、一台相机、十来张照片,就能把相机的内参、畸变系数和外参全部求出来。这篇文章我会把整个流程拆开讲清楚,从棋盘格打印PDF怎么生成,到测试图片怎么采集,再到OpenCV代码一步步怎么写,最后附上我实际跑标定踩过的坑和排查技巧。无论你是刚接触视觉的新手,还是想系统梳理一遍标定细节的从业者,这篇都值得看完再动手。

1. 为什么是张正友标定法

1.1 相机标定到底在标什么

相机成像可以简单理解成把三维世界投影到二维像素平面,但这个投影不是理想的小孔成像,镜头本身会引入畸变,CMOS安装也会有轻微偏差。标定的核心就是求出一组参数,用来描述“空间点三维坐标 -> 像素坐标”的映射关系。这组参数分两部分:内参(焦距、主点、畸变系数)描述相机自身属性;外参(旋转矩阵、平移向量)描述相机在世界坐标系下的位姿。

很多人觉得现在工厂出厂的相机都有标定数据,没必要自己标,但实际使用中镜头装上之后,法兰距、镜头个体差异、温度变化都会让出厂参数失效。尤其是用了工业相机搭配不同镜头,或者手机摄像头模组,标定几乎是必做的第一步。张正友标定法之所以流行,是因为它只需要一块平面棋盘格,不需要昂贵的标定设备,精度却足够大多数场景使用。

1.2 张正友标定法的核心思路

张正友博士在1998年提出的这个方法,巧妙在把三维标定问题简化成了二维问题。标定板是平面,所以棋盘格上所有角点的世界坐标z分量可以设为0,这样原本需要求解的单应性矩阵H就变成8个自由度,通过多张不同角度的照片,可以线性求解出内参和外参的初值,再做非线性优化得到精确解。

这种方法降低了对标定设备的要求,你不需要知道相机与标定板之间的精确距离,也不需要像传统两步法那样做复杂的三维靶标。你只需要不断变换棋盘格的姿态、角度、位置,拍下十几张照片,OpenCV就能算出结果。原理上它是介于传统标定和自标定之间的一种折中方案,精度比自标定高,操作又比传统标定简单,所以成了事实上的工业标准。

1.3 棋盘格为什么是首选

棋盘格的高对比度黑白方块让角点检测非常稳定,OpenCV的findChessboardCorners在多数光照条件下都能找到角点。而且棋盘格角点是两个方块的交叉点,亚像素级定位相对容易,配合cornerSubPix可以进一步把角点精度做到0.1像素以内。

相比圆点标定板,棋盘格的角点有明确的“顺序性”,OpenCV能自动按行列排序输出,所以代码实现起来很简单。另外,棋盘格对打印设备的精度要求不高,普通打印机加上硬质底板就能用,这点对个人开发者非常友好。你要是做高精度的工业视觉,可以买玻璃或陶瓷基板的标定板,但学习和验证阶段自己打印完全够用。

2. 标定前的准备:棋盘格与测试图片

2.1 自己生成棋盘格打印PDF

张正友标定用的棋盘格,严格来说要定义两个关键参数:内角点数(intersection points count)和方格边长。内角点数指的是黑白格子交叉点的数量,不是格子数量。例如一个10x7的格子阵列,内角点是9x6,OpenCV中通常写作(9, 6)。采集图片时,程序需要检测这些内角点,所以这个数必须和实际标定板完全一致,否则检测不到。

我用Python生成一张可以直接打印的棋盘格图片,核心代码如下:

import cv2 import numpy as np # 内角点数 (width, height) board_size = (9, 6) # 每个方格的边长,单位mm,对应打印后实际尺寸 square_size_mm = 25 # 每个方格在图片中的像素大小,决定图片分辨率 pixels_per_square = 120 margin = 60 # 四周留白,方便裁剪 cols = board_size[0] + 1 rows = board_size[1] + 1 img_w = cols * pixels_per_square + margin * 2 img_h = rows * pixels_per_square + margin * 2 chessboard = np.ones((img_h, img_w), dtype=np.uint8) * 255 for i in range(rows): for j in range(cols): if (i + j) % 2 == 0: x0 = margin + j * pixels_per_square y0 = margin + i * pixels_per_square chessboard[y0:y0 + pixels_per_square, x0:x0 + pixels_per_square] = 0 cv2.imwrite('chessboard.png', chessboard)

生成图片后,打印时务必关掉“适应页面”或“缩放以适合”选项,按100%比例打印,然后量一下实际格子的边长,把square_size_mm改成实测值。因为纸张在打印过程中会有伸缩,如果你需要高精度标定,这一步不能省。打印好后把纸贴在平整的硬板上,不能有褶皱,否则角点坐标本身就带了误差,后面标定精度会大打折扣。

2.2 采集测试图片的要点

有了标定板,接下来就是采集图片。这是整个标定流程里最容易被忽视、却最影响效果的环节。我一般建议拍15到25张图,太少的话内参解算不稳定,太多也没有必要。拍摄时要遵循几个原则:

  • 标定板要占画面面积的1/4到1/3以上,太小了角点检测会不稳定。
  • 每一张照片里,标定板的角度、距离和位置尽量不同,要有左右倾斜、上下倾斜、远近变化、平移变化。
  • 避免所有图片都让标定板正对着相机,否则外参退化,内参也解不准。
  • 保证光照均匀,不要有强反光,不要有阴影压住角点。
  • 手持标定板就行,相机固定,人移动标定板即可,也可以反过来,相机移动而标定板固定。

如果你用的是USB摄像头或手机相机,记得关闭自动对焦、自动曝光和自动白平衡,固定变焦和曝光参数后再拍摄。对焦模糊的图片直接删除,不要勉强用,因为亚像素角点检测依赖边缘锐度,模糊图会让角点定位产生偏差。我自己的经验是,把视频流打开,一边观察角点检测效果一边采集,会比盲拍一堆再筛选高效得多。

3. OpenCV标定代码实现与参数解析

3.1 角点检测与亚像素精细化

OpenCV标定的标准流程分三步:检测每张图的角点、亚像素精细化、传入calibrateCamera解算参数。第一步是基础,我通常这样写:

import cv2 import numpy as np import glob board_size = (9, 6) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp = np.zeros((board_size[0] * board_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:board_size[0], 0:board_size[1]].T.reshape(-1, 2)

objp存放的是棋盘格角点的三维坐标。这里有一个常见的理解误区:为什么这些坐标不是实际的毫米数值?因为张正友标定法里,objp的尺度是可以任意缩放的,你写单位长度1、2、3和写25、50、75,最终得到的内参矩阵焦距会有对应倍数变化,但畸变系数和归一化坐标结果不变。所以更标准的做法是直接用实际物理尺寸,也就是乘以square_size_mm,这样得到的焦距单位就是像素,没什么问题。

接下来读取图片并找角点:

image_files = glob.glob('calib_images/*.jpg') objpoints = [] imgpoints = [] for fname in image_files: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, board_size, None) if ret: corners_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners_sub) cv2.drawChessboardCorners(img, board_size, corners_sub, ret) cv2.imshow('check', img) cv2.waitKey(100) else: print(f'角点检测失败: {fname}') cv2.destroyAllWindows()

findChessboardCorners返回的corners已经按行排列,顺序是从左上角开始横向扫描,这依赖于OpenCV内部的四边形拓扑分析。你可以打印一下corners的shape,通常是(54, 1, 2),对应54个角点,每个点两个坐标。角点检测失败时不要跳过就完事,最好看一下原因,见第5节。

3.2 调用calibrateCamera完成标定

角点全部检测完成后,用calibrateCamera一次性解算所有参数:

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print('内参矩阵:') print(mtx) print('畸变系数:') print(dist) print('重投影误差:', ret)

这里必须解释一下每个返回值的含义。mtx是3x3内参矩阵,形如:

[[fx, 0, cx], [ 0, fy, cy], [ 0, 0, 1]]

fxfy是等效焦距,单位是像素,理想情况下两者接近;cxcy是主点坐标,理想情况下在图像中心附近,但实际会有偏移。dist是畸变系数,OpenCV默认包含5项(k1, k2, p1, p2, k3),前两个是径向畸变,中间两个是切向畸变,第五个是薄棱镜畸变的高阶项。rvecstvecs是每张图片对应的旋转向量和平移向量,也就是外参,每张图对应一组。

特别注意ret返回值,它是所有角点在归一化相机坐标系下的重投影均方根误差,单位是像素。这个值越小越好,一般低于0.5就算不错,低于0.1是相当好的结果。如果大于1,说明标定数据里有脏数据,需要排查。

3.3 为什么要用亚像素精细化

可能有人会问,直接用findChessboardCorners检测到的角点不行吗?实际上,初始检测的角点精度受图像分辨率、模糊程度影响,只能到像素级,而张正友标定法对标定板角点的坐标精度非常敏感,微小偏差会被放大到内参上。cornerSubPix通过迭代求取灰度梯度的交点,能把角点定位精度提高到亚像素级别。

这个函数需要几个参数:搜索窗口大小(11, 11)表示在每个角点周围11x11像素范围内微调;(-1, -1)表示没有零迭代区;criteria是迭代停止条件,一般用最大迭代次数30次和最小误差0.001。窗口大小不是越大越好,对于高分辨率图像可以用(15, 15),但窗口太大反而容易受到邻近角点影响。

如果你用的是灰度图,cornerSubPix直接处理即可。如果彩色图有严重的色差,可以转成灰度再操作,OpenCV内部计算也是基于灰度梯度的。还有一个小技巧:对图片先做一次cv2.cvtColor后,如果检测效果不好,可以尝试用cv2.adaptiveThreshold对图像做预处理,增强棋盘格边缘,但绝大多数场景下不需要。

4. 实操结果分析:怎么判断标定质量

4.1 读懂内参矩阵和畸变系数

标定完成后第一件事,不是记录参数,而是验证参数是否合理。内参矩阵里的fxfy应该很接近,因为它们分别代表x、y方向归一化焦距,虽然镜头可能略有各向异性,但差值一般不会超过几个百分点。如果你用的是普通工业镜头,fxfy相差10%以上,多半是标定板图片姿态覆盖不够,或者代码里objp的坐标方向定义错了。

主点cxcy一般在图像中心附近,比如1280x960的图像,主点大约在(640, 480)附近,偏差几十上百个像素是正常的。如果主点跑到图像边缘甚至外面,那就说明标定失败,需要重新检查数据源。畸变系数k1k2一般很小,在-0.1到0.1之间都算合理,如果出现几十几百的离谱值,基本是求解迭代出问题了。

更直观的验证方法是把标定结果用于图像去畸变,对比一下效果:

h, w = img.shape[:2] new_camera_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted = cv2.undistort(img, mtx, dist, None, new_camera_mtx)

这里可以观察画面边缘的直线是否被拉直。棋盘格的边、背景里的门框、墙线都是很好的参考。如果去畸变后边缘的弯曲消失,说明标定结果基本可信。

4.2 重投影误差与图像数量的关系

重投影误差是标定结果最直接的量化指标。它的计算思路是:用求解出的内参、外参和畸变系数,把每张标定板上的三维角点投影到像素平面,计算投影点与实际检测角点之间的距离。误差小于0.5像素说明内参和外参之间的一致性很好,但要注意,重投影误差小不代表你的相机内参一定准确,因为它只衡量“自洽性”。比如所有标定板都放在画面中央,重投影误差可能很小,但内参外推能力很差,换一个角度就不准了。

所以标定过程中,图片的数量和角度覆盖比单张图片的角点精度更重要。15到25张图已经是工程上的经验值,少于10张容易过拟合;超过30张如果没有新的角度信息,改善也有限。如果你发现重投影误差偏大,先检查是不是有某张图角点检测异常,把那张图剔除后重新标定,往往误差就能降下来。

5. 常见问题与排查技巧

5.1 角点检测失败的原因

findChessboardCorners返回False是新手最常见的卡点。我总结了几种高频原因:

  • 棋盘格内角点数定义错误。比如实际棋盘格是10x7格子,内角点数是9x6,你却写了(10, 7),那永远检测不到。
  • 标定板在画面中太小,角点间距只有几个像素,OpenCV的四边形检测无法稳定识别。解决办法是让标定板靠近镜头,或者提高拍摄分辨率。
  • 光照不均匀或者反光,导致某些角点对比度太低。可以尝试调整光源方向,或者用cv2.adaptiveThreshold预处理。
  • 图片模糊或者运动拖影,角点边缘不锐利。这种情况只能重新采集。
  • 标定板没有完整出现在画面里,边缘的角点被裁掉。务必保证所有内角点都在画面内,且有少量边距。

排查时可以先对单张图做可视化,把检测到的角点画出来,确定是检测算法的问题还是图像本身的问题。

5.2 标定结果漂移、误差大怎么处理

如果重投影误差大于1像素,或者内参明显不合理,我建议按顺序做三件事:

第一,检查所有图片中,棋盘格姿态是否足够丰富。光有不同远近、没有旋转倾斜的图片,外参估计会病态,内参也不可靠。最好保证棋盘格在每张图里都有明显的朝向变化,比如绕x轴、y轴各转15到30度。第二,剔除角点检测质量差的图片。可以在findChessboardCorners之后,加一个cv2.cornerSubPix前后的角点坐标差值统计,差异超过0.5像素的图直接标记出来。第三,尝试降低畸变模型阶数。OpenCV默认用5个畸变系数,但普通镜头用3个或2个往往更稳定。代码里可以传calibrateCamera(..., flags=cv2.CALIB_ZERO_TANGENT_DIST)只估计径向畸变,或者用CALIB_FIX_K3固定k3为0,减少参数过多带来的波动。

5.3 双目、红外等扩展场景的注意点

很多人标定完单目之后想继续做双目,或者用红外相机。双目标定不是简单的两个单目拼起来,而是在单目内参基础上再做立体标定,用cv2.stereoCalibrate求出两个相机之间的旋转和平移。张正友标定法同样适用,但需要左右相机同时采集同一块棋盘格,所以采集时要保证标定板在左右画面中同时可见,且尽量覆盖公共视野。如果你用红外相机,注意红外光的波长会让镜头畸变跟可见光不同,必须单独标定,不能直接复用可见光的参数。

至于热搜词里提到的Kalibr标定,那是VIO/SLAM领域更常用的标定工具,它支持标定相机内参、相机与IMU之间的外参。原理上和张正友标定法同源,但对时间戳同步、IMU残差处理有额外要求。如果你在做SLAM,可以先按本文把单目内参和畸变系数标准,再用Kalibr完成协同标定,两者并不冲突。

6. 最后再分享一点实操体会

我在实际项目里标过几十台相机,最大的感受是:张正友标定法本身已经很成熟,出问题的地方基本都在数据采集端。与其反复调代码参数,不如多花时间把标定板贴平、把图片拍好、把角度覆盖到位。自己打印棋盘格时,千万不要用普通的喷墨纸直接平铺在桌上,稍微翘起一角,角点在空间上就不再共面,最终标定的主点会明显偏移。我一般会把打印好的格子纸用双面胶贴在亚克力板或者玻璃板上,边缘压平,再量一下实际尺寸填入代码。

另外,标定完别急着删除测试图片,我习惯把每张图的角点检测结果和重投影误差都保存下来,作为一份“数据报告”。如果后续发现某个镜头换过或者焦距动过,还能快速定位是哪一批图出了问题。最后再说一个小技巧:标定代码里把objp的坐标写成实际毫米值,不仅方便和机械臂、移液平台等对量的场景对接,也便于团队成员之间交叉验证结果。希望这份流程能帮你少踩几个坑,一次跑通标定。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 9:39:23

动力电池CCS设计全解析:从概念区分到工程验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:39:18

WPS正则表达式三入口:通配符、VBA与JS宏引擎差异详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:38:42

规则集启用后为什么不生效?完整指南

规则集启用后为什么不生效?完整指南 【免费下载链接】docs The open-source repo for docs.github.com 项目地址: https://gitcode.com/GitHub_Trending/do/docs 规则集(Ruleset)显示"已启用",推送却照样通过、合并照样被拦&#xff0c…

作者头像 李华
网站建设 2026/9/7 9:38:02

WPF ListBox拖拽实战:从事件路由到MVVM数据绑定的完整指南

简介:面向WPF初中级开发者的拖拽交互示例包,围绕两个ListBox之间的Drag & Drop功能展开,涵盖跨列表移动元素、上下按钮调整顺序、拖动时改变边框颜色等常见交互场景。包内共37个文件,以cs源码、xaml界面定义为主,并…

作者头像 李华
网站建设 2026/9/7 9:35:26

C标准库源码深度拆解:从malloc到printf的内存与格式化底层原理

简介:面向 C/C 学习者和系统程序员的 C 标准库源代码包,完整覆盖标准输入输出、字符串处理、内存管理、数学运算、时间日期及文件系统接口等模块,解决深入理解库函数底层实现与 C 语言运行机制的学习需求。整个压缩包共 1266 个文件&#xff…

作者头像 李华
网站建设 2026/9/7 9:33:21

Proxyman v6.16.0实战:Mac下HTTP/HTTPS抓包与解密指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华