Jatagan · Engineering Note

Jatagan Filter — second-stage AI verification

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.

Status office side complete, field agent running Hardware Hailo-8, 26 TOPS Latency 17.8 ms/frame

The problem

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.

Why one frame is not enough

Presence and crossing are different questions, and only one of them fits in a photograph.

Real crossing tripwire path intersects the line Walking alongside tripwire path never intersects it The highlighted frame is identical in both: a person, near the line. Only the grey frames before and after tell them apart.
The false positive that survives object detection. Someone standing beside a tripwire, or walking parallel to it, produces the same photograph as someone stepping over it. A single frame cannot refute the camera. A trajectory can.

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.

Architecture

Neither end can accept inbound connections, so both dial out.

Trailer (field) Dahua camera RTSP substream eventManager.cgi (IVS) trailer_agent.py 8 s ring buffer window extraction disk retry queue LTE, no inbound HTTPS ~1 MB Cloudflare Tunnel Office box server.py POST /detect-event event_filter.py detect every frame link tracks by IoU test path vs geometry yolo_hailo.py → Hailo-8 confirmed only → email with annotated snapshot Verdicts confirmed present, no crossing nothing of interest
Outbound-only by design. In production the trailer sits behind an LTE modem with no public address, and the office box has no open ports. cloudflared gives the server an HTTPS URL; the agent posts to it. If the link drops, packages queue on disk and retry.

Why not MQTT, and why not object storage

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.

The event package

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
ContentsWhat it is for
12–16 frames at 4 fpsdetect the object and reconstruct its path
Tripwire geometry (8192 space)something to test the path against
The camera's own claimcompare both filters; audit later
Per-frame timestampsorder the trajectory
One full-resolution framewhat 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.

Three verdicts, not two

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.

The accelerator, and what it cost to get working

Everything below was discovered the hard way and is written down so it is discovered only once.

ConstraintConsequence
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.

Measured

MetricValueNote
Inference, yolov8m @ 640×64017.8 ms~56 fps
End to end over HTTP, one image21.4 msincluding decode and NMS
A 16-frame event window~250 msnegligible beside the network
Package size per event~1 MBsized for LTE
Model accuracy (COCO mAP)50.08yolov8l 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.

Components

FileRoleState
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.

Open questions

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.