How Bart Sakwerda's Varjo Teleport capture of the Cebu–Cordova Link Expressway (Cebu, Philippines) was cleaned, oriented, compressed for the portfolio site, segmented with SAM 3 and rebuilt in Blender, with every mistake and fix along the way.
Source 2024-ph-cebu-cclexbridge-teleport-knut202609.ply: 638 MB, 2,703,173 Gaussians, full
spherical harmonics (3 bands), exported from Varjo Teleport. splat-transform --stats plus a few numpy
percentiles showed what we were dealing with:
| Finding | Numbers | Consequence |
|---|---|---|
| Compact scene | 90% of Gaussians within 2.6 km of the centre (units are metres) | crop to a sphere |
| Sky shell | ~3% between 400 and 600 km away | background from the capture app: remove |
| Near-transparent | 683,000 (25%) under 2% opacity | invisible weight: remove |
| Very large splats | ~18,000 over 15 m, most at sea level | keep the sea, drop the ones in the air |
| Orientation | stored "z-down" | rotate for y-up web viewers |
First pass with splat-transform: drop invalid and < 1% opacity Gaussians, crop to a 6 km sphere, drop splats larger than 15 m or 40 m.
splat-transform in.ply -N -V opacity,gt,0.01 -S -63,-62,118,6000 \ -V scale_0,lt,15 -V scale_1,lt,15 -V scale_2,lt,15 test.sog
splat-transform's filters can only combine with AND, so the final rule was written in numpy: keep big splats only inside the sea-surface band (z 110–150, found from a histogram of the big splats), drop them everywhere else.
python clean_splat.py in.ply clean.ply --radius 6000 --center -63,-62,118 \ --max-scale 15 --band-axis z --band 110,150 --band-max-scale 300
Web viewers expect y-up, so the file was rotated 90° about X. To double-check, I measured heights, and the numbers said the scene was upside down. They were wrong: water reflections are stored as real geometry, a mirror-image bridge below the sea surface, so "most points below the surface" looked like up was down.
splat-transform clean.ply -r 90,0,0 yup.ply splat-transform yup.ply -m cclex-bridge-cebu.sog # 29 MB, full colour detail splat-transform yup.ply -H 1 --decimate-adaptive 1000000 m.ply splat-transform m.ply -m cclex-bridge-cebu-mobile.sog # 14 MB for phones
splat-transform cclex-bridge-cebu.sog --unbundled index.html builds a SuperSplat viewer page.
Edits: opening camera (renderer coordinates) (-900, 350, 300) → (0, -100, 0), fov 55, background
blue-grey so the semi-transparent sea reads as water, and a tiny script that loads the 14 MB file on phones.
The portfolio lives on disk7aerials _PORTFOLIO-drones-bartsakwerda/site/ (backup on the Samsung).
Splat-native segmentation models are trained on indoor scans, so for aerial splats the practical route is:
render views, segment them in 2D, vote the labels back onto the Gaussians. SAM 3 runs locally through Apple's
MLX (mlx-community/sam3-image): about 10 s for the first image pass, then under 3 s per text prompt.
To vote labels onto the Gaussians, their centres are projected into each rendered view with a depth test. The first check failed: the dots did not line up. The cause was that splat-transform's renderer shows the file rotated 180° about Z; with that flip the projection matches (correlation 0.43 against ~0 for every other option).
A parametric model built by script in Blender 5.2 from the public dimensions: 390 m cable-stayed main span, two 145 m pylons, 51 m navigation clearance, 27 m four-lane deck (8.9 km total length). Assumed: 150 m back spans, single central pylon column and cable plane (as in the scan), 16 stays per side, piers every 45 m. The scan's centres are included as a hidden reference layer for alignment.
On a Pix4D point cloud of Lapu-Lapu / Mactan (18.8 M points) a rule-based PDAL pipeline (outliers → SMRF ground → height above ground → greenness and coplanarity) ran in under 3 minutes and wrote a classified LAZ 72% smaller than the LAS.
| Lesson | What to do |
|---|---|
| Capture apps add a far sky shell | crop to a sphere from percentile distances |
| 25% of Gaussians can be invisible | -V opacity,gt,0.01 (linear scale) |
| Sea and floaters are both big splats | drop big splats only outside the surface band (numpy OR rule) |
| Reflections are mirror geometry | judge orientation from renders; check for mirroring |
| Renderer = file rotated 180° about Z | apply it in any projection; cameras are in renderer coordinates |
| Capture-app terms and privacy | check Varjo/Luma/Postshot export rights; blur people and number plates |
gaussian-splat-pipeline (~/.claude/skills/) with clean_splat.py,
seg_lift.py and a Blender bridge-twin template.