IU Tips

Plateauデータを使って市街モデルを作る【Plateau】【Python】

2024.11.12

こんにちは。
IU BIM STUDIO原田です。
今回は国土交通省のPlateauのデータを使って簡易な市街モデルを作成したいと思います。

Plateauとは

Plateau(プラトー)とは国土交通省が行っている3D都市モデルのオープンデータ推進プロジェクトです。防災、デジタルツインなどに使用するためのデータ基盤となっています。
https://www.mlit.go.jp/plateau

今回の内容

Plateauのデータを建築の業務に活用したいというケースはあると思います。ただ、UnityやUnreal EngineなどのゲームエンジンではSDK(ソフトウェア開発キット)が出ているのですが、RevitやArchicad、Rhinocerosなど建築系のソフトで直接使えるようにはなっていません。

そこで、Plateauのデータを建築系ソフトで使う方法を試してみました。

今回はPlateauのデータから必要なものを取得してJSONに書き出し、簡易な市街モデルを作成したいと思います。今回はPlateauデータの処理からJSONの書き出しまでです。

データの取得

まずは元になる市街データを取得します。
PlateauのWebサイトにアクセスし、右上のメニューからOpen Dataをクリックします。

次に、Plateauオープンデータポータルサイトへのリンクをクリックします。

すると、G空間情報センターというサイトに移動します。

ページをスクロールしていくと各自治体のデータが表示されています。

今回は大阪市2022を使用します。

リンクをクリックした先のページをまた下にスクロールしていくと、データへのリンクが表示されています。

今回はCityGML(V3)を使用しますので、そこのボタンからダウンロードを選択します。
容量が約1.4GBありますのでご注意ください。

これでデータの準備はできました。

PlateauUtils

PythonでPlateauのデータを扱いやすくするためにPlateau Utilsというライブラリがあります。今回はこちらを使ってデータを処理していきます。

Plateau Utilsについて詳しいことは下記をご確認ください。
https://github.com/Project-PLATEAU/PlateauUtils

Plateau UtilsではGoogle Colabolatoryを使用しているとのことですが、今回はWindowsローカル環境を使用します。

チュートリアルは下記リンクにあります。
https://project-plateau.github.io/PlateauUtils

ライブラリのインストール

Plateau Utilsをインストールします。

pip install git+https://github.com/eukarya-inc/plateauutils/

データのパース

それではPlateau Utilsを使って市街モデルを読み込んでいきます。

from shapely import from_wkt
from plateauutils.parser.city_gml_parser import CityGMLParser
import pyproj
import json

target_polygon = from_wkt(
    "POLYGON ((135.497542 34.686394,135.500916 34.686394,135.500916 34.682314,135.497542 34.682314,135.497542 34.686394))"
)
parser = CityGMLParser(target_polygon)
path = "27100_osaka-shi_city_2022_citygml_3_op.zip"
result = parser.parse(path)

まずは、市街モデル全体の内から取り出したい範囲を指定します。

Shapelyのfrom_wktという関数で指定範囲のポリゴンを作成します。
wktって何?って思ったらwell-known textとのことでした。よくある書き方っていう意味かなと思ったらそのような書式があるそうです。
https://ja.wikipedia.org/wiki/Well-known_text

次に、CityGMLParserオブジェクトを作成し、ファイルパスを指定してパースします。
公式のチュートリアルではdownload_and_parseメソッドを使っているのですが、ローカル環境だと実行する度にダウンロードしてしまうのでparseメソッドを使います。

エラー発生

試してみるとエラーが発生して処理が完了できませんでした。

原因を探ってみると、parseメソッド内でパスを取り扱う際にバックスラッシュの読み込みでエラーが起きているためのようでした。

おそらくGoogle Colabとローカルの環境の違いによるものかと思います。

元のコードは下記の通りでした。

with zipfile.ZipFile(target_path) as zip_file:
    for name in zip_file.namelist():
        for target in self.targets:
            path = os.path.join("udx", "bldg", target + "_bldg")
            if name.find(path) >= 0 and name.endswith(".gml"):
                hit_targets.append(name)

Pathlibを使えばより良いかもしれませんが、今回はバックスラッシュをスラッシュに変更する処理を入れるのみとします。

with zipfile.ZipFile(target_path) as zip_file:
    for name in zip_file.namelist():
        for target in self.targets:
            path = os.path.join("udx", "bldg", target + "_bldg")
            path = path.replace("\\","/")  # バックスラッシュを修正
            if name.find(path) >= 0 and name.endswith(".gml"):
                hit_targets.append(name)

このような所が数か所ありました。

建物データの処理

チュートリアルを見るとPlateau Utilsでは下記の属性を抽出するとあります。

gmlidid
center中心座標
min_height最小高さ
measured_height測定高さ
building_structure_type建物構造種別(コード)
usage建物用途(コード)

今回は市街モデルを作成したいので底面の座標と建物の高さが必要です。建物の高さはデフォルトのmeasured_heightで良いでしょう。地形の高低差が大きい場所では最小高さも合わせて取得したほうが良いかもしれませんが今回は省略します。

市街モデルを作るには建物の座標情報が欲しいですが、こちらには含まれていません。チュートリアルではカスタマイズする場合はCityGMLParserの_parserメソッドを書き換えるように記載があります。

なので、こちらに底面の情報を取得するコードを書き足していきます。

データの確認

処理を書く前にデータの確認をする必要があります。

cityGMLのデータを確認してみましょう。データは下記のようになっています。

<?xml version='1.0' encoding='UTF-8'?>
<core:CityModel xmlns:brid="http://www.opengis.net/citygml/bridge/2.0" xmlns:tran="http://www.opengis.net/citygml/transportation/2.0" xmlns:frn="http://www.opengis.net/citygml/cityfurniture/2.0" xmlns:wtr="http://www.opengis.net/citygml/waterbody/2.0" xmlns:sch="http://www.ascc.net/xml/schematron" xmlns:veg="http://www.opengis.net/citygml/vegetation/2.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:tun="http://www.opengis.net/citygml/tunnel/2.0" xmlns:tex="http://www.opengis.net/citygml/texturedsurface/2.0" xmlns:gml="http://www.opengis.net/gml" xmlns:app="http://www.opengis.net/citygml/appearance/2.0" xmlns:gen="http://www.opengis.net/citygml/generics/2.0" xmlns:dem="http://www.opengis.net/citygml/relief/2.0" xmlns:luse="http://www.opengis.net/citygml/landuse/2.0" xmlns:uro="https://www.geospatial.jp/iur/uro/3.0" xmlns:xAL="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0" xmlns:bldg="http://www.opengis.net/citygml/building/2.0" xmlns:smil20="http://www.w3.org/2001/SMIL20/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:smil20lang="http://www.w3.org/2001/SMIL20/Language" xmlns:pbase="http://www.opengis.net/citygml/profiles/base/2.0" xmlns:core="http://www.opengis.net/citygml/2.0" xmlns:grp="http://www.opengis.net/citygml/cityobjectgroup/2.0" xsi:schemaLocation="https://www.geospatial.jp/iur/uro/3.0 ../../schemas/iur/uro/3.0/urbanObject.xsd  http://www.opengis.net/citygml/2.0 http://schemas.opengis.net/citygml/2.0/cityGMLBase.xsd http://www.opengis.net/citygml/landuse/2.0 http://schemas.opengis.net/citygml/landuse/2.0/landUse.xsd  http://www.opengis.net/citygml/building/2.0 http://schemas.opengis.net/citygml/building/2.0/building.xsd  http://www.opengis.net/citygml/transportation/2.0 http://schemas.opengis.net/citygml/transportation/2.0/transportation.xsd  http://www.opengis.net/citygml/generics/2.0 http://schemas.opengis.net/citygml/generics/2.0/generics.xsd  http://www.opengis.net/citygml/relief/2.0 http://schemas.opengis.net/citygml/relief/2.0/relief.xsd  http://www.opengis.net/citygml/cityobjectgroup/2.0 http://schemas.opengis.net/citygml/cityobjectgroup/2.0/cityObjectGroup.xsd  http://www.opengis.net/gml http://schemas.opengis.net/gml/3.1.1/base/gml.xsd http://www.opengis.net/citygml/appearance/2.0 http://schemas.opengis.net/citygml/appearance/2.0/appearance.xsd">
    <gml:boundedBy>
        <gml:Envelope srsName="http://www.opengis.net/def/crs/EPSG/0/6697" srsDimension="3">
            <gml:lowerCorner>34.589634677118006 135.49873090106684 0</gml:lowerCorner>
            <gml:upperCorner>34.59176021058151 135.50006488875817 18.509999999999998</gml:upperCorner>
        </gml:Envelope>
    </gml:boundedBy>
    <core:cityObjectMember>
        <bldg:Building gml:id="bldg_fdbf34c2-400a-4ce6-aa4c-898f17fa4d78">
            <gen:measureAttribute name="計測周辺長">
                <gen:value uom="m">25.314672973</gen:value>
            </gen:measureAttribute>
            <gen:intAttribute name="ユーザーID">
                <gen:value>100979</gen:value>
            </gen:intAttribute>
            <gen:intAttribute name="1/2500図郭">
                <gen:value>111</gen:value>
            </gen:intAttribute>
            <gen:stringAttribute name="区名">
                <gen:value>住吉区</gen:value>
            </gen:stringAttribute>
            <gen:stringAttribute name="町丁目名称">
                <gen:value>山之内5丁目</gen:value>
            </gen:stringAttribute>
            <gen:intAttribute name="街区番号">
                <gen:value>3</gen:value>
            </gen:intAttribute>
            <bldg:class codeSpace="../../codelists/Building_class.xml">3001</bldg:class>
            <bldg:usage codeSpace="../../codelists/Building_usage.xml">411</bldg:usage>
            <bldg:measuredHeight uom="m">7.0</bldg:measuredHeight>
            <bldg:storeysAboveGround>2</bldg:storeysAboveGround>
            <bldg:lod0FootPrint>
                <gml:MultiSurface>
                    <gml:surfaceMember>
                        <gml:Polygon>
                            <gml:exterior>
                                <gml:LinearRing>
                                    <gml:posList>34.589874882491166 135.49988043536536 0 34.58992363068757 135.5000226116492 0 34.58993713085785 135.5000170801959 0 34.58994621284853 135.50003337753944 0 34.59000018003319 135.5000030760373 0 34.589945799969456 135.49984241339286 0 34.589874882491166 135.49988043536536 0 </gml:posList>
                                </gml:LinearRing>
                            </gml:exterior>
                        </gml:Polygon>
                    </gml:surfaceMember>
                </gml:MultiSurface>
            </bldg:lod0FootPrint>

データの中に<bldg.:lod0Footprint>というものがあります。Plateauの説明を見てみるとLOD0は高さ情報のない平面形状とのことなので、これを建物高さ分押し出すことで簡易な市街モデルを作成します。

3D都市モデルの特徴と活用法 – LOD
https://www.mlit.go.jp/plateau/learning/tpc01-2/#p1_3_2

CityGMLParserのカスタマイズ

それでは_parseメソッドに処理を書き足します。

# lod0FootPrintのposlistを取得
foot_print = city_object_member.find(".//bldg:lod0FootPrint", ns)
poslist = foot_print.find(".//gml:posList",ns)
poslist_split = poslist.text.split(" ")
coords = list(map(float, poslist_split))
sorted_coords = [(coords[i],coords[i+1],coords[i+2])for i in range(0,len(coords),3)]

まず、LOD0FootPrintの子要素の中から緯度経度情報の入ったposListを取得します。
posListはスペース区切りになっているので、分割してリストにします。
リストにしたものを数値に変換し、緯度、経度、高度でセットになるようにまとめていきます。

ここでなぜかエラーが起きました。

ここで上記のcityGMLのデータを再度確認してもらうとわかるのですが、最後の地点の高度値が0の場合0の後に半角スペースが含まれてしまっているため、リストの最後に不要な要素が1つ追加されエラーが起きていました。

なので、空文字列を削除する処理を追加します。

# FEATURE:lod0FootPrintのposlistを取得
foot_print = city_object_member.find(".//bldg:lod0FootPrint", ns)
poslist = foot_print.find(".//gml:posList",ns)
poslist_split = poslist.text.split(" ")
filtered_poslist = list(filter(lambda x: x != "",poslist_split)) # 空文字列を削除
coords = list(map(float,filtered_poslist))
sorted_coords = [(coords[i],coords[i+1],coords[i+2])for i in range(0,len(coords),3)]

これで座標を取り出せました。こういう細かい処理をするときの変数の命名がの正解が分かりませんね…
座標を取り出せたら、返り値に追加します。

# 返り値に入る値を作成
return_value = {
    "gid": gid,
    "center": None,
    "min_height": 10000,
    "measured_height": measured_height,
    "building_structure_type": building_structure_type_text,
    "usage": usage_text,
    "footprint":sorted_coords
}


これを元にまたデータ処理を行います。

JSONで受け渡し

cityGMLをパースして受け取ったデータは一度JSONに書出し他のソフトに受け渡します。
その処理を書いていきましょう。

def create_transformer() -> pyproj.Transformer:
    src_proj = "EPSG:6697"
    dest_proj = "EPSG:6674"
    transformer = pyproj.Transformer.from_crs(src_proj, dest_proj, always_xy=True)
    return transformer

transformer = create_transformer()
bldg_data = []

for item in result:
    height = item["measured_height"]

    if height is None:
        continue

    coords = []
    for coord in item["footprint"]:
        x, y = transformer.transform(coord[1], coord[0])
        z = coord[2]
        coords.append((x, y, z))

    bldg_data.append({"coords": coords, "height": height})

with open("data.json", "w", encoding="utf-8") as f:
    json.dump(bldg_data, f)

まず、緯度経度を平面直角座標に変換するpyprojのトランスフォーマーを作成します。これは国土数値情報の回でやったのと同様です。

詳しくはこちらの記事をご覧ください。

Bldg_dataリストを作成し、パースの返り値を個別に処理していきます。

返り値から建物高さを取得し、フットプリントの座標情報を個別に処理していきます。
Plateauのデータはx,yは緯度経度、高さはメートルのため緯度経度は変換し、高さはそのままでGrasshopperで使える座標値に変換してリストに追加します。今回はLOD0のフットプリントのため高さは全て0となっています。

Grasshopperで必要なデータだけ取り出したらjsonファイルとして保存します。

書き出したJSONファイルは下記のような感じです。

[
    {
        "coords": [
            [
                -46869.83000000091,
                -146672.14000010333,
                0.0
            ],
            [
                -46868.329999997906,
                -146652.1400001045,
                0.0
            ],
            [
                -46880.07999999935,
                -146651.1400001031,
                0.0
            ],
            [
                -46881.779999999766,
                -146671.0600001041,
                0.0
            ],
            [
                -46869.83000000091,
                -146672.14000010333,
                0.0
            ]
        ],
        "height": 35.8
    },
    ......
] 

今回のまとめ

これでPlateauのデータから建物の座標と高さを取り出し、データ受け渡し用のJSONファイルを保存することができました。

長くなるので今回はここまでです。
次回はJSONファイルをGrasshopperで読み込み、市街モデルを作成するところまでやってみたいと思います。

最後までお読みいただきありがとうございました。
下記に、ライブラリのカスタマイズ部分以外のコードを載せておきます。

from shapely import from_wkt
from plateauutils.parser.city_gml_parser import CityGMLParser
import pyproj
import json


def create_transformer() -> pyproj.Transformer:
    src_proj = "EPSG:6697"
    dest_proj = "EPSG:6674"
    transformer = pyproj.Transformer.from_crs(src_proj, dest_proj, always_xy=True)
    return transformer


def main():
    target_polygon = from_wkt(
        "POLYGON ((135.497542 34.686394,135.500916 34.686394,135.500916 34.682314,135.497542 34.682314,135.497542 34.686394))"
    )

    parser = CityGMLParser(target_polygon)
    path = "27100_osaka-shi_city_2022_citygml_3_op.zip"
    result = parser.parse(path)

    transformer = create_transformer()

    bldg_data = []
    for item in result:
        height = item["measured_height"]
        if height is None:
            continue

        coords = []
        for coord in item["footprint"]:
            x, y = transformer.transform(coord[1], coord[0])
            z = coord[2]
            coords.append((x, y, z))

        bldg_data.append({"coords": coords, "height": height})

    with open("data.json", "w", encoding="utf-8") as f:
        json.dump(bldg_data, f)


if __name__ == "__main__":
    main()