Adding fixes

This commit is contained in:
OmriAx 2025-02-26 14:04:27 +02:00
parent 11177292de
commit ee45f50e09
4 changed files with 612 additions and 180 deletions

View File

@ -11,7 +11,7 @@ Frigate supports multiple different detectors that work on different types of ha
**Most Hardware**
- [Coral EdgeTPU](#edge-tpu-detector): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices.
- [Hailo](#hailo-8l): The Hailo8 AI Acceleration module is available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices.
- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices.
**AMD**
- [ROCm](#amdrocm-gpu-detector): ROCm can run on AMD Discrete GPUs to provide efficient object detection.
@ -124,14 +124,36 @@ detectors:
device: pci
```
## Hailo-8l
## Hailo-8
This detector is available for use with Hailo-8 AI Acceleration Module.
This detector is available for use with Hailo-8 and Hailo-8L AI Acceleration Module.
See the [installation docs](../frigate/installation.md#hailo-8l) for information on configuring the hailo8.
### Configuration
#### YOLO (Recommended)
```yaml
detectors:
hailo8l:
type: hailo8l
device: PCIe
model:
width: 640
height: 640
input_tensor: nhwc
input_pixel_format: rgb
input_dtype: int
model_type: hailoyolo
# The detector will automatically use the appropriate model:
# - YOLOv8s for Hailo-8L hardware
# - YOLOv8m for Hailo-8 hardware
```
#### SSD
```yaml
detectors:
hailo8l:
@ -148,6 +170,33 @@ model:
```
### Custom Models
The Hailo-8l detector supports all YOLO models that have been compiled for the Hailo hardware and include post-processing. The detector automatically detects your hardware type (Hailo-8 or Hailo-8L) and uses the appropriate model.
#### Using a Custom URL
You can specify a custom URL to download a model directly:
```yaml
detectors:
hailo8l:
type: hailo8l
device: PCIe
url: https://custom-model-url.com/path/to/model.hef
model:
width: 640
height: 640
input_tensor: nhwc
input_pixel_format: rgb
input_dtype: int
model_type: hailoyolo
```
The detector will automatically handle different output formats from all supported YOLO variants. It's important to match the `model_type` with the actual model architecture for proper processing.
* Tsted custom models : yolov5 , yolov8 , yolov9 , yolov11
## OpenVINO Detector
The OpenVINO detector type runs an OpenVINO IR model on AMD and Intel CPUs, Intel GPUs and Intel VPU hardware. To configure an OpenVINO detector, set the `"type"` attribute to `"openvino"`.

View File

@ -92,11 +92,37 @@ Inference speeds will vary greatly depending on the GPU and the model used.
With the [rocm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs.
### Hailo-8l PCIe
### Hailo-8
Frigate supports the Hailo-8l M.2 card on any hardware but currently it is only tested on the Raspberry Pi5 PCIe hat from the AI kit.
Frigate supports the Hailo8 and Hailo-8L AI Acceleration Module on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo accelerator provides dedicated hardware for efficiently running neural network inference.
The inference time for the Hailo-8L chip at time of writing is around 17-21 ms for the SSD MobileNet Version 1 model.
The inference time for the Hailo-8L chip is around 17-21 ms for the SSD MobileNet Version 1 model and 15-18 ms for YOLOv8s models. For the more powerful Hailo-8 chip, the YOLOv8m model has an inference time of approximately 12-15 ms.
In real-world testing with 8 cameras running simultaneously, each camera maintained a detection rate of approximately 20-25 FPS, demonstrating the Hailo accelerator's capability to handle multiple video streams efficiently.
Testing on x86 platforms has also been conducted with excellent results. The x86 implementation benefits from having two PCIe lanes available instead of one, resulting in improved FPS , throughput and lower latency compared to the Raspberry Pi setup.
#### Supported Models
The Hailo-8L detector supports all YOLO variants that have been compiled for Hailo hardware with post-processing, including:
- YOLOv5
- YOLOv8
- any Yolo variant with HailoRT Post-process
- SSD mobilnet v1
| Model Type | Hardware | Inference Time | Resolution |
|------------|----------|----------------|------------|
| SSD MobileNet V1 | Hailo-8L (RPi) | 17-21 ms | 300×300 |
| SSD MobileNet V1 | Hailo-8L (x86) | 12-15 ms | 300×300 |
| SSD MobileNet V1 | Hailo-8 (rpi) | 13-16 ms | 300×300 |
| YOLOv8s | Hailo-8L (RPi) | 15-18 ms | 640×640 |
| YOLOv8s | Hailo-8L (x86) | 10-13 ms | 640×640 |
| YOLOv8m | Hailo-8 (RPi) | 12-15 ms | 640×640 |
| YOLOv8m | Hailo-8 (x86) | 8-11 ms | 640×640 |
The detector automatically detects your hardware type (Hailo-8 or Hailo-8L) and downloads the appropriate model. The Hailo detector optimizes inference by maintaining a persistent pipeline between detection calls, reducing overhead and providing fast, consistent performance with multiple cameras.
## Community Supported Detectors

View File

@ -100,9 +100,9 @@ By default, the Raspberry Pi limits the amount of memory available to the GPU. I
Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with <a href="https://amzn.to/3a2mH0P" target="_blank" rel="nofollow noopener sponsored">this</a> (affiliate link).
### Hailo-8L
### Hailo-8
The Hailo-8L is an M.2 card typically connected to a carrier board for PCIe, which then connects to the Raspberry Pi 5 as part of the AI Kit. However, it can also be used on other boards equipped with an M.2 M key edge connector.
The Hailo-8 and Hailo-8L AI accelerators are available in both M.2 and HAT form factors for the Raspberry Pi. The M.2 version typically connects to a carrier board for PCIe, which then interfaces with the Raspberry Pi 5 as part of the AI Kit. The HAT version can be mounted directly onto compatible Raspberry Pi models. Both form factors have been successfully tested on x86 platforms as well, making them versatile options for various computing environments.
#### Installation

View File

@ -1,204 +1,561 @@
import logging
import os
import queue
import threading
import subprocess
import urllib.request
import numpy as np
from hailo_platform import (
HEF,
ConfigureParams,
FormatType,
HailoRTException,
HailoStreamInterface,
VDevice,
HailoSchedulingAlgorithm,
InferVStreams,
InputVStreamParams,
OutputVStreamParams
)
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
try:
from hailo_platform import (
HEF,
ConfigureParams,
FormatType,
HailoRTException,
HailoStreamInterface,
InferVStreams,
InputVStreamParams,
OutputVStreamParams,
VDevice,
)
except ModuleNotFoundError:
pass
from pydantic import BaseModel, Field
from typing_extensions import Literal
from typing import Optional
from functools import partial
from typing import Dict, Optional, List
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum, InputTensorEnum, PixelFormatEnum, InputDTypeEnum
# Setup logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
file_handler = logging.FileHandler('hailo_detector_debug.log')
file_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Define the detector key for Hailo
DETECTOR_KEY = "hailo8l"
ARCH = None
class ModelConfig(BaseModel):
path: str = Field(default=None, title="Model Path")
def detect_hailo_arch():
try:
# Run the hailortcli command to get device information
result = subprocess.run(['hailortcli', 'fw-control', 'identify'], capture_output=True, text=True)
# Check if the command was successful
if result.returncode != 0:
print(f"Error running hailortcli: {result.stderr}")
return None
# Search for the "Device Architecture" line in the output
for line in result.stdout.split('\n'):
if "Device Architecture" in line:
if "HAILO8L" in line:
return "hailo8l"
elif "HAILO8" in line:
return "hailo8"
print("Could not determine Hailo architecture from device information.")
return None
except Exception as e:
print(f"An error occurred while detecting Hailo architecture: {e}")
return None
# Configuration class for Hailo detector
class HailoDetectorConfig(BaseDetectorConfig):
type: Literal[DETECTOR_KEY]
device: str = Field(default="PCIe", title="Device Type")
url: Optional[str] = Field(default=None, title="Model URL")
dir: Optional[str] = Field(default=None, title="Model Directory")
model: ModelConfig
class HailoAsyncInference:
def __init__(self, config: HailoDetectorConfig):
self.config = config
self.input_queue = queue.Queue()
self.output_queue = queue.Queue()
params = VDevice.create_params()
params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN
self.target = VDevice(params)
# Initialize HEF
self.hef = HEF(self.config.model.path)
self.infer_model = self.target.create_infer_model(self.config.model.path)
self.infer_model.set_batch_size(1)
def infer(self):
while True:
batch_data = self.input_queue.get()
if batch_data is None:
break
with self.infer_model.configure() as configured_model:
bindings_list = []
for frame in batch_data:
# Create empty output buffers
output_buffers = {
output_info.name: np.empty(
self.infer_model.output(output_info.name).shape,
dtype=np.float32
)
for output_info in self.hef.get_output_vstream_infos()
}
# Create bindings using the configured model
binding = configured_model.create_bindings(output_buffers=output_buffers)
binding.input().set_buffer(frame)
bindings_list.append(binding)
# Run async inference on the configured model
configured_model.run_async(bindings_list, partial(self._callback, batch_data=batch_data))
def _callback(self, completion_info, bindings_list, batch_data):
if completion_info.exception:
logger.error(f"Inference error: {completion_info.exception}")
else:
results = [binding.output().get_buffer() for binding in bindings_list]
self.output_queue.put((batch_data, results))
def stop(self):
self.input_queue.put(None)
type: Literal[DETECTOR_KEY] # Type of the detector
device: str = Field(default="PCIe", title="Device Type") # Device type (e.g., PCIe)
url: Optional[str] = Field(default=None, title="Custom Model URL")
# Hailo detector class implementation
class HailoDetector(DetectionApi):
type_key = DETECTOR_KEY
DEFAULT_CACHE_DIR = "/config/model_cache/"
type_key = DETECTOR_KEY # Set the type key to the Hailo detector key
def __init__(self, detector_config: HailoDetectorConfig):
super().__init__(detector_config)
self.config = detector_config
# Get the model path
model_path = self.check_and_prepare_model()
self.config.model.path = model_path
print(self.config.model.path)
# Initialize async inference with the correct model path
self.async_inference = HailoAsyncInference(detector_config)
self.worker_thread = threading.Thread(target=self.async_inference.infer)
self.worker_thread.start()
print(f"[INIT] Starting HailoDetector initialization with config: {detector_config}")
logger.info(f"[INIT] Starting HailoDetector initialization with config: {detector_config}")
# Set global ARCH variable
global ARCH
ARCH = detect_hailo_arch()
logger.info(f"[INIT] Detected Hailo architecture: {ARCH}")
def check_and_prepare_model(self) -> str:
"""
Check if model exists at specified path, download from URL if needed.
Returns the final model path to use.
"""
# Ensure cache directory exists
if not os.path.exists(self.DEFAULT_CACHE_DIR):
os.makedirs(self.DEFAULT_CACHE_DIR)
supported_models = [
ModelTypeEnum.ssd,
ModelTypeEnum.yolov9,
ModelTypeEnum.hailoyolo,
]
model_path = self.config.dir # the directory path of the model
model_url = self.config.url # the url of the model
# Initialize device type and model path from the configuration
self.h8l_device_type = detector_config.device
self.h8l_model_path = detector_config.model.path
self.h8l_model_type = detector_config.model.model_type
if (model_path and os.path.isfile(model_path)):
return model_path
if (model_url):
model_filename = os.path.basename(model_url)
model_file_path = os.path.join(self.DEFAULT_CACHE_DIR, model_filename)
if os.path.isfile(model_file_path):
return model_file_path
else:
logger.info(f"Downloading model from URL: {model_url}")
try:
urllib.request.urlretrieve(model_url, model_file_path)
logger.info(f"Model downloaded successfully to: {model_file_path}")
return model_file_path
except Exception as e:
logger.error(f"Failed to download model: {str(e)}")
raise RuntimeError(f"Failed to download model from {model_url}")
raise RuntimeError("No valid model path or URL provided")
# Set configuration based on model type
self.set_correct_config(self.h8l_model_type)
# Override with custom URL if provided
if hasattr(detector_config, "url") and detector_config.url:
self.model_url = detector_config.url
self.expected_model_filename = self.model_url.split('/')[-1]
self.check_and_prepare_model()
try:
# Validate device type
if self.h8l_device_type not in ["PCIe", "M.2"]:
raise ValueError(f"Unsupported device type: {self.h8l_device_type}")
# Initialize the Hailo device
logger.info("[INIT] Creating VDevice instance")
self.target = VDevice()
# Load the HEF (Hailo's binary format for neural networks)
logger.info(f"[INIT] Loading HEF from {self.h8l_model_path}")
self.hef = HEF(self.h8l_model_path)
# Create configuration parameters from the HEF
logger.info("[INIT] Creating configuration parameters")
self.configure_params = ConfigureParams.create_from_hef(
hef=self.hef, interface=HailoStreamInterface.PCIe
)
# Configure the device with the HEF
logger.info("[INIT] Configuring device with HEF")
self.network_groups = self.target.configure(self.hef, self.configure_params)
self.network_group = self.network_groups[0]
self.network_group_params = self.network_group.create_params()
# Create input and output virtual stream parameters
logger.info("[INIT] Creating input/output stream parameters")
self.input_vstream_params = InputVStreamParams.make(
self.network_group,
format_type=self.hef.get_input_vstream_infos()[0].format.type,
)
self.output_vstream_params = OutputVStreamParams.make(
self.network_group, format_type=getattr(FormatType, self.output_type)
)
# Get input and output stream information from the HEF
self.input_vstream_info = self.hef.get_input_vstream_infos()
self.output_vstream_info = self.hef.get_output_vstream_infos()
for i, info in enumerate(self.input_vstream_info):
logger.info(f"[INIT] Input Stream {i}: Name={info.name}, Format={info.format}, Shape={info.shape}")
for i, info in enumerate(self.output_vstream_info):
logger.info(f"[INIT] Output Stream {i}: Name={info.name}, Format={info.format}, Shape={info.shape}")
logger.info("Hailo device initialized successfully")
except HailoRTException as e:
logger.error(f"HailoRTException during initialization: {e}")
raise
except Exception as e:
logger.error(f"Failed to initialize Hailo device: {e}")
raise
def set_correct_config(self, modelname):
if modelname == ModelTypeEnum.ssd:
self.h8l_model_height = 300
self.h8l_model_width = 300
self.h8l_tensor_format = InputTensorEnum.nhwc
self.h8l_pixel_format = PixelFormatEnum.rgb
self.h8l_input_dtype = InputDTypeEnum.float
self.cache_dir = "/config/model_cache/h8l_cache"
self.expected_model_filename = "ssd_mobilenet_v1.hef"
self.output_type = "FLOAT32"
if ARCH == "hailo8":
self.model_url = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/ssd_mobilenet_v1.hef"
else:
self.model_url = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef"
else:
self.h8l_model_height = 640
self.h8l_model_width = 640
self.h8l_tensor_format = InputTensorEnum.nhwc
self.h8l_pixel_format = PixelFormatEnum.rgb # Default to RGB
self.h8l_input_dtype = InputDTypeEnum.int
self.cache_dir = "/config/model_cache/h8l_cache"
self.output_type = "FLOAT32"
if ARCH == "hailo8":
self.model_url = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov8m.hef"
self.expected_model_filename = "yolov8m.hef"
else:
self.model_url = "https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/yolov8s.hef"
self.expected_model_filename = "yolov8s.hef"
def check_and_prepare_model(self):
logger.info(f"[CHECK_MODEL] Checking for model at {self.cache_dir}/{self.expected_model_filename}")
# Ensure cache directory exists
if not os.path.exists(self.cache_dir):
logger.info(f"[CHECK_MODEL] Creating cache directory: {self.cache_dir}")
os.makedirs(self.cache_dir)
# Check for the expected model file
model_file_path = os.path.join(self.cache_dir, self.expected_model_filename)
if not os.path.isfile(model_file_path):
logger.info(f"[CHECK_MODEL] Model not found at {model_file_path}, downloading from {self.model_url}")
urllib.request.urlretrieve(self.model_url, model_file_path)
logger.info(f"[CHECK_MODEL] Model downloaded to {model_file_path}")
else:
logger.info(f"[CHECK_MODEL] Model already exists at {model_file_path}")
self.h8l_model_path = model_file_path
def detect_raw(self, tensor_input):
"""
Perform inference and return raw detection results.
"""
preprocessed_input = self.preprocess(tensor_input)
self.async_inference.input_queue.put([preprocessed_input])
logger.info("[DETECT_RAW] Starting detection")
if tensor_input is None:
error_msg = "[DETECT_RAW] The 'tensor_input' argument must be provided"
logger.error(error_msg)
raise ValueError(error_msg)
# Log input tensor information
logger.info(f"[DETECT_RAW] Input tensor type: {type(tensor_input)}")
if isinstance(tensor_input, np.ndarray):
logger.info(f"[DETECT_RAW] Input tensor shape: {tensor_input.shape}")
logger.info(f"[DETECT_RAW] Input tensor dtype: {tensor_input.dtype}")
logger.info(f"[DETECT_RAW] Input tensor min value: {np.min(tensor_input)}")
logger.info(f"[DETECT_RAW] Input tensor max value: {np.max(tensor_input)}")
logger.info(f"[DETECT_RAW] Input tensor mean value: {np.mean(tensor_input)}")
# Print sample of the tensor (first few elements)
flat_sample = tensor_input.flatten()[:10]
logger.info(f"[DETECT_RAW] Input tensor sample: {flat_sample}")
elif isinstance(tensor_input, list):
logger.info(f"[DETECT_RAW] Input is a list with length: {len(tensor_input)}")
tensor_input = np.array(tensor_input)
logger.info(f"[DETECT_RAW] Converted to array with shape: {tensor_input.shape}, dtype: {tensor_input.dtype}")
elif isinstance(tensor_input, dict):
logger.info(f"[DETECT_RAW] Input is a dictionary with keys: {tensor_input.keys()}")
input_data = tensor_input
logger.debug("[DETECT_RAW] Input data prepared for inference")
try:
batch_data, raw_results = self.async_inference.output_queue.get(timeout=5)
return self.postprocess(raw_results)
except queue.Empty:
logger.warning("Inference timed out")
logger.info("[DETECT_RAW] Creating inference pipeline")
with InferVStreams(
self.network_group,
self.input_vstream_params,
self.output_vstream_params,
) as infer_pipeline:
input_dict = {}
if isinstance(input_data, dict):
logger.info("[DETECT_RAW] Input is already a dictionary, using as-is")
input_dict = input_data
elif isinstance(input_data, (list, tuple)):
logger.info("[DETECT_RAW] Converting list/tuple to dictionary for inference")
for idx, layer_info in enumerate(self.input_vstream_info):
input_dict[layer_info.name] = input_data[idx]
logger.info(f"[DETECT_RAW] Assigned data to input layer '{layer_info.name}'")
else:
if len(input_data.shape) == 3:
logger.info(f"[DETECT_RAW] Adding batch dimension to input with shape {input_data.shape}")
input_data = np.expand_dims(input_data, axis=0)
logger.info(f"[DETECT_RAW] New input shape after adding batch dimension: {input_data.shape}")
input_dict[self.input_vstream_info[0].name] = input_data
logger.info(f"[DETECT_RAW] Assigned data to input layer '{self.input_vstream_info[0].name}'")
logger.info(f"[DETECT_RAW] Final input dictionary keys: {list(input_dict.keys())}")
# Log details about each input layer
for key, value in input_dict.items():
if isinstance(value, np.ndarray):
logger.info(f"[DETECT_RAW] Layer '{key}' has shape: {value.shape}, dtype: {value.dtype}")
logger.info("[DETECT_RAW] Activating network group")
with self.network_group.activate(self.network_group_params):
logger.info("[DETECT_RAW] Running inference")
raw_output = infer_pipeline.infer(input_dict)
logger.info(f"[DETECT_RAW] Inference complete, output keys: {list(raw_output.keys())}")
# Log details about output structure for debugging
for key, value in raw_output.items():
logger.info(f"[DETECT_RAW] Output layer '{key}' details:")
debug_output_structure(value, prefix=" ")
# Process outputs based on model type
if self.h8l_model_type in [ModelTypeEnum.hailoyolo, ModelTypeEnum.yolov9, ModelTypeEnum.yolox, ModelTypeEnum.yolonas]:
logger.info(f"[DETECT_RAW] Processing YOLO-type output for model type: {self.h8l_model_type}")
detections = self.process_yolo_output(raw_output)
else:
# Default to SSD processing
logger.info(f"[DETECT_RAW] Processing SSD output for model type: {self.h8l_model_type}")
expected_output_name = self.output_vstream_info[0].name
if expected_output_name not in raw_output:
error_msg = f"[DETECT_RAW] Missing output stream {expected_output_name} in inference results"
logger.error(error_msg)
return np.zeros((20, 6), np.float32)
detections = self.process_ssd_output(raw_output[expected_output_name])
logger.info(f"[DETECT_RAW] Processed detections shape: {detections.shape}")
return detections
except HailoRTException as e:
logger.error(f"[DETECT_RAW] HailoRTException during inference: {e}")
return np.zeros((20, 6), np.float32)
def preprocess(self, frame):
input_shape = (self.async_inference.hef.get_input_vstream_infos()[0].shape)
resized_frame = np.resize(frame, input_shape)
return resized_frame / 255.0
def postprocess(self, raw_output):
model_type = self.async_inference.config.model.type
if model_type == "ssd_mobilenet_v1":
return self._process_ssd(raw_output)
elif model_type in ["yolov8s", "yolov8m", "yolov6n"]:
return self._process_yolo(raw_output, version=model_type[-1])
else:
logger.error(f"Unsupported model type: {model_type}")
return []
def _process_ssd(self, raw_output):
detections = []
for detection in raw_output[1]:
score = detection[4]
if score >= self.async_inference.config.model.score_threshold:
ymin, xmin, ymax, xmax = detection[:4]
detections.append({
"bounding_box": [xmin, ymin, xmax, ymax],
"score": score,
"class": int(detection[5])
})
except Exception as e:
logger.error(f"[DETECT_RAW] Exception during inference: {e}")
return np.zeros((20, 6), np.float32)
finally:
logger.debug("[DETECT_RAW] Exiting function")
def process_yolo_output(self, raw_output):
"""
Process YOLO outputs to match the expected Frigate detection format.
Returns detections in the format [class_id, score, ymin, xmin, ymax, xmax]
"""
logger.info("[PROCESS_YOLO] Processing YOLO output")
# Initialize empty array for our results - match TFLite format
detections = np.zeros((20, 6), np.float32)
try:
# Identify output layers for boxes, classes, and scores
boxes_layer = None
classes_layer = None
scores_layer = None
count_layer = None
# Try to identify layers by name pattern
for key in raw_output.keys():
key_lower = key.lower()
if any(box_term in key_lower for box_term in ['box', 'bbox', 'location']):
boxes_layer = key
elif any(class_term in key_lower for class_term in ['class', 'category', 'label']):
classes_layer = key
elif any(score_term in key_lower for score_term in ['score', 'confidence', 'prob']):
scores_layer = key
elif any(count_term in key_lower for count_term in ['count', 'num', 'detection_count']):
count_layer = key
logger.info(f"[PROCESS_YOLO] Identified layers - Boxes: {boxes_layer}, Classes: {classes_layer}, "
f"Scores: {scores_layer}, Count: {count_layer}")
# If we found all necessary layers
if boxes_layer and classes_layer and scores_layer:
# Extract data from the identified layers
boxes = raw_output[boxes_layer]
class_ids = raw_output[classes_layer]
scores = raw_output[scores_layer]
# If these are lists, extract the first element (batch)
if isinstance(boxes, list) and len(boxes) > 0:
boxes = boxes[0]
if isinstance(class_ids, list) and len(class_ids) > 0:
class_ids = class_ids[0]
if isinstance(scores, list) and len(scores) > 0:
scores = scores[0]
# Get detection count (if available)
count = 0
if count_layer:
count_val = raw_output[count_layer]
if isinstance(count_val, list) and len(count_val) > 0:
count_val = count_val[0]
count = int(count_val[0] if isinstance(count_val, np.ndarray) else count_val)
else:
# Use the length of scores as count
count = len(scores) if hasattr(scores, '__len__') else 0
# Process detections like in the example
for i in range(count):
if i >= 20: # Limit to 20 detections
break
if scores[i] < 0.4: # Use 0.4 threshold as in the example
continue
# Add detection in the format [class_id, score, ymin, xmin, ymax, xmax]
detections[i] = [
float(class_ids[i]),
float(scores[i]),
float(boxes[i][0]), # ymin
float(boxes[i][1]), # xmin
float(boxes[i][2]), # ymax
float(boxes[i][3]), # xmax
]
else:
# Fallback: Try to process output as a combined detection array
logger.info("[PROCESS_YOLO] Couldn't identify separate output layers, trying unified format")
# Look for a detection array in the output
detection_layer = None
for key, value in raw_output.items():
if isinstance(value, list) and len(value) > 0:
if isinstance(value[0], np.ndarray) and value[0].ndim >= 2:
detection_layer = key
break
if detection_layer:
# Get the detection array
detection_array = raw_output[detection_layer]
if isinstance(detection_array, list):
detection_array = detection_array[0] # First batch
# Process each detection
detection_count = 0
for i, detection in enumerate(detection_array):
if detection_count >= 20:
break
# Format depends on YOLO variant but typically includes:
# class_id, score, box coordinates (could be [x,y,w,h] or [xmin,ymin,xmax,ymax])
# Extract elements based on shape
if len(detection) >= 6: # Likely [class_id, score, xmin, ymin, xmax, ymax]
class_id = detection[0]
score = detection[1]
# Check if this is actually [x, y, w, h, conf, class_id]
if score > 1.0: # Score shouldn't be > 1, might be a coordinate
# Reorganize assuming [x, y, w, h, conf, class_id] format
x, y, w, h, confidence, *class_probs = detection
# Get class with highest probability
if len(class_probs) > 1:
class_id = np.argmax(class_probs)
score = confidence * class_probs[class_id]
else:
class_id = class_probs[0]
score = confidence
# Convert [x,y,w,h] to [ymin,xmin,ymax,xmax]
xmin = x - w/2
ymin = y - h/2
xmax = x + w/2
ymax = y + h/2
else:
# Use as is, but verify we have box coordinates
xmin, ymin, xmax, ymax = detection[2:6]
elif len(detection) >= 4: # Might be [class_id, score, xmin, ymin]
class_id = detection[0]
score = detection[1]
# For incomplete boxes, assume zeros
xmin, ymin = detection[2:4]
xmax = xmin + 0.1 # Small default size
ymax = ymin + 0.1
else:
# Skip invalid detections
continue
# Skip low confidence detections
if score < 0.4:
continue
# Add to detection array
detections[detection_count] = [
float(class_id),
float(score),
float(ymin),
float(xmin),
float(ymax),
float(xmax)
]
detection_count += 1
logger.info(f"[PROCESS_YOLO] Processed {np.count_nonzero(detections[:, 1] > 0)} valid detections")
except Exception as e:
logger.error(f"[PROCESS_YOLO] Error processing YOLO output: {e}")
# detections already initialized as zeros
return detections
def _process_yolo(self, raw_output, version):
detections = []
for detection in raw_output[1]:
confidence = detection[4]
if confidence >= self.async_inference.config.model.score_threshold:
x, y, w, h = detection[:4]
ymin, xmin, ymax, xmax = y - h / 2, x - w / 2, y + h / 2, x + w / 2
class_id = np.argmax(detection[5:])
detections.append({
"bounding_box": [xmin, ymin, xmax, ymax],
"score": confidence,
"class": class_id
})
return detections
def process_ssd_output(self, raw_output):
"""
Process SSD MobileNet v1 output with special handling for jagged arrays
"""
logger.info("[PROCESS_SSD] Processing SSD output")
def stop(self):
self.async_inference.stop()
self.worker_thread.join()
# Initialize empty lists for our results
all_detections = []
try:
if isinstance(raw_output, list) and len(raw_output) > 0:
# Handle first level of nesting
raw_detections = raw_output[0]
logger.debug(f"[PROCESS_SSD] First level output type: {type(raw_detections)}")
# Process all valid detections
for i, detection_group in enumerate(raw_detections):
# Skip empty arrays or invalid data
if not isinstance(detection_group, np.ndarray):
continue
# Skip empty arrays
if detection_group.size == 0:
continue
# For the arrays with actual detections
if detection_group.shape[0] > 0:
# Extract the detection data - typical format is (ymin, xmin, ymax, xmax, score)
for j in range(detection_group.shape[0]):
detection = detection_group[j]
# Check if we have 5 values (expected format)
if len(detection) == 5:
ymin, xmin, ymax, xmax, score = detection
class_id = i # Use index as class ID
# Add detection if score is reasonable
if 0 <= score <= 1.0 and score > 0.1: # Basic threshold
all_detections.append([float(class_id), float(score),
float(ymin), float(xmin),
float(ymax), float(xmax)])
# Convert to numpy array if we have valid detections
if all_detections:
detections_array = np.array(all_detections, dtype=np.float32)
# Sort by score (descending)
sorted_idx = np.argsort(detections_array[:, 1])[::-1]
detections_array = detections_array[sorted_idx]
# Take top 20 (or fewer if less available)
detections_array = detections_array[:20]
else:
detections_array = np.zeros((0, 6), dtype=np.float32)
except Exception as e:
logger.error(f"[PROCESS_SSD] Error processing SSD output: {e}")
detections_array = np.zeros((0, 6), dtype=np.float32)
# Pad to 20 detections if needed
if len(detections_array) < 20:
padding = np.zeros((20 - len(detections_array), 6), dtype=np.float32)
detections_array = np.vstack((detections_array, padding))
logger.info(f"[PROCESS_SSD] Final output shape: {detections_array.shape}")
return detections_array
def process_detections(self, raw_detections, threshold=0.5):
"""
Legacy detection processing method, kept for compatibility.
Now redirects to the more robust process_ssd_output method.
"""
logger.info("[PROCESS] Starting to process detections")
logger.info(f"[PROCESS] Using threshold: {threshold}")
# Wrap the raw_detections in a list to match expected format for process_ssd_output
if not isinstance(raw_detections, list):
raw_detections = [raw_detections]
# Process using the more robust method
return self.process_ssd_output(raw_detections)
def close(self):
logger.info("[CLOSE] Closing Hailo device")
try:
self.target.close()
logger.info("Hailo device closed successfully")
except Exception as e:
logger.error(f"Failed to close Hailo device: {e}")
raise