diff --git a/h10support.patch b/h10support.patch deleted file mode 100644 index 3dc33898e..000000000 --- a/h10support.patch +++ /dev/null @@ -1,1546 +0,0 @@ -From 9e19dd642835d3675c079bd5e60a824d5e3316fc Mon Sep 17 00:00:00 2001 -From: matieu-d -Date: Sun, 29 Mar 2026 17:44:10 +0200 -Subject: [PATCH 1/3] Add Hailo-10H support - ---- - docker-compose.yml | 2 + - docker/hailo10h/user_installation.sh | 7 + - docker/main/install_hailort.sh | 6 +- - frigate/detectors/plugins/hailo10h.py | 415 ++++++++++++++++++++++++++ - 4 files changed, 427 insertions(+), 3 deletions(-) - create mode 100644 docker/hailo10h/user_installation.sh - create mode 100755 frigate/detectors/plugins/hailo10h.py - -diff --git a/docker-compose.yml b/docker-compose.yml -index db63297d5e..b7ab930e79 100644 ---- a/docker-compose.yml -+++ b/docker-compose.yml -@@ -27,6 +27,8 @@ services: - # devices: - # - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB - # - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware -+ devices: -+ - /dev/hailo0 - volumes: - - .:/workspace/frigate:cached - - ./web/dist:/opt/frigate/web:cached -diff --git a/docker/hailo10h/user_installation.sh b/docker/hailo10h/user_installation.sh -new file mode 100644 -index 0000000000..8a0e73538b ---- /dev/null -+++ b/docker/hailo10h/user_installation.sh -@@ -0,0 +1,7 @@ -+#!/bin/bash -+ -+# Update package list and install hailo driver version 5.1.1 for Hailo-10H -+sudo apt update -+sudo apt install -y hailo-h10-all=5.1.1 -+ -+ -diff --git a/docker/main/install_hailort.sh b/docker/main/install_hailort.sh -index 2e568a14ed..4fac2852e7 100755 ---- a/docker/main/install_hailort.sh -+++ b/docker/main/install_hailort.sh -@@ -2,7 +2,7 @@ - - set -euxo pipefail - --hailo_version="4.21.0" -+hailo_version="5.1.1" - - if [[ "${TARGETARCH}" == "amd64" ]]; then - arch="x86_64" -@@ -10,5 +10,5 @@ elif [[ "${TARGETARCH}" == "arm64" ]]; then - arch="aarch64" - fi - --wget -qO- "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - --wget -P /wheels/ "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" -+wget -qO- "https://github.com/mathieu-d/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - -+wget -P /wheels/ "https://github.com/mathieu-d/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" -diff --git a/frigate/detectors/plugins/hailo10h.py b/frigate/detectors/plugins/hailo10h.py -new file mode 100755 -index 0000000000..d930742923 ---- /dev/null -+++ b/frigate/detectors/plugins/hailo10h.py -@@ -0,0 +1,415 @@ -+import logging -+import os -+import subprocess -+import threading -+import urllib.request -+from functools import partial -+from typing import Dict, List, Optional, Tuple -+ -+import cv2 -+import numpy as np -+from pydantic import ConfigDict, Field -+from typing_extensions import Literal -+ -+from frigate.const import MODEL_CACHE_DIR -+from frigate.detectors.detection_api import DetectionApi -+from frigate.detectors.detector_config import ( -+ BaseDetectorConfig, -+) -+from frigate.object_detection.util import RequestStore, ResponseStore -+ -+logger = logging.getLogger(__name__) -+ -+ -+# ----------------- Utility Functions ----------------- # -+ -+ -+def preprocess_tensor(image: np.ndarray, model_w: int, model_h: int) -> np.ndarray: -+ """ -+ Resize an image with unchanged aspect ratio using padding. -+ Assumes input image shape is (H, W, 3). -+ """ -+ if image.ndim == 4 and image.shape[0] == 1: -+ image = image[0] -+ -+ h, w = image.shape[:2] -+ scale = min(model_w / w, model_h / h) -+ new_w, new_h = int(w * scale), int(h * scale) -+ resized_image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_CUBIC) -+ padded_image = np.full((model_h, model_w, 3), 114, dtype=image.dtype) -+ x_offset = (model_w - new_w) // 2 -+ y_offset = (model_h - new_h) // 2 -+ padded_image[y_offset : y_offset + new_h, x_offset : x_offset + new_w] = ( -+ resized_image -+ ) -+ return padded_image -+ -+ -+# ----------------- Global Constants ----------------- # -+DETECTOR_KEY = "hailo10h" -+ARCH = None -+H10H_DEFAULT_MODEL = "yolov6n.hef" -+H10H_DEFAULT_URL = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v5.2.0/hailo10h/yolov6n.hef" -+ -+ -+def detect_hailo_arch(): -+ try: -+ result = subprocess.run( -+ ["hailortcli", "fw-control", "identify"], capture_output=True, text=True -+ ) -+ if result.returncode != 0: -+ logger.error(f"Inference error: {result.stderr}") -+ return None -+ for line in result.stdout.split("\n"): -+ if "Device Architecture" in line: -+ if "HAILO10H" in line: -+ return "hailo10h" -+ logger.error("Inference error: Could not determine Hailo architecture.") -+ return None -+ except Exception as e: -+ logger.error(f"Inference error: {e}") -+ return None -+ -+ -+# ----------------- HailoAsyncInference Class ----------------- # -+class HailoAsyncInference: -+ def __init__( -+ self, -+ hef_path: str, -+ input_store: RequestStore, -+ output_store: ResponseStore, -+ batch_size: int = 1, -+ input_type: Optional[str] = None, -+ output_type: Optional[Dict[str, str]] = None, -+ send_original_frame: bool = False, -+ ) -> None: -+ # when importing hailo it activates the driver -+ # which leaves processes running even though it may not be used. -+ try: -+ from hailo_platform import ( -+ HEF, -+ FormatType, -+ HailoSchedulingAlgorithm, -+ VDevice, -+ ) -+ except ModuleNotFoundError: -+ pass -+ -+ self.input_store = input_store -+ self.output_store = output_store -+ -+ params = VDevice.create_params() -+ params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN -+ -+ self.hef = HEF(hef_path) -+ self.target = VDevice(params) -+ self.infer_model = self.target.create_infer_model(hef_path) -+ self.infer_model.set_batch_size(batch_size) -+ -+ if input_type is not None: -+ self.infer_model.input().set_format_type(getattr(FormatType, input_type)) -+ -+ if output_type is not None: -+ for output_name, output_type in output_type.items(): -+ self.infer_model.output(output_name).set_format_type( -+ getattr(FormatType, output_type) -+ ) -+ -+ self.output_type = output_type -+ self.send_original_frame = send_original_frame -+ -+ def callback( -+ self, -+ completion_info, -+ bindings_list: List, -+ input_batch: List, -+ request_ids: List[int], -+ ): -+ if completion_info.exception: -+ logger.error(f"Inference error: {completion_info.exception}") -+ else: -+ for i, bindings in enumerate(bindings_list): -+ if len(bindings._output_names) == 1: -+ result = bindings.output().get_buffer() -+ else: -+ result = { -+ name: np.expand_dims(bindings.output(name).get_buffer(), axis=0) -+ for name in bindings._output_names -+ } -+ self.output_store.put(request_ids[i], (input_batch[i], result)) -+ -+ def _create_bindings(self, configured_infer_model) -> object: -+ if self.output_type is None: -+ output_buffers = { -+ output_info.name: np.empty( -+ self.infer_model.output(output_info.name).shape, -+ dtype=getattr( -+ np, str(output_info.format.type).split(".")[1].lower() -+ ), -+ ) -+ for output_info in self.hef.get_output_vstream_infos() -+ } -+ else: -+ output_buffers = { -+ name: np.empty( -+ self.infer_model.output(name).shape, -+ dtype=getattr(np, self.output_type[name].lower()), -+ ) -+ for name in self.output_type -+ } -+ return configured_infer_model.create_bindings(output_buffers=output_buffers) -+ -+ def get_input_shape(self) -> Tuple[int, ...]: -+ return self.hef.get_input_vstream_infos()[0].shape -+ -+ def run(self) -> None: -+ job = None -+ with self.infer_model.configure() as configured_infer_model: -+ while True: -+ batch_data = self.input_store.get() -+ -+ if batch_data is None: -+ break -+ -+ request_id, frame_data = batch_data -+ preprocessed_batch = [frame_data] -+ request_ids = [request_id] -+ input_batch = preprocessed_batch # non-send_original_frame mode -+ -+ bindings_list = [] -+ for frame in preprocessed_batch: -+ bindings = self._create_bindings(configured_infer_model) -+ bindings.input().set_buffer(np.array(frame)) -+ bindings_list.append(bindings) -+ configured_infer_model.wait_for_async_ready(timeout_ms=10000) -+ job = configured_infer_model.run_async( -+ bindings_list, -+ partial( -+ self.callback, -+ input_batch=input_batch, -+ request_ids=request_ids, -+ bindings_list=bindings_list, -+ ), -+ ) -+ -+ if job is not None: -+ job.wait(100) -+ -+ -+# ----------------- HailoDetector Class ----------------- # -+class HailoDetector(DetectionApi): -+ type_key = DETECTOR_KEY -+ -+ def __init__(self, detector_config: "HailoDetectorConfig"): -+ global ARCH -+ ARCH = detect_hailo_arch() -+ self.cache_dir = MODEL_CACHE_DIR -+ self.device_type = detector_config.device -+ self.model_height = ( -+ detector_config.model.height -+ if hasattr(detector_config.model, "height") -+ else None -+ ) -+ self.model_width = ( -+ detector_config.model.width -+ if hasattr(detector_config.model, "width") -+ else None -+ ) -+ self.model_type = ( -+ detector_config.model.model_type -+ if hasattr(detector_config.model, "model_type") -+ else None -+ ) -+ self.tensor_format = ( -+ detector_config.model.input_tensor -+ if hasattr(detector_config.model, "input_tensor") -+ else None -+ ) -+ self.pixel_format = ( -+ detector_config.model.input_pixel_format -+ if hasattr(detector_config.model, "input_pixel_format") -+ else None -+ ) -+ self.input_dtype = ( -+ detector_config.model.input_dtype -+ if hasattr(detector_config.model, "input_dtype") -+ else None -+ ) -+ self.output_type = "FLOAT32" -+ self.set_path_and_url(detector_config.model.path) -+ self.working_model_path = self.check_and_prepare() -+ -+ self.batch_size = 1 -+ self.input_store = RequestStore() -+ self.response_store = ResponseStore() -+ -+ try: -+ logger.debug(f"[INIT] Loading HEF model from {self.working_model_path}") -+ self.inference_engine = HailoAsyncInference( -+ self.working_model_path, -+ self.input_store, -+ self.response_store, -+ self.batch_size, -+ ) -+ self.input_shape = self.inference_engine.get_input_shape() -+ logger.debug(f"[INIT] Model input shape: {self.input_shape}") -+ self.inference_thread = threading.Thread( -+ target=self.inference_engine.run, daemon=True -+ ) -+ self.inference_thread.start() -+ except Exception as e: -+ logger.error(f"[INIT] Failed to initialize HailoAsyncInference: {e}") -+ raise -+ -+ def set_path_and_url(self, path: str = None): -+ if not path: -+ self.model_path = None -+ self.url = None -+ return -+ if self.is_url(path): -+ self.url = path -+ self.model_path = None -+ else: -+ self.model_path = path -+ self.url = None -+ -+ def is_url(self, url: str) -> bool: -+ return ( -+ url.startswith("http://") -+ or url.startswith("https://") -+ or url.startswith("www.") -+ ) -+ -+ @staticmethod -+ def extract_model_name(path: str = None, url: str = None) -> str: -+ if path and path.endswith(".hef"): -+ return os.path.basename(path) -+ elif url and url.endswith(".hef"): -+ return os.path.basename(url) -+ else: -+ return H10H_DEFAULT_MODEL -+ -+ @staticmethod -+ def download_model(url: str, destination: str): -+ if not url.endswith(".hef"): -+ raise ValueError("Invalid model URL. Only .hef files are supported.") -+ try: -+ urllib.request.urlretrieve(url, destination) -+ logger.debug(f"Downloaded model to {destination}") -+ except Exception as e: -+ raise RuntimeError(f"Failed to download model from {url}: {str(e)}") -+ -+ def check_and_prepare(self) -> str: -+ if not os.path.exists(self.cache_dir): -+ os.makedirs(self.cache_dir) -+ model_name = self.extract_model_name(self.model_path, self.url) -+ cached_model_path = os.path.join(self.cache_dir, model_name) -+ if not self.model_path and not self.url: -+ if os.path.exists(cached_model_path): -+ logger.debug(f"Model found in cache: {cached_model_path}") -+ return cached_model_path -+ else: -+ logger.debug(f"Downloading default model: {model_name}") -+ self.download_model(H10H_DEFAULT_URL, cached_model_path) -+ -+ elif self.url: -+ logger.debug(f"Downloading model from URL: {self.url}") -+ self.download_model(self.url, cached_model_path) -+ elif self.model_path: -+ if os.path.exists(self.model_path): -+ logger.debug(f"Using existing model at: {self.model_path}") -+ return self.model_path -+ else: -+ raise FileNotFoundError(f"Model file not found at: {self.model_path}") -+ return cached_model_path -+ -+ def detect_raw(self, tensor_input): -+ tensor_input = self.preprocess(tensor_input) -+ -+ if isinstance(tensor_input, np.ndarray) and len(tensor_input.shape) == 3: -+ tensor_input = np.expand_dims(tensor_input, axis=0) -+ -+ request_id = self.input_store.put(tensor_input) -+ -+ try: -+ _, infer_results = self.response_store.get(request_id, timeout=1.0) -+ except TimeoutError: -+ logger.error( -+ f"Timeout waiting for inference results for request {request_id}" -+ ) -+ -+ if not self.inference_thread.is_alive(): -+ raise RuntimeError( -+ "HailoRT inference thread has stopped, restart required." -+ ) -+ -+ return np.zeros((20, 6), dtype=np.float32) -+ -+ if isinstance(infer_results, list) and len(infer_results) == 1: -+ infer_results = infer_results[0] -+ -+ threshold = 0.4 -+ all_detections = [] -+ for class_id, detection_set in enumerate(infer_results): -+ if not isinstance(detection_set, np.ndarray) or detection_set.size == 0: -+ continue -+ for det in detection_set: -+ if det.shape[0] < 5: -+ continue -+ score = float(det[4]) -+ if score < threshold: -+ continue -+ all_detections.append([class_id, score, det[0], det[1], det[2], det[3]]) -+ -+ if len(all_detections) == 0: -+ detections_array = np.zeros((20, 6), dtype=np.float32) -+ else: -+ detections_array = np.array(all_detections, dtype=np.float32) -+ if detections_array.shape[0] > 20: -+ detections_array = detections_array[:20, :] -+ elif detections_array.shape[0] < 20: -+ pad = np.zeros((20 - detections_array.shape[0], 6), dtype=np.float32) -+ detections_array = np.vstack((detections_array, pad)) -+ -+ return detections_array -+ -+ def preprocess(self, image): -+ if isinstance(image, np.ndarray): -+ processed = preprocess_tensor( -+ image, self.input_shape[1], self.input_shape[0] -+ ) -+ return np.expand_dims(processed, axis=0) -+ else: -+ raise ValueError("Unsupported image format for preprocessing") -+ -+ def close(self): -+ """Properly shuts down the inference engine and releases the VDevice.""" -+ logger.debug("[CLOSE] Closing HailoDetector") -+ try: -+ if hasattr(self, "inference_engine"): -+ if hasattr(self.inference_engine, "target"): -+ self.inference_engine.target.release() -+ logger.debug("Hailo VDevice released successfully") -+ except Exception as e: -+ logger.error(f"Failed to close Hailo device: {e}") -+ raise -+ -+ def __del__(self): -+ """Destructor to ensure cleanup when the object is deleted.""" -+ self.close() -+ -+ -+# ----------------- HailoDetectorConfig Class ----------------- # -+class HailoDetectorConfig(BaseDetectorConfig): -+ """Hailo-8/Hailo-8L detector using HEF models and the HailoRT SDK for inference on Hailo hardware.""" -+ -+ model_config = ConfigDict( -+ title="Hailo-8/Hailo-8L", -+ ) -+ -+ type: Literal[DETECTOR_KEY] -+ device: str = Field( -+ default="PCIe", -+ title="Device Type", -+ description="The device to use for Hailo inference (e.g. 'PCIe', 'M.2').", -+ ) - -From 0a704fefc3081dcc0c0e287afee29f3c0f27753e Mon Sep 17 00:00:00 2001 -From: matieu-d -Date: Sun, 29 Mar 2026 17:44:10 +0200 -Subject: [PATCH 2/3] Add Hailo-10H support - ---- - docker-compose.yml | 6 + - docker/hailo10h/user_installation.sh | 7 + - docker/main/Dockerfile | 2 + - docker/main/install_hailort.sh | 6 +- - frigate/detectors/plugins/hailo10h.py | 415 ++++++++++++++++++++++++++ - frigate/stats/util.py | 9 + - 6 files changed, 441 insertions(+), 4 deletions(-) - create mode 100644 docker/hailo10h/user_installation.sh - create mode 100755 frigate/detectors/plugins/hailo10h.py - -diff --git a/docker-compose.yml b/docker-compose.yml -index db63297d5e..b78e840cbb 100644 ---- a/docker-compose.yml -+++ b/docker-compose.yml -@@ -12,6 +12,10 @@ services: - build: - context: . - dockerfile: docker/main/Dockerfile -+ # Use args to specify hailort version ans location -+ args: -+ HAILORT_VERSION: "5.1.1" -+ HAILORT_GIT_REPO: "mathieu-d/frigate" - # Use target devcontainer-trt for TensorRT dev - target: devcontainer - ## Uncomment this block for nvidia gpu support -@@ -27,6 +31,8 @@ services: - # devices: - # - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB - # - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware -+ devices: -+ - /dev/hailo0 - volumes: - - .:/workspace/frigate:cached - - ./web/dist:/opt/frigate/web:cached -diff --git a/docker/hailo10h/user_installation.sh b/docker/hailo10h/user_installation.sh -new file mode 100644 -index 0000000000..8a0e73538b ---- /dev/null -+++ b/docker/hailo10h/user_installation.sh -@@ -0,0 +1,7 @@ -+#!/bin/bash -+ -+# Update package list and install hailo driver version 5.1.1 for Hailo-10H -+sudo apt update -+sudo apt install -y hailo-h10-all=5.1.1 -+ -+ -diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile -index b143200330..6874c27f83 100644 ---- a/docker/main/Dockerfile -+++ b/docker/main/Dockerfile -@@ -149,6 +149,8 @@ FROM base AS wheels - ARG DEBIAN_FRONTEND - ARG TARGETARCH - ARG DEBUG=false -+ARG HAILORT_VERSION = "4.21.0" -+ARG HAILORT_GIT_REPO = "frigate-nvr/hailort" - - # Use a separate container to build wheels to prevent build dependencies in final image - RUN apt-get -qq update \ -diff --git a/docker/main/install_hailort.sh b/docker/main/install_hailort.sh -index 2e568a14ed..96d64cc1e4 100755 ---- a/docker/main/install_hailort.sh -+++ b/docker/main/install_hailort.sh -@@ -2,13 +2,11 @@ - - set -euxo pipefail - --hailo_version="4.21.0" -- - if [[ "${TARGETARCH}" == "amd64" ]]; then - arch="x86_64" - elif [[ "${TARGETARCH}" == "arm64" ]]; then - arch="aarch64" - fi - --wget -qO- "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - --wget -P /wheels/ "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" -+wget -qO- "https://github.com/${HAILORT_GIT_REPO}/releases/download/v${HAILORT_VERSION}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - -+wget -P /wheels/ "https://github.com/${HAILORT_GIT_REPO}/releases/download/v${HAILORT_VERSION}/hailort-${HAILORT_VERSION}-cp311-cp311-linux_${arch}.whl" -diff --git a/frigate/detectors/plugins/hailo10h.py b/frigate/detectors/plugins/hailo10h.py -new file mode 100755 -index 0000000000..f03054707b ---- /dev/null -+++ b/frigate/detectors/plugins/hailo10h.py -@@ -0,0 +1,415 @@ -+import logging -+import os -+import subprocess -+import threading -+import urllib.request -+from functools import partial -+from typing import Dict, List, Optional, Tuple -+ -+import cv2 -+import numpy as np -+from pydantic import ConfigDict, Field -+from typing_extensions import Literal -+ -+from frigate.const import MODEL_CACHE_DIR -+from frigate.detectors.detection_api import DetectionApi -+from frigate.detectors.detector_config import ( -+ BaseDetectorConfig, -+) -+from frigate.object_detection.util import RequestStore, ResponseStore -+ -+logger = logging.getLogger(__name__) -+ -+ -+# ----------------- Utility Functions ----------------- # -+ -+ -+def preprocess_tensor(image: np.ndarray, model_w: int, model_h: int) -> np.ndarray: -+ """ -+ Resize an image with unchanged aspect ratio using padding. -+ Assumes input image shape is (H, W, 3). -+ """ -+ if image.ndim == 4 and image.shape[0] == 1: -+ image = image[0] -+ -+ h, w = image.shape[:2] -+ scale = min(model_w / w, model_h / h) -+ new_w, new_h = int(w * scale), int(h * scale) -+ resized_image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_CUBIC) -+ padded_image = np.full((model_h, model_w, 3), 114, dtype=image.dtype) -+ x_offset = (model_w - new_w) // 2 -+ y_offset = (model_h - new_h) // 2 -+ padded_image[y_offset : y_offset + new_h, x_offset : x_offset + new_w] = ( -+ resized_image -+ ) -+ return padded_image -+ -+ -+# ----------------- Global Constants ----------------- # -+DETECTOR_KEY = "hailo10h" -+ARCH = None -+H10H_DEFAULT_MODEL = "yolov6n.hef" -+H10H_DEFAULT_URL = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v5.2.0/hailo10h/yolov6n.hef" -+ -+ -+def detect_hailo_arch(): -+ try: -+ result = subprocess.run( -+ ["hailortcli", "fw-control", "identify"], capture_output=True, text=True -+ ) -+ if result.returncode != 0: -+ logger.error(f"Inference error: {result.stderr}") -+ return None -+ for line in result.stdout.split("\n"): -+ if "Device Architecture" in line: -+ if "HAILO10H" in line: -+ return "hailo10h" -+ logger.error("Inference error: Could not determine Hailo architecture.") -+ return None -+ except Exception as e: -+ logger.error(f"Inference error: {e}") -+ return None -+ -+ -+# ----------------- HailoAsyncInference Class ----------------- # -+class HailoAsyncInference: -+ def __init__( -+ self, -+ hef_path: str, -+ input_store: RequestStore, -+ output_store: ResponseStore, -+ batch_size: int = 1, -+ input_type: Optional[str] = None, -+ output_type: Optional[Dict[str, str]] = None, -+ send_original_frame: bool = False, -+ ) -> None: -+ # when importing hailo it activates the driver -+ # which leaves processes running even though it may not be used. -+ try: -+ from hailo_platform import ( -+ HEF, -+ FormatType, -+ HailoSchedulingAlgorithm, -+ VDevice, -+ ) -+ except ModuleNotFoundError: -+ pass -+ -+ self.input_store = input_store -+ self.output_store = output_store -+ -+ params = VDevice.create_params() -+ params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN -+ -+ self.hef = HEF(hef_path) -+ self.target = VDevice(params) -+ self.infer_model = self.target.create_infer_model(hef_path) -+ self.infer_model.set_batch_size(batch_size) -+ -+ if input_type is not None: -+ self.infer_model.input().set_format_type(getattr(FormatType, input_type)) -+ -+ if output_type is not None: -+ for output_name, output_type in output_type.items(): -+ self.infer_model.output(output_name).set_format_type( -+ getattr(FormatType, output_type) -+ ) -+ -+ self.output_type = output_type -+ self.send_original_frame = send_original_frame -+ -+ def callback( -+ self, -+ completion_info, -+ bindings_list: List, -+ input_batch: List, -+ request_ids: List[int], -+ ): -+ if completion_info.exception: -+ logger.error(f"Inference error: {completion_info.exception}") -+ else: -+ for i, bindings in enumerate(bindings_list): -+ if len(bindings._output_names) == 1: -+ result = bindings.output().get_buffer() -+ else: -+ result = { -+ name: np.expand_dims(bindings.output(name).get_buffer(), axis=0) -+ for name in bindings._output_names -+ } -+ self.output_store.put(request_ids[i], (input_batch[i], result)) -+ -+ def _create_bindings(self, configured_infer_model) -> object: -+ if self.output_type is None: -+ output_buffers = { -+ output_info.name: np.empty( -+ self.infer_model.output(output_info.name).shape, -+ dtype=getattr( -+ np, str(output_info.format.type).split(".")[1].lower() -+ ), -+ ) -+ for output_info in self.hef.get_output_vstream_infos() -+ } -+ else: -+ output_buffers = { -+ name: np.empty( -+ self.infer_model.output(name).shape, -+ dtype=getattr(np, self.output_type[name].lower()), -+ ) -+ for name in self.output_type -+ } -+ return configured_infer_model.create_bindings(output_buffers=output_buffers) -+ -+ def get_input_shape(self) -> Tuple[int, ...]: -+ return self.hef.get_input_vstream_infos()[0].shape -+ -+ def run(self) -> None: -+ job = None -+ with self.infer_model.configure() as configured_infer_model: -+ while True: -+ batch_data = self.input_store.get() -+ -+ if batch_data is None: -+ break -+ -+ request_id, frame_data = batch_data -+ preprocessed_batch = [frame_data] -+ request_ids = [request_id] -+ input_batch = preprocessed_batch # non-send_original_frame mode -+ -+ bindings_list = [] -+ for frame in preprocessed_batch: -+ bindings = self._create_bindings(configured_infer_model) -+ bindings.input().set_buffer(np.array(frame)) -+ bindings_list.append(bindings) -+ configured_infer_model.wait_for_async_ready(timeout_ms=10000) -+ job = configured_infer_model.run_async( -+ bindings_list, -+ partial( -+ self.callback, -+ input_batch=input_batch, -+ request_ids=request_ids, -+ bindings_list=bindings_list, -+ ), -+ ) -+ -+ if job is not None: -+ job.wait(100) -+ -+ -+# ----------------- HailoDetector Class ----------------- # -+class HailoDetector(DetectionApi): -+ type_key = DETECTOR_KEY -+ -+ def __init__(self, detector_config: "HailoDetectorConfig"): -+ global ARCH -+ ARCH = detect_hailo_arch() -+ self.cache_dir = MODEL_CACHE_DIR -+ self.device_type = detector_config.device -+ self.model_height = ( -+ detector_config.model.height -+ if hasattr(detector_config.model, "height") -+ else None -+ ) -+ self.model_width = ( -+ detector_config.model.width -+ if hasattr(detector_config.model, "width") -+ else None -+ ) -+ self.model_type = ( -+ detector_config.model.model_type -+ if hasattr(detector_config.model, "model_type") -+ else None -+ ) -+ self.tensor_format = ( -+ detector_config.model.input_tensor -+ if hasattr(detector_config.model, "input_tensor") -+ else None -+ ) -+ self.pixel_format = ( -+ detector_config.model.input_pixel_format -+ if hasattr(detector_config.model, "input_pixel_format") -+ else None -+ ) -+ self.input_dtype = ( -+ detector_config.model.input_dtype -+ if hasattr(detector_config.model, "input_dtype") -+ else None -+ ) -+ self.output_type = "FLOAT32" -+ self.set_path_and_url(detector_config.model.path) -+ self.working_model_path = self.check_and_prepare() -+ -+ self.batch_size = 1 -+ self.input_store = RequestStore() -+ self.response_store = ResponseStore() -+ -+ try: -+ logger.debug(f"[INIT] Loading HEF model from {self.working_model_path}") -+ self.inference_engine = HailoAsyncInference( -+ self.working_model_path, -+ self.input_store, -+ self.response_store, -+ self.batch_size, -+ ) -+ self.input_shape = self.inference_engine.get_input_shape() -+ logger.debug(f"[INIT] Model input shape: {self.input_shape}") -+ self.inference_thread = threading.Thread( -+ target=self.inference_engine.run, daemon=True -+ ) -+ self.inference_thread.start() -+ except Exception as e: -+ logger.error(f"[INIT] Failed to initialize HailoAsyncInference: {e}") -+ raise -+ -+ def set_path_and_url(self, path: str = None): -+ if not path: -+ self.model_path = None -+ self.url = None -+ return -+ if self.is_url(path): -+ self.url = path -+ self.model_path = None -+ else: -+ self.model_path = path -+ self.url = None -+ -+ def is_url(self, url: str) -> bool: -+ return ( -+ url.startswith("http://") -+ or url.startswith("https://") -+ or url.startswith("www.") -+ ) -+ -+ @staticmethod -+ def extract_model_name(path: str = None, url: str = None) -> str: -+ if path and path.endswith(".hef"): -+ return os.path.basename(path) -+ elif url and url.endswith(".hef"): -+ return os.path.basename(url) -+ else: -+ return H10H_DEFAULT_MODEL -+ -+ @staticmethod -+ def download_model(url: str, destination: str): -+ if not url.endswith(".hef"): -+ raise ValueError("Invalid model URL. Only .hef files are supported.") -+ try: -+ urllib.request.urlretrieve(url, destination) -+ logger.debug(f"Downloaded model to {destination}") -+ except Exception as e: -+ raise RuntimeError(f"Failed to download model from {url}: {str(e)}") -+ -+ def check_and_prepare(self) -> str: -+ if not os.path.exists(self.cache_dir): -+ os.makedirs(self.cache_dir) -+ model_name = self.extract_model_name(self.model_path, self.url) -+ cached_model_path = os.path.join(self.cache_dir, model_name) -+ if not self.model_path and not self.url: -+ if os.path.exists(cached_model_path): -+ logger.debug(f"Model found in cache: {cached_model_path}") -+ return cached_model_path -+ else: -+ logger.debug(f"Downloading default model: {model_name}") -+ self.download_model(H10H_DEFAULT_URL, cached_model_path) -+ -+ elif self.url: -+ logger.debug(f"Downloading model from URL: {self.url}") -+ self.download_model(self.url, cached_model_path) -+ elif self.model_path: -+ if os.path.exists(self.model_path): -+ logger.debug(f"Using existing model at: {self.model_path}") -+ return self.model_path -+ else: -+ raise FileNotFoundError(f"Model file not found at: {self.model_path}") -+ return cached_model_path -+ -+ def detect_raw(self, tensor_input): -+ tensor_input = self.preprocess(tensor_input) -+ -+ if isinstance(tensor_input, np.ndarray) and len(tensor_input.shape) == 3: -+ tensor_input = np.expand_dims(tensor_input, axis=0) -+ -+ request_id = self.input_store.put(tensor_input) -+ -+ try: -+ _, infer_results = self.response_store.get(request_id, timeout=1.0) -+ except TimeoutError: -+ logger.error( -+ f"Timeout waiting for inference results for request {request_id}" -+ ) -+ -+ if not self.inference_thread.is_alive(): -+ raise RuntimeError( -+ "HailoRT inference thread has stopped, restart required." -+ ) -+ -+ return np.zeros((20, 6), dtype=np.float32) -+ -+ if isinstance(infer_results, list) and len(infer_results) == 1: -+ infer_results = infer_results[0] -+ -+ threshold = 0.4 -+ all_detections = [] -+ for class_id, detection_set in enumerate(infer_results): -+ if not isinstance(detection_set, np.ndarray) or detection_set.size == 0: -+ continue -+ for det in detection_set: -+ if det.shape[0] < 5: -+ continue -+ score = float(det[4]) -+ if score < threshold: -+ continue -+ all_detections.append([class_id, score, det[0], det[1], det[2], det[3]]) -+ -+ if len(all_detections) == 0: -+ detections_array = np.zeros((20, 6), dtype=np.float32) -+ else: -+ detections_array = np.array(all_detections, dtype=np.float32) -+ if detections_array.shape[0] > 20: -+ detections_array = detections_array[:20, :] -+ elif detections_array.shape[0] < 20: -+ pad = np.zeros((20 - detections_array.shape[0], 6), dtype=np.float32) -+ detections_array = np.vstack((detections_array, pad)) -+ -+ return detections_array -+ -+ def preprocess(self, image): -+ if isinstance(image, np.ndarray): -+ processed = preprocess_tensor( -+ image, self.input_shape[1], self.input_shape[0] -+ ) -+ return np.expand_dims(processed, axis=0) -+ else: -+ raise ValueError("Unsupported image format for preprocessing") -+ -+ def close(self): -+ """Properly shuts down the inference engine and releases the VDevice.""" -+ logger.debug("[CLOSE] Closing HailoDetector") -+ try: -+ if hasattr(self, "inference_engine"): -+ if hasattr(self.inference_engine, "target"): -+ self.inference_engine.target.release() -+ logger.debug("Hailo VDevice released successfully") -+ except Exception as e: -+ logger.error(f"Failed to close Hailo device: {e}") -+ raise -+ -+ def __del__(self): -+ """Destructor to ensure cleanup when the object is deleted.""" -+ self.close() -+ -+ -+# ----------------- HailoDetectorConfig Class ----------------- # -+class HailoDetectorConfig(BaseDetectorConfig): -+ """Hailo10H detector using HEF models and the HailoRT SDK for inference on Hailo hardware.""" -+ -+ model_config = ConfigDict( -+ title="Hailo-10H", -+ ) -+ -+ type: Literal[DETECTOR_KEY] -+ device: str = Field( -+ default="PCIe", -+ title="Device Type", -+ description="The device to use for Hailo inference (e.g. 'PCIe', 'M.2').", -+ ) -diff --git a/frigate/stats/util.py b/frigate/stats/util.py -index f4f91f83f7..1ed3f1a7d9 100644 ---- a/frigate/stats/util.py -+++ b/frigate/stats/util.py -@@ -122,6 +122,15 @@ def get_detector_temperature( - if index < len(hailo_device_names): - device_name = hailo_device_names[index] - return hailo_temps[device_name] -+ elif detector_type == "hailo10h": -+ # Get temperatures for Hailo devices -+ hailo_temps = get_hailo_temps() -+ if hailo_temps: -+ hailo_device_names = sorted(hailo_temps.keys()) -+ index = detector_index_by_type.get("hailo10h", 0) -+ if index < len(hailo_device_names): -+ device_name = hailo_device_names[index] -+ return hailo_temps[device_name] - elif detector_type == "rknn": - # Rockchip temperatures are handled by the GPU / NPU stats - # as there are not detector specific temperatures - -From e68fd2662ff4a708af15823aa1ff3c422e0e27d8 Mon Sep 17 00:00:00 2001 -From: matieu-d -Date: Sun, 29 Mar 2026 17:44:10 +0200 -Subject: [PATCH 3/3] Add Hailo-10H support - ---- - Makefile | 7 + - docker-compose.yml | 6 + - docker/hailo10h/user_installation.sh | 7 + - docker/main/Dockerfile | 4 + - docker/main/install_hailort.sh | 6 +- - frigate/detectors/plugins/hailo10h.py | 415 ++++++++++++++++++++++++++ - 6 files changed, 442 insertions(+), 3 deletions(-) - create mode 100644 docker/hailo10h/user_installation.sh - create mode 100755 frigate/detectors/plugins/hailo10h.py - -diff --git a/Makefile b/Makefile -index 3800399ea1..54cd714e0a 100644 ---- a/Makefile -+++ b/Makefile -@@ -21,6 +21,13 @@ local: version - --tag frigate:latest \ - --load - -+localh10: version -+ docker buildx build --target=frigate --file docker/main/Dockerfile . \ -+ --build-arg HAILORT_VERSION=5.1.1 \ -+ --build-arg HAILORT_GIT_REPO=mathieu-d/hailort \ -+ --tag frigate:latest \ -+ --load -+ - debug: version - docker buildx build --target=frigate --file docker/main/Dockerfile . \ - --build-arg DEBUG=true \ -diff --git a/docker-compose.yml b/docker-compose.yml -index db63297d5e..96c1d87eaa 100644 ---- a/docker-compose.yml -+++ b/docker-compose.yml -@@ -12,6 +12,10 @@ services: - build: - context: . - dockerfile: docker/main/Dockerfile -+ # Use args to specify hailort version ans location -+ args: -+ HAILORT_VERSION: "5.1.1" -+ HAILORT_GIT_REPO: "mathieu-d/hailort" - # Use target devcontainer-trt for TensorRT dev - target: devcontainer - ## Uncomment this block for nvidia gpu support -@@ -27,6 +31,8 @@ services: - # devices: - # - /dev/bus/usb:/dev/bus/usb # Uncomment for Google Coral USB - # - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware -+ devices: -+ - /dev/hailo0 - volumes: - - .:/workspace/frigate:cached - - ./web/dist:/opt/frigate/web:cached -diff --git a/docker/hailo10h/user_installation.sh b/docker/hailo10h/user_installation.sh -new file mode 100644 -index 0000000000..8a0e73538b ---- /dev/null -+++ b/docker/hailo10h/user_installation.sh -@@ -0,0 +1,7 @@ -+#!/bin/bash -+ -+# Update package list and install hailo driver version 5.1.1 for Hailo-10H -+sudo apt update -+sudo apt install -y hailo-h10-all=5.1.1 -+ -+ -diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile -index b143200330..a66817dab3 100644 ---- a/docker/main/Dockerfile -+++ b/docker/main/Dockerfile -@@ -149,6 +149,8 @@ FROM base AS wheels - ARG DEBIAN_FRONTEND - ARG TARGETARCH - ARG DEBUG=false -+ARG HAILORT_VERSION=4.21.0 -+ARG HAILORT_GIT_REPO=frigate-nvr/hailort - - # Use a separate container to build wheels to prevent build dependencies in final image - RUN apt-get -qq update \ -@@ -326,6 +328,8 @@ CMD ["sleep", "infinity"] - # This should be architecture agnostic, so speed up the build on multiarch by not using QEMU. - FROM --platform=$BUILDPLATFORM node:20 AS web-build - -+ENV NODE_OPTIONS="--max-old-space-size=4096" -+ - WORKDIR /work - COPY web/package.json web/package-lock.json ./ - RUN npm install -diff --git a/docker/main/install_hailort.sh b/docker/main/install_hailort.sh -index 2e568a14ed..4fac2852e7 100755 ---- a/docker/main/install_hailort.sh -+++ b/docker/main/install_hailort.sh -@@ -2,7 +2,7 @@ - - set -euxo pipefail - --hailo_version="4.21.0" -+hailo_version="5.1.1" - - if [[ "${TARGETARCH}" == "amd64" ]]; then - arch="x86_64" -@@ -10,5 +10,5 @@ elif [[ "${TARGETARCH}" == "arm64" ]]; then - arch="aarch64" - fi - --wget -qO- "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - --wget -P /wheels/ "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" -+wget -qO- "https://github.com/mathieu-d/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - -+wget -P /wheels/ "https://github.com/mathieu-d/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" -diff --git a/frigate/detectors/plugins/hailo10h.py b/frigate/detectors/plugins/hailo10h.py -new file mode 100755 -index 0000000000..d930742923 ---- /dev/null -+++ b/frigate/detectors/plugins/hailo10h.py -@@ -0,0 +1,415 @@ -+import logging -+import os -+import subprocess -+import threading -+import urllib.request -+from functools import partial -+from typing import Dict, List, Optional, Tuple -+ -+import cv2 -+import numpy as np -+from pydantic import ConfigDict, Field -+from typing_extensions import Literal -+ -+from frigate.const import MODEL_CACHE_DIR -+from frigate.detectors.detection_api import DetectionApi -+from frigate.detectors.detector_config import ( -+ BaseDetectorConfig, -+) -+from frigate.object_detection.util import RequestStore, ResponseStore -+ -+logger = logging.getLogger(__name__) -+ -+ -+# ----------------- Utility Functions ----------------- # -+ -+ -+def preprocess_tensor(image: np.ndarray, model_w: int, model_h: int) -> np.ndarray: -+ """ -+ Resize an image with unchanged aspect ratio using padding. -+ Assumes input image shape is (H, W, 3). -+ """ -+ if image.ndim == 4 and image.shape[0] == 1: -+ image = image[0] -+ -+ h, w = image.shape[:2] -+ scale = min(model_w / w, model_h / h) -+ new_w, new_h = int(w * scale), int(h * scale) -+ resized_image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_CUBIC) -+ padded_image = np.full((model_h, model_w, 3), 114, dtype=image.dtype) -+ x_offset = (model_w - new_w) // 2 -+ y_offset = (model_h - new_h) // 2 -+ padded_image[y_offset : y_offset + new_h, x_offset : x_offset + new_w] = ( -+ resized_image -+ ) -+ return padded_image -+ -+ -+# ----------------- Global Constants ----------------- # -+DETECTOR_KEY = "hailo10h" -+ARCH = None -+H10H_DEFAULT_MODEL = "yolov6n.hef" -+H10H_DEFAULT_URL = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v5.2.0/hailo10h/yolov6n.hef" -+ -+ -+def detect_hailo_arch(): -+ try: -+ result = subprocess.run( -+ ["hailortcli", "fw-control", "identify"], capture_output=True, text=True -+ ) -+ if result.returncode != 0: -+ logger.error(f"Inference error: {result.stderr}") -+ return None -+ for line in result.stdout.split("\n"): -+ if "Device Architecture" in line: -+ if "HAILO10H" in line: -+ return "hailo10h" -+ logger.error("Inference error: Could not determine Hailo architecture.") -+ return None -+ except Exception as e: -+ logger.error(f"Inference error: {e}") -+ return None -+ -+ -+# ----------------- HailoAsyncInference Class ----------------- # -+class HailoAsyncInference: -+ def __init__( -+ self, -+ hef_path: str, -+ input_store: RequestStore, -+ output_store: ResponseStore, -+ batch_size: int = 1, -+ input_type: Optional[str] = None, -+ output_type: Optional[Dict[str, str]] = None, -+ send_original_frame: bool = False, -+ ) -> None: -+ # when importing hailo it activates the driver -+ # which leaves processes running even though it may not be used. -+ try: -+ from hailo_platform import ( -+ HEF, -+ FormatType, -+ HailoSchedulingAlgorithm, -+ VDevice, -+ ) -+ except ModuleNotFoundError: -+ pass -+ -+ self.input_store = input_store -+ self.output_store = output_store -+ -+ params = VDevice.create_params() -+ params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN -+ -+ self.hef = HEF(hef_path) -+ self.target = VDevice(params) -+ self.infer_model = self.target.create_infer_model(hef_path) -+ self.infer_model.set_batch_size(batch_size) -+ -+ if input_type is not None: -+ self.infer_model.input().set_format_type(getattr(FormatType, input_type)) -+ -+ if output_type is not None: -+ for output_name, output_type in output_type.items(): -+ self.infer_model.output(output_name).set_format_type( -+ getattr(FormatType, output_type) -+ ) -+ -+ self.output_type = output_type -+ self.send_original_frame = send_original_frame -+ -+ def callback( -+ self, -+ completion_info, -+ bindings_list: List, -+ input_batch: List, -+ request_ids: List[int], -+ ): -+ if completion_info.exception: -+ logger.error(f"Inference error: {completion_info.exception}") -+ else: -+ for i, bindings in enumerate(bindings_list): -+ if len(bindings._output_names) == 1: -+ result = bindings.output().get_buffer() -+ else: -+ result = { -+ name: np.expand_dims(bindings.output(name).get_buffer(), axis=0) -+ for name in bindings._output_names -+ } -+ self.output_store.put(request_ids[i], (input_batch[i], result)) -+ -+ def _create_bindings(self, configured_infer_model) -> object: -+ if self.output_type is None: -+ output_buffers = { -+ output_info.name: np.empty( -+ self.infer_model.output(output_info.name).shape, -+ dtype=getattr( -+ np, str(output_info.format.type).split(".")[1].lower() -+ ), -+ ) -+ for output_info in self.hef.get_output_vstream_infos() -+ } -+ else: -+ output_buffers = { -+ name: np.empty( -+ self.infer_model.output(name).shape, -+ dtype=getattr(np, self.output_type[name].lower()), -+ ) -+ for name in self.output_type -+ } -+ return configured_infer_model.create_bindings(output_buffers=output_buffers) -+ -+ def get_input_shape(self) -> Tuple[int, ...]: -+ return self.hef.get_input_vstream_infos()[0].shape -+ -+ def run(self) -> None: -+ job = None -+ with self.infer_model.configure() as configured_infer_model: -+ while True: -+ batch_data = self.input_store.get() -+ -+ if batch_data is None: -+ break -+ -+ request_id, frame_data = batch_data -+ preprocessed_batch = [frame_data] -+ request_ids = [request_id] -+ input_batch = preprocessed_batch # non-send_original_frame mode -+ -+ bindings_list = [] -+ for frame in preprocessed_batch: -+ bindings = self._create_bindings(configured_infer_model) -+ bindings.input().set_buffer(np.array(frame)) -+ bindings_list.append(bindings) -+ configured_infer_model.wait_for_async_ready(timeout_ms=10000) -+ job = configured_infer_model.run_async( -+ bindings_list, -+ partial( -+ self.callback, -+ input_batch=input_batch, -+ request_ids=request_ids, -+ bindings_list=bindings_list, -+ ), -+ ) -+ -+ if job is not None: -+ job.wait(100) -+ -+ -+# ----------------- HailoDetector Class ----------------- # -+class HailoDetector(DetectionApi): -+ type_key = DETECTOR_KEY -+ -+ def __init__(self, detector_config: "HailoDetectorConfig"): -+ global ARCH -+ ARCH = detect_hailo_arch() -+ self.cache_dir = MODEL_CACHE_DIR -+ self.device_type = detector_config.device -+ self.model_height = ( -+ detector_config.model.height -+ if hasattr(detector_config.model, "height") -+ else None -+ ) -+ self.model_width = ( -+ detector_config.model.width -+ if hasattr(detector_config.model, "width") -+ else None -+ ) -+ self.model_type = ( -+ detector_config.model.model_type -+ if hasattr(detector_config.model, "model_type") -+ else None -+ ) -+ self.tensor_format = ( -+ detector_config.model.input_tensor -+ if hasattr(detector_config.model, "input_tensor") -+ else None -+ ) -+ self.pixel_format = ( -+ detector_config.model.input_pixel_format -+ if hasattr(detector_config.model, "input_pixel_format") -+ else None -+ ) -+ self.input_dtype = ( -+ detector_config.model.input_dtype -+ if hasattr(detector_config.model, "input_dtype") -+ else None -+ ) -+ self.output_type = "FLOAT32" -+ self.set_path_and_url(detector_config.model.path) -+ self.working_model_path = self.check_and_prepare() -+ -+ self.batch_size = 1 -+ self.input_store = RequestStore() -+ self.response_store = ResponseStore() -+ -+ try: -+ logger.debug(f"[INIT] Loading HEF model from {self.working_model_path}") -+ self.inference_engine = HailoAsyncInference( -+ self.working_model_path, -+ self.input_store, -+ self.response_store, -+ self.batch_size, -+ ) -+ self.input_shape = self.inference_engine.get_input_shape() -+ logger.debug(f"[INIT] Model input shape: {self.input_shape}") -+ self.inference_thread = threading.Thread( -+ target=self.inference_engine.run, daemon=True -+ ) -+ self.inference_thread.start() -+ except Exception as e: -+ logger.error(f"[INIT] Failed to initialize HailoAsyncInference: {e}") -+ raise -+ -+ def set_path_and_url(self, path: str = None): -+ if not path: -+ self.model_path = None -+ self.url = None -+ return -+ if self.is_url(path): -+ self.url = path -+ self.model_path = None -+ else: -+ self.model_path = path -+ self.url = None -+ -+ def is_url(self, url: str) -> bool: -+ return ( -+ url.startswith("http://") -+ or url.startswith("https://") -+ or url.startswith("www.") -+ ) -+ -+ @staticmethod -+ def extract_model_name(path: str = None, url: str = None) -> str: -+ if path and path.endswith(".hef"): -+ return os.path.basename(path) -+ elif url and url.endswith(".hef"): -+ return os.path.basename(url) -+ else: -+ return H10H_DEFAULT_MODEL -+ -+ @staticmethod -+ def download_model(url: str, destination: str): -+ if not url.endswith(".hef"): -+ raise ValueError("Invalid model URL. Only .hef files are supported.") -+ try: -+ urllib.request.urlretrieve(url, destination) -+ logger.debug(f"Downloaded model to {destination}") -+ except Exception as e: -+ raise RuntimeError(f"Failed to download model from {url}: {str(e)}") -+ -+ def check_and_prepare(self) -> str: -+ if not os.path.exists(self.cache_dir): -+ os.makedirs(self.cache_dir) -+ model_name = self.extract_model_name(self.model_path, self.url) -+ cached_model_path = os.path.join(self.cache_dir, model_name) -+ if not self.model_path and not self.url: -+ if os.path.exists(cached_model_path): -+ logger.debug(f"Model found in cache: {cached_model_path}") -+ return cached_model_path -+ else: -+ logger.debug(f"Downloading default model: {model_name}") -+ self.download_model(H10H_DEFAULT_URL, cached_model_path) -+ -+ elif self.url: -+ logger.debug(f"Downloading model from URL: {self.url}") -+ self.download_model(self.url, cached_model_path) -+ elif self.model_path: -+ if os.path.exists(self.model_path): -+ logger.debug(f"Using existing model at: {self.model_path}") -+ return self.model_path -+ else: -+ raise FileNotFoundError(f"Model file not found at: {self.model_path}") -+ return cached_model_path -+ -+ def detect_raw(self, tensor_input): -+ tensor_input = self.preprocess(tensor_input) -+ -+ if isinstance(tensor_input, np.ndarray) and len(tensor_input.shape) == 3: -+ tensor_input = np.expand_dims(tensor_input, axis=0) -+ -+ request_id = self.input_store.put(tensor_input) -+ -+ try: -+ _, infer_results = self.response_store.get(request_id, timeout=1.0) -+ except TimeoutError: -+ logger.error( -+ f"Timeout waiting for inference results for request {request_id}" -+ ) -+ -+ if not self.inference_thread.is_alive(): -+ raise RuntimeError( -+ "HailoRT inference thread has stopped, restart required." -+ ) -+ -+ return np.zeros((20, 6), dtype=np.float32) -+ -+ if isinstance(infer_results, list) and len(infer_results) == 1: -+ infer_results = infer_results[0] -+ -+ threshold = 0.4 -+ all_detections = [] -+ for class_id, detection_set in enumerate(infer_results): -+ if not isinstance(detection_set, np.ndarray) or detection_set.size == 0: -+ continue -+ for det in detection_set: -+ if det.shape[0] < 5: -+ continue -+ score = float(det[4]) -+ if score < threshold: -+ continue -+ all_detections.append([class_id, score, det[0], det[1], det[2], det[3]]) -+ -+ if len(all_detections) == 0: -+ detections_array = np.zeros((20, 6), dtype=np.float32) -+ else: -+ detections_array = np.array(all_detections, dtype=np.float32) -+ if detections_array.shape[0] > 20: -+ detections_array = detections_array[:20, :] -+ elif detections_array.shape[0] < 20: -+ pad = np.zeros((20 - detections_array.shape[0], 6), dtype=np.float32) -+ detections_array = np.vstack((detections_array, pad)) -+ -+ return detections_array -+ -+ def preprocess(self, image): -+ if isinstance(image, np.ndarray): -+ processed = preprocess_tensor( -+ image, self.input_shape[1], self.input_shape[0] -+ ) -+ return np.expand_dims(processed, axis=0) -+ else: -+ raise ValueError("Unsupported image format for preprocessing") -+ -+ def close(self): -+ """Properly shuts down the inference engine and releases the VDevice.""" -+ logger.debug("[CLOSE] Closing HailoDetector") -+ try: -+ if hasattr(self, "inference_engine"): -+ if hasattr(self.inference_engine, "target"): -+ self.inference_engine.target.release() -+ logger.debug("Hailo VDevice released successfully") -+ except Exception as e: -+ logger.error(f"Failed to close Hailo device: {e}") -+ raise -+ -+ def __del__(self): -+ """Destructor to ensure cleanup when the object is deleted.""" -+ self.close() -+ -+ -+# ----------------- HailoDetectorConfig Class ----------------- # -+class HailoDetectorConfig(BaseDetectorConfig): -+ """Hailo-8/Hailo-8L detector using HEF models and the HailoRT SDK for inference on Hailo hardware.""" -+ -+ model_config = ConfigDict( -+ title="Hailo-8/Hailo-8L", -+ ) -+ -+ type: Literal[DETECTOR_KEY] -+ device: str = Field( -+ default="PCIe", -+ title="Device Type", -+ description="The device to use for Hailo inference (e.g. 'PCIe', 'M.2').", -+ )