Working with 3D city models in Python

Balázs Dukai @BalazsDukai, FOSS4G 2019

Tweet #CityJSON

3D geoinformation research group, TU Delft, Netherlands

Repo of this talk: https://github.com/balazsdukai/foss4g2019

3D + city + model ?

Semantic models

Useful for urban analysis

García-Sánchez, C., van Beeck, J., Gorlé, C., Predictive Large Eddy Simulations for Urban Flows: Challenges and Opportunities, Building and Environment, 139, 146-156, 2018.

And many more...

3d city model applications

...mostly just production of the models

many available, but who uses them? For more than visualisation?

open 3d city models

In truth, 3D CMs are a bit difficult to work with

Our built environment is complex, and the objects are complex too

Software are lagging behind

  • not many software supports 3D city models

  • if they do, mostly propietary data model and format

  • large, "eterprise"-type applications (think Esri, FME, Bentley ... )

  • few tools accessible for the individual developer / hobbyist

cityjson logo

Key concepts of CityJSON

  • simple, as in easy to implement
  • designed with programmers in mind
  • fully developed in the open
  • flattened hierarchy of objects
  • implementation first

GitHub Issues

JSON-based encoding of the CityGML data model

Compression ~6x over CityGML

cityjson paper

Let's have a look-see, shall we?

An empty CityJSON file

A CityObject

Geometry

  • boundaries definition uses vertex indices (inspired by Wavefront OBJ)
  • We have a vertex list at the root of the document
  • Vertices are not repeated (unlike Simple Features)
  • semantics are linked to the boundary surfaces
In [1]:
import json
import os

path = os.path.join('data', 'rotterdam_subset.json')
with open(path) as fin:
    cm = json.loads(fin.read())
    
print(f"There are {len(cm['CityObjects'])} CityObjects")

# list all IDs
for id in cm['CityObjects']:
    print(id, "\t")
There are 16 CityObjects
{C9D4A5CF-094A-47DA-97E4-4A3BFD75D3AE} 	
{71B60053-BC28-404D-BAB9-8A642AAC0CF4} 	
{6271F75F-E8D8-4EE4-AC46-9DB02771A031} 	
{DE77E78F-B110-43D2-A55C-8B61911192DE} 	
{19935DFC-F7B3-4D6E-92DD-C48EE1D1519A} 	
{953BC999-2F92-4B38-95CF-218F7E05AFA9} 	
{8D716FDE-18DD-4FB5-AB06-9D207377240E} 	
{C6AAF95B-8C09-4130-AB4D-6777A2A18A2E} 	
{72390BDE-903C-4C8C-8A3F-2DF5647CD9B4} 	
{8244B286-63E2-436E-9D4E-169B8ACFE9D0} 	
{87316D28-7574-4763-B9CE-BF6A2DF8092C} 	
{CD98680D-A8DD-4106-A18E-15EE2A908D75} 	
{64A9018E-4F56-47CD-941F-43F6F0C4285B} 	
{459F183A-D0C2-4F8A-8B5F-C498EFDE366D} 	
{237D41CC-991E-4308-8986-42ABFB4F7431} 	
{23D8CA22-0C82-4453-A11E-B3F2B3116DB4} 	
  • Working with a CityJSON file is straightforward. One can open it with the standard library and get going.
  • But you need to know the schema well.
  • And you need to write everything from scratch.

cjio

cjio has a (quite) stable CLI

$ cjio city_model.json reproject 2056 export --format glb /out/model.glb

and an experimental API

from cjio import cityjson

cm = cityjson.load('city_model.json')

cm.get_cityobjects(type='building')

pip install cjio

pip install git+https://github.com/tudelft3d/cjio@develop

cjio's CLI

In [2]:
! cjio --help
Usage: cjio [OPTIONS] INPUT COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...

  Process and manipulate a CityJSON file, and allow different outputs. The
  different operators can be chained to perform several processing in one
  step, the CityJSON model goes through the different operators.

  To get help on specific command, eg for 'validate':

      cjio validate --help

  Usage examples:

      cjio example.json info validate
      cjio example.json assign_epsg 7145 remove_textures export output.obj
      cjio example.json subset --id house12 save out.json

Options:
  --version                Show the version and exit.
  --ignore_duplicate_keys  Load a CityJSON file even if some City Objects have
                           the same IDs (technically invalid file)
  --help                   Show this message and exit.

Commands:
  assign_epsg                Assign a (new) EPSG.
  clean                      Clean = remove_duplicate_vertices +...
  compress                   Compress a CityJSON file, ie stores its...
  decompress                 Decompress a CityJSON file, ie remove the...
  export                     Export the CityJSON to another format.
  extract_lod                Extract only one LoD for a dataset.
  info                       Output info in simple JSON.
  locate_textures            Output the location of the texture files.
  merge                      Merge the current CityJSON with others.
  partition                  Partition the city model into tiles.
  remove_duplicate_vertices  Remove duplicate vertices a CityJSON file.
  remove_materials           Remove all materials from a CityJSON file.
  remove_orphan_vertices     Remove orphan vertices a CityJSON file.
  remove_textures            Remove all textures from a CityJSON file.
  reproject                  Reproject the CityJSON to a new EPSG.
  save                       Save the city model to a CityJSON file.
  subset                     Create a subset of a CityJSON file.
  translate                  Translate the file by its (-minx, -miny,...
  update_bbox                Update the bbox of a CityJSON file.
  update_textures            Update the location of the texture files.
  upgrade_version            Upgrade the CityJSON to the latest version.
  validate                   Validate the CityJSON file: (1) against its...
In [3]:
! cjio data/rotterdam_subset.json info
Parsing data/rotterdam_subset.json
{
  "cityjson_version": "1.0",
  "epsg": 7415,
  "bbox": [
    90454.18900000001,
    435614.88,
    0.0,
    91002.41900000001,
    436048.217,
    18.29
  ],
  "transform/compressed": true,
  "cityobjects_total": 16,
  "cityobjects_present": [
    "Building"
  ],
  "materials": false,
  "textures": true
}
In [5]:
! cjio data/rotterdam_subset.json \
    subset --exclude --id "{CD98680D-A8DD-4106-A18E-15EE2A908D75}" \
    merge data/rotterdam_one.json \
    reproject 2056 \
    save data/test_rotterdam.json
Parsing data/rotterdam_subset.json
Subset of CityJSON
Merging files
Reproject to EPSG:2056
  [####################################]  100%          
Saving CityJSON to a file /home/balazs/Reports/talk_cjio_foss4g_2019/data/test_rotterdam.json
  • The CLI was first, no plans for API

  • Works with whole city model only

  • Functions for the CLI work with the JSON directly, passing it along

  • Simple and effective architecture

cjio's API

Allow read --> explore --> modify --> write iteration

Work with CityObjects and their parts

Functions for common operations

Inspired by the tidyverse from the R ecosystem

Load a CityJSON

In [7]:
path = os.path.join('data', 'rotterdam_subset.json')

cm = cityjson.load(path)

print(type(cm))
<class 'cjio.cityjson.CityJSON'>

Getting objects from the model

Get CityObjects by their type, or a list of types. Also by their IDs.

Note that get_cityobjects() == cm.cityobjects

In [10]:
buildings = cm.get_cityobjects(type='building')

# both Building and BuildingPart objects
buildings_parts = cm.get_cityobjects(type=['building', 'buildingpart'])

r_ids = ['{C9D4A5CF-094A-47DA-97E4-4A3BFD75D3AE}',
         '{6271F75F-E8D8-4EE4-AC46-9DB02771A031}']
buildings_ids = cm.get_cityobjects(id=r_ids)

Analysing CityModels

In [25]:
path = os.path.join('data', 'zurich.json')
zurich = cityjson.load(path, transform=True)

A simple geometry function

In [26]:
def compute_footprint_area(co):
    """Compute the area of the footprint"""
    footprint_area = 0
    for geom in co.geometry:
        
        # only LoD2 (or higher) objects have semantic surfaces
        if geom.lod >= 2.0:
            footprints = geom.get_surfaces(type='groundsurface')
            
            # there can be many surfaces with label 'groundsurface'
            for i,f in footprints.items():
                for multisurface in geom.get_surface_boundaries(f):
                    for surface in multisurface:
                        
                        # cast to Shapely polygon
                        shapely_poly = Polygon(surface)
                        footprint_area += shapely_poly.area
                        
    return footprint_area

Compute new attributes

In [27]:
for co_id, co in zurich.cityobjects.items():
    co.attributes['nr_vertices'] = len(co.get_vertices())
    co.attributes['fp_area'] = compute_footprint_area(co)
    zurich.cityobjects[co_id] = co
In [29]:
df = zurich.to_dataframe()
df.head()
Out[29]:
creationDate Geomtype nr_vertices fp_area class Herkunft QualitaetStatus FileCreationDate Region GebaeudeStatus
UUID_93fc5bae-4446-4336-9ff8-6679ebfdfde3 2017-01-23 1.0 24 65.209763 NaN NaN NaN NaN NaN NaN
UUID_c9884c4e-1cac-47f5-b88b-6fb074c0ae50 2017-01-23 NaN 0 0.000000 BB01 EE_LB_2007 1.0 2012-02-23 2.0 1.0
UUID_a4a09780-153f-4385-ad19-3a92a6c4eec4 2017-01-23 1.0 38 20.784309 NaN NaN NaN NaN NaN NaN
UUID_ba0bb815-5276-4e35-b4c1-878cbf6ba934 2017-01-23 NaN 0 0.000000 BB07 EE_LB_2007 1.0 2012-02-23 2.0 1.0
UUID_bb1835bc-7437-453f-ac08-885de0503aaa 2017-01-23 1.0 87 69.363823 NaN NaN NaN NaN NaN NaN
In [33]:
%matplotlib notebook
model = cluster.DBSCAN(eps=0.2).fit(df_logtransform)

plot_model_results(model, df_logtransform)

Save the results back to CityJSON

In [35]:
for co_id, co in zurich.cityobjects.items():
    if co_id in df_subset.index:
        ml_results = dict(df_subset.loc[co_id])
    else:
        ml_results = {'nr_vertices': 'nan', 'fp_area': 'nan', 'dbscan': 'nan'}
    new_attrs = {**co.attributes, **ml_results}
    co.attributes = new_attrs
    zurich.cityobjects[co_id] = co
In [36]:
path_out = os.path.join('data', 'zurich_output.json')
cityjson.save(zurich, path_out)

And view the results in QGIS again

Other software

Online CityJSON viewer

QGIS plugin

Azul

Full conversion CityGML <--> CityJSON

Thank you!

Balázs Dukai

b.dukai@tudelft.nl

@BalazsDukai

Repo of this talk: https://github.com/balazsdukai/foss4g2019

cityjson.org

viewer.cityjson.org

QGIS plugin: github.com/tudelft3d/cityjson-qgis-plugin

Azul – CityJSON viewer on Mac – check the AppStore

cjio: github.com/tudelft3d/cjio & tudelft3d.github.io/cjio/