用C#完成最小二乘法拟合平面方程,再计算点到面的距离

发布于:2025-06-02 ⋅ 阅读:(26) ⋅ 点赞:(0)

用C#完成最小二乘法拟合平面方程,再计算点到面的距离

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {

            double[] Xs =
                { 1, 2, 3, 4,
                1, 2, 3, 4,
                1, 2, 3, 4,
                1, 2, 3, 4 };

            double[] Ys =
                { 1, 1, 1, 1,
                2, 2, 2, 2,
                3, 3, 3, 3,
                4, 4, 4, 4};

            double[] Zs =
                {0, 0, 0, 0,
                0, 1, 1, 0,
                0, 1, 1, 0,
                0, 0, 0, 0 };

            double A, B, C, D;
            string strMsg = string.Empty;
            Operator3D.FitPlane(Xs, Ys, Zs, out A, out B, out C, out D, out strMsg);


            double dist = Operator3D.DistanceP2Plane(0, 0, 0, A, B, C, D, out strMsg);
        }
    }

    class Operator3D
    {
        /// <summary>
        /// 最小二乘法拟合平面方程,该方法通过计算协方差矩阵,并使用雅可比(Jacobi)方法求解其特征向量,从而得到最佳拟合平面的参数A\B\C\D
        /// </summary>
        /// <param name="Xs">三维点X坐标</param>
        /// <param name="Ys">三维点Y坐标</param>
        /// <param name="Zs">三维点Z坐标</param>
        /// <param name="A">平面方程一般式参数A</param>
        /// <param name="B">平面方程一般式参数B</param>
        /// <param name="C">平面方程一般式参数C</param>
        /// <param name="D">平面方程一般式参数D</param>
        /// <param name="errorMsg"></param>
        /// <returns></returns>
        public static bool FitPlane(double[] Xs, double[] Ys, double[] Zs, out double A, out double B, out double C, out double D, out string error)
        {
            error = string.Empty;
            A = B = C = D = 0;

            try 
            {
                // 验证输入数据有效性:保证输入数据不为空,数据长度一致,且至少有3个点
                if (Xs == null || Ys == null || Zs == null)
                {
                    error = "输入Xs、Ys、Zs数据为null";
                    return false;
                }
                else if (Xs.Length != Ys.Length || Xs.Length != Zs.Length)
                {
                    error = "输入Xs、Ys、Zs数据长度不同";
                    return false;
                }
                else if (Xs.Length < 3)
                {
                    error = "输入数据点数小于3";
                    return false;
                }

                int n = Xs.Length;

                // 计算质心
                double x0 = Average(Xs);
                double y0 = Average(Ys);
                double z0 = Average(Zs);

                // 计算协方差矩阵
                double[,] cov = new double[3, 3];
                for (int i = 0; i < n; i++)
                {
                    double dx = Xs[i] - x0;
                    double dy = Ys[i] - y0;
                    double dz = Zs[i] - z0;

                    cov[0, 0] += dx * dx;
                    cov[0, 1] += dx * dy;
                    cov[0, 2] += dx * dz;

                    cov[1, 1] += dy * dy;
                    cov[1, 2] += dy * dz;

                    cov[2, 2] += dz * dz;
                }
                // 填充对称元素
                cov[1, 0] = cov[0, 1];
                cov[2, 0] = cov[0, 2];
                cov[2, 1] = cov[1, 2];

                // 计算特征值和特征向量
                if (!JacobiEigenvalue(cov, out double[] eigenvalues, out double[,] eigenvectors))
                {
                    error = "雅可比特征值分解失败";
                    return false;
                }


                // 找到最小特征值的索引
                int minIndex = 0;
                for (int i = 1; i < 3; i++)
                    if (eigenvalues[i] < eigenvalues[minIndex])
                        minIndex = i;

                // 获取法向量
                A = eigenvectors[0, minIndex];
                B = eigenvectors[1, minIndex];
                C = eigenvectors[2, minIndex];

                // 计算D
                D = -(A * x0 + B * y0 + C * z0);

                // 归一化平面方程(可选)
                double norm = Math.Sqrt(A * A + B * B + C * C);
                if (norm < 1e-12) // 避免除以零
                {
                    error = "除数不能为零";
                    return false;
                }

                A /= norm;
                B /= norm;
                C /= norm;
                D /= norm;

                return true;
            }
            catch 
            {
                error = "拟合平面失败";
                return false;
            }
        }

        //计算平均数
        private static double Average(double[] arr)
        {
            double sum = 0;
            foreach (double d in arr)
                sum += d;
            return sum / arr.Length;
        }

        //雅可比特征值分解
        private static bool JacobiEigenvalue(double[,] matrix, out double[] eigenvalues, out double[,] eigenvectors)
        {
            int n = 3;
            eigenvalues = new double[n];
            eigenvectors = new double[n, n];
            for (int i = 0; i < n; i++)
            {
                eigenvectors[i, i] = 1.0;
                for (int j = 0; j < n; j++)
                    if (i != j)
                        eigenvectors[i, j] = 0.0;
            }

            const int maxIterations = 50;
            const double epsilon = 1e-12;

            for (int k = 0; k < maxIterations; k++)
            {
                // 找到最大的非对角元素
                int p = 0, q = 0;
                double maxVal = 0;
                for (int i = 0; i < n; i++)
                    for (int j = i + 1; j < n; j++)
                        if (Math.Abs(matrix[i, j]) > maxVal)
                        {
                            maxVal = Math.Abs(matrix[i, j]);
                            p = i;
                            q = j;
                        }

                if (maxVal < epsilon)
                    break;

                // 计算旋转角度
                double theta = 0.5 * Math.Atan2(2 * matrix[p, q], matrix[q, q] - matrix[p, p]);
                double c = Math.Cos(theta);
                double s = Math.Sin(theta);

                // 更新矩阵
                double new_pp = c * c * matrix[p, p] - 2 * c * s * matrix[p, q] + s * s * matrix[q, q];
                double new_qq = s * s * matrix[p, p] + 2 * c * s * matrix[p, q] + c * c * matrix[q, q];
                double new_pq = (c * c - s * s) * matrix[p, q] + c * s * (matrix[p, p] - matrix[q, q]);

                matrix[p, p] = new_pp;
                matrix[q, q] = new_qq;
                matrix[p, q] = matrix[q, p] = new_pq;

                // 更新其他行和列
                for (int i = 0; i < n; i++)
                {
                    if (i != p && i != q)
                    {
                        double new_ip = c * matrix[i, p] - s * matrix[i, q];
                        double new_iq = s * matrix[i, p] + c * matrix[i, q];
                        matrix[i, p] = matrix[p, i] = new_ip;
                        matrix[i, q] = matrix[q, i] = new_iq;
                    }
                }

                // 更新特征向量
                for (int i = 0; i < n; i++)
                {
                    double temp = eigenvectors[i, p];
                    eigenvectors[i, p] = c * temp - s * eigenvectors[i, q];
                    eigenvectors[i, q] = s * temp + c * eigenvectors[i, q];
                }
            }

            // 提取特征值
            for (int i = 0; i < n; i++)
                eigenvalues[i] = matrix[i, i];

            return true;
        }

        /// <summary>
        /// 计算点到平面的带符号距离
        /// </summary>
        /// <param name="X">点的X坐标</param>
        /// <param name="Y">点的Y坐标</param>
        /// <param name="Z">点的Z坐标</param>
        /// <param name="A">平面方程系数A</param>
        /// <param name="B">平面方程系数B</param>
        /// <param name="C">平面方程系数C</param>
        /// <param name="D">平面方程系数D</param>
        /// <returns>
        /// 带符号的距离值:
        /// - 正值表示点在法向量指向的一侧
        /// - 负值表示点在法向量反向的一侧
        /// - 零表示点在平面上
        /// </returns>
        public static double DistanceP2Plane(double X, double Y, double Z, double A, double B, double C, double D, out string error)
        {
            error = string.Empty;
            try
            {
                // 计算公式:(A*X + B*Y + C*Z + D) / √(A² + B² + C²)
                double numerator = A * X + B * Y + C * Z + D;
                double denominator = Math.Sqrt(A * A + B * B + C * C);

                // 处理极小值防止除以零
                if (Math.Abs(denominator) < 1e-12)
                {
                    error = "非法平面参数";
                    return double.NaN; // 非法平面参数时返回NaN
                }
                return numerator / denominator;
            }
            catch (Exception)
            {
                error = "非法平面参数,计算失败";
                throw;
            }
        }

在这里插入图片描述