ARTICLE DETAIL

资讯详情

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

OpenCV相机标定Python实战:棋盘格角点检测与内参矩阵详解

OpenCV相机标定Python实战:棋盘格角点检测与内参矩阵详解 简介一套基于Python的相机内参标定程序专为需要快速完成相机校准的开发者与视觉爱好者准备。程序依托张正友棋盘格标定法只需准备拍摄好的棋盘格图片并修改主程序中的文件夹路径与实际格子边长即可自动求取相机内参适用于入门学习、实验验证和项目预研等场景。压缩包共十四个文件包含十张JPG棋盘格样本、两个Python脚本主校准程序与图片显示辅助工具、一份TXT说明文档以及一张PNG棋盘格模板整体大小约2.19MB轻量且便于本地部署。资源内置十一乘十黑白棋盘格图可打印后拍摄也可直接放在显示器上采集并附带图片浏览辅助程序帮助你快速上手。目前已有九百零七人学习使用对刚接触相机标定或需要现成工具的同学来说是一份省时省力的实操参考。1. 这个相机标定Python程序为什么值得花一晚上读一遍如果你已经能在OpenCV里跑通读图、灰度、找轮廓但一碰到“相机标定”四个字就发怵那么这个带棋盘格的Python项目是一个很好的切入口。它只有两个核心脚本main.py负责从文件夹里自动读图、找棋盘格角点、算内参ChessboardView.py负责把11x10的棋盘格铺在屏幕上供你拍摄。你只需要改图片文件夹路径和物理棋盘格边长就能拿到内参矩阵fx, fy, cx, cy和畸变系数k1, k2, p1, p2, k3。适合3D测量前需要去畸变的视觉工程师也适合第一次接触相机标定的Python学习者。我不打算把它包装成“自动化标定神器”而是想结合张正友标定法的实现细节把图怎么拍、参数怎么设、结果怎么验证一次说清。2. 棋盘格与张正友标定法内参矩阵和畸变模型的来龙去脉2.1 世界坐标到像素坐标一条射线的两次变换先从最基础的相机模型说起。一个三维点P(X, Y, Z)经过刚体变换变成相机坐标系下的(Xc, Yc, Zc)再经过针孔投影变成图像平面上的(x, y)最后从毫米单位缩放到像素单位。整条链路可以写成s [u, v, 1]ᵀ K [R|t] [X, Y, Z, 1]ᵀ。其中K就是本文要标定的内参矩阵它包含fx、fy、cx、cyR和t是相机相对于棋盘格的外参一张棋盘格图片对应一组外参但整个标定过程只有一套内参。张正友标定法最精妙的地方就是让棋盘格平放在Z0平面上把外参暂时消去先利用单应性矩阵H获得约束方程再通过多张不同姿态的图片联立解出K。这里的关键在于棋盘格上的角点只要知道它们在同一平面上的相对位置就能构造出一个3×3的单应矩阵H每张图提供一个H而每个H对内参矩阵给出两个约束。因此理论上只需要3张图就能把fx, fy, cx, cy初步标定出来这也是为什么这个项目不要求你收集几十张图——12到20张已经是相对稳妥的数量。2.2 为什么是黑白棋盘格而不是二维码或圆点标定图案需要满足两个条件特征点位置已知且在相机视野里可以被稳定检测。黑白棋盘格的内部角点是黑白区域的交点两侧梯度方向变化明确亚像素定位精度比圆点阵高这是OpenCV把findChessboardCorners做成内置函数的原因。11x10的棋盘格由10x9个内部角点组成这个换算关系很容易被忽略。如果你用的是显示器上的棋盘格要注意屏幕的像素颗粒本身也是一种纹理手机拍出来容易产生摩尔纹导致角点被误检。常见做法是让棋盘格占住屏幕的大部分区域同时把屏幕亮度调低减少高光反射和摩尔纹干扰。如果打印棋盘格要保证纸张平整褶皱会破坏“所有角点共面”的前提标定出来的内参会有系统性误差。2.3 用ChessboardView.py在屏幕上显示标定图案项目里的ChessboardView.py做的事情很简单加载11x10_chessboard.png并持续显示在窗口中。你可以直接运行它让手机对着屏幕拍也可以把这个脚本当作图片查看器确认棋盘格图片没有被误缩放。下面是一个可以替代的参考实现import cv2 IMG_PATH 11x10_chessboard.png board_img cv2.imread(IMG_PATH) print(f棋盘格图片尺寸: {board_img.shape[1]} x {board_img.shape[0]}) while True: cv2.imshow(chessboard, board_img) # 按 q 退出按空格暂停方便切到手机相机拍照 key cv2.waitKey(1) 0xFF if key ord(q): break cv2.destroyAllWindows()这段代码的逻辑是持续把棋盘格显示在窗口中waitKey(1)让画面保持流畅刷新不会因为循环阻塞导致窗口无响应。print那一行只是确认读进来的图片尺寸如果显示的尺寸和原图不符要先去查文件格式不要继续往下拍。对手机拍摄来说屏幕亮度建议调到中等偏低过亮会造成过曝白色格子糊成一片角点定位精度会明显下降。3. main.py核心代码解析从文件夹路径到内参矩阵的完整流水线3.1 读取图片并检测棋盘格角点main.py的流程分四步遍历图片、检测角点、生成世界坐标、调用calibrateCamera。我逐段拆开讲先看角点检测部分import cv2 import glob import numpy as np img_dir imgs/phone # 改成你自己的图片文件夹 pattern_size (10, 9) # 内角点数11x10棋盘格 - 10x9 image_size None image_points [] # 每张图检测到的像素坐标角点 img_files sorted(glob.glob(img_dir /*.jpg)) for idx, fname in enumerate(img_files): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners( gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE ) if not ret: print(f第{idx1}张未检测到角点: {fname}) continue # 亚像素精化提高角点坐标精度 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria ) image_points.append(corners_refined) if image_size is None: image_size (gray.shape[1], gray.shape[0])这个循环里最关键的是flags参数。CALIB_CB_ADAPTIVE_THRESH会在检测前做自适应二值化适合光照不均的照片CALIB_CB_NORMALIZE_IMAGE用直方图归一化能缓解显示器反光带来的对比度异常。cornerSubPix做亚像素插值邻域窗口(11, 11)是常用值迭代30次、精度0.001像素足够收敛。如果检测失败程序会继续下一张而不是中断这样你最后能统计出哪几张图片不可用。3.2 生成世界坐标格子边长在这里起作用对于每个检测成功的角点需要生成它对应的世界坐标。因为棋盘格是平面物体所有角点的Z坐标都设为0。世界坐标的单位必须和“格子边长”一致比如你量出格子边长是23mm那么相邻角点的物理间距就是23chessboard_size (10, 9) square_size 23.0 # 单位 mm按手头棋盘格实际尺寸改 object_points [] for idx in range(len(image_points)): 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) objp * square_size object_points.append(objp)mgrid生成从(0,0)到(10,9)的网格表示的是棋盘格内部角点在棋盘格平面上的编号乘上square_size才变成物理坐标。这里要注意不要把格子数11x10直接传进pattern_size因为findChessboardCorners需要的是内角点数传错的话OpenCV会提示找不到角点或者角点数量不匹配。两个变量命名不同但指向同一个概念改的时候要同步。3.3 calibrateCamera参数与返回值拆解retval, mtx, dist, rvecs, tvecs \ cv2.calibrateCamera(object_points, image_points, image_size, None, None, flagscv2.CALIB_RATIONAL_MODEL) print(重投影误差(RMS):, retval) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel())calibrateCamera返回的retval是全部图片的平均重投影误差单位是像素这是判断标定好坏的第一指标。mtx是3×3内参矩阵格式为[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]。dist是按flags选择的畸变系数数组默认只估k1、k2、p1、p2四个参数加上CALIB_RATIONAL_MODEL后启用k3甚至k4-k6适合广角和鱼眼镜头。cameraMatrix和distCoeffs传入None表示让算法自动初始化不需要手动给初值。rvecs和tvecs是每张图对应的外参一组图片就有一组旋转向量和平移向量。如果你只关心去畸变这两个返回值可以暂时不理会但如果后续要做位姿估计它们可以直接喂给solvePnP。3.4 把标定结果保存成文件标定结果要落盘不然每次开机都要重算。常见做法是同时保存npz和txt两种格式np.savez(camera_calibration.npz, mtxmtx, distdist, retvalretval) with open(camera_calibration.txt, w) as f: f.write(fx, cx: {:.6f}, {:.6f}\n.format(mtx[0, 0], mtx[0, 2])) f.write(fy, cy: {:.6f}, {:.6f}\n.format(mtx[1, 1], mtx[1, 2])) f.write(dist: {}\n.format(, .join(f{x:.6f} for x in dist.ravel())))txt里手写的fx/cx以及畸变系数方便你在不启动Python的情况下快速查看npz则是给后续脚本直接加载用的。我一般会把文件名加上拍摄设备或时间戳比如camera_calibration_phone_20250608.npz避免多次标定后混淆。注意write模式会覆盖旧文件不要在一次标定里重复运行这段代码后才发现历史结果被冲掉了。4. 实战标定手机拍摄的坑与参数修改清单4.1 显示器拍摄与打印拍摄的选择棋盘格可以打印出来拍也可以放在显示器上拍。如果打印你需要量的是黑色方块的实际边长用尺子量不要相信图片文件里标注的数字因为打印机的缩放不一定是精确的100%。如果放在屏幕上拍你量的就是显示器上显示出来的方块边长。两种方式的标定原理一样但要注意区分不要混用。打印方案的优势是棋盘格是物理平面稳定性好适合后续做高精度测量劣势是纸张容易翘边反光控制需要额外补光。屏幕方案的优势是调整方便格子大小可以随意缩放劣势是屏幕玻璃反光、摩尔纹以及亮度不均匀对拍照角度更敏感。我的建议是临时验证用屏幕需要保存长期标定结果时打印贴在硬纸板上。4.2 修改文件夹路径与格子边长在main.py中真正需要修改的只有两处图片文件夹路径img_dir以及棋盘格实际边长square_size。图片文件夹里建议只放同一个相机、同一个分辨率拍的图片不要混入其他来源的图。程序会按文件名排序后逐个读取如果你增删过图片要保证所有图片拍的是同一个棋盘格。下面是参数对照表方便你在动手前确认每一项的用途参数位置示例值含义错误后果img_dirimgs/phone相机拍摄图片的文件夹路径路径不存在时glob返回空列表程序报错pattern_size(10,9)棋盘格内部角点列数、行数与实际角点数不符时检测失败或误检square_size23.0方格边长单位mm单位不一致导致fx/fy按比例缩放这里解释一个常见困惑fx和fy的单位是像素物理意义是焦距与像素尺寸的比值。如果你把square_size从23改成46那么世界坐标的尺度变大了标定得到的fx也会近似翻倍因为同一个像素尺度被对应到了更大的物理距离。如果只用来做去畸变这个比例变化影响不大但如果要用fx换算物理焦距就必须保证单位一致。4.3 拍摄数量、角度与分布建议拍15到20张。拍摄时保持棋盘格清晰焦点对准棋盘格不要有运动模糊。角度分布可以参考这个配置姿态类型数量说明正对棋盘格3~5张相机与棋盘格平面平行距离远近错开倾斜棋盘格5~8张倾角10°到45°之间覆盖不同方向棋盘格位于画面四角4~6张覆盖畸变最大的边缘区域正对的角度用来约束主点cx、cy的估计倾斜角度用来约束fx、fy和畸变系数边缘位置则让畸变参数有足够的观测数据。如果只拍正对的图畸变参数会非常不稳定retval看起来很小但实际去畸变效果很差。4.4 检测失败和误差偏大的排查方法一张图片检测不到角点常见原因是过曝、欠曝、拍糊或者棋盘格被截断。程序里我一般会在检测成功后调用cv2.drawChessboardCorners画出来看一眼如果发现某个区域的角点位置串位说明图片有反光或运动模糊直接删掉这一张比硬凑要好。标定完成后先看retval。RMS小于0.5像素说明结果基本可信大于1像素就要回到数据层面排查。先用可视化确认每一张图的角点是否正确再检查image_size是否和图片实际尺寸一致。最后的补救手段是调整flags从默认切换到CALIB_RATIONAL_MODEL或者加上CALIB_FIX_K3看哪一组参数在验证集上的误差最小。不要一开始就上rational model参数太多会过拟合到个别噪声点上。5. 标定结果的下游应用去畸变、物理焦距换算与重投影误差复核5.1 用undistort把图片还原成理想透视拿到mtx和dist后最直接的使用场景就是图像去畸变new_camera_matrix, roi cv2.getOptimalNewCameraMatrix( mtx, dist, (w, h), alpha0, (w, h) ) undistorted cv2.undistort(img, mtx, dist, None, new_camera_matrix)getOptimalNewCameraMatrix里的alpha控制视场保留比例。alpha0会裁剪掉畸变校正后没有像素内容的边缘区域输出图像没有黑边适合工业测量场景alpha1保留全部像素边缘会出现黑色空洞适合视觉SLAM这种需要完整视场的场景。实际操作中我几乎都用alpha0因为下游算法一般不关心视场角损失但很讨厌黑边干扰。5.2 从fx换算出物理焦距标定输出的fx和fy单位是像素。如果想得到镜头的毫米焦距要用比例关系物理焦距 ≈ fx × 传感器物理宽度 / 图像像素宽度。比如一部手机图像宽度为4000像素传感器宽度6.17mm那么fx对应的物理焦距就是fx × 6.17 / 4000。这个换算只对同一对焦距离成立手机摄像头是自动对焦不同工作距离下物理焦距会变化所以标定结果只是当前对焦状态的一次采样。对固定焦距的工业相机这个换算就非常可靠。5.3 逐张图计算重投影误差最后提供一个不依赖retval的复核方法逐张输出每张图片的重投影误差方便定位是哪一张图在拖后腿def project_check(rvec, tvec, obj_pts, img_pts, mtx, dist): proj_pts, _ cv2.projectPoints( obj_pts, rvec, tvec, mtx, dist ) err np.sqrt(np.mean(np.sum( (img_pts.reshape(-1, 2) - proj_pts.reshape(-1, 2)) ** 2, axis1 ))) return err for i in range(len(object_points)): err project_check( rvecs[i], tvecs[i], object_points[i], image_points[i], mtx, dist ) print(fimage {i:02d} rms: {err:.4f} px)这个脚本用cv2.projectPoints把世界坐标重新投影回像素平面和检测到的角点做差得到单张图片的RMS。一旦某张图的误差明显大于其他图比如超过整体均值的2倍多半是这一张的姿态太极端或者角点亚像素定位失败。把这一张剔除后重新标定整体retval通常能立刻降下来。这是区分新手和老手的关键一步新手只看总RMS老手看的是每张图的误差分布因为平均值会被角点质量好的图片掩盖。本文还有配套的精品资源点击获取
返回列表