OpenCV 笔记(17):轮廓的椭圆拟合、直线拟合

1. 椭圆拟合

轮廓的椭圆拟合是指用椭圆来近似轮廓的形状。当这个椭圆的长轴和短轴相等时,它就是一个圆。

椭圆拟合的基本思路是:对于给定平面上的一组样本点,寻找一个椭圆,使其尽可能接近这些样本点。也就是说,将图像中的一组数据以椭圆方程为模型进行拟合,使某一椭圆方程尽量满足这些数据,并求出该椭圆方程的各个参数。

椭圆拟合有以下几种常用方法:

  • 最小二乘法:最小二乘法是基于最小化拟合误差的思想,通过迭代的方法求解椭圆参数。该方法的优点是简单易实现,缺点是计算量大,当轮廓点数较多时,容易出现收敛问题。
  • 极大似然法:极大似然法是基于概率统计的思想,通过最大化椭圆模型的似然函数求解椭圆参数。该方法的优点是收敛速度快,计算量小,缺点是对初始值敏感。
  • 最小距离法:最小距离法是基于最小化样本点到椭圆的距离的思想,通过迭代的方法求解椭圆参数。该方法的优点是计算量小,收敛速度快,缺点是对初始值敏感。

在 OpenCV 提供了三种 fitEllipse()、fitEllipseAMS()、fitEllipseDirect() 函数实现椭圆拟合。

RotatedRect fitEllipse( InputArray points );

RotatedRect fitEllipseAMS( InputArray points );

RotatedRect fitEllipseDirect( InputArray points );

其输出的 RotatedRect 包含了

  • 椭圆的中心位置
  • 长轴的直径
  • 短轴的直径
  • 旋转角度

OpenCV 提供的这三个函数还是有一定区别的:

函数 算法 优点 缺点
fitEllipse() 最小二乘法 简单易实现 计算量大,收敛问题
fitEllipseAMS() 改进的最小二乘法 收敛速度快,精度高 计算量略大
fitEllipseDirect() 直接求解 计算量最小 对初始值敏感

在实际应用中,可以根据具体情况选择合适的椭圆拟合函数。如果对拟合精度要求较高,可以使用 fitEllipseAMS() 函数。如果对计算速度要求较高,可以使用 fitEllipseDirect() 函数。

下面的例子展示了找到有效的轮廓后,对这些轮廓进行椭圆拟合。

#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(".../test.jpg");
    imshow("src", src);

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

    threshold(gray,thresh,0,255,THRESH_BINARY_INV | 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;
        }

        RotatedRect rrt = fitEllipse(contours[i]);
        Point2f center = rrt.center;
        float w = rrt.size.width;
        float h = rrt.size.height;
        float angle = rrt.angle;
        printf("w = %f, h = %f , angle = %f\n",w,h,angle);
        ellipse(src,rrt, Scalar(0, 255, 255), 8, 8);
    }
    imshow("result", src);

    waitKey(0);
    return 0;
}

执行结果:

w = 172.647202, h = 569.465088 , angle = 76.293610
w = 174.775482, h = 544.108643 , angle = 134.146057
w = 172.592422, h = 589.588135 , angle = 175.138290
w = 178.092865, h = 536.354919 , angle = 154.063202
w = 170.954330, h = 539.261047 , angle = 113.472153
w = 166.067673, h = 571.457947 , angle = 45.668461
w = 162.812332, h = 559.349915 , angle = 97.494217
w = 149.240463, h = 615.341309 , angle = 20.167418
w = 152.298386, h = 594.066528 , angle = 11.754380
w = 144.079239, h = 591.841553 , angle = 69.640686
w = 154.095871, h = 518.927307 , angle = 42.097614
椭圆拟合.png

2. 直线拟合

轮廓的直线拟合是将一个轮廓近似表示为一条与该轮廓形状相近的直线。

直线拟合可以用于以下几个方面:

  • 形状识别:通过直线拟合可以提取图像中物体的轮廓特征,这些特征可以用于物体识别。例如,可以通过拟合直线来识别图像中的道路、边缘等。
  • 目标跟踪:通过直线拟合可以对目标的运动进行跟踪。例如,可以通过拟合直线来跟踪图像中的车辆、飞机等。
  • 图像分割:通过直线拟合可以将图像分割成不同的区域。例如,可以通过拟合直线来分割图像中的文字、图形等。

直线拟合有以下几种常用方法:

  • 最小二乘法:最小二乘法是基于最小化拟合误差的思想,通过迭代的方法求解直线参数。该方法的优点是简单易实现,缺点是计算量大,当轮廓点数较多时,容易出现收敛问题。
  • 最小距离法:最小距离法是基于最小化样本点到直线的距离的思想,通过迭代的方法求解直线参数。该方法的优点是计算量小,收敛速度快,缺点是对初始值敏感。
  • 基于图像特征的方法:基于图像特征的方法是利用图像特征来拟合直线。常用的图像特征包括边缘点、极值点、角点等。该方法的优点是鲁棒性强,缺点是计算量大。

在 OpenCV 提供了 fitLine() 函数实现直线拟合。

void fitLine( InputArray points, OutputArray line, int distType,
                           double param, double reps, double aeps );

第一个参数 points:表示输入点集,可以是 Point 数组或 Mat 矩阵。
第二个参数 line:输出直线。

  • 对于二维直线而言类型为 Vec4f,包含 (vx, vy, x0, y0),其中(vx, vy) 表示直线的方向,(x0, y0) 表示直线上的一点。
  • 对于三维直线类型则是 Vec6f,包含 (vx, vy, vz, x0, y0, z0),其中(vx, vy, vz) 表示直线的方向,(x0, y0, z0) 表示直线上的一点。

第三个参数 distType:表示距离类型,也就是在直线拟合时使用哪种算法。这里的算法基于 M-estimators 实现:

  • DIST_L1: \rho(r) = r
  • DIST_L2: \rho(r) = \frac{r^2}{2}
  • DIST_C
  • DIST_L12: \rho(r)=2(\sqrt{1+\frac{r^2}{2}}-1)
  • DIST_FAIR: \rho(r)= C^2(\frac{r}{C} -\log(1+\frac{r}{C})),C=1.3998
  • DIST_WELSCH: \rho(r)= \frac{C^2}{2}(1 - \exp(-(\frac{r}{C})^2)),C=2.9846
  • DIST_HUBER: \rho(r)=\begin{cases} \frac{r^2}{2}, & \text{if }r < C \\ C(r-\frac{C}{2}), & \text{otherwise} \end{cases},C=1.345

第四个参数 param:表示距离参数,跟所选的距离类型有关。如果为 0,则自动选择最佳值。
第五个参数 reps:表示拟合直线所需要的径向精度,通常该值被设定为 0.01。
第六个参数 aeps:表示拟合直线所需要的角度精度,通常该值被设定为 0.01。

下面的例子,将一些点拟合成一条直线,并找到直线的极值点。

#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

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

    vector<Point> points;
    points.push_back(Point(48, 58));
    points.push_back(Point(105, 98));
    points.push_back(Point(155, 160));
    points.push_back(Point(212, 220));
    points.push_back(Point(248, 260));
    points.push_back(Point(320, 300));
    points.push_back(Point(350, 360));
    points.push_back(Point(412, 400));

    //将拟合点绘制到空白图上
    for (int i = 0; i < points.size(); i++)
    {
        circle(image, points[i], 5, cv::Scalar(0, 0, 255), 2, 8);
    }
    imshow("src", image);

    cv::Vec4f line_para;
    cv::fitLine(points, line_para, cv::DIST_L2, 0, 1e-2, 1e-2);
    std::cout << "line_para = " << line_para << std::endl;

    //获取直线的斜率、截矩
    float vx = line_para[0];
    float vy = line_para[1];
    float x0 = line_para[2];
    float y0 = line_para[3];

    float k = vy / vx;
    float b = y0 - k*x0;

    // 寻找直线的极值点
    int minx = 0, miny = 10000;
    int maxx = 0, maxy = 0;
    for (int i = 0; i < points.size(); i++) {
        Point pt = points[i];
        if (miny > pt.y) {
            miny = pt.y;
        }
        if (maxy < pt.y) {
            maxy = pt.y;
        }
    }
    maxx = (maxy - b) / k;
    minx = (miny - b) / k;
    line(image, Point(maxx, maxy), Point(minx, miny), Scalar(255, 0, 0), 2, 8);

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

    return 0;
}
点的直线拟合.png

下面的例子,对轮廓进行直线拟合。

#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(".../test.jpg");
    imshow("src", src);

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

    threshold(gray,thresh,0,255,THRESH_BINARY_INV | 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 t = 0; t< contours.size(); t++) {
        double area = contourArea(contours[t]);

        if (area < 1000) {
            continue;
        }

        // 直线拟合
        Vec4f line_para;
        fitLine(contours[t], line_para, DIST_L2, 0, 0.01, 0.01);
        //获取直线的斜率、截矩
        float vx = line_para[0];
        float vy = line_para[1];
        float x0 = line_para[2];
        float y0 = line_para[3];

        float k = vy / vx;
        float b = y0 - k*x0;

        // 寻找直线的极值点
        int minx = 0, miny = 10000;
        int maxx = 0, maxy = 0;
        for (int i = 0; i < contours[t].size(); i++) {
            Point pt = contours[t][i];
            if (miny > pt.y) {
                miny = pt.y;
            }
            if (maxy < pt.y) {
                maxy = pt.y;
            }
        }
        maxx = (maxy - b) / k;
        minx = (miny - b) / k;
        line(src, Point(maxx, maxy), Point(minx, miny), Scalar(255, 0, 0), 8, 8);
    }
    imshow("result", src);

    waitKey(0);
    return 0;
}
轮廓的直线拟合.png

3. 总结

本文介绍了在 OpenCV 中如何对轮廓进行椭圆拟合和直线拟合。它们可以用于提取轮廓的特征,简化轮廓的表示,提高轮廓的处理效率。它们在图像分割、目标识别、目标跟踪等任务中有着广泛的应用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容