23.OpenCV轮廓逼近与拟合

OpenCV轮廓逼近与拟合

在计算机视觉中,轮廓是图像中边界或形状的重要表达形式。然而,直接从图像中提取的轮廓常常包含大量冗余点,且噪声较多。为了更好地描述图像中的形状,我们通常需要对轮廓进行逼近和拟合,从而降低数据维度、获得光滑的形状描述,并便于后续分析处理。本文将介绍轮廓逼近与拟合的基本原理,在 OpenCV 中如何使用相关函数(如 approxPolyDPfitLineboundingRectminEnclosingCirclefitEllipse 等)实现这些操作,并通过代码示例展示实际应用。

1. 轮廓逼近

1.1 基本原理

轮廓逼近的主要目的是用较少的点描述原始轮廓,大大降低数据量的同时,保留原始轮廓的主要形状特征。常用的方法为Douglas-Peucker 算法,通过设定容差(ε)来控制逼近精度。简单来说,算法会剔除那些不会显著改变轮廓形状的冗余点,从而生成一个更简洁的多边形。

1.2 approxPolyDP API

在 OpenCV 中,approxPolyDP 函数实现了 Douglas-Peucker 算法,其函数原型如下:

cpp 复制代码
void approxPolyDP(InputArray curve, OutputArray approxCurve, double epsilon, bool closed);
  • curve:输入轮廓点集。
  • approxCurve:输出逼近后的轮廓点集。
  • epsilon:最大距离误差,通常设置为轮廓周长的一定比例(如 1% 或 2%)。
  • closed:指示轮廓是否是闭合曲线。

1.3 参考代码

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    Mat src = imread("E:/image/pic3.png");
    if (src.empty())
    {
        cout << "无法加载图像!" << endl;
        return -1;
    }

    Mat gray, binary;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

    // 创建一幅画布用于显示结果
    Mat approxImage = src.clone();

    // 依次对每个轮廓进行逼近
    for (size_t i = 0; i < contours.size(); i++)
    {
        // epsilon 定义逼近精度,通常取轮廓弧长的一定比例
        double epsilon = 0.01 * arcLength(contours[i], true);
        vector<Point> approxCurve;
        approxPolyDP(contours[i], approxCurve, epsilon, true);

        // 绘制逼近后的多边形,颜色为红色
        polylines(approxImage, approxCurve, true, Scalar(0, 0, 255), 2);
    }

    imshow("逼近轮廓", approxImage);
    waitKey(0);
    return 0;
}

说明

  • arcLength 计算轮廓的周长,然后用一定比例作为逼近精度 epsilon
  • approxPolyDP 根据指定精度将轮廓逼近为更简单的多边形。
  • 最后用 polylines 绘制逼近后的多边形轮廓。
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
    // 1. 读取输入图像,并转换为灰度图
    Mat inputImg = imread("E:/image/pic4.png");
  
    if (inputImg.empty()) {
        cerr << "图像加载失败!" << endl;
        return -1;
    }
    Mat grayInput;
    cvtColor(inputImg, grayInput, COLOR_BGR2GRAY);

    // 2. 对图像应用阈值处理得到二值图像
    Mat binaryInput;
    threshold(grayInput, binaryInput, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

    vector<vector<Point>> contoursInput, contoursTemplate;
    vector<Vec4i> hierarchy;
    findContours(binaryInput, contoursInput, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

    // 5. 遍历输入图像中的所有轮廓,计算与模板轮廓的匹配度
    for (size_t i = 0; i < contoursInput.size(); i++) 
    {
        //多边形逼近
        Mat ret;
		approxPolyDP(contoursInput[i], ret, 3, true);
       
        double area = contourArea(contoursInput[i]);
        double l = arcLength(contoursInput[i], true);
        printf("corner %d  ,area %.2f,  length %.2f\n", ret.rows , area,l);
        Moments m = moments(contoursInput[i]);
        //drawContours(inputImg, contoursInput, static_cast<int>(i), Scalar(0, 255, 0), 2);
        int cx = static_cast<int>(m.m10 / m.m00);
        int cy = static_cast<int>(m.m01 / m.m00);
		circle(inputImg, Point(cx, cy), 5, Scalar(0, 0, 255), -1);
        if (ret.rows==6)
        {
			putText(inputImg, "poly", Point(cx+10, cy), FONT_HERSHEY_PLAIN, 1, Scalar(0, 255, 0), 1);
		}
        else if (ret.rows == 5)
        {
			putText(inputImg, "pentagon", Point(cx + 10, cy), FONT_HERSHEY_PLAIN, 1, Scalar(0, 255, 0), 1);
		}
        else if (ret.rows == 4)
        {
			putText(inputImg, "rectangle", Point(cx + 10, cy), FONT_HERSHEY_PLAIN, 1, Scalar(0, 255, 0), 1);
		}
        else if (ret.rows == 3)
        {
			putText(inputImg, "triangle", Point(cx + 10, cy), FONT_HERSHEY_PLAIN, 1, Scalar(0, 255, 0), 1);
		}
        else if (ret.rows > 10)
        {
			putText(inputImg, "circle", Point(cx + 10, cy), FONT_HERSHEY_PLAIN, 1, Scalar(0, 255, 0), 1);
        }
    }

    //6. 显示结果\n   
    cv::imshow("轮廓逼近", ~inputImg);
    cv::waitKey(0);
    return 0;
}

通过判断逼近结果点的个数判断多边形的类型

2. 轮廓拟合

在某些应用中,我们不仅需要简化轮廓,还需要拟合出轮廓对应的几何形状(例如最小外接矩形、椭圆拟合、直线拟合等)。OpenCV 提供了多个拟合函数,下面分别介绍常用的几种。

2.1 拟合最小外接矩形

minAreaRect 可用于求解轮廓的最小外接矩形,其结果可以带旋转角度。示例代码如下:

cpp 复制代码
// 针对当前轮廓拟合最小外接矩形
RotatedRect minRect = minAreaRect(contours[i]);

// 通过 boxPoints 得到矩形的四个顶点
Point2f rectPoints[4];
minRect.points(rectPoints);
for (int j = 0; j < 4; j++)
{
    line(approxImage, rectPoints[j], rectPoints[(j+1)%4], Scalar(255, 0, 0), 2);
}

说明

  • 返回的 RotatedRect 包含中心点、尺寸和旋转角度。
  • boxPoints 函数可以提取矩形的四个顶点,便于绘制.

2.2 拟合椭圆

对曲线较为平滑的轮廓,可以拟合椭圆。注意:输入点数量需大于 5。示例代码如下:

cpp 复制代码
if (contours[i].size() > 5)
{
    RotatedRect ellipseRect = fitEllipse(contours[i]);
    ellipse(approxImage, ellipseRect, Scalar(0, 255, 255), 2);
}

说明

  • fitEllipse 返回一个 RotatedRect 对象,描述拟合出来的椭圆。
  • ellipse 函数绘制拟合好的椭圆,颜色可以自行设置。

2.3 拟合直线

有时希望从轮廓点中提取直线信息,这时可以使用 fitLine。示例代码如下:

cpp 复制代码
Vec4f lineParams;
fitLine(contours[i], lineParams, DIST_L2, 0, 0.01, 0.01);
// lineParams 格式为 (vx, vy, x0, y0),其中 (vx,vy) 为方向向量,(x0,y0) 为直线上一个点

// 根据方向向量确定直线的两个端点便于绘制
int imgWidth = src.cols, imgHeight = src.rows;
Point point0(lineParams[2], lineParams[3]);
Point point1;
point1.x = point0.x + (int)(lineParams[0] * imgWidth);
point1.y = point0.y + (int)(lineParams[1] * imgHeight);

line(approxImage, point0, point1, Scalar(255, 255, 0), 2);

说明

  • fitLine 返回直线参数,其中 (vx, vy) 表示直线方向, (x0, y0) 表示直线上一个点。
  • 根据图像尺寸和方向向量,可以构造直线的端点用于绘制。

2.4 综合示例

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
    // 读取图像
    Mat src = imread("E:/image/pic2.png");
    if (src.empty())
    {
        cout << "无法加载图像!" << endl;
        return -1;
    }

    // 预处理:灰度与二值化
    Mat gray, binary;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

    // 提取轮廓
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

    // 用于结果展示
    Mat result = src.clone();

    for (size_t i = 0; i < contours.size(); i++)
    {
  
        // 绘制原始轮廓(绿色)
        drawContours(result, contours, i, Scalar(0, 255, 0), 2);

        // 进行轮廓逼近
        double epsilon = 0.01 * arcLength(contours[i], true);
        vector<Point> approxCurve;
        approxPolyDP(contours[i], approxCurve, epsilon, true);
        polylines(result, approxCurve, true, Scalar(0, 0, 255), 2);

        double area = contourArea(contours[i]);
        double l = arcLength(contours[i], true);
        printf("corner %d  ,area %.2f,  length %.2f\n", approxCurve.size(), area, l);

        // 拟合最小外接矩形(蓝色)
        RotatedRect minRect = minAreaRect(contours[i]);
        Point2f rectPoints[4];
        minRect.points(rectPoints);
        for (int j = 0; j < 4; j++)
        {
            line(result, rectPoints[j], rectPoints[(j + 1) % 4], Scalar(255, 0, 0), 2);
        }

        // 拟合椭圆(黄色),要求轮廓至少有 5 个点
        if (contours[i].size() > 5)
        {
            RotatedRect ellipseRect = fitEllipse(contours[i]);
            ellipse(result, ellipseRect, Scalar(0, 255, 255), 2);
        }
    }

    imshow("轮廓逼近与拟合", result);
    waitKey(0);
    return 0;
}

3. 总结

本文介绍了如何在 OpenCV C++ 中进行轮廓检测、逼近和拟合。

  • 轮廓检测: 使用 findContours 提取图像中的轮廓;
  • 轮廓逼近: 通过 approxPolyDP 对复杂边缘进行简化;
  • 拟合: 利用 minAreaRectfitEllipsefitLine 分别求解最小外接矩形、椭圆和直线。

掌握上述方法后,你可以在各种图像分析与计算机视觉项目中更高效地处理和分析目标边界信息。

相关推荐
才思喷涌的小书虫1 小时前
学术分享:基于 ARCADE 数据集评估 Grounding DINO、YOLO 和 DINO 在血管狭窄检测中的效果
人工智能·yolo·目标检测·计算机视觉·ai·语言模型·视觉检测
yuhouxiyang2 小时前
学习海康VisionMaster之边缘交点
学习·计算机视觉
Clocky74 小时前
图像预处理-插值方法
图像处理·人工智能·计算机视觉
qp6 小时前
22.OpenCV轮廓匹配原理介绍与使用
人工智能·opencv·计算机视觉
kaaokou6 小时前
论文笔记——KIMI-VL:具有增强推理能力的有效开源视觉语言模型
深度学习·计算机视觉·vlm
源客z6 小时前
Stable Diffusion +双Contronet:从 ControlNet 边缘图到双条件融合:实现服装图像生成的技术演进——项目学习记录
图像处理·人工智能·计算机视觉·stable diffusion
yuhouxiyang6 小时前
学习海康VisionMaster之平行线查找
学习·计算机视觉
studyer_domi7 小时前
Matlab 汽车ABS的bangbang控制和模糊PID控制
计算机视觉·matlab·汽车
dafaycoding7 小时前
使用 Perlin Noise、Catmull-Rom 创建闭合平滑曲线
android·计算机视觉
源客z8 小时前
SD + Contronet,扩散模型V1.5+约束条件后续优化:保存Canny边缘图,便于视觉理解——stable diffusion项目学习笔记
图像处理·算法·计算机视觉