From 238073f9431901775b1f8a0f0f88f8cb55bbbe15 Mon Sep 17 00:00:00 2001 From: aahmetdurmaz Date: Thu, 10 Sep 2026 20:26:00 +0300 Subject: [PATCH] fix: assume the largest PPU anchor table when the .dxnn has no layout --- frigate/detectors/plugins/deepx.py | 26 ++++++----- frigate/test/test_deepx.py | 71 +++++++++++++++--------------- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/frigate/detectors/plugins/deepx.py b/frigate/detectors/plugins/deepx.py index 538e63cb0c..0194873b92 100644 --- a/frigate/detectors/plugins/deepx.py +++ b/frigate/detectors/plugins/deepx.py @@ -77,9 +77,6 @@ PPU_ANCHORS_BY_SCALES = { } PPU_MAX_STRIDE = 32 -# the largest scale count we have a table for; a count below this with no -# table of its own is under-evidenced rather than disproven, so it still -# gets the biggest table as a best guess PPU_MAX_KNOWN_SCALES = max(PPU_ANCHORS_BY_SCALES) # .dxnn: "DXNN", a 4-byte LE version, then a JSON index padded to @@ -627,15 +624,18 @@ def ppu_scale_count_lower_bound(records: np.ndarray, width: int, height: int) -> def resolve_scale_count( records: np.ndarray, scale_count: int | None, width: int, height: int ) -> int: - """`scale_count` if given, else the fewest scales this frame alone - proves, defaulting to 3 when the frame has no records to read.""" + """`scale_count` if given, else PPU_MAX_KNOWN_SCALES unless this frame + proves more; a frame showing only the finer layers near the origin is + what a bigger head looks like too, so it never argues for fewer.""" if scale_count is not None: return scale_count if len(records) == 0: - return 3 + return PPU_MAX_KNOWN_SCALES - return ppu_scale_count_lower_bound(records, width, height) + return max( + PPU_MAX_KNOWN_SCALES, ppu_scale_count_lower_bound(records, width, height) + ) def ppu_grid_regression_geometry( @@ -1047,10 +1047,11 @@ class DeepxDetector(DetectionApi): if layout is None: logger.warning( - "Could not read the PPU head layout from %s; inferring its " - "scale count from the detections instead, which can misplace " - "boxes until a record proves every scale", + "Could not read the PPU head layout from %s; assuming a " + "%d-scale head, so a head with fewer scales will decode to " + "misplaced boxes", model_path, + PPU_MAX_KNOWN_SCALES, ) return None @@ -1182,7 +1183,9 @@ class DeepxDetector(DetectionApi): def decode_ppu(self, outputs: list[np.ndarray]) -> np.ndarray: """Decode PPU records. The head layout read from the model fixes the kind, scale count and grid decode from the first frame; without - it, each is settled from the records and kept once proven.""" + it, the kind and grid decode are settled from the records and kept + once proven, and the scale count is PPU_MAX_KNOWN_SCALES unless a + record proves more (a partial frame never argues for fewer).""" records = ppu_records(outputs) boxes = None if records is not None and len(records): @@ -1201,6 +1204,7 @@ class DeepxDetector(DetectionApi): if boxes is not None: if self.ppu_layout is None: self.ppu_scale_count = max( + PPU_MAX_KNOWN_SCALES, self.ppu_scale_count, ppu_scale_count_lower_bound(records, self.width, self.height), ) diff --git a/frigate/test/test_deepx.py b/frigate/test/test_deepx.py index bfec5c117c..797a1c0af5 100644 --- a/frigate/test/test_deepx.py +++ b/frigate/test/test_deepx.py @@ -362,17 +362,16 @@ class TestDeepxPpuDecode(unittest.TestCase): self.assertAlmostEqual(detections[0][4], 0.137188, places=5) self.assertAlmostEqual(detections[0][5], 0.125750, places=5) - def test_a_grid_cell_past_the_coarser_layout_proves_a_third_scale(self): - """A layer-0 cell at column 40 of a 640 input needs stride 8, so the - head has three scales even though no layer-2 record has shown up; - the layer-1 record in the same frame must then read against the - three-scale table, not the two-scale one its layer index alone - would suggest.""" + def test_a_frame_of_only_the_finer_layers_still_reads_as_three_scales(self): + """Layers 0 and 1 near the origin look the same from a two-scale + head and from the finer two thirds of a three-scale one, so with no + scale_count given the frame reads against the three-scale table, + never the two-scale one its highest layer index alone suggests.""" detections = decode_ppu( [ build_ppu_records( build_ppu_record((0.5, 0.5, 0.4, 0.4), grid=(3, 4, 0, 1)), - build_ppu_record((0.6, 0.4, 0.3, 0.7), grid=(7, 40, 1, 0)), + build_ppu_record((0.6, 0.4, 0.3, 0.7), grid=(7, 9, 1, 0)), ) ], 640, @@ -388,6 +387,10 @@ class TestDeepxPpuDecode(unittest.TestCase): self.assertAlmostEqual(layer_one[0][2], 0.057, places=5) self.assertAlmostEqual(layer_one[0][4], 0.118, places=5) self.assertAlmostEqual(layer_one[0][5], 0.1275, places=5) + # layer 0 of 3 is stride 8 and box 1 is the 16x30 anchor + layer_zero = detections[np.isclose(detections[:, 3], 0.116750)] + self.assertEqual(len(layer_zero), 1) + self.assertAlmostEqual(layer_zero[0][2], 0.045313, places=5) def test_scale_count_lower_bound_reads_layer_and_grid_position(self): """Every record proves at least one scale past its layer, and a @@ -1221,12 +1224,11 @@ class TestDeepxDetectorLayout(unittest.TestCase): self.assertEqual(second[0][0], 3) self.assertTrue(detector.ppu_anchor_free) - def test_ppu_scale_count_is_kept_once_a_coarser_layer_is_seen(self): - """A frame that only shows layer 0 cannot tell a two-scale head from - a three-scale one and falls back to three; once a frame proves - layer 1 exists, two scales is the best-evidenced count and every - later layer-0-only frame must use the two-scale table, until some - record proves a third scale.""" + def test_ppu_scale_count_without_a_layout_never_drops_below_three(self): + """With no layout to read, a three-scale head whose first frames + only hold layers 0 and 1 near the origin must not be read as a + two-scale head: those records are exactly what a three-scale head + emits too, so every frame keeps the three-scale table.""" detector, _ = self._detector( ModelTypeEnum.yologeneric, [{"shape": [8400]}], ppu=True ) @@ -1236,31 +1238,29 @@ class TestDeepxDetectorLayout(unittest.TestCase): build_ppu_record((0.6, 0.4, 0.3, 0.7), grid=(7, 9, 1, 0)) ] first = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) - # layer 0 read against the three-scale default: stride 8, anchor 16x30 + # layer 0 of 3: stride 8, anchor 16x30 self.assertAlmostEqual(first[0][3], 0.116750, places=5) + self.assertEqual(detector.ppu_scale_count, 3) detector.session.run.return_value = [ build_ppu_record((0.5, 0.5, 0.4, 0.4), grid=(3, 4, 0, 1)) ] second = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) - # layer 1 of 2 is stride 32 and box 0 is the 81x82 anchor - self.assertAlmostEqual(second[0][3], 0.1845, places=5) - self.assertEqual(detector.ppu_scale_count, 2) + # layer 1 of 3: stride 16, anchor 30x61, not layer 1 of 2 + self.assertAlmostEqual(second[0][3], 0.0975, places=5) + self.assertEqual(detector.ppu_scale_count, 3) detector.session.run.return_value = [ build_ppu_record((0.6, 0.4, 0.3, 0.7), grid=(7, 9, 1, 0)) ] third = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) - # the same layer-0 record as the first frame, now read against the - # two-scale table proven by the second frame: stride 16, anchor 23x27 - self.assertAlmostEqual(third[0][3], 0.236031, places=5) + self.assertAlmostEqual(third[0][3], 0.116750, places=5) - def test_ppu_scale_count_grows_when_a_grid_cell_proves_a_third_scale(self): - """A three-scale head whose early frames only ever hold layers 0 - and 1 near the origin reads as two scales; a later layer-0 record - at column 40 of 640 needs stride 8, which proves the third scale - without a layer-2 record ever appearing, and every frame after - that must read against the three-scale table.""" + def test_ppu_scale_count_grows_when_a_grid_cell_proves_a_fourth_scale(self): + """A layer-0 record at column 80 of 640 needs stride 4, which proves + a fourth scale with no layer-3 record ever appearing; there is no + table for that, so it is reported once and every frame after that + stays unplaced rather than reading against the three-scale table.""" detector, _ = self._detector( ModelTypeEnum.yologeneric, [{"shape": [8400]}], ppu=True ) @@ -1269,20 +1269,19 @@ class TestDeepxDetectorLayout(unittest.TestCase): layer_one = build_ppu_record((0.5, 0.5, 0.4, 0.4), grid=(3, 4, 0, 1)) detector.session.run.return_value = [layer_one] first = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) - # read as layer 1 of 2: stride 32, anchor 81x82 - self.assertAlmostEqual(first[0][3], 0.1845, places=5) - self.assertEqual(detector.ppu_scale_count, 2) + self.assertAlmostEqual(first[0][3], 0.0975, places=5) detector.session.run.return_value = [ - build_ppu_record((0.6, 0.4, 0.3, 0.7), grid=(7, 40, 1, 0)) + build_ppu_record((0.6, 0.4, 0.3, 0.7), grid=(7, 80, 1, 0)) ] - detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) - self.assertEqual(detector.ppu_scale_count, 3) + with self.assertLogs("frigate.detectors.plugins.deepx", level="ERROR") as logs: + second = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) + detector.session.run.return_value = [layer_one] + third = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) - detector.session.run.return_value = [layer_one] - third = detector.detect_raw(np.zeros((1, 640, 640, 3), np.uint8)) - # the same record, now layer 1 of 3: stride 16, anchor 30x61 - self.assertAlmostEqual(third[0][3], 0.0975, places=5) + self.assertEqual(detector.ppu_scale_count, 4) + self.assertTrue(np.all(second == 0) and np.all(third == 0)) + self.assertEqual(len([r for r in logs.records if r.levelname == "ERROR"]), 1) def _ppu_detector_for(self, ppu, layers): """A PPU detector loaded from a model file that describes its head."""