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 running against four cameras, verifying real events Hardware Hailo-8, 26 TOPS · yolov8x Latency ~8 s from crossing to email

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.

The verdicts

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.

Resolution: why more pixels did not help

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.

Worked examples

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.

1 — Vehicle arriving, correctly confirmed

right — confirmed
A white car in a parking lot, boxed in
    green as 'car 0.94', with a red rule line zigzagging across the scene
Sent by trailer
18 frames, 887 KB · rule Rule1 24/7 · camera claimed Vehicle
Office concluded
confirmed — car at 0.94, tracked 18/18 frames, 369 ms
Reality
Correct. The car had just arrived and crossed the tripwire on its way in.

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

2 — And the mis-diagnosis that followed

right verdict, wrong conclusion
A white pickup truck at a kerb,
    boxed in green as 'truck 0.80', with red rule segments crossing over it
Sent by trailer
18 frames, 572 KB · rule Rule2 24/7 · camera claimed Vehicle
Office concluded
confirmed — truck at 0.90, tracked 18/18 frames
Reality
Also correct — another arrival.

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

3 — Camera sees a person in an empty garden

right — rejected
An empty lawn with shrubs and tree
    shadows, crossed by a red V-shaped rule line, with no people present
Sent by trailer
18 frames · rule Rule1 24/7 · camera claimed Human at [160, 256 → 4608, 5824]
Office concluded
rejected — no object of interest held across frames; nothing glimpsed at all
Reality
Correct. No email sent.

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

4 — A real person we failed to see

wrong — false negative
A parking area with a white pickup
    and other vehicles; an amber box on the far pavement marks where the camera
    reported a human, with no green box anywhere
Sent by trailer
18 frames, 577 KB · camera claimed Human at [5688, 1488 → 6088, 3488]
Office concluded
rejected — camera reported a Human where we detected nothing; the objects we did see (car, truck) moved 4.2 px
Reality
There was a person there. We missed her.

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

5 — Something is there, but it is not a person

right — rejected
A grass verge beside a path, with a
    small pale object on the lawn marked by an amber box labelled camera Human,
    next to a green utility cabinet for scale
Sent by trailer
18 frames, 2720 KB · rule Rule2 · camera claimed Human
Office concluded
rejected — nothing of interest detected there; the truck in view moved 0.1 px
Reality
Correct. A bird or wind-blown litter, confirmed by the operator.

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

6 — A genuine crossing, after the fixes

right — confirmed
A pedestrian walking along a pavement
    beside parked vehicles, boxed in green as 'person 0.90' with the camera's
    amber box slightly lower
Sent by trailer
17 frames + native-scale crops, 1761 KB · camera claimed Human
Office concluded
confirmed — person at 0.90, tracked 17/17 frames, travelled 106 px, 59 detections from the crops
Reality
Correct. Email sent, 6 s after she crossed.

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

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
Inference, yolov8l @ 640×64033.2 msmAP 52.61
Inference, yolov8x @ 640×64051.9 msmAP 53.61 — in use
Inference, yolov7e6 @ 1280×1280125.3 msmAP 55.37, NMS on-chip — candidate
End to end over HTTP, one image21.4 msincluding decode and NMS
Crossing to email~8 smeasured on real crossings
Package size, substream only~550 KBbefore native-scale crops
Package size, with crops~1.9 MBthe cost of actually seeing people
Camera event lag0.6–2 sonly measurable after correcting clock skew
Four agents, 1080p1.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.

Where the eight seconds go

Most of the delay is a deliberate wait, not slowness. Knowing which part is which is what makes it safe to leave alone.

StageTimeNature
Person crosses → camera emits the event0.6–2 s the camera's own processing; outside our control
Event → package built3 s deliberate — waiting for the frames after the crossing
Package → verdict2 s upload plus ~40 inferences
Verdict → email delivered2 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.

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