ARTICLE DETAIL

资讯详情

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

Python+OpenCV相机标定实战:从棋盘格设计到畸变校正

Python+OpenCV相机标定实战:从棋盘格设计到畸变校正 直接用现成的例子聊吧。去年我帮一个做机器人抓取的朋友做视觉定位他买了个工业相机装上去之后测距误差大到离谱测个300毫米的距离能偏出十几毫米。折腾半天最后发现不是相机质量问题而是镜头畸变和安装误差根本没修正。后来我用Python加OpenCV给他做了个标准的内参标定误差直接压到亚毫米级整个项目才算真正跑起来。这事其实特别典型很多刚接触视觉的人都有个误区觉得相机买回来画面清晰就能直接用来测量。但实际上相机镜头的光学畸变、感光芯片的安装误差、镜头和芯片的相对位置都会导致像素坐标和实际物理坐标之间不是一个简单的等比关系。相机标定干的事情就是把这些误差用数学方式描述出来然后通过算法把这些误差修正掉。这篇文章就以我那个机器人项目的实操过程为例完整走一遍利用Python语言进行相机标定的流程。从标定原理、棋盘格设计和图像采集到OpenCV代码实现、重投影误差评估再到各种奇葩问题的排查思路全部安排上。不管你是做视觉测量、机器人抓取、SLAM还是简单的图像畸变矫正这套东西都能用得上。1. 相机标定整体设计与思路拆解1.1 标定的本质先搞清楚你在解什么方程相机标定说白了就是求解一个从三维世界坐标到二维像素坐标的映射关系。这里面包含了几个环节的变换世界坐标系下的点先通过刚体变换旋转矩阵R和平移向量t转到相机坐标系再通过透视投影落到图像物理坐标系最后通过像素坐标系转换到最终的像素坐标。这几个变换串在一起可以写成这样一个式子s * [u, v, 1]^T K * [R | t] * [X, Y, Z, 1]^T其中K是内参矩阵R和t是外参s是尺度因子。内参描述的是相机自身的性质焦距、主点位置、畸变系数外参描述的是相机在世界坐标系中的位置姿态。这里有个关键点对于单目相机内参标定我们只需估计K和畸变系数如果要标定外参那还需要额外设计标定场景比如棋盘格放置在世界坐标系的已知位置或者用车载相机对着已知尺寸的标定物采集。畸变模型常用的是Brown-Conrady模型包含径向畸变k1, k2, k3和切向畸变p1, p2。径向畸变是镜头形状导致的画面的边缘尤其明显表现为直线变弯切向畸变是镜头和成像平面不完全平行导致的表现为原本平行的线在画面边缘产生偏移。1.2 为什么选OpenCV它把最难的部分封装好了相机标定这套流程自己做算法实现非常痛苦要处理非线性优化、多视图几何、特征点匹配之类的老多问题。OpenCV提供了cv2.calibrateCamera、cv2.findChessboardCorners等封装程度很高的接口可以直接完成角点提取和参数求解。Python版OpenCV的优势在于接口设计和C版本基本一致网上资料多遇到问题很容易搜到解决方案配NumPy、Matplotlib做数据处理和可视化非常方便标定结果的质量检查重投影误差分布可视化写起来很顺手跨平台Windows、Linux都能跑我后来在Ubuntu 18.04的工控机上跑同样的代码也没遇到兼容问题实际项目中我基本只用Python加OpenCV就把标定流程干完了唯一让我额外装了的是OpenCV的contrib模块原因是需要用aruco板做外参标定普通版OpenCV不自带aruco模块。1.3 标定板的选择棋盘格还是圆点阵OpenCV支持棋盘格、圆点阵列、ArUco码等多种标定板。我个人的经验是内参标定优先用棋盘格外参标定优先用ArUco码。棋盘格的优点是角点检测极其稳定。cv2.findChessboardCorners内部经过了图像二值化、四边形筛选、亚像素细化等步骤只要棋盘格没有被极端遮挡或者严重过曝基本都能检测到。棋盘格的角点位置精度可以用cv2.cornerSubPix进一步细化到亚像素级别。圆点阵列的检测也支持但对图像质量要求更高光线不均匀时容易出现误检。ArUco码的优点是每个码都有独立id可以快速识别出多个码的位置适合做外参标定尤其是相机和激光雷达联合标定时ArUco码比棋盘格好使。如果你做的是简单畸变矫正其实一张不规则的棋盘格照片就够了。但如果要做完整的标定建议采集15至25张不同位姿的图像覆盖画面中心和边缘每张棋盘格都在画面中有不同的倾斜角度和距离。2. 核心细节解析与实操要点2.1 棋盘格规格设计别等打印出来才后悔棋盘格的规格设计直接决定了角点检测的稳定性和标定精度。这里有个常见坑买的棋盘格是A4纸大小一个格子边长25毫米内角点数量是9乘6拍出来的图像能检测到角点但标定出来的焦距值总是飘误差特别大。真正原因是棋盘格尺寸太小画面中只占了不到三分之一区域角点覆盖的视场范围太窄导致优化问题的约束不够。设计棋盘格时重点考虑三件事内角点数量比如9乘6的意思是横向9个内角点、纵向6个内角点对应的物理格子数是10乘7。选多少合适主要看你的应用场景。如果标定板要在画面中占据足够大的比例格子数量多意味着同样的物理尺寸下每个格子更小但角点过于密集容易导致检测错误。我常用的是9乘6或8乘6这个数量在OpenCV的默认参数下识别很稳。物理尺寸格子边长要结合实际视场来选择。如果相机距离标定板300毫米画面中要占据约200毫米宽那选15毫米或20毫米的格子都行。关键是确保打印后棋盘格在画面中至少有50%以上的面积占比。打印材质不建议用普通A4纸直接打印纸张起皱会导致棋盘格平面不共面精度直接报废。我给朋友推荐的是贴在铝板上的哑光覆膜棋盘格造价不高而且耐用。室外场景还得防反光哑光膜是必须的。2.2 图像采集规范拍不好后面都白搭图像采集是相机标定里最容易被忽略但最关键的一步。OpenCV的标定算法在数学上是凸优化问题理论上只要有足够多的图像就能收敛到全局最优但实际拍摄的图片质量不好重投影误差依然会很大。采集图像要注意的地方拍摄数量至少15张建议20张以上。图像覆盖要包含相机视场中心区域和边缘区域同时要覆盖不同的倾斜角度标定板平面和相机光轴之间的夹角最好从0度到45度都覆盖一遍。光照要均匀避免强反光和过曝棋盘格黑白格交界处的对比度要清晰。如果使用玻璃镜面反光严重的标定板建议调整拍摄角度或者用偏振片消除反光。标定板必须是全幅对焦清晰的画面中不能有模糊模糊的图像会直接导致角点提取精度下降。拍摄时标定板尽量保持平整手持时容易弯曲用刚性背板支撑最好。拍摄过程中相机保持固定移动标定板即可。我还试过一个极限做法用一段视频来抽取关键帧。把标定板在相机面前慢慢转动和移动用cv2.VideoCapture录一段15秒的视频然后间隔5帧抽一张图再筛选出清晰的图片。这个方法能偷懒但需要额外写筛选逻辑否则很容易把模糊帧混进去。2.3 棋盘格角点提取的参数选择OpenCV中cv2.findChessboardCorners的第三个参数是flags很多人不关注它实际上这个参数的设置直接影响角点检测成功率和定位精度。常用的flags组合cv2.CALIB_CB_ADAPTIVE_THRESH用自适应阈值替代全局阈值对光照不均的图像效果明显建议加上cv2.CALIB_CB_NORMALIZE_IMAGE在阈值化前对图像做直方图均衡化对过暗或过亮的图像有帮助cv2.CALIB_CB_FILTER_QUADS四边形筛选提升检测精度我的习惯是默认都加上corners, ids cv2.findChessboardCorners(gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE cv2.CALIB_CB_FILTER_QUADS)角点检测完成后还需要用cv2.cornerSubPix做亚像素细化。这一步非常必要因为findChessboardCorners找到的角点精度通常是像素级的而亚像素细化能到0.1像素以内。细化参数通常设置winSize为(11, 11)零区域为(-1, -1)终止条件是迭代30次或误差小于0.001。3. 实操过程与核心环节实现3.1 环境准备Python和OpenCV装好即可我用的环境是Python 3.8加OpenCV 4.5.5这套组合在Windows和Ubuntu上都很稳定。如果还没装好环境直接参照下面步骤# 推荐使用虚拟环境方便隔离不同项目的依赖 python -m venv calib_env # Windows下激活虚拟环境 calib_env\Scripts\activate # Linux下激活虚拟环境 source calib_env/bin/activate # 安装核心依赖 pip install opencv-python opencv-contrib-python numpy matplotlibopencv-python和opencv-contrib-python一起装可能会冲突建议直接只装contrib版本因为contrib里包含了普通版的所有模块还多了aruco等扩展模块pip install opencv-contrib-python装完后检查一下版本确保cv2能正常导入。Windows下如果安装的是官方Python建议用64位版本不然还会遇到位宽不匹配的问题。3.2 完整代码实现从图像导入到标定结果输出这里的完整代码是我那个机器人项目的标定模块去掉了很多项目相关的逻辑保留了核心调用链可以拿来即用import cv2 import numpy as np import glob import matplotlib.pyplot as plt # 棋盘格内角点数横向纵向 pattern_size (9, 6) # 棋盘格格子边长单位毫米 square_size 25.0 # 生成棋盘格上所有内角点的世界坐标z0平面 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 [] # 世界坐标系中的3D点 img_points [] # 图像坐标系中的2D点 images glob.glob(calib_imgs/*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 flags cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE cv2.CALIB_CB_FILTER_QUADS ret, corners cv2.findChessboardCorners(gray, pattern_size, flagsflags) if ret: obj_points.append(objp) # 亚像素细化把角点精度从像素级提升到亚像素级 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) img_points.append(corners2) # 绘制并显示角点方便确认每张图的检测效果 cv2.drawChessboardCorners(img, pattern_size, corners2, ret) cv2.imshow(corners, img) cv2.waitKey(100) else: print(f未能找到角点: {fname}) cv2.destroyAllWindows() # 相机标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(重投影误差:, ret) print(内参矩阵:\n, mtx) print(畸变系数:\n, dist)这里cv2.calibrateCamera返回的ret是重投影均方根误差单位是像素。这个值能直接反映标定质量一般小于0.1像素说明标定结果很好小于0.5像素也算可用大于1像素就要考虑是不是图像质量问题或者标定板规格填写有误。3.3 结果分析与重投影误差评估标定完成后拿到内参矩阵mtx和畸变系数dist但光看数值不够还得做两件事一个是直观评估每个角点的重投影误差分布另一个是用标定结果实际矫正图像看看效果。逐图误差分析可以帮助定位哪些图像质量差# 计算每张图的重投影误差并可视化 mean_error 0 per_image_errors [] for i in range(len(obj_points)): img_points_proj, _ cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) error cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2) / len(img_points_proj) per_image_errors.append(error) mean_error error mean_error / len(obj_points) print(平均重投影误差:, mean_error) # 画出每张图的误差柱状图 plt.bar(range(len(per_image_errors)), per_image_errors) plt.xlabel(图像序号) plt.ylabel(重投影误差像素) plt.title(每张标定图像的重投影误差) plt.show()误差特别大的那几张建议直接从数据集中剔除重新标定。还有一个检查点内参矩阵的cx和cy应该接近图像中心坐标。如果偏差过大说明标定板图像过于集中在一侧需要补拍中心区域或对称位姿的图像。图像矫正验证畸变校正效果# 读取一张测试图像并矫正畸变 img cv2.imread(test_img.jpg) h, w img.shape[:2] # 计算新的相机内参矩阵可以调节alpha参数控制图像保留范围 newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) # 用双线性插值做畸变矫正 undistorted cv2.undistort(img, mtx, dist, None, newcameramtx) # 裁剪掉矫正后的黑色边框 x, y, w_roi, h_roi roi undistorted undistorted[y:yh_roi, x:xw_roi] # 把原图和矫正图拼在一起对比 combined np.hstack([img, undistorted]) cv2.imwrite(undistort_compare.jpg, combined)矫正后的图像边缘的直线应该恢复成直线比如拍摄一个长方形的包装盒矫正后的边缘就不再是弧线了。如果矫正效果不理想比如边缘出现严重的紫色锯齿或波浪通常说明dist参数中的k1、k2、k3估计不准确可以检查采集图像中是否包含足够多靠近画面边缘的棋盘格图像。4. 常见问题与排查技巧实录4.1 角点检测失败的几种原因findChessboardCorners返回False的情况非常普遍。我总结下来最常见的是这几种pattern_size填写错误这是最高频的问题。很多人把棋盘格标称的9x6直接当成pattern_size但这个参数要求的是内角点数也就是物理格子数减一。我在项目中用的标定板是横向10个格子、纵向7个格子内角点就是9x6。如果填成10x7检测必然失败。标定板占据画面太小棋盘格太小的时候角点之间的像素距离过短OpenCV的四边形检测算法容易漏检。建议标定板在画面中的宽度占比超过50%。光照不均或过曝强光照射会在棋盘格表面产生强烈反光导致黑白格的对比度区域失衡。用哑光覆膜标定板能减少这个问题。运动模糊手持标定板快速移动时相机的快门速度不够快图像会拖影。确保图像中的棋盘格边缘清晰锐利即可。4.2 标定结果误差过大的调试路线标定完成后如果重投影误差大于0.5像素建议按这个顺序排查第一步检查标定板的物理尺寸是否有误。square_size填错会导致焦距等参数的绝对值全部偏差而重投影误差可能看起来还好因为它是像素误差单位尺寸错不会直接反映在这个指标上但会直接影响后续的测距精度。第二步检查世界坐标生成是否正确。objp中mgrid生成坐标时是(x, y)配对注意顺序别反了。如果顺序反了标定能收敛但内参矩阵的对角线元素会异常。第三步剔除误差大的图像。一张模糊的图像可能会把整体误差拉高很多。用上面第三节的逐图误差可视化把明显离群的那几张图删掉再重新标定。第四步考虑是否存在标定板弯曲。如果标定板是普通纸张打印的贴在不平的桌面上曲面的棋盘格会让重投影误差始终压不下来换铝板或亚克力板做支撑能解决。这里有个直观的评估公式标定后的测距误差 重投影误差像素 * 像素对应的物理尺寸毫米/像素。假设像素对应的物理尺寸是0.1毫米每像素重投影误差为1像素那测距误差就有0.1毫米级别如果重投影误差能压到0.05像素那测距误差能到5微米级别。所以重投影误差越低压得越低测量结果越可靠。4.3 OpenCV版本不一致导致的坑老版本OpenCV比如3.x的cv2.calibrateCamera返回值和4.x版本完全一样但内部算法有细微差异导致同样的图像在不同版本下标定出来的结果略有不同。这个不奇怪不同版本的优化实现有差异。真正需要注意的坑是cv2.findChessboardCorners在OpenCV 4.5以上版本对flags参数的处理有变化某些组合可能不生效。如果升级OpenCV后发现原本正常的代码检测率变差了可以先只保留CALIB_CB_ADAPTIVE_THRESH试试。另外OpenCV 4.x中cv2.cornerSubPix支持了基于亚像素精度的角点细化推荐在细化前先把图像转换成灰度图uint8或float32都行但不要传彩色图否则会报类型错误。4.4 给双目或多相机系统做标定的补充如果做双目相机标定除了单目标定还要标定两个相机之间的相对位姿旋转矩阵R和平移向量T。OpenCV提供了cv2.stereoCalibrate接口输入是两摄像头同一时刻拍摄的标定板图像在单目标定的基础上求解双目的相对外参。具体流程是先用单目标定分别得到两个相机的内参再用cv2.stereoCalibrate对双目图像联合优化得到相机之间的R和T。我个人建议在实施双目标定前确保两个相机的单目重投影误差都小于0.2像素否则联合标定结果很难收敛。还有一个容易忽略的问题双目系统中的两个相机最好选择同型号镜头这样畸变系数接近立体匹配时的极线校效果会更好。如果两个镜头焦距差异太大标定结果虽然能算出来但实际立体匹配的视差范围会很别扭。另外补充一点如果你做的是机器人的手眼标定那还要把标定板放在机械臂末端采集多组机械臂位姿和对应的标定板图像用cv2.calibrateHandEye来求解相机和机械臂基座之间的变换关系。这类标定的目标函数和单目不太一样误差来源也更多对图像质量和机械臂执行精度都有要求。5. 写在最后的实战经验又啰嗦了不少最后说点个人体会。我做相机标定做了这么多年最大的感悟是标定算法本身不需要懂太深的数学就能跑通但真正决定标定质量的往往是采集数据时的基本功。你拍摄的20张图像如果都是相同位姿、标定板集中在画面中央算法再怎么优化也救不回来。反过来哪怕你用稍微旧一点的OpenCV版本只要图像覆盖了画面的各个区域、每个角点都清晰锐利、标定板是刚性的结果绝对差不到哪里去。另外标定并不是一劳永逸的事情。镜头稍微碰了一下、调了个焦距、换了个安装位置内参和外参都会发生变化。如果项目对测量精度要求高开工前用几分钟快速拍一组图像重新标定一次真的能避免后续很多莫名其妙的误差问题。如果后续想做更深的方向可以往双目立体视觉、相机激光雷达联合标定、手眼标定这几个方向扩展。原理相通代码框架基本都是这套东西只是变换关系里多了一两个环节而已。
返回列表