Personal project, Photographs to 3D
ParallaxOps
Turns 15–60 overlapping photographs into a COLMAP 3D reconstruction on one laptop, then checks the geometry against ground truth.
- 0.31%Camera position error as a share of rig spread, ETH3D terrace vs ground truth
- 0.52°Median camera rotation error on the same scene
- 23/23ETH3D terrace DSLR images registered, 87 s through the platform
- 433 msAPI p95 during a reconstruction at 32 concurrent users, 0 errors (351 ms idle)
How it works
- UploadPresigned direct-to-storage uploads with size and SHA-256 signed into the URL
- OrchestrateDurable Temporal workflows, one heavy job per worker process
- ReconstructCOLMAP structure-from-motion; GPU dense reconstruction is opt-in
- Quality checkGeometric QC plus a capture-order warning
- ViewPXLD multi-resolution point cloud in Three.js with camera frusta
Problem
Given a set of overlapping photographs, produce a real 3D reconstruction and say, with numbers, how good it is. I wanted a platform that uploads, reconstructs and displays end to end on one laptop (Ryzen 9, RTX 4060) with no paid services, and that measures its geometry against ground truth instead of trusting its own health signals.
Approach
Uploads go straight to object storage (SeaweedFS, S3-compatible) through presigned URLs. The upload size and SHA-256 are signed into the URL, so the store, not the API, verifies the bytes. A tenant-scoped Fastify API hands work to durable Temporal workflows, and a worker runs COLMAP 4.2.1 structure-from-motion, with GPU dense reconstruction as an option. Never more than one heavy job runs at a time. A live stage timeline shows progress, and a Three.js viewer draws the point cloud with camera frusta.
For the viewer I designed PXLD, a multi-resolution point-cloud format. The browser still receives every point (up to 8M per cloud), but it can draw a coarse level first and fill in the rest.
I tested reliability against real processes (Postgres, object store, Temporal, worker, browser), not mocks. A worker hard-killed mid-job is retried on a new worker, and the job completes with no orphan engine or scratch. Cancel reaches CANCELLED in 6.1 s.
When an outside code review came in, I checked each item against the code and fixed what held up: signed upload size and digest, cursor paging, lease-guarded background jobs, a worker-to-API event replay test and a capture-order QC warning. The repo has 199 automated tests, and CI (actionlint, ruff, mypy on 3 OSs, Biome, tsc, vitest, web build) is green.
Results
On the ETH3D terrace scene (23 DSLR images), measured against ground truth: 23/23 images registered, camera position error 0.31% of rig spread, 0.52° median rotation error, 87 s through the platform. A GPU dense run on the same scene produced 248K points. The viewer draws the first PXLD level in 106 ms and all 248K points in 147 ms (software WebGL). API p95 is 351 ms idle and 433 ms during a reconstruction at 32 concurrent users, with 0 errors.
I also pre-registered an experiment on a quality-aware compute controller. It did not pass: +27 to +37 points of registered images, but at 3× cost. A dynamic-object masking experiment was a null result. Both are published in the repo.
What I learned
The controller failure was the most useful result. Registered-image fraction and reprojection error, the signals the controller relied on, looked healthy on wrong geometry: its cameras were 20–70× further from ground truth. A reconstruction can pass its own checks and still be wrong, so the numbers that matter are the ones scored against ground truth.
Writing the plan down before running the experiment is what made the negative result publishable instead of something to quietly tune away.
Killing real workers told me more than mocks would have. And multi-host scale-out stays described as a design until I have run it on more than one host.
Limits, stated not hidden
- Evaluated on two scenes, mostly one, on one laptop.
- Sparse by default; dense reconstruction is opt-in and needs an NVIDIA GPU.
- Images alone give no metric scale.
- One heavy job per worker process; multi-host scale-out is a design, not a claim.
- No login system, by design; isolation is tenant scoping only.
- The Docker Compose path is unverified.
- The pre-registered quality-aware controller failed its test: +27 to +37 points of registered images, at 3× cost.
- A dynamic-object masking experiment produced a null result.