XCAICX docs Product Contact

Zones, tripwires & events

How detections become the events your VMS acts on: what fires, exactly when, and how often.

Coordinates#

All geometry is normalised to [0,1], origin at top-left. A zone defined once survives a resolution change, a sensor swap and a crop change — which is the whole reason it is not in pixels.

c
const xcaicx_point bay[] = {
    {0.55f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.55f, 1.0f}
};

The reference point#

A track is tested by the midpoint of its bottom edge — the ground contact point — not the box centre.

text
┌─────────┐
│         │
│  track  │
│         │
└────●────┘   ← this point is what enters and exits zones

Note

Using the box centre makes a tall person "enter" a floor zone while they are still a metre outside it, and makes a person standing at the edge flap in and out as their box height changes with posture. The bottom-edge midpoint is what a floor plan actually means.

Zones#

c
int32_t zone_id = -1;
xcaicx_stream_add_zone(stream, "packing-bay", bay, 4,
                       /*dwell_seconds=*/30, &zone_id);
python
s.add_zone("packing-bay", [(0.55, 0), (1, 0), (1, 1), (0.55, 1)], dwell_seconds=30)

A zone is a polygon of three or more points. Membership is an even-odd crossing test, in the form that handles a vertex sitting exactly on the ray consistently — which is what stops a track resting on a zone edge from flapping enter/exit forever.

Polygons may be concave. They are not required to be convex, and winding order does not matter.

Zone events#

EventFires when
XCAICX_EVENT_ZONE_ENTERThe reference point moves from outside to inside
XCAICX_EVENT_ZONE_EXITIt moves from inside to outside
XCAICX_EVENT_DWELL_EXCEEDEDIt has been continuously inside for dwell_seconds

Each carries zone_id, zone_name, track_id, the track's class in detail, its score, and pts_us.

Dwell#

dwell_seconds = 0 disables the dwell timer for that zone.

Dwell is measured from the pts_us of the entering frame, so it is video time, not wall time — a stream running at half speed still produces correct dwell measurements, and a stalled pts_us produces none.

Careful

DWELL_EXCEEDED fires once per visit, not once per frame. Leaving and re-entering arms it again. If you are counting dwell events to bill or to alarm, that once-per-visit semantic is the one to build against.

Tripwires#

c
int32_t line_id = -1;
xcaicx_stream_add_line(stream, "doorway",
                       (xcaicx_point){0.5f, 0.0f},
                       (xcaicx_point){0.5f, 1.0f}, &line_id);

A tripwire is directed. Crossing from the a side to the b side is reported with detail = "a_to_b"; the other way is "b_to_a". That is what makes an entry/exit counter possible from one line:

c
if (e->kind == XCAICX_EVENT_LINE_CROSS) {
    if (strcmp(e->detail, "a_to_b") == 0) ++entered;
    else                                   ++exited;
}

Crossing is detected by a sign change in the signed area of the triangle (a, b, p), and is only counted if the crossing point falls within the segment's span — not on the infinite line through it. A person walking well past the end of a short tripwire does not trip it.

Module events#

Two events come from the modules rather than from geometry:

Eventdetail valuesSource
XCAICX_EVENT_PPE_VIOLATIONno_helmet, no_vestPPE module
XCAICX_EVENT_DEFECT_FOUNDscratch, blemishDefect module
XCAICX_EVENT_PLATE_READThe plate text, or plate_candidateANPR module

PPE reports the absence, not the presence, because the actionable and billable event on a factory floor is the violation. A compliant worker produces no event, which also keeps the event stream proportional to what needs attention.

PLATE_READ carries plate_candidate when the build has no text model — the reference backend localises plates but cannot read them. With a production ANPR bundle, detail and detection.text carry the plate string and text_score its confidence.

Reading events#

c
for (size_t i = 0; i < res->event_count; ++i) {
    const xcaicx_event *e = &res->events[i];
    log_event(e->kind,
              e->zone_name ? e->zone_name : "",
              e->detail    ? e->detail    : "",
              e->track_id, e->score, e->pts_us);
}
python
for ev in result.events:
    print(ev.kind.name, ev.zone_name, ev.detail, ev.track_id)

Events are valid until the next call on the stream. Copy the strings if you queue them.

Track lifecycle and event fidelity#

Events are only generated for confirmed tracks — those that have been seen min_hits (default 3) times. A one-frame false positive therefore cannot generate a zone entry.

The cost is that a genuinely fast object crossing the frame in two frames will not produce events either. On a fast conveyor or a road camera, that is the trade-off to be aware of; raising the frame rate is the fix, not lowering min_hits, because lowering it re-admits the false positives.

When a track is lost for longer than max_age (default 15 frames of coasting) it is retired and its zone/tripwire state is reaped. If it reappears it is a new track id, and a zone it is standing in will fire a fresh ZONE_ENTER.

Note

That re-entry behaviour is why occupancy should be computed from live track membership rather than by counting ZONE_ENTER minus ZONE_EXIT over a long period. Occlusion behind a pillar longer than max_age will otherwise inflate your count.

Designing zones that behave#

  • Anchor zones to the floor. The reference point is at ground level, so a zone drawn around a person's torso in the image never triggers.
  • Keep tripwires away from frame edges. A track is only confirmed after min_hits frames, so an object entering at the very edge may cross before it is confirmed. Place the line a little inside the frame.
  • Prefer one tripwire to two zones for counting through a doorway. Direction comes for free and there is no dwell state to reason about.
  • Do not overlap zones you intend to count separately — a single track inside both produces events for both, which is correct but is often not what the operator drew.

Tuning#

SymptomChange
Enter/exit flapping on a boundaryMove the boundary off the natural walking line; the even-odd test is already hysteresis-free by design
Events missing for fast moversRaise frame rate, or lower detect_every_n
Too many spurious detectionsRaise detect_threshold
One object producing several boxesLower nms_threshold (more aggressive merging)
Two adjacent objects merged into oneRaise nms_threshold

Next#

Commercial software. Use requires a valid XCAICX licence token. Questions an integrator cannot answer from this page belong in an email to [email protected] — and, usually, in a fix to this page.

© 2026 AZMX AI · xcaicx.com