OpenCV 相机标定流程指南

  • 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/

源代码

#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();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/968228.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Windows】PowerShell 缓存区大小调节

PowerShell 缓存区大小调节 方式1 打开powershell 窗口属性调节方式2&#xff0c;修改 PowerShell 配置文件 方式1 打开powershell 窗口属性调节 打开 CMD&#xff08;按 Win R&#xff0c;输入 cmd&#xff09;。右键标题栏 → 选择 属性&#xff08;Properties&#xff09;…

127,【3】 buuctf [NPUCTF2020]ReadlezPHP

进入靶场 吓我一跳 查看源码 点击 审计 <?php// 定义一个名为 HelloPhp 的类&#xff0c;该类可能用于执行与日期格式化相关的操作 class HelloPhp {// 定义一个公共属性 $a&#xff0c;用于存储日期格式化的模板public $a;// 定义一个公共属性 $b&#xff0c;用于存储…

问题:通过策略模式+工厂模式+模板方法模式实现ifelse优化

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 示例&#xff1a;商城系统有会员系统&#xff0c;不同会员有不同优惠程度&#xff0c;普通会员不优惠&#xff1b;黄金会员打8折&#xff1b;白金会员优惠50元&#xff0c;再打7折&#xff1b; 问题描…

Android Studio2024版本安装环境SDK、Gradle配置

一、软件版本&#xff0c;安装包附上 &#x1f449;android-studio-2024.1.2.12-windows.exe&#x1f448; &#x1f449;百度网盘Android Studio安装包&#x1f448; &#xff08;若下载连链接失效可去百度网盘链接下载&#xff09; 二、软件安装过程 ​ ​ ​ 三、准备运行…

Leetcode - 149双周赛

目录 一、3438. 找到字符串中合法的相邻数字二、3439. 重新安排会议得到最多空余时间 I三、3440. 重新安排会议得到最多空余时间 II四、3441. 变成好标题的最少代价 一、3438. 找到字符串中合法的相邻数字 题目链接 本题有两个条件&#xff1a; 相邻数字互不相同两个数字的的…

使用 meshgrid函数绘制网格点坐标的原理与代码实现

使用 meshgrid 绘制网格点坐标的原理与代码实现 在 MATLAB 中&#xff0c;meshgrid 是一个常用函数&#xff0c;用于生成二维平面网格点的坐标矩阵。本文将详细介绍如何利用 meshgrid 函数生成的矩阵绘制网格点的坐标&#xff0c;并给出具体的代码实现和原理解析。 实现思路 …

【AI赋能】蓝耘智算平台实战指南:3步构建企业级DeepSeek智能助手

蓝耘智算平台实战指南&#xff1a;3步构建企业级DeepSeek智能助手 引言&#xff1a;AI大模型时代的算力革命 在2025年全球AI技术峰会上&#xff0c;DeepSeek-R1凭借其开源架构与实时推理能力&#xff0c;成为首个通过图灵测试的中文大模型。该模型在语言理解、跨模态交互等维…

Mac(m1)本地部署deepseek-R1模型

1. 下载安装ollama 直接下载软件&#xff0c;下载完成之后&#xff0c;安装即可&#xff0c;安装完成之后&#xff0c;命令行中可出现ollama命令 2. 在ollama官网查看需要下载的模型下载命令 1. 在官网查看deepseek对应的模型 2. 选择使用电脑配置的模型 3. copy 对应模型的安…

第七节 文件与流

基本的输入输出&#xff08;iostream&#xff09; C标准库提供了一组丰富的输入/输出功能&#xff0c;C的I/O发生在流中&#xff0c;流是字节序列。如果字节流是从设备&#xff08;键盘、磁盘驱动器、网络连接等&#xff09;流向内存&#xff0c;叫做输入操作。如果字节流是从…

网络安全溯源 思路 网络安全原理

网络安全背景 网络就是实现不同主机之间的通讯。网络出现之初利用TCP/IP协议簇的相关协议概念&#xff0c;已经满足了互连两台主机之间可以进行通讯的目的&#xff0c;虽然看似简简单单几句话&#xff0c;就描述了网络概念与网络出现的目的&#xff0c;但是为了真正实现两台主机…

内网ip网段记录

1.介绍 常见的内网IP段有&#xff1a; A类&#xff1a; 10.0.0.0/8 大型企业内部网络&#xff08;如 AWS、阿里云&#xff09; 10.0.0.0 - 10.255.255.255 B类&#xff1a;172.16.0.0/12 中型企业、学校 172.16.0.0 - 172.31.255.255 C类&#xff1a;192.168.0.0/16 家庭…

SQL Server 逻辑查询处理阶段及其处理顺序

在 SQL Server 中&#xff0c;查询的执行并不是按照我们编写的 SQL 语句的顺序进行的。相反&#xff0c;SQL Server 有自己的一套逻辑处理顺序&#xff0c;这个顺序决定了查询的执行方式和结果集的生成。了解这些处理阶段和顺序对于优化查询性能和调试复杂查询非常重要。 SQL …

四、OSG学习笔记-基础图元

前一章节&#xff1a; 三、OSG学习笔记-应用基础-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/145514021 代码&#xff1a;CuiQingCheng/OsgStudy - Gitee.com 一、绘制盒子模型 下面一个简单的 demo #include<windows.h> #include<osg/Node&…

性格测评小程序03搭建用户管理

目录 1 创建数据源2 搭建后台3 开通权限4 搭建启用禁用功能最终效果总结 性格测评小程序我们期望是用户先进行注册&#xff0c;注册之后使用测评功能。这样方便留存用户的联系信息&#xff0c;日后还可以推送对应的相关活动促进应用的活跃。实现这个功能我们要先创建数据源&…

Ubuntu 如何安装Snipaste截图软件

在Ubuntu上安装Snipaste-2.10.5-x86_64.AppImage的步骤如下&#xff1a; 1. 下载Snipaste AppImage 首先&#xff0c;从Snipaste的官方网站或GitHub Releases页面下载Snipaste-2.10.5-x86_64.AppImage文件。 2. 赋予执行权限 下载完成后&#xff0c;打开终端并导航到文件所在…

突破与重塑:逃离Java舒适区,借Go语言复刻Redis的自我突破和成长

文章目录 写在文章开头为什么想尝试用go复刻redis复刻redis的心路历程程序员对于舒适区的一点看法关于mini-redis的一些展望结语 写在文章开头 在程序员的技术生涯长河中&#xff0c;我们常常会在熟悉的领域中建立起自己的“舒适区”。于我而言&#xff0c;Java 就是这片承载…

【自然语言处理】TextRank 算法提取关键词、短语、句(Python源码实现)

文章目录 一、TextRank 算法提取关键词 [工具包]二、TextRank 算法提取关键短语[工具包]三、TextRank 算法提取关键句[工具包]四、TextRank 算法提取关键句&#xff08;Python源码实现&#xff09; 一、TextRank 算法提取关键词 [工具包] 见链接 【自然语言处理】TextRank 算法…

展厅为何倾向使用三维数字沙盘进行多媒体互动设计?优势探讨!

随着数字技术的迅猛进步&#xff0c;展厅多媒体互动设计正迎来深刻变革。其中&#xff0c;三维数字沙盘作为经典沙盘模型的革新之作&#xff0c;不仅保留了其空间布局直观展示的优点&#xff0c;更巧妙融入光影互动与中控系统&#xff0c;推动展览展示向智能化迈进。今日&#…

SDKMAN! 的英文全称是 Software Development Kit Manager(软件开发工具包管理器)

文章目录 SDKMAN! 的核心功能SDKMAN! 的常用命令SDKMAN! 的优势总结 SDKMAN! 的英文全称是 Software Development Kit Manager。它是一个用于管理多个软件开发工具&#xff08;如 Java、Groovy、Scala、Kotlin 等&#xff09;版本的工具。SDKMAN! 提供了一个简单的方式来安装、…

java配置api,vue网页调用api从oracle数据库读取数据

一、主入口文件 1&#xff1a;java后端端口号 2&#xff1a;数据库类型 和 数据库所在服务器ip地址 3&#xff1a;服务器用户名和密码 二、映射数据库表中的数据 resources/mapper/.xml文件 1&#xff1a;column后变量名是数据库中存储的变量名 property的值是column值的…