How it works(7) GDAL2Mbtiles源码阅读(A) 框架与存储

引入

gdal2Mbtiles是个小工具(以下简称g2m),其作用是将栅格地图(主要是Tiff格式)切成瓦片,存入Mbtiles格式的数据库中,以便于其他支持Mbtiles格式的地图服务器直接调用.
一开始我也是为了用它来切割Tiff底图,发布Tileserver-GL服务的,不过用了一下,发现其切图速度比较快.所以想看一下其内部结构.觉得其代码并不简单,也是一个深思熟虑的系统.

整体架构

gdal2mbtiles.png

通观整体后会发现,g2m的面向对象设计做的很好.虽然最终只能输出png格式的图片,但实现了图片的基类和JPG的图片类,只能导出为Mbtiles格式却也能通过文件存储基类可以实现gdal2folder的功能.详尽的文档与充足的单元测试也说明了这是个成熟的用心的工具.

main.py

整个程序通过setup.py安装后,注册成为命令行工具.最终入口就是main.py,主要负责构建g2m所需参数.
在python中,借助ArgumentParser处理参数是非常容易的事情:

parser=argparse.ArgumentParser()
parser.add_argument("echo",help="echo the string")
args=parser.parse_args()

add_argument()常用的参数:
dest:如果提供dest,例如dest="a",那么可以通过args.a访问该参数
default:设置参数的默认值
action:参数出发的动作
store:保存参数,默认
store_const:保存一个被定义为参数规格一部分的值(常量),而不是一个来自参数解析而来的值。
store_ture/store_false:保存相应的布尔值
append:将值保存在一个列表中。
append_const:将一个定义在参数规格中的值(常量)保存在一个列表中。
count:参数出现的次数
parser.add_argument("-v", "--verbosity", action="count", default=0, help="increase output verbosity")
version:打印程序版本信息
type:把从命令行输入的结果转成设置的类型
choice:允许的参数值
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2], help="increase output verbosity")
help:参数命令的介绍

利用处理后的参数,就可以正式驱动g2m了.

def main(args=None, use_logging=True):
    if args is None:
        args = sys.argv[1:]
    args = parse_args(args=args)
  
  # 避免vips解析sys.argv
    from gdal2Mbtiles.helpers import warp_Mbtiles
  
  # 需要的话构建临时文件
    with input_output(inputfile=args.INPUT,
                      outputfile=args.OUTPUT) as (inputfile, outputfile):
        # 记录元数据
        metadata = dict(
            description=args.description,
            format=args.format,
            name=args.name,
            type=args.layer_type,
            version=args.version,
        )

        # 通过GDAL初始化指定的空间参考
        spatial_ref = SpatialReference.FromEPSG(args.spatial_reference)

        # 初始化波段
        if not args.coloring:
            colors = band = None
        else:
            colors = args.coloring(args.colors)
            band = args.colorize_band

        # 初始化图片格式
        pngdata = {'png8': args.png8}
        # 开始切割
        warp_Mbtiles(inputfile=inputfile.name, outputfile=outputfile.name,
                     # MBTiles
                     metadata=metadata,
                     # GDAL相关参数
                     spatial_ref=spatial_ref, resampling=args.resampling,
                     # 参数渲染
                     min_resolution=args.min_resolution,
                     max_resolution=args.max_resolution,
                     fill_borders=args.fill_borders,
                     zoom_offset=args.zoom_offset,
                     pngdata=pngdata,
                     # 颜色处理
                     colors=colors, band=band)
        return 0

对于输入/输出路径,这里做了特殊的预处理.其值默认为系统输入/输出,如果未指定该值,则建立临时文件.

@contextmanager
def input_output(inputfile, outputfile):
    tempfiles = []

    infile = inputfile
    if inputfile == sys.stdin:
        # 建立临时文件
        infile = NamedTemporaryFile()
        # 将数据从输入流复制到该文件
        copyfileobj(inputfile, infile)
        # 游标归0
        infile.seek(0)
        tempfiles.append(infile)

    outfile = outputfile
    if outputfile == sys.stdout:
        outfile = NamedTemporaryFile()
        tempfiles.append(outfile)

    try:
        yield infile, outfile
        # 最终从临时文件输出到输出流
        if outputfile == sys.stdout:
            copyfileobj(open(outfile.name, 'rb'), outputfile)
    finally:
        for f in tempfiles:
            f.close()

这里使用了contextmanager装饰器,将函数包装为一个支持with调用,结束后自动释放的对象.

通过给一个try…finally…结构的函数头部加上@contextmanager就可以通过with…as…结构来调用它了,这样try块中yield的数据被as出来,finally块中的数据在with..as..块结束的时候被执行。

这里默认的输入输出是系统的输入输出流.这看起来是很奇怪的,既然要用g2m处理栅格地图,输入的文件也应该是个图像文件.
其实这种实现可以使得g2m不单单作为一个闭环的工具,而作为一个由linux管道构成的工具链的一部分.在管道中,数据流从A产出,经由系统输出\输入进入g2m,处理过后再经过系统输出进入管道输出给C.

在看处于最核心的模块helper之前,需要看一下,helper所调用的都是哪些模块.

storages.py/Mbtiles.py

存储的实现.
主要实现了三种存储:

  • 单一文件夹内存储
  • 文件夹分级存储
  • Mbtiles存储

瓦片存储的功能由存储的基类定义:

class Storage(object):

    def __init__(self, renderer, pool=None):
        self.renderer = renderer
        self.hasher = intmd5

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        return

    def get_hash(self, image):
      # 获取哈希值,对于相同的瓦片,不进行重复存储.因为实际在小比例尺下,图片重复的概率会很大.
      # 问题是,所有的哈希值都存于内存,当所切级数变大时,内存占用会很大,索引效率也会降低.
        return self.hasher(image.write_to_memory())

    def filepath(self, x, y, z, hashed):
      # 文件路径,仅对文件型存储起作用
        raise NotImplementedError()

    def post_import(self, pyramid):
      # 生成金字塔后执行,仅对Mbtiles这种需要元数据描述的存储起作用
        pass

    def save(self, x, y, z, image):
    # 最重要的函数,保存瓦片,子类必须要实现
        raise NotImplementedError()

    def save_border(self, x, y, z):
      # 默认的保存边框
        self.save(x=x, y=y, z=z, image=self._border_image())

    @classmethod
    def _border_image(cls, width=TILE_SIDE, height=TILE_SIDE):
      # 类方法,生成透明的外框
        image = VImageAdapter.new_rgba(
            width, height, ink=rgba(r=0, g=0, b=0, a=0)
        )
        image._buf = image
        return image

因为g2m最终暴露的只有存储于Mbtiles中,那就来看一下Mbtiles的类是如何继承基类的:

class MbtilesStorage(Storage):
    def __init__(self, renderer, filename, zoom_offset=0, seen=set(),
                 **kwargs):
        super(MbtilesStorage, self).__init__(renderer=renderer,
                                             **kwargs)
                                             
        self.zoom_offset = zoom_offset
        self.seen = seen
        self._border_hashed = None
        self.Mbtiles = None
    # 不使用工厂模式,也会创建Mbtiles文件
        if isinstance(filename, basestring):
            self.filename = filename
            self.Mbtiles = MBTiles(filename=filename)
        else:
            self.Mbtiles = filename
            self.filename = self.Mbtiles.filename

    def __del__(self):
        if self.Mbtiles is not None:
            self.Mbtiles.close()

    def __exit__(self, type, value, traceback):
        if self.Mbtiles is not None:
            self.Mbtiles.close()

    @classmethod
    def create(cls, renderer, filename, metadata, zoom_offset=None,
               version=None, **kwargs):
        # 工厂模式创建Mbtiles文件
        bounds = metadata.get('bounds', None)
        if bounds is not None:
            metadata['bounds'] = bounds.lower_left + bounds.upper_right
        Mbtiles = MBTiles.create(filename=filename, metadata=metadata,
                                 version=version)
        return cls(renderer=renderer,
                   filename=Mbtiles,
                   zoom_offset=zoom_offset,
                   **kwargs)

    def post_import(self, pyramid):
    # 源影像建金字塔完成后,给Mbtiles赋元数据
        transform = pyramid.dataset.GetCoordinateTransformation(
            dst_ref=SpatialReference.FromEPSG(4326)
        )
        lower_left, upper_right = pyramid.dataset.GetTiledExtents(
            transform=transform
        )
        self.Mbtiles.metadata['bounds'] = (lower_left.x, lower_left.y,
                                           upper_right.x, upper_right.y)

    def save(self, x, y, z, image):
        hashed = self.get_hash(image)
        # 如果有重复的瓦片,就直接写入哈希值,而不是存储瓦片
        if hashed in self.seen:
            self.Mbtiles.insert(x=x, y=y,
                                z=z + self.zoom_offset,
                                hashed=hashed)
        else:
            self.seen.add(hashed)
            contents = self.renderer.render(image)
            if sys.version_info < (3, 0):
                data = buffer(contents)
            else:
                data = memoryview(contents)
            # 插入渲染后的瓦片
            self.Mbtiles.insert(x=x, y=y,
                                z=z + self.zoom_offset,
                                hashed=hashed,
                                data=data)

    def save_border(self, x, y, z):
      # 同瓦片一样,透明的边框也不重复渲染存储
        if self._border_hashed is None:
            image = self._border_image()
            self.save(x=x, y=y, z=z, image=image)
            self._border_hashed = self.get_hash(image)
        else:
            self.Mbtiles.insert(x=x, y=y,
                                z=z + self.zoom_offset,
                                hashed=self._border_hashed)

我们能看到,在存储瓦片到Mbtiles时,有两种方法:

  • 存储x,y,z+图像+图像的哈希值
  • 存储x,y,z+图像的哈希值

Mbtiles设计上的特性就是不存储重复的瓦片.因为它本质上是Sqlite数据库,里面存储有行列号索引表和瓦片图像索引表,
对于相同的瓦片,只要通过相同的瓦片索引,就能关联起来,可以节省大量的重复瓦片空间占用.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,976评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,249评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,449评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,433评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,460评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,132评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,721评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,641评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,180评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,267评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,408评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,076评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,767评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,255评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,386评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,764评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,413评论 2 358

推荐阅读更多精彩内容