2023-04-24 07:24:28 -05:00
import hashlib
2023-04-30 13:32:36 -05:00
import json
2022-12-15 07:12:52 -06:00
import logging
2023-04-30 13:32:36 -05:00
import os
2023-05-29 12:31:17 +02:00
from enum import Enum
2026-08-18 08:11:50 -06:00
from typing import Any , ClassVar
2022-12-15 07:12:52 -06:00
2023-05-29 12:31:17 +02:00
import requests
2024-02-29 16:10:13 -07:00
from pydantic import BaseModel , ConfigDict , Field
2022-12-15 07:12:52 -06:00
from pydantic.fields import PrivateAttr
2023-05-29 12:31:17 +02:00
2025-03-01 05:35:09 +01:00
from frigate.const import DEFAULT_ATTRIBUTE_LABEL_MAP , MODEL_CACHE_DIR
2023-04-30 13:32:36 -05:00
from frigate.plus import PlusApi
2024-07-07 13:53:00 -05:00
from frigate.util.builtin import generate_color_palette , load_labels
2022-12-15 07:12:52 -06:00
logger = logging . getLogger ( __name__ )
2026-08-18 08:11:50 -06:00
# attributes that are recognized rather than shown as a logo
NON_LOGO_ATTRIBUTES = [ "face" , "license_plate" ]
2022-12-15 07:12:52 -06:00
class PixelFormatEnum ( str , Enum ):
rgb = "rgb"
bgr = "bgr"
yuv = "yuv"
class InputTensorEnum ( str , Enum ):
nchw = "nchw"
nhwc = "nhwc"
2025-04-15 07:55:38 -06:00
hwnc = "hwnc"
hwcn = "hwcn"
2022-12-15 07:12:52 -06:00
2024-10-29 09:28:05 -06:00
class InputDTypeEnum ( str , Enum ):
float = "float"
2025-04-18 05:40:06 -06:00
float_denorm = "float_denorm" # non-normalized float
2024-10-29 09:28:05 -06:00
int = "int"
2023-02-03 17:36:37 -08:00
class ModelTypeEnum ( str , Enum ):
2025-03-21 18:55:46 -06:00
dfine = "dfine"
rfdetr = "rfdetr"
2023-02-03 17:36:37 -08:00
ssd = "ssd"
yolox = "yolox"
2024-05-17 18:50:45 +02:00
yolonas = "yolonas"
2025-03-11 21:36:07 +02:00
yologeneric = "yolo-generic"
2023-02-03 17:36:37 -08:00
2026-08-18 08:11:50 -06:00
class SceneEnum ( str , Enum ):
"""The camera environment a detection model is intended for."""
all = "all"
indoor = "indoor"
outdoor = "outdoor"
indoor_thermal = "indoor_thermal"
outdoor_thermal = "outdoor_thermal"
2022-12-15 07:12:52 -06:00
class ModelConfig ( BaseModel ):
2026-08-18 08:11:50 -06:00
scene : SceneEnum = Field (
default = SceneEnum . all ,
title = "Model scene" ,
description = "The camera environment this model is used for. Cameras select a model by setting detect.scene to a matching value, and 'all' is used by any camera that does not set one." ,
)
devices : list [ str ] = Field (
default_factory = list ,
title = "Detection hardware" ,
description = "Hardware this model runs on, as '<detector>' or '<detector>:<device>' (for example 'edgetpu:pci:0' or 'openvino:GPU'). Listing the same device more than once runs additional inference processes on it." ,
)
2026-07-06 09:28:02 -08:00
path : str | None = Field (
2026-02-27 09:55:36 -06:00
None ,
2026-03-26 13:47:24 -05:00
title = "Custom object detector model path" ,
2026-02-27 09:55:36 -06:00
description = "Path to a custom detection model file (or plus://<model_id> for Frigate+ models)." ,
)
2026-07-06 09:28:02 -08:00
labelmap_path : str | None = Field (
2026-02-27 09:55:36 -06:00
None ,
title = "Label map for custom object detector" ,
description = "Path to a labelmap file that maps numeric classes to string labels for the detector." ,
)
width : int = Field (
default = 320 ,
title = "Object detection model input width" ,
description = "Width of the model input tensor in pixels." ,
)
height : int = Field (
default = 320 ,
title = "Object detection model input height" ,
description = "Height of the model input tensor in pixels." ,
2024-02-29 16:10:13 -07:00
)
2026-07-06 09:28:02 -08:00
labelmap : dict [ int , str ] = Field (
2026-02-27 09:55:36 -06:00
default_factory = dict ,
title = "Labelmap customization" ,
description = "Overrides or remapping entries to merge into the standard labelmap." ,
2022-12-15 07:12:52 -06:00
)
2026-07-06 09:28:02 -08:00
attributes_map : dict [ str , list [ str ]] = Field (
2024-09-28 07:49:04 -06:00
default = DEFAULT_ATTRIBUTE_LABEL_MAP ,
2026-02-27 09:55:36 -06:00
title = "Map of object labels to their attribute labels" ,
description = "Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate'])." ,
2024-09-28 07:49:04 -06:00
)
2022-12-15 07:12:52 -06:00
input_tensor : InputTensorEnum = Field (
2026-02-27 09:55:36 -06:00
default = InputTensorEnum . nhwc ,
title = "Model Input Tensor Shape" ,
description = "Tensor format expected by the model: 'nhwc' or 'nchw'." ,
2022-12-15 07:12:52 -06:00
)
input_pixel_format : PixelFormatEnum = Field (
2026-02-27 09:55:36 -06:00
default = PixelFormatEnum . rgb ,
title = "Model Input Pixel Color Format" ,
description = "Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'." ,
2022-12-15 07:12:52 -06:00
)
2024-10-29 09:28:05 -06:00
input_dtype : InputDTypeEnum = Field (
2026-02-27 09:55:36 -06:00
default = InputDTypeEnum . int ,
title = "Model Input D Type" ,
description = "Data type of the model input tensor (for example 'float32')." ,
2024-10-29 09:28:05 -06:00
)
2023-02-03 17:36:37 -08:00
model_type : ModelTypeEnum = Field (
2026-02-27 09:55:36 -06:00
default = ModelTypeEnum . ssd ,
title = "Object Detection Model Type" ,
2026-07-22 11:13:58 -05:00
description = "Detector model architecture type (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine) used by some detectors for optimization." ,
2023-02-03 17:36:37 -08:00
)
2026-07-06 09:28:02 -08:00
_merged_labelmap : dict [ int , str ] | None = PrivateAttr ()
_colormap : dict [ int , tuple [ int , int , int ]] = PrivateAttr ()
2024-09-28 07:49:04 -06:00
_all_attributes : list [ str ] = PrivateAttr ()
2024-10-16 15:22:34 -06:00
_all_attribute_logos : list [ str ] = PrivateAttr ()
2023-04-24 07:24:28 -05:00
_model_hash : str = PrivateAttr ()
2022-12-15 07:12:52 -06:00
@property
2026-07-06 09:28:02 -08:00
def merged_labelmap ( self ) -> dict [ int , str ]:
2022-12-15 07:12:52 -06:00
return self . _merged_labelmap
@property
2026-07-06 09:28:02 -08:00
def colormap ( self ) -> dict [ int , tuple [ int , int , int ]]:
2022-12-15 07:12:52 -06:00
return self . _colormap
2025-01-29 10:52:21 -07:00
@property
def non_logo_attributes ( self ) -> list [ str ]:
2026-08-18 08:11:50 -06:00
return NON_LOGO_ATTRIBUTES
2025-01-29 10:52:21 -07:00
2024-09-28 07:49:04 -06:00
@property
def all_attributes ( self ) -> list [ str ]:
return self . _all_attributes
2024-10-16 15:22:34 -06:00
@property
def all_attribute_logos ( self ) -> list [ str ]:
return self . _all_attribute_logos
2023-04-24 07:24:28 -05:00
@property
def model_hash ( self ) -> str :
return self . _model_hash
2022-12-15 07:12:52 -06:00
def __init__ ( self , ** config ):
super () . __init__ ( ** config )
self . _merged_labelmap = {
** load_labels ( config . get ( "labelmap_path" , "/labelmap.txt" )),
** config . get ( "labelmap" , {}),
}
self . _colormap = {}
2023-01-06 18:31:54 -07:00
2024-09-28 07:49:04 -06:00
# generate list of attribute labels
unique_attributes = set ()
for attributes in self . attributes_map . values ():
unique_attributes . update ( attributes )
self . _all_attributes = list ( unique_attributes )
2024-10-16 15:22:34 -06:00
self . _all_attribute_logos = list (
2025-01-29 10:52:21 -07:00
unique_attributes - set ( self . non_logo_attributes )
2024-10-16 15:22:34 -06:00
)
2024-09-28 07:49:04 -06:00
2023-04-30 13:32:36 -05:00
def check_and_load_plus_model (
self , plus_api : PlusApi , detector : str = None
) -> None :
if not self . path or not self . path . startswith ( "plus://" ):
return
2025-05-09 08:36:44 -05:00
# ensure that model cache dir exists
os . makedirs ( MODEL_CACHE_DIR , exist_ok = True )
2023-04-30 13:32:36 -05:00
model_id = self . path [ 7 :]
2025-03-01 05:35:09 +01:00
self . path = os . path . join ( MODEL_CACHE_DIR , model_id )
2023-04-30 13:32:36 -05:00
model_info_path = f " { self . path } .json"
# download the model if it doesn't exist
if not os . path . isfile ( self . path ):
download_url = plus_api . get_model_download_url ( model_id )
r = requests . get ( download_url )
with open ( self . path , "wb" ) as f :
f . write ( r . content )
# download the model info if it doesn't exist
if not os . path . isfile ( model_info_path ):
model_info = plus_api . get_model_info ( model_id )
with open ( model_info_path , "w" ) as f :
json . dump ( model_info , f )
else :
2026-07-06 09:28:02 -08:00
with open ( model_info_path ) as f :
2025-05-13 16:27:20 +02:00
model_info : dict [ str , Any ] = json . load ( f )
2023-04-30 13:32:36 -05:00
if detector and detector not in model_info [ "supportedDetectors" ]:
raise ValueError ( f "Model does not support detector type of { detector } " )
self . width = model_info [ "width" ]
self . height = model_info [ "height" ]
2025-09-11 16:39:34 -06:00
self . input_tensor = InputTensorEnum ( model_info [ "inputShape" ])
self . input_pixel_format = PixelFormatEnum ( model_info [ "pixelFormat" ])
self . model_type = ModelTypeEnum ( model_info [ "type" ])
2024-09-28 07:49:04 -06:00
2025-08-27 06:27:08 -06:00
if model_info . get ( "inputDataType" ):
2025-09-11 16:39:34 -06:00
self . input_dtype = InputDTypeEnum ( model_info [ "inputDataType" ])
2025-08-27 06:27:08 -06:00
2025-09-09 06:17:56 -06:00
# RKNN always uses NHWC
if detector == "rknn" :
self . input_tensor = InputTensorEnum . nhwc
2024-09-28 07:49:04 -06:00
# generate list of attribute labels
self . attributes_map = {
** model_info . get ( "attributes" , DEFAULT_ATTRIBUTE_LABEL_MAP ),
** self . attributes_map ,
}
unique_attributes = set ()
for attributes in self . attributes_map . values ():
unique_attributes . update ( attributes )
self . _all_attributes = list ( unique_attributes )
2026-08-18 08:11:50 -06:00
self . _all_attribute_logos = list ( unique_attributes - set ( NON_LOGO_ATTRIBUTES ))
2024-09-28 07:49:04 -06:00
2023-04-30 13:32:36 -05:00
self . _merged_labelmap = {
** { int ( key ): val for key , val in model_info [ "labelMap" ] . items ()},
** self . labelmap ,
}
2023-04-24 07:24:28 -05:00
def compute_model_hash ( self ) -> None :
2023-05-21 16:29:57 +03:00
if not self . path or not os . path . exists ( self . path ):
self . _model_hash = hashlib . md5 ( b "unknown" ) . hexdigest ()
else :
with open ( self . path , "rb" ) as f :
file_hash = hashlib . md5 ()
while chunk := f . read ( 8192 ):
file_hash . update ( chunk )
self . _model_hash = file_hash . hexdigest ()
2023-04-24 07:24:28 -05:00
2023-01-06 18:31:54 -07:00
def create_colormap ( self , enabled_labels : set [ str ]) -> None :
2024-10-13 16:48:54 -05:00
"""Get a list of colors for enabled labels that aren't attributes."""
2024-10-13 20:34:51 -06:00
enabled_trackable_labels = list (
filter ( lambda label : label not in self . _all_attributes , enabled_labels )
2024-10-13 16:48:54 -05:00
)
2024-10-13 20:34:51 -06:00
colors = generate_color_palette ( len ( enabled_trackable_labels ))
self . _colormap = {
label : color for label , color in zip ( enabled_trackable_labels , colors )
}
2022-12-15 07:12:52 -06:00
2024-02-29 16:10:13 -07:00
model_config = ConfigDict ( extra = "forbid" , protected_namespaces = ())
2022-12-15 07:12:52 -06:00
class BaseDetectorConfig ( BaseModel ):
2026-08-18 08:11:50 -06:00
# how the trailing part of a device string ("openvino:GPU" -> "GPU") maps onto
# this detector's fields, and whether the same device may be listed more than
# once to run additional inference processes against it. Most accelerators
# multiplex fine, so this is opt-out rather than opt-in.
device_spec_field : ClassVar [ str ] = "device"
device_spec_type : ClassVar [ type ] = str
shareable : ClassVar [ bool ] = True
2022-12-15 07:12:52 -06:00
# the type field must be defined in all subclasses
2026-02-27 09:55:36 -06:00
type : str = Field (
default = "cpu" ,
title = "Detector Type" ,
description = "Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino')." ,
)
2026-07-06 09:28:02 -08:00
model : ModelConfig | None = Field (
2026-02-27 09:55:36 -06:00
default = None ,
title = "Detector specific model configuration" ,
description = "Detector-specific model configuration options (path, input size, etc.)." ,
2022-12-15 07:12:52 -06:00
)
2026-07-06 09:28:02 -08:00
model_path : str | None = Field (
2026-02-27 09:55:36 -06:00
default = None ,
title = "Detector specific model path" ,
description = "File path to the detector model binary if required by the chosen detector." ,
2025-01-07 20:59:37 -07:00
)
2024-02-29 16:10:13 -07:00
model_config = ConfigDict (
extra = "allow" , arbitrary_types_allowed = True , protected_namespaces = ()
)