使用 Perl 和 Imager 模块增强地图绘制应用程序,方法是根据颜色提取和应用高度信息以显示第三维数据,从而在同一个空间中显示更多信息。
最近出现的应用程序极大地简化了开发并使 2-D 地图更加普遍。Microsoft® Live Search Maps 和 Google Maps 等工具将提供增强这些单平面地图的大量工具,但是通常忽略将高度作为第三维信息。本文将提供工具和代码以允许您根据像素颜色提取高度信息,并且跨地图上下文应用高度信息。最终得到第三维数据,从而在同一个空间中显示更多信息并为地图用户提供了一种新的显示方法。
本文将演示如何使用 Imager Perl 模块来读取和处理图像,从而为源图像创建单像素级别的高度段。用 Keyhole 标记语言(Keyhole Markup Language,KML)组合这些图像片段,而 Google Earth 将显示新数据集的近似体积显示效果。
硬件和软件要求
您将需要一台现代计算机才能运行 Google Earth,包括使用 3-D 加速器获得合适的效果。用 Imager 处理大型图像可能需要耗费一些时间,因此拥有快速的处理器和大量 RAM 十分有用。
除了 Google Earth 之外,您将需要 Imager Perl 模块(请参阅 参考资料)以及 Perl 本身。注意,本文提供的所有代码是跨平台的,并且应当可以在运行 Google Earth 和 Perl 的任何一个平台中运行。
本文中使用的一般方法
为了演示简介中描述的概念,使用了美国周边地区的 NOAA 气象雷达数据。确定每个高度的相关颜色梯度,提取每个像素的高度数据,然后将第二级图像分层以生成计算出的高度图像。图 1 显示出示例效果。
图 1. 2-D 和 3-D 降水显示效果示例
在上面的示例中,顶部图像显示了大气中水量回返的标准 2-D 图像。底部的图像显示了同一个回返图像,其中根据显示云顶高度的第二张图像映射高度属性。无论是从很远的距离查看,还是近地查看,Google Earth 图像覆盖显示将给查看者提供更广泛的信息。
虽然主要关注 “Echo Tops” 和 “Merged Reflectivity Composite” 图的雷达返回数据,但是这些技术适用于任意一组平面图像。
基于颜色提取高度信息
NOAA 为获取其图像产品提供了优秀资源,而不是为了数据本身。有关 KML 和图像文件的链接,请参阅 参考资料。在本文中,Echo Tops 图像将提供高度数据,但是将不生成任何可视的像素。所有可视图像均来自 “Merged Reflectivity Composite” 图。图 2 显示了云顶显示效果及相关的空中高度数据示例。
图 2. 有颜色的 Echo Top 图像和键值示例
位于左下角的无序斑点是表示来自整张图像中所选部分的云顶高度的像素数据。以 kft 为单位的高度信息以及颜色条的放大部分显示在图像的顶部。注意多种颜色如何构成渐变的颜色条及其索引值以相对不一致的方式变化。这种不一致性以及在将颜色转换为高度时缺少可以轻松处理的表格,这些是从图像中提取数据时必须处理的常见问题。最常见的问题是用于生成图像的原始数据不可用。
创建每个 “颜色区域” 的平均颜色值是通过首次提取图像右下角中所示的每个颜色区域来执行的。这些色块是从 Echo Tops key 图像中提取的,并且另存为 color.tiles/ 目录中的一系列文件。如果所选数据没有包含每个高度级别的单个颜色,则需要模拟该过程。确保用可以通过 ls -1 color.tiles/ 命令正确排序的文件名保存文件 — 例如:c00.png、c01.png 等。
用 Perl 和 Imager 映射高度
在创建颜色层后,程序的第一步是读取这些颜色层并生成每个层的平均颜色值。创建名为 altitudeMapper.pl 的文件并插入清单 1 的内容。
清单 1. altitudeMapper.pl 头文件,读取颜色层
#!/usr/bin/perl -w # altitudeMapper.pl - create slices of images based on computed height from # color data in separate image use strict; use Imager; die "specify altitude image, slicee image " unless ( @ARGV == 2 ); my( $fNameAlt, $fNameSli ) = @ARGV; my %cls = (); # color averages my %hgt = (); # height data for each pixel ######## compute the average color for each tile ######## for my $fName ( `ls -1 color.tiles/` ) { chomp($fName); my $tileImage = Imager->new; $tileImage->read(file=>"color.tiles/$fName") or die "cannot open $fName: "; my( $rAvg, $gAvg, $bAvg ) = 0; for( my $colN = 0; $colN <= ($tileImage->getwidth -1); $colN++ ) { for( my $rowN = 0; $rowN <= ($tileImage->getheight -1); $rowN++ ) { my $col2 = $tileImage->getpixel( x=>$colN, y=>$rowN ); my ( $r,$g,$b ) = $col2->rgba(); # count all of the colors for averaging later $rAvg += $r; $gAvg += $g; $bAvg += $b; }#row }#column $cls{ $fName }{ r } = $rAvg / ($tileImage->getwidth * $tileImage->getheight); $cls{ $fName }{ g } = $gAvg / ($tileImage->getwidth * $tileImage->getheight); $cls{ $fName }{ b } = $bAvg / ($tileImage->getwidth * $tileImage->getheight); }#for each color.tiles |
在变量声明和用法检查后,将读取 color.tiles 目录中的各个颜色区域文件。使用 Imager getpixel 子例程,记录每个像素的红、绿和蓝颜色组成,程序将在收集了所有数据点后计算其平均值。对于稍后在程序中查找最接近的匹配,这一步至关重要。
代码的下一部分将读取 Echo Tops 图像并根据图像中每个像素的颜色记录相应高度设置。
清单 2. altitudeMapper.pl 读取图像高度数据
########### Read height data ############## my $altImage = Imager->new; $altImage->read( file=> $fNameAlt ) or die "can't open $fNameAlt: "; my $maxCol = $altImage->getwidth -1; # getwidth is a very slow read my $maxRow = $altImage->getheight -1; # getheight is a very slow read for( my $colN = 0; $colN <= $maxCol; $colN++ ) { for( my $rowN = 0; $rowN <= $maxRow; $rowN++ ) { my $currColor = $altImage->getpixel( x=>$colN, y=>$rowN ); my ($r,$g,$b ) = $currColor->rgba(); # skip if white or black pixel next unless ( "$r,$g,$b" ne "0,0,0" && "$r,$g,$b" ne "255,255,255" ); my $totalDev = 10000; my $closest = "none"; for my $key( keys %cls) { my $devR = abs( $cls{$key}{r} - $r ); my $devG = abs( $cls{$key}{g} - $g ); my $devB = abs( $cls{$key}{b} - $b ); # skip if deviation is greater than currently selected next unless ( ($devR + $devG + $devB) < $totalDev ); $totalDev = $devR + $devG + $devB; $closest = $key; }#for key comparison $hgt{ $closest }{ $colN }{ $rowN } = $currColor; }#row print "$colN\n" if( $colN % 100 == 0 ); # progress indicator }#column |
本例中使用的图像是 6,000 x 3,000 像素,因此读取 getwidth 和 getheight 子例程是在 for 循环之前完成的,这样可以显著提高速度。然后读取每个像素,并且通过上一步中记录的每个平均颜色测量其红、绿和蓝色偏差。在找到最接近的匹配后,该像素的高度设置存储在 %hgt 散列变量中。现在相应的高度信息已经记录,允许通过如下所示的第二张图像创建图像分层。
清单 3. 基于高度的 altitudeMapper.pl 图像分层程序
############ Apply height data to image ########### my %layer = (); # pixel x,y data for that layer my %avgLev = (); # average colors for that layer my $lev = keys %cls; # number of image components to process my $compImage = Imager->new; $compImage->read( file=>$fNameSli ) or die "Cannot load $fNameSli: "; $maxCol = $compImage->getwidth -1; # getwidth is a very slow read $maxRow = $compImage->getheight -1; # getheight is a very slow read # process the layers from top to bottom for my $key( reverse sort keys %cls ) { my $imgOut = Imager->new; $imgOut->read( file=>"images/blank6000x3000.png") or die "blank image borked"; for( my $colN = 0; $colN <= $maxCol; $colN++ ) { for( my $rowN = 0; $rowN <= $maxRow; $rowN++ ) { # if layer data exists, and not on the top level if( exists($layer{ $colN }{ $rowN }) && ($lev < (keys %cls)) ) { # the color average from the previous layer my $newColor = Imager::Color->new( $avgLev{ $lev+1 }{ r }, $avgLev{ $lev+1 }{ g }, $avgLev{ $lev+1 }{ b } ); $imgOut->setpixel( x=>$colN, y=>$rowN, color=>$newColor ); }#set to previous layer if exists |
在声明变量并且打开要 “分层” 的图像后,将处理 %hgt 散列中的每个层。imgOut 变量将保存用于像素布置的透明 6,000 x 3,000 画布,并在每次传递结束时写入磁盘中。如果上面一层存在层数据(并且之上还有一层),则先前层的这些平均颜色数据将被写入输出图像。这将确保图像中没有 “缺口”,其中较低的图层将不包含来自较高图层的相应图像。如您在图 1 中所见,底层是一致的颜色,即使较高图层包含不同的黄色和绿色梯度。填充这些缺口是将平面图转换为更适合 3-D 环境的显示效果的要求之一。
如果该图层的数据存在于 $fNameSli(Merged Reflectivity Composite)图像中,则清单 4 将重写这些先前的图层像素。
清单 4. altitudeMapper.pl 复制新图层像素
# copy pixels from the original image if height data exists for this layer if( exists( $hgt{ $key }{ $colN }{ $rowN } ) ) { my $currColor = $compImage->getpixel( x=>$colN, y=>$rowN ); my($r, $g, $b) = $currColor->rgba(); $imgOut->setpixel( x=>$colN, y=>$rowN, color=>$currColor ); $layer{ $colN }{ $rowN } = $currColor; $avgLev{ $lev }{ r } += $r; $avgLev{ $lev }{ g } += $g; $avgLev{ $lev }{ b } += $b; $avgLev{ $lev }{ count } ++; }# if height data exists for that pixel }#row }#column |
除了从 $fNameSli 图像中复制像素之外,将记录该像素的颜色数据以供计算平均值。这将确保从较高图层填充缺口的每一块的颜色背景都是统一的。清单 5 将输出最终图像并计算每个图层颜色值的平均值。
清单 5. altitudeMapper.pl 输出图像,计算图层的平均值
print "write tiles/$key\n"; $imgOut->write( file=>"tiles/$key", type=>"png") or die "can't write $key"; # compute color averages for this level if( exists( $avgLev{ $lev }{ count } ) ) { $avgLev{ $lev }{ r } = $avgLev{ $lev }{ r } / $avgLev{ $lev }{ count }; $avgLev{ $lev }{ g } = $avgLev{ $lev }{ g } / $avgLev{ $lev }{ count }; $avgLev{ $lev }{ b } = $avgLev{ $lev }{ b } / $avgLev{ $lev }{ count }; }#if layer exists $lev--; }#for each key |
注意,$fNameSli(Merged Reflectivity Composite)像素的平均颜色记录在每个图像坐标的 %avgLev 散列中。某些像素颜色可以呈现到不在 $fNameSli 图像中的颜色区域层中,并且这种平均有助于更好地显示没有精确匹配颜色的图像。
altitudeMapper.pl 用法
在 altitudeMapper.pl 代码就绪后,您需要为分层图像和输出图像创建目录,并且创建源透明图像。清单 6 的前三行显示了必须使用的命令。
清单 6. 用法命令
mkdir images mkdir tiles convert -size 6000x3000 xc:none images/blank6000x3000.png # or create with The Gimp perl altitudeMapper.pl \ images/EchoTop_18_20080725-061737.png \ images/MergedReflectivityComposite_20080725-061750.png |
清单 6 中的最后三行将显示 altitudeMapper.pl 程序本身的用法。注意,\ 字符表示用于格式化目的的换行符。第一张图像是云顶数据,而第二张图像是要划分层次的源数据。
生成 KML
您可以浏览位于 tiles 目录中的输出图像文件,也可以继续查看清单 7。
清单 7. kmlGenerator.pl 程序
#!/usr/bin/perl -w # kmlGenerator.pl - create kml for sliced images created by altitudeMapper.pl use strict; die "specify starting altitude, layer distance" unless @ARGV==2; my ( $alt, $distance ) = @ARGV; my $drawOrder = 1; print qq{<?xml version="1.0" encoding="UTF-8"?> \n}; print qq{<kml xmlns="http://earth.google.com/kml/2.0"> \n}; print qq{<Folder> \n}; print qq{ <name>kmlGenerator output</name>\n}; while( my $line = <STDIN> ) { chomp($line); print " <GroundOverlay>\n"; print " <drawOrder>$drawOrder</drawOrder>\n"; print " <Icon><href>$line</href></Icon>\n"; print " <altitude>$alt</altitude>\n"; print " <altitudeMode>absolute</altitudeMode>\n"; print " <name>$drawOrder</name>\n"; print " <LatLonBox>\n"; print " <north>51.000</north>\n"; print " <south>21.000</south>\n"; print " <east>-67.000</east>\n"; print " <west>-127.000</west>\n"; print " </LatLonBox>\n"; print " </GroundOverlay>\n"; $drawOrder++; $alt += $distance; }#while line in print "</Folder>\n"; print "</kml>\n"; |
kmlGenerator.pl 程序将围绕分层图像封装相应的 KML。图层之间的起始高度和距离的各种组合将在特定的距离和视角上生成不同的体积效果。清单 8 将用生成图 1 底部所示图像所使用的参数演示程序用法。
清单 8. kmlGenerator.pl 用法
ls -1 tiles/* | perl kmlGenerator.pl 20000 10000 > kmlOutput.kml |
在 Google Earth 中打开 kmlOutput.kml 文件,查看新的增强了高度属性的图像。
结束语
可以看到,根据独立图像中的图像数据将图像划分为特定组成,可以将第三维数据有效地添加到先前的平面显示效果中。这些颜色平均和图层填空技巧只是实现类似效果的其中一种方法。考虑绘制跟踪高度等于云顶的特定图层边缘的多边形。或者,添加与地平面垂直的、计算出透明度的透明图形,创建针对所有视角的更真实的分层效果。(责任编辑:A6)