CrumbVMS can now read license plates. It keeps a searchable database of the plates it sees, alerts you when a watchlisted plate shows up, and holds a crop of each read. It's off by default, gated by the view_plates capability, and every read is camera-scoped like everything else in Crumb.
The part I care about most: nothing leaves your hardware. There's no plate cloud, no API key, no third party in the loop.
Two engines, chosen per camera
There's one Engine dropdown on each camera, with four values: None, Frigate, Crumb (fast-alpr), or Both.
- Frigate native LPR reads plates off the same
frigate/eventsMQTT stream
Crumb already consumes for object detection. If you run Frigate, this is the zero-effort path. No new services, no new keys.
- The
crumb-alprworker is Crumb's own local OCR engine, an opt-in Python
sidecar behind the alpr Compose profile that runs <code>fast-alpr</code> (a YOLOv9-t ONNX detector plus a CCT-xs ONNX OCR).
The server enforces the per-camera choice at ingest: it looks up the camera's engine and drops any read the engine doesn't accept. So a Frigate-only camera silently ignores worker reads, and vice versa. If you turn on Frigate LPR and see no plates, check that: LPR enabled globally, camera engine set to Frigate or Both.
Why I wrote a second engine
Honestly, because the first one wasn't enough for my own driveway. Frigate's native LPR misread most plates on my wide overview-angle entry camera, where the plate is small in frame. I wanted something I could run on the same box that reads better on that angle, so I built the local worker. It pulls frames from that camera's go2rtc restream, gates on cheap frame-diff motion, votes across the frames of a single vehicle pass, and POSTs one best read per pass with a real crop attached.
It sips power
This is the part I'm proud of. The worker is CPU-only and the models are tiny (7 MB detector, 3 MB OCR). Motion-gated, it idles most of the time: roughly a third of one CPU core at idle, about half a core during an actual plate pass, single-digit watts on a single 1080p camera.
The one knob to leave alone is threads. LPRORTTHREADS and LPRCVTHREADS default to 1 on purpose. The models are small enough that ONNX Runtime's default (one thread per core) turns each inference into an all-core spin-wait that burns 15 to 40 W for no extra speed. Keep them at 1 unless you've measured a reason not to.
Watchlist, ignore list, fuzzy matching
- Watchlist: add plates you care about; a hit fans out as a
platewatchlisthit alert with the crop attached.
- Ignore list: an ignored plate is never stored and never shows up in search,
not merely muted. It fails closed.
- Fuzzy matching: the fuzz value (0 to 0.5) means "up to this fraction of the
plate's characters may differ." On top of that, visually confusable characters cost zero edits: O/0/D, I/1, B/8, S/5, Z/2, G/6. A night-time OCR flip on one of those can never push a real watchlisted plate over the budget. I'd rather never miss an alert.
A note on responsibility
A searchable database of license plates is regulated in some places. Running it lawfully is on you as the operator. Crumb ships it off by default for that reason. Full details in the LPR docs.