ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

相机标定实战:张正友标定法配合OpenCV从棋盘格到内参全解

相机标定实战:张正友标定法配合OpenCV从棋盘格到内参全解 简介一份基于OpenCV实现张正友相机标定的完整工程资源面向需要学习相机标定原理与工程实现的开发者、研究人员解决从标定板制作、图像采集到相机内外参计算与畸变矫正的全流程问题。压缩包共107个文件大小约95.67MB内容涵盖VS2015工程源码cpp、vcxproj、sln、bat自动化脚本等、棋盘格打印PDF、10张手机拍摄与20张摄像头拍摄的标定测试图以及标定得到的mat矩阵与txt文本结果、运行日志等结构清晰便于按需取用。工程代码注释详细支持一键运行能够输出相机内参、外参、旋转和平移矩阵并附带标定效果评价模块同时可使用标定结果对原始棋盘图进行矫正直观展示算法效果且标定结果与Matlab非常接近。资源还提供了pdb、ilk等调试文件方便读者跟踪执行流程测试图像覆盖不同拍摄设备增强了标定的泛化参考价值。目前已有6805人浏览学习适合计算机视觉方向的初学者与进阶者参考。 搞视觉的人基本都逃不过相机标定这一关尤其是做机器人抓取、SLAM、三维重建或者自动驾驶感知的拿到的相机如果内参不准后面所有计算都像用一把没校零的尺子量东西。张正友标定法配合OpenCV实现是目前最常用、也最适合入门的一套方案——你只需要一块棋盘格、一台相机、十来张照片就能把相机的内参、畸变系数和外参全部求出来。这篇文章我会把整个流程拆开讲清楚从棋盘格打印PDF怎么生成到测试图片怎么采集再到OpenCV代码一步步怎么写最后附上我实际跑标定踩过的坑和排查技巧。无论你是刚接触视觉的新手还是想系统梳理一遍标定细节的从业者这篇都值得看完再动手。1. 为什么是张正友标定法1.1 相机标定到底在标什么相机成像可以简单理解成把三维世界投影到二维像素平面但这个投影不是理想的小孔成像镜头本身会引入畸变CMOS安装也会有轻微偏差。标定的核心就是求出一组参数用来描述“空间点三维坐标 - 像素坐标”的映射关系。这组参数分两部分内参焦距、主点、畸变系数描述相机自身属性外参旋转矩阵、平移向量描述相机在世界坐标系下的位姿。很多人觉得现在工厂出厂的相机都有标定数据没必要自己标但实际使用中镜头装上之后法兰距、镜头个体差异、温度变化都会让出厂参数失效。尤其是用了工业相机搭配不同镜头或者手机摄像头模组标定几乎是必做的第一步。张正友标定法之所以流行是因为它只需要一块平面棋盘格不需要昂贵的标定设备精度却足够大多数场景使用。1.2 张正友标定法的核心思路张正友博士在1998年提出的这个方法巧妙在把三维标定问题简化成了二维问题。标定板是平面所以棋盘格上所有角点的世界坐标z分量可以设为0这样原本需要求解的单应性矩阵H就变成8个自由度通过多张不同角度的照片可以线性求解出内参和外参的初值再做非线性优化得到精确解。这种方法降低了对标定设备的要求你不需要知道相机与标定板之间的精确距离也不需要像传统两步法那样做复杂的三维靶标。你只需要不断变换棋盘格的姿态、角度、位置拍下十几张照片OpenCV就能算出结果。原理上它是介于传统标定和自标定之间的一种折中方案精度比自标定高操作又比传统标定简单所以成了事实上的工业标准。1.3 棋盘格为什么是首选棋盘格的高对比度黑白方块让角点检测非常稳定OpenCV的findChessboardCorners在多数光照条件下都能找到角点。而且棋盘格角点是两个方块的交叉点亚像素级定位相对容易配合cornerSubPix可以进一步把角点精度做到0.1像素以内。相比圆点标定板棋盘格的角点有明确的“顺序性”OpenCV能自动按行列排序输出所以代码实现起来很简单。另外棋盘格对打印设备的精度要求不高普通打印机加上硬质底板就能用这点对个人开发者非常友好。你要是做高精度的工业视觉可以买玻璃或陶瓷基板的标定板但学习和验证阶段自己打印完全够用。2. 标定前的准备棋盘格与测试图片2.1 自己生成棋盘格打印PDF张正友标定用的棋盘格严格来说要定义两个关键参数内角点数intersection points count和方格边长。内角点数指的是黑白格子交叉点的数量不是格子数量。例如一个10x7的格子阵列内角点是9x6OpenCV中通常写作(9, 6)。采集图片时程序需要检测这些内角点所以这个数必须和实际标定板完全一致否则检测不到。我用Python生成一张可以直接打印的棋盘格图片核心代码如下import cv2 import numpy as np # 内角点数 (width, height) board_size (9, 6) # 每个方格的边长单位mm对应打印后实际尺寸 square_size_mm 25 # 每个方格在图片中的像素大小决定图片分辨率 pixels_per_square 120 margin 60 # 四周留白方便裁剪 cols board_size[0] 1 rows board_size[1] 1 img_w cols * pixels_per_square margin * 2 img_h rows * pixels_per_square margin * 2 chessboard np.ones((img_h, img_w), dtypenp.uint8) * 255 for i in range(rows): for j in range(cols): if (i j) % 2 0: x0 margin j * pixels_per_square y0 margin i * pixels_per_square chessboard[y0:y0 pixels_per_square, x0:x0 pixels_per_square] 0 cv2.imwrite(chessboard.png, chessboard)生成图片后打印时务必关掉“适应页面”或“缩放以适合”选项按100%比例打印然后量一下实际格子的边长把square_size_mm改成实测值。因为纸张在打印过程中会有伸缩如果你需要高精度标定这一步不能省。打印好后把纸贴在平整的硬板上不能有褶皱否则角点坐标本身就带了误差后面标定精度会大打折扣。2.2 采集测试图片的要点有了标定板接下来就是采集图片。这是整个标定流程里最容易被忽视、却最影响效果的环节。我一般建议拍15到25张图太少的话内参解算不稳定太多也没有必要。拍摄时要遵循几个原则标定板要占画面面积的1/4到1/3以上太小了角点检测会不稳定。每一张照片里标定板的角度、距离和位置尽量不同要有左右倾斜、上下倾斜、远近变化、平移变化。避免所有图片都让标定板正对着相机否则外参退化内参也解不准。保证光照均匀不要有强反光不要有阴影压住角点。手持标定板就行相机固定人移动标定板即可也可以反过来相机移动而标定板固定。如果你用的是USB摄像头或手机相机记得关闭自动对焦、自动曝光和自动白平衡固定变焦和曝光参数后再拍摄。对焦模糊的图片直接删除不要勉强用因为亚像素角点检测依赖边缘锐度模糊图会让角点定位产生偏差。我自己的经验是把视频流打开一边观察角点检测效果一边采集会比盲拍一堆再筛选高效得多。3. OpenCV标定代码实现与参数解析3.1 角点检测与亚像素精细化OpenCV标定的标准流程分三步检测每张图的角点、亚像素精细化、传入calibrateCamera解算参数。第一步是基础我通常这样写import cv2 import numpy as np import glob board_size (9, 6) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp np.zeros((board_size[0] * board_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:board_size[0], 0:board_size[1]].T.reshape(-1, 2)objp存放的是棋盘格角点的三维坐标。这里有一个常见的理解误区为什么这些坐标不是实际的毫米数值因为张正友标定法里objp的尺度是可以任意缩放的你写单位长度1、2、3和写25、50、75最终得到的内参矩阵焦距会有对应倍数变化但畸变系数和归一化坐标结果不变。所以更标准的做法是直接用实际物理尺寸也就是乘以square_size_mm这样得到的焦距单位就是像素没什么问题。接下来读取图片并找角点image_files glob.glob(calib_images/*.jpg) objpoints [] imgpoints [] for fname in image_files: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, board_size, None) if ret: corners_sub cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners_sub) cv2.drawChessboardCorners(img, board_size, corners_sub, ret) cv2.imshow(check, img) cv2.waitKey(100) else: print(f角点检测失败: {fname}) cv2.destroyAllWindows()findChessboardCorners返回的corners已经按行排列顺序是从左上角开始横向扫描这依赖于OpenCV内部的四边形拓扑分析。你可以打印一下corners的shape通常是(54, 1, 2)对应54个角点每个点两个坐标。角点检测失败时不要跳过就完事最好看一下原因见第5节。3.2 调用calibrateCamera完成标定角点全部检测完成后用calibrateCamera一次性解算所有参数ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print(内参矩阵:) print(mtx) print(畸变系数:) print(dist) print(重投影误差:, ret)这里必须解释一下每个返回值的含义。mtx是3x3内参矩阵形如[[fx, 0, cx], [ 0, fy, cy], [ 0, 0, 1]]fx和fy是等效焦距单位是像素理想情况下两者接近cx和cy是主点坐标理想情况下在图像中心附近但实际会有偏移。dist是畸变系数OpenCV默认包含5项(k1, k2, p1, p2, k3)前两个是径向畸变中间两个是切向畸变第五个是薄棱镜畸变的高阶项。rvecs和tvecs是每张图片对应的旋转向量和平移向量也就是外参每张图对应一组。特别注意ret返回值它是所有角点在归一化相机坐标系下的重投影均方根误差单位是像素。这个值越小越好一般低于0.5就算不错低于0.1是相当好的结果。如果大于1说明标定数据里有脏数据需要排查。3.3 为什么要用亚像素精细化可能有人会问直接用findChessboardCorners检测到的角点不行吗实际上初始检测的角点精度受图像分辨率、模糊程度影响只能到像素级而张正友标定法对标定板角点的坐标精度非常敏感微小偏差会被放大到内参上。cornerSubPix通过迭代求取灰度梯度的交点能把角点定位精度提高到亚像素级别。这个函数需要几个参数搜索窗口大小(11, 11)表示在每个角点周围11x11像素范围内微调(-1, -1)表示没有零迭代区criteria是迭代停止条件一般用最大迭代次数30次和最小误差0.001。窗口大小不是越大越好对于高分辨率图像可以用(15, 15)但窗口太大反而容易受到邻近角点影响。如果你用的是灰度图cornerSubPix直接处理即可。如果彩色图有严重的色差可以转成灰度再操作OpenCV内部计算也是基于灰度梯度的。还有一个小技巧对图片先做一次cv2.cvtColor后如果检测效果不好可以尝试用cv2.adaptiveThreshold对图像做预处理增强棋盘格边缘但绝大多数场景下不需要。4. 实操结果分析怎么判断标定质量4.1 读懂内参矩阵和畸变系数标定完成后第一件事不是记录参数而是验证参数是否合理。内参矩阵里的fx和fy应该很接近因为它们分别代表x、y方向归一化焦距虽然镜头可能略有各向异性但差值一般不会超过几个百分点。如果你用的是普通工业镜头fx和fy相差10%以上多半是标定板图片姿态覆盖不够或者代码里objp的坐标方向定义错了。主点cx、cy一般在图像中心附近比如1280x960的图像主点大约在(640, 480)附近偏差几十上百个像素是正常的。如果主点跑到图像边缘甚至外面那就说明标定失败需要重新检查数据源。畸变系数k1、k2一般很小在-0.1到0.1之间都算合理如果出现几十几百的离谱值基本是求解迭代出问题了。更直观的验证方法是把标定结果用于图像去畸变对比一下效果h, w img.shape[:2] new_camera_mtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) undistorted cv2.undistort(img, mtx, dist, None, new_camera_mtx)这里可以观察画面边缘的直线是否被拉直。棋盘格的边、背景里的门框、墙线都是很好的参考。如果去畸变后边缘的弯曲消失说明标定结果基本可信。4.2 重投影误差与图像数量的关系重投影误差是标定结果最直接的量化指标。它的计算思路是用求解出的内参、外参和畸变系数把每张标定板上的三维角点投影到像素平面计算投影点与实际检测角点之间的距离。误差小于0.5像素说明内参和外参之间的一致性很好但要注意重投影误差小不代表你的相机内参一定准确因为它只衡量“自洽性”。比如所有标定板都放在画面中央重投影误差可能很小但内参外推能力很差换一个角度就不准了。所以标定过程中图片的数量和角度覆盖比单张图片的角点精度更重要。15到25张图已经是工程上的经验值少于10张容易过拟合超过30张如果没有新的角度信息改善也有限。如果你发现重投影误差偏大先检查是不是有某张图角点检测异常把那张图剔除后重新标定往往误差就能降下来。5. 常见问题与排查技巧5.1 角点检测失败的原因findChessboardCorners返回False是新手最常见的卡点。我总结了几种高频原因棋盘格内角点数定义错误。比如实际棋盘格是10x7格子内角点数是9x6你却写了(10, 7)那永远检测不到。标定板在画面中太小角点间距只有几个像素OpenCV的四边形检测无法稳定识别。解决办法是让标定板靠近镜头或者提高拍摄分辨率。光照不均匀或者反光导致某些角点对比度太低。可以尝试调整光源方向或者用cv2.adaptiveThreshold预处理。图片模糊或者运动拖影角点边缘不锐利。这种情况只能重新采集。标定板没有完整出现在画面里边缘的角点被裁掉。务必保证所有内角点都在画面内且有少量边距。排查时可以先对单张图做可视化把检测到的角点画出来确定是检测算法的问题还是图像本身的问题。5.2 标定结果漂移、误差大怎么处理如果重投影误差大于1像素或者内参明显不合理我建议按顺序做三件事第一检查所有图片中棋盘格姿态是否足够丰富。光有不同远近、没有旋转倾斜的图片外参估计会病态内参也不可靠。最好保证棋盘格在每张图里都有明显的朝向变化比如绕x轴、y轴各转15到30度。第二剔除角点检测质量差的图片。可以在findChessboardCorners之后加一个cv2.cornerSubPix前后的角点坐标差值统计差异超过0.5像素的图直接标记出来。第三尝试降低畸变模型阶数。OpenCV默认用5个畸变系数但普通镜头用3个或2个往往更稳定。代码里可以传calibrateCamera(..., flagscv2.CALIB_ZERO_TANGENT_DIST)只估计径向畸变或者用CALIB_FIX_K3固定k3为0减少参数过多带来的波动。5.3 双目、红外等扩展场景的注意点很多人标定完单目之后想继续做双目或者用红外相机。双目标定不是简单的两个单目拼起来而是在单目内参基础上再做立体标定用cv2.stereoCalibrate求出两个相机之间的旋转和平移。张正友标定法同样适用但需要左右相机同时采集同一块棋盘格所以采集时要保证标定板在左右画面中同时可见且尽量覆盖公共视野。如果你用红外相机注意红外光的波长会让镜头畸变跟可见光不同必须单独标定不能直接复用可见光的参数。至于热搜词里提到的Kalibr标定那是VIO/SLAM领域更常用的标定工具它支持标定相机内参、相机与IMU之间的外参。原理上和张正友标定法同源但对时间戳同步、IMU残差处理有额外要求。如果你在做SLAM可以先按本文把单目内参和畸变系数标准再用Kalibr完成协同标定两者并不冲突。6. 最后再分享一点实操体会我在实际项目里标过几十台相机最大的感受是张正友标定法本身已经很成熟出问题的地方基本都在数据采集端。与其反复调代码参数不如多花时间把标定板贴平、把图片拍好、把角度覆盖到位。自己打印棋盘格时千万不要用普通的喷墨纸直接平铺在桌上稍微翘起一角角点在空间上就不再共面最终标定的主点会明显偏移。我一般会把打印好的格子纸用双面胶贴在亚克力板或者玻璃板上边缘压平再量一下实际尺寸填入代码。另外标定完别急着删除测试图片我习惯把每张图的角点检测结果和重投影误差都保存下来作为一份“数据报告”。如果后续发现某个镜头换过或者焦距动过还能快速定位是哪一批图出了问题。最后再说一个小技巧标定代码里把objp的坐标写成实际毫米值不仅方便和机械臂、移液平台等对量的场景对接也便于团队成员之间交叉验证结果。希望这份流程能帮你少踩几个坑一次跑通标定。本文还有配套的精品资源点击获取
返回列表