OpenCV图像处理——C++实现亚像素尺寸标定板边缘轮廓提取

news2024/11/20 1:45:08

前言

标定模板(Calibration Target)在机器视觉、图像测量、摄影测量以及三维重建等应用中起着重要的作用。它被用于校正相机的畸变,确定物理尺寸和像素之间的换算关系,并建立相机成像的几何模型。通过使用相机拍摄带有固定间距图案阵列的平板,并经过标定算法的计算,我们能够得到相机的几何参数,从而获得高精度的测量和重建结果。

这种标定模板通常具有固定间距的图案阵列,例如棋盘格、圆点阵列等。这些图案提供了已知尺寸的参考点,使得可以精确地计算相机的内参(内部参数)和外参(外部参数)。内参包括焦距、主点等相机内部的属性,而外参包括相机的位置和方向等外部属性。

通过对标定模板进行拍摄和相应的标定过程,可以消除图像中的畸变效应,实现对物体三维几何位置与其在图像中对应点之间的精确关联。这为各种应用场景提供了可靠的基础,包括计算机视觉任务、图像测量、摄影测量以及三维重建等。标定模板的使用有助于提高测量和重建的准确性,并确保获得可靠的结果。
这是使用的标定板是49个圆点的尺寸标定板,为是更好的演示亚像素,所拍的图像大小为320240pix,标定板的大小为130130pix。

在这里插入图片描述

边缘检测

在计算机视觉和机器视觉领域,广泛应用图像分割,这是将数字图像细分为多个子区域的过程。图像分割的目标是简化或改变图像的表示形式,使其更易于理解和分析。这通常包括定位图像中的物体和边界,生成图像子区域或轮廓线的集合。图像分割方法主要涵盖阈值处理(二值化)、聚类法、边缘检测和区域生长等。解决图像分割问题时,通常需要结合领域知识,因为没有统一的范式可以适用于所有情境。

边缘检测是一种基于灰度变化的常见图像分割方法,其核心是提取图像中不连续部分的特征。常见的边缘检测算子包括差分算子、Roberts算子、Sobel算子、Prewitt算子、Log算子以及Canny算子等。其中,Canny算子由计算机科学家John F. Canny于1986年提出,被认为是目前理论上相对最完善的边缘检测算法。
这里面使用了Canny来进行边缘检测,Canny边缘检测是一种经典的图像处理技术,用于检测图像中的边缘。这个算法由约翰·Canny于1986年提出,被广泛应用于计算机视觉和图像处理领域。以下是Canny边缘检测的基本原理和步骤:

  1. 噪声抑制: 使用高斯滤波器对图像进行平滑处理,以减少噪声的影响。这有助于提高后续边缘检测步骤的准确性。

  2. 计算梯度幅值和方向: 利用Sobel等算子计算图像中每个像素点的梯度幅值和方向。梯度表示图像中的变化率,有助于确定潜在的边缘位置。

  3. 非极大值抑制: 在图像的每个像素点上,仅保留梯度幅值最大的方向,从而细化边缘。

  4. 双阈值检测: 将梯度幅值分为高阈值和低阈值两部分。高于高阈值的像素点被认为是强边缘,低于低阈值的像素点被排除。介于两者之间的像素点被标记为弱边缘。

  5. 边缘连接: 通过强边缘的连接,将弱边缘与强边缘关联起来,形成连续的边缘。

Canny边缘检测的优势在于其高准确性、低误差率和对边缘的单一定位。

	cv::Mat cv_gray;
    if (cv_src.channels() > 1)
    {
        cv::cvtColor(cv_src, cv_gray, cv::COLOR_BGR2GRAY);
    }
    else
    {
        cv_gray = cv_src.clone();
    }
    cv::Mat cv_edge;

    //1.边缘检测
    cv::Canny(cv_gray, cv_edge, 180, 200);

    cv::imshow("edge", cv_edge);
    cv::waitKey();

canny的结果:
在这里插入图片描述

轮廓提取

轮廓是由一系列相连的像素点组成的曲线,反映了物体的基本外形特征。常常用于形状分析以及物体的检测和识别任务。
边缘检测通过检测灰度的突变来寻找图像边界,但通常检测到的边缘是零散的片段,没有形成完整的整体。为了从背景中分离目标,需要将这些边缘像素连接起来形成轮廓。换句话说,轮廓是一种连续的曲线,而边缘未必都是连续的。边缘主要被用作图像的特征,而轮廓则更多地用于分析物体的整体形态。
cv::findContours 是OpenCV库中用于查找图像中轮廓的函数。它的基本用法是在二进制图像中找到对象的轮廓,返回一个包含轮廓点集合的列表。
然而,在使用cv::findContours时,会产生内外两个轮廓,为了减少内部,要对获取的轮廓进行进一步的处理:

**
 * @brief 在二值化边缘图像中检测和分离内部和外部轮廓。
 *
 * @param cv_edge 二值化边缘图像。
 * @param inner_contours 用于存储内部轮廓的向量。
 */
void contour(cv::Mat& cv_edge, std::vector<std::vector<cv::Point>>& inner_contours)
{
    // 存储所有检测到的轮廓的向量
    std::vector<std::vector<cv::Point>> contours;
    // 存储轮廓之间层次关系的层次向量
    std::vector<cv::Vec4i> hierarchy;

    // 在二值化边缘图像中查找轮廓
    cv::findContours(cv_edge, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_NONE);

    // 存储有效轮廓和被排除轮廓的索引的向量
    std::vector<int> valids;
    std::vector<int> excludes;

    // 根据层次关系减少内部轮廓
    edge_contour_valids(hierarchy, valids, excludes);

    // 调整 inner_contours 向量的大小以容纳有效轮廓
    inner_contours.resize(valids.size());

    // 将有效轮廓的索引映射到相应的轮廓,并存储在 inner_contours 中
    std::transform(std::execution::par,
        valids.begin(),
        valids.end(),
        inner_contours.begin(),
        [contours](int i) { return contours[i]; }
    );

    // 存储外部轮廓的向量
    std::vector<std::vector<cv::Point>> external_contours(excludes.size());

    // 将被排除轮廓的索引映射到相应的轮廓,并存储在 external_contours 中
    std::transform(std::execution::par,
        excludes.begin(),
        excludes.end(),
        external_contours.begin(),
        [contours](int i) { return contours[i]; }
    );
}

得到轮廓之后,要获取标定板圆前先获取轮廓的矩:

/**
 * @brief 计算轮廓的属性并进行阈值化,筛选符合条件的轮廓。
 *
 * @param inner_contours 存储内部轮廓的向量。
 * @param cv_cont_and 存储轮廓属性的阈值化结果。
 * @param cont_center 存储轮廓中心点的向量。
 * @param RADIUS_MAX 最大半径阈值。
 */
void contours_attribute(std::vector<cv::Point> &cont_center, std::vector<std::vector<cv::Point>> &inner_contours,
    cv::Mat & cv_cont_and,double RADIUS_MAX)
{
    // 获取轮廓的长度
    std::vector<double> cont_lengths;
    contour_length(inner_contours, cont_lengths);

    // 存储轮廓的半径和纵横比
    std::vector<double> cont_ra, cont_aspect_ratio;
    cv::Mat_<double> cont_length_mat(cont_lengths);

    // 计算轮廓中心点和其他属性
    for (const auto& c : inner_contours)
    {
        auto M = cv::moments(c);

        double area = M.m00;
        auto centerX = int(M.m10 / area);
        auto centerY = int(M.m01 / area);
        double m20 = M.mu20 / area;
        double m02 = M.mu02 / area;
        double m11 = M.mu11 / area;
        double c1 = m20 - m02;
        double c2 = c1 * c1;
        double c3 = 4 * m11 * m11;

        cont_center.emplace_back(centerX, centerY);

        auto ra = sqrt(2.0 * (m20 + m02 + sqrt(c2 + c3)));
        auto rb = sqrt(2.0 * (m20 + m02 - sqrt(c2 + c3)));
        cont_ra.emplace_back(ra);
        cont_aspect_ratio.emplace_back(ra / rb);
    }

    // 将属性向量转为 OpenCV 的 Mat 类型
    cv::Mat_<double> cv_cont_radius(cont_ra);
    cv::Mat_<double> cv_cont_aspect_ratio(cont_aspect_ratio);

    // 定义阈值和计算最大轮廓长度
    cv::Mat cv_aspect_ratio, cv_radius, cv_cont_length;
    const double CONT_LENGTH_MAX = 2 * M_PI * RADIUS_MAX;

    // 对纵横比、半径和轮廓长度进行阈值化
    cv::threshold(cv_cont_aspect_ratio, cv_aspect_ratio, 0.8, 1.0, cv::ThresholdTypes::THRESH_BINARY);
    cv::threshold(cv_cont_radius, cv_radius, RADIUS_MAX, 1.0, cv::ThresholdTypes::THRESH_BINARY_INV);
    cv::threshold(cont_length_mat, cv_cont_length, CONT_LENGTH_MAX, 1.0, cv::ThresholdTypes::THRESH_BINARY_INV);

    // 将阈值结果按位与,存储在 cv_cont_and 中
    cv::Mat and1;
    cv::bitwise_and(cv_aspect_ratio, cv_radius, and1);
    cv::bitwise_and(and1, cv_cont_length, cv_cont_and);
}

在这里插入图片描述

亚像素轮廓提取

使用亚像素插值,获得轮廓的亚像素级坐标:

/**
 * @brief 对图像进行亚像素插值,获得轮廓的亚像素级坐标。
 *
 * @param image_gray 灰度图像。
 * @param filteredCont 过滤后的轮廓向量。
 * @param contSubPixFull 存储亚像素级坐标的向量的向量。
 */
void sub_pix_contour(const cv::Mat& image_gray,
    const std::vector<std::vector<cv::Point>>& filteredCont,
    std::vector<std::shared_ptr<std::vector<std::shared_ptr<cv::Point2d>>>>& contSubPixFull)
{
    // 定义用于插值的系数
    std::vector<double> p_vec{ 0.004711,  0.069321,  0.245410,  0.361117,  0.245410,  0.069321,  0.004711 };
    std::vector<double> d1_vec{ -0.018708,  -0.125376,  -0.193091,  0.000000, 0.193091, 0.125376, 0.018708 };
    std::vector<double> d2_vec{ 0.055336,  0.137778, -0.056554, -0.273118, -0.056554,  0.137778,  0.055336 };

    // 将系数转为 Mat 类型
    auto p = cv::Mat_<double>(p_vec);
    auto d1 = cv::Mat_<double>(d1_vec);
    auto d2 = cv::Mat_<double>(d2_vec);

    // 计算图像梯度
    cv::Mat dx, dy, grad;
    cv::sepFilter2D(image_gray, dy, CV_64F, p, d1);
    cv::sepFilter2D(image_gray, dx, CV_64F, d1, p);
    cv::pow(dy.mul(dy, 1.0) + dx.mul(dx, 1.0), 0.5, grad);

    // 计算更高阶导数
    cv::Mat gy, gx, gyy, gxx, gxy;
    cv::sepFilter2D(grad, gy, CV_64F, p, d1);
    cv::sepFilter2D(grad, gx, CV_64F, d1, p);
    cv::sepFilter2D(grad, gyy, CV_64F, p, d2);
    cv::sepFilter2D(grad, gxx, CV_64F, d2, p);
    cv::sepFilter2D(grad, gxy, CV_64F, d1, d1);

    // 对每个轮廓进行亚像素插值
    contSubPixFull.resize(filteredCont.size());
    std::transform(std::execution::par,
        filteredCont.cbegin(),
        filteredCont.cend(),
        contSubPixFull.begin(),
        [&gy, &gx, &gyy, &gxx, &gxy](const std::vector<cv::Point>& cont)
        {
            return sub_pix_single(gy, gx, gyy, gxx, gxy, cont);
        }
    );
}

把亚像素轮廓坐标显示:

void SubPixEdge::draw_roi(cv::Mat& cv_src, std::vector<std::vector<cv::Point>>& filtered_cont,
    std::vector<std::shared_ptr<std::vector<std::shared_ptr<cv::Point2d>>>>& cont_sub_pix,bool show_roi)
{
    const int up_scale = 50;

    cv::Mat cv_temp = cv_src.clone();
    int src_up_w = up_scale * cv_temp.rows;
    int src_up_h = up_scale * cv_temp.cols;

    cv::Mat cv_src_up = cv::Mat::zeros(src_up_w, src_up_h, CV_8UC3);

    for (int i = 0; i < cv_temp.cols; ++i)
    {
        for (int j = 0; j < cv_temp.rows; ++j)
        {
            cv::Mat roi = cv_src_up(cv::Rect(i * up_scale, j * up_scale, up_scale, up_scale));
            roi = cv_temp.at<cv::Vec<uchar, 3>>(j, i);
        }
    }

    
    std::vector<std::vector<cv::Point>> cont;
    for (int i = 0; i < cont_sub_pix.size(); ++i)
    {
        std::vector<cv::Point> dis_cont;
        for (const auto& p : *cont_sub_pix[i])
        {
            //使用 floor() 函数向下取整
            int x = floor((p->x + 0.5) * up_scale);
            int y = floor((p->y + 0.5) * up_scale);
            cv::drawMarker(cv_src_up, cv::Point(x, y), cv::Scalar(0, 0, 255), cv::MARKER_TILTED_CROSS, 20, 5);
            dis_cont.emplace_back(x, y);
        }
        cont.emplace_back(dis_cont);
    }
    
    cv::drawContours(cv_src_up, cont, -1, cv::Scalar(255, 0, 0), 5);
    
    cv::drawContours(cv_src, filtered_cont, -1, cv::Scalar(255, 0, 0));
    
    if (show_roi)
    {
        for (int i = 0; i < filtered_cont.size(); ++i)
        {
            cv::Rect rect = cv::boundingRect(filtered_cont[i]);

            cv::Mat crop = cv_src(rect);
            if (crop.empty())
            {
                continue;
            }

            int up_w = up_scale * rect.width;
            int up_h = up_scale * rect.height;


            cv::Mat cv_up = cv::Mat::zeros(up_w, up_h, CV_8UC3);

            for (int i = 0; i < crop.cols; ++i)
            {
                for (int j = 0; j < crop.rows; ++j)
                {
                    cv::Mat roi = cv_up(cv::Rect(i * up_scale, j * up_scale, up_scale, up_scale));
                    roi = crop.at<cv::Vec<uchar, 3>>(j, i);
                }
            }


            std::vector<cv::Point> dis_cont;

            for (const auto& p : *cont_sub_pix[i])
            {
                //使用 floor() 函数向下取整
                int x = floor(((p->x - rect.x) + 0.5) * up_scale);
                int y = floor(((p->y - rect.y) + 0.5) * up_scale);
                cv::drawMarker(cv_up, cv::Point(x, y), cv::Scalar(0, 0, 255), cv::MARKER_TILTED_CROSS, 20, 3);
                dis_cont.emplace_back(x, y);
            }

            std::vector<std::vector<cv::Point>> display_contour_full{ dis_cont };
            cv::drawContours(cv_up, display_contour_full, 0, cv::Scalar(255.0, 0.0, 0.0), 3);

            cv::namedWindow("upScaled", 0);
            cv::imshow("upScaled", cv_up);
            cv::waitKey();
        }
    }

    cv::namedWindow("src", 0);
    cv::imshow("src", cv_src);
    cv::namedWindow("sub_pix_src", 0);
    cv::imshow("sub_pix_src", cv_src_up);
    cv::waitKey();
}

左边为像素级边缘轮廓,右边为亚像素边缘轮廓:
在这里插入图片描述

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

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

相关文章

基于simiulink的flyback反激型电路建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 Flyback反激型电路的基本原理 4.2 Flyback反激型电路的数学建模 4.3 Flyback反激型电路的仿真方法 5.完整工程文件 1.课题概述 flyback反激型电路建模与仿真。反激变换器在开关管导通时电源将电能…

IDEA2023 最新版详细图文安装教程(安装+运行测试+汉化+背景图设置)

IDEA2023 最新版详细图文安装教程 名人说&#xff1a;工欲善其事&#xff0c;必先利其器。——《论语》 作者&#xff1a;Code_流苏(CSDN) o(‐&#xff3e;▽&#xff3e;‐)o很高兴你打开了这篇博客&#xff0c;跟着教程去一步步尝试安装吧。 目录 IDEA2023 最新版详细图文安…

LiveGBS流媒体平台GB/T28181常见问题-如何配置快照目录快照存储默认目录目录如何配置

LiveGBS流媒体平台GB/T28181常见问题-如何配置快照目录快照存储默认目录目录如何配置 1、快照目录2、指定快照目录3、搭建GB28181视频直播平台 1、快照目录 部署LiveGBS后&#xff0c; 再查看通道播放后 或是 获取通道快照后&#xff0c;就会在LiveSMS部署的服务器里面存储对应…

大文件断点下载Range下载zip包显示文件损坏

问题&#xff1a;大文件下载&#xff0c;其它格式的文件及rar格式的压缩包正常下载但是 之后zip包下载后解压失败 原因分析: 1. 查看上传文件的属性值 如图&#xff0c;10.4kb是已经约去小数点的值&#xff0c;准确的大小应该是10663字节10.4130859375KB&#xff0c;所以用10.…

C#使用纯OpenCvSharp部署yolov8-pose姿态识别

【源码地址】 github地址&#xff1a;https://github.com/ultralytics/ultralytics 【算法介绍】 Yolov8-Pose算法是一种基于深度神经网络的目标检测算法&#xff0c;用于对人体姿势进行准确检测。该算法在Yolov8的基础上引入了姿势估计模块&#xff0c;通过联合检测和姿势…

解决Android Studio The path ‘X:\XXX‘ does not belong to a directory.

目录 前言 一、问题描述 二、解决方法 前言 在移动应用开发领域&#xff0c;Android Studio作为一款功能强大的集成开发环境&#xff0c;为开发人员提供了丰富的工具和功能。然而&#xff0c;在使用Android Studio的过程中&#xff0c;有时也会遇到各种各样的问题和错误。 &…

彻底理解前端安全面试题(2)—— CSRF 攻击,跨站请求伪造攻击详解,建议收藏(含源码)

前言 前端关于网络安全看似高深莫测&#xff0c;其实来来回回就那么点东西&#xff0c;我总结一下就是 3 1 4&#xff0c;3个用字母描述的【分别是 XSS、CSRF、CORS】 一个中间人攻击。当然 CORS 同源策略是为了防止攻击的安全策略&#xff0c;其他的都是网络攻击。除了这…

以 RoCE+软件定义存储同时实现信创转型与架构升级

目前&#xff0c;不少企业数据中心使用 FC 交换机和集中式 SAN 存储&#xff08;以下简称“FC-SAN 架构”&#xff09;&#xff0c;支持核心业务系统、数据库、AI/ML 等高性能业务场景。而在开展 IT 基础架构信创转型时&#xff0c;很多用户受限于国外交换机&#xff1a;FC 交换…

2015年AMC8数学竞赛中英文真题典型考题、考点分析和答案解析

今天我们来看2015年的AMC8竞赛真题的典型考题和解析&#xff0c;最后利用碎片化时间冲刺&#xff0c;查漏补缺&#xff0c;理解考试。温馨提示&#xff1a;2024年AMC8比赛现在还可以报名&#xff0c;自由报名截止到1月7日&#xff0c;我这里有官方自由报名通道。 2015年AMC8数学…

对基于深度神经网络的Auto Encoder用于异常检测的一些思考

一、前言 现实中&#xff0c;大部分数据都是无标签的&#xff0c;人和动物多数情况下都是通过无监督学习获取概念&#xff0c;故而无监督学习拥有广阔的业务场景。举几个场景&#xff1a;网络流量是正常流量还是攻击流量、视频中的人的行为是否正常、运维中服务器状态是否异常…

Django Web框架

1、创建PyCharm项目 2、安装框架 pip install django4.2.0 3、查看安装的包列表 4、使用命令创建django项目 django-admin startproject web 5、目录结构 6、运行 cd web python manage.py runserver7、初始化后台登录的用户名密码 执行数据库迁移生成数据表 python man…

自定义指令:让 Vue 开发更有趣(中)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

一年中ChatGPT使用情况

介绍 本人是独立开源软件开发者&#xff0c;参与很多项目建设&#xff0c;谈下日常使用情况。 我用了一年多&#xff0c;现在已经离不开&#xff0c;我如指挥家&#xff0c;它是我最忠诚的乐手。 编码 GitHub Copilot&#xff1a;GitHub Copilot是GitHub和OpenAI合作开发的一…

unity C# 中通俗易懂LINQ使用案例

文章目录 1. 从数组或列表中查询元素**&#xff1a;2. **排序与分组**&#xff1a;3. **连接多个数据源**&#xff1a;4. **聚合操作**&#xff1a;5. **分页查询**&#xff1a;6. **多条件查询**&#xff1a;7. **转换和投影&#xff08;Select&#xff09;**&#xff1a;8. *…

[C#]使用onnxruntime部署Detic检测2万1千种类别的物体

【源码地址】 github地址&#xff1a;https://github.com/facebookresearch/Detic/tree/main 【算法介绍】 Detic论文&#xff1a;https://arxiv.org/abs/2201.02605v3 项目源码&#xff1a;https://github.com/facebookresearch/Detic 在Detic论文中&#xff0c;Detic提到…

SpringBoot整合Elasticsearch报错

本文来记录一下SpringBoot整合Elasticsearch报错 文章目录 报错如下报错原因es7.15.2版本下载 报错如下 报错如下 2024-01-02 15:09:10.349 ERROR 134936 --- [nio-8088-exec-6] o.a.c.c.C.[.[.[/]. [dispatcherServlet] : Servlet.service() for servlet [dispatcherServle…

华清远见作业第十九天——IO(第二天)

思维导图&#xff1a; 使用fread、fwrite完成两个文件的拷贝 代码&#xff1a; #include<stdio.h> #include<string.h> #include<stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, const…

kubeadm来快速搭建一个K8S集群

二进制搭建适合大集群&#xff0c;50台以下的主机 kubeadm更适合中下企业的业务集群 我们采用了二进制包搭建出的k8s集群&#xff0c;本次我们采用更为简单的kubeadm的方式来搭建k8s集群。 二进制的搭建更适合50台主机以上的大集群&#xff0c;kubeadm更适合中小型企业的集群…

如何保障集团下达的政策要求有效落地

随着新一轮国企改革的推进&#xff0c;很多国有企业建立了集团化的管控体系。通过集团化经营管理的模式&#xff0c;帮助国有企业凝聚更强的竞争力&#xff0c;集团企业通过资源整合、反向投资、控股、参股等手法创造业务板块之间的协同、互补效应&#xff0c;从而实现战略联动…

Redis(二)数据类型

文章目录 官网备注十大数据类型StringListHashSetZSetBitmapHyperLogLog&#xff1a;GEOStreamBitfield 官网 英文&#xff1a;https://redis.io/commands/ 中文&#xff1a;http://www.redis.cn/commands.html 备注 命令不区分大小写&#xff0c;key区分大小写帮助命令help…