Pythonで複数の数値標高データを一枚絵にする

<p>
  前書いた記事の続き？のようなもの<br>
  <a href="https://qiita.com/mi-miya/items/619c351b506e420d036f">
    『Pythonで国土地理院の標高データを画像化してみる』
  </a><br>
  この記事では、複数のxmlファイルをポポンと放り込んで、適切に並べて一枚絵にしたいと思います
</p>

<h2>準備</h2>

  今回は、任意の都府県のメッシュデータを、まるごと落とします。<br>
  国土地理院のダウンロードページから、任意のメッシュを選択し取得します。<br>
  https://fgd.gsi.go.jp/download/menu.php


<p>
  『基盤地図情報 数値標高モデル』の「ファイル選択へ」というボタンを押すと、ダウンロード画面へ飛びます。<br>
  今回は、大阪府のデータを使用することにします。<br>
  左側にある10mメッシュの10(地形図の等高線)にチェックを入れておいてください
</p>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165044/fa1cf5ce-ee73-acc5-1b68-7dfaf9643448.png" alt="基盤地図情報ダウンロードサービス">

<p>
  この記事に書いてあるコードでは、国土地理院で提供されているデータをそのままグレースケール化しています<br>
  つまり、10mメッシュのデータの場合、xmlファイル一つ毎に1125x750のグレースケール画像が生成され、それを一つ一つ並べて一枚の大きな画像を生成することになります。<br>
  大阪府の場合は、横は6x1125px,縦は10x750pxの馬鹿デカイ画像が生成されることになるので、選択する都府県が小さいに越したことはありません。<br>
  もし一枚絵の画素数を小さくしたい場合は、後述の箇所を適当に弄ってください。
</p>

<p>
  さて、落としてきたZIPファイルは一度解答すると、以下のようなデータ構造になっているかと思います。
</p>

<pre class="code">
解凍したディレクトリ
|-- FG-GML-0000-00-DEM10B.zip
|-- FG-GML-0000-00-DEM10B.zip
|-- etc...
</pre>

<p>
  それぞれのZIPファイルを更に解凍しても良いのですが、少し面倒くさいので今回はZIPファイルのまま扱います
</p>

<p>
  コード全体は、下記の感じ
</p>

```python
import os
import re
import sys
import glob
import zipfile
import numpy as np
from PIL import Image
import xml.etree.ElementTree as ET

WIDTH = 1125
HEIGHT = 750

# 準備
def set_value(dir, rate):
    global WIDTH
    global HEIGHT
    # ファイル名からメッシュをリスト化し並び替える
    meshList = []
    for f in glob.glob(os.path.join(dir, "*.zip")):
        base = os.path.basename(f)
        meshList.append(base[7:11] + base[12:14])
    meshList.sort()

    # 処理予定データ数
    denominator = len(meshList)
    # 処理済みデータ数
    numerator = 0

    # メッシュリストから最東西南北端のメッシュを調べる
    top = right = meshList[-1]
    bottom = left = meshList[0]
    for meshNumber in meshList:
        str(meshNumber)
        if top[0:2] < meshNumber[0:2] or (top[0:2] <= meshNumber[0:2] and top[4] <= meshNumber[4] and top[4] <= meshNumber[4]):
            top = meshNumber
        if bottom[0:2] > meshNumber[0:2] or (bottom[0:2] >= meshNumber[0:2] and bottom[4] >= meshNumber[4]):
            bottom = meshNumber
        if right[2:4] < meshNumber[2:4] or (right[2:4] <= meshNumber[2:4] and right[5] <= meshNumber[5]):
            right = meshNumber
        if left[2:4] > meshNumber[2:4] or (left[2:4] >= meshNumber[2:4] and left[5] >= meshNumber[5]):
            left = meshNumber
    # 最端のメッシュから、キャンバスサイズを出す
    vartical = (int(top[0:2])-int(bottom[0:2])) * 8 + int(top[4]) - int(bottom[4]) + 1
    horizon = (int(right[2:4])-int(left[2:4])) * 8 + int(right[5]) - int(left[5]) + 1
    maxArea = vartical * horizon
    point = top[0:2] + left[2:4]  + top[4] + left[5]
    # canvas生成
    canvas = Image.new('RGB', (int(WIDTH / rate) * horizon, int(HEIGHT / rate) * vartical), (0, 0, 0))

    # ファイル読み取りと画像化
    for zipfile in glob.glob(os.path.join(dir, "*.zip")):
        paste_image(zipfile, point, canvas, rate)
        numerator += 1
        print('%d / %d' % (numerator, denominator))
    canvas.save('dem.png', 'PNG', quality=100)


# 画像化メソッド
def paste_image(z, p, c, r):
    global WIDTH
    global HEIGHT
    point = p
    canvas = c
    rate = r
    with zipfile.ZipFile(z, "r") as zf:
        for info in zf.infolist():
            inner = info
        with zf.open(inner) as zfile:
            root = ET.fromstring(zfile.read())
            namespace = {
                'xml': 'http://fgd.gsi.go.jp/spec/2008/FGD_GMLSchema',
                'gml': 'http://www.opengis.net/gml/3.2'
            }
            dem = root.find('xml:DEM', namespace)
            # メッシュ番号
            mesh = dem.find('xml:mesh', namespace).text
            # セルの配列数(実際の値は1ずつ追加)
            high = dem.find('./xml:coverage/gml:gridDomain/gml:Grid/gml:limits/gml:GridEnvelope/gml:high', namespace).text.split(' ')
            highX = int(high[0]) + 1
            highY = int(high[1]) + 1

            # 画像サイズ設定(データ数 == ピクセル数)
            dataSize = highX * highY

            # 標高データの配列
            dem_text = dem.find('./xml:coverage/gml:rangeSet/gml:DataBlock/gml:tupleList', namespace).text
            data = re.findall(',(.*)\n', dem_text)
            dataNp = np.empty(dataSize)
            for i in range(len(data)):
                if(data[i] == "-9999.00"):
                    dataNp[i] = 0
                else:
                    dataNp[i] = float(data[i])

            # データの開始座標
            startPoint = dem.find('./xml:coverage/gml:coverageFunction/gml:GridFunction/gml:startPoint', namespace).text.split(' ')
            startPointX = int(startPoint[0])
            startPointY = int(startPoint[1])
            startPosition = startPointY * highX + startPointX

            # データ数が不足している場合(画像の上下に空白が有る場合)
            if(len(dataNp) < dataSize):
                add = []
                # 画像下部のデータが不足している場合
                if(startPosition == 0):
                    for i in range(dataSize - len(dataNp)):
                        add.append(0)
                    dataNp.extend(add)
                # 画像上部及び下部のデータが不足している場合
                else:
                    for i in range(startPosition):
                        add.append(0)
                    dataNp[0:0] = add
                    add = []
                    for i in range(dataSize - len(dataNp) - len(add)):
                        add.append(0)
                    dataNp.extend(add)

            # データの8bit整数化
            dataNp = (dataNp / 15).astype(np.uint8)  # 富士山最高所を255に収める場合、dataNpを15で割る
            data = dataNp.reshape(highY, highX)

            # 数値標高データを画像化
            pilImg = Image.fromarray(np.uint8(data))
            pilImg = pilImg.resize((int(highX / rate), int(highY / rate)), Image.LANCZOS) # NEAREST

            # 貼り付けるメッシュの座標を計算
            targetX = (int(point[0:2]) - int(mesh[0:2])) * 8 + int(point[4]) - int(mesh[4])
            targetY = (int(mesh[2:4]) - int(point[2:4])) * 8 + int(mesh[5]) - int(point[5])

            canvas.paste(pilImg, (targetY * int(WIDTH / rate), targetX * int(HEIGHT / rate)))

# ZIPファイルの入ったディレクトリを入力
DIR = sys.argv[1]
RATE = int(sys.argv[2])

set_value(DIR, RATE)
```

<p>
  pythonの実行時に、ZIPファイルの詰まったディレクトリを指定すれば動くはずです
</p>

<h2>補足</h2>

<p>
  set_valueメソッドで、canvasを生成する箇所
</p>

<pre class="code python">
canvas = Image.new('RGB', (1125 * horizon, 750 * vartical), (0, 0, 0))
</pre>

<p>
  ココの1125, 750はそれぞれ数値標高データのセル数を手動で書いてあるだけです<br>
  コード上に出てくるhighX, highYと値自体は同じなので、二度手間だしhighX,Yは毎画像生成の度に値を探してたりもう色々クソなコードになっています。<br>
  (また今度書き直します、多分)<br>
  誰かリファクタリングしてくれたら嬉しいなぁ、なんて思いつつ・・・<br>
  それと、
</p>

<pre class="code python">
dataNp = (dataNp / 15).astype(np.uint8)
</pre>

<p>
  の15は何ぞ？の答えは、前回の記事に書いてますのでご参照願います。<br>
  https://qiita.com/artistan/items/99266407702d4152e9d5
</p>

<h2>デカイ画像はリサイズしよう</h2>

<p>
  最初の方でも書きましたが、このままのコードではデッカイ画像が生成されて不便です。<br>
  なので、set_valueメソッドの下記箇所を
</p>

<pre class="code python">
canvas = Image.new('RGB', (1125 * horizon, 750 * vartical), (0, 0, 0))
</pre>
<p>
  から
</p>
<pre class="code python">
canvas = Image.new('RGB', (1125/10 * horizon, 750/10 * vartical), (0, 0, 0))
</pre>

<p>
  にして、コメントアウトしているpilImageをresizeする箇所を
</p>
<pre class="code python">
pilImg = pilImg.resize((int(highX)/ 10, int(highY) / 10), Image.LANCZOS) # NEAREST
</pre>

<p>
  とかにすれば、10分の1に縮小された画像が生成されて便利よねー、という感じ
</p>

<h2>最後に</h2>

<p>
  上記コードで生成し、適当に縮小した大阪府がこちらになります
</p>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165044/cf32853b-087c-3997-0a7d-e71f29217525.png" alt="dem">

<p>
  大阪はそんなに山の高さがないので面白くないですが、もっと起伏にとんだ場所なら中々見ごたえがあると思います<br>
  グレースケールだと山の形が葉脈みたいになっていて、なんだか新鮮ですね<br>
  分かりにくい説明しか出来ませんでしたが、読んでいただきありがとうございました
</p>