立体地形図を手作りしてみようかな、っと(10)

(7.1)で気付いた通りなんだけど、だんだん取得データ増やしてきたら、欠落データが思ったよりずっと多いのね。

(9)で載せようか迷ってたのは、(9)よりも遥かに広域の「関東平野ほぼ全域」。実際はこの時点で試してたのね。zoom(1/25)で。でもこんななの:

ダウンロードしていないから、という部分は若干あるけれど、「滑らかな欠落部分」があることから、本当に「データ=「データなし」」がこのように分布しているのだということがわかると思う。富士山はやったはずなのに気付かなかったな。なんで山梨が集中的に欠落してるんだろうか。

で、(7.1)で「DEM10ならある、とかないかなぁ?」を観察してみた。

房総半島の「5239-67」を覗いてみたら、あぁ、欠落はなさそうだ。DEM5 の欠落を補うのに DEM10 を使うことは出来そう。更新日付は DEM5 が 2013 年なのに対し DEM10 は 2009 年みたいで、このズレが出る場合もあるかもしれないけど、欠落よりはいいだろう。

まずデータ変換と格納場所をどうしようか。DEM5 だけ扱ってたぶんには「一次メッシュ番号/二次メッシュ番号/三次メッシュ番号.data」で良かった。DEM10 の場合三次メッシュでのファイル分割はなくて、二次メッシュに対応したファイル一個に 1125×750 が含まれている(DEM5のセル一個の5×5倍)。

変換時に頑張るか、ロード時に頑張るか。前者にしたい。つまり DEM10 を三次メッシュレベルに分解して格納してしまいたい。ファイルサイズは増えるけれど、DEM10 は「欠落が問題になる場所でのみ」ダウンロードして使うことしか考えてないので、大問題ではないから。

ファイル名規則とフォルダをどうすっかね? 拡張子を変えて同じ場所に置くか、フォルダで分けてしまうかどっちかなんだけど。フォルダ分けた方が扱いやすいかね? そうしようか。「DEM05/」と「DEM10/」というふうにトップレベルで分けてしまおう。これまで変換済みの DEM5 データの移動もこれなら楽だしね。

てわけで、変換処理(converter.py)を一気に改造:

converter.py (改)
  1 # -*- coding: utf-8 -*-
  2 import re
  3 import os
  4 try:
  5     # for CPython 2.7
  6     import cPickle as pickle
  7 except ImportError:
  8     # Python 3.x or not CPython
  9     import pickle
 10 from os import path
 11 from distutils.dir_util import create_tree
 12 import numpy as np
 13 
 14 
 15 def _dump_data(
 16     st, _raw,
 17     mfirst, msecond, mthird=None):
 18 
 19     def _pickle_dump(data, dest_dir, dest_filebase):
 20         if all(np.isnan(data["elevations"])):
 21             return
 22         pickle.dump(
 23             data,
 24             open(
 25                 path.join(
 26                     dest_dir, "{}.data".format(dest_filebase)), "wb"),
 27             protocol=-1)
 28 
 29     if mthird:  # DEM5
 30         dest_dir_top = "__converted/DEM05"
 31     else:
 32         dest_dir_top = "__converted/DEM10"
 33     dest_dir = path.join(dest_dir_top, mfirst, msecond)
 34     create_tree(dest_dir, ["dummy"])
 35     if mthird:  # DEM5
 36         data = {"startPoint": st, "elevations": _raw}
 37         _pickle_dump(data, dest_dir, mthird)
 38     else:  # DEM10
 39         shape = (750, 1125)
 40         tmp = np.full((shape[1] * shape[0], ), np.nan)
 41         tmp[st:len(_raw) + st] = _raw
 42         tmp = np.flipud(tmp.reshape((shape[0], shape[1])))
 43         dy = shape[0] / 5
 44         dx = shape[1] / 5
 45         for y in range(5):
 46             for x in range(5):
 47                 data22 = tmp[
 48                     y * dy:(y + 1) * dy,
 49                     x * dx:(x + 1) * dx]
 50                 # zoom 2x2 with kronecker product
 51                 data = np.kron(data22, np.ones((2, 2)))
 52                 # split into DEM5 level
 53                 dy0 = data.shape[0] / 2
 54                 dx0 = data.shape[1] / 2
 55                 for y0 in range(2):
 56                     for x0 in range(2):
 57                         leaf = data[
 58                             y0 * dy0:(y0 + 1) * dy0,
 59                             x0 * dx0:(x0 + 1) * dx0]
 60                         leaf = np.flipud(leaf).reshape(
 61                             leaf.shape[0] * leaf.shape[1])
 62                         idx = np.where(np.isnan(leaf) == False)[0]
 63                         if len(idx) == 0:
 64                             continue
 65                         dest_filebase = "{}{}".format(
 66                             y * 2 + y0,
 67                             x * 2 + x0)
 68                         _pickle_dump(
 69                             {
 70                                 "startPoint":
 71                                     int(idx[0]),
 72                                 "elevations":
 73                                     leaf[idx[0]:idx[-1] + 1].tolist()
 74                                 },
 75                             dest_dir,
 76                             dest_filebase)
 77 
 78 
 79 _STRGX = re.compile(
 80     r"<gml:startPoint> *(.*) (.*)</gml:startPoint>")
 81 _FNRGX1 = re.compile(
 82     r"FG-GML-(.*)-(.*)-(.*)-DEM5[AB]-.*.xml", flags=re.I)
 83 _FNRGX2 = re.compile(
 84     r"FG-GML-(.*)-(.*)-DEM10[AB]-.*.xml", flags=re.I)
 85 
 86 
 87 def _convert_xml(srcgml):
 88     _st = (0, 0)
 89     _elevs_raw = []
 90 
 91     with open(srcgml, "r") as fi:
 92         # find start of grid data
 93         while True:
 94             line = fi.readline()
 95             if "gml:tupleList" in line:
 96                 break
 97 
 98         # read grid data
 99         while True:
100             line = fi.readline().strip()
101             if not line:
102                 continue
103             if "gml:tupleList" in line:
104                 break
105             ty, d = line.split(",")
106             d = float(d)
107             # we need to treat sea level always NaN rather than zero.
108             if ty.decode('cp932') in (u'海水面', u'データなし'):
109                 d = float('NaN')
110             # invalid data are stored with '-9999.0'
111             elif d == -9999.0:
112                 d = float('NaN')
113             _elevs_raw.append(d)
114 
115         # read start-point
116         while True:
117             line = fi.readline()
118             m = _STRGX.search(line)
119             if m:
120                 _st = (
121                     int(m.group(1)), int(m.group(2)))
122                 break
123 
124     # pickle data
125     m = _FNRGX1.match(path.basename(srcgml))
126     if m:  # DEM5
127         shape = (150, 225)
128         st = _st[1] * shape[1] + _st[0]
129         _dump_data(
130             st, _elevs_raw, m.group(1), m.group(2), m.group(3))
131     else:  # DEM10
132         shape = (750, 1125)
133         st = _st[1] * shape[1] + _st[0]
134         m = _FNRGX2.match(path.basename(srcgml))
135         _dump_data(st, _elevs_raw, m.group(1), m.group(2))
136 
137 #
138 for root, dirs, files in os.walk("__extracted"):
139     for fn in files:
140         if ".xml" in fn:
141             srcgml = path.join(root, fn)
142             _convert_xml(srcgml)
143             print(srcgml)

本質とは関係ない部分での改造としては、lowerCorner、upperCorner、low、high の読み込みをもはややめてる。なんかヘンな空白入ってて処理に失敗したのが理由だけど、絶対使わないからさ、この情報。それと、全域が「海水面,-9999.」の場合はファイル自体存在しないものと思っていたら、DEM5B の中にそんなのがいた。無駄なのでこれは捨てることに。

ハイライトした部分が本題で…、頭使った。知恵熱が出そうだ。最初1次元のままやろうとしたけど、無理があったので、ndarray 任せの方法で。ちょっと面白がって欲しいのはクロネッカー積(np.kron)してる部分ね。ここを ndimage.zoom しても悪くはないんだけれど、生データの解像度以上に滑らかなのはオカシイことなので、np.kron での「拡大」をした。

ゴールは「DEM5 にないものを DEM10 から補う」ではあるけれど、先に DEM10 だけで単独で正しくないといけないので確認せねば。単独範囲では形状がわからず、正解かどうかわからなかったので、「5239-66」、「5239-76」、「5239-77」とともに:

オッケーだね。まごうことなき富津岬る。

では、「DEM5 にないものを DEM10 から補う」を。converted_loader.py はこうした:

converted_loader.py (改)
  1 # -*- coding: utf-8 -*-
  2 import os
  3 try:
  4     # for CPython 2.7
  5     import cPickle as pickle
  6 except ImportError:
  7     # Python 3.x or not CPython
  8     import pickle
  9 import numpy as np
 10 
 11 
 12 def _calc_pos0(lon_or_lat, lseg):
 13     a = (lon_or_lat % 100) / lseg
 14     b = (a - int(a)) / (1 / 8.)
 15     c = (b / (1 / 10.)) % 10
 16     return (
 17         int(a),
 18         int(b),
 19         int(c),
 20         round((int(a) + int(b) / 8. + int(c) / 8. / 10.) * lseg, 9),
 21         round((int(a) + int(b) / 8. + (int(c) + 1) / 8. / 10.) * lseg, 9))
 22 
 23 
 24 def _calc_from_latlon(lat, lon):
 25     _fromlat = _calc_pos0(lat, 2/3.)
 26     _fromlon = _calc_pos0(lon, 1.)
 27     return (
 28         "{}{}/{}{}/{}{}".format(
 29             _fromlat[0], _fromlon[0],
 30             _fromlat[1], _fromlon[1],
 31             _fromlat[2], _fromlon[2]),
 32         (_fromlat[3], _fromlon[3] + 100),
 33         (_fromlat[4], _fromlon[4] + 100))
 34 
 35 
 36 _shape = (150, 225)
 37 
 38 
 39 def _load0(fn):
 40     with open(fn, "rb") as fi:
 41         _d = pickle.load(fi)
 42         _st = _d["startPoint"]
 43         _raw = _d["elevations"]
 44         elevs = np.full((_shape[1] * _shape[0], ), np.nan)
 45         elevs[_st:len(_raw) + _st] = _raw
 46         elevs = elevs.reshape((_shape[0], _shape[1]))
 47         return np.flipud(elevs)
 48 
 49 
 50 def load(lat, lon, convert_fun=None, use_dem10=True):
 51     base, lc, uc = _calc_from_latlon(lat, lon)
 52 
 53     dem05fn = "__converted/DEM05/" + base + ".data"
 54     dem10fn = "__converted/DEM10/" + base + ".data"
 55 
 56     elevs1, elevs2 = None, None
 57 
 58     if use_dem10 and os.path.exists(dem10fn):
 59         elevs1 = _load0(dem10fn)
 60 
 61     if os.path.exists(dem05fn):
 62         elevs2 = _load0(dem05fn)
 63 
 64     if elevs1 is None:
 65         if elevs2 is None:
 66             elevs = np.full((_shape[0], _shape[1]), np.nan)
 67             print("data was not found: {}".format(base))
 68         else:
 69             elevs = elevs2
 70             print("DEM5:               {}".format(base))
 71     else:
 72         elevs = elevs1
 73         if elevs2 is not None:
 74             notnan = (np.isnan(elevs2) == False)
 75             elevs[notnan] = elevs2[notnan]
 76             print("DEM5 + DEM10:       {}".format(base))
 77         else:
 78             print("DEM10:              {}".format(base))
 79 
 80     if convert_fun:
 81         elevs = convert_fun(elevs)
 82 
 83     return elevs, lc, uc
 84 
 85 
 86 def _load_range0(lc_lat, lc_lon, uc_lat, uc_lon, convert_fun=None, use_dem10=True):
 87     #
 88     def _stfun(fun, left, right):
 89         if left is None:
 90             return right
 91         return fun((left, right))
 92     #
 93     ydis = (2. / 3 / 8 / 10)
 94     xdis = (1. / 8 / 10)
 95 
 96     # adjust startpoint to center of mesh
 97     # (for special boundary case.)
 98     lc_lat = int(lc_lat / ydis) * ydis + ydis / 2.
 99     lc_lon = int(lc_lon / xdis) * xdis + xdis / 2.
100 
101     ycells = int(np.ceil(uc_lat / ydis) - np.floor(lc_lat / ydis))
102     xcells = int(np.ceil(uc_lon / xdis) - np.floor(lc_lon / xdis))
103     result, lc, uc = None, None, None
104     #
105     for i in range(ycells):
106         tmp = None
107         for j in range(xcells):
108             e, lc_tmp, uc_tmp = load(
109                 lc_lat + ydis * i,
110                 lc_lon + xdis * j,
111                 convert_fun, use_dem10)
112             #
113             if not lc:
114                 lc = lc_tmp
115             uc = uc_tmp
116             #
117             tmp = _stfun(np.hstack, tmp, e)
118         #
119         result = _stfun(np.vstack, result, tmp)
120     #
121     return result, lc, uc
122 
123 
124 def load_range(lat1, lon1, lat2, lon2, convert_fun=None, use_dem10=True):
125     lc_lat = min(lat1, lat2)
126     lc_lon = min(lon1, lon2)
127     uc_lat = max(lat1, lat2)
128     uc_lon = max(lon1, lon2)
129 
130     elevs, lc, uc = _load_range0(
131         lc_lat, lc_lon, uc_lat, uc_lon,
132         convert_fun, use_dem10)
133     X = np.linspace(lc[1], uc[1], elevs.shape[1])
134     Y = np.linspace(lc[0], uc[0], elevs.shape[0])
135     xmin, xmax = np.where(X <= lc_lon)[0][-1], np.where(X >= uc_lon)[0][0]
136     ymin, ymax = np.where(Y <= lc_lat)[0][-1], np.where(Y >= uc_lat)[0][0]
137     return X[xmin:xmax], Y[ymin:ymax], elevs[ymin:ymax, xmin:xmax]

ハイライトした「notnan」部分は、「DEM10 をベースにし、DEM5 データの非NaN部分を取り込む」としていて、まぁこれがキモ。なんだけど、池などの「内水面」が DEM10 だと標高データ持ってるみたいで、ちょっと識別しにくくなるのが難点だな…。

なお、ほかのハイライト箇所2箇所、adjust startpoint to center of meshとある部分と np.where 部分はともに、最初から間違っていた境界処理の修正。これはこれまで書いてきた converted_loader.py 引用してる投稿全部に追記入れといた。(ぴったり境界位置だった場合に困ったことになっていた。)

以下結果。

上述房総半島について「DEM5のみ、DEM10と組み合わせ」をアニメーションGIFにしたもの:

mergeDEM5DEM10-anim

おっけーだ。おっけーなんだけど、最大に拡大してじっくり見ると、解像度が変わる境界が実は線になって見えちゃったりすんのよね。まぁいいか…。

箱根・富士山(範囲「35.076203, 139.160385」~「35.539109, 138.540344」、zoom(1/3))は PDF でどーぞ:

Unable to display PDF
Click here to download

Unable to display PDF
Click here to download

解像度の変わり目が気になるかどうか、自分の目で確かめてみて。

にしても、「富士山だけ見たい」ってのがもう、かなりの広域扱わないといけないのね。山頂付近だけてならともかく、裾野からの全体をみたければ。上の絵にした領域だと、1/3ズームでも shape が (2777, 3720) になる。処理時間は3分30秒とかそんな。縮小しないと shape は (8331, 11160)。これはかなりキツいサイズ。(ちなみに最初の「関東平野全域」は、1/25 で 50分くらいかかってる。)