有一系列的数据点 {xi,yi}。我们知道这些数据点近似的落在一个圆上。依据这些数据预计这个圆的參数就是一个非常有意义的问题。今天就来讲讲怎样来做圆的拟合。圆拟合的方法有非常多种,最小二乘法属于比較简单的一种。
今天就先将这样的。
我们知道圆方程能够写为:
(x?xc)2+(y?yc)2=R2
通常的最小二乘拟合要求距离的平方和最小。也就是
f=∑((xi?xc)2+(yi?yc)2??????????????????√?R)2
最小。
这个算起来会非常麻烦。
也得不到解析解。
所以我们退而求其次。
f=∑((xi?xc)2+(yi?yc)2?R2)2
这个式子要简单的多。我们定义一个辅助函数:
g(x,y)=(x?xc)2+(y?yc)2?R2
那么上面的式子能够表示为:
f=∑g(xi,yi)2
依照最小二乘法的通常的步骤,可知f 取极值时相应以下的条件。
?f?xc=0?f?yc=0?f?R=0
先来化简 ?f?R=0
?f?R=?2R×∑((xi?xc)2+(yi?yc)2?R2)=?2R×∑g(xi,yi)=0
我们知道半径 R 是不能为 0 的。所以必定有:
∑g(xi,yi)=0
这是个非常实用的结论。剩下的两个式子:
?f?xc=?4∑((xi?xc)2+(yi?yc)2?R2)(xi?xc)=?4∑g(xi,yi)(xi?xc)=?4∑xig(xi,yi)=0?f?yc=?4∑((xi?xc)2+(yi?yc)2?R2)(yi?yc)=?4∑g(xi,yi)(yi?yc)=?4∑yig(xi,yi)=0
也就是以下两个等式:
∑xig(xi,yi)=0∑yig(xi,yi)=0
这里设:
ui=xi?xˉuc=xc?xˉvi=yi?yˉvc=yc?yˉ
当中:
xˉ=∑xi/Nyˉ=∑yi/N
那么简单的推导一下,就会发现:
∑uig(ui,vi)=0∑vig(ui,vi)=0
事实上都不须要推导,这个变量替换仅仅只是是改动了坐标原点的位置。而我们的等式根本就与坐标原点的详细位置无关。
所以必定成立。
这两个式子展开写是这样的:
∑((ui?uc)2+(vi?vc)2?R2)ui=0∑((ui?uc)2+(vi?vc)2?R2)vi=0
进一步展开:
∑(u3i?2u2iuc+uiu2c+uiv2i?2uivivc+uiv2c?uiR2)=0∑(u2ivi?2uiviuc+viu2c+v3i?2v2ivc+viv2c?viR2)=0
我们知道 ∑ui=0, ∑vi=0。所以上面两个式子是能够化简的。
∑(u3i?2u2iuc+uiv2i?2uivivc)=0∑(u2ivi?2uiviuc+v3i?2v2ivc)=0
为了简化式子,我们定义几个參数:
Suuu=∑u3iSvvv=∑v3iSuu=∑u2iSvv=∑v2iSuv=∑uiviSuuv=∑u2iviSuvv=∑uiv2i
那么上面的式子能够写为:
Suuuc+Suvvc=Suuu+Suvv2Suvuc+Svvvc=Suuv+Svvv2
至此,就能够解出uc 和vc 了。
uc=SuuvSuv?SuuuSvv?SuvvSvv+SuvSvvv2(S2uv?SuuSvv)vc=?SuuSuuv+SuuuSuv+SuvSuvv?SuuSvvv2(S2uv?SuuSvv)
那么:
xc=uc+xˉyc=vc+yˉ
还剩下个 R 没求出来。 也非常easy。
∑g(xi,yi)=0∑((xi?xc)2+(yi?yc)2?R2)=0
所以:
R2=∑((xi?xc)2+(yi?yc)2)
好了。
以下给出个代码,这个代码的详细公式和我这里给出的有一点小差异。可是原理是同样的。
/**
* 最小二乘法拟合圆
* 拟合出的圆以圆心坐标和半径的形式表示
* 此代码改编自 newsmth.net 的 jingxing 在 Graphics 版贴出的代码。
* 版权归 jingxing, 我仅仅是搬运工外加一些简单的改动工作。
*/
typedef complex POINT;
bool circleLeastFit(const std::vector &points, double ¢er_x, double ¢er_y, double &radius)
{
center_x = 0.0f;
center_y = 0.0f;
radius = 0.0f;
if (points.size() < 3)
{
return false;
}
double sum_x = 0.0f, sum_y = 0.0f;
double sum_x2 = 0.0f, sum_y2 = 0.0f;
double sum_x3 = 0.0f, sum_y3 = 0.0f;
double sum_xy = 0.0f, sum_x1y2 = 0.0f, sum_x2y1 = 0.0f;
int N = points.size();
for (int i = 0; i < N; i++)
{
double x = points[i].real();
double y = points[i].imag();
double x2 = x * x;
double y2 = y * y;
sum_x += x;
sum_y += y;
sum_x2 += x2;
sum_y2 += y2;
sum_x3 += x2 * x;
sum_y3 += y2 * y;
sum_xy += x * y;
sum_x1y2 += x * y2;
sum_x2y1 += x2 * y;
}
double C, D, E, G, H;
double a, b, c;
C = N * sum_x2 - sum_x * sum_x;
D = N * sum_xy - sum_x * sum_y;
E = N * sum_x3 + N * sum_x1y2 - (sum_x2 + sum_y2) * sum_x;
G = N * sum_y2 - sum_y * sum_y;
H = N * sum_x2y1 + N * sum_y3 - (sum_x2 + sum_y2) * sum_y;
a = (H * D - E * G) / (C * G - D * D);
b = (H * C - E * D) / (D * D - G * C);
c = -(a * sum_x + b * sum_y + sum_x2 + sum_y2) / N;
center_x = a / (-2);
center_y = b / (-2);
radius = sqrt(a * a + b * b - 4 * c) / 2;
return true;
}
下图是个实际測试的结果。对这样的均匀分布的噪声数据计算的结果还是非常准确的。
可是当数据中有部分偏向某一个方向的干扰数据时。结果就不那么乐观了。下图就非常说明问题。
数据点中有 20% 是干扰数据。
拟合出的圆就明显被拽偏了。
之所以出现这个问题就是由于最小二乘拟合的平方项对离群点非常敏感。解决问题就要用其它的拟合算法,比方用距离之和作为拟合判据。等我有空了再写一篇博客介绍其它几种方法。