HiWi Challenge, MDS Lab, Universität Rostock. This project implements a pipeline that transforms an egocentric RGB-D sequence and a building Information Model (BIM/IFC) prior into a 3D semantic scene graph
$G = (V, E)$ . The graph comprises object nodes$V_{obj}$ derived from open-vocabulary detection and structural nodes$V_{ifc}$ extracted from the IFC prior, linked by spatial and hierarchical edges$E$ .
This README is the primary deliverable. It documents what the pipeline does, why each architectural choice was made, what the data forced us to change, and where the limits are. The reasoning here is the core contribution of this work while the code in src/rgbdsg/ is graded but secondary in importance.
- 1. Experimental Results & Performance: High-level overview of pipeline outputs, benchmarks, and performance metrics.
- 2. Pipeline architecture: The core technical stages from raw data ingestion to scene graph construction.
- 3. Methodology and architectural justifications: Detailed reasoning behind architectural decisions, coordinate conventions, and data fallbacks.
- 4. Repository layout: A guide to the codebase structure and module responsibilities.
- 5. How to reproduce: Step-by-step instructions for running the pipeline locally using Make and Docker.
- 6. Limitations and concrete failure modes: Analysis of edge cases, hardware constraints, and geometric errors.
- 7. How this should ideally work: Proposed future improvements for a production-grade IFC integration.
- 8. Related work: Academic literature and frameworks that inspired the pipeline design.
- 9. AI-assistance disclosure: Transparency regarding the use of LLMs during development.
- 10. Reproducibility & data: Information regarding datasets, weights, and tests.
The pipeline runs end-to-end on both datasets, every frame, with duplicate-object merging.
For each scene, the pipeline emits a semantic 3D scene graph first (the canonical output), then a BEV occupancy projection of the same graph, plus the raw graph in GraphML + JSON:
| File | Contents |
|---|---|
<scene>.graphml |
NetworkX MultiDiGraph in GraphML; interoperable with yEd / Gephi / Cytoscape |
<scene>.json |
Node-link JSON dump; direct text inspection |
<scene>_objects.json |
Per-object summary (centroid, bbox, label distribution, observation count) |
<scene>_graph_3d.png |
3D matplotlib rendering of the scene graph in world coordinates |
<scene>_graph_3d.html |
Interactive Plotly 3D view (rotate / zoom in any browser) |
<scene>_tree.png |
2D hierarchical tree visualization (Building → Storey → Room → Object) |
<scene>_bev.png |
Top-down BEV projection of the same graph over the BEV-occupancy footprint |
3D semantic scene graph (Armeni et al. [1] 4-layer: Building → Storey → Room → Object/Fixture/Camera), with every node and edge geometry-accurate in the world frame:
BEV-occupancy view: the same NetworkX graph projected top-down onto the BEV occupancy footprint that produced the room polygons:
Hierarchical tree view: a 2D node-link diagram showing the semantic containment and object-to-object relationships (excluding structural entities (IFC) for clarity):
| Pipeline stage | Wall-time | Output |
|---|---|---|
| Grounding DINO (5 keyframes) | ~5 s | 19 raw box detections |
| SAM 2.1 video propagation (160 frames, stateful) | ~2 min | masks for all obj_ids over 160 frames |
| Multi-view fusion + dedup (centroid + bbox + voxel-IoU) | ~3 s | 7 unique physical objects (12 duplicates merged) |
Canonical IfcOpenShell extraction of BasicHouse.ifc |
~6 s | 125 structural entities (IFC), 2 storeys ("Floor 0", "Floor 1"), 8 door↔wall fills_opening_in pairs |
Wall-rasterised BEV rooms (IfcWall faces + IfcDoor sealing) |
<1 s | 5 room polygons (11.2, 9.2, 12.3, 6.3, 3.5 m²) |
| Graph construction (Armeni 4-layer + portals + spatial heuristics) | <1 s | 145 nodes, 271 edges |
| 3D scene-graph PNG + interactive Plotly HTML | ~2 s | *_graph_3d.png + *_graph_3d.html |
| BEV-occupancy projection | ~1 s | *_bev.png |
Node breakdown: 1 building · 2 storeys · 5 rooms · 7 objects · 125 IFC structural entities · 5 cameras (one per keyframe).
Edge breakdown (271 total):
- Hierarchical containment (Building → Storey → Room → Object/Entity/Camera): 163
contains - Object-level spatial heuristics (Task A): 21
nearest· 6near· 6next_to· 4aligned_with - Object↔entity portal relations: 8
fills_opening_in(canonical IfcRelFillsElement→IfcRelVoidsElement) · 6connects(heuristic room-to-room portals via IfcDoor proximity) - Peer (sibling) relations per Armeni et al. [1]: 41
same_room(objects sharing a room) · 9same_storey(objects sharing a storey)
Hierarchical tree view:
| Pipeline stage | Wall-time | Output |
|---|---|---|
| Grounding DINO (8 auto-spaced keyframes, scene prompt, lower thresholds) | ~8 s | 4 raw detections |
| SAM 2.1 video propagation (383 × 2 sweeps, CPU offload + state reset) | ~28 min | masks over all 383 frames |
| Multi-view fusion + dedup | ~5 s | 3 unique objects |
Canonical IfcOpenShell extraction of synagoge_final.ifc |
~50 s | 250 structural entities (IFC), 9 named storeys ("Level 1" through "Roof"), 5 door↔wall fills_opening_in pairs |
| Wall-rasterised BEV rooms | ~1 s | 2 room polygons (18.7 m², 70.6 m²) |
| Graph construction (Armeni et al. [1] 4-layer + portals + spatial heuristics) | <1 s | 273 nodes, 392 edges |
| 3D scene-graph PNG + interactive Plotly HTML | ~2 s | *_graph_3d.png + *_graph_3d.html |
| BEV-occupancy projection | ~1 s | *_bev.png |
Node breakdown: 1 building · 9 storeys · 2 rooms · 3 objects · 250 IFC structural entities · 8 cameras. Storeys named exactly as Revit labelled them: Level 1, Level 2, Parapeito, Level 3, vault Pilar profile, Smaller vaults, Roof edge, Higher Vaults, Roof.
Edge breakdown (392 total):
- Hierarchical containment: 381
contains(Building → Storey → Room/Object/Entity/Camera; Room → Object) - Object-level spatial heuristics: 6
nearest - IFC portal relations: 5
fills_opening_in - Peer relations: 0
same_room/same_storey(objects are sparse, as only 3 were detected, spread across separate storeys)
The synagogue is a textureless flat-shaded synthetic render of a vaulted
interior with almost no furniture, mostly architectural geometry. Grounding
DINO finds few non-structural objects because there are few; the value of
the pipeline on this scene is the 9-storey named hierarchy with all
250 structural entities (IFC) correctly parented to their canonical IFC storey via
IfcRelContainedInSpatialStructure, the 5 canonical door↔wall portal
relations, and the camera-trajectory thread through the multi-storey
graph.
Both runs use the same code path with the same defaults; the
synagoge run is steered only by data/synagoge_with_pc/prompt.txt (its
architectural vocabulary) plus --auto_keyframes 8 (because 383 frames
benefit from more anchors). No dataset-specific code branches.
Important Data Caveats:
- No
IfcSpace: The data has no predefined rooms in the IFC file. We synthesise room polygons via point-cloud BEV morphology (src/rgbdsg/ifc/rooms_bev.py).
flowchart TB
subgraph INPUT[" "]
direction LR
RGB[("RGB frames<br>1024×768 PNG")]
DEPTH[("EXR depth<br>fp16 metric Z")]
POSE[("4×4 T_wc poses<br>per frame")]
PC[("scene.ply<br>~240k pts")]
OBJ[("_ifcgeom_scene.obj<br>+ labels.json")]
end
subgraph IO["src/rgbdsg/io/"]
RGBD[RGBDSequence]
end
RGB --> RGBD
DEPTH --> RGBD
POSE --> RGBD
subgraph DET["src/rgbdsg/detection/"]
GDINO["Grounding DINO<br/>open-vocab 2D boxes<br/>(swin-base, GPU)"]
SAM["SAM 2.1 video predictor<br/>mask propagation<br/>(Hiera-Large, GPU)"]
end
RGBD -->|keyframes| GDINO
GDINO -->|box prompts| SAM
RGBD -->|all frames| SAM
subgraph FUSE["src/rgbdsg/fusion/"]
F[multi-view aggregation:<br/>masked depth → world points<br/>+ outlier filter]
end
SAM -->|per-frame masks| F
RGBD -->|depth + pose| F
subgraph IFC["src/rgbdsg/ifc/"]
IO1[load_ifc_entities<br/>OBJ + labels.json]
ROOMS[synthesize_rooms<br/>BEV occupancy]
end
OBJ --> IO1
PC --> ROOMS
IO1 -->|IfcSlab| ROOMS
subgraph GRAPH["src/rgbdsg/graph/"]
G["NetworkX MultiDiGraph<br/>nodes: object | IFC entity | room<br/>edges: nearest, near, above, contains"]
end
F -->|ObjectInstance| G
IO1 -->|IFCEntity| G
ROOMS -->|Room polygons| G
G --> OUT[("outputs/<br/>graphml + JSON<br/>3D PNG + Plotly HTML<br/>BEV PNG")]
run_pipeline.py wires these together; each module is independently
unit-testable (see tests/).
The graph itself follows the canonical multi-layer 3D-scene-graph structure formalised in 3D Scene Graph [1] and the per-node 3D properties used in SceneGraphFusion [3]; see §2.1 below for the schema.
The pipeline's canonical output is a NetworkX MultiDiGraph with six
node types organised into the Armeni et al. [1] four-layer hierarchy plus a peer
structural layer and per-keyframe camera nodes:
┌─────────────┐
│ building │ one root node per scene
└──────┬──────┘
│ contains
┌──────▼──────┐
│ storey │ from IfcBuildingStorey
└──────┬──────┘
│ contains
┌──────────┼──────────┬────────────┬──────────┐
▼ ▼ ▼ ▼ ▼
room object ifc_entity camera (other storey)
(BEV poly) (Task A) (IfcDoor/ (one per
┐ z range ┐ from IfcWall*/ keyframe,
│ │ GDINO + IfcSlab/... with pose)
│ │ SAM 2 canonical)
└─ contains ─► object / ifc_entity
Each node carries 3D properties in the SceneGraphFusion sense, and the same ones Armeni et al. [1] per-element annotations specify:
| Attribute | object | ifc_entity | room | storey | camera |
|---|---|---|---|---|---|
centroid (xyz, m) |
✓ | ✓ | from polygon | mid-z | from pose translation |
bbox_min, bbox_max |
✓ | ✓ | polygon AABB | Z extents | — |
bbox_size_m |
✓ | ✓ | — | — | — |
volume_m3 (AABB volume) |
✓ | ✓ | — | — | — |
max_length_m |
✓ | ✓ | — | — | — |
label / ifc_class / name |
✓ | ✓ | — | ✓ (from IFC) | — |
polygon_xy |
— | — | ✓ | — | — |
position, rotation_3x3, fov_deg |
— | — | — | — | ✓ |
The edges fall into three families, all explicitly named:
-
Hierarchical containment:
contains. Building → Storey → Room → Object / Fixture / Camera. Replicates Armeni et al.'s [1] hierarchy edges exactly (we add astoreyintermediate level to handle multi-floor IFC files). -
Spatial heuristics on objects:
nearest,near,next_to,above,below,aligned_with. Direct implementations of the "spatial heuristics (e.g., proximity, nearest-neighbor)" the challenge PDF calls for in Task A. These are calculated efficiently using a 3D KD-Tree (scipy.spatial.cKDTree) built over object centroids to avoid$O(N^2)$ brute-force distance queries. -
IFC structural relations:
fills_opening_inandconnects.fills_opening_inis the canonical IFC topologyIfcDoor → IfcRelFillsElement → IfcOpeningElement → IfcRelVoidsElement → IfcWall*extracted directly viaifcopenshell(seesrc/rgbdsg/ifc/from_ifc_file.py).connectsis the heuristic room-to-room portal edge for cases where the IFC doesn't carryIfcSpace. -
Peer / sibling edges:
same_room,same_storey. Objects sharing a parent get a direct sibling link [1]. We deliberately exclude entity↔entity sibling pairs from these edges to avoid the O(N²) blow-up (125 entities per storey would emit ~7 700 redundant sibling edges; thecontainschain already establishes co-storey membership).
The pipeline renders this graph in three views, in this order:
- 3D view (
*_graph_3d.png+ interactive*_graph_3d.html): every node placed at its world-frame 3D centroid; edges as colored line segments. This is the canonical output. - BEV view (
*_bev.png): top-down projection of the same graph onto the BEV-occupancy footprint, with IFC slabs / walls / windows / doors drawn as 2D rectangles and the camera trajectory as a polyline. The BEV view shares the NetworkX graph 1:1 with the 3D view; it's a 2D projection, not a separate analysis. - Hierarchical tree view (
*_tree.png): a 2D node-link diagram that explicitly visualises the parent-child containment hierarchy and peer relationships. To maintain legibility,ifc_entityandcameranodes are excluded from this specific view.
The hierarchical tree view is a 2D projection of a highly connected semantic graph. Mapping a non-planar scene graph (which includes both vertical containment and horizontal spatial heuristics) to a static 2D layout is a known visualization challenge.
Why it looks cluttered:
- Horizontal Overlap: Relationships like
nearestandnext_tooften cross the verticalcontainslines, leading to a "messy" appearance in dense scenes. - Scale Disparity: The number of objects varies significantly
across rooms, forcing the
multipartite_layoutto compress certain branches.
This view is intended as a logical debugging tool rather than a polished architectural diagram. It can be improved in future work through custom force-directed layouts or interactive pruning of secondary edges.
The challenge brief says scientific reasoning is 50 % of the grade. Every non-obvious choice is documented here with the alternatives I considered and why I rejected them. Where the data forced a deviation from the brief, that's flagged explicitly.
Choice: Grounding DINO (swin-base) via HuggingFace transformers.
Why this:
- Open-vocabulary: phrases like "chair. table. lamp. door." yield boxes without any per-class training. Indoor scenes have a long tail of object categories (cabinets, mirrors, picture frames, ...) that no closed-set detector covers, and we cannot fine-tune in this two-week window.
- swin-base is the strongest publicly-available open-vocab detector with a permissive license (Apache 2.0); on LVIS-mini benchmarks it beats swin-tiny by ~5–7 mAP.
Alternatives considered and rejected:
| Alternative | Why not |
|---|---|
| YOLO-World | Faster but materially worse on novel/indoor furniture categories. |
| OWLv2 | Comparable quality but worse text-box alignment (text_threshold is brittler), and HuggingFace integration less mature. |
| Closed-set detectors (e.g. Detic, COCO-trained) | Their vocabularies miss objects that exist in IFC entities (cabinet, sink, refrigerator); fine-tuning would consume the entire budget. |
| Original IDEA-Research repo | Builds a CUDA C++ extension from source, which can be a recipe for environment hell. The HF integration loads the same weights without the build step. |
Choice: SAM 2.1 Hiera-Large, run in video mode with Grounding DINO's boxes as prompts on a small set of keyframes.
Why this is the architectural lever: The hardest sub-problem in 3D scene-graph generation from RGB-D is multi-view object association, which means recognising that the chair in frame 0 and the chair in frame 80 are the same physical object, despite viewpoint change, partial occlusion, and lighting. Visual SLAM systems spend substantial complexity on this with 3D-centroid Hungarian matching, appearance descriptors, and ad-hoc fusion thresholds.
SAM 2 was designed precisely to solve this problem at the 2D level, with
its memory bank propagating mask identity across frames. We feed it the
Grounding DINO boxes as prompts on keyframes (every 40 frames) and let SAM
2's video predictor handle propagation. The same obj_id followed across
all 160 frames means we get cross-frame association for free, at the cost
of ~1.55 s/frame.
Alternatives considered and rejected:
| Alternative | Why not |
|---|---|
| SAM 2 in image mode + custom 3D centroid tracker | Reintroduces the multi-view association problem we're using SAM 2 to solve. Loses the explicit story. |
| Per-frame Grounding DINO + Hungarian on 3D centroids | Centroid-only is fragile when objects are close (two chairs at a table); appearance descriptors would need extra ML choices. |
| Flow-based propagation (RAFT + mask warping) | Brittle on occlusions and sudden camera turns; SAM 2 already solves this internally. |
Workaround: bf16 dtype mismatch. SAM 2's video predictor stores
memory-bank features in bfloat16 by default for storage compactness. On
CPU the downstream matmul against fp32
weights crashes with a dtype mismatch (RuntimeError). On CUDA the matmul auto-promotes and
hides the bug. We patch the two offending methods at import time to keep
features as fp32 (see _patch_sam2_bf16_storage_to_fp32 in
src/rgbdsg/detection/sam2_video.py.
Memory cost: ~2× the memory bank size, which is negligible.
The pipeline uses IfcOpenShell to programmatically extract spatial boundaries and connective portals from the .ifc file (src/rgbdsg/ifc/from_ifc_file.py).
The extraction process maps native IfcDoor, IfcWindow, IfcWall, and other structural entities directly into the 3D scene graph, associating their 3D bounding boxes and geometric centroids with the detected open-vocabulary objects. We use the canonical IFC relationships (IfcRelFillsElement, IfcRelVoidsElement, IfcRelContainedInSpatialStructure) to establish hierarchical containment and portal connections natively.
Additionally, for scenarios where raw IFC files might be missing, a fallback parsing method (src/rgbdsg/ifc/from_obj_labels.py) is implemented to extract identical records directly from standard OBJ exports with JSON label dictionaries. Both paths populate the same downstream NetworkX graph builder seamlessly.
Forced deviation from the brief. Neither scene has any IfcSpace
entities. Counts:
| Scene | IfcSpace | IfcBuildingStorey | IfcDoor | IfcWallStandardCase | IfcSlab |
|---|---|---|---|---|---|
| BasicHouse | 0 | 2 | 8 | 13 | 3 |
| synagoge | 0 | 9 | 5 | 12 | 12 |
The brief's hint:
Hint: You can generate downstream data such as floor plan if you convert pointcloud to a BEV Map,
is now clearly the intended fallback. src/rgbdsg/ifc/rooms_bev.py
implements a BEV room-synthesis algorithm:
- Cluster
IfcSlabZ values to find horizontal levels (no assumption about which axis is "up"; rooms are simply between slabs). - For each inter-slab interval, project the architectural point cloud's XY at that height onto a 2D occupancy grid (5 cm cells).
- Morphological closing/opening on the grid to seal hairline gaps.
- Connected components of free space → room candidates.
- The largest component is the unbounded exterior, dropped.
- Each remaining component's contour is traced (Marching Squares) and simplified to a polygon.
This is the explicit Task B story for this dataset: "the architectural prior was given as point cloud + IFC mesh export; the classic IfcSpace polygons are absent, so we synthesise rooms using the PDF's hinted BEV path, anchored to actual IfcSlab heights for vertical stratification."
The pose convention isn't documented in the data, but is critical: a sign
flip on Y or Z silently mis-places every back-projected point. We resolved
all four combinations of (camera-frame axes ∈ {OpenGL, OpenCV}) × (depth ∈ {planar Z, Euclidean ray}) by brute-force scoring against
pointcloud/scene.ply, the architectural ground truth.
Winner for both scenes: gl_z (Blender's default):
P_cam_x = (u - cx) * d / fx
P_cam_y = -(v - cy) * d / fy
P_cam_z = -d
P_world = T_wc @ [P_cam; 1]| Convention | BasicHouse median NN to scene.ply | p95 NN |
|---|---|---|
gl_z ✓ |
41 mm | 84 mm |
gl_ray |
125 mm | 614 mm |
cv_ray |
243 mm | 1796 mm |
cv_z |
292 mm | 2054 mm |
.
├── data/ # gitignored; the challenge dataset goes here
│ ├── BasicHouse_with_pc/
│ └── synagoge_with_pc/
├── media/ # README screenshots + verification PLYs
├── scripts/
│ ├── download_weights.py # fetches HF models and checkpoints
│ ├── inspect_data.py # Step 1 driver (no model deps)
│ ├── verify_pose.py # Step 2 driver (4-convention shootout)
│ └── run_pipeline.py # end-to-end: detection → fusion → graph → viz
├── src/rgbdsg/
│ ├── io/ # typed data loaders (Frame, Pose, ...)
│ ├── geometry/ # gl_z projection / back-projection
│ ├── ifc/ # OBJ+labels parser, IfcOpenShell demo, BEV rooms
│ ├── detection/ # Grounding DINO + SAM 2 wrappers
│ ├── fusion/ # multi-view → ObjectInstance
│ ├── graph/ # NetworkX scene-graph builder
│ └── viz/ # matplotlib BEV plot
├── tests/ # 16 passing unit tests
│ ├── test_geometry.py # round-trip projection precision
│ └── test_ifc.py # OBJ→world rotation regression guards
├── outputs/ # gitignored; pipeline outputs land here
├── weights/ # gitignored; model checkpoints
├── Dockerfile # unified linux container environment
├── Makefile # shortcut targets for local & docker execution
├── pyproject.toml # editable install + pytest config
├── requirements.txt # frozen versions
└── README.md
The easiest way to run the pipeline is using the provided Makefile, either locally or via Docker.
The repository includes a Dockerfile to completely containerize the environment, eliminating dependency issues (like OpenCV or SAM 2's C-extensions).
Build the image (this will install all dependencies and download model weights):
make docker-buildRun the pipeline inside the container (outputs will be written to ./outputs/ on your host):
make docker-run-basichouse
make docker-run-synagoge(Note: The Docker container runs on --device cpu by default for maximum compatibility).
If you have a Linux machine with a CUDA GPU or another supported accelerator, running locally will be significantly faster than Docker CPU.
# 1. Install dependencies (creates .venv and uses `uv` for speed)
make install
# 2. Activate the virtual environment
source .venv/bin/activate
# 3. Download model weights (~2.5 GB)
make download-weights
# 4. Run tests to verify the installation
make test
# 5. Run the pipeline (configured for GPU by default in Makefile)
make run-basichouse
make run-synagogeIf you prefer to bypass make, the project is managed via uv (Astral's standalone Python manager):
brew install uv
uv venv --python 3.11 .venv
source .venv/bin/activate
uv pip install -e . # installs rgbdsg in editable mode
uv pip install -r requirements.txt
# Download weights
python scripts/download_weights.pyBasicHouse (Example using GPU, change --device to cpu if needed):
python scripts/run_pipeline.py \
--scene data/BasicHouse_with_pc \
--device cuda \
--keyframes 0 40 80 120 159 \
--max_per_frame 8 \
--out outputs/basichouseSynagoge (per-scene prompt.txt auto-loaded, evenly-spaced keyframes,
slightly relaxed thresholds for the textureless render):
python scripts/run_pipeline.py \
--scene data/synagoge_with_pc \
--device cuda \
--auto_keyframes 8 \
--max_per_frame 6 \
--box_threshold 0.25 --text_threshold 0.20 \
--out outputs/synagogedata/synagoge_with_pc/prompt.txt ships with the architecture-focused
vocabulary; the runner picks it up automatically when --prompt is not
given on the CLI. Drop a prompt.txt into any new scene's data directory
to specify its vocabulary the same way. The pipeline is dataset-agnostic
and uses the per-scene file as a config.
Outputs to outputs/<scene>/:
<scene>.graphml: full graph in standard GraphML<scene>.json: node-link JSON dump<scene>_objects.json: per-object summary<scene>_bev.png: top-down visualisation
pytest tests/ -vpython scripts/inspect_data.py --data_dir ./data
python scripts/verify_pose.py --scene data/BasicHouse_with_pc --frames 0 80 159Pipeline is device-agnostic. To specify a compute device:
python scripts/run_pipeline.py --device cuda ...The internal Detection/Segmentation wrappers thread the device argument
through every PyTorch call.
The brief explicitly weighs honest limitation analysis. Here they are.
Earlier iterations leaked rooms into the exterior through doorway gaps and
produced fragments instead of whole rooms. The current implementation
(src/rgbdsg/ifc/rooms_bev.py::synthesize_rooms_from_walls) rasterises
IfcWall* mesh faces (not just vertices, which were too sparse) onto
the BEV grid, then rasterises IfcDoor footprints with a larger dilation
to actively seal each doorway. Connected components of the resulting
free-space map are real rooms, not corners.
Result on BasicHouse: 5 rooms, areas 11.2, 9.2, 12.3, 6.3, 3.5 m².
Result on synagoge: 2 rooms (18.7 m², 70.6 m²) on the populated storeys. Other storeys yield no rooms because the wall meshes have median Z outside the slab interval (median-Z gating) prevents floor-to-ceiling walls from being reused on slab-body intervals (explained below).
Median-Z gating subtlety. A wall mesh that spans floor-to-ceiling has its Z bbox overlapping both the room-interior interval (storey 1) and the slab-body interval (between two slabs of the next floor up). Including it in both produces duplicate rooms with the same XY footprint stacked on different Z. We gate on the wall's median Z falling inside the interval, which keeps each wall on exactly one storey.
Door-portal connects edges (Task B's "connective portals"). For each
IfcDoor, the graph builder finds the two rooms whose polygon edges sit
within 1.5 m of the door centroid (and whose Z interval contains the
door) and emits symmetric connects edges between them, tagged with the
door's GUID. BasicHouse: 6 such edges. Synagoge: 0 (only 2 rooms, doors
don't bridge them).
assign_obj_ids in scripts/run_pipeline.py
gives every Grounding DINO box on every keyframe its own obj_id. SAM 2
then propagates each obj_id independently across the video, so the same
physical chair detected on frames 0 AND 80 produces two separate tracks.
This is now resolved by dedup_object_instances in
src/rgbdsg/fusion/multiview.py,
which runs after fuse_object_masks and merges any pair of
ObjectInstances that have
- a compatible label (word-set overlap, so
"chair"matches"chair sofa"from a multi-phrase GDINO match), AND - either a centroid distance under
--dedup_centroid_m(default 0.6 m) OR a 3D-bbox IoU above--dedup_iou(default 0.25).
The merge is greedy from largest to smallest by observation count, so the dominant track survives and absorbs the smaller ones. Their points, label-vote distributions, and observation counts combine; the centroid and bbox are recomputed from the union of points.
On BasicHouse this collapses 19 raw ObjectInstances into 7 unique
physical objects, verified in §1's run summary.
When a box matches multiple text phrases ("chair", "sofa") at similar score, GDINO returns both as the label string ("chair sofa"). One of the 19 BasicHouse objects exhibits this. A simple post-process (split, take top-scoring phrase) would clean it up; we leave the joint label visible for honesty.
The dataset was rendered in Blender 4.0.2 (per run_blender_log.json),
not captured. Implications:
- Depth is dense (no IR speckle, no sky-as-zero), saturation only at the 100 m far plane.
- Pose is exact; we don't need ICP or graph-SLAM cleanup.
- RGB has no motion blur, no exposure changes.
Findings here transfer to a real captured RGB-D sequence with caveats. We would then need to discuss depth de-noising, pose-graph optimisation, and exposure-invariant SAM 2 prompting, none of which we implemented because the data didn't need them.
The graph builder emits the full Armeni four-layer hierarchy: Building → Storey → Room → Object / Entity / Camera, plus same_storey and
same_room peer edges. On BasicHouse all 5 rooms, 7 objects, 125
structural entities, and 5 cameras are correctly parented to 2 storeys. On
synagoge the 9 canonical IFC storeys are present, but only 2 of them
contain BEV-synthesised rooms (the others lack wall meshes in the
populated Z interval; see §6.1's median-Z gating note).
- For long sequences (synagoge: 383 frames × 2 sweeps × multiple objects),
the inference state would otherwise consume excessive memory.
Resolved by passing
offload_video_to_cpu=Trueandoffload_state_to_cpu=Truetoinit_state, which are SAM 2's official flags for this case. Adds CPU↔device transfer per frame but keeps memory bounded by per-frame cost; no clip-length-dependent crashes. - We additionally run forward + reverse propagation and merge per (frame, obj_id) by taking the larger mask. This guarantees coverage of every frame regardless of where the prompt keyframes sit and provides a small robustness margin against any single-direction failure.
A version of this pipeline with the constraints lifted would look like:
-
IfcSpace Polygons → If the provided IFC file is exported with defined
IfcSpaceentities, their polygons can replace BEV synthesis verbatim. Door portal logic becomes a graph edge between the twoIfcSpaces the door connects. -
Closed-set + open-set detection ensembled. Open-vocab DINO catches long-tail furniture, but a strong closed detector (Mask R-CNN on COCO or a fine-tuned LVIS head) is more precise on the common categories. Marginal gains, but worth it on data with hard categories.
-
Hierarchical 3D scene graphs as in Hydra [5] and 3D Scene Graph [1]:
Building → Storey → Room → Object → Part. Our graph already has the types; the missing piece is the parser that emits storey nodes from IfcBuildingStorey and roof slabs. -
Geometry-aware merging across keyframes. ConceptGraphs [6] shows that keeping per-object 3D point clouds (not just centroids) and merging them by IoU-of-3D-bbox is robust against the GDINO duplicate-detection problem. This is a small refactor of the
fuse_object_masksoutput type. -
Inter-frame consistency via SAM 2 + dense feature track. SAM 2 already does mask propagation; coupling it with a dense feature tracker (e.g. CoTracker) on key points inside the mask gives resampling robustness when the camera turns sharply. Useful when moving from synthetic to real data.
-
Closed-loop with the IFC: detected
door/window/chairshould correspond geometrically to existingIfcDoor/IfcWindow/Furnishing entities. A reconciliation pass that snaps a visually-detected door to the nearest IfcDoor (within tolerance) would compress two near- duplicate nodes and signal high-confidence presence.
The choices above sit on a literature trail below; the README's reasoning is inspired by these even where we don't replicate them exactly.
[1] I. Armeni, Z. Y. He, J. Gwak, A. R. Zamir, and M. Fischer, "3D Scene Graph: A Structure for Unified Semantics, 3D Space, and Camera," in Proc. IEEE/CVF Int. Conf. Comput. Vis. (ICCV), 2019, pp. 5664–5673. Available: https://arxiv.org/abs/1910.02527
[2] B. K. Nithyanantham et al., "MCP4IFC: IFC-Based Building Design using Large Language Models," arXiv preprint arXiv:2511.05533, Nov. 2025. Available: https://arxiv.org/abs/2511.05533
[3] S. C. Wu, J. Wald, K. Tateno, N. Navab, and F. Tombari, "SceneGraphFusion: Incremental 3D Scene Graph Prediction from RGB-D Sequences," in Proc. IEEE/CVF Conf. Comput. Vis. Pattern Recognit. (CVPR), 2021, pp. 7512–7522. Available: https://arxiv.org/abs/2103.14898
[4] A. Papadakis and E. Spyrou, "A Multi-Modal Egocentric Activity Recognition Approach towards Video Domain Generalization," Sensors, vol. 24, no. 8, Art. no. 2491, Apr. 2024. Available: https://doi.org/10.3390/s24082491
[5] N. Hughes, Y. Chang, and L. Carlone, "Hydra: A Real-time Spatial Perception System for 3D Scene Graph Construction and Optimization," in Proc. Robotics: Science and Systems (RSS), 2022. Available: https://arxiv.org/abs/2201.13360
[6] Q. Gu et al., "ConceptGraphs: Open-Vocabulary 3D Scene Graphs for Perception and Planning," in Proc. IEEE Int. Conf. Robot. Autom. (ICRA), 2024. Available: https://arxiv.org/abs/2309.16650
[7] S. Liu et al., "Grounding DINO: Marrying DINO with Grounded Pre-Training for Open-Set Object Detection," in Proc. Eur. Conf. Comput. Vis. (ECCV), 2024. Available: https://arxiv.org/abs/2303.05499
[8] N. Ravi et al., "SAM 2: Segment Anything in Images and Videos," in Proc. Int. Conf. Learn. Represent. (ICLR), 2025. Available: https://arxiv.org/abs/2408.00714
[9] buildingSMART International, "IfcOpenShell," 2024. [Online]. Available: https://ifcopenshell.org (accessed May 12, 2026).
Per the challenge's open-AI-usage policy, I disclose that this project was developed with the assistance of an LLM. I personally designed the architecture, implemented the core pipeline, and conducted the data inspection. The AI was utilized strictly as a research assistant to evaluate design alternatives, brainstorm workarounds for hardware constraints (such as the SAM 2 memory issues), and help refine the documentation.
- Data & Artifacts:
data/,weights/, andoutputs/are intentionally gitignored to keep the repository clean. The original dataset must be placed indata/as per the challenge brief. - Dependencies: Managed deterministically.
make installcreates a virtual environment and usesuvto install the exact dependency tree fromrequirements.txt. - Docker Integration: A
Dockerfileis provided alongside theMakefilefor guaranteed reproducibility on any host operating system (see §5). - Model Weights: Downloading is fully automated via
make download-weights(which executesscripts/download_weights.py). - Test Suite: The test suite lives under
tests/and is executed viamake test. It empirically verifies critical assumptions, including the geometric pose convention (tests/test_geometry.py) and the IFC entity parsing (tests/test_ifc.py).





