Download segment.py from nelikCode/Video-Background-Removal: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/nelikCode/Video-Background-Removal/resolve/main/segment.py
- Command line
-
hf download hf://spaces/nelikCode/Video-Background-Removal/segment.py
-
curl -L -o segment.py https://huggingface.co/spaces/nelikCode/Video-Background-Removal/resolve/main/segment.py
11.6 kB
| import queue | |
| import shutil | |
| import subprocess | |
| import sys | |
| import threading | |
| import cv2 | |
| import numpy as np | |
| import torch | |
| from transformers import ( | |
| AutoConfig, | |
| AutoImageProcessor, | |
| AutoModel, | |
| AutoModelForObjectDetection, | |
| Sam2VideoProcessor, | |
| ) | |
| TRACKER = "yonigozlan/EdgeTAM-hf" | |
| DETECTOR = "PekingU/rtdetr_v2_r18vd" | |
| SIZE = 512 # must be a multiple of 256 | |
| LOST_AFTER, RETRY_EVERY = 3, 5 # frames | |
| class Cancelled(Exception): | |
| pass | |
| def get_device(): | |
| if torch.cuda.is_available(): | |
| return "cuda" | |
| if torch.backends.mps.is_available(): | |
| return "mps" | |
| return "cpu" | |
| def hex_to_rgb(color): | |
| color = color.lstrip("#") | |
| return tuple(int(color[i : i + 2], 16) for i in (0, 2, 4)) | |
| def first_frame(video): | |
| cap = cv2.VideoCapture(video) | |
| ok, bgr = cap.read() | |
| cap.release() | |
| if not ok: | |
| raise ValueError(f"Cannot read video: {video}") | |
| return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) | |
| def rank_subjects(boxes, scores, width, height): | |
| """Order detections by importance: confidence x relative size x closeness to the center.""" | |
| boxes, scores = np.asarray(boxes, dtype=float).reshape(-1, 4), np.asarray( | |
| scores, dtype=float | |
| ) | |
| area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) / (width * height) | |
| cx = (boxes[:, 0] + boxes[:, 2]) / 2 / width | |
| cy = (boxes[:, 1] + boxes[:, 3]) / 2 / height | |
| centrality = 1 - np.hypot(cx - 0.5, cy - 0.5) / np.hypot(0.5, 0.5) | |
| return np.argsort(-(scores * np.sqrt(area) * centrality)).tolist() | |
| def box_iou(a, b): | |
| w = max(0, min(a[2], b[2]) - max(a[0], b[0])) | |
| h = max(0, min(a[3], b[3]) - max(a[1], b[1])) | |
| union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - w * h | |
| return w * h / union if union > 0 else 0.0 | |
| def box_distance(b, box): | |
| """Center offset in units of `box`'s diagonal, plus the log size ratio.""" | |
| dx = (b[0] + b[2] - box[0] - box[2]) / 2 | |
| dy = (b[1] + b[3] - box[1] - box[3]) / 2 | |
| size = np.log( | |
| ((b[2] - b[0]) * (b[3] - b[1]) + 1) | |
| / ((box[2] - box[0]) * (box[3] - box[1]) + 1) | |
| ) | |
| return np.hypot(dx, dy) / np.hypot(box[2] - box[0], box[3] - box[1]) + abs(size) | |
| def closest(subjects, box): | |
| return min(subjects, key=lambda s: box_distance(s["box"], box)) | |
| def mask_box(alpha): | |
| ys, xs = np.nonzero(alpha > 127) | |
| return ( | |
| None | |
| if len(xs) == 0 | |
| else [int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1] | |
| ) | |
| def ffmpeg_exe(): | |
| if path := shutil.which("ffmpeg"): | |
| return path | |
| import imageio_ffmpeg | |
| return imageio_ffmpeg.get_ffmpeg_exe() | |
| def video_codec(width, height, fps): | |
| bitrate = str(max(2_000_000, int(width * height * fps * 0.15))) | |
| if sys.platform == "darwin": | |
| return ["-c:v", "h264_videotoolbox", "-b:v", bitrate] | |
| if torch.cuda.is_available(): | |
| return ["-c:v", "h264_nvenc", "-b:v", bitrate] | |
| return ["-c:v", "libx264", "-preset", "veryfast", "-crf", "18"] | |
| class Segmenter: | |
| def __init__(self, device=None): | |
| self.device = device or get_device() | |
| self.dtype = torch.float32 if self.device == "cpu" else torch.float16 | |
| config = AutoConfig.from_pretrained(TRACKER) | |
| s = SIZE // 16 | |
| config.image_size = config.prompt_encoder_config.image_size = SIZE | |
| config.vision_config.backbone_feature_sizes = [ | |
| [4 * s, 4 * s], | |
| [2 * s, 2 * s], | |
| [s, s], | |
| ] | |
| config.memory_attention_rope_feat_sizes = [s, s] | |
| self.tracker = ( | |
| AutoModel.from_pretrained(TRACKER, config=config) | |
| .to(self.device, self.dtype) | |
| .eval() | |
| ) | |
| self.processor = Sam2VideoProcessor.from_pretrained(TRACKER) | |
| self.processor.image_processor.size = {"height": SIZE, "width": SIZE} | |
| self.processor.video_processor.size = {"height": SIZE, "width": SIZE} | |
| self.processor.target_size = SIZE | |
| self.detector_processor = AutoImageProcessor.from_pretrained(DETECTOR) | |
| # on CPU: it runs once per video, and MPS is not safe to share across threads | |
| self.detector = AutoModelForObjectDetection.from_pretrained(DETECTOR).eval() | |
| def detect(self, frame, threshold=0.5): | |
| """Return the subjects found in an RGB frame, most important first.""" | |
| height, width = frame.shape[:2] | |
| inputs = self.detector_processor(images=frame, return_tensors="pt") | |
| result = self.detector_processor.post_process_object_detection( | |
| self.detector(**inputs), threshold=threshold, target_sizes=[(height, width)] | |
| )[0] | |
| boxes, scores = result["boxes"].numpy(), result["scores"].numpy() | |
| labels = [self.detector.config.id2label[int(i)] for i in result["labels"]] | |
| return [ | |
| { | |
| "box": [round(float(v)) for v in boxes[i]], | |
| "label": labels[i], | |
| "score": round(float(scores[i]), 3), | |
| } | |
| for i in rank_subjects(boxes, scores, width, height) | |
| ] | |
| def segment( | |
| self, | |
| video, | |
| output, | |
| box=None, | |
| color="#009000", | |
| fast=False, | |
| progress=None, | |
| cancel=None, | |
| ): | |
| """Track the subject inside `box` (x0, y0, x1, y1 on the first frame) and write it on `color`.""" | |
| subjects = self.detect(first_frame(video)) | |
| if box is None: | |
| if not subjects: | |
| raise ValueError( | |
| "No subject found on the first frame. Give a bounding box." | |
| ) | |
| box = subjects[0]["box"] | |
| # when the subject is lost, look for it again, but only if the detector knows what it is | |
| label = next( | |
| (s["label"] for s in subjects if box_iou(s["box"], box) > 0.5), None | |
| ) | |
| cap = cv2.VideoCapture(video) | |
| fps = cap.get(cv2.CAP_PROP_FPS) or 25 | |
| width, height = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int( | |
| cap.get(cv2.CAP_PROP_FRAME_HEIGHT) | |
| ) | |
| total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| background = np.array(hex_to_rgb(color), dtype=np.uint16) | |
| ffmpeg = subprocess.Popen( | |
| [ | |
| ffmpeg_exe(), | |
| *f"-y -loglevel error -f rawvideo -pix_fmt rgb24 -s {width}x{height}".split(), | |
| *f"-r {fps} -i -".split(), | |
| *["-i", video, "-map", "0:v", "-map", "1:a:0?", "-shortest"], | |
| *["-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2"], | |
| *video_codec(width, height, fps), | |
| *"-pix_fmt yuv420p -c:a aac -movflags +faststart".split(), | |
| output, | |
| ], | |
| stdin=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| ) | |
| frames, masks, errors, stop = ( | |
| queue.Queue(8), | |
| queue.Queue(8), | |
| [], | |
| threading.Event(), | |
| ) | |
| def read(): | |
| try: | |
| while not stop.is_set(): | |
| ok, bgr = cap.read() | |
| if not ok: | |
| break | |
| rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) | |
| frames.put((rgb, self.processor(images=rgb, return_tensors="pt"))) | |
| except Exception as e: | |
| errors.append(e) | |
| finally: | |
| frames.put(None) | |
| def write(): | |
| while (item := masks.get()) is not None: | |
| if errors: | |
| continue | |
| rgb, alpha = item | |
| try: | |
| out = (rgb * alpha + background * (255 - alpha)) // 255 | |
| ffmpeg.stdin.write(out.astype(np.uint8).tobytes()) | |
| except Exception as e: | |
| errors.append(e) | |
| reader, writer = threading.Thread(target=read), threading.Thread(target=write) | |
| reader.start() | |
| writer.start() | |
| # fast: shorter memory, and the model skips every other frame while it tracks | |
| self.tracker.num_maskmem = 3 if fast else 7 | |
| stride = 2 if fast else 1 | |
| session = self.processor.init_video_session( | |
| inference_device=self.device, dtype=self.dtype | |
| ) | |
| keep = max( | |
| self.tracker.num_maskmem, self.tracker.config.max_object_pointers_in_encoder | |
| ) | |
| done, item, lost, last_box, t = 0, True, 0, box, 0 | |
| try: | |
| while not errors and (item := frames.get()) is not None: | |
| if cancel is not None and cancel.is_set(): | |
| raise Cancelled() | |
| rgb, inputs = item | |
| if done % stride and not lost: | |
| masks.put((rgb, alpha)) | |
| done += 1 | |
| if progress: | |
| progress(done, max(total, done)) | |
| continue | |
| prompt = box if done == 0 else None | |
| if ( | |
| label | |
| and lost >= LOST_AFTER | |
| and (lost - LOST_AFTER) % RETRY_EVERY == 0 | |
| ): | |
| # the detector often renames an object seen from a new angle, | |
| # so a different label is fine if it is where the subject was | |
| found = self.detect(rgb, 0.3) | |
| same = [s for s in found if s["label"] == label] | |
| near = [s for s in found if box_distance(s["box"], last_box) < 1] | |
| if same or near: | |
| prompt = closest(same or near, last_box)["box"] | |
| if prompt: | |
| self.processor.add_inputs_to_inference_session( | |
| inference_session=session, | |
| frame_idx=t, | |
| obj_ids=1, | |
| input_boxes=[[[float(v) for v in prompt]]], | |
| original_size=inputs.original_sizes[0], | |
| ) | |
| result = self.tracker( | |
| inference_session=session, | |
| frame_idx=t, | |
| frame=inputs.pixel_values[0].to(self.device, self.dtype), | |
| ) | |
| session.processed_frames.pop(t - 1, None) | |
| for outputs in session.output_dict_per_obj.values(): | |
| outputs["non_cond_frame_outputs"].pop(t - keep, None) | |
| t += 1 | |
| logits = self.processor.post_process_masks( | |
| [result.pred_masks], | |
| original_sizes=inputs.original_sizes, | |
| binarize=False, | |
| )[0][0, 0] | |
| alpha = ( | |
| (torch.sigmoid(logits.float()) * 255).to(torch.uint8).cpu().numpy() | |
| ) | |
| found_box = mask_box(alpha) | |
| alpha = alpha[..., None].astype(np.uint16) | |
| masks.put((rgb, alpha)) | |
| lost, last_box = (0, found_box) if found_box else (lost + 1, last_box) | |
| done += 1 | |
| if progress: | |
| progress(done, max(total, done)) | |
| finally: | |
| stop.set() | |
| while item is not None: | |
| item = frames.get() | |
| masks.put(None) | |
| reader.join() | |
| writer.join() | |
| cap.release() | |
| ffmpeg.stdin.close() | |
| stderr = ffmpeg.stderr.read().decode() | |
| ffmpeg.wait() | |
| if errors: | |
| raise errors[0] | |
| if ffmpeg.returncode != 0: | |
| raise RuntimeError(f"ffmpeg failed: {stderr}") | |
| return box | |