Path: blob/master/video-editor/video_editor.py
641 views
import logging1import os2import sys3import tempfile4import time5from pathlib import Path67import cv28import ffmpegcv9import numpy as np10import requests11from flask import Flask, jsonify, request12from interpolator import Interpolator13from utils import draw_bounding_box_on_image1415LOG_LEVEL = os.environ.get("LOGGING", "INFO").upper()1617logging.basicConfig(18stream=sys.stdout,19level=LOG_LEVEL,20datefmt="%Y-%m-%d %H:%M:%S",21format="%(levelname)-5s [%(name)s.%(lineno)d] => %(message)s",22)2324lgr = logging.getLogger("video-editor")2526BASE_WORKING_DIR = "/user-data/"272829def recognition_api(cv2_frame, data, sdk_url, api_key):30retval, buffer = cv2.imencode(".jpg", cv2_frame)3132if sdk_url:33url = sdk_url + "/v1/plate-reader/"34headers = None35else:36if api_key is None:37raise Exception("A TOKEN is required if using Cloud API")3839url = "https://api.platerecognizer.com/v1/plate-reader/"40headers = {"Authorization": "Token " + api_key}4142while True:43response = requests.post(44url, files=dict(upload=buffer), headers=headers, data=data45)4647if response.status_code < 200 or response.status_code > 300:48if response.status_code == 429:49time.sleep(1)50else:51logging.error(response.text)52raise Exception("Error running recognition")53else:54res_json = response.json()55if "error" in res_json:56logging.error(response.text)57raise Exception("Error running recognition")5859return res_json606162def visualize_frame(cv2_frame, sdk_url, snapshot_api_token):63run_recognition_response = recognition_api(64cv2_frame, {}, sdk_url, snapshot_api_token65)6667for result in run_recognition_response["results"]:68plate_bounding_box = result["box"]69plate = result["plate"]70draw_bounding_box_on_image(71cv2_frame,72plate_bounding_box["ymin"],73plate_bounding_box["xmin"],74plate_bounding_box["ymax"],75plate_bounding_box["xmax"],76plate,77)7879# Vehicle box80if result["vehicle"]["score"] > 0:81vehicle_bounding_box = result["vehicle"]["box"]82vehicle = result["vehicle"]["type"]83draw_bounding_box_on_image(84cv2_frame,85vehicle_bounding_box["ymin"],86vehicle_bounding_box["xmin"],87vehicle_bounding_box["ymax"],88vehicle_bounding_box["xmax"],89vehicle,90)9192return cv2_frame939495def blur_api(cv2_frame, blur_url):96retval, buffer = cv2.imencode(".jpg", cv2_frame)9798response = requests.post(blur_url, files=dict(upload=("frame.jpg", buffer)))99if response.status_code < 200 or response.status_code > 300:100logging.error(response.text)101raise Exception("Error performing blur")102else:103return response104105106def get_blur_polygons(cv2_frame: np.ndarray, blur_url: str):107"""108Call Blur API to request polygons to be blurred.109"""110blur_response = blur_api(cv2_frame, blur_url)111polygons = [112np.array(plate["polygon"], dtype=np.float32)113for plate in blur_response.json()["plates"]114]115116return polygons117118119def save_frame(count, cv2_image, save_dir, image_format="jpg"):120save_path = f"{save_dir}frame_{count}.{image_format}"121lgr.debug(f"saving frame to: {save_path}")122if image_format == "png":123# default 3, 9 is highest compression124cv2.imwrite(save_path, cv2_image, [int(cv2.IMWRITE_PNG_COMPRESSION), 3])125126elif image_format == "jpg":127# default 95, 100 is best quality128cv2.imwrite(save_path, cv2_image, [cv2.IMWRITE_JPEG_QUALITY, 95])129130else:131raise Exception(f"Unrecognized Output format: {image_format}")132133134def init_writer(filename, fps):135return ffmpegcv.noblock(ffmpegcv.VideoWriter, filename, "h264", fps)136137138def process_video(video, action):139filename = video.filename140lgr.debug(f"Processing video: {filename}")141142# check processing actions for camera143lgr.debug(f"enabled_actions: {action}")144145frames_enabled = "frames" in action146visualization_enabled = "visualization" in action147blur_enabled = "blur" in action148149lgr.debug(f"CONFIG frames_enabled: {frames_enabled}")150lgr.debug(f"CONFIG visualization_enabled: {visualization_enabled}")151lgr.debug(f"CONFIG blur_enabled: {blur_enabled}")152153out1, out2, frames_output_dir, sdk_url, snapshot_api_token, blur_url = (154None,155None,156None,157None,158None,159None,160)161162temp_dir = tempfile.mkdtemp()163164# Save the uploaded video file to the temporary directory165video_path = os.path.join(temp_dir, video.filename)166video.save(video_path)167168cap = ffmpegcv.VideoCapture(video_path)169170if not cap.isOpened():171lgr.debug("Error opening video stream or file")172exit(1)173174filename_stem = Path(video_path).stem175video_format_ext = "mp4"176177# Override FPS if provided178try:179fps = int(os.environ.get("FPS"))180except TypeError:181# ffmpegcv cap.fps is not reliable182# Calculate FPS manually by counting frames for 500ms183184fps_cap = cv2.VideoCapture(video_path)185frame_count = 0186last_frame_time = 0187while fps_cap.isOpened():188ret, _ = fps_cap.read()189last_frame_time = fps_cap.get(cv2.CAP_PROP_POS_MSEC)190# Stop at half a second or no more frames191if not ret or last_frame_time >= 500:192break193frame_count += 1194fps_cap.release()195assert last_frame_time > 0, "Video too short or frames are not readable"196fps = frame_count * 1000 / last_frame_time197lgr.debug(f"FPS: {fps}")198199if visualization_enabled:200output1_filename = (201f"{BASE_WORKING_DIR}{filename_stem}_visualization.{video_format_ext}"202)203out1 = init_writer(output1_filename, fps)204205if blur_enabled:206output2_filename = f"{BASE_WORKING_DIR}{filename_stem}_blur.{video_format_ext}"207out2 = init_writer(output2_filename, fps)208209# Create the output dir for frames if missing210if frames_enabled:211frames_output_dir = f"{BASE_WORKING_DIR}{filename_stem}_frames/"212Path(frames_output_dir).mkdir(parents=True, exist_ok=True)213lgr.debug(f"CONFIG frames_output_dir: {frames_output_dir}")214215# Parse visualization parameters216if visualization_enabled:217sdk_url = os.environ.get("SDK_URL")218snapshot_api_token = os.environ.get("TOKEN")219220lgr.debug(f"CONFIG sdk_url: {sdk_url}")221lgr.debug(f"CONFIG snapshot_api_token: {snapshot_api_token}")222223# Parse blur parameters224if blur_enabled:225blur_url = os.environ.get("BLUR_URL")226227try:228sample_rate = int(os.environ.get("SAMPLE"))229except Exception:230sample_rate = 5231keyframe_residue = 1 % sample_rate # for sample_rate = 1232interpolator = Interpolator(sample_rate, out2)233interpolator.start()234235start = time.time()236frame_count = 0237while cap.isOpened():238ret, frame = cap.read()239if ret:240lgr.debug(f"Processing frame: {frame_count}")241frame_count += 1242243if frames_enabled:244save_frame(frame_count, frame, frames_output_dir)245246if visualization_enabled:247# adding filled rectangle on each frame248visualized_frame = visualize_frame(frame, sdk_url, snapshot_api_token)249out1.write(visualized_frame)250251if blur_enabled:252if frame_count % sample_rate == keyframe_residue:253# Keyframe254polygons = get_blur_polygons(frame, blur_url)255interpolator.feed_keyframe(frame, frame_count, polygons)256else:257# Skipframes258interpolator.feed_skipframe(frame)259else:260break261262# Flush the remaining skipframes263if blur_enabled and interpolator.is_flush_needed(frame_count):264frame, _ = interpolator.frame_buffer.queue[-1]265polygons = get_blur_polygons(frame, blur_url)266interpolator.flush(frame_count, polygons)267interpolator.close()268269cap.release()270if out1:271out1.release()272if out2:273out2.release()274275lgr.debug(f"Frame count: {frame_count}")276lgr.debug(f"Time taken: {time.time() - start}")277lgr.debug(f"Done processing video {filename}")278os.remove(video_path)279os.rmdir(temp_dir)280281282app = Flask(__name__)283284285@app.route("/process-video", methods=["POST"])286def process_video_route():287if "upload" not in request.files or "action" not in request.form:288return jsonify({"error": "Invalid request"}), 400289290file = request.files["upload"]291action = request.form["action"]292293if file.filename == "":294return jsonify({"error": "No selected file"}), 400295296try:297process_video(file, action)298except Exception as e:299lgr.error("Error:", exc_info=e)300return jsonify({"error": str(e)}), 500301302return jsonify("Done."), 200303304305app.run(host="0.0.0.0", port=8081, debug=True)306307308