空间直线的最小二乘拟合

转自:http://blog.chinaunix.net/uid-25296429-id-4901934.html
注意:用(y-y0)/n = (z-z0)/m1 = x/1推导得出的m1理想状态下应为m的倒数,但实际情况并非如此。

可推算m*m1 = sqr(p(x,z)) = sqr(conv(x, z))/(var(x)*var(z)), 也就是x和z线性相关系数的平方。

理论推导过程:

求矩阵即可得到m,n,x0,y0,直线方程的各个参数即为所得。
#include 
#include 
#include 
#include 
#define MAX 10
#define PtSum 38
#define MTX_AMT 100


void Inverse(double *matrix1[],double *matrix2[],int n,double d);
double Determinant(double* matrix[],int n);
double AlCo(double* matrix[],int jie,int row,int column);
double Cofactor(double* matrix[],int jie,int row,int column);
void Empty(double *matrix[],int row,int column);


int main(int argc, CHAR* argv[])
{
    double array[MTX_AMT][3],*Matrix[2],*IMatrix[2];//允许有MTX_AMT个点
    //设定待拟合的直线(x -1)/3 = (y - 2)/2 = z/1
    //x = 3z + 1
    //y = 2z + 2
    for (int i = 0; i < PtSum;i++)
    {
        array[i][2] = (double)i; 
        array[i][0] = 3*array[i][2] + 1 + rand()/RAND_MAX;  //引入噪声
        array[i][1] = 2*array[i][2] + 2 + rand()/RAND_MAX;  //引入噪声
    }
    double X0,Y0,m,n;//这是待拟合的四个参数,已知分别对应:1、2、3、2现在我们来拟合
    for ( i = 0;i < 2;i++)
    {
        Matrix[i] =  new double[2];
        IMatrix[i] = new double[2];
    }
    Empty(Matrix,2,2);//将矩阵的前两行两列置位0


    for (int j = 0;j < PtSum;j++)//
    {
        *(Matrix[0] + 0) += array[j][2]*array[j][2];//Matrix[0][0]=array[0][2]*array[0][2]=z*z
        *(Matrix[0] + 1) += array[j][2];//Matrix[0][1]=array[0][2]=z
    }
    *(Matrix[1] + 0) = *(Matrix[0] + 1);//Matrix[1][0]=z;
    *(Matrix[1] + 1) = 1.0*PtSum;//Matrix[1][1]=n;


//下面两步是求Matrix的逆矩阵并存到IMatrix中
//A^(-1)=(1/|A|)×A* ,其中A^(-1)表示矩阵A的逆矩阵,其中|A|为矩阵A的行列式,A*为矩阵A的伴随矩阵。
    double d = Determinant(Matrix,2);
    Inverse(Matrix,IMatrix,2,d);//反转存到IMatrix中


    Empty(Matrix,2,2);
    for ( i = 0;i < PtSum;i++)
    {
        *(Matrix[0] + 0) += array[i][0]*array[i][2];//xi*zi的和
        *(Matrix[0] + 1) += array[i][0];//xi
        *(Matrix[1] + 0) += array[i][1]*array[i][2];//yi*zi的和
        *(Matrix[1] + 1) += array[i][1];//yi
    }
    m = *(Matrix[0] + 0)**(IMatrix[0] + 0) + *(Matrix[0] + 1)**(IMatrix[1] + 0);
    X0= *(Matrix[0] + 0)**(IMatrix[0] + 1) + *(Matrix[0] + 1)**(IMatrix[1] + 1);
    n = *(Matrix[1] + 0)**(IMatrix[0] + 0) + *(Matrix[1] + 1)**(IMatrix[1] + 0);
    Y0= *(Matrix[1] + 0)**(IMatrix[0] + 1) + *(Matrix[1] + 1)**(IMatrix[1] + 1);
    printf("%5.2f\t%5.2f\t%5.2f\t%5.2f\t",X0,Y0,m,n);
    for ( i = 0;i < 2;i++)
    {
        delete[] Matrix[i];
        delete[] IMatrix[i];
    }
    getchar();
    return 0;
}


void Empty(double *matrix[],int row,int column)//初始化
{
    for (int i = 0;i < row;i++)
    {
        for (int j = 0;j < column;j++)
        {
            *(matrix[i] + j) = 0.0;
        }
    }
}


void Inverse(double *matrix1[],double *matrix2[],int n,double d) //

    int i,j; 
    for(i=0;i<n;i++) 
         matrix2[i]=(double *)malloc(n*sizeof(double)); //给IMatrix一维指针分配空间
    for(i=0;i<n;i++) 
         for(j=0;j<n;j++) 
             *(matrix2[j]+i)=(AlCo(matrix1,n,i,j)/d); //给IMatrix二维数据分配空间



double Determinant(double* matrix[],int n) // 计算行列式
{  
    double result=0,temp;  
    int i;  
    if(n == 1)  
        result=(*matrix[0]);  
    else  
    {  
        for(i=0;i<n;i++)  
         {  
            temp=AlCo(matrix,n,n-1,i);  
            result+=(*(matrix[n-1]+i))*temp;  
        }  
    }  
    return result;  
}  


double AlCo(double* matrix[],int jie,int row,int column)  //
{  
    double result; 
    if((row+column)%2 == 0) //如果不是偶数倍
        result = Cofactor(matrix,jie,row,column);  
    else result=(-1)*Cofactor(matrix,jie,row,column); 
    return result;  
}  


double Cofactor(double* matrix[],int jie,int row,int column)  
{  
    double result;  
    int i,j;  
    double* smallmatr[MAX-1];  
    for(i=0;i<jie-1;i++)  
         smallmatr[i]= new double[jie - 1];//n*n矩阵
    for(i=0;i<row;i++)  
         for(j=0;j<column;j++)  
             *(smallmatr[i]+j)=*(matrix[i]+j);  //给smallmatr赋值
    for(i=row;i<jie-1;i++)  
         for(j=0;j<column;j++)  
             *(smallmatr[i]+j)=*(matrix[i+1]+j);  
    for(i=0;i<row;i++)  
         for(j=column;j<jie-1;j++)  
             *(smallmatr[i]+j)=*(matrix[i]+j+1);  
    for(i=row;i<jie-1;i++)  
         for(j=column;j<jie-1;j++)  
             *(smallmatr[i]+j)=*(matrix[i+1]+j+1);  
    result = Determinant(smallmatr,jie-1); 
    for(i=0;i<jie-1;i++)
         delete[] smallmatr[i];
    return result;   


<think>我们使用整体最小二乘(Total Least Squares)方法拟合空间直线空间直线可以表示为两个平面的交线,但更直接的方式是使用点向式(一点和方向向量)。参数化形式为: $$ \mathbf{p} = \mathbf{p}_0 + t \cdot \mathbf{d}, \quad t \in \mathbb{R} $$ 其中$\mathbf{p}_0$是直线上一点,$\mathbf{d}$是方向向量(单位向量)。 给定一组三维点$\{\mathbf{p}_i = (x_i, y_i, z_i) \mid i=1,\dots,n\}$,我们要求一个空间直线使得所有点到直线的距离平方和最小。 误差函数为点到直线的距离: $$ d_i = \| \mathbf{p}_i - \mathbf{p}_0 \|^2 - \frac{[(\mathbf{p}_i-\mathbf{p}_0)\cdot\mathbf{d}]^2}{\|\mathbf{d}\|^2} $$ 由于$\|\mathbf{d}\|=1$,则: $$ d_i = \| \mathbf{p}_i - \mathbf{p}_0 \|^2 - [(\mathbf{p}_i-\mathbf{p}_0)\cdot\mathbf{d}]^2 $$ 整体最小二乘通过最小化投影误差来求解。常用方法为奇异值分解(SVD)或特征值分解(EVD)。 ### 算法步骤 1. **计算质心**: $$ \bar{\mathbf{p}} = \frac{1}{n} \sum_{i=1}^{n} \mathbf{p}_i $$ 2. **计算去质心坐标**: $$ \mathbf{q}_i = \mathbf{p}_i - \bar{\mathbf{p}} $$ 3. **构建矩阵**: 将去质心坐标点按行排列成矩阵: $$ A = \begin{bmatrix} \mathbf{q}_1 \\ \mathbf{q}_2 \\ \vdots \\ \mathbf{q}_n \end{bmatrix} = \begin{bmatrix} x_1-\bar{x} & y_1-\bar{y} & z_1-\bar{z} \\ x_2-\bar{x} & y_2-\bar{y} & z_2-\bar{z} \\ \vdots & \vdots & \vdots \\ x_n-\bar{x} & y_n-\bar{y} & z_n-\bar{z} \end{bmatrix} $$ 4. **计算协方差矩阵**: $$ C = A^T A \quad (3\times3) $$ 5. **特征值分解**: 对$C$进行特征分解: $$ C = V \Lambda V^T $$ 其中特征值按从小到大排列:$\lambda_1 \leq \lambda_2 \leq \lambda_3$,对应的特征向量为$\mathbf{v}_1, \mathbf{v}_2, \mathbf{v}_3$。 6. **方向向量**: 最小特征值对应的特征向量$\mathbf{v}_1$即为直线的方向向量$\mathbf{d}$(因为直线方向是数据变化最小的方向)。 7. **直线上点**: 质心$\bar{\mathbf{p}}$位于直线上,因此$\mathbf{p}_0 = \bar{\mathbf{p}}$。 ### 证明 上述方法等价于最小化所有点到直线的距离平方和。因为点到直线的距离平方和可以表示为: $$ \sum_{i=1}^{n} d_i^2 = \sum_{i=1}^{n} \| \mathbf{q}_i \|^2 - \mathbf{d}^T C \mathbf{d} $$ 由于$\sum \| \mathbf{q}_i \|^2$是常数,所以最小化距离平方和等价于最大化$\mathbf{d}^T C \mathbf{d}$(在$\|\mathbf{d}\|=1$约束下)。根据Rayleigh商,最大值在最大特征值对应的特征向量处取得,最小值在最小特征值对应的特征向量处。但注意:距离平方和公式中,我们最小化的是$\sum \| \mathbf{q}_i \|^2 - \mathbf{d}^T C \mathbf{d}$,因此要最小化距离平方和,就要最大化$\mathbf{d}^T C \mathbf{d}$?不对,重新推导: 实际上,点到直线的距离平方和可以写为: $$ \sum_{i=1}^{n} \| \mathbf{q}_i - (\mathbf{d} \mathbf{d}^T) \mathbf{q}_i \|^2 = \sum_{i=1}^{n} \| \mathbf{q}_i \|^2 - \mathbf{d}^T \left( \sum_{i=1}^{n} \mathbf{q}_i \mathbf{q}_i^T \right) \mathbf{d} = \sum \| \mathbf{q}_i \|^2 - \mathbf{d}^T C \mathbf{d} $$ 因此,最小化距离平方和等价于最大化$\mathbf{d}^T C \mathbf{d}$(因为后面是减去它)。所以,我们应取最大特征值对应的特征向量作为方向向量?这里存在一个常见的混淆。 注意:协方差矩阵$C$的特征值表示数据在该特征向量方向上的方差。而直线拟合中,我们希望直线方向是数据分布最分散的方向(即最大方差方向),这样点到直线的投影距离(即残差)最小。因此,我们取最大特征值对应的特征向量作为直线方向。 但是,在平面拟合中,我们取最小特征值对应的特征向量作为法向量(因为平面法向量方向是数据变化最小的方向)。而直线拟合则相反,取最大特征值对应的特征向量作为方向向量。 因此,修正步骤6:方向向量取最大特征值对应的特征向量。 ### 修正后的步骤 1. 计算质心$\bar{\mathbf{p}}$。 2. 计算去质心坐标$\mathbf{q}_i$。 3. 构建矩阵$A$($n\times 3$)。 4. 计算协方差矩阵$C = A^T A$($3\times 3$)。 5. 对$C$进行特征分解:$C = V \Lambda V^T$,特征值按从大到小排列:$\lambda_1 \geq \lambda_2 \geq \lambda_3$,对应的特征向量为$\mathbf{v}_1, \mathbf{v}_2, \mathbf{v}_3$。 6. 直线的方向向量为$\mathbf{d} = \mathbf{v}_1$(最大特征值对应的特征向量)。 7. 直线上一点为$\mathbf{p}_0 = \bar{\mathbf{p}}$。 ### C++实现(使用Eigen库) ```cpp #include <iostream> #include <vector> #include <Eigen/Dense> using namespace Eigen; struct Line3D { Vector3d point; // 直线上一点 Vector3d direction; // 方向向量(单位向量) }; bool fitLine3D(const std::vector<Vector3d>& points, Line3D& line) { const int n = points.size(); if (n < 2) return false; // 至少需要两个点 // 1. 计算质心 Vector3d centroid(0, 0, 0); for (const auto& p : points) centroid += p; centroid /= n; // 2. 构建去质心数据矩阵 A (n x 3) MatrixXd A(n, 3); for (int i = 0; i < n; ++i) { A.row(i) = points[i] - centroid; } // 3. 计算协方差矩阵 C = A^T * A (3x3) Matrix3d C = A.transpose() * A; // 4. 特征分解 EigenSolver<Matrix3d> solver(C); Vector3d eigenvalues = solver.eigenvalues().real(); Matrix3d eigenvectors = solver.eigenvectors().real(); // 5. 找到最大特征值的索引 int max_index = 0; double max_eigenvalue = eigenvalues[0]; for (int i = 1; i < 3; ++i) { if (eigenvalues[i] > max_eigenvalue) { max_eigenvalue = eigenvalues[i]; max_index = i; } } // 6. 方向向量为最大特征值对应的特征向量 line.direction = eigenvectors.col(max_index).normalized(); // 确保单位向量 line.point = centroid; return true; } int main() { // 示例:沿x轴的点 std::vector<Vector3d> points; points.push_back(Vector3d(0, 0, 0)); points.push_back(Vector3d(1, 0, 0)); points.push_back(Vector3d(2, 0, 0)); points.push_back(Vector3d(3, 0, 0)); Line3D line; if (fitLine3D(points, line)) { std::cout << "Point on line: " << line.point.transpose() << std::endl; std::cout << "Direction vector: " << line.direction.transpose() << std::endl; } return 0; } ``` ### 说明 1. **特征分解**:使用`EigenSolver`,注意提取实特征值和实特征向量(因为协方差矩阵是实对称矩阵,特征值都是实数)。 2. **单位化**:特征向量可能是任意长度,我们将其单位化。 3. **稳定性**:当数据点共线时,最大特征值远大于其余两个;当数据点分散时,三个特征值都较大。如果数据点完全共线,则最小两个特征值为0(理论上)。 4. **精度评估**:计算点到直线的距离平方和的平均值作为拟合误差。 ### 点到直线的距离计算 ```cpp double distancePointToLine(const Vector3d& point, const Line3D& line) { Vector3d vec = point - line.point; // 计算垂直投影的向量 Vector3d proj = vec.dot(line.direction) * line.direction; // 计算垂线向量 Vector3d perp = vec - proj; return perp.norm(); } ``` ### 相关问题 1. **如何评估空间直线拟合的精度?** 计算所有点到直线的距离,然后求均方根误差(RMSE): $$ \text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} d_i^2} $$ 其中$d_i$是第$i$个点到直线的距离。 2. **当数据点存在较大噪声时,如何提高拟合精度?** 可采用RANSAC(随机采样一致性)算法进行鲁棒拟合[^2]。其步骤为: - 随机选择两点确定一条直线 - 计算其他点到该直线的距离,统计内点(距离小于阈值的点) - 重复多次,选择内点最多的直线 - 用所有内点重新拟合直线(使用上述最小二乘法) 3. **空间直线拟合与平面拟合有何异同?** - **相同点**:都使用PCA(主成分分析)方法,基于协方差矩阵的特征分解。 - **不同点**: - 平面拟合:取最小特征值对应的特征向量作为法向量。 - 直线拟合:取最大特征值对应的特征向量作为方向向量。 4. **如何将拟合结果转换为参数方程或对称式方程?** 已知一点$\mathbf{p}_0=(x_0,y_0,z_0)$和方向向量$\mathbf{d}=(l,m,n)$,则对称式方程为: $$ \frac{x-x_0}{l} = \frac{y-y_0}{m} = \frac{z-z_0}{n} $$ 当方向向量分量有0时,需特殊处理(避免除0)。 5. **如果数据点近似共线但不完全共线,拟合结果是否稳定?** 是稳定的,因为PCA对数据中的小扰动不敏感。但应注意,当数据点分布范围较小时,拟合结果可能对噪声敏感,因此应尽量使用分布范围广的数据点。 [^1]: 整体最小二乘通过奇异值分解求解,适用于误差在多个维度的情况[^1]。 [^2]: RANSAC是一种鲁棒估计算法,能够有效处理包含大量离群点的数据[^2]。</think>### C++ 实现最小二乘拟合三维空间直线的方法 #### 算法原理 空间直线拟合使用整体最小二乘法(Total Least Squares),通过奇异值分解(SVD)求解。直线参数化形式为: $$ \mathbf{p} = \mathbf{p}_0 + t \cdot \mathbf{d} $$ 其中: - $\mathbf{p}_0$ 是直线上一点 - $\mathbf{d}$ 是方向向量(单位向量) - $t$ 是参数 误差函数为点到直线的垂直距离: $$ d_i = \| (\mathbf{p}_i - \mathbf{p}_0) \times \mathbf{d} \| $$ #### 数学推导 1. **计算质心**: $$ \bar{\mathbf{p}} = \frac{1}{n}\sum_{i=1}^{n}\mathbf{p}_i $$ 2. **构建协方差矩阵**: $$ C = \sum_{i=1}^{n} (\mathbf{p}_i - \bar{\mathbf{p}})(\mathbf{p}_i - \bar{\mathbf{p}})^T $$ 3. **特征值分解**: $$ C = V \Lambda V^T $$ 最大特征值对应的特征向量即为直线方向 $\mathbf{d}$,质心 $\bar{\mathbf{p}}$ 作为直线上点 $\mathbf{p}_0$[^1]。 #### C++ 实现(使用 Eigen 库) ```cpp #include <iostream> #include <vector> #include <Eigen/Dense> #include <Eigen/Eigenvalues> using namespace Eigen; struct Line3D { Vector3d point; // 直线上一点 Vector3d direction; // 方向向量(单位化) }; bool fitLine3D(const std::vector<Vector3d>& points, Line3D& line) { const size_t n = points.size(); if (n < 2) return false; // 至少需要两个点 // 1. 计算质心 Vector3d centroid(0, 0, 0); for (const auto& p : points) centroid += p; centroid /= n; // 2. 构建协方差矩阵 Matrix3d cov = Matrix3d::Zero(); for (const auto& p : points) { Vector3d d = p - centroid; cov += d * d.transpose(); } cov /= n; // 3. 特征值分解 SelfAdjointEigenSolver<Matrix3d> solver(cov); Matrix3d eigenvectors = solver.eigenvectors(); // 4. 取最大特征值对应的特征向量作为方向 Vector3d eigenvalues = solver.eigenvalues(); int max_index = 0; for (int i = 1; i < 3; ++i) { if (eigenvalues[i] > eigenvalues[max_index]) max_index = i; } line.direction = eigenvectors.col(max_index).normalized(); line.point = centroid; return true; } // 计算点到直线的距离 double distanceToLine(const Vector3d& point, const Line3D& line) { Vector3d diff = point - line.point; Vector3d cross = diff.cross(line.direction); return cross.norm(); } int main() { // 测试数据:沿x轴方向的点 std::vector<Vector3d> points = { {0, 0.1, 0.2}, {1, -0.1, 0.1}, {2, 0.2, -0.1}, {3, -0.2, 0.1} }; Line3D fitted_line; if (fitLine3D(points, fitted_line)) { std::cout << "Point on line: " << fitted_line.point.transpose() << "\n"; std::cout << "Direction: " << fitted_line.direction.transpose() << "\n"; // 计算拟合误差 double total_error = 0; for (const auto& p : points) { double d = distanceToLine(p, fitted_line); total_error += d * d; std::cout << "Distance to line: " << d << "\n"; } std::cout << "RMSE: " << sqrt(total_error / points.size()) << "\n"; } return 0; } ``` #### 关键说明 1. **特征值分解**:协方差矩阵的特征向量对应数据的主方向,最大特征值对应的特征向量即直线方向[^1] 2. **单位化**:`normalized()` 确保方向向量长度为1 3. **距离计算**:利用叉乘模长计算点到直线的垂直距离 $$ d = \| (\mathbf{p} - \mathbf{p}_0) \times \mathbf{d} \| $$ 4. **鲁棒性处理**:添加正则化防止奇异矩阵 ```cpp cov += 1e-8 * Matrix3d::Identity(); // 正则化项 ``` #### 算法复杂度 - **时间复杂度**:$O(n)$(数据点数) + $O(1)$(固定大小的矩阵分解) - **空间复杂度**:$O(1)$(仅存储3×3矩阵) #### 拟合精度评估 计算均方根误差(RMSE): $$ \text{RMSE} = \sqrt{\frac{1}{n}\sum_{i=1}^{n} d_i^2} $$ 其中 $d_i$ 是第 $i$ 个点到直线的距离。 #### 应用场景 1. 工业机器人轨迹校准 2. 点云数据处理(如PCL库中的特征提取) 3. 三维重建中的边缘检测 4. 运动目标轨迹分析 ### 相关问题 1. **如何评估空间直线拟合的精度?** 计算所有点到直线的距离平方和,然后求均方根误差(RMSE)[^2]: $$ \text{RMSE} = \sqrt{\frac{1}{n}\sum_{i=1}^{n} \left\| (\mathbf{p}_i - \mathbf{p}_0) \times \mathbf{d} \right\|^2} $$ 2. **当数据存在离群点时如何提高拟合精度?** 采用RANSAC(随机采样一致性)算法: - 随机选择两点确定初始直线 - 计算内点(距离小于阈值的点) - 重复迭代,选择内点最多的模型 - 用所有内点重新拟合直线[^2] 3. **空间直线拟合与平面拟合有何异同?** - **相同点**:都使用PCA和特征值分解 - **不同点**: - 直线拟合取最大特征值对应的特征向量 - 平面拟合取最小特征值对应的特征向量(法向量) 4. **如何将拟合结果转换为参数方程?** 给定 $\mathbf{p}_0 = (x_0,y_0,z_0)$ 和 $\mathbf{d} = (a,b,c)$,参数方程为: $$ \begin{cases} x = x_0 + a \cdot t \\ y = y_0 + b \cdot t \\ z = z_0 + c \cdot t \end{cases} \quad t \in \mathbb{R} $$ 5. **当数据点完全共线时拟合结果如何?** 协方差矩阵会有两个零特征值,最大特征值对应的特征向量即直线方向,此时所有点到直线的距离为0[^1]。 [^1]: 整体最小二乘通过特征值分解求解直线参数,最大化投影方差[^1]。 [^2]: RANSAC算法能有效处理包含离群点的数据[^2]。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值