版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
用C++实现最小二乘法,最小区域法,最小包容圆拟合圆的算法和在Qt中的运行结果
我们工科生经常要处理数据,大家在处理数据的过程中遇到圆拟合的问题,一般会用到最小包容圆,最小外接圆,最小二乘拟合圆,最小区域法(遗传算法,粒子群算法),这里为了方便大家参考,提供C++实现最小二乘法,最小包容圆法,最小区域法(粒子群算法)的算法。话不多说,先上在Qt中的运行结果:
如图红色的点是随机生成的,图一中随机点在以原点为圆心,半径为10的圆内随机生成。其余两个图中点的范围在以原点为圆心,半径为9.5和10.5的同心圆范围内。黑色的线为拟合的圆。可以看到最小区域法的拟合比较精确。
下面附上三个算法代码的源文件和头文件(Qt中的代码,和C++稍微有一点不同,主要是为了可视化。大家可以根据需要自行修改。)
#ifndef LEASTSQUAREMETHODFITTINGCIRCLE_H
#define LEASTSQUAREMETHODFITTINGCIRCLE_H
#include <QList>
typedef struct pointInfo
{
double x;
double y;
double radius;
}pointInfo;
pointInfo LSFit(QList<pointInfo> &Point); //最小二乘圆拟合函数
#endif // LEASTSQUAREMETHODFITTINGCIRCLE_H
#include "leastsquaremethodfittingcircle.h"
/* 最小二乘法圆拟合函数 */
pointInfo LSFit(QList<pointInfo> &Point)
{
pointInfo tempPoint; //定义临时变量,存储最小二乘圆心坐标
double X1=0,Y1=0,X2=0,Y2=0,X3=0,Y3=0;
double X1Y1=0,X1Y2=0,X2Y1=0;
int N=Point.size();
for(int i=0;i<Point.size();i++)
{
double x=Point.at(i).x;
double y=Point.at(i).y;
double x2=x*x,y2=y*y;
X1+=x;
Y1+=y;
X2+=x2;
Y2+=y2;
X3+=x*x2;
Y3+=y*y2;
X1Y1+=x*y;
X1Y2+=x*y2;
X2Y1+=x2*y;
}
double a=0,b=0,c=0;
double C1=0,D1=0,E1=0,G1=0,H1=0;
C1=N*X2-X1*X1;
D1=N*X1Y1-X1*Y1;
E1=N*X3+N*X1Y2-(X2+Y2)*X1;
G1=N*Y2-Y1*Y1;
H1=N*X2Y1+N*Y3-(X2+Y2)*Y1;
a=(H1*D1-E1*G1)/(C1*G1-D1*D1);
b=(H1*C1-E1*D1)/(D1*D1-G1*C1);
c=-(a*X1+b*Y1+X2+Y2)/N;
tempPoint.x=a/(-2);
tempPoint.y=b/(-2);
tempPoint.radius=(sqrt(a*a+b*b-4*c))/2;
return tempPoint;
}
最小包容圆
#ifndef MINIMALENCLOSINGCIRCLE_H
#define MINIMALENCLOSINGCIRCLE_H
#include <QList>
#include <algorithm>
#include <qmath.h>
#include <random>
struct Point
{
double x;
double y;
Point subtract(const Point &p) const;
double distance(const Point &p) const;
double cross(const Point &p) const;// Signed area / determinant thing
};
struct Circle
{
public:
static const Circle INVALID;
Point c;
double r;
bool contains(const Point &p) const;
private:
static const double MULTIPLICATIVE_EPSILON;
};
Circle MakeSmallestEnclosingCircle(QList<Point> &points);
Circle MakeSmallestEnclosingCircleOnePoint(QList<Point> &points, int end, Point &p);
Circle MakeSmallestEnclosingCircleTwoPoints(QList<Point> &points, int end, Point &p, Point &q);
Circle MakeDiameter(const Point &a, const Point &b);
Circle MakeCircumcircle(const Point &a, const Point &b, const Point &c);
#endif // MINIMALENCLOSINGCIRCLE_H
#include "minimalenclosingcircle.h"
/* 求取坐标差值函数 */
Point Point::subtract(const Point &p) const
{
return Point{x - p.x, y - p.y}; //q点与P点横纵坐标的差值
}
/* 求取两点间距离函数 */
double Point::distance(const Point &p) const
{
//hypot求斜边的长度,返回两点的距离值
return hypot(x - p.x,y-p.y); //求p点的坐标与圆c的圆心的距离
}
/* 求向量的叉乘 */
double Point::cross(const Point &p) const
{
return x * p.y - y * p.x;
}
/*定义INVALID*/
const Circle Circle::INVALID{Point{0, 0}, -1};
/* 定义MULTIPLICATIVE_EPSILON */
const double Circle::MULTIPLICATIVE_EPSILON = 1 + 1e-14;
bool Circle::contains(const Point &p) const
{
return c.distance(p) <= r * MULTIPLICATIVE_EPSILON; //r为圆c的半径,如果距离点p到圆c的距离小于半径(也就是说,点p在圆c内)返回真
}
/*初始化圆没有边界,圆为坐标(0,0),半径-1*/
Circle MakeSmallestEnclosingCircle(QList<Point> &points)
{
QList<Point> shuffled=points;
Circle c(Circle::INVALID); //初始化为坐标(0,0) 半径为-1的圆 显然不存在{Point{0, 0}, -1};
for (int i = 0; i < shuffled.size(); i++)
{
Point p = shuffled.at(i);
if (c.r < 0 || !c.contains(p)) //如果半径小于0或者点p不在圆c内,执行下面的语句。不在圆内就有了第一个点
{
c = MakeSmallestEnclosingCircleOnePoint(shuffled, i + 1, p);
}
}
return c;
}
/*知道了圆的一个点,要确定第二个点*/
Circle MakeSmallestEnclosingCircleOnePoint(QList<Point> &points, int end, Point &p)
{
Circle c{p, 0};
for (int i = 0; i < end; i++)
{
Point q = points.at(i);
if (!c.contains(q)) //p不在圆c内就执行以下语句,有了第二个点
{
if (c.r == 0)
c = MakeDiameter(p, q);//第一次的r等于0.以p,q两点为直径当做圆
else
{
c = MakeSmallestEnclosingCircleTwoPoints(points, i + 1, p, q);
}
}
}
return c;
}
/* 知道了圆的两个点 */
Circle MakeSmallestEnclosingCircleTwoPoints(QList<Point> &points, int end, Point &p, Point &q)
{
Circle circ = MakeDiameter(p, q);
Circle left = Circle::INVALID;
Circle right = Circle::INVALID;
//有任意一点不在以p和q为圆的内部
Point pq = q.subtract(p);
for (int i = 0; i < end; i++)
{
const Point &r = points.at(i);
if (circ.contains(r))//如果以圆p,q为直径的圆 包括点r 跳出这次循环
{
continue;
}
//找一个外接圆,并在左边或右边分类
double cross = pq.cross(r.subtract(p));//可以用向量的方式判断第三点在q的左边还是右边
Circle c = MakeCircumcircle(p, q, r);
if (c.r < 0)
{
continue;
}
else if (cross > 0 && (left.r < 0 || pq.cross(c.c.subtract(p)) > pq.cross(left.c.subtract(p))))//点r在q的左边
{
left = c;
}
else if (cross < 0 && (right.r < 0 || pq.cross(c.c.subtract(p)) < pq.cross(right.c.subtract(p))))//点r在q的右边
{
right = c;
}
}
//选择返回哪个圆
if (left.r < 0 && right.r < 0)
return circ;
else if (left.r < 0)
return right;
else if (right.r < 0)
return left;
else
return left.r <= right.r ? left : right;//返回半径较小的那个圆
}
/*两点为直径画圆*/
Circle MakeDiameter(const Point &a, const Point &b)
{
Point c{(a.x + b.x) / 2, (a.y + b.y) / 2};//求 a,b两点连线的中点,当做圆心
return Circle{c, qMax(c.distance(a),c.distance(b))};//返回较大值
}
/*三点确定一个圆*/
Circle MakeCircumcircle(const Point &a, const Point &b, const Point &c)
{
//数学上的外接圆算法
double ox= (qMin(qMin(a.x,b.x),c.x)+qMax(qMin(a.x,b.x),c.x))/2;//求一条连线的中点
double oy= (qMin(qMin(a.y,b.y),c.y)+qMax(qMin(a.y,b.y),c.y))/2;
double ax = a.x - ox, ay = a.y - oy;
double bx = b.x - ox, by = b.y - oy;
double cx = c.x - ox, cy = c.y - oy;
double d = (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) * 2;
if (d == 0)
return Circle::INVALID;
double x = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
double y = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
Point p{ox + x, oy + y};
double r=qMax(qMax(p.distance(a),p.distance(b)),p.distance(c));
return Circle{p, r};
}
当然最后一个比较复杂,代码质量不太好,答主还在调试优化。有兴趣可以关注答主微信公众号,不定时分享一些关于Qt知识。也可以一起讨论。