news 2026/10/12 0:43:00

张正友标定法+OpenCV实战:相机内参标定与畸变校正指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
张正友标定法+OpenCV实战:相机内参标定与畸变校正指南

简介:这份资源是张正友相机标定法的OpenCV完整实现工程,面向计算机视觉初学者、图像处理课程学习者以及需要快速搭建标定实验的开发者,用于解决相机内参、外参求解与镜头畸变矫正问题。压缩包共93个文件、约13.83MB,包含2个cpp源码文件、vcxproj与sln工程文件、可执行程序,以及14张不同角度的bmp标定图、14张jpg图像和1张棋盘图,另附标定结果与数据txt文本,工程结构清晰、注释详尽,可一键运行。程序完整覆盖棋盘角点提取、标定板与图像坐标关系建立、相机参数计算、旋转与平移矩阵输出、重投影误差评价及原始棋盘图矫正等环节,读者可据此理解标定全流程并直接复用到3D重建、增强现实等场景。目前已有85人学习下载,适合作为课程实验或项目起步的参考范例。

1. 张正友标定法配 OpenCV:从一张棋盘图到可用的相机内参

你手里有一台相机,拍出来的图总带着说不清的畸变,做测量、做三维重建、做视觉定位时误差大得离谱,问题往往不在算法,而在相机内参压根没标定过。张正友标定法就是解决这件事的经典方案:打印一张棋盘图,从不同角度拍十几张,用 OpenCV 算出相机的内参矩阵和畸变系数。它不需要昂贵的标定板,也不需要精密位移台,一张 A4 纸加一台普通相机就能跑通。这篇笔记面向想真正把相机标定落地的人——不管你是刚装好 OpenCV 的新手,还是被畸变折腾过的老手,下面从棋盘图怎么准备、代码怎么写、参数怎么调、坑在哪,一步步讲清楚。

2. 棋盘图与标定原理:为什么一张纸就能算出相机内参

2.1 张正友标定法到底在算什么

相机成像可以用一个简单的模型描述:世界坐标系里的三维点,经过相机外参(旋转和平移)变换到相机坐标系,再经过内参矩阵投影到像素平面。内参矩阵长这样:

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

fx、fy 是焦距在 x、y 方向的像素表示,cx、cy 是主点(光轴与像平面的交点)的像素坐标。畸变系数则描述镜头带来的径向和切向偏移,OpenCV 里通常用 5 个参数 k1、k2、p1、p2、k3 表示。

张正友标定法的巧妙之处在于:棋盘图上的角点在世界坐标系里的位置是已知的(假设棋盘在 Z=0 平面上),每个角点在图像里的像素位置可以检测出来,这样就建立了一组 3D-2D 对应关系。单张棋盘图能提供足够的约束求解单应矩阵,多张不同姿态的棋盘图联立,就能把内参和畸变系数解出来。它不需要知道棋盘在空间中的精确位姿,只需要知道棋盘格子的物理尺寸。

常见做法是拍 10 到 20 张,棋盘覆盖图像的不同区域、不同倾斜角度。张正友原论文里用 5 个以上姿态就能得到稳定解,但实际工程里我一般会拍到 15 张以上,因为姿态多样性直接决定标定结果的鲁棒性。

2.2 棋盘图的制作与拍摄要点

棋盘图不是随便找张图打印就行。几个硬性要求:

第一,棋盘格必须是黑白分明、边缘锐利的方格,格子数量建议 9x6 或 11x8(内角点数量,不是格子数)。OpenCV 的findChessboardCorners检测的是内角点,也就是黑白格交界处的交叉点。如果你打印的是 10x7 的格子,内角点就是 9x6。

第二,打印后要贴在硬质平板上。纸张弯曲会让角点世界坐标的平面假设失效,标定出来的畸变系数会偏。我一般用亚克力板或硬纸板,用胶棒把打印纸完全贴合,边角不能翘。

第三,拍摄时棋盘要覆盖图像的不同区域。不要只拍中间,要拍到四个角和边缘,因为畸变在边缘最明显,边缘没有角点的话畸变系数约束不足。同时要有不同倾斜角度,纯正对相机的棋盘图对解算外参帮助有限。

第四,光照要均匀,避免反光和阴影。棋盘格局部过曝或过暗都会导致角点检测失败或偏移。室内用柔光灯从两侧打光比较稳。

第五,拍摄距离要覆盖实际使用范围。如果你标定的是近距离工作的相机,就不要在很远的地方拍棋盘,否则标定出的焦距和实际工作距离不匹配。

提示:棋盘格尺寸用尺子量准,比如每格 25mm,这个数值要填进代码里的square_size参数,量错了标定结果整体尺度就错了。

2.3 用 OpenCV 检测角点的最小代码

下面这段代码是标定流程的第一步:读入棋盘图,检测角点,并可视化确认检测是否正确。

import cv2 import numpy as np import glob # 棋盘内角点数量:行数 x 列数,注意是内角点不是格子数 pattern_size = (9, 6) # 每格物理尺寸,单位 mm,根据实际打印尺寸填写 square_size = 25.0 # 准备世界坐标系下的角点:Z=0 平面上的网格 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= square_size obj_points = [] # 3D 点 img_points = [] # 2D 点 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, pattern_size, None) if ret: # 亚像素级角点精化,窗口大小和迭代终止条件 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 可视化,按任意键看下一张 cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow('corners', img) cv2.waitKey(500) else: print(f'角点检测失败: {fname}') cv2.destroyAllWindows()

逻辑说明:findChessboardCorners返回布尔值和角点像素坐标,角点顺序是从左上到右下按行排列。cornerSubPix在初步角点附近做亚像素迭代,把精度从整像素提升到亚像素级,这一步对最终标定精度影响很大,不要省。criteria里 30 是最大迭代次数,0.001 是收敛阈值,窗口 (11,11) 是搜索区域,一般不用改。

参数说明:pattern_size必须和棋盘实际内角点数一致,写错了检测直接失败。square_size只影响世界坐标的尺度,不影响内参矩阵的数值,但如果你后续要做三维测量,这个值必须准确。

3. 完整标定程序:从角点检测到内参输出

3.1 标定主流程与 calibrateCamera 参数

角点收集完成后,调用cv2.calibrateCamera一次性解算内参、畸变系数和每张图的外参。

import cv2 import numpy as np import glob pattern_size = (9, 6) square_size = 25.0 objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= square_size obj_points = [] img_points = [] h, w = 0, 0 images = glob.glob('calib_images/*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w = gray.shape[:2] ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 标定:返回 RMS 重投影误差、内参矩阵、畸变系数、旋转向量、平移向量 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, (w, h), None, None ) print('RMS 重投影误差:', ret) print('内参矩阵:\n', mtx) print('畸变系数:', dist.ravel()) # 保存结果 np.savez('calib_result.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)

逻辑说明:calibrateCamera的输入是所有图像的 3D-2D 对应点、图像尺寸,输出内参和畸变。RMS 重投影误差是衡量标定质量的核心指标,它表示把 3D 点按标定结果投影回图像后,与检测到的 2D 点之间的平均像素距离。这个值越小越好,一般小于 0.5 像素算不错,小于 0.3 像素算很好。如果超过 1 像素,说明有图像角点检测不准或者姿态太单一。

参数说明:(w, h)是图像尺寸,必须和实际图像一致,否则主点坐标会错。None, None表示不预设内参初值,让 OpenCV 自己算。如果你知道大致焦距,可以传入初值加速收敛,但一般不需要。

3.2 用重投影误差逐张排查坏图

整体 RMS 达标不代表每张图都没问题。我习惯逐张算重投影误差,把误差大的图挑出来看。

import cv2 import numpy as np data = np.load('calib_result.npz') mtx, dist = data['mtx'], data['dist'] rvecs, tvecs = data['rvecs'], data['tvecs'] # 重新加载角点数据(实际使用时建议把 obj_points/img_points 也保存下来) # 这里假设你已经有了 obj_points 和 img_points for i in range(len(obj_points)): img_points_proj, _ = cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], mtx, dist ) error = cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2) / len(img_points_proj) print(f'第 {i} 张图重投影误差: {error:.4f} 像素')

逻辑说明:projectPoints把世界坐标点按标定得到的外参和内参投影到图像平面,再和实际检测的角点算 L2 距离。逐张看误差,如果某张图误差明显高于其他图,通常是那张图角点检测有偏移,或者棋盘在拍摄时动了、纸张不平。

参数说明:cv2.norm的NORM_L2是欧氏距离,除以角点数得到平均每点误差。这个值在 0.1 到 0.3 之间比较正常。

3.3 畸变校正与效果验证

标定完要验证畸变校正效果,最直接的方法是用undistort处理一张原图,看直线是否变直。

import cv2 import numpy as np data = np.load('calib_result.npz') mtx, dist = data['mtx'], data['dist'] img = cv2.imread('calib_images/test.jpg') h, w = img.shape[:2] # 方法一:直接 undistort undistorted = cv2.undistort(img, mtx, dist, None, mtx) # 方法二:先 getOptimalNewCameraMatrix 再 remap,可控制保留区域 new_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted2 = cv2.undistort(img, mtx, dist, None, new_mtx) cv2.imwrite('undistorted.jpg', undistorted) cv2.imwrite('undistorted_optimal.jpg', undistorted2)

逻辑说明:undistort内部先算映射表再重映射。getOptimalNewCameraMatrix的 alpha 参数控制保留多少边缘像素:alpha=0 只保留有效区域,会裁掉黑边;alpha=1 保留所有像素,但边缘会有黑边。实际工程里如果后续要做目标检测,一般用 alpha=0 裁掉黑边,避免黑边干扰。

参数说明:new_mtx是调整后的内参矩阵,和原始mtx不同,后续如果要用校正后的图像做三维重建,必须用new_mtx而不是mtx。

4. 避坑与排查:标定翻车的五个血泪经验

4.1 角点检测失败或检测到错误数量

现象:findChessboardCorners返回 False,或者返回 True 但角点顺序错乱。

原因:棋盘格对比度不够、光照不均、棋盘倾斜角度过大导致透视变形严重、棋盘格尺寸和pattern_size不匹配。还有一种常见情况是棋盘图边缘被裁掉,角点不完整。

解决:先确认pattern_size填的是内角点数而不是格子数。用cv2.imshow逐张看检测结果,失败的图单独拿出来分析。如果是个别图失败,直接剔除;如果大面积失败,重新拍。拍摄时保证棋盘完整出现在画面内,四周留白。

4.2 RMS 误差很小但实际校正效果很差

现象:标定报告 RMS 0.2 像素,但用undistort校正后直线还是弯的。

原因:标定用的棋盘图姿态太单一,全部正对相机或者全部在图像中央。这种情况下内参和畸变系数之间存在耦合,数学上误差小,但外推到边缘区域就失效。

解决:重新拍,确保棋盘覆盖图像四个角和边缘,倾斜角度多样化。我一般要求至少 5 张图里棋盘出现在不同象限,至少 3 张有明显倾斜。

4.3 畸变系数符号或量级异常

现象:标定出的 k1 是正数且很大,或者 k1、k2 量级差了好几个数量级。

原因:棋盘图纸张弯曲、拍摄时相机移动导致运动模糊、角点亚像素精化失败。另外,如果棋盘格物理尺寸填错,虽然不影响内参,但会影响外参尺度,间接影响畸变估计。

解决:检查棋盘是否平整,拍摄时用三脚架或稳定支撑。重新跑角点检测,确认每张图的角点都精确落在黑白格交叉点上。如果个别图角点偏移,剔除后重新标定。

4.4 不同次标定结果差异大

现象:同一台相机,两次标定得到的内参矩阵差别超过 5%。

原因:标定过程本身有随机性,尤其是图像数量少、姿态覆盖差的时候。另外,如果两次标定用的棋盘图物理尺寸不同,或者图像分辨率不同,结果也会不同。

解决:增加图像数量到 20 张以上,覆盖更多姿态。标定完成后保存结果,后续使用固定内参,不要每次用都重新标。如果必须重新标,用同一套拍摄规范。

4.5 用标定结果做 solvePnP 时位姿解算不稳定

现象:用solvePnP算相机位姿,结果跳动大,或者和实际不符。

原因:solvePnP依赖准确的内参和畸变系数。如果标定结果本身有偏差,位姿解算会被放大。另外,solvePnP用的 3D 点如果和标定时世界坐标系定义不一致,也会出错。

解决:先验证标定结果,用projectPoints检查重投影误差。确认solvePnP输入的 3D 点坐标和标定时objp的坐标系一致。如果用的是校正后的图像,solvePnP要传入new_mtx而不是原始mtx,并且畸变系数传 0。

5. 进阶技巧:用标定结果做单目测距与精度验证

标定不是终点,内参和畸变系数是后续所有视觉任务的基础。这里给一个用标定结果做单目测距的验证方法,顺便检验标定精度。

假设相机已经标定,现在要测一个已知物理尺寸的物体到相机的距离。用相似三角形原理:

import cv2 import numpy as np data = np.load('calib_result.npz') mtx, dist = data['mtx'], data['dist'] # 已知物体实际宽度 W,在图像中检测到的像素宽度 P # 距离 D = (W * fx) / P fx = mtx[0, 0] def estimate_distance(real_width_mm, pixel_width): return (real_width_mm * fx) / pixel_width # 示例:实际宽度 100mm,图像中占 200 像素 d = estimate_distance(100.0, 200.0) print(f'估计距离: {d:.2f} mm')

逻辑说明:这个公式假设物体平面平行于像平面,实际使用中如果物体有倾斜,需要先做透视校正。fx 从标定结果里取,所以标定精度直接决定测距精度。

参数说明:real_width_mm是物体实际物理宽度,pixel_width是在图像里量出来的像素宽度。这个方法的误差来源包括:标定误差、像素测量误差、物体倾斜。在 1 米以内,标定良好的相机测距误差可以控制在 2% 以内。

验证标定精度的另一个方法是拍一张已知尺寸的棋盘图,用undistort校正后重新检测角点,看角点间距是否和物理尺寸一致。如果偏差超过 1%,说明标定还有问题。

我自己的习惯是:每次标定完,一定拿一张没参与标定的棋盘图做验证,算重投影误差。如果验证图的误差比标定图的平均误差大很多,说明标定过拟合了,姿态覆盖不够,得重拍。这个习惯帮我省了很多事后返工的时间。希望帮到你。

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

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

331张图像小样本YOLO训练:行人车辆检测实战与部署边界

简介:这是一份面向YOLO系列目标检测学习者的行人车辆标注数据集,适用于yolov5、yolov8、yolov9、yolov7、yolov10及yolo11等主流算法,可直接用于模型训练与验证测试,适合入门练手或课程项目实践。资源包共994个文件,包…

作者头像 李华
网站建设 2026/10/12 0:24:55

Python深度学习驾驶员状态检测识别:从模型到工程落地

简介:这是一份Python基于深度学习的驾驶员状态检测识别项目源码与配套文档,适合计算机专业毕业生、开发者及需要项目实战的学习者。项目完整覆盖从数据预览、特征提取、模型微调到评估的流程,基于Keras实现多种经典卷积网络的迁移学习&#x…

作者头像 李华
网站建设 2026/10/12 0:24:34

Android 16 开发板 eth0 静态 IP 配置实战与避坑指南

我在嵌入式开发里和 Android 系统打交道的时间不短,最近手头有一台基于 Android 16 工程固件的开发板,遇到一个很典型的需求:要把有线网口 eth0 的 IP 固定下来,方便和上位机通信。老实说,如果在普通 Linux 服务器上&a…

作者头像 李华
网站建设 2026/10/12 0:22:01

Python景点数据分析系统:爬虫、数据库与可视化实战

简介:一套基于Python的热门景点数据分析与可视化系统项目实例,面向具备Python基础、希望掌握全栈式数据分析流程的研发人员与数据分析师,适用于文旅决策、景区运营优化和在线旅游平台推荐等场景。内容围绕完整项目闭环展开:从数据…

作者头像 李华
网站建设 2026/10/12 0:20:12

AnyPS5:多台PS5主机数据迁移与备份校验自动化工具指南

如果你手上同时有两台以上同世代的主机,我猜你大概率经历过这种时刻:客厅一台、书房一台,或者是换机时要把旧机器的数据倒腾到新机器上。官方自带的迁移功能能用,但流程非常啰嗦,备份完心里还没底——到底哪些东西备份…

作者头像 李华
网站建设 2026/10/12 0:19:59

Python的文件操作:读写文本文件

390 Python的文件操作:读写文本文件 程序处理的数据从哪来?存到哪去?答案是:文件。 不管是读取配置文件、写入日志、还是处理用户上传的数据,文件操作都是每个程序员的必备技能。 今天我们就来聊聊Python是怎么和文件打交道的。 一、打开和关闭文件 1.1 open()函数 …

作者头像 李华