Catcam
YOLO on an RTSP kitchen camera that only fires when the cat is on the counter.
A home cat-on-the-counter detector: drain an RTSP stream, run YOLO on CPU, and treat a polygon as the thing that matters — not a bounding box overlapping the island. When it trips it snapshots, writes a clip with pre-roll, and can play a deterrent on a Sonos speaker. A browser UI is how the zones get drawn; a capture loop writes YOLO labels so the stock COCO model can be fine-tuned on this kitchen.
Stay on the live frame
Consumer cameras buffer. A naive VideoCapture.read() in the inference loop will happily classify a cat that left the counter two seconds ago, then spend the next few cycles catching up. Catcam drains the FFMPEG decoder on its own thread, over TCP with the buffer stripped, and keeps only the most recent frame. The inference loop copies that one. If the camera drops, it reconnects rather than spinning on a dead handle.
What counts as on the counter
A bounding box overlapping the island is not the same as a cat standing on it. The zone test uses the bottom-center of the box — roughly where the paws meet a surface — against polygons drawn in a browser over the MJPEG view. Clicking corners out of order produces a self-intersecting bowtie whose area is a fraction of what you meant, so points are sorted around the centroid on the way in. Switch the camera from a 640 preview to the main stream and those pixel coordinates cover a postage stamp; the code notices and offers the scale.
Stock YOLO also splits confidence between cat and dog on the same animal. Those classes are merged before the threshold, otherwise a cat the model is unsure about never fires. One detection is still not an alert: it has to land in-zone on N of the last M inference frames, and a person in the frame clears the hit list, because the point of a kitchen deterrent is not to yell at whoever is cooking.
Inference itself is ONNX Runtime on CPU, at a few frames a second. Running the whole wide-angle frame letterboxed to 640 makes a distant counter a handful of pixels. Zone mode crops around each polygon, padded and floored at a minimum edge so you are not upscaling blur, then maps boxes back to full-frame coordinates. Overlapping crops are merged with NMS. Round-robin across zones keeps the loop on budget when there are several counters.
After it fires
A snapshot is the cheap record. The useful one is a clip: every loop pushes into a rolling pre-roll buffer, a trigger freezes that buffer and keeps recording until the cat has been gone for a few seconds — or hits a hard cap, so a nap on the island does not fill the disk. Encoding runs on a worker thread so the inference loop is not waiting on mp4v.
The optional deterrent is a wav served from this machine and played on a Sonos via SoCo, volume restored afterwards. The audio server and the camera URL stay in a gitignored config; the example file is what someone else copies.
Close the loop on the model
COCO has never seen this kitchen. While it runs, the process writes full-res frames and YOLO-format labels — including empty label files, which are valid negatives, on a slower cadence so the set is not all cats. The web UI can force a capture tagged missed / good / empty. Labels are the model’s current guesses, which is the point: you correct what it gets wrong and train. data.yaml is already in the layout Ultralytics expects.
- Decoder thread keeps only the freshest frame, so inference never classifies a queue of stale RTSP.
- The zone test uses the box’s bottom-center — paws on the counter, not a body hanging over it.
- Per-zone crops, so a far counter is not three pixels in a letterboxed 640 square.
- Need N hits in the last M frames, and a person in view cancels the alert.