posts - 118,  comments - 276,  views - 45万
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

转载请注明作者及出处,谢谢

这两天手头有个项目,需要绘制等值线,本以为是一个很简单的事情,没有想到刚开始就发现竟然无从着手,研究了一个星期,终于把线条画出来了,基本思路是先三角网剖分,然后再等值线追踪,最后绘制;没有对等值线进行光滑处理,示例图中看起来比较光滑是因为取点比较密集,也没有打算进行等值线填色,因为项目中没有这个需求,(而且在我的项目中高程点是网格状分布,而不是离散点,因此我做的三角网剖分简单,但是等值线追踪算法是完全满足离散点要求的)。先上几个效果图:

示例图(黄颜色圆圈代表光源,高程值为光源照度)

图1

图2

图3

等值线标注示意图

效果一:高程值压线了

效果二:高程值在线条下方

效果三:高程值没有按照等值线方向旋转

效果四:在很小的封闭式等值线圈内进行标注

绘制等值线的理论有很多,但是通常大家采用的是三角网剖+等值线追踪,网上有很多论文可以参考:

Delaunay三角网剖分算法

Delaunay三角剖分(Delaunay Triangulation)相关知识

三角网格等值线自动生成方法及程序实现

这里是一个绘制、填充等值线的源代码(《C#实现基于三角网的等值线追踪及填充算法》)

这里是一些三角网剖分源代码,有各种语言的实现

这里是一些其他等值线绘制方法

这里是论文的链接地址,该链接包含很多论文地址。

基本上来说,在《三角网格等值线自动生成方法及程序实现》一文中,提供的理论就足以指导绘制作业了,但是不幸的是,相当多的程序员可能不会一下子就能行成一个比较清晰明确的思路,我就是其中之一,看了好多的文章才明白,噢!原来是这么回事啊。另外还是那句老话:“眼里过千遍,不如手里过一遍”,所谓纸上得来终觉浅,绝知此事要躬行,看算法,看代码,可能比较难以一下子明白其中的原理,很多时候人家用了一个算法(或数据结构),你可能很不以为然,但是当你自已动手时,才发现原来那样用有那样用的妙处(或只能那么用),眼高手低永远是我等程序员的通病。

ok,进入正题

在我的项目中,是计算在一个空间内部,假设排布了n盏灯,那么需要计算出空间内部每一个点上的照度是多少,同时绘制出照度的等值线来,计算照度不在本文计论范围之内,于是我们假设把空间划分成了一个矩阵,矩阵中每个点上的照度值均已获得。于是很自然想到“使用网格绘制等值线”这么一个关键词,发现居然很少有相关资料,于是只能把把关键词修改为“绘制等值线”,于是发现了很多关于绘制等值线方法的论文,但是基本上前面都带了另一个词:三角网。为什么要三角网呢?我们先来看一下使用网格绘制时一种情形

图1

在使用网格绘制等值线时,假定我们是从这个网格的左边起开始查找等值线的走向,则等值线有三个方向可以“出去”,判断和确定等值线的走向将是一个非常困难的事情,而且极有可能出现锯齿状等值线(如下图所示)

图2

而使用三角网为基础来绘制等值线时,则是下图的情况

图3

在这里我有一个问题,到目前为止还没有解决,但是也没有找到解决问题的理论依据,在使用网格时,需要判断等值线的走向,有个论文中提出一个原则:假设等值线的“入口”在左边上(图1A点),则在网格中,如果等值线的“出口”有多个(图1B,C,D点),那么首先要看到等值线到A的趋势更倾向于择哪个点(B,C,D),其次是看哪个点到A点的距离近则取哪个。但是实际上这样做的代价太大了,任何人要去写一段判断趋势的代码,肯定都是个头疼的问题,因此实际上的作法是:在由左向右的追踪过程中,先看哪个点的X值和A点的X值接近则取哪一个,如果有多个点的X值和A点的X值相等,则看哪个点到A点的距离小则取哪一个,如果这个距离也相等,则只能通过趋势判断了(见《基于规则网格等值线生成算法及其应用》1.2.2)。实际从程序员的角度来讲,只要你明确表示在程序中会用到某个功能,那么你一年用n+1次和用1次是没有区别的,因此如果你想把程序写的能覆盖所有的可能,判断趋势看起来是不可避免的。

虽然三角形简化了上述这种在网格中追踪等值线的难度,但是并没有完全解决需要判断趋势的问题,为啥咧?使用三角形时只可能有两个“出口”,使用网格中的原则基本上就可以判断取哪个点了,但是理论上在点A所在的边上,仍一个点,到B,和C的X值和距离是相等的,见下图

图4

在图4中,三角形ABC是一个等腰三角形,这就意味着A点到B以及C的距离相等,而且B和C的X值也相等...理论上来讲,如果A是起点,哪还谈什么趋势呢?还要不要让人活了...

OK,现在让我们一起默念“阿门”,求助于概率吧,不要出现这种情况,反正我是不打算处理这种情况的,出了Bug再说吧。值的说明的是,在约大多数的论文中,没有提到该如何去处理这情况,有个别论文中指出:“每个三角形上不可能三边都有同值的等值点,另一边上必定有同值的等值点”(见《如何根据离散点自动绘制等值线(等高线)之 三角形法》1.b),但是这个说法我持怀疑态度,但我现在不打算处理这种情况(实际上,我在代码中连距离和X值都没有处理)。

有了理论作为依据,接下来的事情就是先把矩阵转换为三角网,怎么转换是个问题:实际上很多时候关于Delaunay的论文都是在假定数据(如灯具照度、高度、降水量等)是离散数据,即不是规则分布的,因此在这些论文中都是在讲如果把这些离散点划分为三角形,当然,能把离散点剖分出三角网,则么规则点就不在话下了。但在我的项目中是不存在这些需求,因此我需要做的就是把矩阵中的每个网格一分为二,切成两个直角三角形,一率从左上角向右下角切,如图所示:

图5

切的有点密,这个可以视具体情况自已定夺。

在这里,需要把三角形、边、点的数据结构给出来:

VPoint代表一个点,由X,Y轴坐标及高程值构成

public class VPoint
{
    public float X { get; set; }
 
    public float Y { get; set; }
 
    public decimal Value { get; set; }
 
    public override bool Equals(object obj)
    {
        if (obj is VPoint)
        {
            var tmp = obj as VPoint;
            return this.X == tmp.X && this.Y == tmp.Y;
        }
 
        return false;
    }
 
    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}

Edge代表一个边,由两个点构成,这里需要稍作解释,在图5中,有一些三角形的一条边是整个三角网的边框,即空间的四个边,开放式的等值的起点和终点必然会落在这些最外层的边上,因此需要对这些最外层边作一个标记,如何标记呢?很显然,三角网中除了这些最外层边,其他所有的边都会同时属于两个三角形,因此,我们的Edge对象有两个属性:Trangle1和Trangle2,这两个属性分别标识这个Edge对象所属的两个三角形,那么这些最外层的Edge对象比较可怜,他们只属于一个Trangle,属性Trangle2的值一定是null,参见Trangle类型。

public class Edge
{
    public VPoint P1 { get; set; }
 
    public VPoint P2 { get; set; }
 
    public Triangle Triangle1 { get; set; }
 
    public Triangle Triangle2 { get; set; }
 
    public bool IsBorder
    {
        get
        {
            return Triangle2 == null;
        }
    }
 
    public override bool Equals(object obj)
    {
        if (obj is Edge)
        {
            Edge tmp = obj as Edge;
            return this.P1 == tmp.P1 && this.P2 == tmp.P2;
        }
        return false;
    }
 
    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}

Trangle代表一个三角形,由三个边构成,我总是把水平的那条边称为A,竖直的那条边称为B,斜边称为C,其他的属性值以后会用,这里就不一一介绍了,当然,现在代码还没有进行最终整理,有可能有些属性也许会被干掉,最终只留下必要的。

public class Triangle
{
    private Edge a, b, c;
 
    /// <summary>
    /// 邻边
    /// </summary>
    public Edge A
    {
        get
        {
            return this.a;
        }
        set
        {
            this.a = value;
            if (this.a.Triangle1 == null)
            {
                this.a.Triangle1 = this;
            }
            else
            {
                this.a.Triangle2 = this;
            }
        }
    }
 
    /// <summary>
    /// 对边
    /// </summary>
    public Edge B
    {
        get
        {
            return this.b;
        }
        set
        {
            this.b = value;
            if (this.b.Triangle1 == null)
            {
                this.b.Triangle1 = this;
            }
            else
            {
                this.b.Triangle2 = this;
            }
        }
    }
 
    /// <summary>
    /// 斜边
    /// </summary>
    public Edge C
    {
        get
        {
            return this.c;
        }
        set
        {
            this.c = value;
            if (this.c.Triangle1 == null)
            {
                this.c.Triangle1 = this;
            }
            else
            {
                this.c.Triangle2 = this;
            }
        }
    }
 
    /// <summary>
    /// 是否使用,-1:临时使用; 0:未使用; 1:使用
    /// </summary>
    public int UsedStatus { get; set; }
 
    public Edge BorderEdge
    {
        get
        {
            if (this.A.IsBorder) { return this.A; }
            if (this.B.IsBorder) { return this.B; }
            return null;
        }
    }
 
    public Edge[] Get2OtherEdge(Edge relativeEdge)
    {
        Edge[] edges = new Edge[2];
        if (relativeEdge == this.A)
        {
            edges[0] = this.B;
            edges[1] = this.C;
        }
        else if (relativeEdge == this.B)
        {
            edges[0] = this.A;
            edges[1] = this.C;
        }
        else
        {
            edges[0] = this.A;
            edges[1] = this.B;
        }
        return edges;
    }
 
    public VPoint FindPoint(float x, float y)
    {
        if (this.A != null)
        {
            if (this.A.P1.X == x && this.A.P1.Y == y) { return this.A.P1; }
            if (this.A.P2.X == x && this.A.P2.Y == y) { return this.A.P2; }
        }
        if (this.B != null)
        {
            if (this.B.P1.X == x && this.B.P1.Y == y) { return this.B.P1; }
            if (this.B.P2.X == x && this.B.P2.Y == y) { return this.B.P2; }
        }
        if (this.C != null)
        {
            if (this.C.P1.X == x && this.C.P1.Y == y) { return this.C.P1; }
            if (this.C.P2.X == x && this.C.P2.Y == y) { return this.C.P2; }
        }
        return null;
    }
 
    public Edge FindEdge(VPoint p1, VPoint p2)
    {
        if (this.A != null)
        {
            if (this.A.P1 == p1 && this.A.P2 == p2) { return this.A; }
        }
        if (this.B != null)
        {
            if (this.B.P1 == p1 && this.B.P2 == p2) { return this.B; }
        }
        if (this.C != null)
        {
            if (this.C.P1 == p1 && this.C.P2 == p2) { return this.C; }
        }
        return null;
    }
}

OK,现在是时候说说我是如何建立一个IList<Trangle>的吧,即生成三角网列表

一开始,需要解决“有木有”的问题,于是我先建立一个IList<Trangle>的实例result,然后生成四个VPoint对象,代表一个网格的四个角,然后再生成五个Edge对象,再使用五个Edge生成两个Trangle对象,然后把两个Trangle对象加入到result中,以后每次都去查找result中有没有指定X,Y的VPoint,以及查找result中有没有指定P1和P2的Edge,如果有的话,就分别拿出来去构成新的Edge(对于VPoint来说)或者Trangle(对于Edge来说),如果没有的话就new一个实例出来,然后再通过Trangle对象存到result中,参见Trangle.FindPoint方法和Trangle.FindEdge方法,因为大量使用Linq方法查询数据,造成的结果是45 × 27的矩阵,生成三角网需要花费00:00:04.7031250!后来我改进了方法,使用一个二维数组来保存VPoint对象实例(VPoint[x, y]),使用一个四维数组来保存Edge对象实例(Edge[p1x, p1y, p2x, p2y]),直接用矩阵的序号来获取可能已存在的实例,果然性能得到极大的提升,运行耗时仅00:00:00.0156250。

for (int x = 0; x < xCount - step; x += step)
{
    for (int y = 0; y < yCount - step; y += step)
    {
        var p1 = matrix[x, y];
        var p2 = matrix[x + step, y];
        var p3 = matrix[x + step, y + step];
        var p4 = matrix[x, y + step];
 
        VPoint tmpP1, tmpP2, tmpP3, tmpP4;
        Edge edge1, edge2, edge3, edge4, edge5;
        Triangle triangle1 = new Triangle(), triangle2 = new Triangle();
 
        tmpP1 = this.FindOrCreateNewPoint(tmpMatrix, x, y, p1, zoomFactor);
        tmpP2 = this.FindOrCreateNewPoint(tmpMatrix, x + 1, y, p2, zoomFactor);
        tmpP3 = this.FindOrCreateNewPoint(tmpMatrix, x + 1, y + 1, p3, zoomFactor);
        tmpP4 = this.FindOrCreateNewPoint(tmpMatrix, x, y + 1, p4, zoomFactor);
 
        edge1 = this.FindOrCreateNewEdge(tmpEdges, tmpMatrix, x, y + 1, x + 1, y + 1);
        edge2 = this.FindOrCreateNewEdge(tmpEdges, tmpMatrix, x, y, x, y + 1);
        edge3 = this.FindOrCreateNewEdge(tmpEdges, tmpMatrix, x, y, x + 1, y);
        edge4 = this.FindOrCreateNewEdge(tmpEdges, tmpMatrix, x + 1, y, x + 1, y + 1);
        edge5 = this.FindOrCreateNewEdge(tmpEdges, tmpMatrix, x, y, x + 1, y + 1);
 
        triangle1.A = edge1;
        triangle1.B = edge2;
        triangle1.C = edge5;
        triangle2.A = edge3;
        triangle2.B = edge4;
        triangle2.C = edge5;
 
        result.Add(triangle1);
        result.Add(triangle2);
    }
}

matrix是原始数据,在我的项目是照度点矩阵,step的作用是由原始数据中生成三角网时可以跳过step个点取一点。

有了三角网,接下来应该去进行等值线追踪了。

稍事休息,接下来讲等值线追踪。。。

posted on   think8848  阅读(9902)  评论(13编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示