diff --git a/frigate/detectors/plugins/deepx.py b/frigate/detectors/plugins/deepx.py index f8cd19480e..dcda843b26 100644 --- a/frigate/detectors/plugins/deepx.py +++ b/frigate/detectors/plugins/deepx.py @@ -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", - ) + ) \ No newline at end of file diff --git a/frigate/test/test_deepx.py b/frigate/test/test_deepx.py index 8e830bb19a..14d30518a6 100644 --- a/frigate/test/test_deepx.py +++ b/frigate/test/test_deepx.py @@ -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) \ No newline at end of file