Path: blob/master/video-editor/interpolator.py
641 views
import math1import os2from multiprocessing import Event3from queue import Queue4from threading import Lock, Thread5from typing import Any67import cv28import numpy as np91011class Interpolator(Thread):12"""13Consumes keyframes with polygons and skipframes.14Interpolates polygon positions for skipframes.15Sends all frames to a writer in the order they were received.16"""1718OPTICAL_FLOW_WINDOW = (31, 31)19GRAY_PADDING = OPTICAL_FLOW_WINDOW[0]20PADDING_PARAMS = (21GRAY_PADDING,22GRAY_PADDING,23GRAY_PADDING,24GRAY_PADDING,25cv2.BORDER_REPLICATE,26None,270,28)2930def __init__(self, sample_rate: int, writer: Any):31super().__init__()32self.writer = writer33# self.frame_buffer = FrameBuffer(2 * sample_rate)34self.frame_buffer: Queue[tuple[np.ndarray, np.ndarray]] = Queue(2 * sample_rate)35self.frame_buffer.put((np.array([]), np.array([]))) # prime with dummy frame36# State variables37self.cur_frame: np.ndarray = np.ndarray([], dtype=np.uint8)38self.cur_gray: np.ndarray = np.ndarray([], dtype=np.uint8)39self.cur_keyframe_num = 040self.prev_keyframe_num = 041self.cur_polygons: list[np.ndarray] = []42self.old_polygons: list[np.ndarray] = []4344# Synchonization variables45self.keyframe_event = Event()46self.processing_lock = Lock()47self.quit = False4849# Read blur parameters form environment50try:51video_blur_str = os.environ.get("VIDEO_BLUR")52if video_blur_str:53self.blur_amount = int(video_blur_str)54self.blur_amount = max(1, min(10, self.blur_amount))55except Exception:56self.blur_amount = 35758def run(self) -> None:59"""60Checks whether the new keyframe has arrived in a loop.61If so, sets a lock and starts processing it.62"""63while not self.quit:64self.keyframe_event.wait()65self.keyframe_event.clear()66if self.cur_keyframe_num > self.prev_keyframe_num:67with self.processing_lock:68self._process_keyframe()6970def _bounding_box(self, polygon: np.ndarray, shape: tuple[int, ...]) -> list[int]:71"""72Returns the bounding box of a polygon.73Right and bottom edges are exclusive.74Trims the box to fit the frame.75"""76box = [77math.floor(np.min(polygon[:, 0])),78math.floor(np.min(polygon[:, 1])),79math.ceil(np.max(polygon[:, 0])) + 1,80math.ceil(np.max(polygon[:, 1])) + 1,81]82box[0] = max(0, box[0])83box[1] = max(0, box[1])84box[2] = min(shape[1], box[2])85box[3] = min(shape[0], box[3])8687return box8889def _blur_polygons(self, frame: np.ndarray, polygons: list[np.ndarray]):90"""91Draws blurred polygons to the frame.92"""93result = frame.copy()94channel_count = frame.shape[2]95ignore_mask_color = (255,) * channel_count9697for poly in polygons:98box = self._bounding_box(poly, frame.shape)99polygon_height = box[3] - box[1]100polygon_width = box[2] - box[0]101if polygon_height < 1 or polygon_width < 1:102continue103104# Prep mask105window = result[box[1] : box[3], box[0] : box[2]]106mask = np.zeros(window.shape, dtype=np.uint8)107cv2.fillConvexPoly(108mask, np.int32(poly - box[:2]), ignore_mask_color, cv2.LINE_AA109)110111# Prep totally blurred image112kernel_width = (polygon_width // self.blur_amount) | 1113kernel_height = (polygon_height // self.blur_amount) | 1114blurred_window = cv2.GaussianBlur(window, (kernel_width, kernel_height), 0)115116# Combine original and blur117result[box[1] : box[3], box[0] : box[2]] = cv2.bitwise_and(118window, cv2.bitwise_not(mask)119) + cv2.bitwise_and(blurred_window, mask)120121self.writer.write(result)122123def _is_consistent(self, shift: np.ndarray) -> bool:124"""125Checks if the optical flow shifts are consistent between each other.126"""127lengths = np.linalg.norm(shift, axis=1)128if np.max(lengths) < 2:129return True130131# Check average displacement length132avg_shift = np.mean(shift, axis=0)133avg_shift_len = np.linalg.norm(avg_shift)134if avg_shift_len < 0.5:135return False136avg_shift /= avg_shift_len # normalize137138# Check if direction is consistent with the average139for i, vec in enumerate(shift):140# This will not work if the polygon is moving towards the camera141# TODO: come up with a better check142if np.dot(vec, avg_shift) < 0.5 * lengths[i]:143return False144145return True146147def _interpolate_polygons(148self,149) -> list[tuple[np.ndarray, np.ndarray, list[np.ndarray]]]:150"""151Approximates blurred polygons between keyframes.152Returns a List of frames to write and their approximated polygons.153"""154# Propagate old polygons forward with optical flow155num_frames = self.cur_keyframe_num - self.prev_keyframe_num - 1156frames_to_blur = []157_, prev_gray = self.frame_buffer.get_nowait()158prev_polygons = self.old_polygons159for _ in range(num_frames):160next_frame, next_gray = self.frame_buffer.get_nowait()161polygons = []162for poly in prev_polygons:163new_poly, _, _ = cv2.calcOpticalFlowPyrLK(164prev_gray,165next_gray,166poly + self.GRAY_PADDING,167None,168winSize=self.OPTICAL_FLOW_WINDOW,169maxLevel=10,170criteria=(171cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,17210,1730.03,174),175)176new_poly = new_poly - self.GRAY_PADDING177shift = new_poly - poly178if self._is_consistent(shift):179polygons.append(new_poly)180frames_to_blur.append((next_frame, next_gray, polygons))181prev_gray = next_gray182prev_polygons = polygons183184# Propagate new polygons backward with optical flow185next_polygons = self.cur_polygons186next_gray = self.cur_gray187for i in range(num_frames):188_, prev_gray, inter_polygons = frames_to_blur[-i - 1]189polygons = []190for poly in next_polygons:191new_poly, _, _ = cv2.calcOpticalFlowPyrLK(192next_gray,193prev_gray,194poly + self.GRAY_PADDING,195None,196winSize=self.OPTICAL_FLOW_WINDOW,197maxLevel=10,198criteria=(199cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,20010,2010.03,202),203)204new_poly -= self.GRAY_PADDING205shift = new_poly - poly206if self._is_consistent(shift):207polygons.append(new_poly)208inter_polygons.extend(polygons)209next_gray = prev_gray210next_polygons = polygons211212return frames_to_blur213214def _process_keyframe(self) -> None:215"""216Uses new keyframe to interpolates the previous sequence of skipframes.217Sends all frames and their polygons to the writer.218"""219# Draw skipframes220frames_to_blur = self._interpolate_polygons()221for skipframe, _, polygons in frames_to_blur:222self._blur_polygons(skipframe, polygons)223# Draw current frame224self._blur_polygons(self.cur_frame, self.cur_polygons)225226# Update state variables227self.old_polygons = self.cur_polygons228self.prev_keyframe_num = self.cur_keyframe_num229230def _gray_pad(self, frame: np.ndarray) -> np.ndarray:231"""232Converts a frame to grayscale and pads it.233"""234gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)235return cv2.copyMakeBorder(gray, *self.PADDING_PARAMS)236237def feed_keyframe(238self, frame: np.ndarray, frame_count: int, polygons: list[np.ndarray]239) -> None:240"""241Waits if the previous keyframe is still being processed.242Otherwise records the new keyframe and its info.243Then triggers the processing event and returns.244"""245with self.processing_lock:246gray = self._gray_pad(frame)247self.frame_buffer.put((frame, gray))248249# Update state variables250self.cur_frame = frame251self.cur_gray = gray252self.cur_keyframe_num = frame_count253self.cur_polygons = polygons254255self.keyframe_event.set() # signal to start processing256257def feed_skipframe(self, frame: np.ndarray) -> None:258"""259Stacks skipframes into a buffer.260"""261self.frame_buffer.put((frame, self._gray_pad(frame)))262263def is_flush_needed(self, frame_count: int) -> bool:264"""265Determines if the buffer has remaining skipframes to be flushed.266"""267return self.cur_keyframe_num < frame_count268269def flush(self, frame_count: int, polygons: list[np.ndarray]) -> None:270"""271Extracts the previously pushed skipframe.272Then processes it as a new keyframe.273"""274with self.processing_lock:275# Check if last frame is already processed276if self.cur_keyframe_num >= frame_count:277return278279# Update state variables280self.cur_frame, self.cur_gray = self.frame_buffer.queue[-1]281self.cur_keyframe_num = frame_count282self.cur_polygons = polygons283284self.keyframe_event.set() # signal to start processing285286def close(self) -> None:287"""288Finishes processing frames and stops the thread.289"""290with self.processing_lock:291self.quit = True292self.keyframe_event.set()293self.join()294295296