1. 用ogr和PIL把矢量数据转化成栅格图像

1.1. 求索

对于类似MapServer这样的WebGIS来说(不要跟我讲Google Map……),有一个把矢量转化到栅格的需求。因为各浏览器对矢量图形支持并不算很好,svg需要下载插件,vml只能用于ie的绘图,其他内核的浏览器就无法识别(现在的网络编程可以用一个词来形容--乱糟糟,各个浏览器都有自己的规范。一段JavaScript不能保证在所有浏览器中都能按预订的方式运行,真正的一次编写,到处调试 -_-!)于是只剩下图像了(还好各个浏览器对图像支持还比较一致)。(发发牢骚,为什么当时设计第一个浏览器的时候,不首先把矢量支持放进去?等到天下大乱才来统一江湖,就难了。这时候,就是一将功成万骨枯了。可怜的都是我们这些小小的程序员!在这纷乱的世界中如尘埃一般被扫入历史的垃圾堆)

前一段用mod_python实现了一个类似MapServer的东西,但是只支持栅格,现在想玩玩矢量,于是我在网上狂搜能把矢量数据转化成图像的库,但结果几乎绝望。倒是找到有个svg->图像的库matplotlib,看了简介,好像可以把SVG、PS、EMF变成栅格图,底层使用agg来转化--看上去很美。不过如果把XY坐标先弄成SVG,然后转成栅格图,中间要通过svg的文本,二进制变文本……算了,成倍的体积增长,我还心疼我的硬盘呢!而且就算把二进制画成svg了,还要svg转栅格,还不如直接用svg好了。

我后来又想,在wxwidgets中画图有个buffer模式,那个buffer就是个图片,能不能在buffer中绘制完矢量图形后再把buffer导出成图片呢?后来想想,自己都想笑,难道前端浏览器在浏览,后端服务器还要开个wx窗口在那里画图?几百个用户怎么办,难道开几百个窗口?呵呵,亏我想得出来。(不过我总觉得,wx既然能把矢量画到buffer的bmp图片中,里面应该有矢量到栅格转换的代码,不过我wx接触得不深,不知道有没有相关调用接口可以在不开窗口的情况下把矢量图画到图片上,如果你知道可以,不妨EMail告诉我。不过想到wx的体积……最终还是放弃了)

难道……真的要我自己写…… -_-!

那天百无聊赖翻看PIL的文档,突然,一颗绚烂的流星划破暗夜的天际,佛的光辉瞬间笼罩了我的全身,整个世界在一刹那豁然开朗,黑暗往两旁推开,一座洒满落英的虹桥在脚下盛开、延展,我仿佛渐渐地听到了天界仙子们天籁般的笑语,于是凡体一轻,竟然飘荡起来…… -- 打住打住,这里不是在写玄奇小说 :) 用词夸张了点,不过真的无法形容当时的欣喜--我找到了!

用PIL的Image.Draw可以把矢量图形画到一个图像中。这个功能对于ogr来说就意味着有一个可以和图像进行转化的通道!!

梦里寻他千百度,蓦然回首,那库却在灯火阑珊处!

PIL和GDAL可以互相共享数据,互相协调互相补充,这很好。现在再加上PIL对ogr的矢量绘图支持,真的是--

于是我抬起我冷峻的头颅,剑如水一般从华丽的剑鞘中缓缓地流淌而出,极慢极慢……

终于,一道华彩如电般闪过,一切在瞬间升华后又归于平静,空气像是冻结了一样。

周围,死寂……

只有风,无声无息得从脸边划过,如刀。

又终于,我开口说话了,然而却只有两个字--

--“Perfect!”

这就是我对PIL+gdal+ogr的评介!

(似乎这一篇的名称可以改成“江湖”,哈哈,说笑,小说看多了)

1.2. 身手

我们现在来看看ImageDraw模块到底怎么玩。

请大家翻开PIL手册第32页,这里有一个例子

   1 Example: Draw a Grey Cross Over an Image
   2 import Image, ImageDraw
   3 im = Image.open("lena.pgm")
   4 draw = ImageDraw.Draw(im)
   5 draw.line((0, 0) + im.size, fill=128)
   6 draw.line((0, im.size[1], im.size[0], 0), fill=128)
   7 del draw
   8 # write to stdout
   9 im.save(sys.stdout, "PNG")

这个例子展示了怎么在一个图片上画矢量图,这么看来在图片上画图和在wx的窗口中画图没什么两样。最后只是多个save步骤罢了。

注意,这个例子如果什么都不干直接运行是会出错的。提示没有lena.pgm的图片存在。

这个lena.pgm是什么东东?我当时以为是一个格式模板,找遍手册也没有发现其他的例子。后来以为是一个空文件,但是建立后发现PIL不认。最后才发现其实它是一张图片。要我们自己建立的。可以打开附件里的画图软件,保存一个空的png格式图片,然后改扩展名为lena.pgm,就可以了。然后运行,出来的效果……等一下,控制台疯了!出来的是什么东东?

呵呵,那是一张图。你可以用这一句来代替最后那句语句

   1 im.save("lena.png","PNG")

运行,在本目录下生成一个lena.png的图片,打开看看,是一个白底色的图片,上面有个线条划出的叉!

很好。我们迈出了可喜的第一步。

下面开始看看那段代码到底做了什么。

首先是打开一张图(Image.Open,空不空白无所谓),然后打开绘制通道(ImageDraw.Draw,在图像上建立一个绘制环境),然后抬笔画一条线(第一个draw.line,从[0,0]画到图像的最宽最长处,也就是从左上抬笔画到右下。画笔填充的颜色是128)然后抬笔又画一条线(第二个draw.line,位置从左下画到右上,填充也是128)然后释放绘制环境(del draw),然后把被画的图片另存为一个新的图片(im.save,对于原例子是把图片打印到标准输出,也就是控制台上,但是那样做我们只可以领略来自火星的朋友的问候,而对于我那句就是把它保存成png格式的图片,让我们可以看到画矢量数据到图片上的真实效果。)

通过这个例子,我们可以练习练习姑苏慕容的以彼之道还施彼身,把代码接过包装包装,再扔出我们的代码。

首先导入库们

   1 import Image, ImageDraw
   2 import sys
   3 import ogr
   4 from Numeric import *

然后打开一个ShapeFile文件获取里面的信息

   1 ds = ogr.Open("e:/gisdata/data/streets.shp")
   2 layer = ds.GetLayer()
   3 extent = layer.GetExtent()
   4 geowidth = extent[1]-extent[0]
   5 geoheight = extent[3]-extent[2]
   6 width = 1000
   7 height = 1000/geowidth*geoheight
   8 scale = geowidth/width

注意,这里我把宽度控制在1000内。不控制……地球那么大,难道你要画那么大?你答应,你的硬盘也不答应。

下面创建一个空的PNG图像。

   1 # create a new png file
   2 imn = Image.new("RGB",(width,height),(255,255,255))
   3 imn.save("test.pgm","PNG")
   4 del imn

这里的Image.new创建了一个RGB颜色模式的图片,大小是上面计算的图形在缩小到宽1000时的大小。填充为RGB(255,255,255) 创建Image有好几种模式,具体看手册第10页的mode,依次是1位黑白,8位黑白,8位灰度,RGB真彩色,RGBA带透明的彩色,CMYK黄品青黑印刷色,YCbCr亮蓝红数码色,32位整数,32位浮点等等模式,自己看着办吧。不过要指定了颜色模式,填充色就要注意了,填充的数组大小和数据类型应该和颜色模式对应。RGB的就用三个0~255值的数组来设置,RGBA就要四个……

Image.new并不能写入数据到硬盘上,所以要用imn.save写入,这里保存的是PNG的格式。

最后释放资源。

这样在硬盘上就有可以拿来画的画布了。不用再开附件的画图程序了。

下面就在画布上任意挥洒你的画笔吧(确切的说是PIL的画笔)!

为了看代码舒服点,我封装了一个类,全部代码如下:

   1 import Image, ImageDraw
   2 import sys,math
   3 import ogr
   4 from Numeric import *
   5 class PILImgDC:
   6     def __init__(self,layer):
   7         self.layer = layer
   8         self.features = [layer.GetFeature(i) for i in range(layer.GetFeatureCount())]
   9         FeatureDrawMap = {
  10             #ogr.wkbPoint:self.__DrawPoint,
  11             #ogr.wkbMultiPoint:self.__DrawPoints,
  12             ogr.wkbLineString:self.__DrawLine,
  13             ogr.wkbMultiLineString:self.__DrawLines,
  14             #ogr.wkbPolygon:self.__DrawPolygon,
  15             #ogr.wkbMultiPolygon:self.__DrawPolygons
  16             }
  17         self.featureDrawMap = FeatureDrawMap
  18         self.fill = (128,128,128)
  19     def SetFillColour(self,color):
  20         self.fill = color
  21     def SetInputImg(self,imgname):
  22         self.iimgname = imgname
  23     def DrawAll(self,width,ofname):
  24         extent = layer.GetExtent()
  25         geowidth = math.fabs(extent[1]-extent[0])
  26         geoheight = math.fabs(extent[3]-extent[2])
  27         self.minx = min(extent[0],extent[1])
  28         self.maxy = max(extent[3],extent[2])
  29         height = width/geowidth*geoheight
  30         self.scale = width/geowidth
  31         im = Image.new("RGB",(width,height),(255,255,255))
  32         self.draw = ImageDraw.Draw(im)
  33         for feature in self.features:
  34             geomref = feature.GetGeometryRef()
  35             geomreftype = geomref.GetGeometryType()
  36             drawFun = self.featureDrawMap[geomreftype]
  37             drawFun(geomref)
  38         del self.draw
  39         im.save(ofname, "PNG")
  40     def __GetPoints(self,georef):
  41         pointcount = georef.GetPointCount()
  42         xs,ys = [],[]
  43         for i in range(pointcount):
  44             xs.append(georef.GetX(i))
  45             ys.append(georef.GetY(i))
  46         xs = array(xs,Float); ys = array(ys,Float)
  47         xs = (xs-self.minx)*self.scale
  48         ys = (self.maxy-ys)*self.scale
  49         xs.shape=(-1,1);ys.shape=(-1,1)
  50         points = concatenate((xs,ys),1)
  51         return points
  52     def __DrawLine(self,georef):
  53         points = self.__GetPoints(georef)
  54         xys = reshape(points, (1,-1))[0]
  55         self.draw.line(xys,fill=self.fill)
  56     def __DrawLines(self,georef):
  57         sublinecount = georef.GetGeometryCount()
  58         for i in range(sublinecount):
  59             self.__DrawLine(georef.GetGeometryRef(i))
  60 if __name__=="__main__":
  61     ds = ogr.Open("e:/gisdata/shp/lines.shp")
  62     layer = ds.GetLayer()
  63     dc = PILImgDC(layer)
  64     dc.SetFillColour((100,100,0))
  65     dc.DrawAll(1000,"output.png")

要紧的步骤都在DrawAll方法里面。

其实和上面介绍的步骤一样,打开layer(这在外面做了),计算长宽和缩放系数,新建一个内存区域准备画图(注意这里我们不再需要pgm文件,我们直接在内存中建立一块区域,然后在内存中画,pgm文件只是说明可以在现有图像上画矢量图而已),然后for循环把feature里面的几何图像的XY点坐标全部塞入数组,然后把这些数组组成的线画到图像中。保存输出。_ _ GetPoints是获取线段的所有点(你当然会做得比我更快),速度的关键就在这里了。_ _ DrawLine和_ _ DrawLines两个函数是进行线和多线几何图形结构的解析。我这里只实现了线的绘制,关于点,多点,多边形,多多边形,我把登记位置留在_ _init_ _的字典中,后期只要加进去就好。

关于画其他的形状,比如多边形等可以看PIL手册32页开始Methods小项底下那些函数解析,有画弧,位图,弦,椭圆,线,扇形弧,点,多边形,正方形,还可以写字符。当然我们不需要那么多。只要点,线,多边形,差不多就OK了,如果要标注,可以用用draw.text。够了。

最后还要注意:用来画矢量图的可以是空的图片,也可以是有东西的图片。如果是有东西的图片,那么就不能用new创建新的png。只能用open打开。上面的代码就只要在DrawAll前面调用一下:SetInputImg,传入要用来做底图的图片位置,就可以在保留原底图的条件下画出矢量图。 但是要两个图层的地理坐标能正确匹配就不是上面这几句就可以搞定的了。

打完,收功!

1.3. wx中的代码

研究了一下,wx确实可以在内存中画出来。并且保存成文件。而且不用额外打开窗口。

import wx
import random

def makeRandomLines(num, w, h):
    pnts = []
    for i in range(num):
        x1 = random.randint(0, w)
        y1 = random.randint(0, h)
        x2 = random.randint(0, w)
        y2 = random.randint(0, h)
        pnts.append( (x1,y1, x2,y2) )
    return pnts

app = wx.PySimpleApp()
bitmap = wx.EmptyBitmap(100,100)

dc = wx.MemoryDC();
dc.SelectObject(bitmap)

lines = makeRandomLines(12,100,100)

dc.BeginDrawing()

dc.DrawLineList(lines, wx.Pen("GREEN", 5))

dc.EndDrawing()

bitmap.SaveFile("f:/proj/sav.bmp",wx.BITMAP_TYPE_BMP )


哈,貌似还不错!

2. 反馈

如果您发现我写的东西中有问题,或者您对我写的东西有意见,请登陆这个论坛