From 755f51f1adda04474f50468ec9ef84b2d641299d Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 1 Dec 2025 12:49:13 -0700 Subject: [PATCH] Make number of classification images to be kept configurable --- docs/docs/configuration/reference.md | 32 +++++++++++++++++++ frigate/config/classification.py | 5 +++ .../real_time/custom_classification.py | 25 ++++++++++++++- frigate/util/classification.py | 3 +- web/src/types/frigateConfig.ts | 1 + 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index f8b49303f..21c921a26 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -710,6 +710,38 @@ audio_transcription: # List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10 language: en +# Optional: Configuration for custom classification models +classification: + custom: + # Required: name of the classification model + model_name: + # Optional: Enable running the model (default: shown below) + enabled: True + # Optional: Name of classification model (default: shown below) + name: None + # Optional: Classification score threshold to change the state (default: shown below) + threshold: 0.8 + # Optional: Number of classification attempts to save in the recent classifications tab (default: shown below) + # NOTE: Defaults to 200 for object classification and 100 for state classification if not specified + save_attempts: None + # Optional: Object classification configuration + object_config: + # Required: Object types to classify + objects: [dog] + # Optional: Type of classification that is applied (default: shown below) + classification_type: sub_label + # Optional: State classification configuration + state_config: + # Required: Cameras to run classification on + cameras: + camera_name: + # Required: Crop of image frame on this camera to run classification on + crop: [0, 180, 220, 400] + # Optional: If classification should be run when motion is detected in the crop (default: shown below) + motion: False + # Optional: Interval to run classification on in seconds (default: shown below) + interval: None + # Optional: Restream configuration # Uses https://github.com/AlexxIT/go2rtc (v1.9.10) # NOTE: The default go2rtc API port (1984) must be used, diff --git a/frigate/config/classification.py b/frigate/config/classification.py index bdcbf48f1..fb8e3de29 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -105,6 +105,11 @@ class CustomClassificationConfig(FrigateBaseModel): threshold: float = Field( default=0.8, title="Classification score threshold to change the state." ) + save_attempts: int | None = Field( + default=None, + title="Number of classification attempts to save in the recent classifications tab. If not specified, defaults to 200 for object classification and 100 for state classification.", + ge=0, + ) object_config: CustomClassificationObjectConfig | None = Field(default=None) state_config: CustomClassificationStateConfig | None = Field(default=None) diff --git a/frigate/data_processing/real_time/custom_classification.py b/frigate/data_processing/real_time/custom_classification.py index 179d2f43f..c8f31db76 100644 --- a/frigate/data_processing/real_time/custom_classification.py +++ b/frigate/data_processing/real_time/custom_classification.py @@ -250,6 +250,11 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): if self.interpreter is None: # When interpreter is None, always save (score is 0.0, which is < 1.0) if self._should_save_image(camera, "unknown", 0.0): + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 100 + ) write_classification_attempt( self.train_dir, cv2.cvtColor(frame, cv2.COLOR_RGB2BGR), @@ -257,6 +262,7 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): now, "unknown", 0.0, + max_files=save_attempts, ) return @@ -277,6 +283,11 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): detected_state = self.labelmap[best_id] if self._should_save_image(camera, detected_state, score): + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 100 + ) write_classification_attempt( self.train_dir, cv2.cvtColor(frame, cv2.COLOR_RGB2BGR), @@ -284,6 +295,7 @@ class CustomStateClassificationProcessor(RealTimeProcessorApi): now, detected_state, score, + max_files=save_attempts, ) if score < self.model_config.threshold: @@ -482,6 +494,11 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): return if self.interpreter is None: + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 200 + ) write_classification_attempt( self.train_dir, cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), @@ -489,6 +506,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): now, "unknown", 0.0, + max_files=save_attempts, ) return @@ -506,6 +524,11 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): score = round(probs[best_id], 2) self.__update_metrics(datetime.datetime.now().timestamp() - now) + save_attempts = ( + self.model_config.save_attempts + if self.model_config.save_attempts is not None + else 200 + ) write_classification_attempt( self.train_dir, cv2.cvtColor(crop, cv2.COLOR_RGB2BGR), @@ -513,7 +536,7 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi): now, self.labelmap[best_id], score, - max_files=200, + max_files=save_attempts, ) if score < self.model_config.threshold: diff --git a/frigate/util/classification.py b/frigate/util/classification.py index a74094c32..1f4213315 100644 --- a/frigate/util/classification.py +++ b/frigate/util/classification.py @@ -330,7 +330,7 @@ def collect_state_classification_examples( 1. Queries review items from specified cameras 2. Selects 100 balanced timestamps across the data 3. Extracts keyframes from recordings (cropped to specified regions) - 4. Selects 20 most visually distinct images + 4. Selects 24 most visually distinct images 5. Saves them to the dataset directory Args: @@ -660,7 +660,6 @@ def collect_object_classification_examples( Args: model_name: Name of the classification model label: Object label to collect (e.g., "person", "car") - cameras: List of camera names to collect examples from """ dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset") temp_dir = os.path.join(dataset_dir, "temp") diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index c7cbb50b8..985fe3457 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -305,6 +305,7 @@ export type CustomClassificationModelConfig = { enabled: boolean; name: string; threshold: number; + save_attempts?: number; object_config?: { objects: string[]; classification_type: string;