Photogrammetry pipeline for building a digital twin of the UT campus. Takes photos of building facades and produces segmented, scaled 3D meshes of windows and doors β ready for Blender inspection and Sionna RF ray tracing simulation.
photo β Grounding DINO β SAM β scaled .obj meshes β Blender / Sionna RT
- Grounding DINO detects bounding boxes for windows and doors using open-vocabulary text prompts. Runs on the full building crop plus a tiled pass to catch small repeated elements like individual window panes.
- Filtering removes false positives using a class whitelist, spatial crop zones (sky, ground, trees, adjacent structures), aspect ratio bounds, and min/max area thresholds.
- NMS (non-maximum suppression) deduplicates overlapping detections across tiles.
- SAM (Segment Anything) takes each surviving bounding box and produces a precise pixel-level mask.
- Scale conversion maps pixel coordinates to real-world meters using a known wall height reference.
- Mesh export extrudes each polygon into a 3D mesh with per-class depth (window glass = 5 cm, door = 10 cm) and exports
.objfiles grouped by semantic class. - Sionna descriptor writes a JSON scene file mapping each mesh to an ITU electromagnetic material for RF simulation.
facade-sam/
βββ app/
β βββ run_facade_pipeline.py # main pipeline
β βββ view_meshes.py # mask overlay + scale summary
β βββ sionna_scene_loader.py # loads output into Sionna RT
βββ checkpoints/
β βββ groundingdino_swint_ogc.pth
β βββ sam_vit_h_4b8939.pth
βββ input/
β βββ building.jpg # your photo goes here
βββ output/
β βββ masks/ # binary PNG mask per detection
β βββ meshes/ # per-object .obj files
β βββ per_class/ # one merged .obj per semantic class
β βββ combined_scene.obj # everything in one file
β βββ mask_overlay.png # visual QA β masks drawn on original image
β βββ sionna_scene.json # scale + material metadata
βββ scripts/
β βββ download_models.sh
βββ .hf_cache/ # HuggingFace model cache (auto-created)
βββ Dockerfile
βββ Makefile
βββ requirements.txt
- Docker Desktop running with WSL2 integration enabled
- NVIDIA GPU with drivers installed (CPU fallback works but is slow)
- ~15 GB disk space for model checkpoints and Docker image
make download-modelsDownloads sam_vit_h_4b8939.pth (~2.4 GB) and groundingdino_swint_ogc.pth (~700 MB) into checkpoints/.
make buildcp /path/to/your/photo.jpg input/building.jpgPhoto tips for best results:
- Shoot straight-on to the facade, not at an angle
- Full building height should be visible β this sets the scale
- Good even lighting, avoid harsh shadows across windows
- Keep cars, trees, and adjacent buildings to a minimum in frame
Open app/run_facade_pipeline.py and update the config section at the top:
KNOWN_WALL_HEIGHT_M = 15.0 # estimated facade height in meters (3-4m per floor)
CROP_TOP_FRACTION = 0.03 # fraction of image height to ignore from top (sky)
CROP_BOTTOM_FRACTION = 0.30 # fraction to ignore from bottom (ground, cars)
CROP_LEFT_FRACTION = 0.27 # fraction to ignore from left (adjacent structures)
CROP_RIGHT_FRACTION = 0.92 # fraction of width to keep (crop right edge)These crop values are tuned per-photo. See the tuning guide below.
make run-pipelinemake viewSaves output/mask_overlay.png showing detected regions on the original image. Open on Windows:
explorer.exe output/mask_overlay.pngFile β Import β Wavefront (.obj)- Select
output/combined_scene.obj(everything) or files fromoutput/per_class/(one object per class β recommended) - Import settings:
- Forward: Y
- Up: Z
- Scale: 1.0 (already in meters)
- Press
Numpad 1for front view,Numpad .to zoom to selection
After verifying geometry in Blender, load the scene for ray tracing.
The Sionna workflow has two stages:
1. Scene loader (app/sionna_scene_loader.py)
Reads output/sionna_scene.json (written by the main pipeline) and builds a Mitsuba 3 XML scene file (output/sionna_scene.xml). Each per-class .obj mesh (e.g. output/per_class/window.obj) is registered as a shape, and each shape is assigned an ITU electromagnetic BSDF material. The XML is then loaded into Sionna's Scene object using load_scene().
ITU electromagnetic material assignments:
| Semantic class | Sionna material |
|---|---|
| window | itu_glass |
| glass_window | itu_glass |
| window_pane | itu_glass |
| door | itu_wood |
| brick_wall | itu_brick |
| concrete_wall | itu_concrete |
| pma_building | itu_concrete |
Run the loader to generate output/sionna_scene.xml:
pip install sionna tensorflow
python3 app/sionna_scene_loader.py2. Ray tracing experiments (run_sionna.py)
Runs four experiments on the loaded scene at 3.5 GHz (mid-band 5G). TX and RX are single-element isotropic vertical-polarization arrays. Paths are solved with PathSolver.
| Experiment | What it measures |
|---|---|
| 1 β Non-LoS path validation | Places TX at [10, -20, 5] m and RX at [0, 20, 2] m with no line-of-sight. Counts multipath components and prints the path coefficient tensor shape. Confirms the facade geometry is producing reflected/diffracted paths. |
| 2 β TX height sweep | Sweeps TX height through 1.5 m, 10 m, 20 m, 35 m while keeping RX fixed. Reports path count and total received power at each height. Shows how elevation above the facade changes multipath richness. |
| 3 β Reflections on vs off | Compares max_depth=4 (reflections enabled) against max_depth=0 (LoS only). Reports path count and power for both. Quantifies how much the facade contributes to received signal beyond direct path. |
| 4 β Reflection depth analysis | Steps max_depth from 0 to 5 and records cumulative path count and power at each bounce level. Shows the marginal contribution of each additional reflection order. |
Run all experiments:
python3 run_sionna.pyScene setup (shared across all experiments)
scene.frequency = 3.5e9 # 3.5 GHz
scene.tx_array = PlanarArray(num_rows=1, num_cols=1, pattern="iso", polarization="V")
scene.rx_array = PlanarArray(num_rows=1, num_cols=1, pattern="iso", polarization="V")Power reported in experiments 2β4 is the sum of squared real and imaginary path coefficients: Ξ£ |a|Β².
All parameters are at the top of app/run_facade_pipeline.py:
| Parameter | Default | Description |
|---|---|---|
BOX_THRESHOLD |
0.22 |
DINO confidence for bounding boxes. Lower = more detections, more noise. |
TEXT_THRESHOLD |
0.18 |
DINO text-match confidence. |
USE_TILING |
True |
Run DINO on overlapping tiles to catch small windows. |
TILE_SIZE |
1024 |
Tile size in pixels. |
TILE_OVERLAP |
256 |
Overlap between tiles β prevents missing windows at tile edges. |
NMS_IOU_THRESH |
0.4 |
IoU threshold for deduplication across tiles. |
| Parameter | Default | Description |
|---|---|---|
KNOWN_WALL_HEIGHT_M |
15.0 |
Estimated facade height. Sets pixelβmeter scale. |
CROP_TOP_FRACTION |
0.03 |
Ignore top N% of image (sky). |
CROP_BOTTOM_FRACTION |
0.30 |
Ignore bottom N% (ground, cars, parking lot). |
CROP_LEFT_FRACTION |
0.27 |
Ignore left N% (adjacent buildings, trees). |
CROP_RIGHT_FRACTION |
0.92 |
Keep only left N% of width (cuts right edge structures). |
| Parameter | Default | Description |
|---|---|---|
MIN_BOX_AREA_FRACTION |
0.0005 |
Minimum box area as fraction of image. Removes noise. |
MAX_BOX_AREA_FRACTION |
0.02 |
Maximum box area. Removes large false positives (walls, slabs). |
MIN_ASPECT |
0.4 |
Minimum width/height ratio. Rejects very tall thin strips. |
MAX_ASPECT |
2.5 |
Maximum width/height ratio. Rejects wide slabs. |
- Lower
BOX_THRESHOLDto0.18,TEXT_THRESHOLDto0.14 - Check crop fractions aren't cutting into the facade
- Open
output/mask_overlay.pngto visualize exactly what's being found
- Tighten crop fractions to exclude problem areas
- Lower
MAX_BOX_AREA_FRACTIONto kill large slab detections - Lower
MAX_ASPECTto kill wide horizontal strips
- Reduce
CROP_TOP_FRACTION(try0.03)
- Reduce
CROP_BOTTOM_FRACTION(try0.25)
- Increase
CROP_LEFT_FRACTION
- Decrease
CROP_RIGHT_FRACTION
- Adjust
KNOWN_WALL_HEIGHT_Mβ measure the actual building if possible
- Increase
EXTRUDE_DEPTH_Mvalues in config (currently 5 cm for glass, 10 cm for doors)
| Command | Description |
|---|---|
make build |
Build the Docker image |
make run |
Run the full pipeline |
make view |
Print detection summary, save mask overlay |
make shell |
Open bash shell inside the container |
make download-models |
Download SAM and DINO checkpoints |
make clean |
Clear all output files |
End of File error when running make download-models: Run wsl sudo apt install dos2unix and then wsl sudo dos2unix scripts/download_models.sh
Permission denied on make clean
Output files are owned by root (Docker runs as root). The Makefile uses sudo rm. Run make clean with your password.
Cannot connect to Docker daemon
Docker Desktop is not running. Start it from the Windows Start menu and wait for "Engine running".
exec format error on docker-credential-desktop.exe
WSL2 credential helper issue:
echo '{}' > ~/.docker/config.jsonPermissionError: /.cache when loading models
The HuggingFace cache dir isn't writable. Make sure .hf_cache/ exists in the project root:
mkdir -p .hf_cache
make runArcGIS/ArcPy is can't validate license: Sometimes happens if you use Ctrl+C to force quit in the middle of a run.
Open task manager and end any lingering python tasks related to ArcGIS (often have ERSI in the file path). Then, try
opening the ArcGIS Pro app, then closing it and running the pipeline again.
| Model | Source | Size |
|---|---|---|
| Grounding DINO Base | IDEA-Research/grounding-dino-base on HuggingFace |
~700 MB |
| SAM ViT-H | Meta AI / facebookresearch/segment-anything | ~2.4 GB |
- Occluded windows (behind trees, signs, cars) cannot be detected from a single photo. Take a second photo from a slightly different angle to fill gaps, then merge both
sionna_scene.jsonoutputs. - Perspective distortion β photos taken at an angle produce skewed geometry. Shooting straight-on minimizes this. True correction requires camera calibration (focal length + GPS distance).
- Crop zones are per-photo β each new building photo needs its own crop fraction tuning. See the tuning guide above.
- Replace
KNOWN_WALL_HEIGHT_Mwith proper camera calibration (focal length + GPS + compass) - Multi-image merging β stitch detections from multiple photos of the same facade
- Auto crop zone detection β use sky/ground segmentation to set fractions automatically
- Mitsuba XML export for direct Sionna
load_scene()ingestion - Depth estimation (MiDaS) to improve Z positioning of window elements