拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

相机标定全流程实战:内参、双目与多模型标定源码落地指南

相机标定全流程实战:内参、双目与多模型标定源码落地指南

简介:这份资源是面向计算机视觉学习者与工程师的相机标定项目源码包,聚焦相机内参标定与双目标定两大核心任务,支持多种相机模型与多种标定板,可服务于机器人视觉、自动驾驶、三维重建、视频监控等场景,适合具备一定图像处理基础、希望深入理解标定流程的中高级开发者。压缩包共223个文件,约24.15MB,以170张jpg标定图像、17个h头文件、15个cc实现文件为主,辅以yaml配置、pdf文档、md说明及少量py脚本,涵盖相机模型参数估计、外参求解、标定板角点检测、图像预处理、畸变校正与结果验证等模块,并包含张正友标定法等经典实现。目前已有420人学习下载。读者可借此梳理从特征点提取到参数优化的完整链路,对照源码理解不同相机模型与标定板的适配差异,并参考其目录组织与接口设计,快速搭建自己的标定实验环境。

1. 相机标定项目拆解:内参+双目+多模型多标定板,一套源码怎么落地

做过视觉项目的人多半遇到过这种场景:同一台相机,昨天测出来的焦距和今天差了几个像素,双目测距忽远忽近,机械臂抓取总是偏那么一两毫米。排查半天,问题不在算法,而在标定。相机标定就是求解相机成像几何模型参数的过程,内参标定解决单目相机焦距、主点、畸变系数,双目标定在此基础上还要解出两个相机之间的旋转和平移关系。这个项目标题里提到的“支持多种相机模型+多种标定板”,意味着它不是只跑一个 OpenCV 示例,而是把针孔、鱼眼、以及不同标定板类型都纳入了同一套流程。适合谁?做机器人视觉、自动驾驶感知、工业测量、三维重建的工程师,尤其是手里有 D435i 这类深度相机、需要自己重新标定而不是直接用出厂参数的场景。下面按“概念选型→单目内参→双目标定→避坑→进阶验证”的顺序,把可复现的路径讲清楚。

2. 相机模型与标定板选型:先搞清楚你手里的相机该用哪套参数

2.1 针孔、鱼眼、以及为什么不能一套参数打天下

相机模型本质是把三维世界点投影到二维像素的数学映射。最常见的是针孔模型加径向切向畸变,OpenCV 里对应cv2.CALIB_MODEL_PINHOLE或默认的calibrateCamera。它的参数向量通常是 4 个内参(fx, fy, cx, cy)加 5 个畸变系数(k1, k2, p1, p2, k3)。这套模型在普通工业相机、USB 摄像头上够用,视场角一般小于 120 度。

但如果你用的是鱼眼镜头或者 D435i 的广角红外相机,针孔模型会在边缘产生巨大残差。这时需要鱼眼模型,OpenCV 提供cv2.fisheye模块,参数是 4 个内参加 4 个畸变系数(k1~k4),投影函数用的是等距投影而不是小孔成像。项目标题说“支持多种相机模型”,落地时通常就是根据相机类型选择不同的标定函数和畸变参数维度。

还有一类是 omnidirectional 或 MEI 模型,用于视场角超过 180 度的折反射相机。这类模型在 OpenCV 里没有直接内置,常见做法是用cv2.calibrateCamera配合自定义投影函数,或者用 Kalibr 这类工具。我一般会先看相机规格书里的视场角和镜头类型,再决定用哪套模型。选错了模型,后面重投影误差怎么调都下不来。

提示:D435i 的 RGB 模块可以用针孔模型,但红外双目模块建议用鱼眼模型或至少允许更多畸变系数,否则边缘深度值会明显漂移。

2.2 棋盘格、圆点阵、ChArUco:标定板怎么选、怎么摆

标定板的选择直接影响角点检测精度和标定鲁棒性。常见三类:

标定板类型检测方式优点缺点
棋盘格角点检测角点定位精度高,OpenCV 原生支持遮挡敏感,部分遮挡就丢角点
圆点阵圆心检测对模糊和噪声更鲁棒圆心提取有偏心误差,透视畸变大时不准
ChArUcoArUco 码+棋盘格部分遮挡仍可标定,可实时反馈需要生成和打印,参数稍多

项目标题说“支持多种标定板”,落地时通常就是封装了findChessboardCorners、findCirclesGrid和CharucoDetector三条路径。我一般会优先用 ChArUco,因为它允许你在标定过程中实时看到哪些视角已经覆盖、哪些区域还缺数据,不用等跑完才发现某块区域没有样本。

标定板摆放有几个硬性要求:至少 10 到 20 个不同姿态,覆盖图像中心、四角、边缘;倾斜角度不要超过 45 度,否则角点检测容易失败;标定板要平整,打印后贴在硬质平板上,翘曲会直接引入系统误差。双目标定还要保证标定板同时出现在左右图像中,且尽量覆盖双目共同视场的各个区域。

2.3 从标题到落地:一套标定源码通常包含哪些模块

一个能打的标定项目,源码结构一般分四层:图像采集与标定板检测、单目标定、双目标定、结果验证与导出。采集部分负责读相机流或图像文件夹,检测部分根据标定板类型调用对应函数,单目标定输出内参和畸变,双目标定在单目基础上用stereoCalibrate求解外参,最后用重投影误差和极线校正效果来验证。

如果你拿到的是“项目源码”,先看它有没有把相机模型和标定板类型做成可配置参数。好的实现会把模型类型、标定板尺寸、方格大小、图像路径都放在配置文件里,而不是硬编码。这样你换一台相机只需要改配置,不用动代码。下面几章按这个流程逐步展开。

3. 单目内参标定实操:从采集图像到重投影误差低于 0.3 像素

3.1 采集与角点检测:为什么你的标定结果总是不稳

单目标定的第一步是采集足够多且姿态丰富的标定板图像。我一般会采集 20 到 30 张,太少会导致参数欠约束,太多则可能引入模糊或运动模糊的坏样本。采集时注意:光照均匀,避免反光;标定板在图像中占比 1/3 到 1/2;每张图都要完整出现在画面内,不要切边。

角点检测用cv2.findChessboardCorners配合cv2.cornerSubPix做亚像素优化。这一步的精度直接决定标定上限。常见翻车点是棋盘格行列数传错,比如内角点数是 9x6,你传了 10x7,函数直接返回 False。另一个坑是图像格式,OpenCV 读进来是 BGR,但角点检测只关心灰度,所以先转灰度再检测。

import cv2 import numpy as np import glob # 棋盘格内角点行列数,注意是内角点,不是方格数 pattern_size = (9, 6) square_size = 25.0 # 毫米,实际方格边长 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) 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 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, pattern_size, None) if ret: corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners_refined) else: print(f'角点检测失败: {fname}') print(f'有效图像: {len(objpoints)} / {len(images)}')

这段代码里pattern_size是内角点数,比如 9x6 的棋盘格有 10x7 个方格,但内角点是 9x6。square_size是实际物理尺寸,单位要和后续双目平移向量一致,一般用毫米。cornerSubPix的搜索窗口 11x11 是经验值,图像分辨率高时可以适当加大。如果有效图像少于 10 张,标定结果基本不可信,需要重新采集。

3.2 调用 calibrateCamera:参数怎么设、返回值怎么看

拿到角点后调用cv2.calibrateCamera。这个函数的核心参数是图像尺寸、三维点、二维点、以及 flags。flags 里常用的是cv2.CALIB_FIX_ASPECT_RATIO和cv2.CALIB_ZERO_TANGENT_DIST,前者固定 fx/fy 比例,后者强制切向畸变为零。对于大多数工业相机,切向畸变很小,可以固定为零来减少参数数量,提高稳定性。

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None, flags=cv2.CALIB_FIX_ASPECT_RATIO ) print('内参矩阵:\n', mtx) print('畸变系数:', dist.ravel()) print('平均重投影误差:', ret) # 计算每张图的重投影误差,找出坏样本 total_err = 0 for i in range(len(objpoints)): imgpoints_proj, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) err = cv2.norm(imgpoints[i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj) total_err += err if err > 0.5: print(f'第 {i} 张图误差偏大: {err:.3f} 像素') print(f'整体平均误差: {total_err / len(objpoints):.3f} 像素')

ret是平均重投影误差,单位是像素。一般要求低于 0.5 像素,低于 0.3 算优秀。如果高于 1 像素,说明角点检测有问题或者标定板不平整。mtx是 3x3 内参矩阵,dist是畸变系数。注意CALIB_FIX_ASPECT_RATIO会让 fx 和 fy 成比例,如果相机像素不是正方形,这个 flag 可能引入偏差,可以先不加,看结果再决定。

3.3 畸变校正验证:用 undistort 看边缘是否拉直

标定完必须做畸变校正验证。用cv2.undistort对原图去畸变,看直线是否变直,尤其是图像边缘。如果边缘还有明显弯曲,说明畸变系数没解好,可能是样本没有覆盖边缘区域。

img = cv2.imread('calib_images/test.jpg') 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)

getOptimalNewCameraMatrix的最后一个参数 alpha 控制保留多少边缘像素。alpha=0 会裁掉所有无效像素,alpha=1 保留全部但可能有黑边。我一般用 alpha=0.5 到 1 之间,看实际需求。如果校正后边缘仍然弯曲,回到采集步骤,增加边缘区域的标定板图像。

4. 双目标定:从单目内参到 stereoCalibrate 的完整链路

4.1 双目为什么要先单目标定再标外参

双目相机标定不是直接扔两张图进去就完事。标准流程是:先分别对左右相机做单目标定,得到各自的内参和畸变系数;然后用cv2.stereoCalibrate在已知内参的基础上求解右相机相对于左相机的旋转矩阵 R 和平移向量 T。这样做的好处是参数解耦,单目内参的精度不会和外参混在一起优化,出问题时容易定位。

如果跳过单目直接做双目标定,stereoCalibrate会同时优化所有参数,包括两个相机的内参和外参。参数维度高,容易陷入局部最优,而且你无法判断误差是来自内参还是外参。所以常见做法是:单目标定拿到稳定的内参后,在stereoCalibrate里用cv2.CALIB_FIX_INTRINSIC固定内参,只优化外参。

4.2 stereoCalibrate 调用与极线校正

双目标定需要左右相机同时拍摄同一块标定板,且标定板在左右图中都能检测到角点。采集时注意左右相机同步,如果是硬件触发最好,软件触发要保证时间差小于 1 毫秒,否则运动模糊会导致角点错位。

# 假设左右相机分别已经得到 objpoints, imgpoints_left, imgpoints_right # 且左右图像尺寸一致 ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_l, dist_l, mtx_r, dist_r, gray_left.shape[::-1], flags=cv2.CALIB_FIX_INTRINSIC, criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) ) print('双目重投影误差:', ret) print('旋转矩阵 R:\n', R) print('平移向量 T:\n', T.ravel())

ret是双目重投影误差,一般要求低于 0.5 像素。R和T是右相机相对于左相机的位姿。T的模长就是双目基线,单位和你标定板尺寸一致。如果基线明显偏离实际值,检查标定板尺寸是否填错。

拿到 R 和 T 后,用cv2.stereoRectify做极线校正,让左右图像的极线水平对齐。这一步是后续双目匹配和深度计算的基础。

R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_left.shape[::-1], R, T, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0 ) # 生成映射表,用于实时校正 map1x, map1y = cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_left.shape[::-1], cv2.CV_32FC1) map2x, map2y = cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_right.shape[::-1], cv2.CV_32FC1)

Q是视差到深度的重投影矩阵,后续用cv2.reprojectImageTo3D可以直接得到三维点云。alpha=0表示校正后裁掉无效区域,适合实时系统。如果要做完整视场分析,用alpha=1。

4.3 用极线校正效果判断双目标定是否合格

双目标定合不合格,最直观的验证是极线校正。校正后,左右图像中同一物体应该在同一水平线上。你可以打开校正后的左右图,找几个特征点,看它们的 y 坐标是否一致。如果偏差超过 1 到 2 像素,说明外参不够准。

另一个验证是看stereoRectify输出的roi1和roi2,如果有效区域太小,说明两个相机共同视场覆盖不够,或者标定板姿态没有覆盖共同视场。这时需要重新采集,确保标定板在左右图中都出现在各个区域。

注意:D435i 这类双目结构光相机,红外发射器会干扰标定板检测。标定时用不透明胶带遮住红外发射器,只用红外双目图像做标定,否则角点检测会被散斑干扰。

5. 标定避坑与排查:那些让重投影误差下不来的真实原因

5.1 角点检测失败或角点顺序错乱

现象:findChessboardCorners返回 False,或者检测到的角点顺序和预期不一致,导致标定结果完全错误。

原因:棋盘格行列数传错是最常见的。比如标定板是 10x7 方格,内角点应该是 9x6,你传了 10x7 就永远检测不到。另一个原因是图像模糊或光照不均,角点对比度不够。角点顺序错乱通常发生在标定板旋转角度过大时,检测算法把起始角点认到了另一侧。

解决:先用一张清晰图像单独测试findChessboardCorners,确认行列数正确。如果图像模糊,重新采集。对于大角度旋转,可以在检测前做直方图均衡化,或者改用 ChArUco 板,它的 ArUco 码能提供唯一 ID,不会出现顺序错乱。

5.2 重投影误差低于 0.3 但实际使用仍然不准

现象:标定报告显示平均重投影误差 0.2 像素,但拿去做测距或三维重建,误差却有几十毫米。

原因:重投影误差只反映标定板角点的拟合程度,不反映标定板本身的平整度和尺寸精度。如果标定板打印后翘曲,或者方格尺寸测量不准,角点拟合再好,物理尺度也是错的。另一个原因是标定板姿态覆盖不均匀,所有图像都集中在图像中心,边缘畸变没有被约束。

解决:用硬质平板贴标定板,打印时用高精度打印机。方格尺寸用游标卡尺实测,不要直接用打印设置里的标称值。采集时强制覆盖图像四角和边缘,至少 30% 的图像中标定板位于边缘区域。

5.3 双目基线跳变或深度图出现大面积空洞

现象:双目标定后,基线值每次标定都不一样,或者深度图中间出现大片无值区域。

原因:基线跳变通常是左右相机同步没做好,运动模糊导致角点错位。深度空洞则可能是极线校正后有效区域太小,或者双目匹配算法在纹理缺失区域失败。

解决:双目采集时用硬件触发或至少保证静止拍摄。如果必须动态拍摄,提高快门速度到 1/500 秒以上。深度空洞问题先检查stereoRectify的roi,如果有效区域小于 60%,重新采集标定图像,确保标定板覆盖共同视场的各个角落。

5.4 鱼眼模型标定后边缘仍然弯曲

现象:用cv2.fisheye.calibrate标定后,去畸变图像边缘仍然有弯曲,或者出现奇怪的拉伸。

原因:鱼眼模型的畸变系数 k1~k4 需要足够的边缘样本才能解好。如果标定板图像都集中在中心,边缘畸变没有被约束,k3 和 k4 会解得很随意。另一个原因是鱼眼标定对初值敏感,cv2.fisheye.calibrate需要传入内参初值,如果初值偏差太大,优化会失败。

解决:采集时让标定板出现在图像边缘,至少 10 张图像中标定板位于边缘区域。内参初值可以用针孔模型先标一次,把结果作为鱼眼标定的初值。如果还是不行,尝试固定 k3 和 k4 为零,只解 k1 和 k2,看边缘是否改善。

5.5 标定结果在不同分辨率下不通用

现象:在 1280x720 下标定得到内参,换到 640x480 直接用,结果完全不对。

原因:内参矩阵中的 fx、fy、cx、cy 都是像素单位,和图像分辨率直接相关。分辨率改变后,这些参数需要按比例缩放。畸变系数是无量纲的,通常不变,但某些模型下也会受影响。

解决:换分辨率后重新标定,或者按比例缩放内参。缩放公式:fx_new = fx_old * (w_new / w_old),fy 同理,cx 和 cy 也按比例缩放。但注意,如果相机做了裁剪而不是缩放,主点偏移需要单独计算。最稳妥的做法是每个分辨率单独标定。

6. 标定结果验证与进阶技巧:用极线误差和三维重建做最终裁判

标定完别急着收工,重投影误差只是及格线,真正能说明问题的是极线误差和三维重建精度。我一般会做两步验证。

第一步,极线误差检查。用stereoRectify校正后,在左右图中手动找 10 到 20 对匹配点,计算它们的 y 坐标差值。如果平均差值小于 0.5 像素,说明双目标定合格。如果大于 1 像素,回到stereoCalibrate检查外参。可以用下面的代码快速计算:

# 假设 pts_left 和 pts_right 是校正后左右图中的匹配点,形状为 Nx2 pts_left = np.array([[100, 200], [300, 400], [500, 300]], dtype=np.float32) pts_right = np.array([[95, 200.5], [295, 400.3], [495, 300.2]], dtype=np.float32) y_diff = np.abs(pts_left[:, 1] - pts_right[:, 1]) print(f'平均极线误差: {y_diff.mean():.3f} 像素') print(f'最大极线误差: {y_diff.max():.3f} 像素')

第二步,三维重建验证。用校正后的图像做双目匹配,生成视差图,再用reprojectImageTo3D得到三维点云。拿一个已知尺寸的物体放在视场中,测量点云中对应点的距离,和实际尺寸对比。如果误差在 1% 以内,标定结果可以放心用。如果误差超过 5%,检查标定板尺寸和基线是否准确。

进阶技巧方面,如果你经常需要重新标定,可以把整个流程封装成配置文件驱动。相机模型、标定板类型、图像路径、输出路径都写在 YAML 里,换相机只改配置。另外,标定结果要保存成可读格式,比如 JSON 或 YAML,方便后续程序加载。我习惯把内参、畸变、双目外参、极线校正映射表全部存到一个文件里,部署时直接读,避免每次重新标定。

还有一个血泪经验:标定环境尽量和实际使用环境一致。温度变化会导致镜头轻微形变,如果标定时在空调房,使用时在户外高温,内参会漂移。工业场景下,我一般会在工作温度下静置相机 30 分钟再标定。标定板不用时放在干燥箱里,避免受潮翘曲。这些细节看起来玄学,但翻车的时候往往就是它们。

希望帮到你。

本文还有配套的精品资源,点击获取

返回列表