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

通觀整體后會(huì)發(fā)現(xiàn),g2m的面向?qū)ο笤O(shè)計(jì)做的很好.雖然最終只能輸出png格式的圖片,但實(shí)現(xiàn)了圖片的基類和JPG的圖片類,只能導(dǎo)出為Mbtiles格式卻也能通過(guò)文件存儲(chǔ)基類可以實(shí)現(xiàn)gdal2folder的功能.詳盡的文檔與充足的單元測(cè)試也說(shuō)明了這是個(gè)成熟的用心的工具.
main.py
整個(gè)程序通過(guò)setup.py安裝后,注冊(cè)成為命令行工具.最終入口就是main.py,主要負(fù)責(zé)構(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",那么可以通過(guò)args.a訪問(wèn)該參數(shù)
default:設(shè)置參數(shù)的默認(rèn)值
action:參數(shù)出發(fā)的動(dòng)作
store:保存參數(shù),默認(rèn)
store_const:保存一個(gè)被定義為參數(shù)規(guī)格一部分的值(常量),而不是一個(gè)來(lái)自參數(shù)解析而來(lái)的值。
store_ture/store_false:保存相應(yīng)的布爾值
append:將值保存在一個(gè)列表中。
append_const:將一個(gè)定義在參數(shù)規(guī)格中的值(常量)保存在一個(gè)列表中。
count:參數(shù)出現(xiàn)的次數(shù)
parser.add_argument("-v", "--verbosity", action="count", default=0, help="increase output verbosity")
version:打印程序版本信息
type:把從命令行輸入的結(jié)果轉(zhuǎn)成設(shè)置的類型
choice:允許的參數(shù)值
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2], help="increase output verbosity")
help:參數(shù)命令的介紹
利用處理后的參數(shù),就可以正式驅(qū)動(dòng)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)建臨時(shí)文件
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,
)
# 通過(guò)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}
# 開(kāi)始切割
warp_Mbtiles(inputfile=inputfile.name, outputfile=outputfile.name,
# MBTiles
metadata=metadata,
# GDAL相關(guān)參數(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
對(duì)于輸入/輸出路徑,這里做了特殊的預(yù)處理.其值默認(rèn)為系統(tǒng)輸入/輸出,如果未指定該值,則建立臨時(shí)文件.
@contextmanager
def input_output(inputfile, outputfile):
tempfiles = []
infile = inputfile
if inputfile == sys.stdin:
# 建立臨時(shí)文件
infile = NamedTemporaryFile()
# 將數(shù)據(jù)從輸入流復(fù)制到該文件
copyfileobj(inputfile, infile)
# 游標(biāo)歸0
infile.seek(0)
tempfiles.append(infile)
outfile = outputfile
if outputfile == sys.stdout:
outfile = NamedTemporaryFile()
tempfiles.append(outfile)
try:
yield infile, outfile
# 最終從臨時(shí)文件輸出到輸出流
if outputfile == sys.stdout:
copyfileobj(open(outfile.name, 'rb'), outputfile)
finally:
for f in tempfiles:
f.close()
這里使用了contextmanager裝飾器,將函數(shù)包裝為一個(gè)支持with調(diào)用,結(jié)束后自動(dòng)釋放的對(duì)象.
通過(guò)給一個(gè)try…finally…結(jié)構(gòu)的函數(shù)頭部加上@contextmanager就可以通過(guò)with…as…結(jié)構(gòu)來(lái)調(diào)用它了,這樣try塊中yield的數(shù)據(jù)被as出來(lái),finally塊中的數(shù)據(jù)在with..as..塊結(jié)束的時(shí)候被執(zhí)行。
這里默認(rèn)的輸入輸出是系統(tǒng)的輸入輸出流.這看起來(lái)是很奇怪的,既然要用g2m處理柵格地圖,輸入的文件也應(yīng)該是個(gè)圖像文件.
其實(shí)這種實(shí)現(xiàn)可以使得g2m不單單作為一個(gè)閉環(huán)的工具,而作為一個(gè)由linux管道構(gòu)成的工具鏈的一部分.在管道中,數(shù)據(jù)流從A產(chǎn)出,經(jīng)由系統(tǒng)輸出\輸入進(jìn)入g2m,處理過(guò)后再經(jīng)過(guò)系統(tǒng)輸出進(jìn)入管道輸出給C.
在看處于最核心的模塊helper之前,需要看一下,helper所調(diào)用的都是哪些模塊.
storages.py/Mbtiles.py
存儲(chǔ)的實(shí)現(xiàn).
主要實(shí)現(xiàn)了三種存儲(chǔ):
- 單一文件夾內(nèi)存儲(chǔ)
- 文件夾分級(jí)存儲(chǔ)
- Mbtiles存儲(chǔ)
瓦片存儲(chǔ)的功能由存儲(chǔ)的基類定義:
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):
# 獲取哈希值,對(duì)于相同的瓦片,不進(jìn)行重復(fù)存儲(chǔ).因?yàn)閷?shí)際在小比例尺下,圖片重復(fù)的概率會(huì)很大.
# 問(wèn)題是,所有的哈希值都存于內(nèi)存,當(dāng)所切級(jí)數(shù)變大時(shí),內(nèi)存占用會(huì)很大,索引效率也會(huì)降低.
return self.hasher(image.write_to_memory())
def filepath(self, x, y, z, hashed):
# 文件路徑,僅對(duì)文件型存儲(chǔ)起作用
raise NotImplementedError()
def post_import(self, pyramid):
# 生成金字塔后執(zhí)行,僅對(duì)Mbtiles這種需要元數(shù)據(jù)描述的存儲(chǔ)起作用
pass
def save(self, x, y, z, image):
# 最重要的函數(shù),保存瓦片,子類必須要實(shí)現(xiàn)
raise NotImplementedError()
def save_border(self, x, y, z):
# 默認(rèn)的保存邊框
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
因?yàn)間2m最終暴露的只有存儲(chǔ)于Mbtiles中,那就來(lái)看一下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
# 不使用工廠模式,也會(huì)創(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)
# 如果有重復(fù)的瓦片,就直接寫(xiě)入哈希值,而不是存儲(chǔ)瓦片
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):
# 同瓦片一樣,透明的邊框也不重復(fù)渲染存儲(chǔ)
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)
我們能看到,在存儲(chǔ)瓦片到Mbtiles時(shí),有兩種方法:
- 存儲(chǔ)x,y,z+圖像+圖像的哈希值
- 存儲(chǔ)x,y,z+圖像的哈希值
Mbtiles設(shè)計(jì)上的特性就是不存儲(chǔ)重復(fù)的瓦片.因?yàn)樗举|(zhì)上是Sqlite數(shù)據(jù)庫(kù),里面存儲(chǔ)有行列號(hào)索引表和瓦片圖像索引表,
對(duì)于相同的瓦片,只要通過(guò)相同的瓦片索引,就能關(guān)聯(lián)起來(lái),可以節(jié)省大量的重復(fù)瓦片空間占用.