add ability to parse and upload image from recording to frigate+

This commit is contained in:
Nicolas Mowen 2024-05-02 09:38:47 -06:00
parent b69c1828cb
commit fba0f66d79
2 changed files with 125 additions and 26 deletions

View File

@ -7,6 +7,7 @@ import os
import subprocess as sp import subprocess as sp
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from io import BytesIO
from urllib.parse import unquote from urllib.parse import unquote
import cv2 import cv2
@ -14,6 +15,7 @@ import numpy as np
import pytz import pytz
from flask import Blueprint, Response, current_app, jsonify, make_response, request from flask import Blueprint, Response, current_app, jsonify, make_response, request
from peewee import DoesNotExist, fn from peewee import DoesNotExist, fn
from PIL import Image
from tzlocal import get_localzone_name from tzlocal import get_localzone_name
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@ -26,6 +28,7 @@ from frigate.const import (
) )
from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment from frigate.models import Event, Previews, Recordings, Regions, ReviewSegment
from frigate.util.builtin import get_tz_modifiers from frigate.util.builtin import get_tz_modifiers
from frigate.util.image import get_image_from_recording
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -205,30 +208,20 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
try: try:
recording: Recordings = recording_query.get() recording: Recordings = recording_query.get()
time_in_segment = frame_time - recording.start_time time_in_segment = frame_time - recording.start_time
image_data = get_image_from_recording(recording.path, time_in_segment)
ffmpeg_cmd = [ if not image_data:
"ffmpeg", return make_response(
"-hide_banner", jsonify(
"-loglevel", {
"warning", "success": False,
"-ss", "message": f"Unable to parse frame at time {frame_time}",
f"00:00:{time_in_segment}", }
"-i", ),
recording.path, 404,
"-frames:v",
"1",
"-c:v",
"png",
"-f",
"image2pipe",
"-",
]
process = sp.run(
ffmpeg_cmd,
capture_output=True,
) )
response = make_response(process.stdout)
response = make_response(image_data)
response.headers["Content-Type"] = "image/png" response.headers["Content-Type"] = "image/png"
return response return response
except DoesNotExist: except DoesNotExist:
@ -243,6 +236,71 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
) )
@MediaBp.route("/<camera_name>/plus/<frame_time>")
def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str):
if camera_name not in current_app.frigate_config.cameras:
return make_response(
jsonify({"success": False, "message": "Camera not found"}),
404,
)
frame_time = float(frame_time)
recording_query = (
Recordings.select(
Recordings.path,
Recordings.start_time,
)
.where(
(
(frame_time >= Recordings.start_time)
& (frame_time <= Recordings.end_time)
)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.desc())
.limit(1)
)
try:
recording: Recordings = recording_query.get()
time_in_segment = frame_time - recording.start_time
image_data = get_image_from_recording(recording.path, time_in_segment)
if not image_data:
return make_response(
jsonify(
{
"success": False,
"message": f"Unable to parse frame at time {frame_time}",
}
),
404,
)
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR)
current_app.plus_api.upload_image(nd, camera_name)
return make_response(
jsonify(
{
"success": True,
"message": "Successfully submitted image.",
}
),
200,
)
except DoesNotExist:
return make_response(
jsonify(
{
"success": False,
"message": "Recording not found at {}".format(frame_time),
}
),
404,
)
@MediaBp.route("/recordings/storage", methods=["GET"]) @MediaBp.route("/recordings/storage", methods=["GET"])
def get_recordings_storage_usage(): def get_recordings_storage_usage():
recording_stats = current_app.stats_emitter.get_latest_stats()["service"][ recording_stats = current_app.stats_emitter.get_latest_stats()["service"][

View File

@ -2,6 +2,7 @@
import datetime import datetime
import logging import logging
import subprocess as sp
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from multiprocessing import shared_memory from multiprocessing import shared_memory
from string import printable from string import printable
@ -360,14 +361,17 @@ def yuv_crop_and_resize(frame, region, height=None):
# copy u2 # copy u2
yuv_cropped_frame[ yuv_cropped_frame[
size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height, size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height,
size // 2 + uv_channel_x_offset : size // 2 size // 2
+ uv_channel_x_offset : size // 2
+ uv_channel_x_offset + uv_channel_x_offset
+ uv_crop_width, + uv_crop_width,
] = frame[u2[1] : u2[3], u2[0] : u2[2]] ] = frame[u2[1] : u2[3], u2[0] : u2[2]]
# copy v1 # copy v1
yuv_cropped_frame[ yuv_cropped_frame[
size + size // 4 + uv_channel_y_offset : size size
+ size // 4
+ uv_channel_y_offset : size
+ size // 4 + size // 4
+ uv_channel_y_offset + uv_channel_y_offset
+ uv_crop_height, + uv_crop_height,
@ -376,11 +380,14 @@ def yuv_crop_and_resize(frame, region, height=None):
# copy v2 # copy v2
yuv_cropped_frame[ yuv_cropped_frame[
size + size // 4 + uv_channel_y_offset : size size
+ size // 4
+ uv_channel_y_offset : size
+ size // 4 + size // 4
+ uv_channel_y_offset + uv_channel_y_offset
+ uv_crop_height, + uv_crop_height,
size // 2 + uv_channel_x_offset : size // 2 size // 2
+ uv_channel_x_offset : size // 2
+ uv_channel_x_offset + uv_channel_x_offset
+ uv_crop_width, + uv_crop_width,
] = frame[v2[1] : v2[3], v2[0] : v2[2]] ] = frame[v2[1] : v2[3], v2[0] : v2[2]]
@ -746,3 +753,37 @@ def add_mask(mask: str, mask_img: np.ndarray):
] ]
) )
cv2.fillPoly(mask_img, pts=[contour], color=(0)) cv2.fillPoly(mask_img, pts=[contour], color=(0))
def get_image_from_recording(
file_path: str, relative_frame_time: float
) -> Optional[any]:
"""retrieve a frame from given time in recording file."""
ffmpeg_cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel",
"warning",
"-ss",
f"00:00:{relative_frame_time}",
"-i",
file_path,
"-frames:v",
"1",
"-c:v",
"png",
"-f",
"image2pipe",
"-",
]
process = sp.run(
ffmpeg_cmd,
capture_output=True,
)
if process.returncode == 0:
return process.stdout
else:
return None