A live web service that turns a set of overlapping photos into a 3D Gaussian-splat scene, rendered on a GPU.
This started as an exercise in wiring real infrastructure together: how do you put a small, always-on web API in front of compute that’s far too heavy to run on the same box? A lightweight FastAPI service on an endpoint host like Render.com stays responsive, while the minutes-long GPU work is offloaded to a serverless GPU on RunPod. You send the service a set of overlapping photos; it coordinates the job and hands back a 3D scene file.
The splatting itself runs inside a Docker image of
Nerfstudio on the GPU worker: it does
the structure-from-motion to recover camera poses, then trains a gsplat Gaussian-splatting
model and exports the .ply. The surrounding system is small but complete — a FastAPI API,
a decoupled RunPod GPU worker, Cloudflare R2 (S3-compatible) object storage with presigned
URLs for file exchange, and an async submit → poll → download job API.
Live demo on modest infrastructure — if it doesn’t respond on the first try, give it a few seconds and reload.
1 · See a result — no setup. Download a sample scene —
a real .ply produced by the service. Drag it into a splat viewer like
antimatter15.com/splat or SuperSplat.
2 · Drive the live API in your browser. Open the interactive Swagger UI
for the real endpoints — /upload, /nerfify, /jobs/{id}, /jobs/{id}/result.
3 · Run the client — about 25 lines. Needs Python 3 and pip install httpx, plus ~20+
overlapping photos of a single object or scene. No photos handy? The
fern scene from the LLFF dataset
on Kaggle works well.
# pip install httpx — run: python try_nerf.py photo1.jpg photo2.jpg ...
import httpx, time, sys, os
BASE = "https://nerf.mattbouchard.com"
frames = sys.argv[1:] or sys.exit("pass 20+ overlapping photos of one object/scene")
with httpx.Client(base_url=BASE, timeout=120) as c:
ids = []
for p in frames: # 1. upload each frame
with open(p, "rb") as f:
r = c.post("/upload", files={"file": (os.path.basename(p), f, "image/jpeg")})
r.raise_for_status(); ids.append(r.json()["id"])
print(f"uploaded {len(ids)} frames")
job = c.post("/nerfify", json={"images": ids}).json()["job_id"] # 2. start GPU job (202)
print("job:", job)
while True: # 3. poll until done
status = c.get(f"/jobs/{job}").json()["status"]
print("status:", status)
if status in ("done", "failed"): break
time.sleep(5)
if status == "done": # 4. download the splat
r = c.get(f"/jobs/{job}/result", follow_redirects=True)
open("scene.ply", "wb").write(r.content)
print("saved scene.ply — drop it into https://antimatter15.com/splat/")