ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenCV相机标定实战:从原理到C++代码实现与手眼标定

OpenCV相机标定实战:从原理到C++代码实现与手眼标定 1. 项目概述为什么相机标定是机器视觉的基石如果你正在用C和OpenCV捣鼓机器人、自动驾驶或者三维重建那么“相机标定”这个词你肯定绕不过去。它听起来有点学术但说白了就是给你的相机做一次“体检”和“定位”。想象一下你新买了一把尺子但它的刻度不准或者你闭上一只眼睛看东西距离感会错乱。相机也一样由于镜头制造、组装过程中的物理限制它拍出来的图像天生就带有畸变同时相机在三维空间里的位置和朝向外参如果不清楚你就无法把图像中的像素点和真实世界的位置对应起来。这就是相机标定要解决的两个核心问题内参标定和外参标定。内参就像是相机的“身份证”描述了它的内部光学特性比如焦距、主点坐标、畸变系数。外参则是相机在某个特定时刻的“工作证”告诉我们相机坐标系相对于世界坐标系的位置和旋转。只有拿到了这两张“证”你的视觉算法才能从“看图说话”升级到“精准测量”。我见过太多项目算法写得天花乱坠但因为标定没做好精度始终上不去最后还得回头补课。所以无论你是做机械臂的“手眼标定”还是做双目立体视觉亦或是简单的单目测量扎实的标定都是第一步也是最关键的一步。2. 核心原理拆解从针孔模型到张正友标定法2.1 相机成像的数学模型针孔模型是起点所有标定的理论基础都源于一个简单的模型针孔相机模型。你可以把它想象成一个没有镜头的暗箱光线通过一个小孔在箱内壁成像。这个模型用数学描述就是一个从三维世界点[X, Y, Z]到二维图像像素点[u, v]的投影变换。这个变换过程可以分解为几步世界坐标系到相机坐标系通过旋转矩阵R和平移向量t即外参完成。这回答了“相机摆在哪里朝哪个方向看”的问题。相机坐标系到图像物理坐标系利用相似三角形原理进行透视投影核心参数是焦距f。图像物理坐标系到像素坐标系涉及像素尺寸dx, dy和图像中心主点[cx, cy]的偏移。焦距f与像素尺寸结合就得到了我们常说的内参矩阵中的fx和fy。最终内参矩阵K通常是一个3x3的矩阵K [ fx, 0, cx; 0, fy, cy; 0, 0, 1 ]其中fx f / dx,fy f / dy。fx和fy可能不相等这对应于像素不是完美的正方形。注意这里有一个初学者极易混淆的点。很多教程和函数接口里提到的“焦距”往往指的是fx和fy其单位是像素而不是物理意义上的毫米。这是因为在数字图像中我们最终关心的是像素位置。如果你的相机传感器尺寸和分辨率已知可以进行换算但在大多数纯视觉算法中直接使用像素焦距即可。2.2 透镜带来的现实问题畸变针孔模型是理想的但真实的相机需要透镜来汇聚更多光线透镜就会引入畸变。主要分为两类径向畸变由透镜形状导致图像边缘的直线会变弯。分为桶形畸变图像边缘向内凹和枕形畸变图像边缘向外凸。通常用k1, k2, k3, ...等参数来描述。切向畸变由透镜组装时与成像平面不平行导致。用p1, p2参数描述。标定的一个重要任务就是估计出k1, k2, p1, p2有时还有k3这些畸变系数从而在后续处理中校正图像让它们重新符合针孔模型。2.3 张正友标定法为何成为工业标准在张正友教授于1998年提出他的方法之前相机标定需要高精度的三维标定场成本高昂且操作复杂。张氏标定法的革命性在于它只需要你打印一张黑白棋盘格或圆点网格图案从不同角度拍摄几张照片就能高精度地求解出所有内参、畸变系数以及每次拍摄的外参。其核心思想可以概括为单应性矩阵估计对于每一张标定板图片棋盘格角点所在的平面世界坐标Z0与图像像素平面之间存在一个单应性变换矩阵H。通过检测到的角点像素坐标和已知的棋盘格物理坐标可以求解出这个H。内参约束求解单应性矩阵H与内参矩阵K和外参R, t有关。利用旋转矩阵R的正交性其列向量两两点积为0且自身模长为1可以推导出关于K中元素的约束方程。通过多张图片多个H构建方程组就能解出内参矩阵K。非线性优化以上步骤得到的初值可能不够精确。张氏标定法最后通过最大似然估计构建重投影误差最小化的目标函数对所有参数内参、畸变、所有外参进行联合非线性优化通常使用Levenberg-Marquardt算法从而得到最优解。重投影误差是评估标定质量的金标准。它是指将标定板已知的3D角点用当前估计的相机参数投影回图像得到2D点然后计算这些投影点与实际检测到的角点像素坐标之间的平均距离单位像素。这个误差值越小标定精度通常越高。OpenCV的calibrateCamera函数最终返回的正是这个误差。3. 实战准备环境、工具与标定板3.1 开发环境搭建C、OpenCV与VSCode工欲善其事必先利其器。一个顺手的开发环境能避免很多不必要的麻烦。C编译器在Windows上推荐使用MSVCVisual Studio自带或MinGW-w64。如果你遇到error: microsoft visual c 14.0 or greater is required这类错误通常是因为编译某些Python包或依赖时缺少VC运行时库对于纯C的OpenCV项目确保你的IDE或CMake正确配置了编译器路径即可。Linux/macOS下GCC或Clang是标准选择。OpenCV安装Windows最省事的方法是下载官网预编译好的.exe安装包如opencv-4.9.0-windows.exe运行后它会解压到一个目录。你需要在系统环境变量Path中添加该目录下的\build\x64\vc15\bin具体路径根据你的Visual Studio版本而定。然后在你的C项目中在IDE或CMake中配置好包含目录和库目录。Linux (Ubuntu)使用包管理器非常方便sudo apt install libopencv-dev。如果需要特定版本或自定义模块从源码编译是更好的选择。编译时记得cmake加上-D BUILD_opencv_worldON可以生成单个整合的库文件方便链接。验证安装写一个简单的程序包含#include opencv2/opencv.hpp并尝试读取显示一张图片能成功运行即说明环境基本OK。IDE选择VSCode凭借其轻量和强大的插件生态C/C、CMake Tools成为很多人的首选。关键在于配置好c_cpp_properties.json用于智能提示、tasks.json用于构建和launch.json用于调试。对于初学者使用Visual Studio或CLion这类全功能IDE在项目配置上会更直观一些。实操心得我强烈建议使用CMake来管理你的C OpenCV项目而不是直接在IDE里配置一堆路径。写一个简单的CMakeLists.txt利用find_package(OpenCV REQUIRED)让CMake自动寻找OpenCV再通过target_link_libraries(your_target ${OpenCV_LIBS})链接。这样你的项目在任何配置好OpenCV的机器上都能一键编译可移植性极佳。这也是现代C项目的标准做法。3.2 标定板的选择与制作标定板是标定算法的“尺子”其精度直接决定标定结果的上限。棋盘格 vs 圆点网格棋盘格OpenCV内置支持最好角点检测函数findChessboardCorners非常鲁棒。角点是“亚像素”级别的精度高。这是最推荐新手使用的类型。圆点网格对称/非对称适用于某些镜头畸变极大、角点检测困难的场景。OpenCV提供findCirclesGrid来检测。非对称圆点网格可以避免方向歧义。制作要点高对比度确保黑白分明打印时不要有反光。哑光材质最好。平整贴在平整的硬质板材上如亚克力板、铝板拍摄时尽量保证板子不变形。尺寸精确你需要精确测量棋盘格一个方格的物理边长例如25.0毫米。这个值是后续所有计算的世界坐标基准其测量误差会直接带入标定结果。用游标卡尺多次测量取平均。大小合适标定板在图像中应占据足够大的面积建议在1/4到1/2图像宽度但又要确保在不同角度下都能被完整拍摄到。一个关键技巧在打印店打印后最好用高精度扫描仪扫描一下然后在图像软件中测量像素尺寸再结合打印的DPI例如300 DPI反推物理尺寸这有时比直接用尺子量更准因为它消除了打印可能存在的均匀缩放误差。4. 手把手代码实现单目相机标定全流程接下来我们用一个完整的C程序来串起整个标定流程。我会详细解释每一步的意图和关键参数。4.1 第一步采集标定图片这是最重要的数据准备阶段图片质量决定标定成败。#include opencv2/opencv.hpp #include iostream #include vector #include filesystem // C17用于遍历目录 namespace fs std::filesystem; int main() { cv::VideoCapture cap(0); // 打开默认相机如果是视频文件改为文件路径 if (!cap.isOpened()) { std::cerr 无法打开相机 std::endl; return -1; } cv::Size boardSize(9, 6); // 棋盘格内角点数量宽度方向9个高度方向6个 std::vectorstd::vectorcv::Point2f imagePoints; // 保存所有图片检测到的角点像素坐标 std::vectorstd::vectorcv::Point3f objectPoints; // 保存对应的世界坐标3D // 生成世界坐标假设棋盘格在Z0平面上方格边长为25.0mm std::vectorcv::Point3f obj; for (int i 0; i boardSize.height; i) { for (int j 0; j boardSize.width; j) { obj.push_back(cv::Point3f(j * 25.0f, i * 25.0f, 0.0f)); } } int savedCount 0; const int numPicsNeeded 15; // 至少需要10-15张越多越好建议20 std::string saveDir ./calib_imgs/; fs::create_directories(saveDir); // 创建保存目录 std::cout 按 s 键保存当前帧按 q 键退出采集。 std::endl; while (savedCount numPicsNeeded) { cv::Mat frame; cap frame; if (frame.empty()) break; cv::Mat gray; cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); std::vectorcv::Point2f corners; bool found cv::findChessboardCorners(gray, boardSize, corners, cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); cv::Mat view frame.clone(); if (found) { // 亚像素级角点精确化提升定位精度 cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS cv::TermCriteria::MAX_ITER, 30, 0.001)); cv::drawChessboardCorners(view, boardSize, cv::Mat(corners), found); // 提示可以保存 cv::putText(view, Checkerboard FOUND! Press s to save., cv::Point(10, 30), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 255, 0), 2); } cv::imshow(Camera Calibration - Collecting Images, view); char key (char)cv::waitKey(30); if (key s found) { std::string filename saveDir calib_ std::to_string(savedCount) .jpg; cv::imwrite(filename, frame); imagePoints.push_back(corners); objectPoints.push_back(obj); // 每张图对应的世界坐标是一样的 savedCount; std::cout 已保存图片: filename [ savedCount / numPicsNeeded ] std::endl; } else if (key q) { break; } } cap.release(); cv::destroyAllWindows(); if (imagePoints.size() 5) { // OpenCV建议至少3张但实际最好多于5张 std::cerr 采集的有效图片太少无法进行标定。 std::endl; return -1; } // 接下来进入标定计算步骤... return 0; }采集技巧与注意事项姿态多样性这是成功的关键让标定板覆盖图像各个区域中心、四个角、四条边并呈现各种角度平视、俯仰、倾斜、旋转。想象用标定板在相机视野前“扇风”。光照均匀避免局部过亮或过暗避免反光。阴影不要覆盖角点区域。对焦清晰确保棋盘格线条清晰角点处不模糊。数量要求通常需要15-25张高质量图片。太少可能导致参数求解不稳定太多超过50张则优化时间变长收益递减。4.2 第二步执行标定计算采集完图片后我们使用OpenCV的核心函数calibrateCamera进行计算。// 接上一段代码在采集到足够图片后 cv::Mat cameraMatrix, distCoeffs; std::vectorcv::Mat rvecs, tvecs; // 每张图片的旋转向量和平移向量 // 初始化相机矩阵一个合理的猜测有助于优化 cv::Size imageSize cv::imread(saveDir calib_0.jpg).size(); cameraMatrix cv::Mat::eye(3, 3, CV_64F); cameraMatrix.atdouble(0, 0) 800.0; // 一个合理的初始焦距猜测值像素 cameraMatrix.atdouble(1, 1) 800.0; cameraMatrix.atdouble(0, 2) imageSize.width / 2.0; // 主点初始设为图像中心 cameraMatrix.atdouble(1, 2) imageSize.height / 2.0; distCoeffs cv::Mat::zeros(5, 1, CV_64F); // 初始化畸变系数为0 (k1, k2, p1, p2, k3) // 执行标定 double reprojError cv::calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, cv::CALIB_FIX_ASPECT_RATIO // 固定fx/fy的比例通常不固定 // | cv::CALIB_ZERO_TANGENT_DIST // 如果不希望估计切向畸变可以取消注释 // | cv::CALIB_FIX_K3 // 如果不希望估计k3可以取消注释 ); std::cout \n 标定完成 std::endl; std::cout 重投影误差 (RMS): reprojError 像素 std::endl; std::cout 相机内参矩阵 K:\n cameraMatrix std::endl; std::cout 畸变系数 [k1, k2, p1, p2, k3, ...]:\n distCoeffs.t() std::endl; // .t() 转置以便阅读 // 保存标定结果 cv::FileStorage fs(camera_calibration.yml, cv::FileStorage::WRITE); fs camera_matrix cameraMatrix; fs distortion_coefficients distCoeffs; fs reprojection_error reprojError; fs.release(); std::cout 标定参数已保存至 camera_calibration.yml。 std::endl;关键参数解析cv::CALIB_FIX_ASPECT_RATIO如果你确信相机的像素是完美的正方形即fx fy可以启用此标志。但对于大多数消费级相机CMOS像素并非严格正方形所以通常不启用此标志让算法自由估计fx和fy。cv::CALIB_ZERO_TANGENT_DIST如果你使用的镜头质量很好组装精度高切向畸变可以忽略可以启用此标志强制p1p20简化模型。对于普通镜头建议先不启用看估计出的p1, p2值是否很小再做决定。cv::CALIB_FIX_K3径向畸变通常用k1, k2已能很好校正。k3用于校正更极端的畸变。如果标定板没有覆盖图像最边缘估计k3可能不稳定。可以先启用如果重投影误差仍不理想再尝试放开估计。如何解读结果重投影误差理想情况应小于0.5像素。0.1-0.3像素是优秀水平0.3-0.8像素是可接受水平大于1像素则需要检查标定板、图片质量或采集姿态。内参矩阵fx,fy应在合理范围。对于常见的1080p网络摄像头焦距通常在500-1000像素之间。cx,cy应接近图像中心width/2, height/2偏差几十像素是正常的。畸变系数k1通常为负值桶形畸变或小的正值枕形畸变绝对值在0.1量级或更小。p1,p2通常非常接近0例如1e-3量级。如果某个值异常大例如k1 1很可能标定过程出了问题。4.3 第三步验证与图像校正标定完成后必须验证其效果。最直观的方法就是看校正前后的图像对比。// 验证读取一张新的、未参与标定的图片进行校正 cv::Mat testImage cv::imread(./test_image.jpg); if (testImage.empty()) { std::cerr 无法读取测试图片 std::endl; // 也可以用参与标定的图片验证 testImage cv::imread(saveDir calib_0.jpg); } cv::Mat undistortedImage; // 方法1使用initUndistortRectifyMap remap (适用于需要多次校正同一相机图片的场景效率高) cv::Mat map1, map2; cv::initUndistortRectifyMap(cameraMatrix, distCoeffs, cv::Mat(), cameraMatrix, // 这里可以传入一个新的相机矩阵用于改变校正后的视角 testImage.size(), CV_32FC1, map1, map2); cv::remap(testImage, undistortedImage, map1, map2, cv::INTER_LINEAR); // 方法2直接使用undistort函数 (简单直接每次调用都计算映射) // cv::undistort(testImage, undistortedImage, cameraMatrix, distCoeffs); // 并排显示 cv::Mat comparison; cv::hconcat(testImage, undistortedImage, comparison); cv::putText(comparison, Original (Distorted), cv::Point(10, 30), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2); cv::putText(comparison, Undistorted, cv::Point(testImage.cols 10, 30), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 255, 0), 2); // 画一些直线来辅助观察畸变校正效果例如棋盘格的边缘应变直 cv::imshow(Distortion Correction Comparison, comparison); cv::waitKey(0);验证要点直线变直观察图像中原本应该是直线的物体如门窗边缘、标定板边框校正后应变得更直。边缘区域特别注意图像四个角的校正效果畸变校正主要作用于这些区域。有效视野校正过程本质是对图像进行重新投影图像边缘的像素可能会被“拉伸”到画布外导致校正后的图像出现黑边无效区域。initUndistortRectifyMap的第四个参数可以传入一个新的相机矩阵通过调整其焦距或主点可以实现不同形式的裁剪或缩放以消除黑边或保持视野。5. 进阶应用手眼标定Eye-in-Hand精讲这是机器人视觉中的核心问题。当相机固定在机械臂末端Eye-in-Hand时我们需要求出相机坐标系到机械臂末端工具坐标系法兰盘的固定变换关系。你提到的“法兰只平移无旋转”是一个简化场景但理解通用解法更重要。5.1 问题定义与数学模型我们有baseHend机械臂底座坐标系到末端法兰坐标系的变换由机器人控制器给出已知。endHcam末端法兰坐标系到相机坐标系的变换这是我们要求解的固定值。camHboard相机坐标系到标定板坐标系的变换通过相机标定得到即每张图片的R, t已知。baseHboard底座坐标系到标定板坐标系的变换固定值但未知。当机械臂移动时标定板固定不动。对于两个不同的机械臂位姿i和j我们可以列出方程baseHend_i * endHcam * camHboard_i baseHboardbaseHend_j * endHcam * camHboard_j baseHboard由于baseHboard是固定的两式相等baseHend_i * endHcam * camHboard_i baseHend_j * endHcam * camHboard_j移项后得到经典的AX XB形式(baseHend_j)^-1 * baseHend_i * endHcam endHcam * camHboard_j * (camHboard_i)^-1令A (baseHend_j)^-1 * baseHend_iB camHboard_j * (camHboard_i)^-1 则方程为A * X X * B其中X endHcam。我们需要至少两组运动三个不同位姿来求解X。OpenCV提供了calibrateHandEye函数来求解这个问题。5.2 C代码实现手眼标定假设我们已经通过之前的标定获得了多组camHboard即rvecs,tvecs并且从机器人控制器同步记录了对应的baseHend需要转换为4x4齐次变换矩阵。// 假设我们已经有了 // std::vectorcv::Mat baseHend_list; // 从机器人读取的底座到末端的变换矩阵 (4x4, CV_64F) // std::vectorcv::Mat camHboard_list; // 相机标定得到的相机到标定板的变换矩阵 (4x4, CV_64F) // 它们一一对应且数量至少为3组。 std::vectorcv::Mat R_base2end, t_base2end; std::vectorcv::Mat R_cam2board, t_cam2board; // 将4x4齐次矩阵分解为旋转矩阵和平移向量或直接准备这些数据 for (const auto H : baseHend_list) { cv::Mat R, t; // 假设H是4x4矩阵提取左上3x3为R前3行第4列为t H(cv::Rect(0,0,3,3)).copyTo(R); H(cv::Rect(3,0,1,3)).copyTo(t); // 注意OpenCV矩阵是(row, col) R_base2end.push_back(R); t_base2end.push_back(t); } // 同理从camHboard_list填充R_cam2board和t_cam2board... // 注意calibrateCamera得到的rvec是旋转向量需要用Rodrigues转换为旋转矩阵 for (int i 0; i rvecs.size(); i) { cv::Mat R; cv::Rodrigues(rvecs[i], R); // 将旋转向量转为旋转矩阵 R_cam2board.push_back(R); t_cam2board.push_back(tvecs[i]); } // 确保数据数量一致且足够 if (R_base2end.size() ! R_cam2board.size() || R_base2end.size() 3) { std::cerr 手眼标定数据不足或不对应 std::endl; return -1; } // 执行手眼标定 cv::Mat R_end2cam, t_end2cam; // 这就是我们要求的 X 的旋转和平移部分 cv::calibrateHandEye(R_base2end, t_base2end, R_cam2board, t_cam2board, R_end2cam, t_end2cam, cv::CALIB_HAND_EYE_TSAI); // 有多种算法可选 std::cout \n 手眼标定结果 std::endl; std::cout 旋转矩阵 R_end2cam:\n R_end2cam std::endl; std::cout 平移向量 t_end2cam (单位与标定板尺度一致如mm):\n t_end2cam.t() std::endl; // 可以将结果保存为4x4变换矩阵 cv::Mat endHcam cv::Mat::eye(4, 4, CV_64F); R_end2cam.copyTo(endHcam(cv::Rect(0,0,3,3))); t_end2cam.copyTo(endHcam(cv::Rect(3,0,1,3)));算法选择calibrateHandEye提供了多种求解算法如CALIB_HAND_EYE_TSAI,CALIB_HAND_EYE_PARK,CALIB_HAND_EYE_HORAUD等。Tsai算法是经典且常用的。如果结果不理想可以尝试换一种算法。“法兰只平移无旋转”场景如果机械臂末端法兰在运动过程中保持方向不变例如只做XYZ方向的平移那么baseHend_i和baseHend_j之间的旋转部分R是单位矩阵。这实际上简化了AXXB方程因为A和B的旋转部分都是单位阵方程退化为只求解平移关系。你仍然可以使用calibrateHandEye但更简单的方法是在多个平移位姿下相机看到的标定板位姿变化camHboard的反变换应该等于机械臂的末端平移变化。通过最小二乘法可以直接求解出endHcam的平移部分。旋转部分则可以通过法兰的机械设计图纸直接得到或者让机械臂做一次纯旋转运动来单独标定。5.3 手眼标定的实操要点与常见陷阱数据同步是生命线机械臂位姿baseHend和相机拍摄图像的时刻必须严格同步。哪怕几十毫秒的延迟在高速运动下也会引入巨大误差。最好使用硬件触发相机并在触发信号发出时立刻读取机器人位姿。运动激励要充分和相机内参标定一样机械臂的运动需要多样化。不仅要有平移还要有绕不同轴的旋转。运动范围要尽可能大以充分激发所有参数。标定板固定且稳定标定板必须牢固固定在采集过程中不能有丝毫移动。最好使用磁性底座或夹具固定在厚重的平台上。坐标系一致性确保你从机器人控制器读取的baseHend矩阵的定义是底座到法兰还是法兰到底座旋转矩阵是前乘还是后乘与你的代码中的坐标系变换链定义一致。这是最容易出错的地方。尺度统一baseHend的平移单位通常是毫米或米而camHboard的平移单位是你定义标定板方格边长时使用的单位例如毫米。必须统一通常我们都使用毫米。验证结果求解出endHcam后可以将其代入AXXB方程计算重投影误差这里是指空间变换的误差。也可以让机械臂移动到一个新位置用求得的X和当前的baseHend预测标定板在相机中的位姿与实际检测的位姿进行比较。6. 常见问题排查与性能优化在实际操作中你肯定会遇到各种问题。下面是一个快速排查指南。问题现象可能原因排查与解决思路findChessboardCorners检测失败1. 棋盘格不在视野内或不全。2. 光照太暗或反光。3. 棋盘格模糊失焦。4.boardSize参数设置错误数的是内角点。1. 确保棋盘格完整可见调整姿态。2. 改善光照使用哑光标定板。3. 手动对焦或调整光圈。4. 仔细核对棋盘格内角点行列数。重投影误差过大 (1像素)1. 标定板图像质量差模糊、畸变过大。2. 采集姿态不够多样。3. 标定板物理尺寸测量不准。4. 角点检测不准亚像素优化未启用。1. 重新采集高质量图片。2. 确保覆盖视野各个区域和角度。3. 用更精确的方法测量方格尺寸。4. 确认代码中调用了cornerSubPix。内参焦距 (fx,fy) 异常1. 标定板尺寸单位错误如用成了厘米。2. 图像分辨率读取错误。3. 相机传感器信息极度非常规。1. 检查objectPoints中的坐标值单位建议用mm。2. 打印imageSize确认是否正确。3. 与相机数据手册的理论焦距进行粗略对比。畸变系数 (k1) 绝对值巨大1. 标定板图像只集中在视野中心未激发边缘畸变。2. 镜头是鱼眼镜头但使用了普通镜头模型标定。1. 必须采集覆盖图像边缘的图片。2. 鱼眼镜头需使用fisheye::calibrate或omnidir::calibrate。校正后图像有严重黑边这是正常现象畸变校正将边缘扭曲的像素“拉直”后原图边缘信息可能映射到画布外。使用getOptimalNewCameraMatrix获取一个缩放后的新相机矩阵配合initUndistortRectifyMap可以裁剪掉黑边或缩放图像填满画面。手眼标定结果不稳定1. 数据不同步。2. 运动激励不足特别是缺少旋转。3. 相机内参标定不准。4. 机器人位姿读数有噪声或延迟。1. 实现硬件触发与同步。2. 设计包含多轴旋转的运动轨迹。3. 重新进行高精度的相机内参标定。4. 检查机器人通信和滤波设置。性能优化技巧多线程采集对于高帧率相机采集和角点检测可以放在不同线程避免因检测耗时导致丢帧或运动模糊。使用更快的角点检测对于非常规图案或高速场景可以研究findChessboardCornersSB基于分水岭算法速度更快或自定义的角点检测方法。标定板优化使用 AprilTag 或 Charuco 板。它们结合了二进制编码的标记点和棋盘格即使部分被遮挡也能稳定检测并且能提供更准确的角点位置。OpenCV 的aruco模块对此有良好支持。增量式标定如果相机参数会缓慢变化如温度漂移可以实现一个在线标定流程定期采集新数据并更新标定参数。相机标定是一个实践出真知的过程。第一次做可能觉得步骤繁琐但一旦打通它就变成了一个可靠的工具。记住高质量的输入数据标定板图片是成功的一半。耐心做好数据采集仔细理解每个参数的含义你的视觉系统就有了一个精准的“眼睛”。
返回列表