
Litter Watch — Street Garbage Detection
Computer Vision · Civic Tech
Takes a folder of video to a trained detection model on your own GPU, without a single byte leaving the machine.
A local-first computer-vision workbench: import video, extract frames, annotate them, build immutable dataset versions, train YOLO models on a local GPU, evaluate the results and run inference — with no cloud storage, hosted GPU or external annotation service anywhere in the loop.

Annotation Maker is the whole detection pipeline — Import → Extract → Annotate → Dataset → Train → Evaluate → Infer → Export — running as one FastAPI process on a single machine. It was built because the alternative for a small team is stitching together a hosted annotation tool, a cloud bucket and a rented GPU, then paying for all three and handing over the footage.
The interesting engineering is in the job model. Frame extraction, dataset builds, training and inference are launched by a supervisor inside the API but execute as separate OS processes with all state in SQLite. That makes cancellation real — a training loop cannot be interrupted from another thread — keeps a CUDA out-of-memory error from taking the API down with it, and lets an interrupted job resume after a restart. There is no message broker to install.
Two rules hold the data model honest. Dataset versions are immutable: a version copies annotation geometry at snapshot time rather than referencing live rows, so editing a box later cannot silently change what a model was trained on. And predictions are never ground truth — model output is stored separately from annotations, and promoting a prediction is an explicit action.
GPU use is explicit rather than best-effort: asking for CUDA when CUDA is unavailable fails immediately with a diagnostic instead of quietly falling back to CPU, because a training run that drops to CPU wastes hours before anyone notices.
Building a detection model meant renting three services — a hosted annotation tool, cloud storage and a GPU — and uploading the source footage to all of them. For work on private or client-owned video that is both a recurring cost and a disclosure problem, and the round trip between labelling and training made iteration slow.
Traced where time actually went on a real detection project and found it was not labelling — it was the hand-offs: exporting a dataset, uploading it, configuring a training job, then pulling results back to decide what to label next.
Collapsed the entire loop into one local application. A FastAPI process serves the built React SPA and owns a SQLite database of metadata and paths; large assets stay on disk, dataset versions hardlink frames instead of copying them, and training runs against the local GPU. Labelling, training and evaluation are three clicks apart, so the next labelling decision is informed by the last run.
FastAPI serves both the REST API and the built React/Vite SPA — one process runs the whole application
SQLite (WAL) holds metadata and paths only; video, frames, weights and exports live on the filesystem
Job supervisor inside the API launches each long task as a separate OS process, with all state in SQLite
Ultralytics YOLO on PyTorch with an explicit CUDA device policy — no silent CPU fallback
Immutable dataset versions snapshot annotation geometry and hardlink frames into versioned directories
Alembic migrations over a workspace directory that is separate from the code checkout
A training loop cannot be interrupted from another thread, so in-process jobs made cancellation a lie and a CUDA out-of-memory error took the API down with it.
Moved every long job into its own OS process supervised by the API, with all state in SQLite. Cancellation became a real kill, a crashing job stopped taking the server with it, and an interrupted run could resume after a restart — without adding a message broker.
Editing an annotation after training silently changed what a past model had been trained on, making runs impossible to compare or reproduce.
Made dataset versions immutable — a version copies annotation geometry at snapshot time rather than referencing live rows, and every training run records the exact dataset version hash it used.
Frames extracted from the same video are near-identical, so a random train/validation split leaks the validation set into training and reports a wildly optimistic mAP.
Split video-aware: all frames from one source video land on the same side of the split, so validation scores reflect unseen footage.
The full detect-model loop — import, label, version, train, evaluate, infer — runs on one machine with no cloud dependency and no per-seat or per-GPU-hour cost. Footage never leaves the device, which makes the tool usable on client-owned and private video that could not be uploaded at all.





