├── .gitmodules ├── LICENSE.txt ├── README.md ├── appviewer.py ├── blender ├── blendertest.py └── blendertest.sh ├── doc ├── Hakone_523960.png ├── Kawasaki_533925.png ├── Osaka_513573.png ├── Osaka_523503.png ├── Yokohama_533915.png ├── Yokosuka_523975.png ├── blender.png └── plateaupy.png ├── download_plateau.py ├── plateaupy ├── __init__.py ├── plbldg.py ├── plcodelists.py ├── pldem.py ├── plluse.py ├── plobj.py ├── ploptions.py ├── plparser.py ├── pltran.py ├── plutils.py └── plvisualizer.py ├── requirements.txt └── test ├── statistics.py └── testVerticesTransformer.py /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "plateaupy/thirdparty/earcutpython"] 2 | path = plateaupy/thirdparty/earcutpython 3 | url = https://github.com/joshuaskelly/earcut-python.git 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yukiyoshi Sasao, Rintaro Kuroda, Kenji Iida 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plateaupy 2 | ![plateaupy](doc/plateaupy.png) 3 | 4 | [PLATEAU(CityGML)](https://www.mlit.go.jp/plateau/)のPython版パーサおよびビューア用モジュールです。 5 | 3D表示は[Open3D](http://www.open3d.org/)または[Blender Python (bpy)](https://docs.blender.org/api/current/index.html)で行います。 6 | 7 | ## はじめに 8 | 本ソフトウェアは、[東京23区から新しい世界を創るアイデアソン/ハッカソン](https://asciistartup.connpass.com/event/198420/)で開発されたものです。 9 | 開発チーム: ***チーム名「影の功労者」,3名*** 10 | -> ハッカソンでグランプリをいただきました。このREADMEの最後に作品を紹介します。 11 | 本リポジトリにPLATEAU(CityGML)のデータは含みません。 12 | ハッカソン中は非公開データでしたが、[G空間情報センター](https://www.geospatial.jp/ckan/organization/toshi)で正式公開されました。 13 | この公開データでも動作するよう修正しました。データダウンロードスクリプトも用意しています。 14 | 公開データではFBXやOBJなどの3次元データも用意されましたが、本ソフトウェアはCityGMLのみから解析、3次元構築します。 15 | 16 | 機能一覧 17 | * bldg(建物)のLOD 0,1,2 のパース、表示、LOD2テクスチャ表示(遅い)、メタデータのパース 18 | * dem(地表)のパース、表示 19 | * tran(道路)のパース、表示 20 | * 一度パースしたデータをキャッシュしておき次回から高速読み込み 21 | * codelists定義のパース 22 | * Open3D TriangleMesh への変換 23 | * Blender Object への変換 24 | * .plyファイルへの出力 25 | 26 | 未対応 27 | * luse(建設予定値)のパース、表示 28 | * brid(橋)のパース、表示、テクスチャ表示 29 | * bldg(建物)のLOD3以上のパース、表示 30 | 31 | ## 動作環境 32 | Python3 (Python 3.6.4, Ubuntu18.04 で確認) 33 | 34 | ## インストール 35 | 取得 36 | >git clone --recursive https://github.com/AcculusSasao/plateaupy.git 37 | 38 | モジュールインストール 39 | 40 | >cd plateaupy 41 | >pip install -r requirements.txt 42 | 43 | [Open3D v0.11.2](https://github.com/intel-isl/Open3D/releases/tag/v0.11.2) を取得しインストールしてください。 44 | >pip install open3d-0.11.2-***.whl 45 | 46 | ## PLATEAU(CityGML)データ 47 | 48 | ### PLATEAU 公開データ 49 | [G空間情報センター](https://www.geospatial.jp/ckan/organization/toshi)で続々と公開されています。 50 | まずは[東京都23区](https://www.geospatial.jp/ckan/dataset/plateau-tokyo23ku)の[CityGML](https://www.geospatial.jp/ckan/dataset/plateau-tokyo23ku-citygml-2020)で確認します。データ構造が同じであれば他都市でも可能です。 51 | 52 | ### データダウンロードスクリプト download_plateau.py 53 | 以下で東京都23区PLATEAU-CityGMLデータをダウンロードし展開します。 54 | > python download_plateau.py plateau-tokyo23ku-citygml-2020 55 | 56 | 第1引数(データセット名)の文字列はヘルプで一覧を確認できます。文字列ではなくインデックス番号 0~ を指定することもできます。 57 | > python download_plateau.py -h 58 | 59 | デフォルトのダウンロード/展開先は「CityGML2020/」ですが、コマンド引数 --basedir で指定することもできます。 60 | 今後ダウンロードアドレスが変わるかもしれませんが、変更になった場合は download_plateau.py 内のアドレスを変更してください。 61 | 以下のようなディレクトリ構成となります。 62 | 63 | CityGML2020/ 64 |   | 65 |   plateau-tokyo23ku-citygml-2020/   ← appviewerの-pathsで指定するパス 66 |     | 67 |     archive/ 68 |       *.zip, *.7z, .. 69 |     codelists/ 70 |       *.xml, .. 71 |     metadata/ 72 |       *.xml, .. 73 |     specification/ 74 |       *.png, .. 75 |     udx/ 76 |       bldg/,brid/,dem/,frm/,luse/,tran/, .. 77 | 78 | ## ビューアアプリ appviewer の使い方 79 | 以降のコマンド実行で、上記PLATEAUデータの場所をコマンド引数 -paths で指定する必要があります。 80 | 例: -paths CityGML2020/plateau-tokyo23ku-citygml-2020 81 | または、appviewerはデフォルトでパス path_to_citygml を参照するため、リンクを作成することで -paths の指定が不要になります。 82 | > ln -s CityGML2020/plateau-tokyo23ku-citygml-2020 path_to_citygml 83 | 84 | 1. 区画番号(メッシュコード)一覧を表示します。 85 | >python appviewer.py -cmd locations 86 | 87 | 以下が表示されます。 88 | 89 | >locations: [533925, 533926, 533934, 533935, 533936, 533937, 533944, 533945, 533946, 533947, 533954, 533955, 533956, 533957] 90 | 91 | 92 | 2. 区画番号 533925 の、建物(bldg)・道路(tran)・地面(dem) を表示します。-locを指定しなければ全区画を対象としますが時間がかかります。 93 | 94 | >python appviewer.py -loc 533925 95 | 96 | 読み込みにしばらく時間がかかります。 97 | 成功するとOpen3Dの3D画面が起動し、マウス操作できます。ESCキーで終了します。 98 | 99 | 100 | 3. 一度読み込んだデータはキャッシュファイルに保存し、次回以降はコマンドオプション -c を使用することで高速に起動します。 101 | 102 | >python appviewer.py -loc 533925 -c 103 | 104 | 4. オプション -k で、gml種類 0:bldg, 1:dem, 2:luse, 3:tran 4:brid を指定できます。 105 | 106 | >python appviewer.py -loc 533925 -c -k 0 107 | 108 | 5. オプション -lod2texture でLOD2のテクスチャを表示します。ただし動作が非常に遅いため場所を限定したほうが良いです。 109 | 110 | >python appviewer.py -paths ../CityGML_02 -k 0 -loc 53392633 -lod2texture 111 | 112 | 6. オプション -plypath [ディレクトリ] で、[ディレクトリ]に .ply ファイルを保存します。 113 | >python appviewer.py -loc 533925 -c -plypath tmp 114 | 115 | 7. コマンドdumpmetaで、bldg内のメタデータを表示します。 116 | 117 | >python appviewer.py -loc 533925 -c -cmd dumpmeta 118 | 119 | 8. コマンドcodelistsで、codelists定義を表示します。 120 | 121 | >python appviewer.py -cmd codelists 122 | 123 | ## Blender-Python 124 | ![blender](doc/blender.png) 125 | ### Blender-Python インストール 126 | 127 | [blender/blendertest.sh](blender/blendertest.sh)を参考にしてください。 128 | Blender 2.91.2 で確認しています。2.8以降bpyの仕様が大きく変わったため、少なくとも2.8以降である必要があります。 129 | Blender-Python(bpy)はBlender内のPythonで実行されるため、このPythonに必要モジュールをインストールする必要があります。 130 | Blenderのインストールディレクトリを $BLENDER とすると、まずはpipと必要モジュールをインストールします。 131 | 132 | >BLENDER_PYTHON=$BLENDER/2.91/python/bin/python3.7m 133 | >$BLENDER_PYTHON -m ensurepip 134 | >BLENDER_PIP=$BLENDER/2.91/python/bin/pip3 135 | >$BLENDER_PIP install --upgrade pip 136 | >$BLENDER_PIP install lxml open3d opencv-python 137 | 138 | ### Blender-Python 実行 139 | 140 | >$BLENDER/blender --python [blender/blendertest.py](blender/blendertest.py) --python-use-system-env 141 | 142 | 内容は[blender/blendertest.py](blender/blendertest.py)を参考にしてください。 143 | args を必要に応じて修正します。 144 | またBlender表示時にオブジェクト座標をCityGMLのXYZ[meter]そのままだと見づらいため、 145 | 中心位置を vbase という変数に示す値に移動して、表示しています。 146 | 147 | ## 課題 148 | 149 | 既知の不具合・課題 (取消線は解決済) 150 | 1. 緯度経度->直交座標変換が、おそらく正確ではない plutils.py 内 convertPolarToCartsian() 151 | 2. 1.と関係するかもしれないが、おそらく、建物・地面・道路の位置が微妙にずれている。(気のせいかもしれない) 152 | 3. ~~建物のポリゴンの法線方向が逆のものがあり、建物の壁が表示されないものがある。~~ 153 | 4. 道路(tran)の位置情報は高さが全てゼロで、地面(dem)の情報を引っ張ってこなければならない。 154 | 155 | あると良さそうなもの 156 | 1. 衛星画像をテクスチャとして地面に貼り付ける 157 | 2. 動作高速化 (ポリゴン読み込みコードの最適化、ポリゴン数の削減など) 158 | 3. [東京公共交通オープンデータ](https://tokyochallenge.odpt.org/)APIの利用 159 | 160 | ## plateaupyモジュール の説明 161 | 162 | 1. plparserで与えられたパスから.gmlを検索します。この時点では解析は行いません。 163 | 164 | ``` 165 | import plateaupy 166 | pl = plateaupy.plparser(paths=['path_to_citygml']) 167 | ``` 168 | 169 | * pl.locations にメッシュコード一覧がリストとして格納されます。 170 | * pl.codelists にcodelists定義が辞書として格納されます。 171 | 172 | 2. .gmlを解析します。引数で解析対象を変更できますので、appviewer.pyの使用法を参考にしてください。 173 | 174 | ``` 175 | pl.loadFiles() 176 | ``` 177 | 178 | * pl.bldg に建物情報が辞書として格納されます。 179 | * pl.dem に地表情報が辞書として格納されます。 180 | * pl.tran に道路情報が辞書として格納されます。 181 | 182 | bldg, dem, tran のクラスである plbldg, pldem, pltran は plobjクラスを親クラスとしています。plobjのメンバmeshesには、クラスplmeshのリストとして、解析したポリゴンデータが格納されます。このplmeshにはOpen3DのTriangleMeshや、BlenderのObjectに変換するメンバ関数があります。 183 | 184 | 3. 読み込んだ.gmlの全てをOpen3DのTriangleMeshに変換して取得することができます。 185 | 186 | ``` 187 | meshes = pl.get_Open3D_TriangleMesh() 188 | ``` 189 | 190 | 4. Open3DのVisualizerをラップした表示用クラスを用意しています。 191 | 192 | ``` 193 | from plateaupy.plvisualizer import Visualizer3D 194 | vi = Visualizer3D() 195 | for mesh in meshes: 196 | vi.vis.add_geometry(mesh) 197 | while True: 198 | key = vi.wait(1) 199 | if key == 27: # ESC 200 | break 201 | ``` 202 | 203 | 204 | 205 | 206 | ## ライセンス 207 | [MIT License](LICENSE.txt) 208 | 209 | 使用している外部モジュールは各々のライセンスに従ってください。 210 | * earcut-python 211 | https://github.com/joshuaskelly/earcut-python 212 | 213 | # 各都市の表示例 214 | 大阪 513573 215 | ![plateaupy](doc/Osaka_513573.png) 216 | 大阪 523503 217 | ![plateaupy](doc/Osaka_523503.png) 218 | 横浜 533915 219 | ![plateaupy](doc/Yokohama_533915.png) 220 | 川崎 533925 221 | ![plateaupy](doc/Kawasaki_533925.png) 222 | 横須賀 523975 223 | ![plateaupy](doc/Yokosuka_523975.png) 224 | 箱根 523960 225 | ![plateaupy](doc/Hakone_523960.png) 226 | 227 | 228 | ## Appendix : ハッカソンで開発した作品「都市SYM」 229 | 230 | [![PLATEAU 3D people flow](http://img.youtube.com/vi/2o--uUFSiZ8/0.jpg)](http://www.youtube.com/watch?v=2o--uUFSiZ8 "PLATEAU 3D people flow") 231 | 232 | plateaupyを用いた大規模3D人流シミュレータです。建物は黒のワイヤーフレーム、人は色の付いたキューブで表されています。以下のようにPLATEAU情報を利用しています。応用例として例えば、災害避難時の経過時間やボトルネックの場所分析、をすることができます。 233 | 234 | * 道路(tran)を画像解析して交差点と道を認識し、最短経路探索。 235 | * 建物用途(usage)ごとに色を変え、人が現在いる建物、あるいは向かっている建物の用途の色を、人の色として表示。 236 | * 建物の床面積や階数情報から、建物の各階に入室可能な人の容量を制限。 237 | 238 | -------------------------------------------------------------------------------- /appviewer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import numpy as np 4 | import argparse 5 | import open3d as o3d 6 | 7 | import plateaupy 8 | 9 | # argparser 10 | parser = argparse.ArgumentParser(description='plateaupy appviewer') 11 | parser.add_argument('-paths','--paths',help='list of paths to CityGML dirctories',default=['path_to_citygml'],type=str,nargs='*') 12 | parser.add_argument('-cmd','--cmd',help='special command [locations,codelists,dumpmeta]',default='',type=str) 13 | parser.add_argument('-k','--kind',help='gml kind, -1:all, 0:bldg, 1:dem, 2:luse, 3:tran, 4:brid',default=-1,type=int) 14 | parser.add_argument('-loc','--location',help='location index number',default=-1,type=int) 15 | parser.add_argument('-c','--cache',action='store_true', help='use cache data') 16 | parser.add_argument('-cpath','--cachepath',help='cache directory name',default='cached',type=str) 17 | parser.add_argument('-color','--color',help='color',default=None,type=float,nargs=3) 18 | parser.add_argument('-bgcolor','--bgcolor',help='background color',default=[1,1,1],type=float,nargs=3) 19 | parser.add_argument('-lod0','--lod0',action='store_true', help='use LOD0 in bldg.') 20 | parser.add_argument('-lod2texture','--lod2texture',action='store_true', help='show LOD2 texture images (too slow).') 21 | parser.add_argument('-zh','--zeroheight',action='store_true', help='force to set height values as zero.') 22 | parser.add_argument('-qx','--quarterx',help='force to divide mesh area(6) into the quarter. specify None or 0 or 1', default=None, type=int) 23 | parser.add_argument('-qy','--quartery',help='force to divide mesh area(6) into the quarter. specify None or 0 or 1', default=None, type=int) 24 | parser.add_argument('-plypath','--path_write_ply_files',help='path to write plyfiles.',default=None,type=str) 25 | parser.add_argument('-rec','--recfile',help='a record file name without ext.',default=None,type=str) 26 | parser.add_argument('-show_wire','--show_wire',action='store_true', help='show wireframe in polygons.') 27 | args = parser.parse_args() 28 | 29 | # scan paths 30 | pl = plateaupy.plparser(args.paths) 31 | 32 | # special commands 33 | if args.cmd == 'locations': 34 | print('locations: ',pl.locations) 35 | sys.exit(0) 36 | if args.cmd == 'codelists': 37 | print('codelists: ',pl.codelists) 38 | sys.exit(0) 39 | 40 | # options 41 | options = plateaupy.ploptions() 42 | options.bUseLOD0 = args.lod0 43 | options.bUseLOD2texture = args.lod2texture 44 | options.texturedir = args.cachepath 45 | options.bHeightZero = args.zeroheight 46 | quarter = None 47 | if args.quarterx is not None and args.quartery is not None: 48 | quarter = (args.quartery, args.quarterx) 49 | options.div6toQuarter = quarter 50 | # load 51 | pl.loadFiles( bLoadCache=args.cache, cachedir=args.cachepath, kind=args.kind, location=args.location, options=options ) 52 | 53 | # special commands 54 | if args.cmd == 'dumpmeta': 55 | for obj in pl.bldg.values(): 56 | for building in obj.buildings: 57 | print(building,'\n') 58 | sys.exit(0) 59 | 60 | # write ply files 61 | if args.path_write_ply_files is not None: 62 | print('writing ply files into', args.path_write_ply_files) 63 | os.makedirs( args.path_write_ply_files, exist_ok=True ) 64 | pl.write_Open3D_ply_files(savepath=args.path_write_ply_files, color=args.color) 65 | 66 | # show by Open3D 67 | print('presenting..') 68 | print('ESC to quit.') 69 | meshes = pl.get_Open3D_TriangleMesh(color=args.color) 70 | 71 | from plateaupy.plvisualizer import Visualizer3D 72 | vi = Visualizer3D() 73 | if args.show_wire: 74 | vi.vis.get_render_option().mesh_show_wireframe = True 75 | for mesh in meshes: 76 | vi.vis.add_geometry(mesh) 77 | if args.recfile is not None: 78 | vi.start_recording(args.recfile) 79 | while True: 80 | key = vi.wait(1) 81 | if key == 27: # ESC 82 | break 83 | vi.destroy() 84 | -------------------------------------------------------------------------------- /blender/blendertest.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import plateaupy 3 | import math 4 | 5 | if True: 6 | #reset objects 7 | bpy.ops.object.select_all(action='SELECT') 8 | bpy.ops.object.delete(True) 9 | #world 10 | bpy.context.scene.world.node_tree.nodes["Background"].inputs["Color"].default_value = (0,0,0,1) 11 | #lamp add 12 | bpy.ops.object.light_add(location=(0.0,0.0,2.0)) 13 | #camera add 14 | bpy.ops.object.camera_add(location=(5.0,0.0,0.0)) 15 | bpy.data.objects['Camera'].rotation_euler = (math.pi*1/2, 0, math.pi*1/2) 16 | 17 | ################## 18 | ##### args ##### 19 | ################## 20 | paths = ['../CityGML_01','../CityGML_02'] 21 | cache = False 22 | cachepath = 'cached_blender' 23 | kind = plateaupy.plobj.ALL 24 | location = 533925 25 | options = plateaupy.ploptions() 26 | ################## 27 | 28 | # scan paths 29 | pl = plateaupy.plparser(paths) 30 | 31 | # load 32 | pl.loadFiles( bLoadCache=cache, cachedir=cachepath, kind=kind, location=location, options=options ) 33 | 34 | # decide the base point to show 35 | vbase = None 36 | targets = list(pl.dem.values()) 37 | if len(targets) > 0: 38 | vbase = targets[0].get_center_vertices() 39 | targets = list(pl.bldg.values()) 40 | if vbase is None and len(targets) > 0: 41 | vbase = targets[0].get_center_vertices() 42 | targets = list(pl.tran.values()) 43 | if vbase is None and len(targets) > 0: 44 | vbase = targets[0].get_center_vertices() 45 | targets = list(pl.luse.values()) 46 | if vbase is None and len(targets) > 0: 47 | vbase = targets[0].get_center_vertices() 48 | 49 | # show 50 | pl.show_Blender_Objects(vbase=vbase) 51 | -------------------------------------------------------------------------------- /blender/blendertest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # preparation: 4 | # download 2.91.2 https://www.blender.org/download/ 5 | # specify the directory path 6 | BLENDER=~/blender 7 | 8 | # test script 9 | TEST_SCRIPT=blender/blendertest.py 10 | # blender bin 11 | BLENDER_BIN=$BLENDER/blender 12 | # blender python 13 | BLENDER_PYTHON=$BLENDER/2.91/python/bin/python3.7m 14 | 15 | if [ $# != 0 ]; then 16 | echo 17 | echo check Blender path : $BLENDER 18 | echo 19 | # install pip and modules 20 | $BLENDER_PYTHON -m ensurepip 21 | BLENDER_PIP=$BLENDER/2.91/python/bin/pip3 22 | $BLENDER_PIP install --upgrade pip 23 | $BLENDER_PIP install lxml open3d opencv-python 24 | else 25 | # run 26 | # https://docs.blender.org/manual/en/dev/advanced/command_line/arguments.html#python-options 27 | $BLENDER_BIN --python $TEST_SCRIPT --python-use-system-env 28 | fi 29 | -------------------------------------------------------------------------------- /doc/Hakone_523960.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/Hakone_523960.png -------------------------------------------------------------------------------- /doc/Kawasaki_533925.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/Kawasaki_533925.png -------------------------------------------------------------------------------- /doc/Osaka_513573.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/Osaka_513573.png -------------------------------------------------------------------------------- /doc/Osaka_523503.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/Osaka_523503.png -------------------------------------------------------------------------------- /doc/Yokohama_533915.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/Yokohama_533915.png -------------------------------------------------------------------------------- /doc/Yokosuka_523975.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/Yokosuka_523975.png -------------------------------------------------------------------------------- /doc/blender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/blender.png -------------------------------------------------------------------------------- /doc/plateaupy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AcculusSasao/plateaupy/6affb3d988c3bc71688ff41c29bd826164d13a66/doc/plateaupy.png -------------------------------------------------------------------------------- /download_plateau.py: -------------------------------------------------------------------------------- 1 | import urllib.request, os, sys, subprocess, shutil, glob 2 | from distutils.dir_util import copy_tree 3 | import argparse 4 | 5 | datasets = [ 6 | { 'name': 'plateau-tokyo23ku-citygml-2020', 7 | 'url' : 'https://www.geospatial.jp/ckan/dataset/plateau-tokyo23ku-citygml-2020', 8 | 'codelists': [ 9 | 'https://gic-plateau.s3-ap-northeast-1.amazonaws.com/2020/tokyo23ku/codelists_2.zip', 10 | ], 11 | 'metadata': [ 12 | 'https://gic-plateau.s3-ap-northeast-1.amazonaws.com/2020/tokyo23ku/udx131002020_2.zip', 13 | 'https://gic-plateau.s3-ap-northeast-1.amazonaws.com/2020/tokyo23ku/13000_metadata_lsld_2.xml', 14 | 'https://gic-plateau.s3-ap-northeast-1.amazonaws.com/2020/tokyo23ku/13000_metadata_fld_2.xml', 15 | ], 16 | 'citygml_base': 'https://gic-plateau.s3-ap-northeast-1.amazonaws.com/2020/tokyo23ku/', 17 | 'citygml': [ 18 | '533925_2.zip', 19 | '533926_2.7z', 20 | '533934_2.zip', 21 | '533935_2.7z', 22 | '533936_3.7z', 23 | '533937_2.zip', 24 | '533944_2.zip', 25 | '533945_2.7z', 26 | '533946_2.7z', 27 | '533947_2.zip', 28 | '533954_2.zip', 29 | '533955_2.zip', 30 | '533956_2.zip', 31 | '533957_2.zip', 32 | ], 33 | 'citygml_ex': [ 34 | 'doshasaigai_LOD1_2.zip', 35 | 'kouzuisinsuisoutei_2.zip', 36 | ], 37 | 'specs': [ 38 | 'https://www.geospatial.jp/ckan/dataset/a48396a3-b76f-4e7d-bc7b-04f354e3e5e9/resource/1015f435-3108-45b2-be42-08f78639d37b/download/tokyo23ku-all.png', 39 | 'https://www.geospatial.jp/ckan/dataset/a48396a3-b76f-4e7d-bc7b-04f354e3e5e9/resource/40f54174-7c7d-4f9f-b392-8c8ad585b09b/download/tokyo23ku-1.png', 40 | 'https://www.geospatial.jp/ckan/dataset/a48396a3-b76f-4e7d-bc7b-04f354e3e5e9/resource/40803cc8-c45c-4a81-bb8b-43ea83e3c04b/download/tokyo23ku-2.png', 41 | 'https://www.geospatial.jp/ckan/dataset/a48396a3-b76f-4e7d-bc7b-04f354e3e5e9/resource/780b8865-6642-4537-a6b3-884d259eb591/download/tokyo23ku-3.png', 42 | 'https://www.geospatial.jp/ckan/dataset/a48396a3-b76f-4e7d-bc7b-04f354e3e5e9/resource/0ad3e254-0c51-4346-ac9b-204e1077f7a0/download/tokyo23ku-4.png', 43 | 'https://gic-plateau.s3-ap-northeast-1.amazonaws.com/2020/tokyo23ku/tokyo23ku-ps_2.zip', 44 | ], 45 | }, 46 | { 'name': 'plateau-27100-osaka-shi-2020', 47 | 'url' : 'https://www.geospatial.jp/ckan/dataset/plateau-27100-osaka-shi-2020', 48 | 'citygml_all': [ 49 | #'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/27100_osaka-shi_citygml_2.zip', 50 | 'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/27100_osaka-shi_citygml3.zip', 51 | ], 52 | 'specs': [ 53 | 'https://www.geospatial.jp/ckan/dataset/45719509-717d-4866-a191-cb9e07cbe2a0/resource/1352e404-237b-4d8e-b6cc-5866be50ad13/download/27100osaka-shicatalog.xlsx', 54 | ], 55 | }, 56 | { 57 | 'name': 'plateau-14100-yokohama-city-2020', 58 | 'url' : 'https://www.geospatial.jp/ckan/dataset/plateau-14100-yokohama-city-2020', 59 | 'citygml_all': [ 60 | #'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/14100_yokohama-shi_citygml_2.zip', 61 | 'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/14100_yokohama-shi_citygml3.zip', 62 | ], 63 | 'specs': [ 64 | 'https://www.geospatial.jp/ckan/dataset/6e445bad-43ab-4a89-ac4d-6dcdbd769608/resource/8c947eff-e4f6-4e8e-a6b7-d4765ce1ca26/download/14100yokohama-shicatalog.xlsx', 65 | ], 66 | }, 67 | { 68 | 'name': 'plateau-14130-kawasaki-shi-2020', 69 | 'url' : 'https://www.geospatial.jp/ckan/dataset/plateau-14130-kawasaki-shi-2020', 70 | 'citygml_all': [ 71 | #'https://gic-plateau.s3-ap-northeast-1.amazonaws.com/2020/14130_kawasaki-shi_CityGML.zip', 72 | 'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/14130_kawasaki-shi_CityGML2.zip', 73 | ], 74 | 'specs': [ 75 | 'https://www.geospatial.jp/ckan/dataset/b7d8c630-05ef-4fbd-84e7-96a132204e3f/resource/32cb50b4-0085-47b0-be16-9d122b6e245f/download/14130kawasaki-shicatalog.xlsx', 76 | ], 77 | }, 78 | { 79 | 'name': 'plateau-14201-yokosuka-shi-2020', 80 | 'url' : 'https://www.geospatial.jp/ckan/dataset/plateau-14201-yokosuka-shi-2020', 81 | 'citygml_all': [ 82 | #'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/14201_yokosuka-shi_citygml.zip', 83 | 'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/14201_yokosuka-shi_citygml2.zip', 84 | ], 85 | 'specs': [ 86 | 'https://www.geospatial.jp/ckan/dataset/21e5d06c-442d-406c-be99-c376e80aee37/resource/4b75ee2a-8283-4680-af53-bfd6fbc4f434/download/14201-yokosuka-shicatalog.xlsx', 87 | ], 88 | }, 89 | { 90 | 'name': 'plateau-14382-hakone-machi-2020', 91 | 'url' : 'https://www.geospatial.jp/ckan/dataset/plateau-14382-hakone-machi-2020', 92 | 'citygml_all': [ 93 | #'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/14382_hakone-machi_CityGML_3.zip', 94 | 'https://gic-plateau.s3.ap-northeast-1.amazonaws.com/2020/14382_hakone-machi_CityGML4.zip', 95 | ], 96 | 'specs': [ 97 | 'https://www.geospatial.jp/ckan/dataset/32410073-80ac-4cb0-849c-bad92e83ef69/resource/f5b37e14-8ba8-44f1-b32c-8f97ea640203/download/14382hakone-machicatalog.xlsx', 98 | ], 99 | }, 100 | # add more dataset.. 101 | ] 102 | ''' 103 | { 104 | 'name': '', 105 | 'url' : '', 106 | 'citygml_all': [ 107 | '', 108 | ], 109 | 'specs': [ 110 | '', 111 | ], 112 | }, 113 | ''' 114 | dbnames = [db['name'] for db in datasets] 115 | 116 | parser = argparse.ArgumentParser(description='Download & extract PLATEAU dataset') 117 | parser.add_argument('db',type=str,help='-1(all) or specify dataset, as str or index in '+str(dbnames)) 118 | parser.add_argument('--basedir',type=str,default='CityGML2020',help='dst base directory') 119 | parser.add_argument('--no_download',action='store_true',help='not download files.') 120 | parser.add_argument('--no_extract',action='store_true',help='not extract files.') 121 | 122 | args = parser.parse_args() 123 | 124 | dbs = [] 125 | if args.db=='-1': 126 | dbs = datasets 127 | elif args.db in dbnames: 128 | dbs = [datasets[ dbnames.index(args.db) ]] 129 | elif args.db.isdecimal() and 0 <= int(args.db) and int(args.db) < len(datasets): 130 | dbs = [datasets[int(args.db)]] 131 | else: 132 | print('No dataset:',args.db) 133 | sys.exit(-1) 134 | 135 | def download(url,dstpath): 136 | os.makedirs(dstpath,exist_ok=True) 137 | filename = os.path.basename(url) 138 | print(' download',url,'to',dstpath) 139 | urllib.request.urlretrieve(url,dstpath+'/'+filename) 140 | 141 | def extract(url,dstpath): 142 | os.makedirs(dstpath,exist_ok=True) 143 | fname = os.path.splitext(os.path.basename(url)) 144 | if fname[1] == '.zip': 145 | cmd='unzip -o -q {} -d {}'.format(url,dstpath) 146 | elif fname[1] == '.7z': 147 | cmd='7z x -y -o{} {} > /dev/null'.format(dstpath,url) 148 | elif fname[1] == '.png' or fname[1] == '.xml' or fname[1] == '.xlsx' or fname[1] == '.xls': 149 | cmd='cp {} {}'.format(url,dstpath) 150 | else: 151 | print('unknown ext',fname[1]) 152 | return False 153 | print(' '+cmd) 154 | subprocess.run(cmd,shell=True) 155 | return True 156 | def copy_files(url,dstpath): 157 | cmd='cp -a {} {}'.format(url,dstpath) 158 | print(' '+cmd) 159 | subprocess.run(cmd,shell=True) 160 | def copy_tree_(url,dstpath): 161 | copy_tree(url,dstpath) 162 | print(' copy_tree {} {}'.format(url,dstpath)) 163 | 164 | basedir = args.basedir 165 | ardir = 'archive' 166 | for db in dbs: 167 | print(db['name']) 168 | _basedir = basedir+'/'+db['name'] 169 | _ardir = _basedir+'/'+ardir+'/' 170 | _codelistdir = _basedir+'/codelists/' 171 | _metadatadir = _basedir+'/metadata/' 172 | _specdir = _basedir+'/specification/' 173 | _udxdir = _basedir+'/udx/' 174 | _alldir = _basedir 175 | _tmpdir = _basedir+'/.tmp' 176 | 177 | if not args.no_download: 178 | if 'specs' in db: 179 | for xx in db['specs']: 180 | download(xx,_ardir) 181 | if 'codelists' in db: 182 | for xx in db['codelists']: 183 | download(xx,_ardir) 184 | if 'metadata' in db: 185 | for xx in db['metadata']: 186 | download(xx,_ardir) 187 | if 'citygml' in db: 188 | for xx in db['citygml']: 189 | download(db['citygml_base']+xx,_ardir) 190 | if 'citygml_all' in db: 191 | for xx in db['citygml_all']: 192 | download(xx,_ardir) 193 | if not args.no_extract: 194 | if 'specs' in db: 195 | for xx in db['specs']: 196 | extract(_ardir+os.path.basename(xx),_specdir) 197 | if 'codelists' in db: 198 | for xx in db['codelists']: 199 | extract(_ardir+os.path.basename(xx),_codelistdir) 200 | if 'metadata' in db: 201 | for xx in db['metadata']: 202 | extract(_ardir+os.path.basename(xx),_metadatadir) 203 | if 'citygml' in db: 204 | for xx in db['citygml']: 205 | if os.path.exists(_tmpdir): 206 | shutil.rmtree(_tmpdir) 207 | extract(_ardir+os.path.basename(xx),_tmpdir) 208 | for yy in glob.glob(_tmpdir+'/*'): 209 | pname = os.path.splitext(os.path.basename(yy))[0] 210 | extract(yy,_udxdir+pname) 211 | shutil.rmtree(_tmpdir) 212 | if 'citygml_all' in db: 213 | for xx in db['citygml_all']: 214 | if os.path.exists(_tmpdir): 215 | shutil.rmtree(_tmpdir) 216 | extract(_ardir+os.path.basename(xx),_tmpdir) 217 | tmpfiles = glob.glob(_tmpdir+'/*') 218 | if 'udx' in tmpfiles: 219 | copy_tree_( _tmpdir, _alldir ) 220 | #for yy in tmpfiles: 221 | # copy_files( yy, _alldir ) 222 | elif len(tmpfiles)>0 and os.path.exists(tmpfiles[0]+'/udx'): 223 | copy_tree_( tmpfiles[0], _alldir ) 224 | #for yy in glob.glob(tmpfiles[0]+'/*'): 225 | # copy_files( yy, _alldir ) 226 | else: 227 | bFound = False 228 | for yy in tmpfiles: 229 | if os.path.splitext(yy)[1] == '.zip' or os.path.splitext(yy)[1] == '.7z': 230 | extract(yy, _tmpdir) 231 | for tt in ['codelists','metadata','specification','udx']: 232 | for zz in glob.glob(_tmpdir+'/**/'+tt,recursive=True): 233 | copy_tree_(zz,_alldir+'/'+tt) 234 | bFound = True 235 | if not bFound: 236 | for tt in ['bldg','tran','luse','urf','dem','fld','tnm','lsld','brid','frn']: 237 | for zz in glob.glob(_tmpdir+'/**/'+tt,recursive=True): 238 | copy_files(zz,_udxdir) 239 | bFound = True 240 | if not bFound: 241 | print('\nerror! unknown dir structure:', tmpfiles) 242 | for yy in glob.glob(_udxdir+'**/*.zip',recursive=True): 243 | extract(yy, os.path.dirname(yy)) 244 | for yy in glob.glob(_udxdir+'**/*.7z',recursive=True): 245 | extract(yy, os.path.dirname(yy)) 246 | shutil.rmtree(_tmpdir) 247 | -------------------------------------------------------------------------------- /plateaupy/__init__.py: -------------------------------------------------------------------------------- 1 | from plateaupy.plparser import plparser 2 | from plateaupy.plobj import plobj 3 | from plateaupy.plbldg import plbldg 4 | from plateaupy.pldem import pldem 5 | from plateaupy.plluse import plluse 6 | from plateaupy.pltran import pltran 7 | from plateaupy.ploptions import ploptions 8 | from plateaupy import plutils 9 | -------------------------------------------------------------------------------- /plateaupy/plbldg.py: -------------------------------------------------------------------------------- 1 | from numpy.lib.polynomial import poly 2 | from numpy.lib.twodim_base import tri 3 | from plateaupy.plobj import plmesh, plobj 4 | from plateaupy.plutils import * 5 | from plateaupy.ploptions import ploptions 6 | from plateaupy.thirdparty.earcutpython.earcut.earcut import earcut 7 | import numpy as np 8 | import copy 9 | import pickle 10 | import sys 11 | import os 12 | import cv2 13 | from lxml import etree 14 | import lxml 15 | 16 | _floorheight = 2 # fixed value, the height of 1 floor in meter. 17 | 18 | class Building: 19 | def __init__(self): 20 | self.id = None # gml:id 21 | self.attr = dict() 22 | self.usage = None 23 | self.measuredHeight = None 24 | self.storeysAboveGround = None 25 | self.storeysBelowGround = None 26 | self.address = None 27 | self.buildingDetails = dict() 28 | self.extendedAttribute = dict() 29 | 30 | self.lod0RoofEdge = [] 31 | self.lod1Solid = [] 32 | 33 | #self.lod2Solid = [] 34 | # lod2MultiSurface 35 | self.lod2ground = dict() 36 | self.lod2roof = dict() 37 | self.lod2wall = dict() 38 | self.partex = appParameterizedTexture() 39 | def __str__(self): 40 | return 'Building id={}\n\ 41 | usage={}, measuredHeight={}, storeysAboveGround={}, storeysBelowGround={}\n\ 42 | address={}\n\ 43 | buildingDetails={}\n\ 44 | extendedAttribute={}\n\ 45 | attr={}'\ 46 | .format(self.id, self.usage, self.measuredHeight, self.storeysAboveGround, self.storeysBelowGround, \ 47 | self.address, self.buildingDetails, self.extendedAttribute, self.attr) 48 | # get vertices, triangles from lod0RoofEdge 49 | def getLOD0polygons(self, height=None): 50 | vertices = None 51 | triangles = None 52 | if len(self.lod0RoofEdge) > 0: 53 | vertices = [] 54 | for x in self.lod0RoofEdge[0]: 55 | xx = copy.deepcopy(x) 56 | if height is not None: 57 | xx[2] = height 58 | vertices.append( convertPolarToCartsian( *xx ) ) 59 | vertices = np.array(vertices) 60 | res = earcut(np.array(vertices,dtype=np.int).flatten(), dim=3) 61 | if len(res) > 0: 62 | triangles = np.array(res).reshape((-1,3)) 63 | return vertices, triangles 64 | 65 | class appParameterizedTexture: 66 | def __init__(self): 67 | self.imageURI = None 68 | self.targets = dict() 69 | @classmethod 70 | def search_list(cls, applist, polyid): 71 | for app in applist: 72 | if polyid in app.targets.keys(): 73 | return app 74 | return None 75 | 76 | class plbldg(plobj): 77 | def __init__(self,filename=None, options=ploptions()): 78 | super().__init__() 79 | self.kindstr = 'bldg' 80 | self.buildings = [] # list of Building 81 | if filename is not None: 82 | self.loadFile(filename, options=options) 83 | 84 | def loadFile(self,filename, options=ploptions()): 85 | tree, root = super().loadFile(filename) 86 | nsmap = self.removeNoneKeyFromDic(root.nsmap) 87 | 88 | # scan appearanceMember 89 | partex = [] 90 | for app in tree.xpath('/core:CityModel/app:appearanceMember/app:Appearance/app:surfaceDataMember/app:ParameterizedTexture', namespaces=nsmap): 91 | par = appParameterizedTexture() 92 | for at in app.xpath('app:imageURI', namespaces=nsmap): 93 | par.imageURI = at.text 94 | for at in app.xpath('app:target', namespaces=nsmap): 95 | uri = at.attrib['uri'] 96 | colist = [str2floats(v).reshape((-1,2)) for v in at.xpath('app:TexCoordList/app:textureCoordinates', namespaces=nsmap)] 97 | maxnum = max(map(lambda x:x.shape[0],colist)) 98 | for cidx,co in enumerate(colist): 99 | last = co[-1].reshape(-1,2) 100 | num = maxnum - co.shape[0] 101 | if num > 0: 102 | colist[cidx] = np.append(co,np.tile(co[-1].reshape(-1,2),(num,1)),axis=0) 103 | par.targets[uri] = np.array(colist) 104 | partex.append(par) 105 | 106 | # scan cityObjectMember 107 | blds = tree.xpath('/core:CityModel/core:cityObjectMember/bldg:Building', namespaces=nsmap) 108 | for bld in blds: 109 | b = Building() 110 | # gml:id 111 | b.id = bld.attrib['{'+nsmap['gml']+'}id'] 112 | # stringAttribute 113 | stringAttributes = bld.xpath('gen:stringAttribute', namespaces=nsmap) 114 | for at in stringAttributes: 115 | b.attr[at.attrib['name']] = at.getchildren()[0].text 116 | # genericAttributeSet 117 | genericAttributeSets = bld.xpath('gen:genericAttributeSet', namespaces=nsmap) 118 | for at in genericAttributeSets: 119 | vals = dict() 120 | for ch in at.getchildren(): 121 | vals[ ch.attrib['name'] ] = ch.getchildren()[0].text 122 | b.attr[at.attrib['name']] = vals 123 | # usage 124 | for at in bld.xpath('bldg:usage', namespaces=nsmap): 125 | b.usage = at.text 126 | # measuredHeight 127 | for at in bld.xpath('bldg:measuredHeight', namespaces=nsmap): 128 | b.measuredHeight = at.text 129 | # storeysAboveGround 130 | for at in bld.xpath('bldg:storeysAboveGround', namespaces=nsmap): 131 | b.storeysAboveGround = at.text 132 | # storeysBelowGround 133 | for at in bld.xpath('bldg:storeysBelowGround', namespaces=nsmap): 134 | b.storeysBelowGround = at.text 135 | # address 136 | try: # there are 2 names: 'xAL' and 'xal'.. 137 | for at in bld.xpath('bldg:address/core:Address/core:xalAddress/xAL:AddressDetails/xAL:Address', namespaces=nsmap): 138 | b.address = at.text 139 | except lxml.etree.XPathEvalError as e: 140 | for at in bld.xpath('bldg:address/core:Address/core:xalAddress/xal:AddressDetails/xal:Address', namespaces=nsmap): 141 | b.address = at.text 142 | # buildingDetails 143 | for at in bld.xpath('uro:buildingDetails/uro:BuildingDetails', namespaces=nsmap): 144 | for ch in at.getchildren(): 145 | tag = ch.tag 146 | tag = tag[ tag.rfind('}')+1: ] 147 | b.buildingDetails[tag] = ch.text 148 | # extendedAttribute 149 | for at in bld.xpath('uro:extendedAttribute/uro:KeyValuePair', namespaces=nsmap): 150 | ch = at.getchildren() 151 | b.extendedAttribute[ch[0].text] = ch[1].text 152 | # lod0RoofEdge 153 | vals = bld.xpath('bldg:lod0RoofEdge/gml:MultiSurface/gml:surfaceMember/gml:Polygon/gml:exterior/gml:LinearRing/gml:posList', namespaces=nsmap) 154 | b.lod0RoofEdge = [str2floats(v).reshape((-1,3)) for v in vals] 155 | # lod1Solid 156 | vals = bld.xpath('bldg:lod1Solid/gml:Solid/gml:exterior/gml:CompositeSurface/gml:surfaceMember/gml:Polygon/gml:exterior/gml:LinearRing/gml:posList', namespaces=nsmap) 157 | b.lod1Solid = [str2floats(v).reshape((-1,3)) for v in vals] 158 | minheight = 0 159 | if options.bHeightZero: 160 | # calc min height 161 | minheight = 10000 162 | for x in b.lod1Solid: 163 | if minheight > np.min(x[:,2]): 164 | minheight = np.min(x[:,2]) 165 | if b.storeysBelowGround is not None: 166 | minheight = minheight + (int(b.storeysBelowGround) * _floorheight) 167 | if minheight == 10000: 168 | minheight = 0 169 | for x in b.lod1Solid: 170 | x[:,2] -= minheight 171 | # lod2Solid 172 | # nothing to do for parsing 173 | # lod2MultiSurface : Ground, Roof, Wall 174 | for bb in bld.xpath('bldg:boundedBy/bldg:GroundSurface/bldg:lod2MultiSurface/gml:MultiSurface/gml:surfaceMember/gml:Polygon', namespaces=nsmap): 175 | polyid = '#' + bb.attrib['{'+nsmap['gml']+'}id'] 176 | vals = bb.xpath('gml:exterior/gml:LinearRing/gml:posList', namespaces=nsmap) 177 | surf = [str2floats(v).reshape((-1,3)) for v in vals] 178 | if options.bHeightZero: 179 | if minheight == 0: 180 | # calc min height 181 | minheight = 10000 182 | for x in surf: 183 | if minheight > np.min(x[:,2]): 184 | minheight = np.min(x[:,2]) 185 | if b.storeysBelowGround is not None: 186 | minheight = minheight + (int(b.storeysBelowGround) * _floorheight) 187 | if minheight == 10000: 188 | minheight = 0 189 | for x in surf: 190 | x[:,2] -= minheight 191 | b.lod2ground[polyid] = surf 192 | app = appParameterizedTexture.search_list( partex, polyid ) 193 | if app is not None: 194 | if b.partex.imageURI is None: 195 | b.partex = app 196 | #elif b.partex.imageURI != app.imageURI: 197 | # print('error') 198 | for bb in bld.xpath('bldg:boundedBy/bldg:RoofSurface/bldg:lod2MultiSurface/gml:MultiSurface/gml:surfaceMember/gml:Polygon', namespaces=nsmap): 199 | polyid = '#' + bb.attrib['{'+nsmap['gml']+'}id'] 200 | vals = bb.xpath('gml:exterior/gml:LinearRing/gml:posList', namespaces=nsmap) 201 | surf = [str2floats(v).reshape((-1,3)) for v in vals] 202 | if options.bHeightZero: 203 | for x in surf: 204 | x[:,2] -= minheight 205 | b.lod2roof[polyid] = surf 206 | app = appParameterizedTexture.search_list( partex, polyid ) 207 | if app is not None: 208 | if b.partex.imageURI is None: 209 | b.partex = app 210 | #elif b.partex.imageURI != app.imageURI: 211 | # print('error') 212 | for bb in bld.xpath('bldg:boundedBy/bldg:WallSurface/bldg:lod2MultiSurface/gml:MultiSurface/gml:surfaceMember/gml:Polygon', namespaces=nsmap): 213 | polyid = '#' + bb.attrib['{'+nsmap['gml']+'}id'] 214 | vals = bb.xpath('gml:exterior/gml:LinearRing/gml:posList', namespaces=nsmap) 215 | surf = [str2floats(v).reshape((-1,3)) for v in vals] 216 | if options.bHeightZero: 217 | for x in surf: 218 | x[:,2] -= minheight 219 | b.lod2wall[polyid] = surf 220 | app = appParameterizedTexture.search_list( partex, polyid ) 221 | if app is not None: 222 | if b.partex.imageURI is None: 223 | b.partex = app 224 | #elif b.partex.imageURI != app.imageURI: 225 | # print('error') 226 | self.buildings.append(b) 227 | 228 | # vertices, triangles 229 | if (not options.bUseLOD2texture) or options.bUseLOD0: 230 | mesh = plmesh() 231 | for b in self.buildings: 232 | if options.bUseLOD2texture and (not options.bUseLOD0): 233 | mesh = plmesh() 234 | 235 | if options.bUseLOD0: 236 | # LOD0 237 | vertices, triangles = b.getLOD0polygons() 238 | if vertices is not None and triangles is not None: 239 | vstart = len(mesh.vertices) 240 | mesh.vertices.extend( vertices ) 241 | mesh.triangles.extend( triangles + vstart ) 242 | elif b.lod2ground or b.lod2roof or b.lod2wall: 243 | # LOD2 244 | if options.bUseLOD2texture: 245 | if b.partex.imageURI is not None: 246 | # convert .tif into .png, because o3d.io.read_image() fails. 247 | mesh.texture_filename = os.path.dirname( self.filename ) + '/' + b.partex.imageURI 248 | img = cv2.imread(mesh.texture_filename) 249 | mesh.texture_filename = options.texturedir + '/' + os.path.basename( mesh.texture_filename ) + '.png' 250 | cv2.imwrite(mesh.texture_filename,img) 251 | # ground 252 | for key, value in b.lod2ground.items(): 253 | vertices = [ convertPolarToCartsian( *x ) for x in value[0] ] 254 | res = earcut(np.array(vertices,dtype=np.int).flatten(), dim=3) 255 | if len(res) > 0: 256 | vstart = len(mesh.vertices) 257 | mesh.vertices.extend( vertices ) 258 | triangles = np.array(res).reshape((-1,3)) 259 | mesh.triangles.extend( triangles + vstart ) 260 | # texture 261 | if options.bUseLOD2texture: 262 | if key in b.partex.targets.keys(): 263 | mesh.triangle_uvs.extend( [ b.partex.targets[key][0,x] for x in triangles.reshape((-1)) ] ) 264 | mesh.triangle_material_ids.extend( [0]*len(triangles) ) 265 | else: # add dummy uvs, material_ids (The texture can not appear if the numbers of triangles are different between triangles and them.) 266 | mesh.triangle_uvs.extend( [ np.zeros((2)) for x in range(len(triangles)*3) ] ) 267 | mesh.triangle_material_ids.extend( [0]*len(triangles) ) 268 | # roof 269 | for key, value in b.lod2roof.items(): 270 | vertices = [ convertPolarToCartsian( *x ) for x in value[0] ] 271 | res = earcut(np.array(vertices,dtype=np.int).flatten(), dim=3) 272 | if len(res) > 0: 273 | vstart = len(mesh.vertices) 274 | mesh.vertices.extend( vertices ) 275 | triangles = np.array(res).reshape((-1,3)) 276 | mesh.triangles.extend( triangles + vstart ) 277 | # texture 278 | if options.bUseLOD2texture: 279 | if key in b.partex.targets.keys(): 280 | mesh.triangle_uvs.extend( [ b.partex.targets[key][0,x] for x in triangles.reshape((-1)) ] ) 281 | mesh.triangle_material_ids.extend( [0]*len(triangles) ) 282 | # wall 283 | for key, value in b.lod2wall.items(): 284 | vertices = [ convertPolarToCartsian( *x ) for x in value[0] ] 285 | res = earcut(np.array(vertices,dtype=np.int).flatten(), dim=3) 286 | if len(res) > 0: 287 | vstart = len(mesh.vertices) 288 | mesh.vertices.extend( vertices ) 289 | triangles = np.array(res).reshape((-1,3)) 290 | mesh.triangles.extend( triangles + vstart ) 291 | # texture 292 | if options.bUseLOD2texture: 293 | if key in b.partex.targets.keys(): 294 | mesh.triangle_uvs.extend( [ b.partex.targets[key][0,x] for x in triangles.reshape((-1)) ] ) 295 | mesh.triangle_material_ids.extend( [0]*len(triangles) ) 296 | else: 297 | # LOD1 298 | for plist in b.lod1Solid: 299 | vertices = [ convertPolarToCartsian( *x ) for x in plist ] 300 | res = earcut(np.array(vertices,dtype=np.int).flatten(), dim=3) 301 | if len(res) > 0: 302 | vstart = len(mesh.vertices) 303 | mesh.vertices.extend( vertices ) 304 | triangles = np.array(res).reshape((-1,3)) 305 | mesh.triangles.extend( triangles + vstart ) 306 | # texture 307 | if options.bUseLOD2texture: # add dummy uvs, material_ids (The texture can not appear if the numbers of triangles are different between triangles and them.) 308 | mesh.triangle_uvs.extend( [ np.zeros((2)) for x in range(len(triangles)*3) ] ) 309 | mesh.triangle_material_ids.extend( [0]*len(triangles) ) 310 | if options.bUseLOD2texture: 311 | self.meshes.append(mesh) 312 | if not options.bUseLOD2texture: 313 | self.meshes.append(mesh) 314 | 315 | -------------------------------------------------------------------------------- /plateaupy/plcodelists.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | import glob 4 | from lxml import etree 5 | from plateaupy.plutils import * 6 | 7 | def scan_codelists( dir_codelists ): 8 | gmlDict = dict() 9 | for filename in glob.glob( dir_codelists + '/*.xml' ): 10 | tree = etree.parse(filename) 11 | root = tree.getroot() 12 | # gml:name 13 | vals = tree.xpath('/gml:Dictionary/gml:name', namespaces=root.nsmap) 14 | if len(vals) > 0: 15 | titlename = vals[0].text 16 | gmlDict[titlename] = dict() 17 | defs = tree.xpath('/gml:Dictionary/gml:dictionaryEntry/gml:Definition', namespaces=root.nsmap) 18 | # pair of description and name 19 | for deff in defs: 20 | description = deff.xpath('gml:description', namespaces=root.nsmap)[0].text 21 | name = deff.xpath('gml:name', namespaces=root.nsmap)[0].text 22 | gmlDict[titlename][name] = description 23 | return gmlDict 24 | -------------------------------------------------------------------------------- /plateaupy/pldem.py: -------------------------------------------------------------------------------- 1 | from plateaupy.plobj import plobj,plmesh 2 | from plateaupy.plutils import * 3 | from plateaupy.ploptions import ploptions 4 | import numpy as np 5 | import copy 6 | import os 7 | from lxml import etree 8 | 9 | class pldem(plobj): 10 | def __init__(self,filename=None, options=ploptions()): 11 | super().__init__() 12 | self.kindstr = 'dem' 13 | self.posLists = None # list of 'posList'(LinearRing) : [*,4,3] 14 | if filename is not None: 15 | self.loadFile(filename, options=options) 16 | 17 | def loadFile(self,filename, options=ploptions(), num_search_coincident=100): 18 | tree, root = super().loadFile(filename) 19 | nsmap = self.removeNoneKeyFromDic(root.nsmap) 20 | lt,rb = convertMeshcodeToLatLon( os.path.basename(filename).split('_')[0] ) 21 | #center = (np.array(lt)+np.array(rb))/2 22 | center = ( lt[0]*0.8+rb[0]*0.2, lt[1]*0.2+rb[1]*0.8 ) 23 | # posLists 24 | vals = tree.xpath('/core:CityModel/core:cityObjectMember/dem:ReliefFeature/dem:reliefComponent/dem:TINRelief/dem:tin/gml:TriangulatedSurface/gml:trianglePatches/gml:Triangle/gml:exterior/gml:LinearRing/gml:posList', namespaces=nsmap) 25 | self.posLists = np.array([str2floats(v).reshape((-1,3)) for v in vals]) 26 | if options.bHeightZero: 27 | self.posLists[:,:,2] = 0 28 | #print(self.posLists.shape) 29 | # convert to XYZ 30 | posLists = copy.deepcopy(self.posLists) 31 | usebit = [] 32 | for x in posLists: 33 | for yidx,y in enumerate(x): 34 | bit = True 35 | if options.div6toQuarter is not None: 36 | lat = y[0] 37 | lon = y[1] 38 | if lat < center[0]: 39 | lat = 0 40 | else: 41 | lat = 1 42 | if lon < center[1]: 43 | lon = 0 44 | else: 45 | lon = 1 46 | if (lat,lon) != options.div6toQuarter: 47 | bit = False 48 | if yidx < 3: 49 | usebit.append(bit) 50 | y[:] = convertPolarToCartsian(*y) 51 | # to vertices and triangles 52 | # integrate vertices that are coincident. 53 | mesh = plmesh() 54 | mesh.triangles = np.zeros( (posLists.shape[0], 3), dtype=np.int ) 55 | for xidx,x in enumerate(posLists): 56 | for yidx,y in enumerate(x[:3]): 57 | newid = -1 58 | if options.div6toQuarter is None: 59 | num = xidx 60 | if num > num_search_coincident: 61 | num = num_search_coincident 62 | for _id, vvv in enumerate( mesh.vertices[xidx-num:] ): 63 | if vvv[0]==y[0] and vvv[1]==y[1] and vvv[2]==y[2]: 64 | newid = _id + xidx - num 65 | break 66 | if newid < 0: 67 | newid = len(mesh.vertices) 68 | mesh.vertices.append(y) 69 | mesh.triangles[xidx,yidx] = newid 70 | # remove 71 | if options.div6toQuarter is not None: 72 | newtriangles = [] 73 | for tri in mesh.triangles: 74 | if usebit[tri[0]] and usebit[tri[1]] and usebit[tri[2]]: 75 | newtriangles.append( tri ) 76 | mesh.triangles = np.array(newtriangles) 77 | 78 | self.meshes.append(mesh) 79 | 80 | 81 | -------------------------------------------------------------------------------- /plateaupy/plluse.py: -------------------------------------------------------------------------------- 1 | from plateaupy.plobj import plobj 2 | from plateaupy.plutils import * 3 | from plateaupy.ploptions import ploptions 4 | import numpy as np 5 | import copy 6 | from lxml import etree 7 | 8 | class plluse(plobj): 9 | def __init__(self,filename=None, options=ploptions()): 10 | super().__init__() 11 | self.kindstr = 'luse' 12 | if filename is not None: 13 | self.loadFile(filename, options=options) 14 | def loadFile(self,filename, options=ploptions()): 15 | pass 16 | -------------------------------------------------------------------------------- /plateaupy/plobj.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from lxml import etree 4 | from numpy.lib.npyio import save 5 | from plateaupy.plutils import * 6 | import pickle 7 | 8 | class plmesh: 9 | def __init__(self) -> None: 10 | self.vertices = [] # [ num_of_vertices, 3 ] (float) 11 | self.triangles = [] # [ num_of_triangles, 3 ] (int) 12 | self.texture_filename = None 13 | self.triangle_uvs = [] # [ 3 * num_of_triangles, 2 ] (float) 14 | self.triangle_material_ids = [] # [ num_of_triangles ] (int) 15 | 16 | def get_center_vertices(self): 17 | return np.mean( self.vertices, axis=0 ) 18 | 19 | def to_Open3D_TriangleMesh(self, color=None, wireonly=False): 20 | import open3d as o3d 21 | 22 | mesh = o3d.geometry.TriangleMesh() 23 | mesh.vertices = o3d.utility.Vector3dVector( self.vertices ) 24 | mesh.triangles = o3d.utility.Vector3iVector( self.triangles ) 25 | if self.texture_filename is not None: 26 | mesh.textures = [o3d.io.read_image( self.texture_filename )] 27 | mesh.triangle_uvs = o3d.utility.Vector2dVector( np.array(self.triangle_uvs) ) 28 | mesh.triangle_material_ids = o3d.utility.IntVector( np.array(self.triangle_material_ids, dtype=np.int32) ) 29 | #print( 'triangles = ', np.array(self.triangles).shape ) 30 | #print( 'triangle_uvs = ', np.array(self.triangle_uvs).shape ) 31 | #print( 'triangle_material_ids = ', np.array(self.triangle_material_ids).shape ) 32 | elif color is not None: 33 | mesh.paint_uniform_color( color ) 34 | mesh.compute_vertex_normals() 35 | if wireonly: 36 | mesh = o3d.geometry.LineSet.create_from_triangle_mesh(mesh) 37 | return mesh 38 | 39 | def to_Blender_Object(self, meshname, vbase=None): 40 | import bpy 41 | 42 | namestr = meshname 43 | mesh = bpy.data.meshes.new(name=namestr) 44 | vertices = [ list(v) for v in self.vertices ] 45 | triangles = [ list(t) for t in self.triangles ] 46 | if vbase is not None: 47 | vertices = [ list(np.array(v) - vbase) for v in vertices ] 48 | mesh.from_pydata( vertices, [], triangles ) 49 | mesh.update(calc_edges=True) 50 | obj = bpy.data.objects.new(name=namestr,object_data=mesh) 51 | return obj 52 | 53 | class plobj: 54 | # kind 55 | ALL = -1 56 | BLDG = 0 57 | DEM = 1 58 | LUSE = 2 59 | TRAN = 3 60 | BRID = 4 61 | @staticmethod 62 | def getLocationFromFilename(filename, bLarge=False): 63 | loc = int(os.path.basename(filename).split('_')[0]) 64 | if bLarge and loc >= 1000000: # eg. bldg is 53392546, besides dem is 533925 65 | loc = loc // 100 66 | return loc 67 | @staticmethod 68 | def get6QuarterFromFilename(filename): 69 | division = 8 # 5 70 | locstr = os.path.basename(filename).split('_')[0] 71 | if len(locstr) != 8: 72 | return (-1,-1) 73 | lat = int(locstr[6]) 74 | lon = int(locstr[7]) 75 | if lat < division: 76 | lat = 0 77 | else: 78 | lat = 1 79 | if lon < division: 80 | lon = 0 81 | else: 82 | lon = 1 83 | return (lat,lon) 84 | @staticmethod 85 | def getCacheFilename(cachedir, filename): 86 | return cachedir + '/' + os.path.splitext(os.path.basename(filename))[0] 87 | @staticmethod 88 | def removeNoneKeyFromDic(nsmap): 89 | newnsmap = dict() 90 | for k,v in nsmap.items(): 91 | if k is not None: 92 | newnsmap[k]=v 93 | return newnsmap 94 | 95 | def __init__(self): 96 | self.kindstr = 'obj' 97 | self.filename = None 98 | self.location = 0 # location number 99 | self.lowerCorner = np.zeros((3)) # lowerCorner (lon,lat,height) 100 | self.upperCorner = np.zeros((3)) # upperCorner 101 | self.meshes = [] # list of plmesh 102 | 103 | def loadFile(self,filename): 104 | print('load', filename) 105 | self.filename = filename 106 | self.location = self.getLocationFromFilename(filename) 107 | tree = etree.parse(filename) 108 | root = tree.getroot() 109 | # lowerCorner, upperCorner 110 | nsmap = self.removeNoneKeyFromDic(root.nsmap) 111 | vals = tree.xpath('/core:CityModel/gml:boundedBy/gml:Envelope/gml:lowerCorner', namespaces=nsmap) 112 | if len(vals) > 0: 113 | self.lowerCorner = str2floats(vals[0]) 114 | vals = tree.xpath('/core:CityModel/gml:boundedBy/gml:Envelope/gml:upperCorner', namespaces=nsmap) 115 | if len(vals) > 0: 116 | self.upperCorner = str2floats(vals[0]) 117 | return tree, root 118 | 119 | def get_Open3D_TriangleMesh(self, color=None, wireonly=False): 120 | _color = color 121 | if _color is None: 122 | _color = np.random.rand(3) 123 | return [ m.to_Open3D_TriangleMesh(color=_color, wireonly=wireonly) for m in self.meshes ] 124 | 125 | def write_Open3D_ply_files(self, savepath, color=None): 126 | import open3d as o3d 127 | 128 | meshes = self.get_Open3D_TriangleMesh(color=color) 129 | for idx,m in enumerate(meshes): 130 | filename = savepath + '/' + str(self.location) + '_' + self.kindstr + '_' + str(idx) + '.ply' 131 | o3d.io.write_triangle_mesh( filename, m) 132 | 133 | def get_Blender_Objects(self, vbase=None): 134 | rname = self.kindstr 135 | return [ m.to_Blender_Object(meshname=str(self.location)+'_'+rname+'_'+str(idx),vbase=vbase) for idx,m in enumerate(self.meshes) ] 136 | 137 | def get_center_vertices(self): 138 | centers = np.array([ m.get_center_vertices() for m in self.meshes ]) 139 | return np.mean(centers, axis=0) 140 | 141 | # cache save/load 142 | def save(self,filepath): 143 | with open(filepath+'.pkl', mode='wb') as f: 144 | pickle.dump( self, f) 145 | def load(self,filepath): 146 | try: 147 | with open(filepath+'.pkl', mode='rb') as f: 148 | return pickle.load( f ) 149 | except FileNotFoundError as e: 150 | print(e) 151 | return None 152 | 153 | -------------------------------------------------------------------------------- /plateaupy/ploptions.py: -------------------------------------------------------------------------------- 1 | class ploptions: 2 | def __init__(self) -> None: 3 | # show LOD2 texture 4 | self.bUseLOD2texture = False 5 | self.texturedir = 'cached' 6 | # use LOD0 7 | self.bUseLOD0 = False 8 | # force height = 0 9 | self.bHeightZero = False 10 | # divide the mesh 'AAAABB' automatically into the quarter, the group of 'AAAAABBCC'. 11 | # specify (0,0)or(0,1)or(1,0)or(1,1) as lat,lon 12 | # Now, it is very hard-coded. Do not use it. 13 | self.div6toQuarter = None 14 | -------------------------------------------------------------------------------- /plateaupy/plparser.py: -------------------------------------------------------------------------------- 1 | import os 2 | from plateaupy.ploptions import ploptions 3 | import numpy as np 4 | import glob 5 | from lxml import etree 6 | from plateaupy.plobj import plobj 7 | from plateaupy.plbldg import plbldg 8 | from plateaupy.pldem import pldem 9 | from plateaupy.plluse import plluse 10 | from plateaupy.pltran import pltran 11 | from plateaupy.plutils import * 12 | from plateaupy.plcodelists import scan_codelists 13 | 14 | class plparser: 15 | def __init__(self, paths=None): 16 | # filenames 17 | self.filenames_bldg = [] 18 | self.filenames_dem = [] 19 | self.filenames_luse = [] 20 | self.filenames_tran = [] 21 | # objects (dictonary, the key is obj.location) 22 | self.bldg = dict() 23 | self.dem = dict() 24 | self.luse = dict() 25 | self.tran = dict() 26 | # list of location numbers 27 | self.locations = [] 28 | # add paths 29 | for path in paths: 30 | self.addPath(path) 31 | 32 | def addPath(self,path): # path to CityGML 33 | print('search ' + path) 34 | # now, static path 35 | path_bldg = path + '/udx/bldg' 36 | path_dem = path + '/udx/dem' 37 | path_luse = path + '/udx/luse' 38 | path_tran = path + '/udx/tran' 39 | val = sorted(glob.glob(path_bldg+'/*.gml')) 40 | print(' bldg : ',len(val), 'files') 41 | self.filenames_bldg.extend( val ) 42 | val = sorted(glob.glob(path_dem+'/*.gml')) 43 | print(' dem : ',len(val), 'files') 44 | self.filenames_dem.extend( val ) 45 | val = sorted(glob.glob(path_luse+'/*.gml')) 46 | print(' luse : ',len(val), 'files') 47 | self.filenames_luse.extend( val ) 48 | val = sorted(glob.glob(path_tran+'/*.gml')) 49 | print(' tran : ',len(val), 'files') 50 | self.filenames_tran.extend( val ) 51 | # add locations 52 | locations = list(self.locations) 53 | locations.extend( [plobj.getLocationFromFilename( filename, True ) for filename in self.filenames_bldg] ) 54 | locations.extend( [plobj.getLocationFromFilename( filename, True ) for filename in self.filenames_dem] ) 55 | locations.extend( [plobj.getLocationFromFilename( filename, True ) for filename in self.filenames_luse] ) 56 | locations.extend( [plobj.getLocationFromFilename( filename, True ) for filename in self.filenames_tran] ) 57 | self.locations = sorted(list(set(locations))) 58 | # parse codelists 59 | dir_codelists = glob.glob( path + '/codelists/' ) 60 | if len(dir_codelists) > 0: 61 | self.codelists = scan_codelists(dir_codelists[0]) 62 | 63 | ''' 64 | @param bLoadCache: load cache data or not 65 | @param cachedir: cache directory name 66 | @param kind: specify the type of gml, plobj.ALL, plobj.BLDG,.. 67 | @param location: specify which gml data in the type are loaded, -1:all, <1000:array index, >=1000:location 68 | ''' 69 | def loadFiles(self, bLoadCache=False, cachedir='cached', kind=None, location=-1, options=ploptions()): 70 | if kind is None: 71 | kind = plobj.ALL 72 | if cachedir is not None: 73 | os.makedirs( cachedir, exist_ok=True ) 74 | # prepare filenames 75 | filenames_bldg = [] 76 | filenames_dem = [] 77 | filenames_luse = [] 78 | filenames_tran = [] 79 | if kind==plobj.BLDG or kind==plobj.ALL: 80 | if location < 0: 81 | filenames_bldg = self.filenames_bldg 82 | elif location < 1000: 83 | filenames_bldg.append( self.filenames_bldg[location] ) 84 | else: 85 | for filename in self.filenames_bldg: 86 | if plobj.getLocationFromFilename(filename,False) == location or plobj.getLocationFromFilename(filename,True) == location: 87 | filenames_bldg.append(filename) 88 | if kind==plobj.DEM or kind==plobj.ALL: 89 | if location < 0: 90 | filenames_dem = self.filenames_dem 91 | elif location < 1000: 92 | filenames_dem.append( self.filenames_dem[location] ) 93 | else: 94 | for filename in self.filenames_dem: 95 | if plobj.getLocationFromFilename(filename) == location: 96 | filenames_dem.append(filename) 97 | if kind==plobj.LUSE or kind==plobj.ALL: 98 | if location < 0: 99 | filenames_luse = self.filenames_luse 100 | elif location < 1000: 101 | filenames_luse.append( self.filenames_luse[location] ) 102 | else: 103 | for filename in self.filenames_luse: 104 | if plobj.getLocationFromFilename(filename) == location: 105 | filenames_luse.append(filename) 106 | if kind==plobj.TRAN or kind==plobj.ALL: 107 | if location < 0: 108 | filenames_tran = self.filenames_tran 109 | elif location < 1000: 110 | filenames_tran.append( self.filenames_tran[location] ) 111 | else: 112 | for filename in self.filenames_tran: 113 | if plobj.getLocationFromFilename(filename) == location: 114 | filenames_tran.append(filename) 115 | # load files 116 | if bLoadCache: 117 | print('### loading cache data..') 118 | print('# bldg') 119 | for f in filenames_bldg: 120 | if (options.div6toQuarter is not None) and (options.div6toQuarter != plobj.get6QuarterFromFilename(f)): 121 | continue 122 | obj = plbldg() 123 | res = obj.load(plobj.getCacheFilename(cachedir,f)) 124 | if res is not None: 125 | obj = res 126 | self.bldg[obj.location] = obj 127 | print('# dem') 128 | for f in filenames_dem: 129 | obj = pldem() 130 | res = obj.load(plobj.getCacheFilename(cachedir,f)) 131 | if res is not None: 132 | obj = res 133 | self.dem[obj.location] = obj 134 | print('# luse') 135 | for f in filenames_luse: 136 | obj = plluse() 137 | res = obj.load(plobj.getCacheFilename(cachedir,f)) 138 | if res is not None: 139 | obj = res 140 | self.luse[obj.location] = obj 141 | print('# tran') 142 | for f in filenames_tran: 143 | obj = pltran() 144 | res = obj.load(plobj.getCacheFilename(cachedir,f)) 145 | if res is not None: 146 | obj = res 147 | self.tran[obj.location] = obj 148 | else: 149 | print('### loading GML data..') 150 | print('# bldg') 151 | for f in filenames_bldg: 152 | if (options.div6toQuarter is not None) and (options.div6toQuarter != plobj.get6QuarterFromFilename(f)): 153 | continue 154 | obj = plbldg(f, options=options) 155 | self.bldg[obj.location] = obj 156 | obj.save(plobj.getCacheFilename(cachedir,f)) 157 | print('# dem') 158 | for f in filenames_dem: 159 | obj = pldem(f, options=options) 160 | self.dem[obj.location] = obj 161 | obj.save(plobj.getCacheFilename(cachedir,f)) 162 | print('# luse') 163 | for f in filenames_luse: 164 | obj = plluse(f, options=options) 165 | self.luse[obj.location] = obj 166 | obj.save(plobj.getCacheFilename(cachedir,f)) 167 | print('# tran') 168 | for f in filenames_tran: 169 | #obj = pltran(f, self.dem[ plobj.getLocationFromFilename(f) ]) 170 | obj = pltran(f, options=options) 171 | self.tran[obj.location] = obj 172 | obj.save(plobj.getCacheFilename(cachedir,f)) 173 | 174 | def get_Open3D_TriangleMesh(self, color=None, kindbits=255, wireonly=False): 175 | meshes = [] 176 | if kindbits & (1 << plobj.BLDG): 177 | for obj in self.bldg.values(): 178 | meshes.extend( obj.get_Open3D_TriangleMesh(color=color, wireonly=wireonly) ) 179 | if kindbits & (1 << plobj.DEM): 180 | for obj in self.dem.values(): 181 | meshes.extend( obj.get_Open3D_TriangleMesh(color=color, wireonly=wireonly) ) 182 | if kindbits & (1 << plobj.LUSE): 183 | for obj in self.luse.values(): 184 | meshes.extend( obj.get_Open3D_TriangleMesh(color=color, wireonly=wireonly) ) 185 | if kindbits & (1 << plobj.TRAN): 186 | for obj in self.tran.values(): 187 | meshes.extend( obj.get_Open3D_TriangleMesh(color=color, wireonly=wireonly) ) 188 | return meshes 189 | 190 | def write_Open3D_ply_files(self, savepath, color=None): 191 | for obj in self.bldg.values(): 192 | obj.write_Open3D_ply_files( savepath=savepath, color=color ) 193 | for obj in self.dem.values(): 194 | obj.write_Open3D_ply_files( savepath=savepath, color=color ) 195 | for obj in self.luse.values(): 196 | obj.write_Open3D_ply_files( savepath=savepath, color=color ) 197 | for obj in self.tran.values(): 198 | obj.write_Open3D_ply_files( savepath=savepath, color=color ) 199 | 200 | def show_Blender_Objects(self, vbase=None): 201 | import bpy 202 | scene = bpy.context.scene 203 | for obj in self.bldg.values(): 204 | _obj = obj.get_Blender_Objects(vbase=vbase) 205 | for _o in _obj: 206 | scene.collection.objects.link(_o) 207 | for obj in self.dem.values(): 208 | _obj = obj.get_Blender_Objects(vbase=vbase) 209 | for _o in _obj: 210 | scene.collection.objects.link(_o) 211 | for obj in self.luse.values(): 212 | _obj = obj.get_Blender_Objects(vbase=vbase) 213 | for _o in _obj: 214 | scene.collection.objects.link(_o) 215 | for obj in self.tran.values(): 216 | _obj = obj.get_Blender_Objects(vbase=vbase) 217 | for _o in _obj: 218 | scene.collection.objects.link(_o) 219 | bpy.context.view_layer.update() 220 | -------------------------------------------------------------------------------- /plateaupy/pltran.py: -------------------------------------------------------------------------------- 1 | from plateaupy.plobj import plobj,plmesh 2 | from plateaupy.plutils import * 3 | from plateaupy.ploptions import ploptions 4 | from plateaupy.thirdparty.earcutpython.earcut.earcut import earcut 5 | import numpy as np 6 | import copy 7 | import os 8 | from lxml import etree 9 | 10 | # (!TBD!) road height [in meter] offset in loading, because the height values in .gml are always zero. 11 | #temporary_road_height_offset = 20 12 | 13 | class pltran(plobj): 14 | def __init__(self,filename=None, options=ploptions(),dem=None): 15 | super().__init__() 16 | self.kindstr = 'tran' 17 | self.posLists = None # list of 'posList'(LinearRing) 18 | if filename is not None: 19 | self.loadFile(filename, options=options, dem=dem) 20 | 21 | def loadFile(self,filename, options=ploptions(),dem=None): 22 | tree, root = super().loadFile(filename) 23 | nsmap = self.removeNoneKeyFromDic(root.nsmap) 24 | lt,rb = convertMeshcodeToLatLon( os.path.basename(filename).split('_')[0] ) 25 | #center = (np.array(lt)+np.array(rb))/2 26 | center = ( lt[0]*0.8+rb[0]*0.2, lt[1]*0.2+rb[1]*0.8 ) 27 | # posLists 28 | vals = tree.xpath('/core:CityModel/core:cityObjectMember/tran:Road/tran:lod1MultiSurface/gml:MultiSurface/gml:surfaceMember/gml:Polygon/gml:exterior/gml:LinearRing/gml:posList', namespaces=nsmap) 29 | self.posLists = [str2floats(v).reshape((-1,3)) for v in vals] 30 | if options.bHeightZero: 31 | for x in self.posLists: 32 | x[:,2] = 1 # to be a little bit more than 0 33 | # vertices, triangles 34 | mesh = plmesh() 35 | #self.posLists = self.posLists[:1000] 36 | # invoke multi processes 37 | usebit = [] 38 | for plist in self.posLists: 39 | vertices = [ convertPolarToCartsian( *x ) for x in plist ] 40 | res = earcut(np.array(vertices, dtype=np.int).flatten(), dim=3) 41 | if len(res) > 0: 42 | triangles = np.array(res).reshape((-1,3)) + len(mesh.vertices) 43 | mesh.vertices.extend( vertices ) 44 | mesh.triangles.extend( triangles ) 45 | if options.div6toQuarter is not None: 46 | for x in plist: 47 | bit = True 48 | if options.div6toQuarter is not None: 49 | lat = x[0] 50 | lon = x[1] 51 | if lat < center[0]: 52 | lat = 0 53 | else: 54 | lat = 1 55 | if lon < center[1]: 56 | lon = 0 57 | else: 58 | lon = 1 59 | if (lat,lon) != options.div6toQuarter: 60 | bit = False 61 | usebit.append(bit) 62 | # remove 63 | if options.div6toQuarter is not None: 64 | newtriangles = [] 65 | for tri in mesh.triangles: 66 | if usebit[tri[0]] and usebit[tri[1]] and usebit[tri[2]]: 67 | newtriangles.append( tri ) 68 | mesh.triangles = np.array(newtriangles) 69 | 70 | self.meshes.append(mesh) 71 | 72 | def load(self,filepath): 73 | res = super().load(filepath) 74 | res.meshes[0].vertices = np.array(res.meshes[0].vertices) 75 | ### !!! TBD 76 | #res.meshes[0].vertices[:,2] += temporary_road_height_offset 77 | return res 78 | -------------------------------------------------------------------------------- /plateaupy/plutils.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | import random 4 | import string 5 | import copy 6 | import cv2 7 | 8 | def printMethods(obj): 9 | for x in dir(obj): 10 | print( x, ':', type(eval("obj."+x)) ) 11 | 12 | def randomname(n): 13 | randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)] 14 | return ''.join(randlst) 15 | 16 | def str2floats(x): 17 | return np.array([float(i) for i in x.text.split(' ')]) 18 | 19 | # convert (longitude[rad],latitude[rad],height[meter]) into (X,Y,Z[meter]) 20 | # 21 | # ref: https://vldb.gsi.go.jp/sokuchi/surveycalc/surveycalc/transf.html 22 | # ref: https://vldb.gsi.go.jp/sokuchi/surveycalc/surveycalc/algorithm/trans/trans_alg.html 23 | # ref: http://tancro.e-central.tv/grandmaster/excel/radius.html 24 | def convertPolarToCartsian( lat, lon, hei ): 25 | cosLat = math.cos( lat * math.pi/180 ) 26 | sinLat = math.sin( lat * math.pi/180 ) 27 | cosLon = math.cos( lon * math.pi/180 ) 28 | sinLon = math.sin( lon * math.pi/180 ) 29 | # Semi-mejor axis [in meter] 30 | a = 6378137 31 | # Flattening 32 | f = 1 / 298.257222101 33 | # Eccentricity 34 | e = math.sqrt( 2*f - f*f ) 35 | W = math.sqrt( 1 - e*e * sinLon*sinLon ) 36 | # Prime vertical radius of curvature 37 | N = a / W 38 | # 39 | h = hei 40 | X = ( N + h ) * cosLat * cosLon 41 | Y = ( N + h ) * cosLat * sinLon 42 | Z = ( N * (1 - e*e) + h ) * sinLat 43 | return np.array([X,Y,Z]) 44 | 45 | # return left-top (latitude,longitude) and right-bottom 46 | def convertMeshcodeToLatLon( meshcode ): 47 | smeshcode = str(meshcode) 48 | length = len(smeshcode) 49 | lat = int(smeshcode[0:2]) * 2 / 3 50 | lon = int(smeshcode[2:4]) + 100 51 | lat2 = lat + 2/3 52 | lon2 = lon + 1 53 | if length > 4: 54 | if length >= 6: 55 | lat += int(smeshcode[4:5]) * 2 / 3 / 8 56 | lon += int(smeshcode[5:6]) / 8 57 | lat2 = lat + 2 / 3 / 8 58 | lon2 = lon + 1/8 59 | if length >= 8: 60 | lat += int(smeshcode[6:7]) * 2 / 3 / 8 / 10 61 | lon += int(smeshcode[7:8]) / 8 / 10 62 | lat2 = lat + 2 / 3 / 8 / 10 63 | lon2 = lon + 1 / 8 / 10 64 | return [lat2,lon],[lat,lon2] 65 | 66 | class VerticesTransformer: 67 | def __init__(self, lowerCorner=None, upperCorner=None) -> None: 68 | self.rot = np.eye(3) # rotation matrix 3x3 69 | self.trans = np.zeros((3)) # translation vector 3 70 | self.scaleX = 1 # scale value of x axis 71 | self.aspectXY = 1 # ratio of X / Y 72 | if lowerCorner is not None and upperCorner is not None: 73 | self.calc( lowerCorner, upperCorner ) 74 | 75 | # calculate rot, trans, scaleX, aspectXY 76 | # lowerCorner, upperCorner must be [lat, lon, 0] 77 | def calc(self, lowerCorner, upperCorner): 78 | # prepare 3D points correspoinding (0,0), (0,1), (1,0) 79 | lt = convertPolarToCartsian(*lowerCorner) 80 | rt = convertPolarToCartsian( lowerCorner[0], upperCorner[1], 0 ) 81 | lb = convertPolarToCartsian( upperCorner[0], lowerCorner[1], 0 ) 82 | # base point 83 | self.trans = copy.deepcopy(lt) 84 | # 2 vectors 85 | vecx = rt - self.trans 86 | vecy = lb - self.trans 87 | # aspect ratio X/Y 88 | self.aspectXY = np.linalg.norm(vecx) / np.linalg.norm(vecy) 89 | # scale X by vecx 90 | self.scaleX = 1 / np.linalg.norm(vecx) 91 | vecx *= self.scaleX 92 | vecy *= self.scaleX 93 | # rotation on Z axis 94 | angleZ = math.atan2( vecx[1], vecx[0] ) 95 | rotZ = cv2.Rodrigues( np.array([0,0,-angleZ]) )[0].T 96 | # rotation on Y axis 97 | angleY = math.atan2( vecx[2], vecx[0]/math.cos(angleZ) ) 98 | rotY = cv2.Rodrigues( np.array([0,-angleY,0]) )[0].T 99 | rot = rotZ.dot( rotY ) 100 | # apply for vecy 101 | vecy = vecy.dot( rot ) 102 | # rotation on X axis 103 | angleX = math.atan2( vecy[2], vecy[1] ) 104 | rotX = cv2.Rodrigues( np.array([-angleX,0,0]) )[0].T 105 | rot = rot.dot( rotX ) 106 | self.rot = rot 107 | 108 | def transform(self, v, normscale=1, normaspect=True ): 109 | vv = (v - self.trans).dot(self.rot) 110 | if normscale is not None: 111 | vv *= self.scaleX * normscale 112 | if normaspect: 113 | vv[:,1] *= self.aspectXY 114 | return vv 115 | 116 | def inv_transform(self, vv, normscale=1, normaspect=True ): 117 | invrot = np.linalg.inv(self.rot) 118 | v = copy.deepcopy(vv) 119 | if normaspect: 120 | v[:,1] /= self.aspectXY 121 | if normscale is not None: 122 | v /= (self.scaleX * normscale) 123 | return v.dot( invrot ) + self.trans 124 | 125 | 126 | # create Open3D box 127 | # translation (numpy.ndarray[float64[3, 1]]) – A 3D vector to transform the geometry 128 | def createOpen3Dbox(size=1,translation=None, bLineSet=True, color=None): 129 | import open3d as o3d 130 | mesh = o3d.geometry.TriangleMesh.create_box(width=size,height=size,depth=size) 131 | if translation is not None: 132 | mesh.translate(translation,relative=False) 133 | if color is not None: 134 | mesh.paint_uniform_color( color ) 135 | mesh.compute_vertex_normals() 136 | if bLineSet: 137 | mesh = o3d.geometry.LineSet.create_from_triangle_mesh(mesh) 138 | return mesh 139 | -------------------------------------------------------------------------------- /plateaupy/plvisualizer.py: -------------------------------------------------------------------------------- 1 | import open3d as o3d 2 | import numpy as np 3 | import time 4 | import cv2 5 | usleep = lambda x: time.sleep(x/1000000.0) 6 | 7 | class Visualizer3D: 8 | _bKeyPushedValue = -1 9 | _GLFW_KEY_ESCAPE = 256 10 | _GLFW_KEY_SPACE = 32 11 | _GLFW_KEY_TAB = 258 12 | vislist = [] 13 | 14 | def __init__(self, window_name='PLATEAU',width=800,height=600,bgcolor=[1,1,1], camparfile=None, z_far=None): 15 | vis = o3d.visualization.VisualizerWithKeyCallback() 16 | vis.create_window(window_name=window_name, width=width, height=height) 17 | vis.get_render_option().background_color = np.asarray(bgcolor) 18 | vis.get_render_option().mesh_show_back_face = True 19 | #vis.get_render_option().mesh_show_wireframe = True 20 | vis.get_render_option().show_coordinate_frame = True 21 | if z_far is not None: 22 | vis.get_view_control().set_constant_z_far(z_far) 23 | if camparfile is not None: 24 | campar = o3d.io.read_pinhole_camera_parameters( camparfile ) 25 | vis.get_view_control().convert_from_pinhole_camera_parameters( campar ) 26 | else: 27 | campar = None 28 | 29 | _bKeyPushedValue = -1 30 | # add key callbacks 31 | def keycallback_esc(vis): 32 | Visualizer3D._bKeyPushedValue = 27 33 | def keycallback_space(vis): 34 | Visualizer3D._bKeyPushedValue = 32 35 | def keycallback_tab(vis): 36 | Visualizer3D._bKeyPushedValue = 9 37 | vis.register_key_callback( self._GLFW_KEY_ESCAPE, keycallback_esc ) 38 | vis.register_key_callback( self._GLFW_KEY_SPACE, keycallback_space ) 39 | vis.register_key_callback( self._GLFW_KEY_TAB, keycallback_tab ) 40 | 41 | self.recordfile = None 42 | self.writer = None 43 | 44 | self.vis = vis 45 | self.campar = campar 46 | self.vislist.append( self ) 47 | self.clear() 48 | 49 | def destroy(self): 50 | self.stop_recording() 51 | self.vis.destroy_window() 52 | def clear(self, coord=0, bUpdateReset=True): 53 | self.vis.clear_geometries() 54 | def run(self): 55 | self.vis.run() 56 | def update(self): 57 | self.vis.poll_events() 58 | self.vis.update_renderer() 59 | self.record() 60 | 61 | def start_recording(self, filename, fps=30): 62 | self.recordfile = filename + '.avi' 63 | self.writer = None 64 | self.recfps = fps 65 | def stop_recording(self): 66 | self.recordfile = None 67 | if self.writer is not None: 68 | self.writer.release() 69 | self.writer = None 70 | def record(self): 71 | doOpen = False 72 | if self.recordfile is not None and self.writer is None: 73 | doOpen = True 74 | if self.writer is not None or doOpen: 75 | oimg = self.vis.capture_screen_float_buffer( do_render=False ) 76 | img = np.array(oimg) 77 | img = np.array(img*255,dtype=np.uint8) 78 | img = img[:,:,[2,1,0]] 79 | if doOpen: 80 | fourcc = cv2.VideoWriter_fourcc(*'H264') 81 | self.writer = cv2.VideoWriter( self.recordfile, fourcc, self.recfps, (img.shape[1],img.shape[0]) ) 82 | self.writer.write(img) 83 | 84 | @classmethod 85 | def wait(cls,usec=0): 86 | if usec == 0: 87 | usec = -1 88 | td = 10000 # 10msec 89 | waitusec = 0 90 | key = 0 91 | while usec != 0: 92 | for vis in cls.vislist: 93 | vis.update() 94 | if cls._bKeyPushedValue >= 0: 95 | key = cls._bKeyPushedValue 96 | cls._bKeyPushedValue = -1 97 | # wait 98 | if usec >= 0: 99 | waitusec = min( usec, td ) 100 | else: 101 | waitusec = td 102 | usleep( waitusec ) 103 | if usec >= 0: 104 | usec -= waitusec 105 | return key 106 | 107 | 108 | ### usage 109 | if __name__ == '__main__': 110 | from plateaupy.plutils import createOpen3Dbox 111 | viwer = Visualizer3D() 112 | viwer.vis.add_geometry( createOpen3Dbox(bLineSet=False) ) 113 | while True: 114 | key = viwer.wait(1) 115 | if key==27: # ESC 116 | break 117 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | argparse 3 | lxml 4 | opencv-python 5 | opencv-contrib-python 6 | scipy >= 1.5.4 7 | 8 | #open3d >= 0.9.0.0 9 | #triangle 10 | -------------------------------------------------------------------------------- /test/statistics.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import numpy as np 4 | import argparse 5 | import open3d as o3d 6 | 7 | import plateaupy 8 | 9 | ### usage 10 | # python test/statistics.py -loc 533925 -c 11 | 12 | # argparser 13 | parser = argparse.ArgumentParser(description='plateaupy appviewer') 14 | parser.add_argument('-paths','--paths',help='list of paths to CityGML dirctories',default=['../CityGML_01','../CityGML_02'],type=str,nargs='*') 15 | parser.add_argument('-cmd','--cmd',help='special command [locations,codelists,dumpmeta]',default='',type=str) 16 | parser.add_argument('-k','--kind',help='gml kind, -1:all, 0:bldg, 1:dem, 2:luse, 3:tran, 4:brid',default=-1,type=int) 17 | parser.add_argument('-loc','--location',help='location index number',default=-1,type=int) 18 | parser.add_argument('-c','--cache',action='store_true', help='use cache data') 19 | parser.add_argument('-cpath','--cachepath',help='cache directory name',default='cached',type=str) 20 | args = parser.parse_args() 21 | 22 | # scan paths 23 | pl = plateaupy.plparser(args.paths) 24 | 25 | # load 26 | pl.loadFiles( bLoadCache=args.cache, cachedir=args.cachepath, kind=args.kind, location=args.location, bUseLOD2texture=False ) 27 | 28 | 29 | # show Building_usage 30 | print() 31 | print('### Building_usage in ', args.location) 32 | usage = pl.codelists['Building_usage'] 33 | 34 | res = dict() 35 | for key in list(usage.keys()): 36 | res[key] = 0 37 | 38 | for bldg in list(pl.bldg.values()): 39 | for bl in bldg.buildings: 40 | if bl.usage is not None: 41 | if bl.usage in res: 42 | res[bl.usage] += 1 43 | else: 44 | print('no key:', bl.usage ) 45 | total_cnt = 0 46 | for key, cnt in list(res.items()): 47 | if cnt > 0: 48 | print('{} ({}) : {}'.format(key, usage[key], cnt)) 49 | total_cnt += cnt 50 | print('### total :', total_cnt) 51 | 52 | 53 | # show extendedAttribute_key3 54 | print() 55 | print('### extendedAttribute_key3 建物用途コード(都道府県) in ', args.location) 56 | ExtendedAttribute_key3 = pl.codelists['ExtendedAttribute_key3'] 57 | res = dict() 58 | key = '3' 59 | for bldg in list(pl.bldg.values()): 60 | for bl in bldg.buildings: 61 | if key in bl.extendedAttribute: 62 | val = bl.extendedAttribute[key] 63 | if val not in res: 64 | res[val] = 0 65 | res[val] += 1 66 | res = sorted(res.items(), key=lambda x:int(x[0])) 67 | total_cnt = 0 68 | for key, cnt in res: 69 | if cnt > 0: 70 | if key in ExtendedAttribute_key3: 71 | print('{} ({}) : {}'.format(key, ExtendedAttribute_key3[key], cnt)) 72 | else: 73 | print('{} ({}) : {}'.format(key, key, cnt)) 74 | total_cnt += cnt 75 | print('### total :', total_cnt) 76 | 77 | 78 | # show storeysAboveGround, storeysBelowGround 79 | print() 80 | print('### storeysAboveGround, storeysBelowGround in ', args.location) 81 | storeysAboveGround = dict() 82 | storeysBelowGround = dict() 83 | for bldg in list(pl.bldg.values()): 84 | for bl in bldg.buildings: 85 | if bl.storeysAboveGround is not None: 86 | if bl.storeysAboveGround not in storeysAboveGround: 87 | storeysAboveGround[bl.storeysAboveGround] = 0 88 | storeysAboveGround[bl.storeysAboveGround] += 1 89 | if bl.storeysBelowGround is not None: 90 | if bl.storeysBelowGround not in storeysBelowGround: 91 | storeysBelowGround[bl.storeysBelowGround] = 0 92 | storeysBelowGround[bl.storeysBelowGround] += 1 93 | storeysAboveGround = sorted(storeysAboveGround.items(), key=lambda x:int(x[0])) 94 | storeysBelowGround = sorted(storeysBelowGround.items(), key=lambda x:int(x[0])) 95 | print('storeysAboveGround : ', storeysAboveGround) 96 | print('storeysBelowGround : ', storeysBelowGround) 97 | -------------------------------------------------------------------------------- /test/testVerticesTransformer.py: -------------------------------------------------------------------------------- 1 | import plateaupy 2 | import numpy as np 3 | import open3d as o3d 4 | 5 | if __name__ == '__main__': 6 | bLoadCache = False # set it True after the first run 7 | #location = 533925 8 | location = 533945 9 | 10 | # load 11 | pl = plateaupy.plparser(['../CityGML_01','../CityGML_02']) 12 | pl.loadFiles( bLoadCache=bLoadCache, cachedir='cached', kind=1, location=location) 13 | 14 | # target pldem 15 | dem = pl.dem[location] 16 | # target mesh 17 | mesh = dem.meshes[0] 18 | 19 | # get left-top, right-down corners 20 | low, upp = plateaupy.plutils.convertMeshcodeToLatLon(location) 21 | low.append(0) 22 | upp.append(0) 23 | print( 'meshcode {}, LT={}, RB={}'.format( location, low, upp ) ) 24 | 25 | # transform mesh.vertices into [0,1] 26 | vt = plateaupy.plutils.VerticesTransformer( low, upp ) 27 | normvertices = vt.transform( mesh.vertices ) 28 | 29 | # test for inv_transform 30 | if False: 31 | normvertices = vt.inv_transform( normvertices ) 32 | normvertices = vt.transform( normvertices ) 33 | 34 | # replace it to show 35 | mesh.vertices = normvertices 36 | 37 | # show by Open3D 38 | print('presenting..') 39 | window_name = 'test' 40 | width = 640 41 | height = 480 42 | bgcolor = [1,1,1] 43 | vis = o3d.visualization.VisualizerWithKeyCallback() 44 | vis.create_window(window_name=window_name, width=width, height=height) 45 | vis.get_render_option().background_color = np.asarray(bgcolor) 46 | vis.get_render_option().mesh_show_back_face = True 47 | vis.get_render_option().mesh_show_wireframe = True 48 | vis.get_render_option().show_coordinate_frame = True 49 | 50 | meshes = pl.get_Open3D_TriangleMesh()#color=[0.5,0.5,0.5]) 51 | for mesh in meshes: 52 | vis.add_geometry(mesh) 53 | vis.add_geometry( plateaupy.plutils.createOpen3Dbox(size=1) ) 54 | vis.run() 55 | print('ESC to quit.') 56 | vis.poll_events() 57 | vis.update_renderer() 58 | --------------------------------------------------------------------------------