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.
Each answers a different question. Every one of these exists because a real event exposed the need for it.
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 — "object present but stationary (parked or idle)"
Detection boxes wobble a few pixels between frames even on a parked car. When a rule line runs underneath that car, the wobble alone can walk its ground point across the line and a plain intersection test calls that a crossing. An object must travel more than 15 px before we ask whether it crossed — a flat figure, chosen because measured wobble on stationary vehicles ran 0.1–8.9 px regardless of their size. An earlier version scaled the threshold to the object's width and had to be withdrawn: it demanded far too much of large vehicles and could dismiss a slow arrival as parked.
rejected — "no object of interest held across frames"
An object must appear in at least two frames to count: a single-frame confidence spike is exactly what a reflection produces.
rejected — "camera reported a X where we detected nothing"
The most important verdict, and the last one added. Reporting "the object was parked" when the thing we tracked is not the thing the camera flagged hides our own misses behind a statement about something else. This separates "we looked, and it was not doing what the camera said" from "we never saw it at all" — and only the second kind points at a failure on our side.
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.
The most expensive lesson of the first morning, and the least intuitive.
The filter was missing real people. A camera reported a person crossing, we detected nothing, and the evidence photo looked empty — so the verdict was recorded as the camera hallucinating. It was not. Someone who knew the site looked at the same picture and saw the person immediately.
The obvious response is to send higher-resolution frames. It does nothing. The detector resizes whatever it receives to 640×640 before inference, so a 1080p frame and a 480p frame arrive at the model identically, and a distant person shrinks back to a handful of pixels either way. Measured on the same scene, the substream at 640, the substream at native size and the main stream at native size all returned exactly the same detections.
What works is cropping instead of scaling. The event tells us where the camera saw something; the agent cuts that region out of the main stream at native scale and ships it alongside the frames. The crop is small in bytes precisely because it is small in area, and the person inside it is no longer 45 pixels tall but around 125.
The result
The same event that had been rejected came back confirmed, person at 0.90, tracked across 19 of 21 frames — with 62 of those detections coming from the crops and none from the full frames. The full-frame pass never saw her at all.
One consequence worth planning for: raw frames at 1080p cost about 6 MB each, and an eight-second buffer per camera became 750 MB. Four agents exhausted the machine's paging file. The buffer now retains 5 fps instead of 15 — every frame is still read so the decoder stays current, but only the ones we would actually ship are kept. Four cameras now run in 1.5 GB and under 6% CPU.
Six real events from the first morning: what the trailer sent, what the office concluded, and whether it was right. Amber is what the camera claimed; green is what the Hailo found; red is the rule.
Rule1 24/7 ·
camera claimed Vehicleconfirmed — car at 0.94, tracked
18/18 frames, 369 msThis entry originally appeared here as a false positive, and that verdict-about-the-verdict was wrong. The author looked at this single frame, saw a car sitting still, and concluded the system had been fooled by box wobble. The site operator knew the vehicles had arrived at that hour: the car was moving through the window and came to rest by the end of it. One frame cannot show motion — which is the same lesson the whole system is built on, applied to the person reading the evidence rather than the machine.
The rule geometry is still wrong here: eight vertices where the last two double back, producing the red segments that cut across the frame. That remains worth fixing on the camera.
Rule2 24/7 ·
camera claimed Vehicleconfirmed — truck at 0.90,
tracked 18/18 framesSeeing the same pattern twice felt like confirmation of a defect. It was
confirmation of a habit: reading motion out of a still image. Acting on that
reading introduced a real bug — a rule that an object must travel more than
30% of its own width before the crossing test runs. That sounds
principled and is not: box wobble does not scale with vehicle size, so the
rule demanded 76 px of a car and 15 px of a person, and a vehicle easing into
a space could be dismissed as parked. One archived rejection with 24 px of
travel and a Vehicle report from the camera looks like exactly
that failure.
Measured across real events, wobble on genuinely stationary vehicles ran between 0.1 and 8.9 px regardless of their size. The threshold is now a flat 15 px, which clears the wobble with margin and stops punishing slow arrivals.
Rule1 24/7 ·
camera claimed Human at
[160, 256 → 4608, 5824]rejected — no object of interest
held across frames; nothing glimpsed at allThe camera's own box gives it away: in the 8192-unit rule space it spans more than half the frame. Whatever it reacted to was the size of a building, not a person — mid-morning tree shadows sweeping across the lawn. This is the everyday false positive the whole project exists to absorb.
Human at [5688, 1488 → 6088, 3488]rejected — camera reported a
Human where we detected nothing; the objects we did see (car, truck)
moved 4.2 pxThe verdict was written off as the camera hallucinating — the author looked at this washed-out image and saw an empty pavement. The site operator looked at the same picture and saw the person immediately. In the substream she is roughly 45×40 pixels, and the detector shrinks every frame to 640×640 before inference, so she all but disappears.
This single case drove the largest change in the system: the agent now crops the region the camera points at, at native scale, and sends it alongside the frames. Note also the 4.2 px figure — that is the jitter from examples 1 and 2, measured.
Rule2 ·
camera claimed Humanrejected — nothing of interest
detected there; the truck in view moved 0.1 pxUnlike example 3, the camera is not imagining things — the amber box sits squarely on a real object. It is simply not a person: against the utility cabinet beside it, the shape is about 30 cm. This is why the verdict wording matters. "We detected nothing there" is a claim that can be checked against a picture; "the camera was wrong" is a conclusion that needs one.
Humanconfirmed — person at 0.90,
tracked 17/17 frames, travelled 106 px, 59 detections from the
cropsThe same camera and the same kind of subject as example 4, now resolved correctly. The decisive number is 59: that many detections came from the cropped regions, and the full-frame pass contributed none. Without the crops this event would have been another silent miss.
The amber box sitting slightly below the green one is the camera's own offset, not ours — visible only once both opinions are drawn on the same picture.
What the examples have in common
Of the six, the filter got five right. The single genuine failure was example 4 — a real person we never detected — and it was found only because someone who knew the site contradicted the recorded verdict.
The more uncomfortable pattern is in examples 1 and 2. Both verdicts were correct; the analysis of them was not. Judging motion from a still frame produced a confident diagnosis, and that diagnosis produced a threshold that could reject genuine slow arrivals. A wrong explanation for a right answer is still expensive, because it gets built on.
Every correction on this page came from someone looking at a picture, not from reasoning about numbers. That is why evidence images carry both opinions, why rejections are archived beside confirmations, and why the raw event packages are now kept: so a disputed verdict can be re-examined instead of argued about.
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 |
| Inference, yolov8l @ 640×640 | 33.2 ms | mAP 52.61 |
| Inference, yolov8x @ 640×640 | 51.9 ms | mAP 53.61 — in use |
| Inference, yolov7e6 @ 1280×1280 | 125.3 ms | mAP 55.37, NMS on-chip — candidate |
| End to end over HTTP, one image | 21.4 ms | including decode and NMS |
| Crossing to email | ~8 s | measured on real crossings |
| Package size, substream only | ~550 KB | before native-scale crops |
| Package size, with crops | ~1.9 MB | the cost of actually seeing people |
| Camera event lag | 0.6–2 s | only measurable after correcting clock skew |
| Four agents, 1080p | 1.5 GB / 6% | RAM and CPU on the field machine |
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.
Most of the delay is a deliberate wait, not slowness. Knowing which part is which is what makes it safe to leave alone.
| Stage | Time | Nature |
|---|---|---|
| Person crosses → camera emits the event | 0.6–2 s | the camera's own processing; outside our control |
| Event → package built | 3 s | deliberate — waiting for the frames after the crossing |
| Package → verdict | 2 s | upload plus ~40 inferences |
| Verdict → email delivered | 2 s | mostly the TLS handshake and login, repeated every time |
| Crossing → agent has the email | ~8 s | with an annotated snapshot and a stated reason |
The three seconds spent building the package are the price of the whole design. After an event arrives, the agent waits two more seconds so the frames following the crossing land in the buffer. Without them there is no trajectory, and without a trajectory we are back to being unable to tell someone crossing a line from someone walking beside it. Cutting that wait would make the system faster and worse.
Two of the remaining stages are recoverable without touching decision quality: the SMTP session is rebuilt on every send rather than held open, and there is a half-second safety margin after the window that could be trimmed. Together they would bring this closer to five or six seconds. That work is deferred on purpose — eight seconds from a crossing to a verified email with a photograph is comfortably inside what monitoring needs, and the camera's own unverified alerts do not arrive faster.
| 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 sits at 0.30 with yolov8l — more sensitive than it will end up, chosen deliberately after the missed person. Extra detections can be discarded by the trajectory test; something never detected cannot be recovered. The server archives every rejection and now the raw event package too, so thresholds can be raised against recorded evidence instead of intuition.
Verdicts are only spot-checked, and the checks themselves have erred. Of the first events, six were audited against their images. Five verdicts were correct. But two of those five were initially recorded here as failures, and one genuine failure was initially recorded as a success — each corrected only when the site operator, who knew what had actually happened, contradicted the write-up. Reviewing a proper sample is the next real work, and it should be done by someone who knows the location.
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.