ARTICLE DETAIL

资讯详情

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

C++ 棋盘格角点检测与相机标定:从 OpenCV 到亚像素精度

C++ 棋盘格角点检测与相机标定:从 OpenCV 到亚像素精度 简介面向使用OpenCV进行相机标定的C开发者这份资源提供一套完整的棋盘格自动检测程序将常见Matlab相机标定算法改写为C实现可自动查找图像中最大棋盘格并识别所有独立棋盘格配合OpenCV即可完成相机内参标定。资源压缩包约30MB共1727个文件核心源码包括618个cpp、427个h、130个hpp等源文件另有bmp棋盘格测试图、txt/cmake构建与说明文件以及Eigen矩阵库等第三方依赖支持工程结构较为完整。资源内还带有多个示例图像和OpenCV相关配置可帮助读者理解棋盘格检测、角点提取与内参求解的完整流程。目前已有117人学习下载。通过源码和示例可以掌握算法移植思路、自动化检测多棋盘格的实现细节并快速嵌入自己的视觉标定项目中。1. C 棋盘格自动检测程序从 MATLAB 标定流程拆出的工程问题拿到这个标题我想到的是一句常说的话MATLAB 里detectCheckerboardPoints一行调出几十个角点但换成 C 落地事情远没有看起来那么简单。所谓 C 棋盘格自动检测程序本质是把 MATLAB Camera Calibrator 里「棋盘格角点提取 内参估计」这一段流程用 OpenCV 的findChessboardCorners、cornerSubPix、calibrateCamera重写一遍最终输出相机内参矩阵、畸变系数和重投影误差。它解决的是产线视觉、双目深度估计、AGV 定位里「要脱离 MATLAB 运行时、想嵌进实时采集线程」的场景适合已经用 MATLAB 标定过相机、现在要写 C 服务或嵌入式程序的工程师。整个流程里角点检测只算四成工作量剩下的坑集中在亚像素细化、角点排序和物理坐标映射。2. 棋盘格角点候选提取findChessboardCorners 的预处理与参数选择2.1 检测原理从二值化到棋盘拓扑OpenCV 的findChessboardCorners并不是直接在灰度图上找角点而是一条多阶段几何管线。它先对图像做自适应二值化和归一化再用形态学闭运算连接棋盘格内的黑色方块之后用轮廓检测把每个四边形筛出来最后根据四边形之间的邻接关系构建整个棋盘拓扑。这一步几乎就是 MATLABdetectCheckerboardPoints的粗检测阶段只不过 MATLAB 在光照不均和低对比度图像上做了更强的内部补偿所以显得更「智能」。findChessboardCorners的行为由 flags 参数直接控制四个常用项作用完全不同flags 取值作用适用场景CALIB_CB_ADAPTIVE_THRESH用自适应阈值替代全局阈值处理光照渐变产线环境推荐默认开启CALIB_CB_NORMALIZE_IMAGE检测前对图像做直方图归一化过暗或过亮的标定板图像CALIB_CB_FILTER_QUADS用面积、边长等条件过滤掉不合格四边形背景杂乱的实拍图能显著降低误检CALIB_CB_FAST_CHECK先快速判断图像里大概是否存在棋盘格视频流预检节省算力2.2 提高检出率的一段 C 预处理代码我一般在检测前加一层 CLAHE 局部对比度增强再用多尺度兜底这样能覆盖大多数实拍场景里光照不均匀的问题。cv::Mat gray; cv::cvtColor(colorImage, gray, cv::COLOR_BGR2GRAY); cv::Ptrcv::CLAHE clahe cv::createCLAHE(2.0, cv::Size(16, 16)); cv::Mat enhanced; clahe-apply(gray, enhanced); cv::Size board(9, 6); // 内角点数量不是棋盘格数量 std::vectorcv::Point2f corners; int flags cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE | cv::CALIB_CB_FILTER_QUADS; bool ok cv::findChessboardCorners(enhanced, board, corners, flags); if (!ok) { // 降分辨率再检测一次再把坐标映射回原图 cv::Mat small; cv::resize(enhanced, small, cv::Size(), 0.5, 0.5, cv::INTER_LINEAR); std::vectorcv::Point2f smallCorners; if (cv::findChessboardCorners(small, board, smallCorners, flags)) { for (auto pt : smallCorners) { pt.x * 2.0f; pt.y * 2.0f; } corners std::move(smallCorners); ok true; } }这段代码的关键在于board(9, 6)是内角点数也就是说物理棋盘是 10x7 个方格这是初学者最容易写错的第一处。CLAHE的2.0是对比度限制值越大增强越强但过大会把棋盘格边缘拉出伪轮廓实拍里一般取 1.5 到 3.0 之间。降分辨率检测是因为大分辨率图像里四边形轮廓容易断裂缩小一倍后棋盘拓扑更容易闭合代价是角点定位精度的确会略差所以这一步只作为兜底不能替代正常尺寸下的检测。2.3 检测失败的降级策略如果上述流程在某个图像上还是失败我一般会检查三件事。第一棋盘格是否占满画面三分之一以上太小的话四边形互相粘连形态学闭运算会把格子连成一大片。第二图像是否过曝白色方块高光溢出后角点周围梯度信息丢失CALIB_CB_NORMALIZE_IMAGE只能做线性拉伸无法恢复过曝区域。第三把 flags 里的CALIB_CB_FILTER_QUADS暂时去掉再跑一次确认是不是四边形过滤规则误杀了真实棋盘块。提示在 VSCode 配好 C/C 环境后建议把这段检测逻辑单独编译成一个命令行工具输入一张图片、输出检测结果图调试效率比直接跑完整标定程序高得多。3. 亚像素角点细化cornerSubPix 的误差模型与 4 个必调参数3.1 为什么要做亚像素细化findChessboardCorners返回的角点坐标是像素整数精度对相机标定这种需要重投影误差低于 0.1 像素的场景来说远远不够。棋盘格角点在局部图像里呈现为两个方向的灰度梯度交界理想角点位置周围的梯度方向都垂直于「像素点到角点」的连线据此可以构造一个最小二乘问题对候选点邻域内的每个像素计算梯度方向让所有像素的梯度方向与连线方向的点积之和最小解出的偏移量叠加到原坐标上就是亚像素坐标。cornerSubPix优化的正是这个误差函数它迭代地在邻域内收集梯度信息求解一个 2x2 线性方程组得到偏移量。MATLAB 的detectCheckerboardPoints在亚像素基础上还会对整幅棋盘的所有角点做一次全局优化让角点位置同时满足棋盘网格的几何约束这是 C 里cornerSubPix单点局部优化不具备的。所以 C 端要多做一步用理想网格投影来校验每个角点剔除偏离过大的点。3.2 4 个必调参数及建议值cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 30, 0.001));四个关键参数如下参数含义建议值winSize搜索窗口半宽实际窗口是 (2n1)x(2n1)Size(11, 11)对应 23x23 窗口zeroZone中心零区半宽设为 0 表示不设零区Size(-1, -1)表示无零区MAX_ITER迭代次数上限30 左右足够收敛EPS亚像素偏移量的终止精度0.001 像素再小意义不大winSize是效果最敏感的参数。窗口太小梯度信息不足角点会被噪声拉偏窗口太大会混入旁边棋盘格边缘的梯度角点位置会被相邻方块边缘干扰。对于 9x6 内角点、单个方格 25mm 的常见标定板如果棋盘在画面里占 1/3 以上Size(11, 11)一般够用如果棋盘在画面里很小窗口要跟着缩小到Size(7, 7)否则窗口跨过了好几个棋盘格周期。3.3 剔除不合格角点对应双目标定里的质量门禁cornerSubPix之后我还会加一道基于单应性残差的校验这一步对应原 MATLAB 流程中没有显式暴露、但内部在估计外参时一定会做的角点质量筛选。std::vectorcv::Point2f refined; for (size_t i 0; i corners.size(); i) { cv::Point2f ideal projectToIdealGrid(corners[i], gridStep); if (cv::norm(corners[i] - ideal) 1.5) { refined.push_back(corners[i]); } }projectToIdealGrid的做法是取检测到的所有角点拟合一个棋盘平面单应把理想棋盘网格点投到图像上再计算每个检测点与投影点的距离。残差大于 1.5 像素的点直接剔除。双目相机标定里经常提到「剔除不合格角点」说的就是这个环节——左右目各自检测后用对极几何或单应残差把误匹配点筛掉否则双目标定的立体外参会整体漂移。阈值 1.5 像素不要放宽放宽后内参标定的重投影误差会被「包含错误点」的平均掩盖掉。4. 棋盘格角点排序与物理坐标映射标定前最容易被忽略的一步4.1 排序错误导致标定发散的原因calibrateCamera的输入是成对的 imagePoints 和 objectPoints它要求第 i 个图像点必须对应第 i 个世界坐标点。棋盘格是高度对称的结构如果角点顺序错乱相当于世界坐标系下 (0,0) 挪到了图像里的另一个位置单应性约束被彻底打乱内参矩阵初值就会发散。MATLAB 的detectCheckerboardPoints对输出顺序有固定的约定规则但 OpenCV 不同版本在某些边界情况下返回的角点排序并不一致尤其当棋盘旋转、倒置时。一位真正做过标定落地的工程师不会依赖findChessboardCorners的隐含顺序而是自己按棋盘拓扑重建行列索引。物理世界里棋盘是一个 9x6 的网格每个角点都对应唯一的 (row, col)把图像坐标映射到这个网格上再按约定顺序输出。4.2 用棋盘空间拓扑重建索引我的做法是先找到左上角的锚点再根据两个边缘方向把每个角点投影到网格坐标。// corners 是 findChessboardCorners 的原始输出 int rows 6, cols 9; cv::Point2f origin corners[0]; for (auto p : corners) { if (p.x p.y origin.x origin.y) origin p; } std::vectorcv::Point2f reordered(rows * cols); cv::Point2f axisX corners[1] - origin; // 最近邻之一 double lenX cv::norm(axisX); axisX / lenX; // 找与 origin 距离次小的点作为 Y 方向 double minDist 1e9; cv::Point2f axisY; for (auto p : corners) { double d cv::norm(p - origin); if (d lenX * 0.5 d minDist) { minDist d; axisY p - origin; } } axisY / minDist; for (auto p : corners) { cv::Point2f v p - origin; int col cvRound(v.dot(axisX) / lenX); int row cvRound(v.dot(axisY) / minDist); if (row 0 row rows col 0 col cols) reordered[row * cols col] p; }这段代码用到两个假设第一个origin取图像坐标系里 xy 最小的点在棋盘无大倾斜时约等于左上角第二个X 方向取离origin最近的点Y 方向取次近点。dot投影到两个轴后用距离归一化得到行列号。注意这里lenX是像素距离如果棋盘在画面里有透视畸变边缘处的点会偏离理想网格点但因为cvRound只需要落在正确网格范围内透视造成的偏差通常不足以跳格。4.3 物理世界坐标的生成规则世界坐标采用「X 沿棋盘行方向Y 沿列方向Z 垂直板面」的右手系原点放在第一个内角点上单位是毫米。std::vectorcv::Point3f generateBoardPoints(int rows, int cols, float squareSize) { std::vectorcv::Point3f pts; for (int r 0; r rows; r) for (int c 0; c cols; c) pts.emplace_back(c * squareSize, r * squareSize, 0.0f); return pts; }squareSize必须与实际标定板的格子边长一致。这里有一个非常实际的工程陷阱如果用 25mm 的板子但代码里写 25.0f得到的内参 fx、fy 单位是像素这是正确的如果用英寸但写成 25.0f内参数值不会变但所有平移向量 t 的单位会错。棋盘格标定得到的焦距是以像素为单位的这个值本身不随板子尺寸变化但外参平移量会。5. 相机标定算法实现从单应约束到 calibrateCamera 的 MATLAB 等价映射5.1 张正友标定法在 C 里的数学骨架MATLABestimateCameraParameters底层是张正友标定法C 端calibrateCamera同样如此。核心思路是棋盘平面上的世界点 M经单应矩阵 H 映射到图像点 m每一张棋盘图都给出一组约束。设H [h1 h2 h3]旋转矩阵列向量r1, r2与平移向量t满足[h1 h2 h3] λK[r1 r2 t]。旋转矩阵列向量单位正交因此有r1ᵀr2 0且|r1| |r2|代入后得到两个关于内参矩阵 K 的二次约束h1ᵀ K⁻ᵀ K⁻¹ h2 0h1ᵀ K⁻ᵀ K⁻¹ h1 h2ᵀ K⁻ᵀ K⁻¹ h2每张图提供两个方程B K⁻ᵀK⁻¹是对称矩阵有 6 个未知量所以理论上 3 张不同姿态的棋盘图就能解出内参闭式解。之后再用最大似然估计对全部内参、外参和畸变系数做非线性优化对应 MATLAB 工具箱里的Optimize选项。整个过程在 C 里不需要自己实现calibrateCamera内部完成闭式解初始化加 LM 迭代。5.2 从检测到内参输出的完整 C 最小实现std::vectorstd::vectorcv::Point3f objectPoints; std::vectorstd::vectorcv::Point2f imagePoints; for (auto imgPath : imagePaths) { cv::Mat img cv::imread(imgPath); std::vectorcv::Point2f corners; if (!findBoardCorners(img, corners)) // 封装第2、3章的检测流程 continue; cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 30, 0.001)); objectPoints.push_back(generateBoardPoints(6, 9, 25.0f)); imagePoints.push_back(corners); } cv::Mat K, distCoeffs; std::vectorcv::Mat rvecs, tvecs; double rms cv::calibrateCamera(objectPoints, imagePoints, imgSize, K, distCoeffs, rvecs, tvecs, cv::CALIB_RATIONAL_MODEL, cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 60, 1e-10));CALIB_RATIONAL_MODEL会把畸变模型从默认的 5 参数扩到 8 参数对应 k1, k2, p1, p2, k3, k4, k5, k6与 MATLAB 里选择 3 阶径向畸变模型更接近。但如果标定图数量不足高次畸变项会过拟合我一般先跑默认模型重投影误差大于 0.15 像素再打开CALIB_RATIONAL_MODEL。标定图数量建议不少于 15 张且覆盖画面中心、四周、倾斜和旋转姿态这比单纯追求图片数量更重要。5.3 与 MATLAB 工具箱的参数对应关系MATLAB 功能C/OpenCV 对应说明detectCheckerboardPointsfindChessboardCornerscornerSubPix粗检测加亚像素细化estimateCameraParameterscalibrateCamera闭式解初始化加 LM 优化NumRadialDistCoeffsCALIB_RATIONAL_MODEL及 k1-k6径向畸变阶数对应EstimateTangentialDistortion默认 p1, p2切向畸变默认打开Optimize选项CALIB_FIX_*系列 flags固定主点或固定某阶畸变CALIB_FIX_K3、CALIB_FIX_PRINCIPAL_POINT等 flags 对应 MATLAB 里固定某几个参数不参与优化的选项。需要特别区分的是市面上常说的「相机 9 点标定算法」指的是像素坐标到机械坐标的手眼标定输入是点阵标定板上的圆心或角点输出是变换矩阵和这里棋盘格内参标定是两套算法不要在参数设计上混为一谈。6. 用重投影误差和子集验证收紧标定结果兼谈多尺度检测兜底6.1 三个验证维度拿到 K 和畸变系数后第一步看rms重投影误差小于 0.15 像素是基本及格线。但这个值只说明平均残差小无法暴露某个角点的系统偏差所以我会再做两步验证。第一把 20 张标定图随机分成 A、B 两组分别跑calibrateCamera比较两组得到的 fx 和 fy 差多少偏差超过 0.5% 说明姿态覆盖不够需要补图。第二用标定结果去矫正一张未参与标定的棋盘图查看棋盘边缘直线是否拉直这一步能直观发现畸变模型阶数是否够用。另外双目标定场景会复用同一套棋盘格角点生成双目立体标定所需的左右目对应点对这就是「双目相机标定剔除不合格角点」的价值所在——先剔除再标定立体外参才不会把残差平均到错误匹配上。6.2 多尺度检测兜底实际采集标定图时总有几张因为运动模糊或极端光照导致检测失败这种图直接跳过可能让整组姿态覆盖出现空洞。兜底策略是把第 2 章里的降分辨率检测抽成通用 wrappercv::Mat scales[] {gray, blurred, halfRes}; for (auto s : scales) { if (cv::findChessboardCorners(s, board, corners, flags)) { if (s.cols ! gray.cols) { for (auto pt : corners) { pt.x * (float)gray.cols / s.cols; pt.y * (float)gray.rows / s.rows; } } break; } }三层尺度里blurred用轻度高斯模糊去掉传感器噪声导致的伪四边形halfRes处理高分辨率图像里轮廓断裂的问题。注意放大回原图坐标时如果长宽缩放比不一致要分别用对应轴的比值。把这层多尺度检测和 CLAHE 一起塞进采集线程的预检里误检率通常还能再降半成。本文还有配套的精品资源点击获取
返回列表