Spaces:
Configuration error
Configuration error
Update pipeline/video_pipeline.py
Browse files- pipeline/video_pipeline.py +72 -38
pipeline/video_pipeline.py
CHANGED
|
@@ -105,51 +105,85 @@ def _normalize_input(inp, work_dir: Path) -> str:
|
|
| 105 |
raise TypeError(f"Unsupported input: {type(inp)}")
|
| 106 |
return str(target)
|
| 107 |
|
| 108 |
-
# --- SAM2 Mask Generation (
|
| 109 |
-
def generate_first_frame_mask(video_path, predictor):
|
| 110 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
cap = cv2.VideoCapture(video_path)
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
if not ret:
|
|
|
|
| 115 |
raise ValueError("Failed to read video frame")
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
scaled = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
| 125 |
-
logger.info(f"[sam2] Resized for SAM2: {orig_w}x{orig_h} -> {new_w}x{new_h}")
|
| 126 |
-
else:
|
| 127 |
-
new_h, new_w = orig_h, orig_w
|
| 128 |
-
logger.info(f"[sam2] Using original size for SAM2: {orig_w}x{orig_h}")
|
| 129 |
|
| 130 |
-
|
| 131 |
with torch.inference_mode(), torch.autocast(
|
| 132 |
"cuda", dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32
|
| 133 |
):
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
return
|
| 153 |
|
| 154 |
# --- Temporal Smoothing ---
|
| 155 |
def smooth_alpha_video(alpha_path, output_path, window_size=5):
|
|
@@ -244,7 +278,7 @@ def stage1_create_transparent_video(input_file, sam2_predictor, matanyone_proces
|
|
| 244 |
logger.warning("[stage1] Audio extraction failed, continuing without audio")
|
| 245 |
audio_path = None
|
| 246 |
|
| 247 |
-
# 2) First-frame mask via SAM2 (now at original size)
|
| 248 |
mask = generate_first_frame_mask(input_path, sam2_predictor)
|
| 249 |
mask_path = str(temp_dir / "mask.png")
|
| 250 |
ok = cv2.imwrite(mask_path, mask)
|
|
@@ -255,7 +289,7 @@ def stage1_create_transparent_video(input_file, sam2_predictor, matanyone_proces
|
|
| 255 |
# 3) MatAnyone in a thread with timeout (prevents silent hangs)
|
| 256 |
logger.info(
|
| 257 |
f"[stage1] Starting MatAnyone.process_video "
|
| 258 |
-
f"(input_path={input_path}, mask_path={mask_path}, output_path={temp_dir}, max_size=720)"
|
| 259 |
)
|
| 260 |
|
| 261 |
result_holder = {"ok": False, "fg": None, "alpha": None, "exc": None}
|
|
|
|
| 105 |
raise TypeError(f"Unsupported input: {type(inp)}")
|
| 106 |
return str(target)
|
| 107 |
|
| 108 |
+
# --- SAM2 Mask Generation (multi-frame, returns mask at ORIGINAL video size) ---
|
| 109 |
+
def generate_first_frame_mask(video_path, predictor, num_frames: int = 3):
|
| 110 |
+
"""
|
| 111 |
+
Build a robust seed mask by running SAM2 on the first N frames (default 3),
|
| 112 |
+
upsampling each mask back to the ORIGINAL video resolution, and combining
|
| 113 |
+
them by majority vote. Output is a uint8 mask in {0, 255} at (orig_h, orig_w).
|
| 114 |
+
"""
|
| 115 |
+
import math
|
| 116 |
+
|
| 117 |
cap = cv2.VideoCapture(video_path)
|
| 118 |
+
frames = []
|
| 119 |
+
|
| 120 |
+
# Grab the first frame to establish original size
|
| 121 |
+
ret, first = cap.read()
|
| 122 |
if not ret:
|
| 123 |
+
cap.release()
|
| 124 |
raise ValueError("Failed to read video frame")
|
| 125 |
+
orig_h, orig_w = first.shape[:2]
|
| 126 |
+
frames.append(first)
|
| 127 |
|
| 128 |
+
# Read up to (num_frames-1) more initial frames
|
| 129 |
+
for _ in range(1, max(1, num_frames)):
|
| 130 |
+
ret, f = cap.read()
|
| 131 |
+
if not ret:
|
| 132 |
+
break
|
| 133 |
+
frames.append(f)
|
| 134 |
+
cap.release()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
+
masks_fullres = []
|
| 137 |
with torch.inference_mode(), torch.autocast(
|
| 138 |
"cuda", dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32
|
| 139 |
):
|
| 140 |
+
for idx, frame in enumerate(frames):
|
| 141 |
+
h, w = frame.shape[:2]
|
| 142 |
+
# Downscale for inference if needed (≤1080 on the long side)
|
| 143 |
+
if max(h, w) > 1080:
|
| 144 |
+
scale = 1080 / max(h, w)
|
| 145 |
+
new_w, new_h = int(w * scale), int(h * scale)
|
| 146 |
+
scaled = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
| 147 |
+
logger.info(f"[sam2] f{idx}: resized for SAM2 {w}x{h} -> {new_w}x{new_h}")
|
| 148 |
+
else:
|
| 149 |
+
new_h, new_w = h, w
|
| 150 |
+
scaled = frame
|
| 151 |
+
logger.info(f"[sam2] f{idx}: using original size for SAM2 {w}x{h}")
|
| 152 |
+
|
| 153 |
+
# Run SAM2 on the (possibly) downscaled frame
|
| 154 |
+
predictor.set_image(cv2.cvtColor(scaled, cv2.COLOR_BGR2RGB))
|
| 155 |
+
masks, scores, _ = predictor.predict(
|
| 156 |
+
point_coords=np.array([[new_w // 2, new_h // 2]]),
|
| 157 |
+
point_labels=np.array([1]),
|
| 158 |
+
multimask_output=True,
|
| 159 |
+
)
|
| 160 |
+
mask_small = masks[np.argmax(scores)] # float 0..1
|
| 161 |
+
|
| 162 |
+
# Upsample the mask back to ORIGINAL resolution with NEAREST (no blur)
|
| 163 |
+
if (new_w, new_h) != (orig_w, orig_h):
|
| 164 |
+
mask_full = cv2.resize(
|
| 165 |
+
mask_small.astype(np.float32),
|
| 166 |
+
(orig_w, orig_h),
|
| 167 |
+
interpolation=cv2.INTER_NEAREST,
|
| 168 |
+
)
|
| 169 |
+
logger.info(f"[sam2] f{idx}: upsampled mask -> {orig_w}x{orig_h}")
|
| 170 |
+
else:
|
| 171 |
+
mask_full = mask_small.astype(np.float32)
|
| 172 |
+
|
| 173 |
+
masks_fullres.append(mask_full)
|
| 174 |
+
|
| 175 |
+
if not masks_fullres:
|
| 176 |
+
raise RuntimeError("SAM2 produced no masks")
|
| 177 |
+
|
| 178 |
+
# Majority vote across frames
|
| 179 |
+
stack = np.stack(masks_fullres, axis=0) # (N, H, W), values 0..1
|
| 180 |
+
required = (len(masks_fullres) + 1) // 2 # ceil(N/2)
|
| 181 |
+
vote = (np.sum(stack > 0.5, axis=0) >= required).astype(np.uint8) * 255
|
| 182 |
+
|
| 183 |
+
logger.info(f"[sam2] multi-frame seed: N={len(masks_fullres)}, "
|
| 184 |
+
f"orig_size={orig_w}x{orig_h}, majority={required}/{len(masks_fullres)}")
|
| 185 |
|
| 186 |
+
return vote
|
| 187 |
|
| 188 |
# --- Temporal Smoothing ---
|
| 189 |
def smooth_alpha_video(alpha_path, output_path, window_size=5):
|
|
|
|
| 278 |
logger.warning("[stage1] Audio extraction failed, continuing without audio")
|
| 279 |
audio_path = None
|
| 280 |
|
| 281 |
+
# 2) First-frame mask via SAM2 (now multi-frame at original size)
|
| 282 |
mask = generate_first_frame_mask(input_path, sam2_predictor)
|
| 283 |
mask_path = str(temp_dir / "mask.png")
|
| 284 |
ok = cv2.imwrite(mask_path, mask)
|
|
|
|
| 289 |
# 3) MatAnyone in a thread with timeout (prevents silent hangs)
|
| 290 |
logger.info(
|
| 291 |
f"[stage1] Starting MatAnyone.process_video "
|
| 292 |
+
f"(input_path={input_path}, mask_path={mask_path}, output_path={temp_dir,}, max_size=720)"
|
| 293 |
)
|
| 294 |
|
| 295 |
result_holder = {"ok": False, "fg": None, "alpha": None, "exc": None}
|