Python+OpenCV实现相机标定:从棋盘格到内参矩阵的完整方案
简介面向计算机视觉初学者与机器人、三维重建等领域的开发者这份相机标定Python程序提供了完整可用的内参求解方案。资源自带11×10规格的正友棋盘格图片既可打印后拍摄也可直接放在显示器上配合附带程序使用大幅降低了采集门槛。包内共14个文件包括1个主程序main.py、1个图片预览脚本ChessboardView.py、10张JPG标定样例图、1张PNG棋盘格模板及1个TXT说明文档压缩包仅2.19MB目录结构清晰。使用时只需修改图像文件夹路径与实际棋盘格边长即可运行并得到相机内参方便后续进行畸变矫正、三维重建等任务。目前已有907人学习下载适合希望快速上手标定流程的入门及进阶用户拿到后既可对照样例复现也可替换为自己的拍摄图片。1. 从棋盘格到内参矩阵一套能直接落地的 Camera calibration Python 方案如果你手里有一台相机、一张打印的棋盘格想算出它的内参矩阵和畸变系数那么你需要的不是 Ros 里那套重量级标定流程而是一个能在本地用 Python 直接跑通的最小实现。这就是本标题要解决的事不依赖 ROS、不依赖 MATLAB 工具箱只靠 OpenCV 的 findChessboardCorners 和 calibrateCamera 两个核心函数就能从一组棋盘格照片里得到相机内参。这套方案适合算法工程师验证镜头畸变、机器视觉项目里估算像素当量也适合刚入门相机模型的人理解内参到底在解什么方程。关键是代码可以复制下来直接改路径运行棋盘格用 A4 纸打印即可。2. 先弄懂内参在解什么从像素坐标到相机坐标的映射关系2.1 针孔模型里的 4 个内参和 5 个畸变系数常见做法是先把相机当成针孔模型空间里一个点(X, Y, Z)投影到像素平面(u, v)的过程可以写成s * [u, v, 1]^T K * [R|t] * [X, Y, Z, 1]^T其中K就是内参矩阵展开后是K [fx, 0, cx; 0, fy, cy; 0, 0, 1]fx和fy是 x 和 y 方向的等效焦距单位是像素cx和cy是光心在像素坐标系里的坐标也叫主点。除了这 4 个参数真实镜头还有径向畸变和切向畸变OpenCV 用 5 个系数表达k1, k2, p1, p2, k3。其中k1, k2, k3负责径向畸变p1, p2负责切向畸变。标定棋盘格本质就是利用棋盘格的角点坐标已知这个约束反解出K和畸变系数。你可能注意到cx, cy并不一定等于图像分辨率的一半。如果你拿到的内参里主点位置明显偏离图像中心不要急着认为标定失败了可能是相机感光芯片装配偏移也可能是标定板只覆盖了画面局部区域。2.2 棋盘格为什么比圆点阵列更适合做标定棋盘格的角点是二阶梯度上的鞍点OpenCV 的findChessboardCorners算法对这类角点的定位精度能达到亚像素。相比之下圆点阵列的圆心拟合精度受镜头畸变影响更大而且圆点阵列通常需要额外的编码信息来保证方向唯一性。棋盘格的另一个优势是角点数量可控。你只需要知道每行每列的内角点数不是格数算法就能按照顺序把所有角点排列好不需要你在标定板上预先布置任何朝向标记。当然OpenCV 从 4.5 之后也支持CALIB_CB_MARKER标志让棋盘格带上 ArUco 标记来保证方向但对大多数场景来说普通棋盘格已经够了。2.3 角点顺序和世界坐标的对应规则标定算法要求你把图像上的角点坐标与一个已知的世界坐标对应起来。常见做法是以棋盘格左上角第一个内角点为原点格子边长作为单位长度建立一个平面坐标系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)这里pattern_size是(列数, 行数)的内角点数量。注意mgrid生成的顺序是 x 方向从 0 到cols-1y 方向从 0 到rows-1这个顺序必须和findChessboardCorners返回的角点顺序一致。如果你用的是(9, 6)的棋盘格那么实际是 10x7 的格子内角点是 9x6。我见过不少人在这里把pattern_size填成(10, 7)导致所有棋盘格照片都找不到角点。判断标准很简单看打印出来的棋盘格数一数黑白格子交界点的个数不是格子本身的个数。3. 用 Python 直接跑通棋盘格标定的最小实现3.1 安装依赖与准备标定板开发环境建议用 Python 3.8 及以上版本核心依赖只有两个pip install opencv-python numpy标定板直接用 A4 纸打印即可。如果你要标定的是工业相机或者长焦镜头最好把棋盘格贴在硬纸板或者铝板上避免纸张弯曲。格子边长建议在 20mm 到 50mm 之间边长大小不参与内参计算因为内参和畸变跟实际物理尺寸无关只和像素坐标有关。但如果你之后想把内参与位姿估计结合需要知道实际边长这时才需要精确测量。拍摄时注意三点一是棋盘格要覆盖图像的四个角落和中心区域否则畸变系数估计会不准二是每张照片里棋盘格的角度要有变化倾斜、旋转、远近都要有三是保证角点清晰不要有运动模糊。提示如果你用的是摄像头而不是工业相机先手动固定曝光和焦距不要在采集过程中让自动对焦反复变动否则标定结果会漂移。3.2 角点提取与亚像素精化import cv2 import numpy as np def find_corners(image, pattern_size(9, 6)): gray cv2.cvtColor(image, 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) return corners_refined return None这段代码先转灰度再调用findChessboardCorners找角点找到后用cornerSubPix做亚像素精化。criteria里的30是最大迭代次数0.001是角点位置变化的精度阈值。窗口大小(11, 11)表示在角点周围 11x11 的区域内搜索这个值越大定位越平滑但太小棋盘格纹理复杂的区域容易跑偏。如果你发现某张照片的角点像是「跳格」了也就是同一行的角点突然偏移到另一行通常是棋盘格部分被遮挡或者反光。直接丢弃这张不要手动修正因为畸变拟合对异常点的敏感度很高。3.3 调用 calibrateCamera 求解内参def calibrate(img_files, pattern_size(9, 6), square_size1.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 [] for f in img_files: img cv2.imread(f) corners find_corners(img, pattern_size) if corners is not None: obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, img.shape[:2][::-1], None, None ) return mtx, dist, rvecs, tvecscalibrateCamera的输入有三组关键内容世界坐标点列表、图像角点列表、图像分辨率。输出中mtx是 3x3 内参矩阵dist是 5 个畸变系数rvecs和tvecs是每张图的外参也就是棋盘格相对相机的旋转和平移向量。这里有个容易忽视的参数img.shape[:2][::-1]把图像的(height, width)转成(width, height)传给 OpenCV。如果你直接传img.shape在非方形图像上标定出来的cx和cy会出现系统性的偏差。3.4 标定质量的快速验证重投影误差OpenCV 在calibrateCamera返回值里已经给出了ret这个值就是所有角点的均方根重投影误差。经验阈值是小于 0.5 像素工业场景最好小于 0.2 像素。但ret只能告诉你整体好坏看不出某张图是否拖后腿所以需要逐张计算def per_image_error(obj_points, img_points, rvecs, tvecs, mtx, dist): errors [] for i in range(len(obj_points)): proj, _ cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) err cv2.norm(img_points[i], proj, cv2.NORM_L2) / len(proj) errors.append(err) return errors把每张图的误差打印出来如果某张图的误差明显大于其他图比如超过 2 倍优先检查这张图是否拍摄角度过于极端或者棋盘格边缘被镜头畸变拉得过于模糊。剔除后重新标定通常整体误差会明显下降。4. 参数设置与常见误用的边界4.1 不同畸变模型怎么选calibrateCamera有一个flags参数控制畸变模型的复杂度默认None表示使用 5 个畸变系数(k1, k2, p1, p2, k3)。如果你的镜头是鱼眼或者超广角5 参数模型拟合不了边缘的大畸变这时要用cv2.fisheye模块。flags 取值含义适用场景默认 None使用 5 参数畸变模型普通工业镜头、手机主摄CALIB_ZERO_TANGENT_DIST切向畸变强制为 0镜头装配工艺好、只需径向修正CALIB_FIX_K1-K3固定部分径向系数已知镜头工厂标定值、只标主点CV_CALIB_USE_INTRINSIC_GUESS用给定的初始内参迭代相机内参已知、标定外参我在实际项目里一般先用默认 5 参数标一遍如果发现dist里k3接近 0 且不改善重投影误差就改用CALIB_FIX_K3重新标定这样能减少过拟合。4.2 照片数量和位姿分布的硬性要求最少需要多少张照片理论上是 3 张不同姿态的棋盘格就能解出内参但实际经验是至少 10 到 15 张。这里的关键不是数量而是位姿多样性。如果你把 20 张照片都拍成棋盘格正对相机、只有远近变化那么你的标定结果在画面中心区域是准的但边缘畸变系数会很不稳。推荐的位置分布是正面 3 到 4 张左右旋转各 2 张上下俯仰各 2 张四个角落各 1 张再加上略微倾斜的 2 张。每张照片里棋盘格面积不要小于画面的十分之一否则角点提取虽然成功但亚像素定位精度会下降。# 采集时可以保存为 jpg但不要用手机自带的美化或者 HDR 模式 # 如果使用工业相机建议保存为无损 PNG 或 BMP4.3 棋盘格角点提取失败的六个常见原因角点提取是整套流程里最容易出问题的环节。我把踩过的坑按出现频率排了个序分辨率太低棋盘格在画面里太小角点之间的像素距离不足 10 个像素提取失败率极高。强反光打印的棋盘格纸张反光时黑白格的灰度对比会反转findChessboardCorners找不到鞍点。运动模糊手持相机快门太慢角点变成椭圆亚像素定位失效。图案方向棋盘格不是纯平面弯折的纸张会让世界坐标假设失真。亮度过曝白色格子被过曝成一片纯白黑色格子的边缘梯度信息丢失。背景干扰棋盘格周围有大量类似棋盘格纹理的物体算法会找错角点区域。提示如果你用的是 Python 的cv2.imread读取灰度图注意 OpenCV 默认通道顺序是 BGR。转灰度时用cv2.cvtColor不要直接读成灰度图再用。4.4 标定结果里最容易被误读的三个量fx和fy不是物理焦距而是焦距除以像素尺寸后得到的值。对于像素尺寸接近正方形的传感器fx和fy应当非常接近。如果两者相差 5% 以上检查一下图像是否被缩放或裁剪过。cx和cy是主点坐标正常情况下接近图像中心但并非必须等于中心。标定出来的主点偏离中心几十像素是正常现象。如果偏离几百像素基本可以判定某张图的角点顺序错了。dist的顺序是(k1, k2, p1, p2, k3)不是(p1, p2, k1, k2, k3)。用的时候如果直接喂给undistort不会出错因为 OpenCV 内部有对应关系但如果你自己写畸变公式顺序就会搞错。5. 内参的应用与验证用 undistort 和 imu 内参标定做交叉验证5.1 用 undistort 验证去畸变效果拿到内参和畸变系数后最容易上手的验证方式是直接对一张未参与标定的照片做去畸变def undistort_image(image, mtx, dist): h, w image.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) dst cv2.undistort(image, mtx, dist, None, newcameramtx) x, y, w_roi, h_roi roi dst_cropped dst[y:y h_roi, x:x w_roi] return dst_croppedgetOptimalNewCameraMatrix的第四个参数alpha1表示保留所有原始像素去畸变后图像边缘会出现黑色区域alpha0表示裁剪掉黑色区域但会损失部分视场角。实际项目里如果你需要保留完整视场角做测量常用alpha1然后根据 ROI 裁剪掉黑边。如果是做人脸或者物体识别alpha0可以让图像更干净。5.2 内参结果与 imu 内参标定的关联在机器人和视觉 SLAM 场景里相机内参标定往往只是第一步后面跟着的是相机与 IMU 之间的外参标定。这里的「imu内参标定」通常指 IMU 自身的零偏和尺度因子而不只是相机内参。但两者之间有一个容易混淆的点IMU 内参标定需要用到相机的内参结果来解算相机位姿进而估计 IMU 轨迹。如果你看到「visionmaster进行相机内参标定」这类工作流它本质上是把棋盘格标定嵌入到更复杂的多传感器标定流程里但第一步仍然是findChessboardCorners加calibrateCamera。棋盘格标定得到的重投影误差会直接影响后续外参标定的精度。如果相机内参标定误差超过 0.5 像素IMU 与相机之间的旋转估计就会明显产生漂移。5.3 用 Remap 做离线去畸变对于视频流逐帧调用undistort性能很差。正确做法是先算一次映射表然后用remap处理每一帧def create_undistort_maps(mtx, dist, size): newcameramtx, _ cv2.getOptimalNewCameraMatrix(mtx, dist, size, 1, size) mapx, mapy cv2.initUndistortRectifyMap( mtx, dist, None, newcameramtx, size, cv2.CV_32FC1 ) return mapx, mapy # 对每一帧 # undistorted cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR)initUndistortRectifyMap生成两个浮点映射表mapx和mapy分别表示目标图像每个像素在原始图像中的采样坐标。用CV_32FC1精度足够不用CV_16SC2后者在插值时会有轻微精度损失。生成一次映射表后视频流里的每帧处理就只是一个查表加插值操作4088x3072 分辨率的图像也能实时跑。5.4 标定结果保存与格式约定把内参和畸变系数保存为 NumPy 格式方便后续加载np.savez(camera_calib.npz, mtxmtx, distdist, newcameramtxnewcameramtx) with np.load(camera_calib.npz) as data: mtx_loaded data[mtx] dist_loaded data[dist]如果你要在 C 或者其他语言里复用直接导出为 JSON 或 YAML 更通用。注意保存时保留原始图像分辨率否则换分辨率之后内参矩阵里的fx, fy, cx, cy都不能直接沿用需要按比例缩放。缩放公式是fx_new fx * new_width / old_width主点同理。6. 进阶验证技巧用棋盘格反投影检查标定板平面度标定做完后除了重投影误差还有一个更直接的验证方法把每张标定照片里的棋盘格角点用标定得到的外参反投影回三维空间检查这些点是否落在同一个平面上。这个技巧能暴露一个重投影误差无法发现的问题——标定板本身不平时内参和畸变系数会被棋盘格的弯曲补偿掉但你换一个场景拍摄去畸变效果就会劣化。def check_planarity(obj_points, rvecs, tvecs, mtx, dist): for i in range(len(obj_points)): world_points obj_points[i].squeeze() # 用该图像的外参把局部坐标转到相机坐标 rot_mat, _ cv2.Rodrigues(rvecs[i]) cam_coords (rot_mat world_points.T).T tvecs[i].ravel() # 拟合平面并计算点到平面的最大距离 centroid cam_coords.mean(axis0) centered cam_coords - centroid _, _, vh np.linalg.svd(centered) normal vh[-1] distances np.abs((cam_coords - centroid) normal) print(fimage {i}: max plane deviation {distances.max():.4f} mm)这段代码的思路是棋盘格上所有角点在理想情况下共面所以先把角点通过外参变换到相机坐标系然后做 SVD 拟合平面最后计算每个角点到拟合平面的距离。如果最大偏差超过 0.1 个格子边长说明标定板有弯曲或者外参解算不稳定。这个检查不需要额外采集数据直接用标定时的中间结果就能跑。另一个容易被忽略的验证点是检查去畸变后的图像里画面边缘的直线是否变直。你把一张有直尺、墙面线条或任何直线结构的照片做去畸变然后在图像编辑器里拉一条参考直线对比。如果边缘直线仍然弯曲说明畸变系数没有完全拟合可能原因是你拍的照片里棋盘格没有覆盖到画面边缘。重新采集时把棋盘格尽量贴近画面四角。最后说一下我的习惯每轮标定我都会固定随机种子采集照片顺序然后在同一台相机上重复标定 3 次比较 3 次内参矩阵的差异。如果fx的波动超过 0.5%说明采集数据里有不可靠的照片需要筛选重拍。这套流程走完你手里的camera_calib.npz才算是真正能直接用于后续开发和验证的产物。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →