說明
很多時候,我們需要運動物體的轉彎半徑去描述其機器效能。但在大多數的現實條件下,我們只能夠獲取到運動物體的 GPS 位置點集,並不能直接得到轉彎半徑或者圓心位置。為此,我們可以利用擬合圓的方式得到圓座標方程,由此得到轉彎半徑和圓心位置。
解決過程
關於擬合圓方程的方法有很多,曾經在這篇譯文中獲益良多代數逼近法、最小二乘法、正交距離迴歸法來擬合圓及其結果對比(Python)。此係列文中也給出了提及的三種方法的效能及效果對比,最終得出最優的解決方案就是最小二乘法。由於最近的學習中又進一步瞭解到,可以利用線性代數的方法去求解。本著大學課程中曾學過的《線性代數》知識,所以想著用此方法再加以解決該問題,以作最對比。
接下來,本文就最小二乘法和線性代數的方法求取圓方程作一論述。
準備
引用矩陣計算庫MathNet.Numerics。該庫是一個強大的科學計算庫,遵循 .Net Standard,所以可以跨平臺使用。
建立描述圓的類
public class Circle
{
/// <summary>
/// 圓心橫座標
/// </summary>
/// <value></value>
public double X { get; set; }
/// <summary>
/// 圓心縱座標
/// </summary>
/// <value></value>
public double Y { get; set; }
/// <summary>
/// 圓半徑
/// </summary>
/// <value></value>
public double R { get; set; }
}
畫圖,引用System.Drawing.Common庫,以實現跨平臺的影象生成。接下來,我們簡單的實現一個影象幫助類來進行影象繪製。
public class ImageHelp
{
private Image _image;
public ImageHelp(int width, int height)
{
_image = new Bitmap(width, height);
var graph = Graphics.FromImage(_image);
graph.Clear(Color.White);
}
public void DrawCicle(Circle circle, Brush brush)
{
var graph = Graphics.FromImage(_image);
var count=200;
var fitPoints = new Point[count+1];
var step = 2 * Math.PI / count;
for (int i = 0; i < count; i++)
{
//circle
var p = new Point();
p.X = (int)(circle.X + Math.Cos(i * step) * circle.R);
p.Y = (int)(circle.Y + Math.Sin(i * step) * circle.R);
fitPoints[i] = p;
}
fitPoints[count] = fitPoints[0];//閉合圓
graph.DrawLines(new Pen(brush, 2), fitPoints);
graph.Dispose();
}
public void DrawPoints(double[] X, double[] Y, Brush brush)
{
var graph = Graphics.FromImage(_image);
for (int i = 0; i < X.Length; i++)
{
graph.DrawEllipse(new Pen(brush, 2), (int)X[i], (int)Y[i], 6, 6);
}
graph.Dispose();
}
public void SaveImage(string file)
{
_image.Save(file, System.Drawing.Imaging.ImageFormat.Png);
}
}
模擬點集,由於現實中的資料採集存在著精度、資料記錄等眾多不確定因素的影像。模擬點集中也將加入一定程度的噪音。以下程式碼中 x 與 y 中儲存著我們的點集資料:
var count = 50;
var step = 2 * Math.PI / 100;
var rd = new Random();
//參照圓
var x0 = 204.1;
var y0 = 213.1;
var r0 = 98.4;
//噪音絕對差
var diff = (int)(r0 * 0.1);
var x = new double[count];
var y = new double[count];
//輸出點集
for (int i = 0; i < count; i++)
{
//circle
x[i] = x0 + Math.Cos(i * step) * r0;
y[i] = y0 + Math.Sin(i * step) * r0;
//noise
x[i] += Math.Cos(rd.Next() % 2 * Math.PI) * rd.Next(diff);
y[i] += Math.Cos(rd.Next() % 2 * Math.PI) * rd.Next(diff);
}
最小二乘法
網上有很多的原理解析,上文中提到的譯文中也有提及,這裡不在過多贅述。直接貼出 c#程式碼實現:
public Circle LeastSquaresFit(double[] X, double[] Y)
{
if (X.Length < 3)
{
return null;
}
double cent_x = 0.0,
cent_y = 0.0,
radius = 0.0;
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 = X.Length;
double x, y, x2, y2;
for (int i = 0; i < N; i++)
{
x = X[i];
y = Y[i];
x2 = x * x;
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;
cent_x = a / (-2);
cent_y = b / (-2);
radius = Math.Sqrt(a * a + b * b - 4 * c) / 2;
var result = new Circle();
result.X = cent_x;
result.Y = cent_y;
result.R = radius;
return result;
}
線性代數
從標準圓方程(x-c1)^2+(y-c2)^2=r^2
中進行方程變換得到2xc1+2yc2+(r^2−c1^2−c2^2)=x^2+y^2
,其中,我們c3
替換常量值r^2−c1^2−c2^2
,即:r^2−c1^2−c2^2=c3
。由此,我們得到2xc1+2yc2+c3=x^2+y^2
,將點集帶入,方程就只剩三個未知數`c1,c2 和 c3。
簡單起見,假設我們有四個點{[0,5],[0,-5],[5,0],[-5,0]}
,代入方程可得到四個方程:
0c1 + 10c2 + c3 = 25
0c1 - 10c2 + c3 = 25
10c1 + 0c2 + c3 = 25
-10c1 + 0c2 + c3 = 25
該方程組比較簡單,一眼便能看出解。但用線性代數我們可以得到矩陣:
/***************************A**********B******C*/
| 0c1 10c2 1c3| | 0 10 1| |c1| |25|
| 0c1 -10c2 1c3| = | 0 -10 1| * |c2| = |25|
| 10c1 0c2 1c3| | 10 0 1| |c3| |25|
|-10c1 0c2 1c3| |-10 0 1| |25|
在矩陣方程中A*B=C
,只需求出矩陣B
即可得到方程組的解。c#中MathNet.Numerics
可以輕鬆勝任這一工作:
public Circle LinearAlgebraFit(double[] X, double[] Y)
{
if (X.Length < 3)
{
return null;
}
var count = X.Length;
var a = new double[count, 3];
var c = new double[count, 1];
for (int i = 0; i < count; i++)
{
//matrix
a[i, 0] = 2 * X[i];
a[i, 1] = 2 * Y[i];
a[i, 2] = 1;
c[i, 0] = X[i] * X[i] + Y[i] * Y[i];
}
var A = DenseMatrix.OfArray(a);
var C = DenseMatrix.OfArray(c);
//A*B=C
var B = A.Solve(C);
double c1 = B.At(0, 0),
c2 = B.At(1, 0),
r = Math.Sqrt(B.At(2, 0) + c1 * c1 + c2 * c2);
var result = new Circle();
result.X = c1;
result.Y = c2;
result.R = r;
return result;
}
最後總結
Console.WriteLine($"raw c1:{x0}, c2:{y0}, r:{r0}");
var fit = new FitCircle();
var sth = new Stopwatch();
sth.Start();
var lsf = fit.LeastSquaresFit(x, y);![](https://img2018.cnblogs.com/blog/1214143/201908/1214143-20190804173821455-1022769486.jpg)
Console.WriteLine($"LeastSquaresFit c1:{lsf.X}, c2:{lsf.Y}, r:{lsf.R}, time:{sth.Elapsed}");
sth.Restart();
var laf = fit.LinearAlgebraFit(x, y);
Console.WriteLine($"LinearAlgebraFit c1:{laf.X}, c2:{laf.Y}, r:{laf.R}, time:{sth.Elapsed}");
var img = new ImageHelp(512, 512);
img.DrawPoints(x, y, Brushes.Red);
img.DrawCicle(lsf, Brushes.Green);
img.DrawCicle(laf, Brushes.Orange);
img.SaveImage("graph.jpeg");
控制檯輸出:
raw c1:204.1, c2:213.1, r:98.4
LeastSquaresFit c1:204.791071061878, c2:210.86075318831, r:100.436594821545, time:00:00:00.0011029
LinearAlgebraFit c1:204.791071061878, c2:210.860753188315, r:100.436594821541, time:00:00:00.1691119
從結果中可以看出,兩種方法的結果基本一樣,在小數點後好幾位才出現差別。但是其計算效率卻差異巨大,最小二乘法比線性代數快上 100 多倍。
在圖中,二者重合(綠色被後面的橙色覆蓋)。
在最小二乘法中,只有一個及其簡單的 for 迴圈,很少涉及記憶體寫。但線上性代數中,需要進行矩陣的生成DenseMatrix.OfArray
,以及矩陣運算,這二者都需要記憶體寫。再者,矩陣計算有著繁重的計算量,這些都在影響著線性代數擬合圓的效率。最終的勝利還是屬於最小二乘法。