8
8

More than 3 years have passed since last update.

バイナリベクトルタイルを Python で読んでみた

Last updated at Posted at 2020-04-04

国土地理院がベクトルタイル提供実験として、全国版のバイナリベクトルタイルの公開を行っています。

国土地理院のバイナリベクトルタイルの仕様(各ズームレベルごとに含まれているレイヤなど)については下記にまとめられています。

Mapbox Vector Tiles (MVT)

国土地理院ベクトルタイルで使われている Mapbox Vector Tiles 形式は Mapbox 社が策定した XYZ タイル型のベクトルデータ形式で、 Protocol Buffer 形式を利用した形式です。

Mapbox Vector Tiles の仕様は以下で確認できます。

すでにいろいろなツールで対応しており、 GDAL/OGR でもバージョン2.3以降は対応しています。そのため ogr2ogr を使ったファイル形式変換や QGIS 読み込みは普通にできます。

が、今回は vector_tile_base を使って Python を使ってパースを行ってみます。

vector_tile_base

Mapbox 社からでている、 Python 用のライブラリです。

既存の MVT ファイルをパースするには、上記の Decode のサンプルを参考にします。(というより、後述のソースは Decode サンプルのまんまです。)

データの取得

サンプルで使用するためのファイルを取得します。

$ wget -O 13_7283_3213.pbf https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/13/7283/3213.pbf

レイヤの確認

import vector_tile_base

with open(r'./13_7283_3213.pbf', 'rb') as f:
    data = f.read()
    vt = vector_tile_base.VectorTile(data)

for l in vt.layers:
    print(l.name)
label
building
waterarea
transp
symbol
road

ジオメトリの確認

import vector_tile_base

with open(r'./13_7283_3213.pbf', 'rb') as f:
    data = f.read()
    vt = vector_tile_base.VectorTile(data)

layer_map = {l.name : l for l in vt.layers}

if 'road' in layer_map:
    layer = layer_map['road']
    print('extent : %d' % layer.extent)

    for f in layer.features:
        print(f.get_geometry())
        break # 全地物を表示すると大変なので、ここではこれ以降の地物はスキップ
extent : 4096
[[[169, 45], [127, 43], [54, 34], [0, 11], [-36, 0], [-80, -8]]]

ここで得られた座標の値は前述のドキュメント(仕様)にあったとおり、整数値で格納されています。そして、タイルの幅は extent であり、座標の単位は 1/extent となります。たとえば extent が4096のタイルであれば、イメージとしては4096×4096ピクセルの画像を想像するとよいでしょう。

get_geometry() で得られる $\left[ 0, \ extent \right]$ の座標値を $extent$ で割ると、 $\left[ 0, \ 1 \right]$ に正規化されたタイル内座標を計算することができます。ただし、適切に線を引くためタイル外のノード情報を持つこともあります。

座標の補正

先程得られたジオメトリの座標は対象のタイルにおける座標でした。実際に GIS データとして使用するには不十分です。

タイル地図は、赤道半径 $a =$ 極半径 $b = 6378137$ [m] の真球メルカトル(疑似メルカトル) EPSG:3857 において、縦横 $S = 2 \pi a = 40075016.685$ [m] の正方形となるように切り取った範囲(東経180度〜西経180度、北緯85.01度〜南緯85.01度)を対象範囲とし、ズームレベルに従いタイルの分割を行っています。

ズームレベル z のときの、個々のタイルの幅は $\frac{2 \pi a}{2^z}$ [m] となります。ただし、単位はメートルですが、あくまでも EPSG:3857 の投影地図上の単位としてであり、実際の距離とは一致しないことに留意が必要です。

タイル座標が z/x/y であるタイルの EPSG:3857 における北西角の座標 $\left( x_\mathrm{offset}, \ y_\mathrm{offset} \right)$ は次式となります。

\begin{array}{lll}
a & \!\!\! = & \!\!\!\!\!\!\!\! 6378137 \\
S & \!\!\! = & \!\!\!\!\!\!\!\! 2 \pi a \  = \  40075016.685 \\
x_\mathrm{offset} & \!\!\! = & \!\!\!\!\! \left( x - 2^{z-1} \right) \times \displaystyle\frac{S}{2^z} \\
y_\mathrm{offset} & \!\!\! = -& \!\!\!\!\! \left( y - 2^{z-1} \right) \times \displaystyle\frac{S}{2^z}
\end{array}

$y_\mathrm{offset}$ の方にマイナスがついているのはタイル座標もタイル内座標も南方向が正なのに対し、 EPSG:3857 の南北軸は北方向が正だからです。

また get_geometry() で得られる正規化前のタイル内座標を $\left( s, t \right)$ とおくと、 EPSG:3857 における対象点の座標 $\left( x_{3857}, y_{3857} \right)$ は次式となります。

\begin{array}{lll}
x_{3857} & \!\!\! =  & \!\!\!\!\! \displaystyle{ \left( \frac{s}{extent} + \left( x - 2^{z-1} \right) \right) \frac{S}{2^z} } \\
y_{3857} & \!\!\! = -& \!\!\!\!\! \displaystyle{ \left( \frac{t}{extent} + \left( y - 2^{z-1} \right) \right) \frac{S}{2^z} }
\end{array}

これにより GIS で使える座標を計算することができました。あとは煮るなり焼くなり好きに料理しましょう。

とはいえ

GDAL/OGR が対応していますので、 QGIS でも普通に開くことができますし、下記のように ogr2ogr を使って他形式に変換することもできますので、わざわざ Python で読んだり、変換したりといったことはあまり必要なさそうです。

ogr2ogr を使う場合、こんな感じで変換します。

$ ogr2ogr -f GPKG 13_7283_3213.gpkg -nlt PROMOTE_TO_MULTI 13_7283_3213.pbf

ただし GDAL/OGR の仕様でファイル名が z/x/y.ext z-x-y.ext z_x_y.ext のいずれかでないとタイル座標が得られず、正しく変換することができません。なお拡張子 extmvt でも pbf でも可能です。

QGIS で読み込む際も同様で、ファイル名を変えないと正しく読み込むことができません。

座標の精度

ところで、座標値は整数値として格納され、 extent を使ってタイル内座標を計算しました。逆に言うと 1/extent 単位で離散化されていると言えます。

extent の値を大きくすることで間隔を小さくすることもできますが、いずれにしても、規定以上の精度を持つことができない仕様になっています。これによって座標値としての圧縮はもちろん、細かすぎるノードを削除しパスを簡略化するができるので、データ量の削減を行うことができるのでしょう。

しかし extent = 4096 の場合、 z = 10 のときの座標間隔は( EPSG:3857 の投影地図における)約9.6 m と結構大きな値となり、座標値としての誤差は無視できません。あくまで縮尺により適切なズームレベルのタイルが読み込まれることを前提とするものであり、細かいデータが不要な縮尺の場合は、データそのものがばっさり切り捨てられるデータ形式であると認識する必要がありそうです。

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8