OpenCV 相机标定流程指南

OpenCV 相机标定流程指南

https://docs.opencv.org/4.x/dc/dbb/tutorial_py_calibration.html

https://learnopencv.com/camera-calibration-using-opencv/

前置准备

  1. 制作标定板:生成高精度棋盘格或圆点标定板。
  2. 采集标定板图像:在不同角度、距离和光照条件下采集多张标定板图像。

OpenCV 官方标定板生成脚本使用教程
!OpenCV 官方标定板脚本下载

访问我的源代码仓库下载已经生成的矢量棋盘网格,使用打印机打印出来即可进行图像标定采集工作。

标定流程

使用 CameraCalib 类进行相机标定:

  1. 添加图像样本:将采集的标定板图像导入标定系统。
  2. 并发检测角点:利用多线程技术并行检测图像中的角点或特征点。
  3. 相机标定:基于检测到的角点,计算相机内参(焦距、主点坐标)和外参(旋转矩阵、平移向量),并优化畸变系数。

结果输出与验证

  1. 打印标定结果:输出相机内参、外参及畸变系数。
  2. 测试图像标定:使用标定结果对测试图像进行畸变校正,验证标定精度。

建议

可信误差:重投影误差应小于 0.5 像素,最大不超过 1.0 像素。

采集夹角要求:摄像头与标定板平面的夹角应控制在 30°~60° 之间,避免极端角度。

[1] https://www.microsoft.com/en-us/research/publication/a-flexible-new-technique-for-camera-calibration/

源代码

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <algorithm>
#include <memory>
#include <vector>
#include <string>
#include <print>
#include <iostream>

class CameraCalib
{
public:
    // 校准模式
    enum class Pattern : uint32_t {
        CALIB_SYMMETRIC_CHESSBOARD_GRID,  // 规则排列的棋盘网格 // chessboard
        CALIB_MARKER_CHESSBOARD_GRID,     // 额外标记的棋盘网格 // marker chessboard
        CALIB_SYMMETRIC_CIRCLES_GRID,     // 规则排列的圆形网格 // circles
        CALIB_ASYMMETRIC_CIRCLES_GRID,    // 交错排列的圆形网格 // acircles
        CALIB_PATTERN_COUNT,              // 标定模式的总数量 用于 for 循环遍历 std::to_underlying(Pattern::CALIB_PATTERN_COUNT);
    };

    struct CameraCalibrationResult {
        cv::Mat cameraMatrix;                     // 相机矩阵(内参数)
        cv::Mat distortionCoefficients;           // 畸变系数
        double reprojectionError;                 // 重投影误差(标定精度指标)
        std::vector<cv::Mat> rotationVectors;     // 旋转向量(外参数)
        std::vector<cv::Mat> translationVectors;  // 平移向量(外参数)
    };

    explicit CameraCalib(int columns, int rows, double square_size /*mm*/, Pattern pattern)
      : patternSize_(columns, rows)
      , squareSize_(square_size)
      , pattern_(pattern) {
        // 构造一个与标定板对应的真实的世界角点数据
        for(int y = 0; y < patternSize_.height; ++y) {
            for(int x = 0; x < patternSize_.width; ++x) {
                realCorners_.emplace_back(x * square_size, y * square_size, 0.0f);
            }
        }
    }

    void addImageSample(const cv::Mat &image) { samples_.emplace_back(image); }

    void addImageSample(const std::string &filename) {
        cv::Mat mat = cv::imread(filename, cv::IMREAD_COLOR);
        if(mat.empty()) {
            std::println(stderr, "can not load filename: {}", filename);
            return;
        }
        addImageSample(mat);
    }

    bool detectCorners(const cv::Mat &image, std::vector<cv::Point2f> &corners) {
        bool found;
        switch(pattern_) {
            using enum Pattern;
            case CALIB_SYMMETRIC_CHESSBOARD_GRID: detectSymmetricChessboardGrid(image, corners, found); break;
            case CALIB_MARKER_CHESSBOARD_GRID: detectMarkerChessboardGrid(image, corners, found); break;
            case CALIB_SYMMETRIC_CIRCLES_GRID: detectSymmetricCirclesGrid(image, corners, found); break;
            case CALIB_ASYMMETRIC_CIRCLES_GRID: detectAsymmetricCirclesGrid(image, corners, found); break;
            default: break;
        }
        return found;
    }

    std::vector<std::vector<cv::Point2f>> detect() {
        std::vector<std::vector<cv::Point2f>> detectedCornerPoints;
        std::mutex mtx;  // 使用 mutex 来保护共享资源
        std::atomic<int> count;
        std::for_each(samples_.cbegin(), samples_.cend(), [&](const cv::Mat &image) {
            std::vector<cv::Point2f> corners;
            bool found = detectCorners(image, corners);
            if(found) {
                count++;
                std::lock_guard<std::mutex> lock(mtx);  // 使用 lock_guard 来保护共享资源
                detectedCornerPoints.push_back(corners);
            }
        });

        std::println("Detection successful: {} corners, total points: {}", int(count), detectedCornerPoints.size());

        return detectedCornerPoints;
    }

    std::unique_ptr<CameraCalibrationResult> calib(std::vector<std::vector<cv::Point2f>> detectedCornerPoints, int width, int height) {
        // 准备真实角点的位置
        std::vector<std::vector<cv::Point3f>> realCornerPoints;
        for(size_t i = 0; i < detectedCornerPoints.size(); ++i) {
            realCornerPoints.emplace_back(realCorners_);
        }

        cv::Size imageSize(width, height);

        // 初始化相机矩阵和畸变系数
        cv::Mat cameraMatrix = cv::Mat::eye(3, 3, CV_64F);
        cv::Mat distCoeffs   = cv::Mat::zeros(5, 1, CV_64F);
        std::vector<cv::Mat> rvecs, tvecs;

        // 进行相机标定
        double reproError = cv::calibrateCamera(realCornerPoints, detectedCornerPoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, cv::CALIB_FIX_K1 + cv::CALIB_FIX_K2 + cv::CALIB_FIX_K3 + cv::CALIB_FIX_K4 + cv::CALIB_FIX_K5);

        // 将标定结果存储到结构体中
        auto result                    = std::make_unique<CameraCalibrationResult>();
        result->cameraMatrix           = cameraMatrix;
        result->distortionCoefficients = distCoeffs;
        result->reprojectionError      = reproError;
        result->rotationVectors        = rvecs;
        result->translationVectors     = tvecs;

        return result;
    }

    // 打印标定结果
    void print(const std::unique_ptr<CameraCalibrationResult> &result) {
        std::cout << "重投影误差: " << result->reprojectionError << std::endl;
        std::cout << "相机矩阵:\n" << result->cameraMatrix << std::endl;
        std::cout << "畸变系数:\n" << result->distortionCoefficients << std::endl;
    }

    // 进行畸变校正测试
    void test(const std::string &filename, const std::unique_ptr<CameraCalibrationResult> &param) {
        // 读取一张测试图像
        cv::Mat image = cv::imread(filename);
        if(image.empty()) {
            std::println("can not load filename");
            return;
        }

        cv::Mat undistortedImage;
        cv::undistort(image, undistortedImage, param->cameraMatrix, param->distortionCoefficients);

        // 显示原图和校准后的图
        cv::namedWindow("Original Image", cv::WINDOW_NORMAL);
        cv::namedWindow("Undistorted Image", cv::WINDOW_NORMAL);
        cv::imshow("Original Image", image);
        cv::imshow("Undistorted Image", undistortedImage);

        // 等待用户输入任意键
        cv::waitKey(0);
    }

private:
    void dbgView(const cv::Mat &image, const std::vector<cv::Point2f> &corners, bool &found) {
        if(!found) {
            std::println("Cannot find corners in the image");
        }

        // Debug and view detected corner points in images
        if constexpr(false) {
            cv::drawChessboardCorners(image, patternSize_, corners, found);
            cv::namedWindow("detectCorners", cv::WINDOW_NORMAL);
            cv::imshow("detectCorners", image);
            cv::waitKey(0);
            cv::destroyAllWindows();
        }
    }

    void detectSymmetricChessboardGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
        if(found = cv::findChessboardCorners(image, patternSize_, image_corners); found) {
            cv::Mat gray;
            cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
            cv::cornerSubPix(gray, image_corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01));
            dbgView(image, image_corners, found);
        }
    }

    void detectMarkerChessboardGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
        if(found = cv::findChessboardCornersSB(image, patternSize_, image_corners); found) {
            dbgView(image, image_corners, found);
        }
    }

    void detectSymmetricCirclesGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
        if(found = cv::findCirclesGrid(image, patternSize_, image_corners, cv::CALIB_CB_SYMMETRIC_GRID); found) {
            cv::Mat gray;
            cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
            cv::cornerSubPix(gray, image_corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01));
            dbgView(image, image_corners, found);
        }
    }

    void detectAsymmetricCirclesGrid(const cv::Mat &image, std::vector<cv::Point2f> &image_corners, bool &found) {
        cv::SimpleBlobDetector::Params params;
        params.minThreshold = 8;
        params.maxThreshold = 255;

        params.filterByArea = true;
        params.minArea      = 50;    // 适当降低,以便检测小圆点
        params.maxArea      = 5000;  // 适当降低,以避免误检大区域

        params.minDistBetweenBlobs = 10;  // 调小以适应紧密排列的圆点

        params.filterByCircularity = false;  // 允许更圆的形状
        params.minCircularity      = 0.7;    // 只有接近圆的目标才被识别

        params.filterByConvexity = true;
        params.minConvexity      = 0.8;  // 只允许较凸的形状

        params.filterByInertia = true;
        params.minInertiaRatio = 0.1;  // 适应不同形状

        params.filterByColor = false;  // 关闭颜色过滤,避免黑白检测问题

        auto blobDetector = cv::SimpleBlobDetector::create(params);

        if(found = cv::findCirclesGrid(image, patternSize_, image_corners, cv::CALIB_CB_ASYMMETRIC_GRID | cv::CALIB_CB_CLUSTERING, blobDetector); found) {
            cv::Mat gray;
            cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
            cv::cornerSubPix(gray, image_corners, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.01));
            dbgView(image, image_corners, found);
        }
    }

private:
    cv::Size patternSize_;
    double squareSize_;
    Pattern pattern_;

    std::vector<cv::Point3f> realCorners_;
    std::vector<cv::Mat> samples_;
};

// 测试函数
static void test_CameraCalib() {
  // 创建一个 CameraCalib 对象,指定标定板大小、每个方格的边长和校准模式
  CameraCalib calib(14, 9, 12.1, CameraCalib::Pattern::CALIB_MARKER_CHESSBOARD_GRID);

  // 加载图像样本
  std::vector<cv::String> result;
  cv::glob("calibration_images/*.png", result, false);
  for (auto &&filename : result) {
    calib.addImageSample(filename);
  }

  // 检测角点
  auto detectedCornerPoints = calib.detect();

  // 进行相机标定
  std::string filename = "calibration_images/checkerboard_radon.png";
  cv::Mat image = cv::imread(filename);
  if (image.empty()) {
    std::println("can not load image");
    return;
  }

  auto param = calib.calib(detectedCornerPoints, image.cols, image.cols);

  // 打印标定结果
  calib.print(param);

  // 测试函数
  calib.test(filename, param);
}

运行测试函数,输出结果如下所示:

Detection successful: 2 corners, total points: 2
重投影误差: 0.0373256
相机矩阵:
[483030.3184975122, 0, 1182.462802265994;
 0, 483084.13533141, 1180.358683128085;
 0, 0, 1]
畸变系数:
[0;
 0;
 -0.002454905573938355;
 9.349667940808669e-05;
 0]

 // 保存标定结果
cv::FileStorage fs("calibration_result.yml", cv::FileStorage::WRITE);
fs << "camera_matrix" << result.cameraMatrix;
fs << "distortion_coefficients" << result.distCoeffs;
fs << "image_size" << result.imageSize;
fs.release();
相关推荐
春末的南方城市1 小时前
Stability AI 联合 UIUC 提出单视图 3D 重建方法SPAR3D,可0.7秒完成重建并支持交互式用户编辑。
人工智能·计算机视觉·3d·aigc·音视频·图像生成
x132572729262 小时前
AI直播的未来:智能化、自动化与个性化并存
人工智能·自动化·语音识别
Elastic 中国社区官方博客3 小时前
如何在 Elasticsearch 中设置向量搜索 - 第二部分
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
深耕云原生4 小时前
MAAS | DeepSeek本地部署如何开启联网搜索?
人工智能·deepseek
weixin_409411024 小时前
面向生成式语言模型场景到底是选择4卡5080还是选择两卡5090D
人工智能·语言模型·自然语言处理
姚瑞南4 小时前
美团智能外呼机器人意图训练全流程
人工智能·机器人
cnbestec4 小时前
Hello Robot 推出Stretch 3移动操作机器人,赋能研究与商业应用
人工智能·机器人
.Net Core 爱好者5 小时前
基于Flask搭建AI应用,本地私有化部署开源大语言模型
人工智能·后端·python·语言模型·自然语言处理·flask
思茂信息5 小时前
CST的TLM算法仿真5G毫米波阵列天线及手机
网络·人工智能·5g·智能手机·软件工程·软件构建