ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

张正友标定实战:OpenCV棋盘格内参矩阵与畸变校正源码解析

张正友标定实战:OpenCV棋盘格内参矩阵与畸变校正源码解析 简介这份项目资源基于OpenCV-Python实现张正友相机标定法面向计算机视觉初学者与开发者旨在解决三维重建、视觉测量中相机内参、外参与畸变系数的精确标定问题。压缩包共15个文件包含12张不同角度的棋盘格标定图像、calibration.py标定脚本、camera_params.xml参数文件及README说明文档整体仅2.43MB结构紧凑、便于直接运行学习。目前已有305人学习下载从角点检测、标定方程构建到最小二乘法求解参数代码与文档提供了完整流程同时给出标定结果的评估与验证方法帮助用户判断参数是否真实反映相机成像特性。通过实战可掌握张正友法的核心步骤并迁移到三维重建、AR等视觉项目中是从理论到实践的高质量参考资源。1. 张正友标定实战项目从棋盘格到内参矩阵一套能直接跑通的OpenCV源码手里只有一张棋盘格没有昂贵的标定板也能算出相机内参矩阵和畸变系数——这是张正友相机标定能流行这么多年的原因。真正让我觉得这套基于OpenCV-Python的项目源码值得下载是发现它不只是贴了一段cv2.calibrateCamera的调用代码而是把采集图片、角点检测、内参解算、结果验证四个环节都串成了可执行的脚本。抄别人标定好的内参文件是我见过最不靠谱的做法fx、fy跟相机传感器本身的像素尺寸绑定换一台相机就完全是另一组数。这个项目适合刚接触相机内参标定的学生也适合做单目测距、三维重建时被畸变坑过几次的工程师。有源码在手每步的输入输出都能对上出了问题也更容易定位。2. 为什么标定内参不能抄别人的张正友在解的四步相机模型2.1 一个像素坐标背后藏着三维到二维的四步变换一个像素坐标 (u, v) 看起来只是二维坐标但实际是空间中一个三维点经过至少四层变换才落到传感器上。第一层是世界坐标系到相机坐标系的刚体变换由外参旋转矩阵 R 和平移向量 t 决定第二层是针孔几何投影把相机坐标系下的点投影到归一化平面第三层是物理坐标到像素坐标引入焦距 fx、fy 和主点 cx、cy第四层才是镜头畸变导致的偏移。很多标定文章把内参和外参混在一起讲实战中更重要的分界线在这里外参随每次拍摄角度变化内参只跟相机自身硬件绑定。所以标定结果不能被简单搬运。fx 的本质是焦距除以像元物理宽度不同相机传感器的像元尺寸不同画面分辨率不同同一个镜头装在不同机身上标定出来的 fx 也不一样。这就是为什么别人分享的 yaml 配置文件放到自己的程序里测距总是差一截。真正实现时张正友的推导并不是让你手写 SVD 分解而是 OpenCV 的 calibrateCamera 内部已经完成了整个优化过程。你只需要正确组织三维点和对应的二维点剩下的交给算法迭代。但这不代表可以不理解原理只有知道 H 矩阵与内参的关系才明白为什么每张标定图片必须来自不同角度为什么棋盘格不能只放在画面中央。2.2 棋盘格为什么是最好的标定板角点检测的数学基础选棋盘格做标定板不是因为它拍出来好看而是因为角点检测的可靠性。OpenCV 的cv2.findChessboardCorners基于图像灰度梯度搜索角点。棋盘格内角点在水平、垂直两个方向上的灰度变化同时出现峰值数学上是稳定且有唯一性的特征相比之下圆点标定板虽然也有开源支持但圆点中心提取对透视变形和光照更敏感解算时不那么稳。调用 findChessboardCorners 时传的棋盘格尺寸是内角点数这个数字很容易填错。比如 10x7 的格子棋盘内角点是 9x6因为角点出现在格子与格子的连接处沿网格边界各减 1。如果传入的尺寸比实际多或少 1函数就会返回 False导致脚本跳过这张图片。亚像素角点提取用cv2.cornerSubPix。它是在角点邻域内用一个窗口做迭代优化把定位精度做到亚像素级。这里窗口尺寸和迭代终止条件是直接能影响精度的参数窗口太小噪声抑制不住迭代次数太少角点位置收敛不到局部极小值。项目源码里通常把窗口设成 (11,11)终止条件设成 30 次迭代加 0.001 像素精度这是经过大量样本验证的经验值。畸变模型方面dist 系数里的 k1、k2 是径向畸变作用于到主点距离的偶数次幂p1、p2 是切向畸变主要由镜头和传感器装配不平行造成。去畸变时如果只做内参变换而不修正畸变直线靠近画面边缘就会弯曲所以标定输出的 dist 是跟内参同等重要的结果。2.3 代码包里的目录结构与调用入口拿到源码包第一件事不是马上跑而是理解目录分工。这类标定项目的常见组织方式如下camera_calibration/ ├── calibration/ │ ├── config.py # 棋盘格参数、路径配置 │ ├── capture_images.py # 摄像头采集脚本 │ ├── calibrate_camera.py # 标定主程序 │ └── undistort_image.py # 去畸变验证脚本 ├── data/ │ └── calibration_images/ # 原图存放位置 ├── output/ │ ├── calibration_result.json # 内参和畸变系数 │ └── visualization.png # 角点绘制结果 └── README.md这个目录结构的用意很明显把 config.py 单独拆出来是因为棋盘格内角点数和格子物理尺寸每一次标定都可能不同但标定主程序的逻辑不需要改。采集脚本和标定脚本分开是因为采集动作发生在摄像头面前需要反复调节距离、角度、光照而标定可以在终端里后台反复运行两者互不干扰。使用顺序是先改 config.py 里的 CHESSBOARD_SIZE 和 SQUARE_SIZE然后运行 capture_images.py 采集 20 到 30 张图再运行 calibrate_camera.py 输出 JSON最后用 undistort_image.py 验证校正效果。如果下载下来的包没有 undistort 脚本也可以直接在标定主程序里把畸变参数打印出来用一小段独立代码验证。明白这个流程后你拿到别的类似项目也能快速找到入口文件。3. 把源码跑起来环境检测、图片采集与标定主程序的完整执行3.1 环境准备opencv-python版本检测和numpy依赖跑标定项目之前先确认 Python 环境里有没有 OpenCV 和 NumPy。常见做法是直接用一条命令验证python -c import cv2, numpy; print(OpenCV, cv2.__version__); print(NumPy, numpy.__version__)如果输出版本号说明环境就绪。如果报 ModuleNotFoundError就用 pip 补上pip install opencv-python numpy这里要提醒一点项目用 cv2.imshow 展示摄像头画面和角点绘制结果所以不能装 opencv-python-headless 版本那个版本没有 highgui 模块调用 imshow 会直接报缺少 Qt 或 GTK 的错。opencv-python 包本身已经包含标定所需的全部模块不需要 extra-contrib 扩展包。版本号尽量选 4.x 以上早期 3.x 版本的 findChessboardCorners 在部分场景下有已知的收敛问题。如果项目会在多人之间传阅最好在 requirements.txt 里固定 opencv-python4.2避免环境不一致带来的玄学差异。3.2 标定板参数设置格子大小、内角点数量的对应关系标定流程里最容易被忽视的是配置文件。先把两个核心参数设对后面所有脚本才能正确运行。# config.py CHESSBOARD_SIZE (9, 6) # 内角点数量宽度9个高度6个 SQUARE_SIZE 30.0 # 单个格子边长单位mm CAMERA_INDEX 0 # 摄像头设备索引 IMAGE_DIR data/calibration_images RESULT_FILE output/calibration_result.json参数说明参数含义常见错误CHESSBOARD_SIZE棋盘格内角点数量把格子数当成角点数直接填成10x7SQUARE_SIZE单个格子的物理边长单位不一致导致平移向量t的单位错乱CAMERA_INDEX摄像头设备编号外接摄像头写0实际是1或2导致打不开CHESSBOARD_SIZE 里的两个数必须在采集前和实际棋盘格对应好。判断标准是你打印的棋盘格如果是 12x9 个正方形格子那么内角点就是 11x8。SQUARE_SIZE 的单位要和业务场景一致比如后续要做单目测距、测三维尺寸一般全用毫米。如果标定板是 A4 纸打印的格子边长可能只有 25 毫米或 30 毫米生成棋盘格图案时会在模板里标注数值不要凭感觉写。我一般建议用激光打印机印在哑光纸上普通喷墨打印边缘有晕染角点锐度不够标定出来的结果会比实际偏一点。3.3 用采集脚本抓取标定图片读取摄像头一帧存放一张采集脚本的作用是把实时画面一帧一帧地显示出来手动挑选合适的角度按下保存。这样比对着相机自动连拍更可控光线、倾斜角、覆盖范围都能自己把握。import cv2 import os from calibration.config import IMAGE_DIR, CAMERA_INDEX save_dir IMAGE_DIR os.makedirs(save_dir, exist_okTrue) cap cv2.VideoCapture(CAMERA_INDEX) if not cap.isOpened(): raise IOError(f无法打开摄像头索引为 {CAMERA_INDEX} 的设备不存在或已被占用) count len([f for f in os.listdir(save_dir) if f.endswith(.jpg)]) while True: ret, frame cap.read() if not ret: continue cv2.imshow(Calibration Capture, frame) key cv2.waitKey(30) if key ord(s): fname os.path.join(save_dir, fboard_{count:02d}.jpg) cv2.imwrite(fname, frame) print(已保存:, fname) count 1 elif key ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明cap.read() 返回一帧图像ret 为 False 时直接跳过避免空帧写入磁盘。count 从已有图片数继续编号防止覆盖之前的采集。保存的图片格式统一用 jpg体积小、质量够标定使用。参数说明waitKey(30) 是控制显示帧率的关键值越大画面刷新越慢、按键响应越迟钝值太小画面会闪。CAMERA_INDEX 默认 0如果打开失败先试试改成 1外接 USB 摄像头往往会占用 1 或 2。采集过程中要让棋盘格保持静止手持移动可以但按下保存那一刻不要抖运动模糊的图片标定效果很差。3.4 执行标定主程序calibrate_camera.py的完整流程采集完成后标定主程序会扫描 data 目录下所有 jpg逐张找角点解算内参并输出结果。import cv2 import numpy as np import glob import json from calibration.config import CHESSBOARD_SIZE, SQUARE_SIZE, IMAGE_DIR, RESULT_FILE criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp np.zeros((CHESSBOARD_SIZE[0] * CHESSBOARD_SIZE[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHESSBOARD_SIZE[0], 0:CHESSBOARD_SIZE[1]].T.reshape(-1, 2) * SQUARE_SIZE obj_points [] img_points [] images glob.glob(IMAGE_DIR /*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHESSBOARD_SIZE, None) if ret: obj_points.append(objp.copy()) corners_sub cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) img_points.append(corners_sub) cv2.drawChessboardCorners(img, CHESSBOARD_SIZE, corners_sub, ret) cv2.imshow(Chessboard Detection, img) cv2.waitKey(50) else: print(跳过角点检测失败的图片:, fname) cv2.destroyAllWindows() ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(重投影误差像素:, ret) print(内参矩阵:) print(mtx) print(畸变系数:) print(dist.ravel()) result { camera_matrix: mtx.tolist(), dist_coeffs: dist.tolist(), rms_error: ret } with open(RESULT_FILE, w) as f: json.dump(result, f, indent2) print(标定结果已写入, RESULT_FILE)代码逻辑分成三段第一段 build objp用 mgrid 生成内角点网格坐标乘上 SQUARE_SIZE 得到真实世界坐标第二段遍历图片用 findChessboardCorners 检测粗角点再用 cornerSubPix 做亚像素精化第三段调用 calibrateCamera 解算最后把结果序列化到 JSON。参数说明里最需要理解的是 calibrateCamera 的入参obj_points 是三维点列表img_points 是二维像素点列表gray.shape[::-1] 把 (height, width) 转成 (width, height)这个是 OpenCV 对图像尺寸的约定写反了看不出来但会影响内参。后两个 None 表示不指定内参初值、不指定畸变初值保持默认求解策略。输出的 ret 是重投影均方根误差单位是像素。经验上 0.1 像素附近说明质量很好0.3 以下都能接受超过 0.8 就该怀疑图片有问题。mtx 是 3x3 内参矩阵dist 包含 [k1, k2, p1, p2, k3] 五个畸变系数k3 这一项对应的是大畸变镜头的大像高区修正普通工业镜头可以不用关注。4. 张正友标定避坑指南采集姿势、亚像素精度与误差排查的五个案例4.1 棋盘格没有充满画幅标定完边缘还是扭曲的现象标定结果重投影误差不到 0.2 像素看起来很好但把生产画面拿去去畸变图像四角依然有明显弯曲直线无法拉直。原因采集的标定图片里棋盘格全部位于画面中央区域边缘和四角几乎没有角点参与约束。calibrateCamera 只能修正它学到的畸变模式画面边缘的强畸变因为缺少采样点在优化时被当成了噪声忽略。解决重新采集时让棋盘格尽量贴近画面边缘四个角和四条边都要有覆盖。我一般会这样控制至少 5 张图棋盘格占据画面 70% 以上同时刻意让棋盘格从左上、右上、左下、右下四个角往中心移动拍摄。判断标准是棋盘格边缘黑块离图像边框零距离或者尽量靠近。这样做完再去畸变边缘弯曲能明显改善。4.2 findChessboardCorners一直返回False图片被批量跳过现象运行标定主程序控制台打印“跳过角点检测失败的图片”一张可用的都没有。原因原因集中在三处。第一是棋盘格内角点数填错10x7 的格子填成了 10x7检测逻辑找不到对应数量的角点第二是光照太强棋盘格白色格子反光过曝梯度特征消失第三是棋盘格太小每个格子折算到图像上不足 30 像素亚像素搜索窗口无法定位。解决先把 config 里的 CHESSBOARD_SIZE 改成实际角点数格子数各减 1。接着检查采集画面确认没有高光反射必要时用手挡住顶光或者调整拍摄角度。最后看棋盘格成像大小把棋盘格移近一点保证单个格子边长至少占 30 像素。倾斜角超过 60 度的图片也容易失败拍摄时尽量控制在 45 度以内。4.3 重投影误差超过1像素畸变系数k3出现异常大值现象标定输出的 ret 是 1.5 像素dist 数组里 k3 达到零点几甚至更大明显不像正常镜头参数。原因图片张数太少一般少于 10 张时内参和畸变参数会出现过拟合另一个可能是采集图片里有虚焦模糊的照片角点亚像素位置偏移拉高了整体误差。解决提高图片数量到 20 张以上并删除模糊图片。项目里可以在循环中加入一个简单验证每次检测完角点把当前图片的角点坐标保存下来标定完成后逐张计算单张重投影误差超过 1 像素的图片直接剔除再重新标定。这个做法是我每次处理误差异常时的固定手段比反复调初始值有效得多。4.4 亚像素窗口设置不当角点定位始终差半个像素现象标定结果肉眼看不出问题但在测距代码里精度始终达不到预期误差在 1% 左右波动重投影误差稳定在 0.4 像素却降不下去。原因cornerSubPix 的窗口尺寸和迭代条件没有调好。项目源码里如果沿用 (5,5) 小窗口对低分辨率摄像头来说噪声抑制不足角点位置在两三个像素范围内抖动。迭代次数太少也会在角点梯度不明显时提前停止收敛。解决把 winsize 改成 (11,11)终止条件设为 30 次迭代加 0.001 像素精度。低分辨率图像、噪声偏大的摄像头建议用 (15,15) 窗口再次精化。这个参数的修改直接体现在 3.4 节的 criteria 变量里改完重跑一遍即可。4.5 同一台相机两次标定结果不一致fx漂移几个像素现象上午跑一遍得到 fx1234.56下午跑一遍变成 1238.23两张结果里的主点坐标也偏了三四像素。原因如果图片集没变最可能是 obj_points 和 img_points 数量不对应某张图片被重复加进列表。如果图片集变过那就是相似图片太多大量近似视角的棋盘格图会让非线性优化对同一类约束过度拟合削弱了不同角度之间的平衡导致数值解不稳定。解决先在 calibrateCamera 之前打印 len(obj_points) 和 len(img_points)确认一致。再检查采集的图片删除视角几乎重复的图保证角度分布多样。另外同一台设备的标定结果应该只做一次然后固定保存不要每次启动程序都重新标定覆盖。项目代码里最好加一道文件锁或者时间戳防止误覆盖。5. 标定以后的验证与进阶重投影误差、去畸变与测距衔接5.1 验证标定结果把三维点投影回图像计算像素误差标定完成后不能直接拿去用先做一次重投影验证。标定主程序已经返回了每张图片的外参 rvecs 和 tvecs把三维角点用 projectPoints 再投回像素平面和检测角点对比for i, fname in enumerate(images): proj_pts, _ cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], mtx, dist ) error np.mean(np.linalg.norm(proj_pts.reshape(-1, 2) - img_points[i].reshape(-1, 2), axis1)) print(fname, 重投影误差:, round(error, 3), 像素)这段代码直接在标定主程序末尾加进去逐张打印误差。如果整体误差在 0.3 像素以下说明标定对这批图片是自洽的。更严格的验证方法是单独再拍 10 张不参与标定的棋盘格图片用同样的方式投影计算误差这叫交叉验证。这样做能排除标定图片本身过拟合的影响。5.2 把内参矩阵用到生产环境去畸变与坐标归一化实际工程里最常用的操作是把标定结果应用到视频帧上。一种做法是用 cv2.undistort但视频流里每次都调 undistort 性能太差。更好的做法是生成一次映射表之后每帧只要 remapimport cv2 import json import numpy as np with open(output/calibration_result.json) as f: data json.load(f) mtx np.array(data[camera_matrix], dtypenp.float64) dist np.array(data[dist_coeffs], dtypenp.float64) frame cv2.imread(data/production_frame.jpg) h, w frame.shape[:2] # alpha1.0保留全部原始像素alpha0.0裁剪到最大内接矩形 new_mtx, _ cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), alpha0.5, newImgSize(w, h)) mapx, mapy cv2.initUndistortRectifyMap( mtx, dist, None, new_mtx, (w, h), cv2.CV_32FC1 ) undistorted cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR) cv2.imshow(Undistorted, undistorted) cv2.waitKey(0)这里 getOptimalNewCameraMatrix 的 alpha 参数很值得注意alpha1.0 时校正结果保留了原始视野但画面边缘有黑边alpha0.0 时黑边被裁剪掉但视野损失较大。测距和定位场景我一般用 0.5留出安全余地。从标定 JSON 里读回内参后要固定这套数据不要每次启动程序都重新标定。生产代码里把 mtx 和 dist 读取一次写入内存视频帧循环里只用 mapx、mapy 做 remap速度能跑满实时。如果要进一步做单目测量下一步就是根据校正后的二维坐标结合 PnP 求解姿态那是另一个模块的事但前提一定是内参准确。说到这想起自己第一次做视觉定位项目图快直接用了别人分享的内参文件结果画面边缘误差到了十几厘米。后来老老实实把标定流程完整走了一遍又在代码里强制加入交叉验证步骤数据才稳定下来。从那以后我每拿到一台新相机都会先花二十分钟跑完这套标定流程再写业务代码验证和校正一次也不省。希望帮到你。本文还有配套的精品资源点击获取
返回列表