OpenCV 笔记(16):轮廓的几何特征

在该系列第十四篇文章中,介绍了很多轮廓的基础特征,包括面积、周长、质心、凸包等等,它们也都是轮廓的几何特征。本文主要介绍的是轮廓形状拟合。

轮廓形状拟合是指通过数学模型来近似轮廓的形状。轮廓形状拟合有助于简化轮廓的表示,并提取轮廓的几何特征,所以它的作用如下:

  • 简化轮廓:可以使用简单的几何形状来近似复杂的轮廓,从而简化轮廓分析。
  • 提取形状特征:可以使用轮廓形状拟合来提取形状特征,例如轮廓的长宽比、面积、周长等。
  • 对象识别:可以使用轮廓形状拟合来识别具有特定形状的对象。

1. 最小外接矩形

在该系列第十四篇文章中,已经介绍过轮廓的外接矩形和最小外接矩形。

  • 外接矩形 boundingRect()
  • 最小外接矩形 minAreaRect()
特征 外接矩形 最小外接矩形
定义 能够包围图形的所有点的最小矩形 能够包围图形的所有点的最小矩形,且其长宽比为正
旋转 不考虑旋转 考虑旋转
面积 可能不是最小的 一定是最小的
用途 用于描述图形的大小和位置 用于描述图形的大小、位置和形状

2. 最小外接圆

轮廓的最小外接圆是指包围轮廓所有点的最小的圆。它可以用来描述轮廓的形状和大小,其圆心位于轮廓的质心。

在 OpenCV 中使用 minEnclosingCircle() 函数来计算轮廓的最小外接圆。

下面的例子,获取图中最大轮廓的最小外接矩形和最小外接圆,分别用蓝色和红色表示。

cpp 复制代码
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"

using namespace std;
using namespace cv;

bool ascendSort(vector<Point> a,vector<Point> b)
{
    return contourArea(a) > contourArea(b);
}

int main(int argc, char **argv) {
    Mat src = imread(".../fruit.jpg");
    imshow("src", src);

    Mat gray,thresh;
    cvtColor(src, gray, cv::COLOR_BGR2GRAY);

    threshold(gray,thresh,0,255,THRESH_BINARY | THRESH_OTSU);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(thresh, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    sort(contours.begin(), contours.end(), ascendSort);//ascending sort

    RotatedRect rrt = minAreaRect(contours[0]);// 获取最大轮廓的最小外接矩形

    Point2f pt[4];
    rrt.points(pt);
    line(src, pt[0], pt[1], Scalar(255, 0, 0), 8, 8);
    line(src, pt[1], pt[2], Scalar(255, 0, 0), 8, 8);
    line(src, pt[2], pt[3], Scalar(255, 0, 0), 8, 8);
    line(src, pt[3], pt[0], Scalar(255, 0, 0), 8, 8);

    Point2f center;
    float radius;
    minEnclosingCircle(contours[0],center,radius); // 获取最大轮廓的最小外接圆

    circle(src, center, radius, Scalar(0, 0, 255),8,8);
    circle(src, center, 8, Scalar(255, 0, 255),8,8);
    imshow("result", src);

    waitKey(0);
    return 0;
}

3. 最大内接圆

轮廓的最大内接圆是指轮廓内部的最大圆。在 OpenCV 中没有提供现成的函数来计算轮廓的最大内接圆,但是提供了 pointPolygonTest() 函数。

cpp 复制代码
double pointPolygonTest( InputArray contour, Point2f pt, bool measureDist );

其作用是计算像素点到轮廓的最小距离,对该函数的参数简单解释一下:

第三个参数 measureDist:计算的距离是否具有方向性的标志。

  • true 表示输出结果具有方向性。如果像素点在轮廓内部,返回值为正数,如果像素点在轮廓外部,返回值为负数。
  • false 表示输出结果不具有方向性。只判断像素点与轮廓之间的位置关系,如果像素点在轮廓的内部,返回值为 -1,如果像素点在轮廓的边缘上,返回值为0,如果像素点在轮廓的外部,返回值为 -1。

由于 pointPolygonTest() 函数返回的是点到轮廓的最短像素距离,只要扫描轮廓内部的点,找到内部点中到轮廓距离最大的点,这个距离就是最大内接圆的半径,并且该点就是圆心。

下面的例子,绘制了一个五角星,并画出它的最小外接圆和最大内接圆。

cpp 复制代码
#include "opencv2/imgproc.hpp"
#include <opencv2/opencv.hpp>
#include <cmath>

using namespace std;
using namespace cv;

int main(int argc, char **argv) {
    Mat image(1600, 1600, CV_8UC3, cv::Scalar(0, 0, 0));

    int centerX = image.cols / 2;
    int centerY = image.rows / 2;
    int radius = 600;

    vector<Point> vertices;
    for (int i = 0; i < 5; i++) {
        double angle = i * 2 * M_PI / 5 + M_PI / 2;
        int x = centerX + radius * cos(angle);
        int y = centerY - radius * sin(angle);
        vertices.push_back(Point(x, y));
    }

    for (int i = 0; i < 5; i++) {
        line(image, vertices[i], vertices[(i + 2) % 5], cv::Scalar(255, 255, 255), 2);
    }

    imshow("src", image);

    Mat gray;
    cvtColor(image,gray,cv::COLOR_BGR2GRAY);

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

    drawContours(image,contours,0,Scalar(255, 0, 255),2);// 绘制五角星的轮廓
    imshow("contours", image);

    Point2f center;
    float r;
    minEnclosingCircle(contours[0],center,r);

    circle(image, center, radius, Scalar(0, 0, 255),8,8); // 绘制最小外接圆
    circle(image, center, 2, Scalar(255, 0, 255),8,8);
    imshow("minEnclosingCircle", image);

    Mat raw_dist(image.size(), CV_32F);
    for (int i = 0; i < image.rows; i++)
    {
        for (int j = 0; j < image.cols; j++)
        {
            raw_dist.at<float>(i, j) = (float)pointPolygonTest(contours[0], Point2f((float)j, (float)i), true);
        }
    }

    // 获取最大内接圆半径
    double minVal, maxVal;
    Point maxDistPt; // inscribed circle center
    minMaxLoc(raw_dist, &minVal, &maxVal, NULL, &maxDistPt);
    minVal = abs(minVal);
    maxVal = abs(maxVal);

    circle(image, maxDistPt, maxVal, Scalar(0, 255, 255),8,8);

    imshow("result", image);
    waitKey(0);
    return 0;
}

4. 轮廓逼近

轮廓逼近是指用更少的点来近似轮廓的曲线的方法。这可以用来简化轮廓的表示,提高计算效率,或减少噪声的影响。

轮廓逼近常用的方法:

  • Douglas-Peucker 算法:该算法是基于贪心算法的,它逐个删除轮廓中距离最近的两个点。
  • Ramer-Douglas-Peucker 算法:该算法是基于 Douglas-Peucker 算法的改进版本,它可以根据指定的阈值来控制轮廓逼近的程度。
  • Bezier 曲线逼近:该算法是基于 Bezier 曲线的逼近方法,它可以生成更加平滑的轮廓。

在 OpenCV 中常用 approxPolyDP() 函数来实现轮廓逼近。该函数使用的是 Douglas-Peucker 算法。

cpp 复制代码
void approxPolyDP( InputArray curve,
                                OutputArray approxCurve,
                                double epsilon, bool closed );

第一个参数 curve:表示输入曲线,可以是 Point 数组或 Mat 矩阵。

第二个参数 approxCurve:表示输出多边形,可以是 Point 数组或 Mat 矩阵。

第三个参数 epsilon:表示近似的精度,表示原始曲线与近似曲线之间的最大距离。该值越小,精度越高。

第四个参数 closed:表示近似的曲线为封闭曲线。

Douglas-Peucker 算法首先从曲线的首尾两点开始,然后不断地寻找离曲线最近的点,并将该点添加到近似曲线中。

算法步骤如下:

  1. 初始化:设置曲线的首尾两点 A 和 B。
  2. 计算曲线上离直线 AB 距离最大的点 C。
  3. 比较距离 C 到 AB 的距离与给定阈值 epsilon 的大小。
    • 如果距离小于 epsilon,则直线 AB 作为曲线的近似,算法结束。
    • 如果距离大于 epsilon,则将 C 添加到近似曲线中,并将直线 AB 分为两个段,分别为 AB 和 AC。
  4. 重复步骤 2 和 3,直到所有点到近似曲线的距离都小于 epsilon。

下面的例子,分别画了一个正方形、三角形、五边形和圆,通过轮廓查找找到它们的轮廓后,使用 approxPolyDP() 函数对轮廓逼近成多边形。

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

using namespace std;
using namespace cv;

bool ascendSort(vector<Point> a,vector<Point> b)
{
    return contourArea(a) > contourArea(b);
}

int main(int argc, char **argv) {
    Mat image(1600, 1600, CV_8UC3, Scalar(0, 0, 0));

    // 画一个正方形
    rectangle(image, Point(100, 100), Point(400, 400), Scalar(255, 255, 0), -1);

    // 画一个三角形
    Point trianglePoints[3] = {Point(500, 800), Point(250, 1300), Point(800, 1100)};
    fillConvexPoly(image, trianglePoints, 3, Scalar(255, 255, 0));

    // 画一个五边形
    Point pentagonPoints[5];
    for (int i = 0; i < 5; i++) {
        pentagonPoints[i] = Point(600 + 200 * cos(2 * CV_PI * i / 5), 600 + 200 * sin(2 * CV_PI * i / 5));
    }
    fillConvexPoly(image, pentagonPoints, 5, Scalar(255, 255, 0));

    // 画一个圆形
    circle(image, Point(1200, 1200), 300, Scalar(255, 255, 0), -1);

    Mat gray,thresh;
    cvtColor(image, gray, cv::COLOR_BGR2GRAY);
    threshold(gray,thresh,0,255,THRESH_BINARY | THRESH_OTSU);
    imshow("thresh", thresh);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(thresh, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    sort(contours.begin(), contours.end(), ascendSort);//ascending sort

    for (size_t i = 0; i< contours.size(); i++) {
        double area = contourArea(contours[i]);

        if (area < 1000) {
            continue;
        }

        vector<Point> approx;
        double perimeter = arcLength(contours[i], true);
        double epsilon = perimeter * 0.01;
        approxPolyDP(contours[i], approx, epsilon, true);

        int approxSize = approx.size();
        cout << "approxSize = " << approxSize << endl;
    }

    imshow("result", image);
    waitKey(0);
    return 0;
}

执行结果:

ini 复制代码
approxSize = 16
approxSize = 3
approxSize = 5
approxSize = 4

上述代码将圆近似成16边形,后续会介绍可以通过轮廓的矩形度来判断是否是圆形。

5. 总结

轮廓是图像中物体的边界,轮廓拟合是将轮廓近似为一系列点或多边形,有利于提取、描述和分析物体的形状,提高轮廓的鲁棒性,或简化轮廓的后处理。后续还会介绍椭圆拟合、直线拟合等等。

相关推荐
棒棒的皮皮2 小时前
【OpenCV】Python图像处理之图像表示方法
图像处理·python·opencv
棒棒的皮皮2 小时前
【OpenCV】Python图像处理之通道拆分与合并
图像处理·python·opencv·计算机视觉
向上的车轮2 小时前
图像处理OpenCV与深度学习框架YOLOv8的主要区别是什么?
图像处理·深度学习·opencv·yolov8
棒棒的皮皮2 小时前
【OpenCV】Python图像处理之像素操作
图像处理·python·opencv
向上的车轮2 小时前
基于深度学习与OpenCV的物体计数技术全解析
人工智能·深度学习·opencv
xrn19972 小时前
Android OpenCV SDK 编译教程(WSL2 Ubuntu 22.04 环境)
android·c++·opencv
棒棒的皮皮20 小时前
【OpenCV】Python图像处理之读取与保存
图像处理·python·opencv
棒棒的皮皮1 天前
【OpenCV】Python图像处理之特征提取
图像处理·python·opencv
lxmyzzs1 天前
【图像算法 - 36】医疗应用:基于 YOLOv12 与 OpenCV 的高精度脑肿瘤检测系统实现
python·深度学习·opencv·yolo·计算机视觉·脑肿瘤检测
yolo_guo1 天前
opencv 学习: 11 图片像素位置变换,以图片加水波纹特效为例
c++·opencv·计算机视觉