From 89366d7b12f59dd2c3f8bd22923064e78bc8526c Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 1 Nov 2023 17:21:30 -0600 Subject: [PATCH] Add endpoint to return camera frame with regions grid overlaid (#8413) * Add endpoint to view grid overload on camera frame * Add api to docs * Formatting --- docs/docs/integrations/api.md | 4 ++ frigate/http.py | 108 +++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md index f080c0a36..09a7be284 100644 --- a/docs/docs/integrations/api.md +++ b/docs/docs/integrations/api.md @@ -263,6 +263,10 @@ Returns the snapshot image from the latest event for the given camera and label Returns the snapshot image from the specific point in that cameras recordings. +### `GET /api//grid.jpg` + +Returns the latest camera image with the regions grid overlaid. + ### `GET /clips/-.jpg` JPG snapshot for the given camera and event id. diff --git a/frigate/http.py b/frigate/http.py index 6bfe04432..9566a067c 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -41,7 +41,7 @@ from frigate.const import ( RECORD_DIR, ) from frigate.events.external import ExternalEventProcessor -from frigate.models import Event, Recordings, Timeline +from frigate.models import Event, Recordings, Regions, Timeline from frigate.object_processing import TrackedObject from frigate.plus import PlusApi from frigate.ptz.onvif import OnvifController @@ -726,6 +726,112 @@ def label_snapshot(camera_name, label): return response +@bp.route("//grid.jpg") +def grid_snapshot(camera_name): + request.args.get("type", default="region") + + if camera_name in current_app.frigate_config.cameras: + detect = current_app.frigate_config.cameras[camera_name].detect + frame = current_app.detected_frames_processor.get_current_frame(camera_name, {}) + retry_interval = float( + current_app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval + or 10 + ) + + if frame is None or datetime.now().timestamp() > ( + current_app.detected_frames_processor.get_current_frame_time(camera_name) + + retry_interval + ): + return make_response( + jsonify({"success": False, "message": "Unable to get valid frame"}), + 500, + ) + + try: + grid = ( + Regions.select(Regions.grid) + .where(Regions.camera == camera_name) + .get() + .grid + ) + except DoesNotExist: + return make_response( + jsonify({"success": False, "message": "Unable to get region grid"}), + 500, + ) + + grid_size = len(grid) + grid_coef = 1.0 / grid_size + width = detect.width + height = detect.height + for x in range(grid_size): + for y in range(grid_size): + cell = grid[x][y] + + if len(cell["sizes"]) == 0: + continue + + std_dev = round(cell["std_dev"] * width, 2) + mean = round(cell["mean"] * width, 2) + cv2.rectangle( + frame, + (int(x * grid_coef * width), int(y * grid_coef * height)), + ( + int((x + 1) * grid_coef * width), + int((y + 1) * grid_coef * height), + ), + (0, 255, 0), + 2, + ) + cv2.putText( + frame, + f"#: {len(cell['sizes'])}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.02) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=0.5, + color=(0, 255, 0), + thickness=2, + ) + cv2.putText( + frame, + f"std: {std_dev}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.05) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=0.5, + color=(0, 255, 0), + thickness=2, + ) + cv2.putText( + frame, + f"avg: {mean}", + ( + int(x * grid_coef * width + 10), + int((y * grid_coef + 0.08) * height), + ), + cv2.FONT_HERSHEY_SIMPLEX, + fontScale=0.5, + color=(0, 255, 0), + thickness=2, + ) + + ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + response = make_response(jpg.tobytes()) + response.headers["Content-Type"] = "image/jpeg" + response.headers["Cache-Control"] = "no-store" + return response + else: + return make_response( + jsonify({"success": False, "message": "Camera not found"}), + 404, + ) + + @bp.route("/events//clip.mp4") def event_clip(id): download = request.args.get("download", type=bool)