A Python-based research framework for constructing semantic-spatial scene graphs from CityGML building models.
This project is research-first: it focuses on semantic and spatial scene graph construction for indoor/outdoor building context, not on generic GIS conversion. Current conversion baseline is CityGML 2.0.
CityGML contains rich semantic and geometric structure, but many downstream tasks need an explicit graph representation. This framework converts building-related CityGML objects into a scene graph that supports:
- semantic hierarchy extraction
- geometry-aware enrichment
- spatial relation modeling
- graph-ready outputs for Neo4j and analysis workflows
Current stable parsing and scorecard assumptions are aligned to CityGML 2.0 building module structures.
Priority object families:
- Building
- BuildingPart
- Room
- BoundarySurface
- Opening (Door, Window)
- BuildingFurniture
- Parse CityGML building-related objects into explicit internal models
- Normalize geometry metadata (LoD, bbox/centroid pipeline-ready fields)
- Extract semantic and spatial relations
- Construct an internal scene graph
- Persist graph data to Neo4j
- Support future research extensions:
- ontology alignment
- sensor/document linkage
- LLM or agent-based querying
- multimodal scene understanding
- CityGML-specific semantic-spatial graph construction pipeline for building scenes
- Explicit v1 support for BuildingFurniture as a first-class indoor object
- Clear separation between parsing, relation logic, graph construction, and persistence
- Reproducible graph output path for experimentation and paper-oriented analysis
Parses CityGML object families into structured domain representations.
Builds internal node and edge structures from parsed semantics.
Targets relation enrichment for:
CONTAINSCONSISTS_OF_BUILDING_PARTINTERIOR_ROOMOUTER_BUILDING_INSTALLATIONINTERIOR_BUILDING_INSTALLATIONROOM_INSTALLATIONBOUNDED_BYHAS_SURFACE_TYPEHAS_OPENINGHAS_APPEARANCEHAS_SURFACE_DATAAPPLIES_TOINSIDEADJACENT_TOTOUCHESCONNECTSINTERSECTSHOSTED_BYADJACENT_SURFACEATTACHED_TOABOVEBELOW
Current spatial inference baseline:
- Geometry-derived AABB method (
bbox_aabb_v1) - Pipeline uses
Object -> Polygon -> LinearRing -> Positioncoordinates to build per-node bbox - Decision order:
INTERSECTS > TOUCHES > ADJACENT_TO - Detailed spec:
docs/spatial_relation_spec_v1.md - Paper-oriented v2 algorithm notes:
docs/spatial_relation_v2_algorithm_notes.md/docs/spatial_relation_v2_algorithm_notes_ko.md
Provides a dedicated storage layer (storage/neo4j) so DB integration stays decoupled from core graph logic.
- Python 3.10+
- pip
- Optional: Neo4j 5.x for DB persistence experiments
- CityGML 2.0 input files for v1 baseline runs (3.0 expansion path reserved in config)
Dependencies are defined in pyproject.toml.
git clone https://github.com/<YOUR_ID>/3DCitySG.git
cd 3DCitySGWindows PowerShell:
py -3.11 -m venv .venv
. .venv\Scripts\Activate.ps1macOS/Linux:
python3 -m venv .venv
source .venv/bin/activatepython -m pip install -U pip
pip install -e .Before running, set your local settings in
configs/default.yaml (or pass your own config file with --config).
Set Neo4j values from your own environment:
neo4j:
uri: bolt://<host>:<port>
username: <your_username>
password: <your_password>
database: <your_database>
batch_size: 5000Example (local default install): bolt://localhost:7687, neo4j, <your-password>, neo4j.
For large imports, tune batch_size (e.g., 2000~10000) based on memory and throughput.
Set spatial inference thresholds for experiments:
spatial:
touch_epsilon: 0.05
adjacent_epsilon: 0.50
intersection_epsilon: 0.000001These values control TOUCHES/ADJACENT_TO/INTERSECTS decisions in import-time spatial relation extraction.
Command source of truth: docs/command_cheatsheet.md
python scripts/run_import.py --input data/input/sample_citygml_v2.gmlpython scripts/run_import.py --input "data/input/fzk_haus_lod2_v2.gml" --output data/output/my_import.jsonpython scripts/run_import.py --input "data/input/fzk_haus_lod2_v2.gml" --output data/output/my_import.json --to-neo4j --config configs/default.yamlpython -m citygml_sg.app.cli import --input "data/input/fzk_haus_lod2_v2.gml" --output data/output/my_import.jsonpython -m citygml_sg.app.cli import --input "data/input/fzk_haus_lod2_v2.gml" --output data/output/my_import.json --to-neo4j --config configs/default.yamlpython scripts/benchmark_queries.py --config configs/default.yaml --output data/output/benchmark_report.json --warmup 1 --repeat 3Built-in benchmark set now includes:
- baseline tier (
B1..B7): stable structural counts - hard tier (
H1..H5): sparse/high-selectivity spatial checks - scenario tier (
S1..S5): human-style query patterns (including room-to-room path-like query)
python -m citygml_sg.app.cli benchmark --config configs/default.yaml --output data/output/benchmark_report.json --warmup 1 --repeat 3python scripts/profile_import_runs.py --input "data/input/fzk_haus_lod2_v2.gml" --runs 3 --config configs/default.yamlpython scripts/profile_import_runs.py --input "data/input/fzk_haus_lod2_v2.gml" --runs 3 --to-neo4j --config configs/default.yamlpython scripts/check_large_scale_baseline.py --baseline configs/baselines/201dong_v1_baseline.json --import-summary data/output/E-TYPE_201dong_after_boundarytype.json --profile-report data/output/import_profile_report_201dong_after_boundarytype.jsonpython scripts/refresh_latest_reports.py --input "data/input/(210812)E-TYPE_201dong-IFC4.gml" --config configs/default.yamlTimestamped outputs are saved with input-derived dataset tag, for example:
data/output/e_type_201dong_ifc4__import_summary_YYYYMMDD_HHMMSS.jsondata/output/e_type_201dong_ifc4__benchmark_report_YYYYMMDD_HHMMSS.jsondata/output/e_type_201dong_ifc4__import_profile_report_YYYYMMDD_HHMMSS.jsondata/output/e_type_201dong_ifc4__profiling_YYYYMMDD_HHMMSS/
Force Neo4j sync before benchmark:
python scripts/refresh_latest_reports.py --input "data/input/(210812)E-TYPE_201dong-IFC4.gml" --config configs/default.yaml --to-neo4jOptional skips:
python scripts/refresh_latest_reports.py --skip-profile --skip-baselineKeep timestamped files only (do not overwrite defaults):
python scripts/refresh_latest_reports.py --no-promote-defaultsOverride dataset tag manually:
python scripts/refresh_latest_reports.py --input "<input.gml>" --dataset-tag "building_a"Generated output:
- default:
data/output/import_summary.json - custom: path passed via
--output
Runtime log now includes in-progress timeline, for example:
[Timeline] [1/6] [--------------------------] parse_xml START
[Timeline] [1/6] [####----------------------] parse_xml DONE (2.184s)
[Timeline] [2/6] [####----------------------] collect_semantics START
...
When --to-neo4j is enabled, both node and edge export progress are printed as percent bars.
- v1 default target: CityGML 2.0
- Config field:
project.citygml_version(inconfigs/default.yaml) - Code constants:
citygml_sg.config.DEFAULT_CITYGML_VERSIONcitygml_sg.config.SUPPORTED_CITYGML_VERSIONS
3.0is reserved for extension; do not switch default baseline without explicit scope update.
- Score formula:
overall = 0.40 * node + 0.30 * relation + 0.30 * property - Fairness rule: expected totals are computed from currently supported extraction scope, not every possible CityGML tag.
- Spatial diagnostics are also exported in scorecard:
spatial_coveragespatial_precision_sanityspatial_pair_statsspatial_pair_family_scores
- Detailed criteria and interpretation guide:
docs/evaluation_scorecard.md
- v1 relation spec (priority, thresholds, metadata, query criteria):
docs/spatial_relation_spec_v1.md
- Guide (why/how/policy):
docs/regression_testing.md - Quick run:
pytest tests/test_pipeline_regression.pypytest tests/test_spatial_priority.pypytest tests/test_spatial_relation_pairs.py- Placeholder modules not wired in v1 runtime path are tracked in:
docs/module_stubs.md
- Development summary:
docs/development_summary.md - Query benchmark guide:
docs/query_benchmark_guide.md - Feature implementation guide:
docs/feature_implementation_guide.md - Testing and scoring guide:
docs/testing_and_scoring_guide.md - Performance profiling guide:
docs/performance_profiling_guide.md - v1 measurement runbook:
docs/v1_measurement_runbook.md - Command cheatsheet:
docs/command_cheatsheet.md - Command cheatsheet (Korean):
docs/command_cheatsheet_ko.md - Reading summary (Korean):
docs/reading_summary_ko.md - Experiment result record template:
docs/experiment_results.md - Experiment result history (archive):
docs/experiment_results_history.md - Dataset comparison tracker:
docs/dataset_result_comparison.md - Dataset comparison history (archive):
docs/dataset_result_history.md
Note: local Korean copies follow the _ko.md suffix and are ignored by Git.
- Building
- BuildingPart
- Room
- BoundarySurface
- Opening, Door, Window
- BuildingFurniture
- full IFC/BIM conversion
- point cloud and CV pipelines
- UI/dashboard development
- cloud/microservice deployment
- production-scale optimization
BuildingFurniture is treated as mandatory in v1, not optional.
It strengthens indoor scene semantics and enables future reasoning on:
- room interior composition
- furniture containment
- furniture-to-boundary relations
- accessibility/connectivity analysis
src/citygml_sg/
|-- app/
|-- config/
|-- domain/
|-- parsers/
|-- extractors/
|-- modules/
| |-- building/
| |-- building_part/
| |-- room/
| |-- boundary_surface/
| |-- opening/
| `-- building_furniture/
|-- relations/
|-- graph/
|-- storage/
| `-- neo4j/
`-- utils/
- Added import runner with explicit file I/O arguments:
python scripts/run_import.py --input <path> --output <path>
- Added module-level parsing hooks for:
- Building, BuildingPart, Room, BoundarySurface, Opening (Door/Window), BuildingFurniture
- Added schema-alignment object parsing:
- BuildingInstallation, IntBuildingInstallation, Address
- Added BoundarySurface subtype coverage:
- OuterCeilingSurface, OuterFloorSurface
- Added node property enrichment from CityGML content:
gml:name->gml_name,gml_name_allgen:*Attribute-> flattened node properties (e.g.,attr_grossplannedarea)
- Added graph build path that currently generates:
CONTAINS(Building/BuildingPart/Room hierarchy and Room->Furniture)CONSISTS_OF_BUILDING_PART(Building/BuildingPart->BuildingPart)INTERIOR_ROOM(Building/BuildingPart->Room)OUTER_BUILDING_INSTALLATION(Building/BuildingPart->BuildingInstallation)INTERIOR_BUILDING_INSTALLATION(Building/BuildingPart->IntBuildingInstallation)ROOM_INSTALLATION(Room->IntBuildingInstallation)BOUNDED_BY(Building/BuildingPart/Room/Installation->BoundarySurface)HAS_SURFACE_TYPE(BoundarySurface->BoundarySurfaceType)HAS_OPENING(BoundarySurface->Opening)HAS_APPEARANCE/HAS_SURFACE_DATA/APPLIES_TO(Appearance subgraph)CONNECTS(Opening(Door)->Room)HOSTED_BY(Opening->BoundarySurface)ADJACENT_SURFACE(BoundarySurface<->BoundarySurface)ATTACHED_TO(Furniture->BoundarySurface, derived from TOUCHES)ABOVE/BELOW(object vertical order: Furniture/Door/Window)INSIDE(Furniture->Room)
- Added geometry subgraph extraction:
- object ->
HAS_GEOMETRY-> Polygon - Polygon ->
HAS_RING-> LinearRing - LinearRing ->
HAS_POS-> Position(x, y, z, order)
- object ->
- Added LoD geometry structure preservation:
- object ->
HAS_LOD_GEOMETRY-> Solid/MultiSurface/MultiCurve - Solid/MultiSurface ->
HAS_GEOMETRY_MEMBER-> Polygon
- object ->
- Added JSON export for parsed graph summary:
- node counts, relation counts, node list, edge list
- Added import execution timeline logs:
- per-stage START/DONE/SKIP messages with progress bar
- final stage timeline with duration bars
- Added sample CityGML input for quick smoke tests:
data/input/sample_citygml_v2.gml
- Keep parser, relation extraction, graph builder, and storage layers decoupled
- Prefer typed, testable, explicit modules over deep abstraction
- Focus on research iteration speed and reproducibility