diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index cf1ec2f113..7c457eab90 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -657,7 +657,7 @@ The DX-RT Python bindings are not part of the Frigate image. They are downloaded Frigate does not bundle a model for this detector. Models must be compiled to DEEPX's `.dxnn` format. -The quickest way to get one is the [DEEPX ModelZoo](https://developer.deepx.ai/modelzoo), which publishes pre-compiled `.dxnn` files for a range of object detection models alongside a JSON file describing how each was compiled: YOLO variants (`model_type: yolo-generic`) and DAMO-YOLO (`model_type: damo-yolo`). Pick a model, download the `.dxnn`, bind-mount it into the container, and point the model's `path` at it. Note which one you picked, since it determines the `model_type` value below. +The quickest way to get one is the [DEEPX ModelZoo](https://developer.deepx.ai/modelzoo), which publishes pre-compiled `.dxnn` files for a range of object detection models alongside a JSON file describing how each was compiled: YOLO variants (`model_type: yolo-generic`) and DAMO-YOLO (`model_type: damo-yolo`). Pick a model, download the `.dxnn`, bind-mount it into the container, and point the model's `path` at it. Note which one you picked, since it determines the `model_type` value below. `model_type` has to be set: Frigate defaults it to `ssd`, which no DEEPX decoder reads, so a model that leaves it unset is rejected at startup. ```yaml models: diff --git a/frigate/detectors/plugins/deepx.py b/frigate/detectors/plugins/deepx.py index fe7840fd4b..0bb4efc543 100644 --- a/frigate/detectors/plugins/deepx.py +++ b/frigate/detectors/plugins/deepx.py @@ -131,11 +131,11 @@ DEEPX_MANIFEST = RuntimeManifest( class DeepxModelTypeEnum(str, Enum): """The subset of Frigate's model types the DEEPX decoders can read. - Frigate reads `model_type` off the root `model` block, where an omitted - value defaults to `ssd`; listing the supported ones here makes the choice - visible, and the detector writes it back at startup. `ssd` is left out on - purpose: the ModelZoo's SSD models carry Pascal VOC's 20 class names, so it - would mean an empty label map, not a config error. + `model_type` sits on the root `model` block, where an omitted value + defaults to `ssd`; a model validator rejects anything not listed here so a + type with no DEEPX decoder fails at startup rather than being decoded as + yolo-generic. `ssd` is left out on purpose: DX-COM compiles no SSD head + this detector can read. """ # PPU-compiled models still use yolo-generic; the `ppu` option is what @@ -814,13 +814,6 @@ class DeepxDetectorConfig(BaseDetectorConfig): description="Which NPU this detector binds to, as PCIe:. " "Empty selects the first NPU.", ) - model_type: DeepxModelTypeEnum = Field( - default=DeepxModelTypeEnum.yologeneric, - title="Detection model architecture", - description="Which decoder reads the model's output. Overrides " - "model.model_type for this detector, whose default of ssd would " - "otherwise apply whenever the root model block leaves it unset.", - ) ppu: bool = Field( default=False, title="PPU-compiled model", @@ -905,6 +898,35 @@ class DeepxDetectorConfig(BaseDetectorConfig): return value + @property + def resolved_model_type(self) -> DeepxModelTypeEnum | None: + """The model type the decoders will actually run, read off the root + model block. None while the model is still unresolved, which is how a + device string is checked on its own before any model is attached.""" + if self.model is None: + return None + + return DeepxModelTypeEnum(self.model.model_type.value) + + @model_validator(mode="after") + def validate_model_type_is_supported(self): + """Reject a model type no DEEPX decoder reads. Frigate defaults + model_type to ssd, which would otherwise fall through to the + yolo-generic decode path and return nonsense instead of an error.""" + if self.model is None: + return self + + try: + DeepxModelTypeEnum(self.model.model_type.value) + except ValueError: + supported = ", ".join(member.value for member in DeepxModelTypeEnum) + raise ValueError( + f"model_type '{self.model.model_type.value}' has no DEEPX " + f"decoder. Set model.model_type to one of: {supported}." + ) from None + + return self + @model_validator(mode="after") def validate_ppu_is_supported(self): """Reject a PPU pairing DX-COM cannot produce, or that has no @@ -919,7 +941,7 @@ class DeepxDetectorConfig(BaseDetectorConfig): "DX-COM. Set ppu to false." ) - if self.model_type is DeepxModelTypeEnum.damoyolo: + if self.resolved_model_type is DeepxModelTypeEnum.damoyolo: raise ValueError( 'PPU is not yet supported for model_type "damo-yolo". Set ppu to false.' ) @@ -971,13 +993,15 @@ class DeepxDetectorConfig(BaseDetectorConfig): """Reject a model_format on a model_type that has no layout to pick: only yolo-generic has variants the decoder cannot tell apart, so a value on DAMO-YOLO's one fixed layout is a mistake, not a choice.""" - if self.model_type is DeepxModelTypeEnum.yologeneric: + model_type = self.resolved_model_type + + if model_type is None or model_type is DeepxModelTypeEnum.yologeneric: return self if self.model_format is not ModelFormatEnum.auto: raise ValueError( "model_format is not used for model_type " - f"{self.model_type.value} -- its output layout is fixed. " + f"{model_type.value} -- its output layout is fixed. " 'Leave it as "auto".' ) @@ -1008,9 +1032,8 @@ class DeepxDetector(DetectionApi): super().__init__(config) - # In 0.19 model_type sits on the model, not the detector: a device - # string fills only the device field, so DeepxDetectorConfig.model_type - # never reflects what the user configured and must not be read here. + # model_type sits on the model, not the detector; a validator has + # already rejected any value with no DEEPX decoder behind it. self.model_type = ModelTypeEnum(config.model.model_type.value) model_path = config.model.path diff --git a/frigate/test/test_deepx.py b/frigate/test/test_deepx.py index c3addba012..0a870e2ef9 100644 --- a/frigate/test/test_deepx.py +++ b/frigate/test/test_deepx.py @@ -8,7 +8,7 @@ from unittest.mock import patch import numpy as np from pydantic import ValidationError -from frigate.detectors.detector_config import ModelTypeEnum +from frigate.detectors.detector_config import ModelConfig, ModelTypeEnum from frigate.detectors.plugins.deepx import ( ANCHOR_FORMATS, ANCHOR_FREE_FORMATS, @@ -42,6 +42,14 @@ TEST_ANCHOR_STRING = "11,17,23,29,31,37;41,47,53,59,61,67;71,73,79,83,89,97" TEST_ANCHORS = parse_anchors(TEST_ANCHOR_STRING) +def model_with_type(model_type) -> ModelConfig: + """The root model block a device is validated against. model_type lives + here rather than on the detector, so every check that keys off it has to + be exercised through a model. The label map is left unset so the test does + not depend on the image's /labelmap.txt.""" + return ModelConfig(model_type=model_type, labelmap_path=None) + + def build_ppu_record( box: tuple[float, float, float, float], grid_y: int, @@ -367,27 +375,39 @@ class TestDeepxDetectorConfig(unittest.TestCase): self.assertIs(config.model_format, ModelFormatEnum.auto) def test_ppu_with_damoyolo_model_type_is_rejected(self): - """No confirmed PPU record layout exists for DAMO-YOLO yet.""" + """No confirmed PPU record layout exists for DAMO-YOLO yet. The type + comes off the model block, so the check has to read it from there + rather than from a detector field the runtime never uses.""" with self.assertRaises(ValidationError): DeepxDetectorConfig( type="deepx", + model=model_with_type(ModelTypeEnum.damoyolo), ppu=True, model_format=ModelFormatEnum.anchor_free, - model_type=DeepxModelTypeEnum.damoyolo, ) + def test_ppu_with_yolo_generic_model_type_is_accepted(self): + config = DeepxDetectorConfig( + type="deepx", + model=model_with_type(ModelTypeEnum.yologeneric), + ppu=True, + model_format=ModelFormatEnum.anchor_free, + ) + + self.assertTrue(config.ppu) + def test_damoyolo_model_type_rejects_a_model_format(self): with self.assertRaises(ValidationError): DeepxDetectorConfig( type="deepx", + model=model_with_type(ModelTypeEnum.damoyolo), model_format=ModelFormatEnum.anchor_free, - model_type=DeepxModelTypeEnum.damoyolo, ) def test_damoyolo_model_type_without_model_format_is_accepted(self): config = DeepxDetectorConfig( type="deepx", - model_type=DeepxModelTypeEnum.damoyolo, + model=model_with_type(ModelTypeEnum.damoyolo), ) self.assertIs(config.model_format, ModelFormatEnum.auto) @@ -781,23 +801,34 @@ if __name__ == "__main__": class TestDeepxModelType(unittest.TestCase): - def test_model_type_defaults_to_yolo_generic(self): - """Frigate's own model_type default is ssd, which this detector has no - decoder for, so the detector needs a default of its own.""" + def test_a_model_type_with_no_decoder_is_rejected(self): + """Frigate defaults model_type to ssd, and every unsupported type + would otherwise fall through to the yolo-generic decode path and + return nonsense rather than an error.""" + for model_type in (ModelTypeEnum.ssd, ModelTypeEnum.dfine): + with self.subTest(model_type=model_type): + with self.assertRaises(ValidationError): + DeepxDetectorConfig(type="deepx", model=model_with_type(model_type)) + + def test_supported_model_types_are_accepted(self): + for model_type in (ModelTypeEnum.yologeneric, ModelTypeEnum.damoyolo): + with self.subTest(model_type=model_type): + config = DeepxDetectorConfig( + type="deepx", model=model_with_type(model_type) + ) + + self.assertEqual(config.resolved_model_type.value, model_type.value) + + def test_an_unresolved_model_defers_the_check(self): + """A device string is validated on its own before any model is + attached, so an absent model must not fail.""" config = DeepxDetectorConfig(type="deepx") - self.assertIs(config.model_type, DeepxModelTypeEnum.yologeneric) - - def test_ssd_is_not_an_accepted_model_type(self): - """The ModelZoo's SSD models are Pascal VOC, whose label names none of - Frigate's COCO-based object config would ever match, so the type is - rejected at config load rather than decoded into a wrong label map.""" - with self.assertRaises(ValidationError): - DeepxDetectorConfig(type="deepx", model_type="ssd") + self.assertIsNone(config.resolved_model_type) def test_model_type_values_match_frigate_model_types(self): - """The detector writes its choice back onto model.model_type, so every - value has to round-trip through ModelTypeEnum.""" + """The detector reads model.model_type back as a ModelTypeEnum, so + every value has to round-trip.""" for value in DeepxModelTypeEnum: with self.subTest(value=value): self.assertEqual(ModelTypeEnum(value.value).value, value.value)