A YOLOv8 model on a Hailo-8 accelerator that decides whether a camera's tripwire alarm deserves a human's attention, before an email reaches the monitoring agents.
The camera is not lying. It is answering an easier question than the one we care about.
A Dahua camera with IVS rules reports CrossLineDetection whenever
something crosses a tripwire. It reports it for shadows sweeping across pavement,
for headlights at night, for rain, for branches moving in wind. Every one of those
becomes an email, and every email costs a monitoring agent the seconds it takes to
open it, look, and dismiss it. Enough of them and the alerts stop being read.
The obvious fix is to run object detection on the alarm snapshot: if there is no person in the frame, drop it. That removes the shadows and the rain. It does not remove the most common false positive, and that is the whole reason this project has the shape it does.
Presence and crossing are different questions, and only one of them fits in a photograph.
So the field agent does not send a snapshot. It sends a window: about two seconds before the event and two after, sampled at 4 fps. The office side detects objects in every frame, links them into tracks, and tests whether a track's path actually intersects the rule geometry.
Neither end can accept inbound connections, so both dial out.
cloudflared gives the server an HTTPS URL; the agent
posts to it. If the link drops, packages queue on disk and retry.MQTT is the right tool for telemetry — trailer alive, camera down, temperature — and it will likely carry exactly that later. It is the wrong tool for a megabyte of JPEGs per event: large payloads work against the protocol, and it means operating a broker for something HTTP already does.
Object storage as the primary route (upload to R2, have the office poll) adds a hop and its latency to a path whose whole purpose is getting an operator's attention quickly. The failure it protects against — office unreachable — is covered more cheaply by a disk-backed retry queue on the trailer. Storage remains attractive later as an archive, not as transport.
What the office needs in order to rule on a crossing.
event.json rule geometry, timestamps, what the camera itself claimed
frames/0000.jpg analysis window, ~4 fps, 640 px long side, q80
hd.jpg full-quality frame, for the operator's email
| Contents | What it is for |
|---|---|
| 12–16 frames at 4 fps | detect the object and reconstruct its path |
| Tripwire geometry (8192 space) | something to test the path against |
| The camera's own claim | compare both filters; audit later |
| Per-frame timestamps | order the trajectory |
| One full-resolution frame | what the agent actually looks at |
Analysis frames are downscaled deliberately. The network resizes everything to 640×640 regardless, so shipping 4K over a metered link buys pixels that are thrown away before inference. Only the operator's frame keeps full quality. Rule geometry travels in Dahua's fixed 8192×8192 space, exactly as the camera config reports it, so it stays valid at any stream resolution.
confirmed
A track of a person or vehicle crosses the rule. The email goes out.
rejected — "object present but its path never crossed the rule"
Someone walked beside the tripwire. The camera fired; we do not. This is the verdict that a snapshot-only filter cannot produce, and the reason for the frame window.
rejected — "no object of interest held across frames"
Shadow, headlight, rain, branch. An object must appear in at least two frames to count: a single-frame confidence spike is exactly what a reflection produces.
Crossing is tested at the object's ground point — the bottom-centre of its box, where feet meet the floor. Using the box centre would register a tall person as having crossed while they are still on the near side.
Everything below was discovered the hard way and is written down so it is discovered only once.
| Constraint | Consequence |
|---|---|
| HailoRT 4.15.0 ships wheels for cp38/39/310 only | Python 3.10 is mandatory; 3.13 cannot talk to the chip |
| Runtime 4.15 pairs with Model Zoo v2.9.0 / DFC 3.25.0 | HEFs from other releases will not load at all |
hailort pins numpy 1.23.3; netifaces has no cp310 wheel |
install with --no-deps; netifaces only finds Hailos over Ethernet, and this one is PCIe |
| v2.9.0 YOLOv8 HEFs carry no on-chip NMS | raw head exposed; DFL decode, sigmoid and NMS written by hand |
| The chip serves one process at a time | the server must own it exclusively and serialize inferences |
| Dahua firmware speaks legacy TLS | OpenSSL 3 refuses it by default; cameras look dead while answering |
The failure mode worth remembering
Error 74, HAILO_OUT_OF_PHYSICAL_DEVICES, means another process
already holds the accelerator. It appears whenever a previous server is left
running — including one orphaned by a closed SSH session. Kill the old process
before starting a new one.
| Metric | Value | Note |
|---|---|---|
| Inference, yolov8m @ 640×640 | 17.8 ms | ~56 fps |
| End to end over HTTP, one image | 21.4 ms | including decode and NMS |
| A 16-frame event window | ~250 ms | negligible beside the network |
| Package size per event | ~1 MB | sized for LTE |
| Model accuracy (COCO mAP) | 50.08 | yolov8l at 52.61 also fits comfortably |
The verdict logic is covered by a synthetic test that pans a still image to fabricate motion. It asserts both outcomes that matter: a moving object is confirmed as crossing, and a stationary one beside the line is rejected with the "never crossed" reason. Both pass.
| File | Role | State |
|---|---|---|
yolo_hailo.py |
YOLOv8 on the Hailo-8: preprocess, infer, DFL decode, NMS, rescale | working |
server.py |
HTTP service, stdlib only. Single image, event window, health | working |
event_filter.py |
Tracking and the geometric crossing test | working |
trailer_agent.py |
Field side: ring buffer, IVS trigger, packaging, retry queue, email | running |
make_event.py, smoke_test.py |
Build packages from clips; exercise the verdict logic offline | working |
| Cloudflare Tunnel | Public HTTPS entry point for the office server | pending |
| Windows service | So the server survives a reboot and a closed session | pending |
| Live viewer | Boxes from both filters drawn together | pending |
Dependencies were held to numpy and opencv on
purpose. After the fight with a missing C compiler, a server that needs nothing
installed is one less thing to break at three in the morning.
Thresholds are untuned. Confidence is at 0.45 and a track must span two frames. Those numbers are guesses until real events accumulate. The server is currently archiving rejections as well as confirmations precisely so they can be reviewed and the thresholds set from evidence rather than intuition.
Deduplication is not built yet. When it is, it must key on the track or the event — never on position. Position-based dedup was already tried on a related system and it silently discarded legitimate new arrivals.
Night performance is unknown. Every measurement so far is daylight or synthetic. IR illumination, motion blur and rain all change detection confidence, and the thresholds that work at noon may not hold at 3am.