OpenCV相机标定实战:从原理到C++实现,解决视觉测量不准问题
1. 项目概述:从“拍不准”到“算得准”的必经之路
做计算机视觉或者机器人相关开发的朋友,肯定都遇到过这样的场景:你写了个程序,想通过摄像头测量一个物体的实际尺寸,或者让机械臂精准地抓取一个东西。结果发现,程序里算出来的位置和实际位置总是差那么一点,角度也歪歪扭扭。这感觉就像戴了一副度数不准的眼镜看世界,一切都有些变形和错位。问题的根源,往往就出在“相机标定”这个环节上。
简单来说,相机标定就是给相机做一次“体检”和“配镜”。我们得搞清楚这台相机自身的“性格缺陷”——它的镜头是不是有畸变?它的成像中心是不是在画面的正中央?这些内在的物理参数,我们称之为内参。同时,我们还要确定相机在三维世界中的“站位”——它相对于某个坐标系的位置和朝向,这些就是外参。只有把这些参数都准确地测量和计算出来,我们才能把相机拍到的二维图像中的像素点,准确地“翻译”回真实三维世界中的坐标,从而实现精准的测量、定位和三维重建。
这个项目,就是带你用C++和业界最常用的开源计算机视觉库OpenCV,亲手走一遍相机标定的完整流程。我不会只给你一堆公式和代码,而是会结合我多次标定的实战经验,告诉你每一步为什么要这么做,参数怎么调,以及那些官方文档里不会写的“坑”在哪里。无论你是刚接触视觉的新手,还是想巩固基础的老手,这篇都能让你对相机标定有一个透彻、可实操的理解。
2. 核心原理拆解:相机如何“看见”世界
在动手写代码之前,我们必须先弄明白相机成像的数学模型。这就像学开车前得知道方向盘、油门、刹车是干嘛的一样,是基础中的基础。
2.1 针孔相机模型:理想很丰满
我们首先从一个最简化的模型开始——针孔相机模型。你可以把它想象成一个封闭的盒子,只在前面开了一个小孔。光线从物体反射后,穿过这个小孔,在盒子后面的感光元件(比如CMOS)上形成一个倒立的像。
这个模型用数学来描述,就是一个从三维世界点[X, Y, Z]到二维图像像素点[u, v]的投影变换。其核心公式如下:
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T别被这个公式吓到,我们一点点拆解:
s:一个任意的尺度因子,可以理解为深度信息。[u, v, 1]^T:图像上的像素坐标(齐次坐标形式)。K:这就是我们要求解的内参矩阵。它描述了相机的内部属性。[R|t]:这就是外参矩阵,由旋转矩阵R和平移向量t组成。它描述了相机坐标系相对于世界坐标系的位置和姿态。[X, Y, Z, 1]^T:三维世界点的坐标(齐次坐标形式)。
内参矩阵 K通常是一个3x3的矩阵,形式如下:
[ fx, 0, cx ] [ 0, fy, cy ] [ 0, 0, 1 ]fx,fy:分别是x轴和y轴方向的焦距,单位是像素。它把真实世界中的距离(毫米)转换成了图像上的像素距离。fx = F / dx,其中F是物理焦距,dx是每个像素的物理宽度。cx,cy:主点坐标,理论上应该是图像的中心点。它表示光轴与成像平面的交点。
注意:这里有一个非常关键的细节。在理想针孔模型中,我们假设像素是完美的正方形,所以
fx = fy。但在现实中,由于制造工艺问题,像素可能不是正方形,或者成像单元在x和y方向上的采样率不同,导致fx和fy有细微差别。标定的一个重要目的就是精确求出这两个值。
2.2 透镜畸变模型:现实很骨感
针孔模型是理想化的,而真实的相机都需要镜头来汇聚更多光线。镜头,尤其是廉价镜头,会引入两种主要的畸变,导致直线在图像中变弯:
径向畸变:这是由于镜头形状缺陷导致的,成像点沿径向方向发生偏移。离图像中心越远,畸变越明显。它又分为:
- 桶形畸变:图像像被吸进了桶里,中心凸出。
- 枕形畸变:图像像枕头的边缘,中心凹陷。 在数学上,通常用
k1,k2,k3等参数来修正径向畸变。
切向畸变:这是由于镜头制造和安装时,透镜平面与成像平面不平行导致的。它使图像看起来有点“歪斜”。用
p1,p2两个参数来修正。
所以,完整的相机标定,不仅要算出内参矩阵K,还要算出一组畸变系数[k1, k2, p1, p2, k3...]。OpenCV中常用的就是这5个参数。
2.3 张正友标定法:为什么是棋盘格?
理解了模型,我们怎么求解这些参数呢?这就需要已知一些三维世界点和它们在图像中对应的二维像素点,然后通过优化算法来反推相机参数。这就是所谓的“基于靶标的标定”。
张正友教授提出的方法之所以成为经典,是因为它巧妙且实用:
- 靶标简单易得:只需要一个打印出来的棋盘格图案。棋盘格的黑白角点(方格交点)在图像中很容易被高精度地自动检测出来。
- 已知世界坐标:我们可以定义棋盘格平面为 Z=0 的世界坐标系平面。这样,每个角点的世界坐标
[X, Y, 0]是已知的(比如方格间距是25毫米,那么角点坐标就是 (0,0), (25,0), (50,0)...)。 - 多角度拍摄:我们从不同角度、不同距离拍摄这个棋盘格(通常需要10-20张)。每一张图片都提供了一组
世界点-图像点的对应关系,并且每一张图片都对应一组不同的外参[R|t](因为相机动了),但所有图片共享同一组内参K和畸变系数。 - 求解与优化:算法首先忽略畸变,用一个闭式解初步估算出内参和外参。然后,将畸变系数考虑进去,利用最大似然估计进行非线性优化(通常使用Levenberg-Marquardt算法),对所有参数(内参、畸变系数、每一张图的外参)进行整体精调,使得所有角点的重投影误差(计算出来的像素位置和实际检测到的像素位置之间的距离)最小。
实操心得:棋盘格的质量直接影响角点检测的精度。一定要用高对比度、平整的硬纸板或亚克力板打印。方格大小要合适,在图像中既不能太小(角点模糊),也不能太大(拍不到足够多的角点)。我一般使用A4纸打印,方格边长在20-30毫米之间,效果比较稳定。
3. 环境准备与工具选型:工欲善其事
在开始C++实现前,我们需要把环境和工具准备好。这里我会给出一个兼顾稳定性和开发效率的方案。
3.1 开发环境搭建
操作系统:推荐使用 Ubuntu 20.04/22.04 LTS 或 Windows 10/11。Linux环境下依赖管理更清晰,Windows下Visual Studio的调试体验更好。本文以Ubuntu为例进行说明,Windows步骤会略有不同。
编译器:C++11及以上标准的编译器。Linux下用g++,Windows下用Visual Studio的MSVC。
构建工具:强烈推荐使用CMake。它是管理C++项目、寻找第三方库的行业标准,跨平台性极佳。
核心库:OpenCV。这是计算机视觉的“瑞士军刀”。我们标定所需的所有函数都在其中。
3.2 OpenCV的安装与验证
在Ubuntu下安装OpenCV非常方便。建议安装OpenCV 4.x版本。
# 更新软件包列表 sudo apt update # 安装编译工具和必要依赖 sudo apt install build-essential cmake git pkg-config sudo apt install libjpeg-dev libtiff-dev libpng-dev libavcodec-dev libavformat-dev libswscale-dev sudo apt install libgtk2.0-dev libcanberra-gtk-module sudo apt install libxvidcore-dev libx264-dev sudo apt install python3-dev python3-numpy sudo apt install libtbb2 libtbb-dev libdc1394-22-dev sudo apt install libv4l-dev v4l-utils sudo apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev sudo apt install libopencv-dev # 这是安装预编译包,如果想从源码编译更高级版本,可以跳过这一步 # 从源码编译安装(以OpenCV 4.8.0为例,版本可换) cd ~ git clone -b 4.8.0 https://github.com/opencv/opencv.git git clone -b 4.8.0 https://github.com/opencv/opencv_contrib.git # 贡献库,包含更多模块 cd opencv mkdir build && cd build cmake -D CMAKE_BUILD_TYPE=RELEASE \ -D CMAKE_INSTALL_PREFIX=/usr/local \ -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \ -D WITH_TBB=ON \ -D WITH_V4L=ON \ -D WITH_QT=ON \ # 如果你想要Qt支持的高斯窗口,可以开启 -D WITH_OPENGL=ON \ -D OPENCV_GENERATE_PKGCONFIG=ON .. # 生成pkg-config文件,方便其他程序查找 make -j$(nproc) # 使用所有CPU核心并行编译,加快速度 sudo make install sudo ldconfig # 更新动态链接库缓存安装完成后,写一个简单的程序验证:
// test_opencv.cpp #include <opencv2/opencv.hpp> #include <iostream> int main() { std::cout << "OpenCV version: " << CV_VERSION << std::endl; cv::Mat img = cv::Mat::zeros(100, 100, CV_8UC1); std::cout << "OpenCV test image created successfully." << std::endl; return 0; }编译并运行:
g++ -std=c++11 test_opencv.cpp -o test_opencv `pkg-config --cflags --libs opencv4` ./test_opencv如果输出了OpenCV版本号和成功信息,说明环境配置成功。
踩坑记录:在Windows上使用Visual Studio,推荐通过
vcpkg来安装OpenCV,可以自动处理库依赖和路径配置,比手动配置属性表要省心得多。命令类似于vcpkg install opencv4:x64-windows。
3.3 项目结构设计
一个好的项目结构能让代码更清晰。我们这样组织:
camera_calibration/ ├── CMakeLists.txt # 项目构建文件 ├── include/ # 头文件(可选,小型项目可直接写cpp里) ├── src/ # 源代码目录 │ ├── main.cpp # 主程序 │ └── calibration_utils.cpp # 标定相关工具函数(如果拆分的话) ├── data/ # 数据目录 │ ├── calibration_pattern.pdf # 棋盘格PDF,用于打印 │ └── captured_images/ # 存放拍摄的棋盘格图片 └── build/ # 编译输出目录(由CMake生成)我们的CMakeLists.txt可以这样写:
cmake_minimum_required(VERSION 3.10) project(CameraCalibration) set(CMAKE_CXX_STANDARD 11) # 寻找OpenCV包 find_package(OpenCV REQUIRED) # 包含头文件目录 include_directories(${OpenCV_INCLUDE_DIRS}) # 添加可执行文件 add_executable(calibrate_camera src/main.cpp) # 链接OpenCV库 target_link_libraries(calibrate_camera ${OpenCV_LIBS})4. C++实现详解:一步步完成标定
接下来,我们进入核心的代码实现环节。我会将整个过程分解为几个清晰的步骤,并附上详细的代码注释。
4.1 第一步:图像采集与角点检测
这个步骤的目标是:读取我们事先拍好的多张棋盘格图片,并在每一张图片上自动找到所有内角点(棋盘格内部的交点)的像素坐标。
#include <opencv2/opencv.hpp> #include <opencv2/calib3d.hpp> #include <iostream> #include <vector> #include <filesystem> // C++17,需要编译器支持。如需兼容旧标准,可用 dirent.h namespace fs = std::filesystem; int main() { // 1. 定义棋盘格的尺寸(内角点数量,不是方格数!) // 例如,一个8x6的棋盘格,有7x5个内角点。 cv::Size boardSize(7, 5); // (width, height) 即列数,行数 int numCorners = boardSize.width * boardSize.height; // 2. 准备数据结构,存储所有图像的世界坐标和图像坐标 std::vector<std::vector<cv::Point3f>> objectPoints; // 三维世界点集(所有图片) std::vector<std::vector<cv::Point2f>> imagePoints; // 二维图像点集(所有图片) // 3. 生成棋盘格的世界坐标(假设Z=0) // 我们定义每个方格的物理尺寸,例如25毫米 float squareSize = 25.0; // 单位:毫米 std::vector<cv::Point3f> obj; for (int i = 0; i < boardSize.height; ++i) { for (int j = 0; j < boardSize.width; ++j) { obj.push_back(cv::Point3f(j * squareSize, i * squareSize, 0)); } } // 4. 遍历指定目录下的所有图片 std::string imageDir = "../data/captured_images/"; std::vector<cv::String> imagePaths; cv::glob(imageDir + "*.jpg", imagePaths); // 假设图片格式为jpg if (imagePaths.empty()) { std::cerr << "错误:在目录 " << imageDir << " 中未找到任何图片!" << std::endl; return -1; } cv::Size imageSize; // 用于记录图像尺寸,所有图片应保持一致 for (const auto& imagePath : imagePaths) { cv::Mat img = cv::imread(imagePath); if (img.empty()) { std::cerr << "无法读取图片: " << imagePath << std::endl; continue; } // 如果是第一张图,记录图像尺寸 if (imageSize.empty()) { imageSize = img.size(); } else if (img.size() != imageSize) { std::cerr << "警告:图片 " << imagePath << " 的尺寸与其他图片不一致,跳过。" << std::endl; continue; } cv::Mat gray; cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); // 5. 查找棋盘格角点 std::vector<cv::Point2f> corners; // 存储当前图片检测到的角点 bool found = cv::findChessboardCorners(gray, boardSize, corners); if (found) { // 6. 亚像素级角点精确化 // 提高角点检测的精度,尤其是对于低分辨率或模糊的图像 cv::TermCriteria criteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.001); cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1), criteria); // 7. 将成功找到的角点对应的世界坐标和图像坐标存入总列表 objectPoints.push_back(obj); imagePoints.push_back(corners); // 8. 在图像上绘制角点,方便可视化检查 cv::drawChessboardCorners(img, boardSize, cv::Mat(corners), found); cv::imshow("Detected Corners", img); cv::waitKey(500); // 每张图显示0.5秒 } else { std::cout << "在图片 " << fs::path(imagePath).filename() << " 中未找到棋盘格角点。" << std::endl; } } cv::destroyAllWindows(); std::cout << "成功处理了 " << objectPoints.size() << " 张有效图片。" << std::endl; if (objectPoints.size() < 10) { // 通常需要至少10张以上不同角度的图片 std::cerr << "警告:有效图片数量较少,标定结果可能不准确。建议拍摄更多不同角度和距离的图片。" << std::endl; } // 至此,我们准备好了 objectPoints 和 imagePoints,可以开始标定了。 // ... (标定代码见下一节) return 0; }注意事项:
boardSize是内角点数,比如棋盘格有8行6列方格,那么内角点就是7行5列。这是最容易搞错的地方!squareSize的单位(毫米)决定了你最终计算出的外参平移向量t的单位。保持一致即可。cornerSubPix是关键步骤,它能将角点定位精度提升到亚像素级别,对提高标定精度至关重要。- 拍摄图片时,要确保棋盘格在画面中清晰,且覆盖整个画面(中心、边缘、四个角都要有),同时要有足够的姿态变化(平放、倾斜、旋转、远近)。
4.2 第二步:执行标定与参数计算
有了足够多的objectPoints和imagePoints,我们就可以调用OpenCV的核心标定函数了。
// 接上一段代码 // 9. 准备输出参数 cv::Mat cameraMatrix = cv::Mat::eye(3, 3, CV_64F); // 内参矩阵,初始化为单位矩阵 cv::Mat distCoeffs = cv::Mat::zeros(5, 1, CV_64F); // 畸变系数,初始为0 [k1, k2, p1, p2, k3] std::vector<cv::Mat> rvecs, tvecs; // 每张图片的旋转向量和平移向量 // 10. 执行相机标定 double reprojectionError = cv::calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, cv::CALIB_FIX_K3); // 我们通常先固定k3为0 std::cout << "\n=== 标定完成 ===" << std::endl; std::cout << "重投影误差 (RMS): " << reprojectionError << " 像素" << std::endl; std::cout << "\n相机内参矩阵 (K):" << std::endl; std::cout << cameraMatrix << std::endl; std::cout << "\n畸变系数 [k1, k2, p1, p2, k3]:" << std::endl; std::cout << distCoeffs.t() << std::endl; // 转置一下,方便看 // 11. 评估标定结果:重投影误差是衡量标定精度的核心指标。 // 一般来说,RMS误差小于0.5像素可以认为是很好的结果,1像素以内是可接受的。 // 误差过大需要检查图片质量、角点检测是否正确、棋盘格是否平整等。 std::cout << "\n=== 各图片重投影误差分析 ===" << std::endl; double totalErr = 0; for (size_t i = 0; i < objectPoints.size(); i++) { std::vector<cv::Point2f> projectedPoints; // 使用标定出的参数,将世界坐标投影回图像 cv::projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix, distCoeffs, projectedPoints); // 计算该图片的平均误差 double err = cv::norm(imagePoints[i], projectedPoints, cv::NORM_L2) / projectedPoints.size(); totalErr += err; std::cout << "图片 " << i << " 平均误差: " << err << " 像素" << std::endl; } std::cout << "所有图片平均重投影误差: " << totalErr / objectPoints.size() << " 像素" << std::endl;关键参数解析:
cv::CALIB_FIX_K3:这是一个标志位。对于畸变不大的镜头(如普通网络摄像头),k3系数的影响很小,有时固定为0可以增加数值稳定性。你也可以去掉这个标志,让算法同时优化k3。reprojectionError:这个返回值是标定过程的总均方根误差。它是所有图片所有角点重投影误差的RMS值,是衡量标定整体质量的金标准。rvecs和tvecs:它们是每张标定图片对应的外参。rvecs是旋转向量(罗德里格斯向量),可以通过cv::Rodrigues()函数转换为3x3的旋转矩阵R。tvecs是平移向量,单位与你定义的squareSize一致(这里是毫米)。
4.3 第三步:保存、加载与应用标定参数
标定一次不容易,我们需要把结果保存下来,供后续程序使用。同时,也要学会如何使用这些参数来校正图像。
// 12. 保存标定参数到文件(YAML格式,OpenCV易于读写) cv::FileStorage fs("../data/camera_calibration.yml", cv::FileStorage::WRITE); if (fs.isOpened()) { fs << "camera_matrix" << cameraMatrix; fs << "distortion_coefficients" << distCoeffs; fs << "image_width" << imageSize.width; fs << "image_height" << imageSize.height; fs << "reprojection_error" << reprojectionError; fs.release(); std::cout << "\n标定参数已保存至 camera_calibration.yml" << std::endl; } else { std::cerr << "无法打开文件用于保存参数!" << std::endl; } // 13. 演示:加载参数并校正一张图片 cv::Mat loadedCameraMatrix, loadedDistCoeffs; cv::FileStorage fs2("../data/camera_calibration.yml", cv::FileStorage::READ); if (fs2.isOpened()) { fs2["camera_matrix"] >> loadedCameraMatrix; fs2["distortion_coefficients"] >> loadedDistCoeffs; fs2.release(); } // 读取一张测试图片(可以是标定集之外的) cv::Mat testImage = cv::imread("../data/test_image.jpg"); if (!testImage.empty()) { cv::Mat undistortedImage; // 方法1:使用 undistort 函数直接校正 cv::undistort(testImage, undistortedImage, loadedCameraMatrix, loadedDistCoeffs); // 方法2:先获取最优的新相机矩阵和ROI,再进行校正(可以裁剪掉黑边) cv::Mat newCameraMatrix = cv::getOptimalNewCameraMatrix(loadedCameraMatrix, loadedDistCoeffs, testImage.size(), 1, // alpha=1 表示保留所有像素,会留有黑边;0表示裁剪掉所有无效像素 testImage.size()); cv::Mat map1, map2; cv::initUndistortRectifyMap(loadedCameraMatrix, loadedDistCoeffs, cv::Mat(), newCameraMatrix, testImage.size(), CV_16SC2, map1, map2); // 计算映射表 cv::remap(testImage, undistortedImage, map1, map2, cv::INTER_LINEAR); // 重映射 // 显示对比 cv::hconcat(testImage, undistortedImage, testImage); cv::imshow("原始图像 (左) vs 校正后图像 (右)", testImage); cv::waitKey(0); } return 0; }实操心得:
getOptimalNewCameraMatrix中的alpha参数很实用。如果你后续的处理需要完整的图像信息,且不介意边缘有少量黑边,可以设为1。如果你希望得到一个完全没有黑边的图像(但会损失一部分视野),可以设为0。通常我会先设为1,看看效果,再根据实际需求调整。
5. 进阶话题与精度提升技巧
基础的标定流程走通了,但要想获得工业级的高精度,还有很多细节需要注意。
5.1 标定板的选择与制作
棋盘格是最常用的,但不是唯一的。还有圆点网格、Charuco板等。
- 棋盘格:优点是好打印,角点检测稳定。缺点是必须完全在视野内才能检测,对视角要求高。
- 圆点网格:圆心检测的精度可能更高,且对部分遮挡不敏感。
- Charuco板:结合了棋盘格和ArUco标记。它即使在部分被遮挡时,也能通过ArUco码提供绝对坐标,鲁棒性更强,非常适合需要高精度和稳定性的场景。
制作建议:
- 材质:使用哑光材料,避免反光。亚光相纸或贴在平整亚克力板上的打印纸是很好的选择。
- 平整度:这是影响精度的关键!一定要将标定板贴平在坚硬的底板上。弯曲的纸会引入无法通过模型修正的误差。
- 尺寸精度:确保打印的方格尺寸准确。可以用游标卡尺测量一下。
5.2 拍摄图像的质量与数量
- 数量:10-20张是较好的起点。太少会导致参数过拟合或求解不稳定,太多则增加计算量,边际收益递减。
- 姿态多样性:这是最重要的原则。要让棋盘格出现在图像的各个位置(中心、四个边缘、四个角),并且要有各种不同的倾斜和旋转角度。避免所有图片都是棋盘格在正中央且基本正对相机。
- 对焦与清晰度:确保棋盘格在每张图片中都清晰。轻微的模糊会严重影响角点亚像素定位的精度。
- 光照均匀:避免强烈的阴影或反光覆盖在棋盘格上。均匀的漫射光是最理想的。
5.3 标定结果的质量评估与验证
除了看总的重投影误差,还有更多验证方法:
- 单张图片误差分析:就像我们代码里做的那样,打印每张图的误差。如果某一张图的误差显著高于其他图片,应该检查这张图(是否模糊、畸变过大、检测错误),考虑将其从标定集中剔除。
- 外参合理性检查:观察计算出的
tvecs(平移向量)。对于手持相机围绕棋盘格拍摄的情况,这些向量的模(距离)应该在一个合理的范围内,并且随着拍摄距离变化而变化。如果某张图的平移向量异常大或小,可能有问题。 - 独立验证集:使用一组未参与标定的棋盘格图片,用标定好的参数去计算重投影误差。这个误差应该与标定集上的误差接近。如果显著变大,说明标定可能过拟合了。
- 立体视觉对极几何验证:如果是双目相机标定,校正后的图像,其对应点应该基本位于同一水平线上。可以画一些对极线来直观检查。
6. 常见问题排查与实战技巧
在实际操作中,你肯定会遇到各种各样的问题。这里我总结了一个速查表:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
findChessboardCorners总是失败 | 1.boardSize设置错误(用了方格数而非角点数)。2. 棋盘格对比度太低、反光或光照不均。 3. 棋盘格在画面中占比太小或太大。 4. 棋盘格被部分遮挡或超出画面。 | 1. 仔细核对boardSize。2. 重新打印高对比度棋盘格,改善光照。 3. 调整拍摄距离。 4. 确保整个棋盘格都在画面内。 |
| 重投影误差很大 (>2像素) | 1. 角点检测不准(图像模糊、棋盘格不平)。 2. 拍摄姿态多样性不足。 3. 棋盘格物理尺寸 ( squareSize) 测量或设置错误。4. 镜头畸变非常严重,而模型参数不足(如只用k1,k2,p1,p2)。 | 1. 检查角点检测可视化图,看角点是否精准落在交点上。确保棋盘格平整。 2. 增加不同角度、距离的图片,特别是要让棋盘格充满画面边缘。 3. 用游标卡尺精确测量方格尺寸。 4. 尝试在 calibrateCamera中优化k3, 甚至k4,k5,k6(需使用CALIB_RATIONAL_MODEL标志)。 |
| 标定后图像校正效果奇怪(仍有弯曲或拉伸) | 1. 内参矩阵或畸变系数加载错误。 2. 校正时使用了错误的图像尺寸。 3. 标定本身失败,参数不可信。 | 1. 检查保存和加载的YAML文件内容是否正确。 2. 确保 undistort或initUndistortRectifyMap使用的图像尺寸与标定时一致 (imageSize)。3. 回到上一步,检查重投影误差,并验证标定结果。 |
| 校正后的图像有黑边 | 这是正常现象。畸变校正后,原图边缘的像素被映射到了图像外部。 | 使用getOptimalNewCameraMatrix并设置alpha=0来获取一个裁剪掉黑边的ROI。或者,在后续处理中忽略这些黑色区域。 |
外参 (tvecs) 数值异常 | 1.squareSize单位错误(例如以为是米,实际是毫米)。2. 世界坐标系定义与标定板方向不符。 | 1. 统一单位。记住tvecs的单位与squareSize一致。2. 确保世界坐标系的X/Y轴与棋盘格的行/列方向对齐。这通常只影响外参的解读,不影响投影关系。 |
独家技巧:
- 自动化采集:写一个简单的程序,实时显示摄像头画面,并自动检测棋盘格。当检测成功且姿态与已有图片差异较大时,自动保存图片并提示。这能极大地提高采集效率和姿态多样性。
- 多阶段标定:对于畸变非常大的鱼眼镜头,OpenCV提供了
fisheye命名空间下的标定函数,使用的是不同的畸变模型。普通镜头用普通模型,鱼眼镜头用鱼眼模型,不要混用。 - 参数固化:对于固定安装的工业相机,标定一次后,内参和畸变系数基本不会变(除非更换镜头或调整焦距)。可以将这些参数硬编码或作为配置文件,无需每次启动都标定。外参则需要根据相机与工作台的相对位置重新计算。
