Merge d93f296691bba288e4e671c6c4cf13244d94d8d0 into d37dff1b491e752d7bf6b0ca2c7efbaedca623cd

This commit is contained in:
Mika 2026-08-29 12:27:52 -07:00 committed by GitHub
commit 32035f8a2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 17 deletions

View File

@ -86,8 +86,9 @@ class ImprovedMotionDetector(MotionDetector):
# Improve contrast
if self.config.improve_contrast:
# TODO tracking moving average of min/max to avoid sudden contrast changes
min_value = np.percentile(resized_frame, 4).astype(np.uint8)
max_value = np.percentile(resized_frame, 96).astype(np.uint8)
min_value, max_value = np.percentile(resized_frame, [4, 96]).astype(
np.uint8
)
# skip contrast calcs if the image is a single color
if min_value < max_value:
# keep track of the last 50 contrast values
@ -101,10 +102,17 @@ class ImprovedMotionDetector(MotionDetector):
avg_min, avg_max = np.mean(self.contrast_values, axis=0)
resized_frame = np.clip(resized_frame, avg_min, avg_max)
resized_frame = (
((resized_frame - avg_min) / (avg_max - avg_min)) * 255
# Apply the clip-and-rescale as a 256-entry uint8 lookup table
# instead of promoting the whole frame to float64 (which
# allocated several full-frame temporaries per frame). The
# per-pixel result is identical since the input is uint8.
contrast_lut = np.clip(
np.arange(256, dtype=np.float64), avg_min, avg_max
)
contrast_lut = (
((contrast_lut - avg_min) / (avg_max - avg_min)) * 255
).astype(np.uint8)
resized_frame = cv2.LUT(resized_frame, contrast_lut)
if self.save_images:
contrasted_saved = resized_frame.copy()

View File

@ -25,7 +25,7 @@ from frigate.track.stationary_classifier import (
)
from frigate.util.image import (
SharedMemoryFrameManager,
get_histogram,
get_histogram_from_bgr,
intersection_over_union,
)
from frigate.util.object import average_boxes, median_of_boxes
@ -523,6 +523,19 @@ class NorfairTracker(ObjectTracker):
yuv_frame = self.frame_manager.get(
frame_name, self.camera_config.frame_shape_yuv
)
# Convert the frame to BGR once per call for histogram embeddings rather
# than once per detection; every detection histograms a region of the
# same frame. Skipped when there are no detections, so a frame without
# objects costs no conversion at all.
bgr_frame: np.ndarray | None = None
if (
detections
and yuv_frame is not None
and self.ptz_metrics.autotracker_enabled.value
):
bgr_frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2BGR_I420)
for obj in detections:
label = obj[0]
if label not in detections_by_type:
@ -536,9 +549,9 @@ class NorfairTracker(ObjectTracker):
points = np.array([[obj[2][0], obj[2][1]], [obj[2][2], obj[2][3]]])
embedding = None
if self.ptz_metrics.autotracker_enabled.value:
embedding = get_histogram(
yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3]
if bgr_frame is not None:
embedding = get_histogram_from_bgr(
bgr_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3]
)
detection = Detection(

View File

@ -1243,6 +1243,14 @@ def get_image_from_recording(
def get_histogram(image, x_min, y_min, x_max, y_max):
image_bgr = cv2.cvtColor(image, cv2.COLOR_YUV2BGR_I420)
return get_histogram_from_bgr(image_bgr, x_min, y_min, x_max, y_max)
def get_histogram_from_bgr(image_bgr, x_min, y_min, x_max, y_max):
"""Compute a normalized BGR histogram for a region of an already-converted
BGR frame. Callers that need histograms for several regions of the same
frame should convert the frame to BGR once and reuse it here rather than
paying a full-frame YUV to BGR conversion per region."""
image_bgr = image_bgr[y_min:y_max, x_min:x_max]
hist = cv2.calcHist(

View File

@ -106,7 +106,15 @@ def get_cpu_stats() -> dict[str, dict]:
"""Get cpu usages for each process id"""
usages = {}
docker_memlimit = get_docker_memlimit_bytes() / 1024
total_mem = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / 1024
page_size = os.sysconf("SC_PAGE_SIZE")
total_mem = page_size * os.sysconf("SC_PHYS_PAGES") / 1024
# These values are invariant across the process loop below, so read them
# once per stats cycle instead of once per process. On hosts with many
# camera ffmpeg instances the per-process reads were a visible spike.
clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"])
with open("/proc/uptime") as f:
system_uptime_sec = int(float(f.read().split()[0]))
system_cpu = psutil.cpu_percent(
interval=None
@ -133,11 +141,6 @@ def get_cpu_stats() -> dict[str, dict]:
stime = int(stats[14])
start_time = int(stats[21])
with open("/proc/uptime") as f:
system_uptime_sec = int(float(f.read().split()[0]))
clk_tck = os.sysconf(os.sysconf_names["SC_CLK_TCK"])
process_utime_sec = utime // clk_tck
process_stime_sec = stime // clk_tck
process_start_time_sec = start_time // clk_tck
@ -148,7 +151,7 @@ def get_cpu_stats() -> dict[str, dict]:
with open(f"/proc/{pid}/statm") as f:
mem_stats = f.readline().split()
mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024
mem_res = int(mem_stats[1]) * page_size / 1024
if docker_memlimit > 0:
mem_pct = round((mem_res / docker_memlimit) * 100, 1)

View File

@ -75,7 +75,15 @@ def capture_frames(
frame_name = f"{config.name}_frame{frame_index}"
frame_buffer = frame_manager.write(frame_name)
try:
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
# read directly into shared memory to avoid allocating and then
# copying a full frame out of the ffmpeg pipe every frame. stdout
# is buffered, so readinto returns a full frame unless the pipe
# closed mid-frame.
if (
ffmpeg_process.stdout.readinto(frame_buffer[:frame_size])
!= frame_size
):
raise EOFError("did not read a full frame from ffmpeg")
except Exception:
# shutdown has been initiated
if stop_event.is_set():