OpenCV 相机标定流程指南

news2025/2/12 22:31:15
  • 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();

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络在线考试|基于vue的网络在线考试系统的设计与实现(源码+数据库+文档)

网络在线考试系统 目录 基于SSM&#xff0b;vue的网络在线考试系统的设计与实现 一、前言 二、系统设计 三、系统功能设计 1功能页面实现 2系统功能模块 3管理员功能模块 4学生功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八…

DEEPSEEK与GPT等AI技术在机床数据采集与数字化转型中的应用与影响

随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;深度学习、自然语言处理等先进技术开始广泛应用于各行各业。在制造业尤其是机床行业&#xff0c;AI技术的融合带来了巨大的变革&#xff0c;尤其在机床数据采集与机床数字化方面的应用。本文将探讨DEEPSEEK、…

【文本处理】如何在批量WORD和txt文本提取手机号码,固话号码,提取邮箱,删除中文,删除英文,提取车牌号等等一些文本提取固定格式的操作,基于WPF的解决方案

企业的应用场景 数据清洗&#xff1a;在进行数据导入或分析之前&#xff0c;往往需要对大量文本数据进行预处理&#xff0c;比如去除文本中的无关字符&#xff08;中文、英文&#xff09;&#xff0c;只保留需要的联系信息&#xff08;手机号码、固话号码、邮箱&#xff09;。…

17vue3实战-----使用配置文件生成简易页面

17vue3实战-----使用配置文件生成简易页面 1.写在前面2.背景3.实现3.1界面效果3.2新建config配置文件3.3封装组件3.4使用组件 1.写在前面 后台管理系统的开发很简单。无论是用户模块、部门模块、角色模块还是其它模块,界面和业务逻辑都相对比较简单&#xff0c;我会省略这些模…

“mysqld --initialize --console ”执行不成功情况总结和解决措施

我的MYSQL版本是9.0.1出现类似下列的报错&#xff1a; 2024-10-29T01:09:55.942951Z 0 [System] [MY-015017] [Server] MySQL Server Initialization - start. 2024-10-29T01:09:55.950379Z 0 [Warning] [MY-010915] [Server] NO_ZERO_DATE, NO_ZERO_IN_DATE and ERROR_FOR_DIV…

STM32 Unix时间戳

Unix时间戳 Unix 时间戳&#xff08;Unix Timestamp&#xff09;定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数&#xff0c;不考虑闰秒 时间戳存储在一个秒计数器中&#xff0c;秒计数器为32位/64位的整型变量 世界上所有时区的秒计数器相同&#xff0c;不同时区通过…

qwen2.5-vl-7B视觉大模型 私有化部署webUI

服务器选用&#xff1a;算力云 部署qwen2.5-vl-7B&#xff0c;24g显卡跑不起图&#xff0c;单问问题就占20g左右。有能力可以用大点的显卡 一、下载模型 Qwen2.5-VL-7B-Instruct 有conda &#xff0c;可以在conda下操作&#xff0c;不知道conda的同学可以参考本博主之前的文章…

java安全中的类加载

java安全中的类加载 提前声明: 本文所涉及的内容仅供参考与教育目的&#xff0c;旨在普及网络安全相关知识。其内容不代表任何机构、组织或个人的权威建议&#xff0c;亦不构成具体的操作指南或法律依据。作者及发布平台对因使用本文信息直接或间接引发的任何风险、损失或法律纠…

如何在Windows中配置MySQL?

MySQL是一个广泛使用的开源关系型数据库管理系统&#xff0c;它支持多种操作系统平台&#xff0c;其中包括Windows。无论是开发者进行本地开发&#xff0c;还是管理员为应用程序配置数据库&#xff0c;MySQL都是一个非常流行的选择。本篇文章将详细介绍如何在Windows操作系统中…

Docker Desktop 镜像源配置

1 打开配置页面 2 docker engine 镜像配置位置 3、替换镜像内容 {"registry-mirrors": ["https://hub-mirror.c.163.com","https://mirror.ccs.tencentyun.com","https://05f073ad3c0010ea0f4bc00b7105ec20.mirror.swr.myhuaweicloud.c…

125,【1】攻防世界unserialize3

进入靶场 代码 <?php // 定义一个名为 xctf 的类 class xctf {// 定义一个公共属性 $flag&#xff0c;初始值为字符串 111public $flag 111;// 定义 __wakeup() 魔术方法// 当使用 unserialize() 函数反序列化对象时&#xff0c;会自动调用 __wakeup() 方法// 在这个方法…

2025年数据资产管理解决方案:资料合集,从基础知识到行业应用的全面解析

在数字化时代&#xff0c;数据已成为企业最宝贵的资产之一。如何有效地管理和利用这些数据&#xff0c;将其转化为实际的经济价值&#xff0c;已成为企业面临的重要课题。 本文将通过数据资产解决方案、数据资产行业报告白皮书、数据资产政策汇编、数据资产基础知识以及数据资…

朝天椒USB服务器:解决加密狗远程连接

本文探讨朝天椒USB服务器用Usb Over Network技术&#xff0c;解决加密狗在虚拟机、云主机甚至异地的远程连接问题。 在企业数字化转型的浪潮中&#xff0c;加密狗作为防止软件盗版的重要手段&#xff0c;广泛应用于各类软件授权场景。然而&#xff0c;随着企业超融合进程不断加…

汽车与AI深度融合:CES Asia 2025前瞻

在科技飞速发展的当下&#xff0c;汽车与AI的融合正成为行业变革的关键驱动力。近日&#xff0c;吉利、极氪、岚图、智己等多家车企纷纷官宣与DeepSeek模型深度融合&#xff0c;其中岚图知音更是将成为首个搭载该模型的量产车型&#xff0c;这无疑是汽车智能化进程中的重要里程…

数据结构与算法-单链表

链表 参考学习&#xff1a;B站-逊哥带你学编程 单链表 单链表-存储结构 typedef int ElemType;typedef struct node{ElemType data;struct node *next; }Node;单链表-初始化 Node *initList() {Node *head (Node *)malloc(sizeof(Node));head->data 0;head->next …

ASP.NET Core 如何使用 C# 向端点发出 POST 请求

使用 C#&#xff0c;将 JSON POST 到 REST API 端点&#xff1b;如何从 REST API 接收 JSON 数据。 本文需要 ASP .NET Core&#xff0c;并兼容 .NET Core 3.1、.NET 6和.NET 8。 要从端点获取数据&#xff0c;请参阅本文。 使用 . 将 JSON 数据发布到端点非常容易HttpClien…

DeepSeek模型R1服务器繁忙,怎么解决?

在当今科技飞速发展的时代&#xff0c;人工智能领域不断涌现出令人瞩目的创新成果&#xff0c;其中DeepSeek模型无疑成为了众多关注焦点。它凭借着先进的技术和卓越的性能&#xff0c;在行业内掀起了一股热潮&#xff0c;吸引了无数目光。然而&#xff0c;如同许多前沿技术在发…

GlusterFS 深度洞察:从架构原理到案例实践的全面解读(上)

文章目录 一.GlusterFS简介二.GlusterFS原理架构三.适用场景四.Glusterfs与其他存储产品对比五.部署GlusterFS集群六. 使用heketi将glusterfs接入k8s作为后端存储 一.GlusterFS简介 GlusterFS是一个免费的开源分布式文件系统&#xff0c;具有无中心节点、堆栈式设计、全局统一…

更新无忧:用 Docker 数据卷确保 Open WebUI 数据持久化

在使用 Docker 部署 Open WebUI 时&#xff0c;如何在更新容器的同时确保数据不丢失&#xff0c;始终是工程师们关注的焦点。每次拉取新版镜像、停止并重启容器时&#xff0c;如果没有正确挂载数据卷&#xff0c;配置和数据库数据极易流失&#xff0c;给生产环境带来不必要的麻…

zyNo.22

常见Web漏洞解析 命令执行漏洞 1.Bash与CMD常用命令 &#xff08;1&#xff09;Bash 读取文件&#xff1a;最常见的命令cat flag 在 Bash 中&#xff0c;cat 以及的tac、nl、more、head、less、tail、od、pr 均为文件读取相关命令&#xff0c;它们的区别如下&#xff1a; …