fix: enhance model type validation and update documentation for DEEPX detector

This commit is contained in:
aahmetdurmaz 2026-09-04 12:13:02 +03:00
parent cca4a3b046
commit fdc0a9acbc
3 changed files with 91 additions and 37 deletions

View File

@ -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:

View File

@ -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:<index>. "
"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

View File

@ -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)