Enable event snapshot API to honour query params after event ends (#22375)
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* Enable event snapshot API to honour query params

* fix unused imports

* Fixes

* Run ruff check --fix

* Web changes

* Further config and web fixes

* Further docs tweak

* Fix missing quality default in MediaEventsSnapshotQueryParams

* Manual events: don't save annotated jpeg; store frame time

* Remove unnecessary grayscale helper

* Add caveat to docs on snapshot_frame_time pre-0.18

* JPG snapshot should not be treated as clean

* Ensure tracked details uses uncropped, bbox'd snapshot

* Ensure all UI pages / menu actions use uncropped, bbox'd

* web lint

* Add missed config helper text

* Expect  SnapshotsConfig not Any

* docs: Remove pre-0.18 note

* Specify timestamp=0 in the UI

* Move tests out of http media

* Correct missed settings.json wording

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Revert to default None for quality

* Correct camera snapshot config wording

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Fix quality=0 handling

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Fix quality=0 handling #2

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* ReRun generate_config_translations

---------

Co-authored-by: leccelecce <example@example.com>
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
leccelecce
2026-03-22 13:33:04 -06:00
committed by GitHub
co-authored by leccelecce Josh Hawkins
parent b6c03c99de
commit ec7040bed5
32 changed files with 797 additions and 475 deletions
+4
View File
@@ -547,7 +547,10 @@ class TrackedObjectProcessor(threading.Thread):
"has_clip": self.config.cameras[camera_name].record.enabled
and include_recording,
"has_snapshot": True,
"snapshot_clean": True,
"snapshot_frame_time": frame_time,
"type": source_type,
"draw": draw,
},
)
)
@@ -603,6 +606,7 @@ class TrackedObjectProcessor(threading.Thread):
"has_clip": self.config.cameras[camera_name].record.enabled
and include_recording,
"has_snapshot": True,
"snapshot_clean": True,
"type": "api",
"recognized_license_plate": plate,
"recognized_license_plate_score": score,
+35 -138
View File
@@ -13,7 +13,6 @@ import numpy as np
from frigate.config import (
CameraConfig,
FilterConfig,
SnapshotsConfig,
UIConfig,
)
from frigate.const import CLIPS_DIR, REPLAY_CAMERA_PREFIX, THUMB_DIR
@@ -22,9 +21,7 @@ from frigate.review.types import SeverityEnum
from frigate.util.builtin import sanitize_float
from frigate.util.image import (
area,
calculate_region,
draw_box_with_label,
draw_timestamp,
get_snapshot_bytes,
is_better_thumbnail,
)
from frigate.util.object import box_inside
@@ -393,6 +390,7 @@ class TrackedObject:
"camera": self.camera_config.name,
"frame_time": self.obj_data["frame_time"],
"snapshot": self.thumbnail_data,
"snapshot_clean": True,
"label": self.obj_data["label"],
"sub_label": self.obj_data.get("sub_label"),
"top_score": self.top_score,
@@ -449,27 +447,15 @@ class TrackedObject:
return img.tobytes()
def get_clean_webp(self) -> bytes | None:
if self.thumbnail_data is None:
return None
try:
best_frame = cv2.cvtColor(
self.frame_cache[self.thumbnail_data["frame_time"]]["frame"],
cv2.COLOR_YUV2BGR_I420,
)
except KeyError:
logger.warning(
f"Unable to create clean webp because frame {self.thumbnail_data['frame_time']} is not in the cache"
)
return None
ret, webp = cv2.imencode(
".webp", best_frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
webp_bytes, _ = self.get_img_bytes(
ext="webp",
timestamp=False,
bounding_box=False,
crop=False,
height=None,
quality=self.camera_config.snapshots.quality,
)
if ret:
return webp.tobytes()
else:
return None
return webp_bytes
def get_img_bytes(
self,
@@ -491,131 +477,42 @@ class TrackedObject:
)
except KeyError:
logger.warning(
f"Unable to create jpg because frame {frame_time} is not in the cache"
f"Unable to create snapshot because frame {frame_time} is not in the cache"
)
return None, None
if bounding_box:
thickness = 2
color = self.colormap.get(self.obj_data["label"], (255, 255, 255))
# draw the bounding boxes on the frame
box = self.thumbnail_data["box"]
draw_box_with_label(
best_frame,
box[0],
box[1],
box[2],
box[3],
self.obj_data["label"],
f"{int(self.thumbnail_data['score'] * 100)}% {int(self.thumbnail_data['area'])}"
+ (
f" {self.thumbnail_data['current_estimated_speed']:.1f}"
if self.thumbnail_data["current_estimated_speed"] != 0
else ""
),
thickness=thickness,
color=color,
)
# draw any attributes
for attribute in self.thumbnail_data["attributes"]:
box = attribute["box"]
box_area = int((box[2] - box[0]) * (box[3] - box[1]))
draw_box_with_label(
best_frame,
box[0],
box[1],
box[2],
box[3],
attribute["label"],
f"{attribute['score']:.0%} {str(box_area)}",
thickness=thickness,
color=color,
)
if crop:
box = self.thumbnail_data["box"]
box_size = 300
region = calculate_region(
best_frame.shape,
box[0],
box[1],
box[2],
box[3],
box_size,
multiplier=1.1,
)
best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
if height:
width = int(height * best_frame.shape[1] / best_frame.shape[0])
best_frame = cv2.resize(
best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA
)
if timestamp:
colors = self.camera_config.timestamp_style.color
draw_timestamp(
best_frame,
self.thumbnail_data["frame_time"],
self.camera_config.timestamp_style.format,
font_effect=self.camera_config.timestamp_style.effect,
font_thickness=self.camera_config.timestamp_style.thickness,
font_color=(colors.blue, colors.green, colors.red),
position=self.camera_config.timestamp_style.position,
)
quality_params = []
if ext == "jpg":
quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality or 70]
elif ext == "webp":
quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), quality or 60]
ret, jpg = cv2.imencode(f".{ext}", best_frame, quality_params)
if ret:
return jpg.tobytes(), frame_time
else:
return None, None
return get_snapshot_bytes(
best_frame,
frame_time,
ext=ext,
timestamp=timestamp,
bounding_box=bounding_box,
crop=crop,
height=height,
quality=quality,
label=self.obj_data["label"],
box=self.thumbnail_data["box"],
score=self.thumbnail_data["score"],
area=self.thumbnail_data["area"],
attributes=self.thumbnail_data["attributes"],
color=self.colormap.get(self.obj_data["label"], (255, 255, 255)),
timestamp_style=self.camera_config.timestamp_style,
estimated_speed=self.thumbnail_data["current_estimated_speed"],
)
def write_snapshot_to_disk(self) -> None:
snapshot_config: SnapshotsConfig = self.camera_config.snapshots
jpg_bytes, _ = self.get_img_bytes(
ext="jpg",
timestamp=snapshot_config.timestamp,
bounding_box=snapshot_config.bounding_box,
crop=snapshot_config.crop,
height=snapshot_config.height,
quality=snapshot_config.quality,
)
if jpg_bytes is None:
webp_bytes = self.get_clean_webp()
if webp_bytes is None:
logger.warning(f"Unable to save snapshot for {self.obj_data['id']}.")
else:
with open(
os.path.join(
CLIPS_DIR, f"{self.camera_config.name}-{self.obj_data['id']}.jpg"
CLIPS_DIR,
f"{self.camera_config.name}-{self.obj_data['id']}-clean.webp",
),
"wb",
) as j:
j.write(jpg_bytes)
# write clean snapshot if enabled
if snapshot_config.clean_copy:
webp_bytes = self.get_clean_webp()
if webp_bytes is None:
logger.warning(
f"Unable to save clean snapshot for {self.obj_data['id']}."
)
else:
with open(
os.path.join(
CLIPS_DIR,
f"{self.camera_config.name}-{self.obj_data['id']}-clean.webp",
),
"wb",
) as p:
p.write(webp_bytes)
) as p:
p.write(webp_bytes)
def write_thumbnail_to_disk(self) -> None:
if not self.camera_config.name: