数学篇 cad.net 葛立恒凸包算法和面积最小包围盒

凸包

参考

安德鲁算法
分治法(其中nfox的项目实现的是分治法)
多边形快速凸包算法(Melkman‘s Algorithm)
还可以这看cpp的代码: https://www.cnblogs.com/VividBinGo/p/11637684.html

定义

凸包又叫凸多边形,本篇文章可能混用两种说法,形象的理解就是一些点(点集)用一根橡皮筋紧紧地包裹外边点.
如果知道了这个定义,那么还有:
用一个保鲜膜裹着三维点,求膜上点集.
用一个最小的球裹着三维点,求球球的中心点和直径.
这样就进入了一个叫拓扑学的学科上.......我的老天鹅.
我竟然搜了半天没看到可以直接拿来用的c#代码,还是小轩轩给我的....

葛立恒凸包

注意一下,如果点集形成的是正交矩形的情况时,
算出来的凸包会多一个点,可以进行后处理.
(你会发现代码实际上是右上角最大点开始的,其他的教程说明从最小点开始算,
这需要知道的是最小最大都是边界点,边界点必然是凸包的边点,用谁起算都可以)
img

包围盒

参考

三维包围盒实现
凸包最小周长外接矩形
凸包最小面积外接矩形

因为没看懂的关系,所以我自己想了以下的粗糙方法实现
我还发现游戏行业还有快速平方根,模糊距离,快速包围盒等等的实现......而cad只能用精确的包围盒 😃

定义

包围盒又叫外接矩形,如下图形式:
先了解AABB叫轴向包围盒,这是cad自带函数可以求得的.(求最小点和最大点).
再是今次的主角OBB有向包围盒.
img

包围盒分二维和三维,由于三维在我的领域内没啥用途,所以我只实现了二维包围盒,
当然了,如果你已经掌握点乘和叉乘的要领,那么你可以根据二维来写出三维来.

条件

条件1: 包围盒不是出现在点集形成的图形上,而是出现在点集的凸包上,看下图可知:
img
条件2:包围盒的矩形边必然和凸包其中一条边重合,所以利用凸包,就可以实现面积最小的包围盒了.

实现方式

如图所示:

得到绕轴转点

先求矩形的第一个角点r1,沿着a->b向量,角点r1会是c点落在这a->b向量上,得到a到r1的距离长度.
再来d点可能比c点更远,e点比d点更远,于是乎需要循环.
当循环出现了本次距离比上次小,说明找到了最后的r1角点了.
用一些数学语言描述一下:
矩形角点r1==a->c点乘a->b得到r1角点,再a->d点乘a->b...轮询,如果r1与a点距离开始进行缩小(点乘距离回归),
​ 那么表示r1确定,以及得到末尾点ptLast=c点,
​ 最后明确得到:a->ptLast点乘a->b就是最近角点r1.

得到轴向矩形

矩形其他角点只需要旋转点集得到,这样就是一个轴向矩形.

其余包围盒:

现在知道了ab段,接着循环bc,cd,de等等....即可得到每个包围矩形,然后就可以求得面积最大或者最小的包围盒.

代码

bug致谢: JieGou(杰狗)

子函数

db.Action委托无返回值写法
入门数学 PointV+凸度
曲线采样
曲线长度
RectAngle类
GetEntityPoint3ds

主函数

#if !HC2020
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
#else
using GrxCAD.DatabaseServices;
using GrxCAD.EditorInput;
using GrxCAD.Geometry;
using GrxCAD.Runtime;
using Acap = GrxCAD.ApplicationServices.Application;
#endif

using System;
using System.Collections.Generic;
using System.Linq;
using JoinBox.BasalMath;

namespace JoinBox
{
    /*
     * 视频参考: https://www.bilibili.com/video/BV1v741197YM
     * 相关学习: https://www.cnblogs.com/VividBinGo/p/11637684.html
     * ① 找到所有点中最左下角的点_p0(按 x 升序排列,如果 x 相同,则按 y 升序排列)
     * ② 以_p0为基准求所有点的极角,并按照极角排序(按极角升序排列,若极角相同,则按距离升序排列),
     *   设这些点为p1,p2,……,pn-1
     * ③ 建立一个栈,_p0,p1进栈,对于P[2..n-1]的每个点,利用叉积判断,
     *   若栈顶的两个点与它不构成"向左转(逆时针)"的关系,则将栈顶的点出栈,
     *   直至没有点需要出栈以后,将当前点进栈
     * ④ 所有点处理完之后栈中保存的点就是凸包了。
     */
    public class CmdTestGrahamClass
    {
        /// <summary>
        /// 求凸包测试命令
        /// </summary>
        [CommandMethod("CmdTest_Graham")]
        public void CmdTest_Graham()
        {
            var dm = Acap.DocumentManager;
            var doc = dm.MdiActiveDocument;
            var db = doc.Database;
            var ed = doc.Editor;
            ed.WriteMessage("\n****{惊惊连盒}求凸包,选择曲线:");

            //定义选择集选项
            var pso = new PromptSelectionOptions
            {
                RejectObjectsOnLockedLayers = true, //不选择锁定图层对象
                AllowDuplicates             = true, //不允许重复选择
            };
            var ssPsr = ed.GetSelection(pso);
            if (ssPsr.Status != PromptStatus.OK)
                return;

            db.Action(tr => {
                var getPts = new List<PointV>();

                var ents = ssPsr.Value.GetObjectIds().ToEntity(tr);
                ents.ForEach(ent => {
                    if (ent is Line line)
                    {
                        getPts.Add(line.StartPoint);
                        getPts.Add(line.EndPoint);
                    }
                    else if (ent is Polyline pl)
                    {
                        pl.GetEntityPoint3ds().ForEach(pt => { getPts.Add(pt); });
                    }
                    else if (ent is Curve curve)
                    {
                        var cs = new CurveSample(curve);
                        cs.GetSamplePoints.ForEach(pt => { getPts.Add(pt); });
                    }
                    else if (ent is DBPoint bPoint)
                        getPts.Add(bPoint.Position);
                    else
                    {
                        var entPosition = ent.GetType().GetProperty("Position");//反射获取属性
                        if (entPosition is not null)
                        {
                            var pt = (Point3d)entPosition.GetValue(null, null);
                            getPts.Add(pt);
                        }
                    }
                });

                //葛立恒方法
                var ptList = GrahamConvexHull(getPts);
                if (ptList == null)
                    return;

                ed.WriteMessage("\n\r凸包对踵点最大距离:" + RotateCalipersMax(ptList));
                ed.WriteMessage("\n\r凸包对踵点最小距离:" + RotateCalipersMin(ptList));

                if (ptList.Length > 1)
                {
                    var disList = new List<double>();
                    for (int i = 0; i < ptList.Length - 1; i++)
                        disList.Add(ptList[i].DistanceTo(ptList[i + 1]));
                    disList.Add(ptList[ptList.Length - 1].DistanceTo(ptList[0]));

                    ed.WriteMessage("\n\r凸包点集最大距离:" + disList.Max());
                    ed.WriteMessage("\n\r凸包点集最小距离:" + disList.Min());
                }

                //生成凸包边界
                var bv = new List<BulgeVertexWidth>();
                for (int i = 0; i < ptList.Length; i++)
                    bv.Add(new BulgeVertexWidth(ptList[i].ToPoint2d(), 0));
                var pl = EntityAdd.AddPolyLineToEntity(bv);
                tr.AddEntityToMsPs(db, pl);

                var rects = Boundingbox(ptList);

                //Test: 求凸包,修改数值以便测试
                int creatAll = 0;
                if (creatAll == 0)
                {
                    //生成所有包围盒
                    int ColorIndex = 0;
                    foreach (var rec in rects)
                    {
                        var ent = rec.ToPolyLine();
                        ent.EntityRotate(rec.Angle, Vector3d.ZAxis, rec.MinPoint.ToPoint3d());
                        ent.ColorIndex = ++ColorIndex;
                        tr.AddEntityToMsPs(db, ent);
                    }
                }
                else
                {
                    //生成计算面积最小的包围盒
                    var rec = rects.OrderBy(item => item.Area).First();
                    var ent = rec.ToPolyLine();
                    ent.EntityRotate(rec.Angle, Vector3d.ZAxis, rec.MinPoint.ToPoint3d());
                    tr.AddEntityToMsPs(db, ent);
                }
            });
        }

        /// <summary>
        /// 求凸包_葛立恒算法,
        /// 凸包多段线在正交的情况下会多一个点
        /// </summary>
        /// <param name="points"></param>
        /// <returns>凸包点集(顺时针)</returns>
        PointV[]? GrahamConvexHull(IEnumerable<PointV> points)
        {
            if (points == null)
                throw new ArgumentNullException(nameof(points));

            //最小最大都是边界点,边界点必然是凸包的边点
            //因为Cosine排序方式是负角度,所以从右上角计算
            PointV maxPt;//p0

            //消重,排序,此处排序将导致点集从顺时针开始
            var ptsOrder = points.Distinct(PointV.Distinct);
            //按角度排序,同角度用距离排序
            ptsOrder = ptsOrder.OrderBy(p => p.X).ThenBy(p => p.Y);
            maxPt = ptsOrder.Last();
            ptsOrder = ptsOrder.OrderByDescending(pn => Cosine(maxPt, pn)).ThenBy(p => maxPt.DistanceTo(p));
            var pts = ptsOrder.ToArray();

            bool whileBreak = false;
            var stack = new Stack<PointV>();
            stack.Push(maxPt);
            stack.Push(pts[1]);
            //从大到小遍历,第三个点开始
            for (int i = 2; i < pts.Length; i++)
            {
                var pn = pts[i];      //第一次为 p2    ,相当于pn
                var p1 = stack.Pop(); //第一次为 p1    ,相当于前一个点(返回并剔除顶部对象)
                var p0 = stack.Peek();//第一次为 maxPt ,相当于后面一个点(查询顶部对象)
                //为真表示要剔除
                while (!whileBreak && p1.CrossAclockwise(p0, pn))
                {
                    if (stack.Count > 1)
                    {
                        //删除顶部对象
                        stack.Pop();

                        //前后点交换,用于while循环,
                        //可参考 https://www.bilibili.com/video/BV1v741197YM 04:15
                        //栈顶就是回滚之后的,再次和qn进行向量叉乘,看看是不是叉乘方向,
                        //是就继续回滚,否则结束循环后加入栈中.
                        p1 = p0;
                        p0 = stack.Peek();
                    }
                    else
                    {
                        //栈少于1,就不再剔除顶部(保护栈中 maxPt 不剔除),结束循环
                        whileBreak = true;
                    }
                }
                stack.Push(p1);
                stack.Push(pn);
                whileBreak = false;
            }

            return RemoveBulgeSamll(stack);
        }

        /// <summary>
        /// 剔除凸度太小的,以免凸包有多余的边点
        /// </summary>
        /// <param name="stack"></param>
        /// <returns></returns>
        PointV[]? RemoveBulgeSamll(IEnumerable<PointV> stack)
        {
            if (stack == null)
                throw new ArgumentNullException(nameof(stack));

           //旧方案没有考虑(尾-头0-头1)这样的情况,现在改过来
           //可以用LinkedList代替
            var link = new LoopList<PointV>(stack);
            var node = link.First;
            if (node == null)
                return null;
            for (int i = 0; i < link.Count; i++)
            {
                var bulge = MathHelper.GetArcBulge(
                    node!.Value.ToPoint2d(),
                    node!.Next!.Value.ToPoint2d(),
                    node!.Next!.Next!.Value.ToPoint2d());

                if (Math.Abs(bulge) < 1e-6)
                    link.Remove(node.Next);//剔除中间
                node = node.Next;
            }
            return link.ToArray();
        }

        /// <summary>
        /// 角度p0和pn的角度
        /// </summary>
        /// <param name="p0">最靠近x轴的点(必然是凸包边界的点)</param>
        /// <param name="pn"></param>
        /// <returns></returns>
        double Cosine(PointV p0, PointV pn)
        {
            double dis = p0.DistanceTo(pn);
            //求角度(高/斜)==sin(角度)
            //距离是0表示是自己和自己的距离,那么0不可以是除数,否则Nan
            //如果1.0为sin周期函数最大,改成0则会令0度的点被忽略了
            return dis == 0.0 ? 1.0 : (pn.Y - p0.Y) / dis;
        }

        /// <summary>
        /// 有向包围盒
        /// </summary>
        /// <param name="ptsOrd">凸包点集(顺时针)</param>
        /// <returns>返回每条边的包围盒</returns>
        List<RectAngle> Boundingbox(IEnumerable<PointV> ptsOrd)
        {
            /*
             * 最小包围盒(外接矩形)
             * 重要的条件:凸多边形的最小周长(最小面积)外接矩形存在一条边和多边形的一条边重合;
             * 角点r1==a->c叉乘a->b得到r1角点,再a->d叉乘a->b...轮询,
             *      如果r1与a点距离开始进行缩小(叉乘距离回归),
             *      那么表示r1确定,以及得到末尾点ptLast图上位c点,
             *      最后明确得到:a->ptLast叉乘a->b就是最近角点r1;
             * 角点r2,r3,r4需要的仅仅是通过a->b向量与X轴角度绕r1旋转得到;
             */
            var ptList = ptsOrd.ToList();
            var rects = new List<RectAngle>();

            //因为利用三点获取第一个交点,
            //所以为了最后的边界,需要加入集合前面,以使得成为一个环
            ptList.Add(ptList[0]);
            ptList.Add(ptList[1]);
            ptList.Add(ptList[2]);

            //边循环,ab..bc..cd..de..
            int numB = 1;
            for (int i = 0; i < ptList.Count - numB; i++)
            {
                double acLengthLast = -1;
                var ptA = ptList[i];
                var ptB = ptList[i + numB];
                //此处循环是求长度求r1点,如果出现点乘距离收缩,就结束
                for (int c_nmuber = i + 2; c_nmuber < ptList.Count; c_nmuber++)
                {
                    //ac->ab点乘求矩形的一个角点(绕轴转点)
                    //角点距离如果出现缩小,就确定了这个点是r1
                    var acLength = ptA.DistanceTo(ptA.DotProduct(ptList[c_nmuber], ptB));
                    //第一次赋值
                    if (acLengthLast == -1)
                    {
                        acLengthLast = acLength;
                    }
                    else if (acLengthLast < acLength)
                    {
                        acLengthLast = acLength;
                    }
                    else
                    {
                        //此处点乘距离已经收缩,求得绕轴转点r1(也是矩形角点),结束循环.
                        PointV r1;
                        var ptCLast = ptList[c_nmuber - 1];      //边界点-1就是上次的
                        var ptC2 = ptA.DotProduct(ptCLast, ptB); //角点计算
                        if (ptA.DistanceTo(ptC2) < ptA.GetDistanceTo(ptB))
                            r1 = ptB;//叉乘点出现在ab线内,此时b才是矩形边点
                        else
                            r1 = ptC2;
                        rects.Add(CreatRect(ptList, ptA, r1));
                        break;
                    }
                }
            }
            return rects;
        }

        /// <summary>
        /// 得到一个轴向矩形和夹角
        /// </summary>
        /// <param name="pts"></param>
        /// <param name="a_point"></param>
        /// <param name="r1">绕轴转点</param>
        /// <returns></returns>
        RectAngle CreatRect(List<PointV> pts, PointV a_point, PointV r1)
        {
            var angle = r1.GetVectorTo(a_point).GetAngle2XAxis();
            //旋转点后求一个轴向矩形
            var ptsRo = new List<PointV>();
            pts.ForEach(pt => {
                ptsRo.Add(pt.RotateBy(-angle, PointV.ZAxis, r1));
            });
            var ox = ptsRo.OrderBy(pt => pt.X);
            var oy = ptsRo.OrderBy(pt => pt.Y);
            var minX = ox.First().X;
            var minY = oy.First().Y;
            var maxX = ox.Last().X;
            var maxY = oy.Last().Y;
            //RectAngle的旋转点是min点
            return new RectAngle(new Rect(minX, minY, maxX, maxY), angle);
        }

        // 概念参考了这里,但是它代码好像有点问题
        // http://www.cppblog.com/staryjy/archive/2009/11/19/101412.html
        /// <summary>
        /// 凸包对踵点最大的距离_旋转卡壳
        /// </summary>
        /// <returns></returns>
        double RotateCalipersMax(IEnumerable<PointV> ptsOrd)
        {
            var pts = ptsOrd.ToList();
            if (pts.Count < 2)
                return 0;
            else if (pts.Count == 2)
                return pts[0].DistanceTo(pts[1]);

            double result = 0;

            int ps = 2;     //2是从下下一个点开始,因为1开始会导致[0]和[0]判断距离
            pts.Add(pts[0]);//但是下下一个点开始就表示没有判断到0->1线段,必须加入尾部判断
            int first = pts.Count - ps;
            for (int i = 0; i < first; i++) //点序是叉乘方向的.
            {
                //叉乘求面积:
                //面积呈现为单峰函数(函数图像中间大两边小,函数从递增到递减),
                //满足结束条件:
                //面积最高峰的时候下一次判断即为>前者(函数递减) || 取余为0(即为遍历到最后了)

                //叉乘求面积,A<B,表示求后者面积数大,直到后者面积数小,结束循环,求最大值长度即可
                while (Math.Abs(pts[i].Cross(pts[i + 1], pts[ps])) <
                       Math.Abs(pts[i].Cross(pts[i + 1], pts[ps + 1])))
                    ps = (ps + 1) % first;//X%Y,X<Y返回X.取余为0(即为遍历到最后了)

                //峰值时候求的三点距离
                //第一次3->0和3->1
                result = Math.Max(result, Math.Max(pts[ps].DistanceTo(pts[i]), pts[ps].DistanceTo(pts[i + 1])));
            }
            return result;
        }

        /// <summary>
        /// 凸包对踵点最小的距离_旋转卡壳
        /// </summary>
        /// <returns></returns>
        double RotateCalipersMin(IEnumerable<PointV> ptsOrd)
        {
            var pts = ptsOrd.ToList();
            if (pts.Count < 2)
                return 0;
            else if (pts.Count == 2)
                return pts[0].DistanceTo(pts[1]);

            var lstmin = new List<double>();

            //点集顺序是叉乘方向的.
            //凸包对踵点最小的距离==非邻点的最小点距,邻边点不计算
            //计算方式是通过获取循环数字,确定执行次数.
            //从i从i+2点开始递增,但是循环数字是递减的,只因不需要重复计算0-2,2-0的情况.
            var last = pts.Count - 1;
            for (int i = 0; i < last; i++)
            {
                //循环次数 = 总数 - 1(前一个点邻边) - i(循环递减)
                int fornum = last - 1 - i;
                //如果i==0,减多1次
                if (i == 0)
                    fornum--;

                int inumq = i + 1;//前一个点(邻边)
                for (int j = 0; j < fornum; j++)
                {
                    inumq++;//前前一个点
                    var dis = pts[i].DistanceTo(pts[inumq]);
                    lstmin.Add(dis);
                }
            }
            return lstmin.Min();  //返回的长度
        }
    }
}

(完)

posted @ 2021-01-16 01:56  惊惊  阅读(2737)  评论(1编辑  收藏  举报