How it works(7) GDAL2Mbtiles源碼閱讀(A) 框架與存儲

引入

gdal2Mbtiles是個小工具(以下簡稱g2m),其作用是將柵格地圖(主要是Tiff格式)切成瓦片,存入Mbtiles格式的數(shù)據(jù)庫中,以便于其他支持Mbtiles格式的地圖服務器直接調(diào)用.
一開始我也是為了用它來切割Tiff底圖,發(fā)布Tileserver-GL服務的,不過用了一下,發(fā)現(xiàn)其切圖速度比較快.所以想看一下其內(nèi)部結(jié)構(gòu).覺得其代碼并不簡單,也是一個深思熟慮的系統(tǒng).

整體架構(gòu)

gdal2mbtiles.png

通觀整體后會發(fā)現(xiàn),g2m的面向?qū)ο笤O計做的很好.雖然最終只能輸出png格式的圖片,但實現(xiàn)了圖片的基類和JPG的圖片類,只能導出為Mbtiles格式卻也能通過文件存儲基類可以實現(xiàn)gdal2folder的功能.詳盡的文檔與充足的單元測試也說明了這是個成熟的用心的工具.

main.py

整個程序通過setup.py安裝后,注冊成為命令行工具.最終入口就是main.py,主要負責構(gòu)建g2m所需參數(shù).
在python中,借助ArgumentParser處理參數(shù)是非常容易的事情:

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

add_argument()常用的參數(shù):
dest:如果提供dest闪水,例如dest="a"糕非,那么可以通過args.a訪問該參數(shù)
default:設置參數(shù)的默認值
action:參數(shù)出發(fā)的動作
store:保存參數(shù),默認
store_const:保存一個被定義為參數(shù)規(guī)格一部分的值(常量)球榆,而不是一個來自參數(shù)解析而來的值朽肥。
store_ture/store_false:保存相應的布爾值
append:將值保存在一個列表中。
append_const:將一個定義在參數(shù)規(guī)格中的值(常量)保存在一個列表中持钉。
count:參數(shù)出現(xiàn)的次數(shù)
parser.add_argument("-v", "--verbosity", action="count", default=0, help="increase output verbosity")
version:打印程序版本信息
type:把從命令行輸入的結(jié)果轉(zhuǎn)成設置的類型
choice:允許的參數(shù)值
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2], help="increase output verbosity")
help:參數(shù)命令的介紹

利用處理后的參數(shù),就可以正式驅(qū)動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
  
  # 需要的話構(gòu)建臨時文件
    with input_output(inputfile=args.INPUT,
                      outputfile=args.OUTPUT) as (inputfile, outputfile):
        # 記錄元數(shù)據(jù)
        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相關參數(shù)
                     spatial_ref=spatial_ref, resampling=args.resampling,
                     # 參數(shù)渲染
                     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

對于輸入/輸出路徑,這里做了特殊的預處理.其值默認為系統(tǒng)輸入/輸出,如果未指定該值,則建立臨時文件.

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

    infile = inputfile
    if inputfile == sys.stdin:
        # 建立臨時文件
        infile = NamedTemporaryFile()
        # 將數(shù)據(jù)從輸入流復制到該文件
        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裝飾器,將函數(shù)包裝為一個支持with調(diào)用,結(jié)束后自動釋放的對象.

通過給一個try…finally…結(jié)構(gòu)的函數(shù)頭部加上@contextmanager就可以通過with…as…結(jié)構(gòu)來調(diào)用它了衡招,這樣try塊中yield的數(shù)據(jù)被as出來,finally塊中的數(shù)據(jù)在with..as..塊結(jié)束的時候被執(zhí)行每强。

這里默認的輸入輸出是系統(tǒng)的輸入輸出流.這看起來是很奇怪的,既然要用g2m處理柵格地圖,輸入的文件也應該是個圖像文件.
其實這種實現(xiàn)可以使得g2m不單單作為一個閉環(huán)的工具,而作為一個由linux管道構(gòu)成的工具鏈的一部分.在管道中,數(shù)據(jù)流從A產(chǎn)出,經(jīng)由系統(tǒng)輸出\輸入進入g2m,處理過后再經(jīng)過系統(tǒng)輸出進入管道輸出給C.

在看處于最核心的模塊helper之前,需要看一下,helper所調(diào)用的都是哪些模塊.

storages.py/Mbtiles.py

存儲的實現(xiàn).
主要實現(xiàn)了三種存儲:

  • 單一文件夾內(nèi)存儲
  • 文件夾分級存儲
  • 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):
      # 獲取哈希值,對于相同的瓦片,不進行重復存儲.因為實際在小比例尺下,圖片重復的概率會很大.
      # 問題是,所有的哈希值都存于內(nèi)存,當所切級數(shù)變大時,內(nèi)存占用會很大,索引效率也會降低.
        return self.hasher(image.write_to_memory())

    def filepath(self, x, y, z, hashed):
      # 文件路徑,僅對文件型存儲起作用
        raise NotImplementedError()

    def post_import(self, pyramid):
      # 生成金字塔后執(zhí)行,僅對Mbtiles這種需要元數(shù)據(jù)描述的存儲起作用
        pass

    def save(self, x, y, z, image):
    # 最重要的函數(shù),保存瓦片,子類必須要實現(xiàn)
        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
    # 不使用工廠模式,也會創(chuàng)建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):
        # 工廠模式創(chuàng)建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賦元數(shù)據(jù)
        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設計上的特性就是不存儲重復的瓦片.因為它本質(zhì)上是Sqlite數(shù)據(jù)庫,里面存儲有行列號索引表和瓦片圖像索引表,
對于相同的瓦片,只要通過相同的瓦片索引,就能關聯(lián)起來,可以節(jié)省大量的重復瓦片空間占用.

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末始腾,一起剝皮案震驚了整個濱河市州刽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浪箭,老刑警劉巖穗椅,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異奶栖,居然都是意外死亡匹表,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門宣鄙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桑孩,“玉大人,你說我怎么就攤上這事框冀×鹘罚” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵明也,是天一觀的道長宣虾。 經(jīng)常有香客問我,道長温数,這世上最難降的妖魔是什么绣硝? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮撑刺,結(jié)果婚禮上鹉胖,老公的妹妹穿的比我還像新娘。我一直安慰自己够傍,他們只是感情好甫菠,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著冕屯,像睡著了一般寂诱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上安聘,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天痰洒,我揣著相機與錄音,去河邊找鬼浴韭。 笑死丘喻,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的念颈。 我是一名探鬼主播泉粉,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舍肠!你這毒婦竟也來了搀继?” 一聲冷哼從身側(cè)響起窘面,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叽躯,沒想到半個月后财边,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡点骑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年酣难,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黑滴。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡憨募,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出袁辈,到底是詐尸還是另有隱情菜谣,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布晚缩,位于F島的核電站尾膊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏荞彼。R本人自食惡果不足惜冈敛,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一穆律、第九天 我趴在偏房一處隱蔽的房頂上張望栋艳。 院中可真熱鬧,春花似錦怨愤、人聲如沸寞缝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽第租。三九已至措拇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間慎宾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工浅悉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留趟据,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓术健,卻偏偏與公主長得像汹碱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子荞估,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容