多点拟合求平面的RANSAC算法

对于多点求平面,直接的传统方法是三点求平面,通过求解方程组得到平面的法向量和截距。

但仅适用于点集能够恰好拟合到一个平面的情况,如果点集不能恰好拟合到一个平面,则需要使用更复杂的方法来处理,例如使用最小二乘平面拟合或者使用RANSAC算法。
利用所有点到目标平面距离之和最小的条件,筛选出目标平面。

RANSAC是“Random Sample Consensus(随机抽样一致)”的缩写。它可以从一组包含“局外点”的观测数据集中,通过迭代方式估计数学模型的参数。它是一种不确定的算法,有一定的概率得出一个合理的结果。为了提高得出合理结果的概率必须提高迭代次数。

1、基本思想:

RANSAC通过反复选择数据中的一组随机子集来达成目标。被选取的子集被假设为局内点,并用下述方法进行验证:

有一个模型适用于假设的局内点,即所有的未知参数都能从假设的局内点计算得出。

用1中得到的模型去测试所有的其它数据,如果某个点适用于估计的模型,认为它也是局内点。

如果有足够多的点被归类为假设的局内点,那么估计的模型就足够合理。

然后,用所有假设的局内点去重新估计模型,因为它仅仅被初始的假设局内点估计过。

最后,通过估计局内点与模型的错误率来评估模型。

2、对上述步骤,进行简单总结如下:

N:样本点个数,K:求解模型需要的最少的点的个数

随机采样K个点
针对该K个点拟合模型
计算其它点到该拟合模型的距离,小于一定阈值当做内点,统计内点个数
重复M次,选择内点数最多的模型
利用所有的内点重新估计模型(可选)
距离代码

using MathNet.Numerics;

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Media.Media3D;
using MathNet.Numerics.LinearAlgebra.Double;
using System.Windows.Forms.VisualStyles;

namespace 平面拟合
{
    public partial class Form1 : Form
    {
       
        public Form1()
        {
            InitializeComponent();
        }
        /// <summary>
        /// 点到直线的距离
        /// </summary>
        /// <param name="point"></param>
        /// <param name="plane"></param>
        /// <returns></returns>
        double PointToPlaneDis(Point3D point, Plane plane)
        {
            var p = point;
            return Math.Abs(plane.Normal.X * p.X + plane.Normal.Y * p.Y + plane.Normal.Z * p.Z + plane.Intercept) /
                         Math.Sqrt(plane.Normal.X * plane.Normal.X + plane.Normal.Y * plane.Normal.Y + plane.Normal.Z * plane.Normal.Z);
        }

        /// <summary>
        /// 随机抽样法多点拟合平面
        /// </summary>
        /// <param name="points"></param>
        /// <param name="iterations">迭代计算次数一般大于点数量</param>
        /// <param name="threshold">筛选条件点到平面的距离</param>
        /// <param name="OkPointMin">满足条件的最多点数</param>
        /// <param name="OkPointMax">满足条件的最少点数</param>
        /// <returns></returns>
        public Plane FitPlaneRANSAC(List<Point3D> points, int iterations, double threshold,int OkPointMin, int OkPointMax,int TryCnt=10)
        {
            // 按照RANSAC算法迭代次数进行循环
            int maxInliers = 0;
            Plane bestPlane = null;
            var step = threshold * 0.1;
            int trayCnt = 0;
        Retry:
            trayCnt++;
            List<int> recordIdx = new List<int> { 0, 0, 0 };
            List<List<int>> recordIdxList = new List<List<int>>();
            // 随机选择三个点构成一个平面
            Random rand = new Random();
            for (int i = 0; i < iterations; i++)
            {
               
                int index1 = rand.Next(points.Count);
                int index2 = rand.Next(points.Count);
                int index3 = rand.Next(points.Count);
                while (index1 == index2 || index1 == index3 || index2 == index3)
                {
                    index1 = rand.Next(points.Count);
                    index2 = rand.Next(points.Count);
                    index3 = rand.Next(points.Count);
                }

                //检测是否已经计算过该平面
                foreach (var record in recordIdxList)
                {
                    if (record.Contains(index1) && record.Contains(index2) && record.Contains(index3))
                    {                     
                        continue;
                    }
                }
                //记录已经计算的平面
                recordIdxList.Add(new List<int> { index1, index2, index3 });

                Point3D p1 = points[index1];
                Point3D p2 = points[index2];
                Point3D p3 = points[index3];
                Plane plane = new Plane(p1, p2, p3);

                // 计算与该平面距离小于阈值的点数
                int inliers = 0;
                foreach (Point3D p in points)
                {
                    double distance = PointToPlaneDis(p, plane);
                    if (distance <= threshold)
                    {
                        inliers++;
                    }
                }

                // 如果当前平面的支持点数比之前的最好平面要多,则更新最好平面
                if (inliers > maxInliers)
                {
                    maxInliers = inliers;
                    bestPlane = plane;
                }
            }
            if(trayCnt> TryCnt)
            {
                //
                return bestPlane;
            }
            if(maxInliers>OkPointMax)
            {
                threshold -= step;//自动调整条件范围
                maxInliers = 0;
                goto Retry;
            }
            else if (maxInliers < OkPointMin)
            {
                threshold += step;//自动调整条件范围
                maxInliers = 0;
                goto Retry;
            }else
            {
                return bestPlane;
            }

            return bestPlane;
        }

        /// <summary>
        /// 使用到所有点到目标平面的距离之和最小筛选平面
        /// </summary>
        /// <param name="points"></param>
        /// <param name="iterations"></param>
        /// <returns></returns>
        public Plane FitPlaneRANSACByAllHight(List<Point3D> points, int iterations)
        {
            // 按照RANSAC算法迭代次数进行循环
            double maxInliers = 9999;
            //防止距离和超出double的最大值,使用两个数记录
            double maxInliers2 = 9999;
            Plane bestPlane = null;
 
            List<List<int>> recordIdxList = new List<List<int>>();
            // 随机选择三个点构成一个平面
            Random rand = new Random();
            for (int i = 0; i < iterations; i++)
            {

                int index1 = rand.Next(points.Count);
                int index2 = rand.Next(points.Count);
                int index3 = rand.Next(points.Count);
                while (index1 == index2 || index1 == index3 || index2 == index3)
                {
                    index1 = rand.Next(points.Count);
                    index2 = rand.Next(points.Count);
                    index3 = rand.Next(points.Count);
                }

                //检测是否已经计算过该平面
                foreach (var record in recordIdxList)
                {
                    if (record.Contains(index1) && record.Contains(index2) && record.Contains(index3))
                    {
                        continue;
                    }
                }
                //记录已经计算的平面
                recordIdxList.Add(new List<int> { index1, index2, index3 });

                Point3D p1 = points[index1];
                Point3D p2 = points[index2];
                Point3D p3 = points[index3];
                Plane plane = new Plane(p1, p2, p3);

                // 计算与该平面距离的和
               double inliers = 0;
                double inliers2 = 0;
                foreach (Point3D p in points)
                {
                    double distance = PointToPlaneDis(p, plane);
                    if (inliers < double.MaxValue - distance - 1)
                    {
                        inliers += distance;
                    }
                    else
                    {
                        if (inliers2 < double.MaxValue - distance - 1)
                        {
                            inliers2 += distance;
                        }
                        else
                            return null;
                    }
                }
                if (inliers2 ==0)
                {
                    // 如果当前平面的支持点数比之前的最好平面要好,则更新最好平面
                    if (inliers < maxInliers)
                    {
                        maxInliers = inliers;
                        bestPlane = plane;
                    }
                }else
                {
                    if (inliers2 +(inliers- double.MaxValue) < maxInliers2+ (maxInliers - double.MaxValue))
                    {
                        maxInliers2 = inliers2;
                        bestPlane = plane;
                    }
                }
            }

            return bestPlane;
        }
        /// <summary>
        ///     平面拟合,  返回拟合度,范围[0,1],越接近1越好
        /// </summary>
        /// <param name="pointList">输入三维点列表</param>
        /// <param name="pixelSize">1像素=?mm, 用于输入的点坐标是像素时转换为mm</param>
        /// <param name="param">输出平面参数</param>
        /// <param name="titl">输出平面角度XY,高度Z</param>
        /// <returns></returns>
        private double PlaneFit(List<Point3D> pointList, double pixelSize, out double[] param, out double[] titl)
        {
            param = new double[] { 0, 0, 0 };
            titl = new double[] { 0, 0, 0 };

            if (pointList.Count == 0) return -1;

            for (var i = 0; i < pointList.Count; i++)
            {
                var pt = pointList[i];
                pt.X *= pixelSize;
                pt.Y *= pixelSize;
                pointList[i] = pt;
            }


            var a = new double[3, 3];
            var b = new double[3];
            double[] c = { 0, 0, 0 };

            a[0, 0] = pointList.Sum(pt => pt.X * pt.X);
            a[0, 1] = pointList.Sum(pt => pt.X * pt.Y);
            a[0, 2] = pointList.Sum(pt => pt.X);

            a[1, 0] = pointList.Sum(pt => pt.Y * pt.X);
            a[1, 1] = pointList.Sum(pt => pt.Y * pt.Y);
            a[1, 2] = pointList.Sum(pt => pt.Y);

            a[2, 0] = pointList.Sum(pt => pt.X);
            a[2, 1] = pointList.Sum(pt => pt.Y);
            a[2, 2] = pointList.Count;


            b[0] = pointList.Sum(pt => pt.X * pt.Z);
            b[1] = pointList.Sum(pt => pt.Y * pt.Z);
            b[2] = pointList.Sum(pt => pt.Z);


            var matrixA = DenseMatrix.OfArray(a);
            var matrixB = DenseMatrix.OfColumnArrays(b);
            var matrixC = matrixA.Solve(matrixB); //求解AC=B的线性方程组,input为右矩阵B,返回求解结果矩阵C
            c = matrixC.ToColumnMajorArray();
            param = c;

            //拟合误差
            var listOld = new List<double>();
            var listNew = new List<double>();
            for (var i = 0; i < pointList.Count; i++)
            {
                var temp = c[0] * pointList[i].X + c[1] * pointList[i].Y + c[2];

                listOld.Add(pointList[i].Z);
                listNew.Add(temp);
            }

            var cnx = pointList.Sum(pt => pt.X) / pointList.Count;
            var cny = pointList.Sum(pt => pt.Y) / pointList.Count;

            var tz = param[0] * cnx + param[1] * cny + param[2];
            var tx = Math.Atan(param[0]) * 180 / Math.PI;
            var ty = Math.Atan(param[1]) * 180 / Math.PI;
            titl[0] = tx;
            titl[1] = ty;
            titl[2] = tz;

            var rs = GoodnessOfFit.RSquared(listOld, listNew);

            return rs;
        }
      
        public bool  CalcPlaneTilt(out string outErrMsg, List<Point3D> pointList, out Point3D outTilt)
        {
            var res = true;

            outErrMsg = "";
            outTilt = new Point3D();
            var pointFs = new List<Point3D>();
            var ff = new Point3D(1000, 1000, 1000);
            if (pointList.Count < 3)
            {
               MessageBox.Show( "计算tilt点数不足3个不能计算平面");
                return false;
            }
            foreach (var pt in pointList)
                pointFs.Add(new Point3D( pt.X*1000, pt.Y* 1000, pt.Z * 1000));


            //平面拟合
            double[] PlaneParam;
            double[] PlaneTilt;
            var rPlaneSquaredLimit = 0.0000001;
            var rSquared = PlaneFit(pointFs, 1, out PlaneParam, out PlaneTilt); //point已经是mm,所以 pixelSize取1
            if (rSquared > rPlaneSquaredLimit || double.IsNaN(rSquared))
            {
                //Tilt值 XYZ, Z是清晰位置高度
                outTilt.X = Math.Round(PlaneTilt[0], 3);
                outTilt.Y = Math.Round(PlaneTilt[1], 3);
                outTilt.Z = Math.Round(PlaneTilt[2] / 1000, 4);

                return true;
            }
            else
            {
                return false;
            }

            return res;
        }

        private async void btnPointCnt_Click(object sender, EventArgs e)
        {
            Plane bestPlane = null;
            Point3D tilt = new Point3D();
            Task tk = Task.Factory.StartNew(() =>
            {
                List<Point3D> AllPoints = new List<Point3D>();
                int pointcnt = 100;
                Random rand = new Random();

                //假设平面5x+4y+z+1=0;生成距离平面距离10以内的300个点
                //使用这300个点,进行随机平面拟合匹配验证结果和目标平面对比
                while (pointcnt > 0)
                {

                    double X = rand.Next(1, 100);
                    double Y = rand.Next(1, 100);
                    var mSqrt = Math.Sqrt(5 * 5 + 4 * 4 + 1);
                    //生成随机距离
                    double randDisZ = (rand.Next(-100, 100)) / 10;
                    //利用距离反求出点的坐标Z
                    double Z = randDisZ * mSqrt - 5 * X - 4 * Y - 1;

                    AllPoints.Add(new Point3D(X, Y, Z));
                    pointcnt--;
                }
                pointcnt = AllPoints.Count;
                bestPlane = FitPlaneRANSAC(AllPoints, 900, 10, pointcnt / 2, pointcnt - 20);
            });

            await tk;
            Thread.Sleep(200);
            var p = bestPlane.Normal;
            var D = bestPlane.Intercept;
            //把Ax+By+Cz+D=0转化两边同时除以C,消去Z的系数
            MessageBox.Show($"求出直线{p.X / p.Z}X+{p.Y / p.Z}Y + Z+{D / p.Z}=0");
        }

        private async void btnPointDisAll_Click(object sender, EventArgs e)
        {
            Plane bestPlane = null;
            Point3D tilt = new Point3D();
            Task tk = Task.Factory.StartNew(() => {
                List<Point3D> AllPoints = new List<Point3D>() {
                new Point3D(x: 22.892,y:2.789,z:2.491),
                 new Point3D(x: 23.460,y:3.789,z:2.622),
                 new Point3D(x: 25.192,y:5.089,z:2.358),
                 new Point3D(x: 26.392,y:4.867,z:2.290),
                 new Point3D(x: 27.098,y:3.889,z:2.077),
                 new Point3D(x: 27.192,y:2.789,z:2.283),
                 new Point3D(x: 26.924,y:1.789,z:2.316),
                 new Point3D(x: 26.242,y:0.970,z:2.327),
                 new Point3D(x: 25.192,y:0.989,z:1.981),
                 new Point3D(x: 24.142,y:0.970,z:2.309),
                 new Point3D(x: 23.114,y:1.589,z:2.331),
                };

                List<double> zlist = new List<double>() {
                2.4768 ,
                2.6202 ,
                2.3562 ,
                2.2881 ,
                2.078  ,
                2.2833 ,
                2.315  ,
                2.326  ,
                1.9901 ,
                2.3084 ,
                2.3301 };
                int i = 0;

                //while (i <= AllPoints.Count - 1)
                //{
                //    var pp = new Point3D(AllPoints[i].X, AllPoints[i].Y, zlist[i]);
                //    AllPoints[i] = pp;
                //    i++;
                //}   
                bestPlane = FitPlaneRANSACByAllHight(AllPoints, 19900);
                CalcPlaneTilt(out var msg, bestPlane.pointXyzList, out  tilt);
            });

            await tk;
            Thread.Sleep(200);
            var p = bestPlane.Normal;
            var D = bestPlane.Intercept;
            //显示和Z轴的夹角(X方向和Y方向,以及Z平均值)
              MessageBox.Show($"求出tilt:X{tilt.X }+Y{tilt.Y } + Z+{tilt.Z}");
        }
    }
    public class Plane
    {
        public Vector3D Normal { get; set; }
        public double Intercept { get; set; }
        public List<Point3D> pointXyzList
        {
            get
            {
                if (Normal.Z != 0 && Normal.X != 0 && Normal.Y != 0)
                    return new List<Point3D>(){new Point3D() { X = 0,Y = 0,Z = -Intercept / Normal.Z},
                    new Point3D() { Z = 0,Y = 0,X = -Intercept / Normal.X},
                    new Point3D() { Z = 0,X = 0,Y = -Intercept / Normal.Y} };
                else
                {
                    return new List<Point3D>() { new Point3D() { X = 0, Y = 0, Z = 0 } };
                }
            }
        }

        public Plane(Point3D p1, Point3D p2, Point3D p3)
        {
            // 计算法向量
            Vector3D v1 = new Vector3D(p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z);
            Vector3D v2 = new Vector3D(p3.X - p1.X, p3.Y - p1.Y, p3.Z - p1.Z);
            Normal = Vector3D.CrossProduct(v1, v2);
            Normal.Normalize();

            // 计算截距
            Intercept = -(Normal.X * p1.X + Normal.Y * p1.Y + Normal.Z * p1.Z);
        }
       
    }
}

该代码首先通过循环执行一定次数的迭代,每次迭代随机选择三个点构成一个平面,计算与该平面距离小于阈值的点数,然后选取支持点数最多的平面作为最好平面。在实际应用中,需要根据具体情况调整迭代次数和阈值的取值。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值