mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-02 21:06:52 +03:00
Add the detection analytics collector
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
"""Detection section: models, the detectors they run on, and inference speed."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from frigate.analytics.collectors.common import rate
|
||||||
|
from frigate.analytics.context import ReportContext
|
||||||
|
from frigate.analytics.schema import DetectionModel, DetectionSection, ModelSource
|
||||||
|
from frigate.config.config import DEFAULT_MODEL
|
||||||
|
from frigate.const import MODEL_CACHE_DIR
|
||||||
|
from frigate.detectors.detector_config import SceneEnum
|
||||||
|
from frigate.detectors.detector_types import DetectorTypeEnum
|
||||||
|
from frigate.detectors.device import runner_names
|
||||||
|
|
||||||
|
# the paths FrigateConfig fills in for a model that sets none
|
||||||
|
BUNDLED_MODEL_PATHS = frozenset(
|
||||||
|
{"/cpu_model.tflite", "/edgetpu_model.tflite", str(DEFAULT_MODEL["path"])}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def model_source(path: str | None) -> ModelSource:
|
||||||
|
"""Default, Frigate+ (a cached model next to its info file), or custom.
|
||||||
|
|
||||||
|
Parsing rewrites plus://<id> to the model cache, so the prefix is gone by now.
|
||||||
|
"""
|
||||||
|
if path is None or path in BUNDLED_MODEL_PATHS:
|
||||||
|
return ModelSource.default
|
||||||
|
|
||||||
|
if path.startswith(f"{MODEL_CACHE_DIR}/") and os.path.isfile(f"{path}.json"):
|
||||||
|
return ModelSource.plus
|
||||||
|
|
||||||
|
return ModelSource.custom
|
||||||
|
|
||||||
|
|
||||||
|
def collect(ctx: ReportContext) -> DetectionSection:
|
||||||
|
config = ctx.config
|
||||||
|
detectors = ctx.stats.get("detectors", {})
|
||||||
|
model_specs = [(model, config.devices_for_model(model)) for model in config.models]
|
||||||
|
|
||||||
|
# FrigateApp.start_detectors names the processes in this same order
|
||||||
|
names = iter(runner_names([spec for _, specs in model_specs for spec in specs]))
|
||||||
|
models: dict[SceneEnum, DetectionModel] = {}
|
||||||
|
|
||||||
|
for model, specs in model_specs:
|
||||||
|
speeds: list[float] = []
|
||||||
|
|
||||||
|
for _ in specs:
|
||||||
|
speed = detectors.get(next(names), {}).get("inference_speed")
|
||||||
|
|
||||||
|
if isinstance(speed, int | float) and speed > 0:
|
||||||
|
speeds.append(float(speed))
|
||||||
|
|
||||||
|
models[model.scene] = DetectionModel(
|
||||||
|
detector=DetectorTypeEnum(specs[0].detector),
|
||||||
|
devices=len(specs),
|
||||||
|
model_type=model.model_type,
|
||||||
|
input=f"{model.width}x{model.height}",
|
||||||
|
source=model_source(model.path),
|
||||||
|
inference_ms=rate(sum(speeds) / len(speeds)) if speeds else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return DetectionSection(
|
||||||
|
models=models,
|
||||||
|
detection_fps=rate(ctx.stats.get("detection_fps")),
|
||||||
|
skipped_fps=rate(ctx.stats.get("skipped_fps")),
|
||||||
|
)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Tests for the detection collector."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from frigate.analytics.collectors import detection
|
||||||
|
from frigate.analytics.schema import ModelSource
|
||||||
|
from frigate.detectors.detector_config import SceneEnum
|
||||||
|
from frigate.test.analytics_helpers import make_config, make_context
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectionCollector(unittest.TestCase):
|
||||||
|
def test_reports_each_model_with_its_mean_inference_speed(self):
|
||||||
|
config = make_config({"models": [{"devices": ["cpu", "cpu"]}]})
|
||||||
|
stats = {
|
||||||
|
"detectors": {
|
||||||
|
"cpu": {"inference_speed": 10.0},
|
||||||
|
"cpu#2": {"inference_speed": 20.0},
|
||||||
|
},
|
||||||
|
"detection_fps": 12.346,
|
||||||
|
"skipped_fps": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
section = detection.collect(make_context(config, stats))
|
||||||
|
|
||||||
|
model = section.models[SceneEnum.all]
|
||||||
|
self.assertEqual(model.detector, "cpu")
|
||||||
|
self.assertEqual(model.devices, 2)
|
||||||
|
self.assertEqual(model.model_type, "ssd")
|
||||||
|
self.assertEqual(model.input, "320x320")
|
||||||
|
self.assertEqual(model.source, ModelSource.default)
|
||||||
|
self.assertEqual(model.inference_ms, 15.0)
|
||||||
|
self.assertEqual(section.detection_fps, 12.35)
|
||||||
|
self.assertEqual(section.skipped_fps, 0.0)
|
||||||
|
|
||||||
|
def test_inference_is_null_before_the_first_stats(self):
|
||||||
|
section = detection.collect(make_context())
|
||||||
|
|
||||||
|
self.assertIsNone(section.models[SceneEnum.all].inference_ms)
|
||||||
|
|
||||||
|
def test_model_source(self):
|
||||||
|
with tempfile.TemporaryDirectory() as cache:
|
||||||
|
plus_model = os.path.join(cache, "abc123")
|
||||||
|
open(f"{plus_model}.json", "w").close()
|
||||||
|
|
||||||
|
with patch.object(detection, "MODEL_CACHE_DIR", cache):
|
||||||
|
self.assertEqual(detection.model_source(plus_model), ModelSource.plus)
|
||||||
|
self.assertEqual(
|
||||||
|
detection.model_source(os.path.join(cache, "no_info")),
|
||||||
|
ModelSource.custom,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(detection.model_source(None), ModelSource.default)
|
||||||
|
self.assertEqual(
|
||||||
|
detection.model_source("/cpu_model.tflite"), ModelSource.default
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
detection.model_source("/config/yolo.onnx"), ModelSource.custom
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user