Back to Work
June 2026 Personal project

NeRF Service — photos → 3D on a GPU

A live web service that turns a set of overlapping photos into a 3D Gaussian-splat scene, rendered on a GPU.

  • FastAPI
  • Python
  • RunPod (serverless GPU)
  • nerfstudio / gsplat
  • Cloudflare R2
  • Docker
  • Render.com
A Gaussian-splat scene produced by the service from a set of overlapping photos, rendered in a browser splat viewer.
A Gaussian-splat scene produced by the service from a set of overlapping photos, rendered in a browser splat viewer.

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.

Architecture: a browser or Python client calls a FastAPI service on Render.com, which dispatches the minutes-long GPU job (structure-from-motion and Gaussian splatting) to a serverless RunPod worker; both exchange files through Cloudflare R2 object storage via presigned URLs. Client browser · Python FastAPI API Render.com · always-on GPU worker · RunPod Nerfstudio (Docker) · gsplat Cloudflare R2 object storage · S3-compatible · .ply in / out ① upload ③ poll · ④ result ② dispatch presigned URLs frames · .ply
Async pipeline: the client submits photos and polls a job; the FastAPI service keeps the endpoint responsive and offloads the heavy structure-from-motion + Gaussian-splatting work to a serverless RunPod GPU, with files exchanged through Cloudflare R2 via presigned URLs.

Try it

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/")