fix: keep only the anchor-free PPU verdict, re-read anchor-based each frame

This commit is contained in:
aahmetdurmaz 2026-09-07 01:59:27 +03:00
parent 82dcdca8c0
commit dec97f674f
2 changed files with 46 additions and 28 deletions

View File

@ -684,8 +684,8 @@ class DeepxDetector(DetectionApi):
self.session = InferenceEngine(str(model_path), options)
self.output = self.inspect_model(config)
self.logged_layout = False
# settled on the first frame that carries a record, then kept
self.ppu_anchor_based: bool | None = None
# set once a frame proves the PPU head anchor-free, then kept
self.ppu_anchor_free = False
self.ppu_anchor_based_reported = False
self.damoyolo_outputs_reported = False
@ -757,12 +757,16 @@ class DeepxDetector(DetectionApi):
return post_process_yolo(outputs, self.width, self.height)
def decode_ppu(self, outputs: list[np.ndarray]) -> np.ndarray:
"""Decode PPU records, settling the head on the first conclusive
frame."""
if self.ppu_anchor_based is None:
self.ppu_anchor_based = ppu_layout_is_anchor_based(outputs)
"""Decode PPU records. A box value above 1 proves the head is
anchor-free and that is kept; the anchor-based verdict is re-read on
every frame, so a sub-pixel box cannot lock the model out."""
if self.ppu_anchor_free:
anchor_based = False
else:
anchor_based = ppu_layout_is_anchor_based(outputs)
self.ppu_anchor_free = anchor_based is False
if self.ppu_anchor_based:
if anchor_based:
if not self.ppu_anchor_based_reported:
self.ppu_anchor_based_reported = True
logger.error(
@ -773,15 +777,15 @@ class DeepxDetector(DetectionApi):
return np.zeros((20, 6), np.float32)
# None here means no frame has carried a record yet, which leaves the
# heuristic to run again on the next one
# None here means nothing conclusive in this frame, and no pixel
# box to draw either
return decode_ppu(
outputs,
self.width,
self.height,
SCORE_THRESHOLD,
NMS_THRESHOLD,
anchor_based=self.ppu_anchor_based,
anchor_based=anchor_based,
)
def decode_damoyolo(self, outputs: list[np.ndarray]) -> np.ndarray:
@ -846,4 +850,4 @@ class DeepxDetector(DetectionApi):
array.dtype,
array.min() if array.size else "n/a",
array.max() if array.size else "n/a",
)
)

View File

@ -707,9 +707,8 @@ class TestDeepxDetectorLayout(unittest.TestCase):
self.assertTrue(np.all(first == 0) and np.all(second == 0))
self.assertEqual(len([r for r in logs.records if r.levelname == "ERROR"]), 1)
def test_the_ppu_head_is_settled_once_and_kept(self):
"""Which head a PPU model was compiled from is a property of the
model. A later frame whose boxes all happen to fall under 1.0 must not
def test_anchor_free_is_kept_once_proven(self):
"""A later frame whose boxes all happen to fall within 1.0 must not
re-open the question and start dropping detections."""
detector, _ = self._detector(
ModelTypeEnum.yologeneric, [{"shape": [8400]}], ppu=True
@ -720,7 +719,7 @@ class TestDeepxDetectorLayout(unittest.TestCase):
]
first = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8))
self.assertFalse(detector.ppu_anchor_based)
self.assertTrue(detector.ppu_anchor_free)
detector.session.run.return_value = [build_ppu_record((0.6, 0.4, 0.3, 0.7))]
second = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8))
@ -730,11 +729,33 @@ class TestDeepxDetectorLayout(unittest.TestCase):
# decoded as pixels, which is a sub-pixel box in the corner
self.assertAlmostEqual(second[0][1], 0.9, places=5)
def test_an_empty_or_all_zero_ppu_frame_leaves_the_head_undecided(self):
def test_an_anchor_based_verdict_is_not_kept(self):
"""An anchor-free model whose first record is a sub-pixel box in the
corner looks anchor-based for that frame only; the next normal box
decodes and settles the head."""
detector, _ = self._detector(
ModelTypeEnum.yologeneric, [{"shape": [8400]}], ppu=True
)
detector.width, detector.height = 640, 640
detector.session.run.return_value = [build_ppu_record((0.6, 0.4, 0.3, 0.7))]
with self.assertLogs("frigate.detectors.plugins.deepx", level="ERROR"):
first = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8))
self.assertTrue(np.all(first == 0))
self.assertFalse(detector.ppu_anchor_free)
detector.session.run.return_value = [
build_ppu_record((320.0, 160.0, 64.0, 32.0), label=3)
]
second = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8))
self.assertEqual(second[0][0], 3)
self.assertTrue(detector.ppu_anchor_free)
def test_an_empty_or_all_zero_ppu_frame_proves_nothing(self):
"""Only a box value above 1 (anchor-free) or a frame of values
strictly inside 0..1 (anchor-based) is evidence; an empty frame or a
degenerate all-zero record is neither and must not freeze the model
as anchor-based."""
degenerate all-zero record is neither."""
detector, _ = self._detector(
ModelTypeEnum.yologeneric, [{"shape": [8400]}], ppu=True
)
@ -748,15 +769,8 @@ class TestDeepxDetectorLayout(unittest.TestCase):
detections = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8))
self.assertTrue(np.all(detections == 0))
self.assertIsNone(detector.ppu_anchor_based)
detector.session.run.return_value = [
build_ppu_record((320.0, 160.0, 64.0, 32.0), label=3)
]
detections = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8))
self.assertFalse(detector.ppu_anchor_based)
self.assertEqual(detections[0][0], 3)
self.assertFalse(detector.ppu_anchor_free)
self.assertFalse(detector.ppu_anchor_based_reported)
def test_damoyolo_is_checked_against_the_model_at_load(self):
"""A YOLO model or a wrong label map under damo-yolo fails at load
@ -878,4 +892,4 @@ class TestDeepxDetectorLayout(unittest.TestCase):
self.assertEqual(detections[0][0], 5)
self.assertAlmostEqual(detections[0][1], 0.8, places=5)
self.assertAlmostEqual(detections[0][4], 300 / 640, places=5)
self.assertAlmostEqual(detections[0][4], 300 / 640, places=5)