1. 椭圆拟合
轮廓的椭圆拟合是指用椭圆来近似轮廓的形状。当这个椭圆的长轴和短轴相等时,它就是一个圆。
椭圆拟合的基本思路是:对于给定平面上的一组样本点,寻找一个椭圆,使其尽可能接近这些样本点。也就是说,将图像中的一组数据以椭圆方程为模型进行拟合,使某一椭圆方程尽量满足这些数据,并求出该椭圆方程的各个参数。
椭圆拟合有以下几种常用方法:
- 最小二乘法:最小二乘法是基于最小化拟合误差的思想,通过迭代的方法求解椭圆参数。该方法的优点是简单易实现,缺点是计算量大,当轮廓点数较多时,容易出现收敛问题。
- 极大似然法:极大似然法是基于概率统计的思想,通过最大化椭圆模型的似然函数求解椭圆参数。该方法的优点是收敛速度快,计算量小,缺点是对初始值敏感。
- 最小距离法:最小距离法是基于最小化样本点到椭圆的距离的思想,通过迭代的方法求解椭圆参数。该方法的优点是计算量小,收敛速度快,缺点是对初始值敏感。
在 OpenCV 提供了三种 fitEllipse()、fitEllipseAMS()、fitEllipseDirect() 函数实现椭圆拟合。
cpp
RotatedRect fitEllipse( InputArray points );
RotatedRect fitEllipseAMS( InputArray points );
RotatedRect fitEllipseDirect( InputArray points );
其输出的 RotatedRect 包含了
- 椭圆的中心位置
- 长轴的直径
- 短轴的直径
- 旋转角度
OpenCV 提供的这三个函数还是有一定区别的:
函数 | 算法 | 优点 | 缺点 |
---|---|---|---|
fitEllipse() | 最小二乘法 | 简单易实现 | 计算量大,收敛问题 |
fitEllipseAMS() | 改进的最小二乘法 | 收敛速度快,精度高 | 计算量略大 |
fitEllipseDirect() | 直接求解 | 计算量最小 | 对初始值敏感 |
在实际应用中,可以根据具体情况选择合适的椭圆拟合函数。如果对拟合精度要求较高,可以使用 fitEllipseAMS() 函数。如果对计算速度要求较高,可以使用 fitEllipseDirect() 函数。
下面的例子展示了找到有效的轮廓后,对这些轮廓进行椭圆拟合。
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(".../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;
}
执行结果:
ini
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
2. 直线拟合
轮廓的直线拟合是将一个轮廓近似表示为一条与该轮廓形状相近的直线。
直线拟合可以用于以下几个方面:
- 形状识别:通过直线拟合可以提取图像中物体的轮廓特征,这些特征可以用于物体识别。例如,可以通过拟合直线来识别图像中的道路、边缘等。
- 目标跟踪:通过直线拟合可以对目标的运动进行跟踪。例如,可以通过拟合直线来跟踪图像中的车辆、飞机等。
- 图像分割:通过直线拟合可以将图像分割成不同的区域。例如,可以通过拟合直线来分割图像中的文字、图形等。
直线拟合有以下几种常用方法:
- 最小二乘法:最小二乘法是基于最小化拟合误差的思想,通过迭代的方法求解直线参数。该方法的优点是简单易实现,缺点是计算量大,当轮廓点数较多时,容易出现收敛问题。
- 最小距离法:最小距离法是基于最小化样本点到直线的距离的思想,通过迭代的方法求解直线参数。该方法的优点是计算量小,收敛速度快,缺点是对初始值敏感。
- 基于图像特征的方法:基于图像特征的方法是利用图像特征来拟合直线。常用的图像特征包括边缘点、极值点、角点等。该方法的优点是鲁棒性强,缺点是计算量大。
在 OpenCV 提供了 fitLine() 函数实现直线拟合。
cpp
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: <math xmlns="http://www.w3.org/1998/Math/MathML"> ρ ( r ) = r \rho(r) = r </math>ρ(r) = r
- DIST_L2: <math xmlns="http://www.w3.org/1998/Math/MathML"> ρ ( r ) = r 2 2 \rho(r) = \frac{r^2}{2} </math>ρ(r) = 2r2
- DIST_C
- DIST_L12: <math xmlns="http://www.w3.org/1998/Math/MathML"> ρ ( r ) = 2 ( 1 + r 2 2 − 1 ) \rho(r)=2(\sqrt{1+\frac{r^2}{2}}-1) </math>ρ(r)=2(1+2r2 −1)
- DIST_FAIR: <math xmlns="http://www.w3.org/1998/Math/MathML"> ρ ( r ) = C 2 ( r C − log ( 1 + r C ) ) \rho(r)= C^2(\frac{r}{C} -\log(1+\frac{r}{C})) </math>ρ(r)= C2(Cr−log(1+Cr)),C=1.3998
- DIST_WELSCH: <math xmlns="http://www.w3.org/1998/Math/MathML"> ρ ( r ) = C 2 2 ( 1 − exp ( − ( r C ) 2 ) ) \rho(r)= \frac{C^2}{2}(1 - \exp(-(\frac{r}{C})^2)) </math>ρ(r)= 2C2(1− exp(−(Cr)2)),C=2.9846
- DIST_HUBER: <math xmlns="http://www.w3.org/1998/Math/MathML"> ρ ( r ) = { r 2 2 , if r < C C ( r − C 2 ) , otherwise \rho(r)=\begin{cases} \frac{r^2}{2}, & \text{if }r < C \\ C(r-\frac{C}{2}), & \text{otherwise} \end{cases} </math>ρ(r)={2r2,C(r−2C),if r < Cotherwise,C=1.345
第四个参数 param:表示距离参数,跟所选的距离类型有关。如果为 0,则自动选择最佳值。 第五个参数 reps:表示拟合直线所需要的径向精度,通常该值被设定为 0.01。 第六个参数 aeps:表示拟合直线所需要的角度精度,通常该值被设定为 0.01。
下面的例子,将一些点拟合成一条直线,并找到直线的极值点。
cpp
#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;
}
下面的例子,对轮廓进行直线拟合。
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(".../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;
}
3. 总结
本文介绍了在 OpenCV 中如何对轮廓进行椭圆拟合和直线拟合。它们可以用于提取轮廓的特征,简化轮廓的表示,提高轮廓的处理效率。它们在图像分割、目标识别、目标跟踪等任务中有着广泛的应用。