はじめに
実験データやログ解析などをしていると,「この地点の標高っていくらなんだろう?」と気になることがあります・
特に,ある程度の数の座標について標高を調べたいとき,1点ずつ地理院地図のビューアーでクリックして確認するのはさすがに現実的ではありません.
そんなわけで今回,「指定した座標から標高を取得するツールをPythonで作ってしまおう」ということで、重い腰を上げることにしました.
ついでに,データの構造や可視化まで一通り試してみたので,備忘録がてら記事にまとめておこうと思います.
そもそも地理院のDEMって何?
DEM(Digital Elevation Model:数値標高モデル)は,地表の標高を一定間隔のグリッドで記録したデータです.
国土地理院が公開している「基盤地図情報 数値標高モデル」は,日本全国をカバーしていて,使いこなせば非常に強力な情報源になります.
このDEMにはいくつか種類があり,たとえば:
- DEM5A:5mメッシュで、航空レーザー測量によって得られた高精度なデータ
- DEM5B/C:航空写真などをもとに作成された少し精度の低いデータ
といった具合に分かれています.
データはZIPファイルで配布されており,中身はGML形式(XMLっぽい)で構造化されています.
ただしこのGMLファイル,そのままでは扱いにくいのが正直なところです.
そこで今回は,これらのデータをGeoTIFF形式に変換し,さらにPythonから簡単に標高を取得したり可視化したりできるようにしてみました.
QGIS等のGISソフトを使って処理する方法もありますが,DIYがモットーなので,Pythonでやってみます!!
DEMのダウンロード
まずは数値標高モデル(DEM)をダウロードしてみます.
上記のリンクから「数値標高モデル」にアクセスし,ダウンロードする範囲と,データの種類を選択するとダウンロードできます.

ダウンロードには,ユーザ登録とログインが必要です(無料).
GMLって何?なぜGeoTIFFに変換するの?
国土地理院から提供されている数値標高モデル(DEM)のデータは、ZIPファイルで配布されていて,中を覗くと .xml
拡張子のファイルがいくつか入っています。
これらは GML(Geography Markup Language)形式と呼ばれるXMLベースのフォーマットで,地理情報を表現するための国際標準仕様です.
たとえば、中身を開いてみるとこんな感じになっています:
<gml:tupleList>
地表面,232.30
地表面,233.10
地表面,235.20
...
</gml:tupleList>
標高の数値がずらっと並んでいるのですが,それぞれがどの緯度・経度に対応しているのか,どういう順番で格納されているのか(北から?南から?といった点は、すぐにはピンと来ません.実際,筆者も「どこがどこなの?」と頭を抱えたクチです.
なぜGeoTIFFに変換するのか?
そんなGMLデータを解析して,任意の座標における標高を求めるには,座標系や行列の構造を理解して,さらに補間処理まで行う必要があります.
…というのは結構大変なので,
まずはGeoTIFFという形式に変換しておくと,後の処理が格段に楽になります。
GeoTIFFとは?
GeoTIFF は,TIFF画像のフォーマットに地理情報(緯度経度・標高・座標系など)を埋め込んだものです.
普通の画像のように扱えるのに,GISツールやPythonライブラリ(例:rasterio)から地理情報付きでアクセスできる,とても便利な形式です.
詳しい仕様や形式の説明は以下のようなサイトも参考になります:
というわけで,まずは変換から始めてみます.
ここから先は,GMLを読み込んでGeoTIFF形式に変換し,それをもとに標高を取得する方法を紹介していきます.
GMLを読み込んでGeoTIFF形式に変換
GMLファイルってどうなってるの?
地理院が提供している標高データ(DEM5Aなど)は,GML(XML)形式で格納されています.
実際の中身をざっくり見てみましょう:
<gml:boundedBy>
<gml:Envelope srsName="fguuid:jgd2011.bl">
<gml:lowerCorner>34.666666667 136.125</gml:lowerCorner>
<gml:upperCorner>34.675 136.1375</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<gml:GridEnvelope>
<gml:low>0 0</gml:low>
<gml:high>224 149</gml:high>
</gml:GridEnvelope>
<gml:tupleList>
地表面,232.30
地表面,233.10
地表面,235.20
...
</gml:tupleList>
ここで重要なのは次の3点です:
<gml:lowerCorner>
〜<gml:upperCorner>
:このファイルがカバーする地理範囲(緯度・経度)<gml:low>
〜<gml:high>
:標高データの行・列数(グリッドサイズ)<gml:tupleList>
:実際の標高値(1次元で並んでる)
ファイル名をベタ打ちしてGMLファイル(1つ)をGeoTIFFに変換するプログラムは以下のようになりました.
8行目にダウンロードしたDEMファイル名を指定して実行すると,変換処理が行われてconverted_dem.tifというファイル名で出力されます.また,66,67行目で緯度,経度を指定しておくと,変換後のtifファイルから標高を読み取って表示します.
(ファイル名:convert_dem.py)
from lxml import etree
import numpy as np
import rasterio
from rasterio.transform import from_bounds
from pathlib import Path
# --- 入力と出力ファイルを指定 ---
xml_path = Path("FG-GML-XXXXXXXXXXXX.xml") # GML (XML) ファイルパス(ダウンロードしたファイル名)
output_tif = Path("converted_dem.tif") # 出力先の GeoTIFF
# --- XMLをパース ---
tree = etree.parse(str(xml_path))
root = tree.getroot()
ns = {"gml": "http://www.opengis.net/gml/3.2"}
# --- グリッドサイズを取得 ---
grid_env = root.find(".//gml:GridEnvelope", ns)
if grid_env is None:
raise ValueError("GridEnvelope が見つかりません")
low = grid_env.find("gml:low", ns).text.split()
high = grid_env.find("gml:high", ns).text.split()
cols = int(high[0]) - int(low[0]) + 1
rows = int(high[1]) - int(low[1]) + 1
# --- 空間範囲を取得 ---
envelope = root.find(".//gml:Envelope", ns)
lower_corner = list(map(float, envelope.find("gml:lowerCorner", ns).text.split()))
upper_corner = list(map(float, envelope.find("gml:upperCorner", ns).text.split()))
# GMLは lat lon の順なので、ここで逆にして代入
min_lat, min_lon = lower_corner
max_lat, max_lon = upper_corner
# --- 標高データを取得 ---
tuple_list_text = root.find(".//gml:tupleList", ns).text.strip()
elevations_flat = [float(line.split(",")[1]) for line in tuple_list_text.splitlines()]
elevations = np.array(elevations_flat).reshape((rows, cols))
# --- アフィン変換行列を計算 ---
transform = from_bounds(min_lon, min_lat, max_lon, max_lat, cols, rows)
# --- XMLパース処理の直後 ---
print(f"サイズ: {cols} × {rows}")
print(f"範囲: 緯度 {min_lat}〜{max_lat}, 経度 {min_lon}〜{max_lon}")
print(f"標高数: {len(elevations_flat)}")
# --- GeoTIFFとして保存 ---
with rasterio.open(
output_tif,
"w",
driver="GTiff",
height=rows,
width=cols,
count=1,
dtype=elevations.dtype,
crs="EPSG:6668", # JGD2011(日本の測地系)
transform=transform,
) as dst:
dst.write(elevations, 1)
print("GeoTIFF変換完了!→", output_tif)
# --- script for test ---
lat = xx.xxxx # ここに緯度経度(deg)を入れておくと読取りチェックできる
lon = xxx.xxxx
with rasterio.open("converted_dem5a.tif") as src:
col, row = src.index(lon, lat) # ← 順番注意
val = src.read(1)[row, col]
print(f"標高: {val:.2f} m")
コアになる部分は以下の通りです.
範囲とサイズの取得
<gml:low>0 0</gml:low>
<gml:high>224 149</gml:high>
の部分からグリッドサイズ(列数=225、行数=150)を計算します.+ 1 になるので注意です(20~23行目)
low = grid_env.find("gml:low", ns).text.split()
high = grid_env.find("gml:high", ns).text.split()
cols = int(high[0]) - int(low[0]) + 1
rows = int(high[1]) - int(low[1]) + 1
標高の並びと行列化
地理院のGMLは「北→南」の順にデータが並んでいるため,GeoTIFFとして正しく出力するには上下を反転しておく必要があります(34~37行目).
# --- 標高データを取得 ---
tuple_list_text = root.find(".//gml:tupleList", ns).text.strip()
elevations_flat = [float(line.split(",")[1]) for line in tuple_list_text.splitlines()]
elevations = np.array(elevations_flat).reshape((rows, cols))
from_boundsでtransform生成
GeoTIFF内部で「この画素はどの緯度経度に対応するか?」を決める大事な情報です(40行目).
transform = from_bounds(min_lon, min_lat, max_lon, max_lat, cols, rows)
GeoTIFFとして保存
ファイルに保存します(48~62行目).
標高データの読み出し
ここまでくると,あとは標準的なGeoTIFFとしてPythonやGISツールから簡単に読み込めるようになります.65行目以降は,デバッグ用のコードで,66,67行目で指定した地点(緯度・経度)の標高データを読み出し,画面に表示しています.
実際には,ある程度の範囲を対象にしようとすると,gmlファイルが多数必要で,これらを一気に変換するようにした方が現実的です.第2部:応用編に続きます.