MogensR commited on
Commit
afc48ea
·
verified ·
1 Parent(s): 3c509a3

Update pipeline/video_pipeline.py

Browse files
Files changed (1) hide show
  1. 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 (now returns mask at ORIGINAL video size) ---
109
- def generate_first_frame_mask(video_path, predictor):
110
- """Generate segmentation mask for the first frame using SAM2, then upsample to original size."""
 
 
 
 
 
 
111
  cap = cv2.VideoCapture(video_path)
112
- ret, frame = cap.read()
113
- cap.release()
 
 
114
  if not ret:
 
115
  raise ValueError("Failed to read video frame")
 
 
116
 
117
- orig_h, orig_w = frame.shape[:2]
118
-
119
- # Downscale for inference if needed (≤1080 on the longer side)
120
- scaled = frame
121
- if max(orig_h, orig_w) > 1080:
122
- scale = 1080 / max(orig_h, orig_w)
123
- new_w, new_h = int(orig_w * scale), int(orig_h * scale)
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
- # Predict mask on the (possibly) downscaled frame
131
  with torch.inference_mode(), torch.autocast(
132
  "cuda", dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32
133
  ):
134
- predictor.set_image(cv2.cvtColor(scaled, cv2.COLOR_BGR2RGB))
135
- h_s, w_s = scaled.shape[:2]
136
- masks, scores, _ = predictor.predict(
137
- point_coords=np.array([[w_s // 2, h_s // 2]]),
138
- point_labels=np.array([1]),
139
- multimask_output=True,
140
- )
141
-
142
- # Pick best mask and convert to 0/255 uint8
143
- mask_small = (masks[np.argmax(scores)] * 255).astype(np.uint8)
144
-
145
- # Upsample back to ORIGINAL video resolution (nearest-neighbor to preserve edges)
146
- if (new_w, new_h) != (orig_w, orig_h):
147
- mask_full = cv2.resize(mask_small, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
148
- logger.info(f"[sam2] Upsampled mask to original: {new_w}x{new_h} -> {orig_w}x{orig_h}")
149
- else:
150
- mask_full = mask_small
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
- return mask_full
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}