mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-30 10:20:30 +03:00
adapt analytics to free-form model scenes, split notice counts, and peewee 4
This commit is contained in:
@@ -7,7 +7,7 @@ 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_config import DEFAULT_SCENE
|
||||
from frigate.detectors.detector_types import DetectorTypeEnum
|
||||
from frigate.detectors.device import runner_names
|
||||
|
||||
@@ -38,7 +38,7 @@ def collect(ctx: ReportContext) -> DetectionSection:
|
||||
|
||||
# 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] = {}
|
||||
models: list[DetectionModel] = []
|
||||
|
||||
for model, specs in model_specs:
|
||||
speeds: list[float] = []
|
||||
@@ -49,13 +49,16 @@ def collect(ctx: ReportContext) -> DetectionSection:
|
||||
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,
|
||||
models.append(
|
||||
DetectionModel(
|
||||
default=model.scene == DEFAULT_SCENE,
|
||||
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(
|
||||
|
||||
@@ -32,11 +32,14 @@ def notice_deltas(notice_stats: list[dict[str, Any]]) -> dict[Any, NoticeCounts]
|
||||
continue
|
||||
|
||||
occurrences = max(row["occurrences"] - row["reported_occurrences"], 0)
|
||||
dismissals = max(row["dismissals"] - row["reported_dismissals"], 0)
|
||||
acknowledgements = max(
|
||||
row["acknowledgements"] - row["reported_acknowledgements"], 0
|
||||
)
|
||||
mutes = max(row["mutes"] - row["reported_mutes"], 0)
|
||||
|
||||
if occurrences or dismissals:
|
||||
if occurrences or acknowledgements or mutes:
|
||||
deltas[NoticeKindKey(row["kind"])] = NoticeCounts(
|
||||
occurrences=occurrences, dismissals=dismissals
|
||||
occurrences=occurrences, acknowledgements=acknowledgements, mutes=mutes
|
||||
)
|
||||
|
||||
return deltas
|
||||
|
||||
@@ -13,7 +13,7 @@ from frigate.config.camera.birdseye import BirdseyeModeEnum
|
||||
from frigate.config.camera.camera import CameraTypeEnum
|
||||
from frigate.config.camera.genai import GenAIProviderEnum, GenAIRoleEnum
|
||||
from frigate.config.classification import ModelSizeEnum
|
||||
from frigate.detectors.detector_config import ModelTypeEnum, SceneEnum
|
||||
from frigate.detectors.detector_config import ModelTypeEnum
|
||||
from frigate.detectors.detector_types import DetectorTypeEnum
|
||||
from frigate.ffmpeg_presets import PRESETS_HW_ACCEL_DECODE, PRESETS_INPUT
|
||||
from frigate.notices.types import NOTICE_KINDS
|
||||
@@ -53,6 +53,7 @@ class GpuVendor(StrEnum):
|
||||
nvidia = "nvidia"
|
||||
rockchip = "rockchip"
|
||||
rpi = "rpi"
|
||||
apple_silicon = "apple-silicon"
|
||||
other = "other"
|
||||
|
||||
|
||||
@@ -63,6 +64,7 @@ class DecodeFamily(StrEnum):
|
||||
intel_qsv = "intel-qsv"
|
||||
jetson = "jetson"
|
||||
rpi = "rpi"
|
||||
apple_silicon = "apple-silicon"
|
||||
other = "other"
|
||||
|
||||
|
||||
@@ -73,12 +75,14 @@ class HardwareKey(StrEnum):
|
||||
openvino_npu = "openvino:NPU"
|
||||
onnx_amd = "onnx:amd"
|
||||
onnx_nvidia = "onnx:nvidia"
|
||||
onnx_lighter = "onnx:lighter"
|
||||
tensorrt = "tensorrt"
|
||||
hailo = "hailo"
|
||||
memryx = "memryx"
|
||||
deepx = "deepx"
|
||||
rknn = "rknn"
|
||||
axengine = "axengine"
|
||||
axelera = "axelera"
|
||||
synaptics = "synaptics"
|
||||
cpu = "cpu"
|
||||
other = "other"
|
||||
@@ -228,6 +232,7 @@ class HardwareSection(AnalyticsModel):
|
||||
|
||||
|
||||
class DetectionModel(AnalyticsModel):
|
||||
default: bool = metric("Whether the model serves cameras that set no scene")
|
||||
detector: DetectorTypeEnum = metric("Detector type the model runs on")
|
||||
devices: int = count("Devices the model runs on")
|
||||
model_type: ModelTypeEnum = metric("Model architecture")
|
||||
@@ -240,7 +245,7 @@ class DetectionModel(AnalyticsModel):
|
||||
|
||||
|
||||
class DetectionSection(AnalyticsModel):
|
||||
models: dict[SceneEnum, DetectionModel] = metric("Detection models keyed by scene")
|
||||
models: list[DetectionModel] = metric("Detection models")
|
||||
detection_fps: float = metric("Detections per second across cameras", ge=0)
|
||||
skipped_fps: float = metric("Frames per second skipped across cameras", ge=0)
|
||||
|
||||
@@ -369,7 +374,8 @@ class FeaturesSection(AnalyticsModel):
|
||||
|
||||
class NoticeCounts(AnalyticsModel):
|
||||
occurrences: int = count("Occurrences since the last accepted report")
|
||||
dismissals: int = count("Dismissals since the last accepted report")
|
||||
acknowledgements: int = count("Acknowledgements since the last accepted report")
|
||||
mutes: int = count("Mutes since the last accepted report")
|
||||
|
||||
|
||||
class HealthSection(AnalyticsModel):
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ class NoticeStats(Model):
|
||||
mutes = IntegerField(default=0)
|
||||
first_seen = DateTimeField()
|
||||
last_seen = DateTimeField()
|
||||
# watermarks for a future analytics reporter; unused until then
|
||||
# counts the last accepted analytics report covered
|
||||
reported_occurrences = IntegerField(default=0)
|
||||
reported_acknowledgements = IntegerField(default=0)
|
||||
reported_mutes = IntegerField(default=0)
|
||||
|
||||
@@ -395,7 +395,8 @@ class NoticeRegistry:
|
||||
for row in snapshot:
|
||||
NoticeStats.update(
|
||||
reported_occurrences=row["occurrences"],
|
||||
reported_dismissals=row["dismissals"],
|
||||
reported_acknowledgements=row["acknowledgements"],
|
||||
reported_mutes=row["mutes"],
|
||||
).where(NoticeStats.kind == row["kind"]).execute()
|
||||
|
||||
def _write_repeats(
|
||||
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
|
||||
|
||||
@@ -25,7 +24,8 @@ class TestDetectionCollector(unittest.TestCase):
|
||||
|
||||
section = detection.collect(make_context(config, stats))
|
||||
|
||||
model = section.models[SceneEnum.all]
|
||||
[model] = section.models
|
||||
self.assertTrue(model.default)
|
||||
self.assertEqual(model.detector, "cpu")
|
||||
self.assertEqual(model.devices, 2)
|
||||
self.assertEqual(model.model_type, "ssd")
|
||||
@@ -35,10 +35,29 @@ class TestDetectionCollector(unittest.TestCase):
|
||||
self.assertEqual(section.detection_fps, 12.35)
|
||||
self.assertEqual(section.skipped_fps, 0.0)
|
||||
|
||||
def test_named_scene_reports_no_name(self):
|
||||
config = make_config(
|
||||
{
|
||||
"models": [
|
||||
{"devices": ["cpu"]},
|
||||
{
|
||||
"scene": "backyard_thermal",
|
||||
"devices": ["cpu"],
|
||||
"path": "/thermal.tflite",
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
section = detection.collect(make_context(config))
|
||||
|
||||
self.assertEqual([m.default for m in section.models], [True, False])
|
||||
self.assertNotIn("backyard_thermal", section.model_dump_json())
|
||||
|
||||
def test_inference_is_null_before_the_first_stats(self):
|
||||
section = detection.collect(make_context())
|
||||
|
||||
self.assertIsNone(section.models[SceneEnum.all].inference_ms)
|
||||
self.assertIsNone(section.models[0].inference_ms)
|
||||
|
||||
def test_model_source(self):
|
||||
with tempfile.TemporaryDirectory() as cache:
|
||||
|
||||
@@ -4,8 +4,8 @@ import logging
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from peewee import SqliteDatabase
|
||||
from peewee_migrate import Router
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
|
||||
from frigate.analytics.collectors import features
|
||||
@@ -49,7 +49,7 @@ CONFIG = {
|
||||
|
||||
class TestFeaturesCollector(unittest.TestCase):
|
||||
def setUp(self):
|
||||
migrate_db = SqliteExtDatabase(TEST_DB)
|
||||
migrate_db = SqliteDatabase(TEST_DB)
|
||||
del logging.getLogger("peewee_migrate").handlers[:]
|
||||
Router(migrate_db).run()
|
||||
migrate_db.close()
|
||||
|
||||
@@ -21,23 +21,29 @@ class TestHealthCollector(unittest.TestCase):
|
||||
{
|
||||
"kind": "detector_stuck",
|
||||
"occurrences": 5,
|
||||
"dismissals": 1,
|
||||
"acknowledgements": 1,
|
||||
"mutes": 0,
|
||||
"reported_occurrences": 3,
|
||||
"reported_dismissals": 1,
|
||||
"reported_acknowledgements": 1,
|
||||
"reported_mutes": 0,
|
||||
},
|
||||
{
|
||||
"kind": "shm_too_low",
|
||||
"occurrences": 2,
|
||||
"dismissals": 0,
|
||||
"acknowledgements": 0,
|
||||
"mutes": 0,
|
||||
"reported_occurrences": 2,
|
||||
"reported_dismissals": 0,
|
||||
"reported_acknowledgements": 0,
|
||||
"reported_mutes": 0,
|
||||
},
|
||||
{
|
||||
"kind": "update_available",
|
||||
"occurrences": 4,
|
||||
"dismissals": 0,
|
||||
"acknowledgements": 0,
|
||||
"mutes": 0,
|
||||
"reported_occurrences": 0,
|
||||
"reported_dismissals": 0,
|
||||
"reported_acknowledgements": 0,
|
||||
"reported_mutes": 0,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -49,10 +55,10 @@ class TestHealthCollector(unittest.TestCase):
|
||||
self.assertTrue(section.retention_unmet)
|
||||
self.assertEqual(
|
||||
{
|
||||
kind: (c.occurrences, c.dismissals)
|
||||
kind: (c.occurrences, c.acknowledgements, c.mutes)
|
||||
for kind, c in section.notices.items()
|
||||
},
|
||||
{"detector_stuck": (2, 0)},
|
||||
{"detector_stuck": (2, 0, 0)},
|
||||
)
|
||||
|
||||
def test_missing_stats_give_zeros(self):
|
||||
|
||||
@@ -6,8 +6,8 @@ import os
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from peewee import SqliteDatabase
|
||||
from peewee_migrate import Router
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
|
||||
from frigate.analytics import report
|
||||
@@ -50,7 +50,7 @@ class TestBuildReport(unittest.TestCase):
|
||||
|
||||
class TestRealCollectors(unittest.TestCase):
|
||||
def setUp(self):
|
||||
migrate_db = SqliteExtDatabase(TEST_DB)
|
||||
migrate_db = SqliteDatabase(TEST_DB)
|
||||
del logging.getLogger("peewee_migrate").handlers[:]
|
||||
Router(migrate_db).run()
|
||||
migrate_db.close()
|
||||
|
||||
@@ -24,9 +24,11 @@ SNAPSHOT = [
|
||||
{
|
||||
"kind": "detector_stuck",
|
||||
"occurrences": 3,
|
||||
"dismissals": 0,
|
||||
"acknowledgements": 0,
|
||||
"mutes": 0,
|
||||
"reported_occurrences": 1,
|
||||
"reported_dismissals": 0,
|
||||
"reported_acknowledgements": 0,
|
||||
"reported_mutes": 0,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -372,7 +372,8 @@ class TestMarkReported(RegistryTestCase):
|
||||
row = self.registry.stats()[0]
|
||||
self.assertEqual(row["occurrences"], 2)
|
||||
self.assertEqual(row["reported_occurrences"], 1)
|
||||
self.assertEqual(row["reported_dismissals"], 0)
|
||||
self.assertEqual(row["reported_acknowledgements"], 0)
|
||||
self.assertEqual(row["reported_mutes"], 0)
|
||||
|
||||
|
||||
class TestApply(RegistryTestCase):
|
||||
|
||||
Reference in New Issue
Block a user