feat(ml): make zone resolution selectable with zone_match_strategy - #69
feat(ml): make zone resolution selectable with zone_match_strategy#69jantman wants to merge 1 commit into
Conversation
Zone matching is bounding-box intersection, not containment, so a
detection can land in several zones at once. Until now a zone that
rejected a detection by pattern simply fell through to the next zone,
letting any later zone it clipped keep it. An exclusion zone bordering a
permissive one was therefore defeated by a few pixels of overlap, and an
ES 6 config -- written against pyzm 0.3.x, where the first intersecting
zone decided -- silently widened on migration with no way back.
Add ZoneMatchStrategy and a zone_match_strategy setting on
DetectorConfig, read from the top-level ml_sequence general section:
any_matching keep on any intersecting zone whose pattern matches
(unchanged, still the default)
first_intersecting the first intersecting zone decides (pyzm 0.3.x)
largest_overlap the zone covering most of the box decides
Under the two new strategies a rejection is final -- by ignore_pattern
or by a pattern mismatch -- which is what makes "never alert on this
label here" expressible. The default is unchanged so upgrading does not
alter which detections survive.
refs ZoneMinder#68
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYSaA3uVUvRUku9LApa3pq
Provisional soak report — deployed on live trafficStatus: provisional. This PR stays in Draft for now; I'm posting evidence as it accumulates rather than waiting. Planning to convert to Ready next week once there's a fuller soak behind it. Running this branch on a live 10-camera ZoneMinder since 2026-08-18, with The case the whole change exists forA real detection where the old and new behaviour genuinely disagree:
That 1.3% sliver rescuing a detection whose bounding box is 11% inside an explicit exclusion zone is exactly the failure mode in #68, and Correctness cross-checkBefore deploying, I replayed a 48-hour corpus of real detections through both
Exact agreement on every detection. Note monitor 1 under Live outcomes have matched the configured strategy's verdict on every detection observed so far (16 at time of writing). One documentation suggestionThe Nothing for this PR to fix; pyzm can't see where the key was written. But a sentence in Caveats
|
Fixes the zone-resolution problem described in #68.
The problem
Zone matching is bounding-box intersection, not containment, so one detection can land in several zones at once. Until now a detection rejected by one zone's
patternfell through and could be kept by any later zone it also intersected. An exclusion zone bordering a permissive one was therefore defeated by any object whose box clipped a few pixels across the shared edge, and an ES 6 config — written against pyzm 0.3.x, where the first intersecting zone decided — silently widened on migration with no way back.The change
New
ZoneMatchStrategyenum and azone_match_strategysetting onDetectorConfig, read from the top-levelml_sequence.generalsection:any_matchingpatternmatches. Unchanged, and still the default.first_intersectinglargest_overlapUnder the two new strategies a rejection is final — by
ignore_patternor by apatternmismatch — so an exclusion zone cannot be overridden by a zone the box merely clips. That is what makes "never alert on this label here" expressible, which #68 notes is currently impossible even withignore_pattern.zone_match_strategyis a global-only key: zone filtering runs once, after all model types have been sequenced, so a per-type section logs the usual "no per-type effect" warning.The default is unchanged. Upgrading pyzm does not alter which detections survive; the new strategies are opt-in.
pyzm/models/config.py— enum, config field,from_dictparsing,_GLOBAL_ONLY_KEYSpyzm/ml/filters.py—filter_by_zone()gains astrategyargument (defaulted, so existing callers are unaffected); one helper per strategy plus a shared_zone_verdict()for the two where a single zone decidespyzm/ml/pipeline.py,pyzm/ml/detector.py— both call sites passconfig.zone_match_strategydocs/guide/detection.rst— new "Resolving overlapping zones" section, cross-referenced from theignore_patterndocs; also drops a stale docstring claim that an empty zone list synthesises a full-image zone (it doesn't — filtering is skipped)Tests
Written first, watched fail. 20 new cases in
tests/test_ml/test_filters.pybuilt on the exact sliver scenario from the issue — verified with shapely thatstreetholds 70.7% of the car's box anddrivewayfar10.4%, matching the numbers reported there. They cover the order-dependence offirst_intersecting, the order-independence and tie-breaking oflargest_overlap, terminalignore_pattern, and rejection of an unknown strategy value.Plus config-parsing tests and wiring tests that exercise the real filter through both
ModelPipeline.run()andDetector._apply_filters(). The wiring tests were mutation-checked: removing thestrategy=kwarg from either call site turns them red.Tier-1 gate: 1120 passed, 81 skipped. 6 pre-existing failures remain (5 PyJWT/starlette deprecation warnings promoted to errors by
filterwarnings=error, 1 alpr subprocess test) — they fail identically onmasterin the same environment and are unrelated to this change. Tier-2 (make release-gate) not run; it needs models and a live ZoneMinder.refs #68
🤖 Generated with Claude Code
https://claude.ai/code/session_01CYSaA3uVUvRUku9LApa3pq