绘制地形图

37
我一直在开发一个可视化项目,用于处理二维连续数据。它适用于研究海拔数据或温度图案等2D地图上的数据。本质上,它是将三维数据压缩成二维加颜色的方式。在我的研究领域中,我实际上并不处理地理高程数据,但这是一个很好的比喻,因此我将在本文中一直使用它。
无论如何,在这个阶段,我有一个“连续颜色”渲染器,我非常满意:

Continuous Color Renderer

渐变是标准的色轮,红色像素表示具有高值的坐标,紫色像素表示低值。
底层数据结构使用一些非常聪明的插值算法,使得可以任意深入地缩放到地图的细节中。
目前,我想画一些地形等高线(使用二次贝塞尔曲线),但我还没有找到任何描述有效算法以找到这些曲线的好文献。
为了让您了解我的想法,这里是一个简陋的实现(当渲染器遇到与等高线相交的像素时,它只使用黑色RGB值):

Continuous Color with Ghetto Topo Lines

然而,这种方法存在几个问题:

  • 图表中斜率较陡的区域会导致拓扑线变细(经常出现断裂)。理想情况下,所有拓扑线都应该连续。

  • 图表中斜率较缓的区域会导致拓扑线变宽(并且经常在渲染区域的外围形成整个黑色区域)。

因此,我正在寻找矢量绘图方法来获得漂亮、完美的1像素厚曲线。算法的基本结构将包括以下步骤:

  1. 在每个离散的高度处,找到一组坐标,使得该坐标的海拔与所需高度非常接近(给定任意epsilon值)。
  2. 消除冗余点。例如,如果三个点在一条完美直线上,则中心点是冗余的,因为可以消除它而不改变曲线的形状。同样地,对于贝塞尔曲线,通常可以通过调整相邻控制点的位置来消除某些锚点。
  3. 将剩余点组装成一个序列,使得两个点之间的每个段近似于高程中性轨迹,并且没有两个线段会相交。每个点序列必须创建一个封闭多边形,或者必须与渲染区域的边界框相交。
  4. 对于每个顶点,找到一对控制点,使得由此产生的曲线相对于步骤#2中消除的冗余点具有最小误差。
  5. 确保当前渲染比例下可见的地形的所有特征都用适当的拓扑线表示。例如,如果数据包含高海拔但直径极小的尖峰,则仍应绘制拓扑线。只有当其特征直径小于图像的总体渲染粒度时,才应忽略垂直特征。

但即使在这些限制下,我仍然可以想到几种不同的启发式方法来找到这些线:

  • 在渲染边界框内找到最高点。从该高点开始沿着几条不同的轨迹向下行进。每当遍历线穿过高度阈值时,将该点添加到特定高度的存储桶中。当遍历路径到达局部最小值时,改变方向并向上行进。

  • 在渲染区域的矩形边界框上执行高分辨率遍历。在每个高度阈值处(以及曲率反转方向的拐点处),将这些点添加到特定高度的存储桶中。完成边界遍历后,从这些桶中的边界点开始向内进行跟踪。

  • 扫描整个渲染区域,在稀疏的规则间隔处进行高度测量。对于每个测量值,使用其与高度阈值的接近程度作为机制来决定是否对其邻居进行插值测量。使用此技术可以更好地保证整个渲染区域的覆盖范围,但是很难将结果点组装成合理的顺序以构建路径。

所以,这些是我的一些想法...

在深入实现之前,我想看看StackOverflow上是否有其他人有这种问题的经验,并且可以提供准确和高效的实现指针。

编辑:

我特别感兴趣ellisbben提出的“梯度”建议。我的核心数据结构(忽略一些优化插值快捷方式)可以表示为一组二维高斯函数的总和,这是完全可微分的。

我想我需要一个数据结构来表示三维斜坡,以及一个计算任意点的斜率向量的函数。就我目前的想法而言,我不知道如何做到这一点(尽管它似乎应该很容易),但如果您有一篇解释数学的链接,我将不胜感激!
更新:
由于ellisbben和Azim的出色贡献,我现在可以计算场中任意点的等高线角度。接下来将绘制真实的地形线!
以下是更新后的渲染图像,带有和不带有我一直在使用的“黑市”光栅化地形渲染器。每个图像包括一千个随机采样点,用红点表示。该点的等高线角度由白线表示。在某些情况下,无法在给定点测量到斜率(基于插值的粒度),因此红点出现时没有相应的等高线角度线。
享受吧!
(注意:这些渲染使用与先前渲染不同的表面地形--因为我在每次迭代时都会随机生成数据结构,当我进行原型设计时--但核心渲染方法是相同的,所以我相信您能理解。)

alt text

alt text

这里有一个有趣的事实:在这些渲染图像的右侧,您会看到一堆奇怪的轮廓线,呈完美的水平和垂直角度。这些是插值过程的产物,使用插值器网格来减少执行核心渲染操作所需的计算量(约降低了500%)。所有这些奇怪的轮廓线都出现在两个插值器网格单元之间的边界上。
幸运的是,这些产物实际上并不重要。虽然在斜率计算期间可以检测到这些产物,但最终的渲染器不会注意到它们,因为它以不同的位深度操作。

再次更新:

在我睡觉之前,作为最后的放纵,这里有另一组渲染图像,一种是老派的“连续颜色”风格,另一种是具有20,000个渐变样本的。在这组渲染图像中,我已经删除了点采样的红点,因为它会使图像过于混乱。

在这里,您可以真正看到我之前提到的插值伪影,这要归功于插值器集合的网格结构。我应该强调,这些伪影在最终轮廓渲染中将完全不可见(因为任何两个相邻插值器单元之间的差异都小于呈现图像的位深度)。

祝您用餐愉快!

alt text

alt text


你说数据是连续的 - 这是否意味着你能够通过算法计算出像素之间坐标的值? - Alnitak
是的,我可以计算任何坐标处的值(具有64位浮点精度)。数据结构实际上是一组2D高斯函数,每个函数都有自己的振幅和标准偏差,插值使用核密度估计器来查找这些中间值。 - benjismith
此外,核密度估计器将渲染区域划分为“插值单元”的矩阵,以便在渲染器没有足够的位深度来渲染这些差异时避免执行昂贵的计算。 - benjismith
关于gnuplot的有趣建议。看起来它可以为3D函数进行不错的等高线渲染。我可能会尝试一下(或者我可能会查看他们的源代码以了解他们是如何做到的):http://chem.skku.ac.kr/~wkpark/tutor/gnuplot/gpdocs/contours.htm - benjismith
我对贝塞尔曲线不是很熟悉,这个能帮你构建它们吗?你能详细说明如何完成它吗? - erickson
显示剩余2条评论
9个回答

9

渐变是一个数学运算符,可以帮助您。

如果您能将插值转换为可微分函数,则高度的梯度将始终指向最陡上升的方向。所有相同高度的曲线都垂直于在该点评估的高度梯度。

您关于从最高点开始的想法是合理的,但如果存在多个局部最大值,则可能会错过特征。

我建议:

  1. 选择要绘制线条的高度值
  2. 在精细,定期间隔的网格上创建一堆点,然后沿着梯度方向向每个点小步行走,直到最近的高度为止,其中您要绘制一条线
  3. 通过使每个点垂直于梯度来创建曲线;通过杀死另一条曲线太靠近它而消除多余的点-但为了避免破坏沙漏形状的中心,您可能需要检查两个点的垂直于梯度的定向向量之间的角度。(当我说定向时,我的意思是确保您计算的梯度和垂直值之间的角度始终以相同的方向为90度。)

非常好。请查看我最近的编辑以获取进一步的澄清问题。 - benjismith

4

针对您对@erickson的评论并回答有关计算函数梯度的问题。 您可以使用数值微分而不是计算300个项的导数,方法如下:

给定图像中的一个点[x,y],您可以计算梯度(最陡峭的方向)。

g={  ( f(x+dx,y)-f(x-dx,y) )/(2*dx), 
  {  ( f(x,y+dy)-f(x,y-dy) )/(2*dy) 

dx和dy可以是您网格中的间距。等高线将垂直于梯度运行。因此,为了获得等高线方向c,我们可以将g=[v,w]乘以矩阵A=[0 -1, 1 0]。

c = [-w,v]

是的,这是我的第一个想法,只是获取x斜率和y斜率的经验性测量。但我不知道如何将其转换为梯度。所以我开始计算整个巨大函数的偏导数。感谢矩阵乘法技巧! - benjismith

3

另外,有一个 marching squares算法似乎适合您的问题,但是如果您使用粗网格,则可能希望平滑结果。

您想要绘制的拓扑曲线是2维标量场的等值面。对于3维中的等值面,有 marching cubes算法。


2

我自己也想要这样的东西,但没有找到基于矢量的解决方案。

不过,基于光栅的解决方案也不错,特别是如果您的数据是基于光栅的。如果您的数据也是基于矢量的(换句话说,您拥有表面的三维模型),则应该能够进行一些真正的数学计算,以找到与不同高度的水平平面相交的曲线。

对于基于光栅的方法,我查看每对相邻像素。如果一个像素在等高线水平以上,而另一个在以下,则显然等高线在它们之间运行。我用来抗锯齿等高线的技巧是将等高线颜色混合到两个像素中,比例接近理想化的等高线。

也许一些示例会有所帮助。假设当前像素处于12英尺的“高度”,相邻像素处于8英尺的高度,并且等高线每10英尺一条。然后,在中间有一条等高线;使用50%不透明度的等高线颜色涂上当前像素。另一个像素位于11英尺处,其相邻像素位于6英尺处。使用80%不透明度的颜色涂上当前像素。

alpha = (contour - neighbor) / (current - neighbor)

很遗憾,我手头没有代码,并且可能还有一些细节需要注意(我模糊地记得还要考虑对角线邻居,并通过 sqrt(2) / 2 进行调整)。希望这足以让您了解大意。


我没有3D模型,但是我的系统中的任何表面都可以表示为一个方程,因此它肯定符合任意精度向量表示的要求。但是这个方程非常复杂和难看。例如,这个页面上的图像使用了一个超过300个术语的方程。它非常丑陋。 - benjismith
是的,我记得你之前关于为插值准备数据集的问题。你可以使用它来做比线性插值更精确的事情,但由于你的目标输出仍然是一个像素,所以差异可能不可见。 - erickson
我喜欢你的方法来确定轮廓线是否运行在两个像素之间。我在我的渲染中采用的方法是看像素值是否接近真实的轮廓线,给定任意的epsilon。(...继续...) - benjismith
我认为你的技术会产生更好的结果,但处理时间会增加4倍。另一方面,你可以结合这两种方法,只有当像素值在轮廓高程的epsilon范围内时才执行多像素例程。嗯…… - benjismith
是的,我仍然想要矢量样条线。实际上,我正在尝试为我的童子军制作定向地图,并希望开发SVG输出。 - erickson

1

我想到了一个方法,你可以在MATLAB中使用等高线函数来实现你想要做的事情。通过对等高线进行低密度近似处理这样的操作,可能只需要对等高线进行一些简单的后处理即可完成。

幸运的是,GNU Octave是MATLAB的克隆版本,它有各种等高线绘图函数的实现。你可以查看该代码以获取几乎肯定是数学上正确的算法和实现。或者,你也可以将处理过程转移到Octave中。请查看与其他语言交互页面,看看是否更容易。

声明:我没有多少使用Octave的经验,也没有测试过它的等高线绘图功能。但是,从我使用MATLAB的经验来看,只需几行代码就可以提供你所需的几乎所有功能,前提是你将数据导入MATLAB中。

此外,祝贺你制作了一个非常像VanGough斜坡场图。


0

我推荐使用CONREC方法:

  • 创建一个空的线段列表
  • 将数据分割成规则的网格正方形
  • 对于每个网格正方形,将正方形分成4个组成三角形:
    • 对于每个三角形,处理情况(a到j):
      • 如果一条线段穿过其中一个情况:
        • 计算其端点
        • 将该线段存储在列表中
  • 绘制线段列表中的每条线段

如果线条太锯齿状,请使用较小的网格。如果线条足够平滑且算法运行时间过长,请使用较大的网格。


0

0

将您渲染的内容与真实世界的地形图进行比较 - 在我看来它们完全一致!我不会做任何改变...


好的,现在线条的位置非常好。但是线条本身都是断断续续、模糊不清和凌乱的。将拓扑线绘制为(...继续...) - benjismith
贝塞尔曲线(我认为)可以提供非常好的清理效果。但它也会带来一些额外的好处,比如使高程区域可以通过鼠标单击进行选择(这将非常酷!) - benjismith

0
将数据写出为HGT文件(一种由美国地质调查局使用的非常简单的数字高程数据格式),并使用免费且开源的gdal_contour工具创建等高线。这对于陆地地图非常有效,约束条件是数据点是带符号的16位数字,非常适合以米为单位的地球高度范围,但可能不足以处理您的数据,我假设您的数据不是实际地形的地图 - 尽管您提到了地形地图。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接