* Add camera name tooltip to previews in recording view

* Apply face area check to cv2 face detection

* Delete review thumbnails

* Don't import hailo until it is used

* Add comment

* Clean up camera name

* Filter out empty keys when updating yaml config

HA ingress seems to randomly add an equal sign to the PUT urls for updating the config from the UI. This fix prevents empty keys from being processed, but still allows empty values.

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins 2025-05-13 09:27:07 -05:00 committed by GitHub
parent f39ddbc00d
commit 2c9bfaa49c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 72 additions and 50 deletions

View File

@ -221,6 +221,13 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
max(0, face_box[0]) : min(frame.shape[1], face_box[2]),
]
# check that face is correct size
if area(face_box) < self.config.cameras[camera].face_recognition.min_area:
logger.debug(
f"Detected face that is smaller than the min_area {face} < {self.config.cameras[camera].face_recognition.min_area}"
)
return
try:
face_frame = cv2.cvtColor(face_frame, cv2.COLOR_RGB2BGR)
except Exception:

View File

@ -8,17 +8,6 @@ from typing import Dict, List, Optional, Tuple
import cv2
import numpy as np
try:
from hailo_platform import (
HEF,
FormatType,
HailoSchedulingAlgorithm,
VDevice,
)
except ModuleNotFoundError:
pass
from pydantic import Field
from typing_extensions import Literal
@ -102,6 +91,18 @@ class HailoAsyncInference:
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
@ -112,24 +113,19 @@ class HailoAsyncInference:
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._set_input_type(input_type)
self.infer_model.input().set_format_type(getattr(FormatType, input_type))
if output_type is not None:
self._set_output_type(output_type)
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 _set_input_type(self, input_type: Optional[str] = None) -> None:
self.infer_model.input().set_format_type(getattr(FormatType, input_type))
def _set_output_type(
self, output_type_dict: Optional[Dict[str, str]] = None
) -> None:
for output_name, output_type in output_type_dict.items():
self.infer_model.output(output_name).set_format_type(
getattr(FormatType, output_type)
)
def callback(
self,
completion_info,

View File

@ -69,7 +69,7 @@ class RecordingCleanup(threading.Thread):
now - datetime.timedelta(days=config.record.detections.retain.days)
).timestamp()
expired_reviews: ReviewSegment = (
ReviewSegment.select(ReviewSegment.id)
ReviewSegment.select(ReviewSegment.id, ReviewSegment.thumb_path)
.where(ReviewSegment.camera == config.name)
.where(
(
@ -84,6 +84,10 @@ class RecordingCleanup(threading.Thread):
.namedtuples()
)
thumbs_to_delete = list(map(lambda x: x[1], expired_reviews))
for thumb_path in thumbs_to_delete:
Path(thumb_path).unlink(missing_ok=True)
max_deletes = 100000
deleted_reviews_list = list(map(lambda x: x[0], expired_reviews))
for i in range(0, len(deleted_reviews_list), max_deletes):

View File

@ -187,6 +187,9 @@ def update_yaml_from_url(file_path, url):
parsed_url = urllib.parse.urlparse(url)
query_string = urllib.parse.parse_qs(parsed_url.query, keep_blank_values=True)
# Filter out empty keys but keep blank values for non-empty keys
query_string = {k: v for k, v in query_string.items() if k}
for key_path_str, new_value_list in query_string.items():
key_path = key_path_str.split(".")
for i in range(len(key_path)):

View File

@ -50,6 +50,11 @@ import { useFullscreen } from "@/hooks/use-fullscreen";
import { useTimezone } from "@/hooks/use-date-utils";
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
import { useTranslation } from "react-i18next";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
type RecordingViewProps = {
startCamera: string;
@ -670,31 +675,38 @@ export function RecordingView({
}
return (
<div
key={cam}
className={
mainCameraAspect == "tall" ? "w-full" : "h-full"
}
style={{
aspectRatio: getCameraAspect(cam),
}}
>
<PreviewPlayer
previewRef={previewRef}
className="size-full"
camera={cam}
timeRange={currentTimeRange}
cameraPreviews={allPreviews ?? []}
startTime={startTime}
isScrubbing={scrubbing}
isVisible={visiblePreviews.includes(cam)}
onControllerReady={(controller) => {
previewRefs.current[cam] = controller;
controller.scrubToTimestamp(startTime);
}}
onClick={() => onSelectCamera(cam)}
/>
</div>
<Tooltip>
<TooltipTrigger asChild>
<div
key={cam}
className={
mainCameraAspect == "tall" ? "w-full" : "h-full"
}
style={{
aspectRatio: getCameraAspect(cam),
}}
>
<PreviewPlayer
previewRef={previewRef}
className="size-full"
camera={cam}
timeRange={currentTimeRange}
cameraPreviews={allPreviews ?? []}
startTime={startTime}
isScrubbing={scrubbing}
isVisible={visiblePreviews.includes(cam)}
onControllerReady={(controller) => {
previewRefs.current[cam] = controller;
controller.scrubToTimestamp(startTime);
}}
onClick={() => onSelectCamera(cam)}
/>
</div>
</TooltipTrigger>
<TooltipContent className="smart-capitalize">
{cam.replaceAll("_", " ")}
</TooltipContent>
</Tooltip>
);
})}
<div className="w-2" />