OpenCV相机标定实战:张正友标定法原理与代码实现 简介一套完整的张正友相机标定OpenCV实现源码基于Visual Studio工程组织面向计算机视觉入门及中级开发者适合需要掌握相机标定、内参外参求解与畸变矫正的读者。代码附带详细注释并配有棋盘图和14张不同角度标定图可一键运行直接输出相机内参、外参、旋转和平移矩阵同时提供标定效果评价功能并能调用标定结果对原始棋盘图进行矫正形成从标定到应用的完整流程。压缩包共92个文件大小10.26MB以cpp源码、sln/vcxproj工程文件、bmp/jpg标定图片、txt结果记录及exe可执行程序为主tlog、pdb等为编译调试辅助文件解压后可直接打开Visual Studio工程并运行便于对照源码理解每个环节。目前已有22719人学习下载是学习张正友标定原理与OpenCV落地的实用参考尤其适合课程设计、毕业设计或项目预研时复用和二次扩展。 去年做视觉测距项目时相机标定是绕不过去的一步。我用张正友相机标定方法配合OpenCV的calibrateCamera完成了整套流程整理了一份带完整程序和自绘棋盘图的源码这篇文章把标定原理、代码实现和我在实际项目中踩过的坑一次性写清楚。这套方案能解决的问题很直接确定相机内参焦距、主点、畸变系数径向、切向并对图像做畸变校正。适合正在做单目测距、双目视觉、AR叠加、机器人抓取定位的朋友参考。前提是你需要一张打印出来的棋盘格以及一个能固定拍照的相机手机也可以。1. 项目概述与标定原理1.1 张正友标定解决什么问题相机标定本质上是在求解一个“映射关系”。三维世界里的点通过镜头投影到二维图像传感器上这个投影过程由相机内参和外参共同决定。内参是相机本身的属性包括焦距、主点位置、畸变系数外参是相机相对世界坐标系的旋转和平移。张正友标定法的价值在于它只需要拍摄一个平面棋盘格的不同姿态就能解算出全部内参和外参。相比传统标定需要高精度三维标定物棋盘格只需要普通打印机打出来贴在硬纸板上门槛低得多精度也足够日常视觉项目使用。OpenCV的calibrateCamera函数底层采用的就是这个思路我们调API时不需要自己推导公式但理解模型对参数调优有很大帮助。1.2 核心数学模型与参数意义可以把相机简化为小孔成像模型。空间中的某个3D点M经过相机内参矩阵K、外参旋转矩阵R和平移向量t最终投影到图像点m。张正友方法利用棋盘格平面上的点z0这一约束将投影关系简化为单应矩阵Hsm K[r1, r2, t]*M这里的H矩阵是一个3x3矩阵每一张棋盘格照片可以提供一个单应矩阵。内参矩阵K包含fx、fyx、y方向以像素为单位的焦距、cx、cy主点坐标即光轴与成像平面的交点。多张不同角度的照片联立方程组就能解出fx、fy、cx、cy和畸变系数。畸变系数里k1、k2是径向畸变系数对应镜头透镜形状带来的桶形或枕形畸变p1、p2是切向畸变系数对应镜头和成像平面不平行导致的装配误差。对于多数普通镜头k1、k2影响最大切向畸变通常很小。理解这些参数后标定程序输出的结果就不再是一堆无意义的数字而是可以逐项验证的物理量。2. 环境准备与棋盘图制作2.1 OpenCV环境搭建我平时用Python居多安装OpenCV非常简单pip install opencv-python opencv-contrib-pythonopencv-python是主库opencv-contrib-python包含了一些扩展模块标定用不到扩展模块但一起装上能避免以后用到sift等算法时再补一次安装。C用户可以通过vcpkg安装vcpkg install opencv4安装时需要确认版本OpenCV 4.x和3.x的API略有差异文中的代码在4.x版本下测试通过3.x需要把findChessboardCorners的调用方式做小改动3.x之前不需要传入CALIB_CB_EXHAUSTIVE等flags。安装完成后建议先跑一个简单脚本读图确认环境正常再进入下一步。2.2 棋盘图生成直接可用的代码项目源码里附带了我自己写的棋盘图生成脚本不需要去网上找图避免打印时因为分辨率被拉伸导致格子比例不对。生成逻辑是计算每个格子的像素范围交替填充黑白色import cv2 import numpy as np def generate_chessboard(cols, rows, square_size, margin50): # cols, rows 表示内角点数量 # 例如 9x6 表示横向9个内角点纵向6个内角点棋盘格实际为10x7格 width cols * square_size margin * 2 height rows * square_size margin * 2 img np.full((height, width), 255, dtypenp.uint8) for i in range(rows 1): for j in range(cols 1): if (i j) % 2 0: x_start margin j * square_size y_start margin i * square_size img[y_start:y_start square_size, x_start:x_start square_size] 0 return img # 生成内角点9x6、每个格子30mm、像素边长300px的棋盘图 # 这里物理尺寸和像素尺寸的比例需要手动控制打印时按实际尺寸缩放即可 img generate_chessboard(9, 6, 300, 100) cv2.imwrite(chessboard_9x6.png, img) print(棋盘图生成完成尺寸, img.shape)注意代码里的cols、rows指定的是内角点数实际棋盘格是(cols1)x(rows1)个格子。生成图片后打印时尽量保持格子是正方形A4纸打印时如果比例不对角点依然能检测到但标定出的fx和fy比例会偏后续需要修正。2.3 棋盘格的选择要诀棋盘格不是随便打印就行的。我第一次做标定直接拿办公A4纸打印贴在墙上拍结果角点经常检测失败。核心原因有三个纸张不平整导致局部反光、图案面积太小、格子数设置错误。实用建议是用A4纸打印后贴在硬纸板或亚克力板上表面一定要平整。格子大小建议20-40mm照片中棋盘格区域占到画面的三分之一以上。内角点数选择9x6或10x7比较合适太少标定不稳定太多角点容易被误检。如果是手机镜头这类广角镜头要考虑边缘畸变棋盘格图案尽量覆盖画面边缘区域。3. 标定完整程序实现3.1 标定主流程整个标定过程分四步第一步读取所有标定照片第二步对每张照片查找棋盘格角点并通过亚像素精化提高坐标精度第三步将所有角点坐标和对应的三维世界坐标送入calibrateCamera第四步计算重投影误差评估标定质量。第三步中的三维世界坐标并不是真实尺寸而是以棋盘格建立坐标系后每格一个单位的坐标。比如9x6的棋盘格第一个角点坐标是(0,0)向右是(1,0)、(2,0)向上是(0,1)、(0,2)单位是“格”。这个坐标和真实物理尺寸之间差一个比例系数不影响内参标定结果因为fx、fy本身是焦距与像素尺寸的比值单位格的实际毫米数会在后续应用中被折算掉。3.2 完整源码下面是项目里可独立运行的Python脚本只需要把图片路径改成你自己的即可import cv2 import numpy as np import glob CHECKERBOARD (9, 6) # 内角点数 square_size 1.0 # 世界坐标系中每个格子的边长单位可任意设定 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) 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) print(找到图片数量, len(images)) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHECKERBOARD, None) if ret: corners2 cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria ) objpoints.append(objp) imgpoints.append(corners2) cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret) cv2.imshow(corners, img) cv2.waitKey(100) else: print(角点检测失败, fname) cv2.destroyAllWindows() if len(objpoints) 0: print(没有可用的标定图片程序退出) exit() ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print(标定误差重投影误差, ret) print(内参矩阵\n, mtx) print(畸变系数, dist.ravel()) # 计算平均重投影误差 mean_error 0 for i in range(len(objpoints)): imgpoints_proj, _ cv2.projectPoints( objpoints[i], rvecs[i], tvecs[i], mtx, dist ) error cv2.norm(imgpoints[i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj) mean_error error print(平均重投影误差, mean_error / len(objpoints)) # 畸变校正示例 img cv2.imread(images[0]) h, w img.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) dst cv2.undistort(img, mtx, dist, None, newcameramtx) cv2.imwrite(undistorted.jpg, dst) print(畸变校正结果已保存为 undistorted.jpg)这个脚本包含了从图像读取到结果输出的完整链路。其中cornerSubPix亚像素精化特别关键直接使用findChessboardCorners返回的角点是像素级精度标定结果波动大加入亚像素精化后误差能明显下降。3.3 核心参数与结果解读calibrateCamera返回的ret值是整体重投影误差代表每个角点重投影坐标与实际检测坐标的平均像素距离。这个值低于0.3像素说明标定质量很好0.3-0.5像素属于正常范围超过0.5像素就需要检查图像质量和角点检测问题了。标定结果中内参矩阵mtx的结构为参数含义重要观察点fxx方向焦距像素与fy接近说明像素是正方形fyy方向焦距像素与fx相差过大需检查棋盘打印比例cx主点x坐标一般接近图像宽度/2cy主点y坐标一般接近图像高度/2k1二阶径向畸变负值表示桶形畸变正值表示枕形畸变k2四阶径向畸变通常比k1小一个量级p1, p2切向畸变系数数值很小说明镜头装配良好我实际标定过一个1600x1200分辨率的工业相机输出类似这样内参矩阵 [[1452.307, 0, 798.452], [0, 1450.118, 601.337], [0, 0, 1]] 畸变系数 [-0.1045, 0.1298, -0.0003, 0.0011, -0.0452]fx和fy非常接近主点坐标约为(798, 601)和理论上图像中心(800, 600)偏差很小说明这颗镜头装配质量不错。k1为负值表明存在轻微的桶形畸变属于广角镜头的正常表现。4. 实操要点与问题排查4.1 图像采集要求采集图像是影响标定质量最大的环节。我分享一下现在的采集流程把棋盘格贴在硬纸板上相机固定在三脚架上拍摄时不断变化棋盘格的姿态。具体做法是让棋盘格在画面中做三组运动平面内旋转、向前后倾斜、向左右倾斜。每种姿态拍摄2-3张总共15-20张。不建议把相机拿在手里拍棋盘格因为相机晃动会导致图像模糊角点亚像素精化失效。也不要只在一个位置变换棋盘格角度如果所有棋盘格都在画面中心边缘畸变就无法被约束标定出的畸变系数会偏小。拍摄时保证光线均匀避免棋盘格表面出现强烈反光。如果反光无法避免可以适当调整相机曝光补偿宁可让白色格子略微偏灰也不要出现高光溢出。4.2 常见问题速查表问题可能原因解决方案findChessboardCorners返回False棋盘格未完整出现在画面中调整拍摄距离确保棋盘格完整且占画面1/3以上角点检测失败但图案完整反光、模糊、分辨率过低改善光照检查对焦提高图片分辨率重投影误差大于0.5像素角点亚像素精化失败降低cornerSubPix窗口尺寸或检查棋盘是否平整每次标定结果差异大图片数量太少或姿态重复增加到20张以上覆盖不同倾斜角度fx和fy相差超过5%棋盘格打印不是正方形用游标卡尺测量格子实际边长修正square_size切向畸变p1、p2过大镜头装配有问题或图片过少增加图片数量检查是否混入了模糊图像图片质量筛选是我后期比较重视的一环。代码里可以加一个判断在角点检测成功后计算角点区域的梯度均值如果太小就丢弃这张图。这个方法能自动排除失焦图像但代码会复杂一些初学阶段手动筛选更直接。4.3 提升精度的独家经验踩过几次坑之后我总结了几条高效经验。第一准备标定板时打印机的色彩管理选择“黑白打印”而不是“灰度打印”黑白打印的黑色区域更均匀对比度更高角点提取更稳定。第二在calibrateCamera里可以按需调整flags。比如传感器像素已知是正方形时加上cv2.CALIB_FIX_ASPECT_RATIO可以约束fxfy减少参数搜索维度。如果镜头畸变很小加上cv2.CALIB_ZERO_TANGENT_DIST可以强制切向畸变系数为零避免过拟合。我通常先不带任何flags跑一遍观察畸变系数是否合理再做针对性约束。第三项目源码里如果重投影误差不理想先不要急着调代码优先检查图像本身。把角点绘制结果一张一张翻过去看注意力放在棋盘边缘的格子是否出现锯齿、角点是否明显偏移。很多时候问题出在打印纸翘边导致棋盘表面不是平面而不是标定算法的问题。项目做完后我还有一个习惯每次标定完会保存一份标定报告内容包括图片数量、重投影误差、内参矩阵和畸变系数。以后相机用了半年再复标定时可以对比参数变化判断镜头是否有松动或老化这个习惯帮我在一次项目里提前发现了镜头固定螺丝松动的问题。如果你也在做视觉测量项目建议保留每次标定的原始图片不要只保存参数后续排查问题时原始图像是最可靠的证据。本文还有配套的精品资源点击获取