
简介这是一套基于OpenCV的张正友相机标定完整实例工程适合计算机视觉初学者与需要实现相机标定的开发者学习。资源包含可直接运行的Visual Studio工程两个cpp源文件均配有详细注释配套14张不同角度的标定图与棋盘图用户只需一键运行即可完成标定流程并输出相机内参、外参、旋转平移矩阵及误差评价结果还能利用标定参数对原始棋盘图进行矫正。压缩包共92个文件以Visual Studio生成日志tlog、标定图像bmp/jpg、文本配置txt及可执行文件exe为主包体仅10.26MB便于快速下载与本地调试。工程代码结构清晰从数据读取到结果输出均有体现适合作为标定学习与项目改造的参考模板。目前已有22719人学习下载足以体现其实用性与参考价值。 做相机标定这件事说难是真不难OpenCV把张正友标定法封装成了现成的接口跑一遍cv2.calibrateCamera就能拿到内参和畸变系数但说简单也绝不简单我见过太多人拿着代码跑通了结果标出来的焦距跟实际差一大截矫正完的图像边缘弯得没法看。问题基本都出在“不知道标定到底在算什么”和“采集图像太随意”这两件事上。这篇就把张正友标定法配合OpenCV的完整实现拆开揉碎从原理到代码从拍图到参数解读再到我踩过的坑一次讲透。这套内容适合刚接触机器视觉的开发者、做机器人抓取或测量项目需要自己标定相机的工程师以及正在做SLAM、三维重建、AR相关课题的同学。读完你不仅能跑通完整程序还能自己判断标定结果到底靠不靠谱。1. 张正友标定法到底在干什么1.1 内参、外参、畸变一次讲清楚相机标定的本质是建立三维世界坐标到二维像素坐标的映射关系。这个映射可以拆成两部分一部分是相机内部的固定属性叫内参另一部分是每次拍摄时相机相对标定板的位置姿态叫外参。内参矩阵长得是这样[ fx 0 cx ] [ 0 fy cy ] [ 0 0 1 ]fx、fy是焦距单位是像素。cx、cy是主点坐标理想情况下应该落在图像正中心但实际生产装配总会有偏差。这三个量其实是四个fx和fy通常不相等决定了空间点投影到图像平面后的基础位置。外参就更好理解了。你把棋盘格放在桌上相机从侧面拍跟从正上方拍得到的图像完全不同。这个“不同”就是由旋转矩阵R和平移向量t描述的它们把世界坐标系变换到相机坐标系。真正让新手头疼的是畸变。镜头加工和装配不可能完美光线经过透镜后会产生径向畸变画面边缘的直线变弯和切向畸变镜头与成像平面不平行导致的拉伸。OpenCV里用k1, k2, p1, p2, k3五个系数来近似描述。其中k1, k2, k3是径向畸变系数p1, p2是切向畸变系数。标定的核心工作之一就是把这五个系数算准这样拍出来的图像才能“掰直”。1.2 棋盘格标定的核心假设与“张氏方法”的精妙之处张正友标定法最聪明的地方在于——它不需要你知道标定板的精确三维坐标只需要一块平面棋盘格。传统标定方法比如Tsai两步法需要精密的3D标定块制作难度高、成本也高。而棋盘格只要一张打印纸就能搞定。为什么棋盘格够用因为张正友证明了单应性矩阵可以在未知内参的情况下把平面标定板上的特征点映射到图像平面。相机的内参约束可以从多个不同角度的单应性矩阵中线性求解出来。简单说你只需要把棋盘格放在不同角度拍十几张照片算法就能从这些平面的投影关系中反推出相机内参。张氏方法的求解流程分两步第一步忽略畸变用线性方法求出内参和外参的初值第二步引入畸变系数用最大似然估计Levenberg-Marquardt算法对所有参数做非线性优化。OpenCV的calibrateCamera函数实现的就是这个过程所以如果你只想调用API确实不用关心底层细节。但如果标定结果不对不理解原理就完全无从下手排查。2. 完整的OpenCV标定实现可直接运行的源码2.1 环境准备与依赖我用的是Python版本这是目前上手最快、调试最方便的方式。需要的依赖只有两个pip install opencv-python opencv-contrib-python pip install numpy如果你用的是C等价的头文件就是opencv2/calib3d.hpp和opencv2/imgproc.hppCMake里链接opencv_calib3d和opencv_imgproc即可。核心流程和Python完全一致函数名都是findChessboardCorners、calibrateCamera这套照着下面代码逻辑翻译就行。注意opencv-contrib-python里包含一些扩展模块标定用不到但装上也不冲突。如果你之前已经装了别的版本OpenCV建议先卸载干净再装避免出现module cv2 has no attribute findChessboardCorners这种让人一头雾水的报错。2.2 标定主程序代码及逐段讲解先放完整代码再逐段解释关键部分。import cv2 import numpy as np import glob # 棋盘格内角点数量注意是“内角点”不是棋盘格数 # 比如棋盘格是9x6的格子内角点就是8x5 CHECKERBOARD (8, 5) 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) 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, None) if ret: objpoints.append(objp) # 亚像素精细化让角点定位更准 corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) imgpoints.append(corners2) # 可视化角点检测结果方便检查 cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret) cv2.imshow(Chessboard, img) cv2.waitKey(200) cv2.destroyAllWindows() # 标定主函数 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print(重投影误差: , ret) print(内参矩阵: \n, mtx) print(畸变系数: , dist.ravel()) # 畸变矫正映射表 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), 5 ) # 保存标定结果 np.savez(camera_params.npz, mtxmtx, distdist, newcameramtxnewcameramtx, roiroi)这里有个特别容易踩坑的点CHECKERBOARD定义的是内角点数量不是棋盘格的行列数。如果你打印的棋盘图是“9格 x 6格”那么内角点就是“8 x 5”。很多人第一次写代码直接用棋盘格数去定义结果findChessboardCorners死活找不到角点或者检测到一半就中断。代码中objp是世界坐标系下的棋盘格点Z轴全为0X轴和Y轴以“格子”为单位。因为后续求解单应性矩阵时只用到了棋盘格的拓扑关系不会用到格子的物理尺寸所以这里单位可以是任意值。但要注意如果你后续需要做“像素到真实毫米”的换算必须在标定的时候就告诉程序每个格子的实际边长方法是把objp[:, :2] * square_size如0.03表示30mm。cv2.cornerSubPix这一步很多人会忽略。直接找出来的角点是像素级别的整数坐标用亚像素精细化之后精度能提升到零点几个像素。这个精度对畸变系数的计算影响非常大尤其是边缘畸变差了半个像素k1、k2就可能偏差明显。2.3 关于棋盘图的准备建议没有现成的棋盘图最简单的办法是用OpenCV自己生成。在本地跑下面这段就能得到一张矢量清晰的棋盘图分辨率设为A4纸打印水平即可import cv2 import numpy as np board_size (9, 6) # 横向9个格子纵向6个格子注意是“格子数” square_size 100 # 每个格子的像素大小 rows, cols board_size height rows * square_size width cols * square_size board np.zeros((height, width), dtypenp.uint8) for i in range(rows): for j in range(cols): if (i j) % 2 0: board[i*square_size:(i1)*square_size, j*square_size:(j1)*square_size] 255 cv2.imwrite(chessboard_9x6.png, board)打印的时候有个关键要求棋盘格必须是平直的。如果你用普通A4纸打印一定要贴在硬纸板或亚克力板上不能有褶皱。我刚开始图省事直接拿软纸去拍结果每张图像的角点X、Y坐标都存在细微变形标定出来的焦距明显偏小这就是软纸在拍摄时轻微弯曲导致的。如果你手边没有打印机也可以把棋盘图显示在平板或显示器上然后移动相机从不同角度拍摄。这种方式比A4纸更平整但要注意屏幕的镜面反光问题后期我会在采集注意事项里细说。3. 实操过程从拍摄到标定结果评估3.1 采集标定图片的注意事项采集图像的质量直接决定标定结果的精度这里面的门道比很多人想象的多。数量与角度。至少拍15到20张不要少于10张。拍摄时让棋盘格出现在画面的不同位置左上、右上、中央、左下、右下。角度上要有明显的倾斜变化——绕X轴旋转15度、绕Y轴旋转20度之类。这样做的目的是让空间中的特征点在不同深度上都有分布否则深度信息不足焦距和主点很难准确收敛。姿态变化比位置变化更重要。我见过有人拍了20张但棋盘格始终平面对着相机只是上下左右移动。这种“零姿态变化”的图片集合标定出来的畸变系数基本是废的因为算法没有从非正面视角中获取径向畸变的约束。记住一个原则正面拍一张就够了其余全部倾斜着拍。光照要均匀。找角点的算法对对比度敏感光照不均或者局部反光都会导致角点检测失败。屏幕显示棋盘格时要特别留意镜面高光可以适当调整屏幕亮度和拍摄角度。用纸质棋盘则尽量在柔和漫射光下拍摄避免直射灯造成亮斑。运动模糊必须避免。手持相机拍摄时按快门的瞬间容易抖动图像稍微模糊一点cornerSubPix亚像素定位就会偏离。最稳妥的做法是把相机固定在三脚架上移动棋盘格来变换姿态和角度。如果必须手持就提高快门速度或者多拍几张挑清晰的用。3.2 运行标定并解读输出参数代码跑完后输出里最核心的是四样东西重投影误差ret、内参矩阵mtx、畸变系数dist、以及优化后的新相机矩阵newcameramtx。重投影误差的单位是像素表示所有棋盘格角点的重投影位置与实际检测位置的均方根误差。经验值小于0.1像素属于极好0.1到0.3像素是优秀0.3到0.5像素可接受超过0.5就需要检查是不是哪张图有问题。这里不是越接近0就一定好如果重投影误差只有0.02反而要怀疑是不是过拟合了——比如图片数量太少或者角度变化太单一。内参矩阵里的fx和fy可以做一个简单合理性检查。市面上常见的CMOS相机像素尺寸在1.4um到3um之间。如果镜头焦距是6mm像素尺寸是2.2um那fx大约在2700到2800像素左右。如果你的标定结果算出来是3000多先找像素尺寸资料确认一下不对就要排查输入图像是不是被缩放过了。畸变系数dist的五个值注意它们的数量级。对普通工业镜头或手机广角镜头k1通常在-0.1到0.1之间如果标定出来的k1是-3或者-5这种离谱数值基本可以判断标定失败原因多半是图片数量不够或者角度覆盖不够全。3.3 标定结果如何验证畸变矫正标定完之后不能只看数字就说标定成功了最终效果必须用矫正后的图像来验证。# 使用之前保存的 mapx, mapy 做重映射 dst cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR) # 根据ROI裁剪掉黑边区域 x, y, w, h roi dst_cropped dst[y:yh, x:xw] cv2.imshow(Undistorted, dst_cropped)理想情况下矫正前后的图像差异主要在边缘。矫正后画面边缘原本弯曲的直线比如门框、窗框、地砖缝应该变直。同时边缘会比原图有轻微裁剪这是正常现象因为畸变矫正本质上是把边缘的像素重新映射侧边会显得比原图略宽。有个实用技巧把原图和矫正图做成一个滑动条或并排对比逐张检查。如果有个别区域出现严重的拉伸变形或“波浪状”扭曲大概率是畸变系数拟合过度了可以试试在getOptimalNewCameraMatrix的第二个参数上调整——alpha参数设成0会让输出图像只保留有效区域设成1则保留所有像素包括黑边。多数场景用0或0.5之间既能去掉黑边又不会过度裁切。4. 常见问题与排查技巧实录4.1 角点检测失败怎么办findChessboardCorners返回False是所有新手的第一个拦路虎。排查优先级如下内角点数量定义是否正确、图片是否足够清晰、棋盘格是否完整出现在画面内、光照是否均匀。如果还是找不到有一个OpenCV调优技巧——把cv2.findChessboardCorners最后一个参数从None改成cv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE。这两个标志位会让算法先做自适应二值化再检测对光照不均的情况有明显改善。再不行可以尝试cv2.CALIB_CB_FAST_CHECK但要注意这个标志位相当于快速预判有时会直接跳过本该成功的图片。还有一个很容易忽视的问题棋盘图像素不能太小。如果棋盘格在画面中只占三四百个像素内角点的轮廓就不够清晰亚像素定位必然不准。建议棋盘格长边占整个画面宽度的三分之一以上。4.2 标定结果误差大或参数不合理的排查思路这类问题最折磨人因为程序能跑通但是出来的参数一看就不对。我遇到的典型情况有这么几种图片数量不足——这是我踩过最深的坑。第一次做标定的时候我只拍了8张图重投影误差0.35左右fx、fy也不合理后来补到18张误差降到0.12参数也正常了。张正友标定法的本质是解一个超定方程图片越多约束越多解越稳定。姿态覆盖不全面——只绕一个方向旋转或者所有图片里棋盘格都集中在画面中央都会导致参数退化。排查方法是把每张图的角点画出来看它们是否遍布整个画面。如果某一边的角点从未出现那这一侧的畸变系数就没有足够的约束。棋盘格不平整或发生形变——前面提到的软纸问题这是物理层面的误差代码怎么调都救不回来。解决办法是更换硬质标定板。混合了不同分辨率的图片——有些场景下会忘记相机设置被改动过拍了两组不同分辨率的图片混在一起标定。内参矩阵跟分辨率是强相关的这种做法会让结果严重偏移。老老实实统一分辨率重新拍一组。4.3 畸变矫正后图像边缘异常矫正出来的图像边缘有严重的拉伸或锯齿基本是畸变系数过估计了。一个常见的原因是在非线性优化里畸变的权重过大参数被带偏了。有个简单的缓解办法用cv2.getOptimalNewCameraMatrix的alpha参数控制输出图像的视野范围。alpha0时OpenCV会裁剪掉所有矫正后产生的空白区域只保留最大内接矩形alpha1时代价是保留全部信息包括黑边。实际项目里我通常取alpha0.5左右既保留接近原有的视场角又能避开最边缘畸变最剧烈的区域。如果你发现自己标定得到的畸变参数在别的程序里比如某些基于Matlab标定结果的工程换算时对不上还要注意一个坐标系差异OpenCV的像素坐标系原点在左上角x向右y向下而在某些计算机图形学或机器人学坐标系里y轴向上。标定结果迁移到其他平台前先确认坐标系定义是否一致否则tvecs和旋转向量符号就会出错。4.4 实操避坑清单汇总这里把我反复踩过、也帮别人排查过的问题整理成一张表方便你对照自查序号问题现象常见原因解决办法1找不到角点内角点数量定义错误数清格数用行数减1、列数减12找不到角点图像模糊或棋盘过小提高快门速度放大棋盘占比3标定误差0.5像素图片太少或角度单一拍到15张以上各方向倾斜拍摄4fx/fy相差巨大混合了不同分辨率图片统一拍照参数重新采集5矫正后图像扭曲畸变系数过拟合减少图片数量或检查异常图片6矫正后黑边过多alpha参数过小调大getOptimalNewCameraMatrix的alpha7实际测量距离不准缺少物理尺寸约束把格子边长乘进objp如0.03m最后总结一个实操习惯每次标定前先拍一张棋盘格在画面正中央的正面图跑通整个流程并确认角点检测正确再开始正式采集。这个“小步验证”能节省大量排查时间别一口气拍完20张结果发现角点数量定义错了白忙一场。就我个人经验而言标定这件事最考验的其实不是代码能力而是对图像质量的判断力。同一套程序认真采集和随手采集标出来的参数可能在10%到20%之间浮动。如果你产线或项目中需要高精度测量建议把采集规范写成文档固定下来棋盘格型号、拍摄距离范围、光照条件、图片数量下限全部明确。好的标定不是调参调出来的是从第一张照片开始就注定的。本文还有配套的精品资源点击获取