mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-18 07:29:45 +00:00
fix: enhance model type validation and update documentation for DEEPX detector
This commit is contained in:
parent
cca4a3b046
commit
fdc0a9acbc
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user