赞
踩
本文主要介绍最小二乘的直接求解法和SVD分解法,利用Eigen库进行矩阵运算求解。 假设有一散点集合,当各点v到一平面S的距离d的平方和最小,该平面s即为最小二乘下的最优解。将距离平方和描述为平面拟合误差
(1)
定义点类如下
- class vec
- {
- public:
- float v[3];
- vec(){}
- vec(float x,float y ,float z)
- {
- v[0] = x; v[1] = y; v[2] = z;
- }
- const float &operator [] (int i) const
- {
- return v[i];
- }
- float &operator [] (int i)
- {
- return v[i];
- }
- };
平面方程表示为
两边同时除以C,
令
方程则可以表示为
先介绍下用定义求解的方式:
(1)式误差可以描述为
要使e最小,应满足
列出如下方程
改写为矩阵
求解上述矩阵获得a0,a1,a2的值,即可求得平面方程。代码如下
- void ComputePlane(const vector<vec>&points, float &A, float &B, float &C, float &D)
- {
- int n = points.size();
- MatrixXd mleft = MatrixXd::Zero(3, 3);//初始化左边项
- VectorXd b = VectorXd::Zero(3); //初始化右边项
- for (int i = 0; i < n; ++i)//遍历点装填两个矩阵
- {
- mleft(0, 0) += points[i][0] * points[i][0];//a11,xi平方和
- mleft(0, 1) += points[i][0] * points[i][1];//a12=a21,xiyi和
- mleft(0, 2) += points[i][0];//a13=a31,xi和
- mleft(1, 1) += points[i][1] * points[i][1];//a22,yi平方和
- mleft(1, 2) += points[i][1];//a23=a32,yi和
- b[0] += points[i][0] * points[i][2];//xizi和
- b[1] += points[i][1] * points[i][2];//yizi和
- b[2] += points[i][2];//zi和
- }
- mleft(2, 2) = n;//a33,n
- mleft(1, 0) = mleft(0, 1);
- mleft(2, 0) = mleft(0, 2);
- mleft(2, 1) = mleft(1, 2);
- VectorXd x = mleft.fullPivHouseholderQr().solve(b);//QR分解求解
- float a0 = x[0]; float a1 = x[1]; float a2 = x[2];//为了便于理解,赋值给临时变量
- A = a0; B = a1; C = -1; D = a2;
- }
然而这种方式还需要计算左边项和右边项各元素的值,相对繁琐,Eigen库可以直接求解超定方程组,左边项为散点x,y值与1,右边项为z值,列出如下方程组直接求解得到的即是最小二乘解。
实现代码如下,传入点集points和待求解平面系数ABCD。不论是采用定义法建立3*3的左边项还是解如下超定方程组,获得的结果是一样的,证明Eigen库求解超静定问题内部采用的是最小二乘的思想。
- void ComputePlane(const vector<vec>&points, float &A, float &B, float &C, float &D)
- {
- int n = points.size();
- MatrixXd mleft = MatrixXd::Ones(n, 3);//初始化左边项
- VectorXd b = VectorXd::Zero(n); //初始化右边项
- for (int i = 0; i < n; ++i)//遍历点装填两个矩阵
- {
- mleft(i, 0) = points[i][0];
- mleft(i, 1) = points[i][1];
- b[i] = points[i][2];
- }
- VectorXd x = mleft.fullPivHouseholderQr().solve(b);//QR分解求解
- float a0 = x[0]; float a1 = x[1]; float a2 = x[2];//为了便于理解,赋值给临时变量
- A = a0; B = a1; C = -1; D = a2;
- }
平面方程表示为ax+by+cz = d,(1)
约束条件为
计算散点坐标的平均值,应满足 (2)
(1)式代入各个散点,(1)-(2)得(3)
列为矩阵,式(3)等价于AX = 0
目标函数为,约束条件,对做奇异值分解,则,且,假设D的最后一个对角元素为最小奇异值,当且仅当
时,取得,此时最小奇异值对应的特征向量即是平面的法向量,即a,b,c。
在Eigen库中,有现成的JacobiSVD类供调用,代码实现如下
- void ComputePlane(const vector<vec>&points, float &A, float &B, float &C, float &D)
- {
- int n = points.size();
- MatrixXd mleft = MatrixXd::Zero(n, 3);//初始化矩阵A
- vec averagepoint(0, 0, 0);//平均坐标
- for (int i = 0; i < n; ++i)
- {
- averagepoint[0] += points[i][0] / n;
- averagepoint[1] += points[i][1] / n;
- averagepoint[2] += points[i][2] / n;
- }
- for (int i = 0; i < n; ++i)//遍历点装填矩阵
- {
- mleft(i, 0) = points[i][0] - averagepoint[0];
- mleft(i, 1) = points[i][1] - averagepoint[1];
- mleft(i, 2) = points[i][2] - averagepoint[2];
- }
- JacobiSVD<MatrixXd> svd(mleft, ComputeFullU | ComputeFullV);//对矩阵A分解
- MatrixXd vt = svd.matrixV().transpose();//获得矩阵v的转置
- Vector3d b(0, 0, 1);
- VectorXd x = vt.fullPivHouseholderQr().solve(b);
- A = x[0]; B = x[1]; C = x[2];
- D = A*averagepoint[0] + B*averagepoint[1] + C * averagepoint[2];
- }
main函数的内容和输出结果如下。输入随机点个数n,随机点的坐标值为0-10的浮点型,输出点坐标集和最后拟合的平面方程。
- int main()
- {
- vector<vec>points;
- int n = 0;
- cin >> n;
- for (int i = 0; i < n;i++)
- {
- float x, y, z;//在0-1内取随机数
- x = double(rand()) / RAND_MAX*10;
- y = double(rand()) / RAND_MAX*10;
- z = double(rand()) / RAND_MAX*10;
- points.push_back(vec(x,y,z));
- }
- for (int i = 0; i < n; i++)
- {
- cout << "(" << points[i][0] << "," << points[i][1] << "," << points[i][2] << ")" << endl;
- }
- float A, B, C, D;
- ComputePlane(points, A, B, C, D);
- cout << "平面方程为 "<<A << "x+" << B << "y+" << C << "z =" << D << endl;
- system("pause");
- return 0;
- }
在做自己的项目过程中,发现svd分解法得到的平面更加准确,如下柱状图拟合1000个平面,横坐标表示平面的序号,纵坐标表示拟合点距离该平面的最大距离,左边是svd分解的结果,最大值在1.2以下,而右边直接求解的结果大于1.2的平面很多,当然检查它们的距离平方和结果也是一样的,svd同样优于直接求解,感兴趣的可以自行验证下。
参考链接
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。