MogensR commited on
Commit
c71f4a2
·
verified ·
1 Parent(s): c62fafc

Update processing/two_stage/two_stage_processor.py

Browse files
processing/two_stage/two_stage_processor.py CHANGED
@@ -1,279 +1,242 @@
1
  #!/usr/bin/env python3
2
  """
3
- MatAnyone Adapter - Robust interface handler for InferenceCore
4
- Handles unknown process_video signatures and provides fallback to step-based processing
 
 
 
 
 
 
 
5
  """
6
 
7
- import os
8
- import cv2
9
- import inspect
10
- import logging
11
  import numpy as np
12
- from typing import Optional, Union
13
 
14
  logger = logging.getLogger(__name__)
15
 
16
- # ---------- Helpers
17
-
18
- def _read_mask_01(path: str, wh=None) -> np.ndarray:
19
- """Read a mask/trimap and return float32 0..1 alpha guidance."""
20
- m = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
21
- if m is None:
22
- raise ValueError(f"Reference mask/trimap not found: {path}")
23
-
24
- # Trimap heuristic: values clustered at {0, ~128, 255}
25
- unique = np.unique(m)
26
- if len(unique) <= 4 and 120 in unique or any(110 <= v <= 140 for v in unique):
27
- # Treat 128± as 'unknown'
28
- a = np.zeros_like(m, dtype=np.float32)
29
- a[m >= 200] = 1.0 # FG
30
- a[(m > 50) & (m < 200)] = 0.5 # Unknown band → 0.5 as neutral prior
31
- else:
32
- # Binary / soft mask → normalize
33
- a = m.astype(np.float32) / 255.0
34
-
35
- if wh is not None and (m.shape[1], m.shape[0]) != wh:
36
- a = cv2.resize(a, wh, interpolation=cv2.INTER_LINEAR)
37
- return np.clip(a, 0.0, 1.0).astype(np.float32)
38
-
39
- def _to_chw01(img_bgr: np.ndarray) -> np.ndarray:
40
- """BGR uint8 HxWx3 -> CHW float32 0..1"""
41
- x = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
42
- return np.transpose(x, (2, 0, 1)).copy()
43
-
44
- def _ensure_dir(d: str) -> str:
45
- os.makedirs(d, exist_ok=True)
46
- return d
47
-
48
- # ---------- Primary entry
49
-
50
- def run_matanyone_with_reference_mask(
51
- inference_core, # your InferenceCore instance
52
- video_path: str,
53
- reference_mask_path: str, # binary mask or trimap
54
- output_dir: str,
55
- *,
56
- fps_override: Optional[float] = None,
57
- write_png: bool = True,
58
- ) -> str:
59
- """
60
- Run MatAnyone with reference mask using robust interface detection.
61
-
62
- Returns: path to alpha PNG directory (if write_png) or alpha video produced by process_video
63
- """
64
- _ensure_dir(output_dir)
65
-
66
- # 1) Try process_video with best-effort argument mapping
67
- proc = getattr(inference_core, "process_video", None)
68
- if callable(proc):
69
- sig = inspect.signature(proc)
70
- params = list(sig.parameters.keys())
71
- logger.info(f"MatAnyone.process_video signature: {params}")
72
-
73
- # Common arg names we'll look for
74
- argmap = {}
75
- for p in params:
76
- lp = p.lower()
77
- if "video" in lp and ("path" in lp or lp == "video"):
78
- argmap[p] = video_path
79
- elif ("trimap" in lp) or ("mask" in lp and "ref" in lp):
80
- argmap[p] = reference_mask_path
81
- elif ("mask" == lp) or ("ref_mask" == lp):
82
- argmap[p] = reference_mask_path
83
- elif ("out" in lp and ("dir" in lp or "path" in lp)):
84
- argmap[p] = output_dir
85
-
86
- # If we at least matched video + (mask/trimap) + out, try it
87
- has_video = any(k for k,v in argmap.items() if v==video_path)
88
- has_mask = any(k for k,v in argmap.items() if v==reference_mask_path)
89
- has_out = any(k for k,v in argmap.items() if v==output_dir)
90
-
91
- if has_video and has_mask and has_out:
92
- try:
93
- logger.info(f"Calling process_video with mapped args: {argmap}")
94
- result = proc(**argmap) # may return a path or None
95
- # If it produced an alpha video, we'll convert to PNGs for safety
96
- produced = _detect_alpha_artifact(output_dir)
97
- if produced and produced.lower().endswith(".mp4"):
98
- alpha_dir = os.path.join(output_dir, "alpha_png")
99
- _mp4_to_alpha_png(produced, alpha_dir)
100
- return alpha_dir
101
- elif produced and os.path.isdir(produced):
102
- # Already a folder of frames
103
- return produced
104
- else:
105
- # Nothing obvious—fall back to manual pipeline
106
- logger.warning("process_video returned no detectable alpha; falling back to step()")
107
- except Exception as e:
108
- logger.warning(f"process_video failed ({e}); falling back to step().")
109
-
110
- # 2) Fallback: step-based pipeline (canonical)
111
- logger.info("Using step-based pipeline: init with first frame (image+mask), then step(image) ...")
112
-
113
- cap = cv2.VideoCapture(video_path)
114
- if not cap.isOpened():
115
- raise ValueError(f"Cannot open video: {video_path}")
116
- fps = fps_override or cap.get(cv2.CAP_PROP_FPS) or 25
117
- w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
118
- h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
119
-
120
- # first frame
121
- ok, frame = cap.read()
122
- if not ok:
123
- cap.release()
124
- raise ValueError("Could not read first frame")
125
 
126
- # Prepare inputs
127
- alpha0 = _read_mask_01(reference_mask_path, wh=(w, h))
128
- img0 = _to_chw01(frame) # CHW 0..1
 
129
 
130
- # Initialize model session
131
- if hasattr(inference_core, "reset"):
132
- inference_core.reset()
133
 
134
- # Many video-matting cores use 'step(image, mask=None)' where mask only on first call
135
- try:
136
- _maybe_step_with_mask(inference_core, img0, alpha0)
137
- except Exception as e:
138
- cap.release()
139
- raise RuntimeError(f"MatAnyone step(init) failed: {e}")
 
 
140
 
141
- # Output dir
142
- alpha_dir = _ensure_dir(os.path.join(output_dir, "alpha_png"))
143
 
144
- # Write first alpha
145
- _write_alpha_png(alpha0, os.path.join(alpha_dir, f"alpha_{0:06d}.png"))
146
 
147
- i = 1
148
- while True:
149
- ok, frame = cap.read()
150
- if not ok:
151
- break
152
- img = _to_chw01(frame)
 
 
153
  try:
154
- alpha_i = _maybe_step(inference_core, img) # should return HxW float 0..1
155
  except Exception as e:
156
- cap.release()
157
- raise RuntimeError(f"MatAnyone step(frame={i}) failed: {e}")
158
-
159
- _write_alpha_png(alpha_i, os.path.join(alpha_dir, f"alpha_{i:06d}.png"))
160
- i += 1
161
-
162
- cap.release()
163
- logger.info(f"Wrote {i} alpha PNG frames to {alpha_dir}")
164
- return alpha_dir
165
-
166
- # ---------- Fallback/IO utilities
167
-
168
- def _maybe_step_with_mask(core, img_chw01: np.ndarray, mask_01: np.ndarray):
169
- """
170
- Initialize MatAnyone with first frame using step(image, mask/trimap).
171
-
172
- FIXED: Removed encode_all_images branch that was causing signature errors.
173
- """
174
- step = getattr(core, "step", None)
175
- if not callable(step):
176
- raise RuntimeError("MatAnyone core has no callable 'step' method")
177
-
178
- try:
179
- # Try step(image, mask) - most common pattern for video matting
180
- return step(img_chw01, mask_01)
181
- except TypeError as e:
182
- logger.warning(f"step(image, mask) failed: {e}")
183
-
184
- # Try with keyword arguments
185
- sig = inspect.signature(step)
186
- names = list(sig.parameters.keys())
187
-
188
- if "image" in names and "mask" in names:
189
- return step(image=img_chw01, mask=mask_01)
190
- elif "image" in names and "trimap" in names:
191
- return step(image=img_chw01, trimap=mask_01)
192
- elif "guidance" in names:
193
- return step(img_chw01, guidance=mask_01)
194
- else:
195
- # Last resort: try positional with single image (no mask)
196
- logger.warning("Could not pass mask to step(), trying image only")
197
- return step(img_chw01)
198
-
199
- def _maybe_step(core, img_chw01: np.ndarray) -> np.ndarray:
200
- """Call step on later frames (no mask). Expect an alpha or prob map; convert to 0..1 HxW."""
201
- out = core.step(img_chw01)
202
- # Some implementations return dict or tuple; try to extract alpha/prob
203
- if isinstance(out, dict):
204
- for k in ("alpha", "mask", "prob", "pha"):
205
- if k in out:
206
- out = out[k]
207
- break
208
- out = _prob_to_alpha01(core, out)
209
- return out
210
-
211
- def _prob_to_alpha01(core, x) -> np.ndarray:
212
- """Convert model output to HxW float32 0..1; use core.output_prob_to_mask if present."""
213
- fn = getattr(core, "output_prob_to_mask", None)
214
- if callable(fn):
215
- try:
216
- y = fn(x)
217
- y = np.asarray(y, dtype=np.float32)
218
- if y.ndim == 3 and y.shape[0] in (1,3):
219
- # CHW -> HW (use single channel)
220
- y = y[0] if y.shape[0] == 1 else np.mean(y, axis=0)
221
- return np.clip(y, 0.0, 1.0)
222
- except Exception:
223
- pass
224
-
225
- # Fallback: assume x is HW or CHW logits/prob
226
- a = np.asarray(x, dtype=np.float32)
227
- if a.ndim == 3 and a.shape[0] in (1,3):
228
- a = a[0] if a.shape[0] == 1 else np.mean(a, axis=0)
229
-
230
- # If looks like logits, sigmoid
231
- if a.max() > 1.5 or a.min() < 0.0:
232
- a = 1.0 / (1.0 + np.exp(-a))
233
- return np.clip(a, 0.0, 1.0)
234
-
235
- def _write_alpha_png(alpha01: np.ndarray, path: str):
236
- """Write alpha as PNG."""
237
- a = np.clip(alpha01, 0.0, 1.0).astype(np.float32)
238
- a = (a * 255.0 + 0.5).astype(np.uint8)
239
- cv2.imwrite(path, a)
240
-
241
- def _mp4_to_alpha_png(alpha_mp4: str, out_dir: str) -> str:
242
- """Convert alpha MP4 to PNG sequence."""
243
- os.makedirs(out_dir, exist_ok=True)
244
- cap = cv2.VideoCapture(alpha_mp4)
245
- if not cap.isOpened():
246
- raise ValueError(f"Could not open alpha mp4: {alpha_mp4}")
247
-
248
- i = 0
249
- while True:
250
- ok, frame = cap.read()
251
- if not ok:
252
- break
253
- if frame.ndim == 3:
254
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
255
- cv2.imwrite(os.path.join(out_dir, f"alpha_{i:06d}.png"), frame)
256
- i += 1
257
- cap.release()
258
- logger.info(f"Converted {i} frames from MP4 to PNG")
259
- return out_dir
260
-
261
- def _detect_alpha_artifact(output_dir: str) -> Optional[str]:
262
- """Find a likely alpha artifact produced by process_video (mp4 or a folder)."""
263
- if not os.path.exists(output_dir):
264
- return None
265
-
266
- # prefer *_alpha.mp4 / pha.mp4
267
- for name in os.listdir(output_dir):
268
- low = name.lower()
269
- if low.endswith(".mp4") and ("alpha" in low or "pha" in low):
270
- return os.path.join(output_dir, name)
271
- if low.startswith("alpha_") and low.endswith(".png"):
272
- # frames were written flat in output_dir
273
- return output_dir
274
-
275
- # any mp4?
276
- for name in os.listdir(output_dir):
277
- if name.lower().endswith(".mp4"):
278
- return os.path.join(output_dir, name)
279
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ TRUE Two-Stage Green-Screen Processing System ✅
4
+
5
+ Stage 1: Original → SAM2(ref mask/trimap) + MatAnyone → ALPHA PNGs → GREEN master (H.264)
6
+ Stage 2: Key GREEN master → Final composite
7
+
8
+ Safe for HF Spaces:
9
+ - No ProRes (uses mp4v for GREEN master)
10
+ - No MoviePy apply_to=['mask'] (prevents black output)
11
+ - Alpha stays lossless as PNG sequence
12
  """
13
 
14
+ import os, cv2, time, logging, tempfile, traceback
15
+ from pathlib import Path
16
+ from typing import Optional, Tuple, Callable, List
 
17
  import numpy as np
18
+ from moviepy.editor import VideoFileClip
19
 
20
  logger = logging.getLogger(__name__)
21
 
22
+ # =========================
23
+ # Constants (RGB colors)
24
+ # =========================
25
+ CHROMA_PRESETS = {
26
+ "office": {"name": "Professional Office", "color": (240, 240, 240)},
27
+ "blue": {"name": "Studio Blue", "color": (70, 130, 180)},
28
+ "green": {"name": "Chroma Green", "color": (0, 177, 64)},
29
+ "white": {"name": "Clean White", "color": (255, 255, 255)},
30
+ "gradient": {"name": "Soft Gradient", "color": (200, 220, 255)}
31
+ }
32
+ PURE_GREEN_RGB = (0, 255, 0)
33
+ DEFAULT_REFERENCE_FRAMES = 3
34
+ TEMP_DIR_PREFIX = "twostage_greenscreen_"
35
+
36
+ def get_quality_profile():
37
+ # keep minimal: only keying knobs if you want to expose later
38
+ class P:
39
+ chroma_tolerance = 25.0
40
+ chroma_softness = 3.0
41
+ spill_suppression = 0.6
42
+ edge_feather = 8
43
+ return P()
44
+
45
+ class TwoStageProcessor:
46
+ def __init__(self, sam2_handler, matanyone_processor):
47
+ logger.info("Initializing True Two-Stage Green Screen Processor…")
48
+ self.sam2_handler = sam2_handler
49
+ self.matanyone_processor = matanyone_processor
50
+ self.temp_dir: Optional[str] = None
51
+ self.quality_profile = get_quality_profile()
52
+
53
+ # ------------------------------------------------------------
54
+
55
+ def process_video(
56
+ self,
57
+ video_path: str,
58
+ background_image_path: Optional[str] = None,
59
+ background_preset: str = "office",
60
+ callback: Optional[Callable] = None,
61
+ trim_seconds: float = 5.0
62
+ ) -> Tuple[Optional[str], Optional[str], str]:
63
+ start = time.time()
64
+ self.temp_dir = tempfile.mkdtemp(prefix=TEMP_DIR_PREFIX)
65
+
66
+ try:
67
+ logger.info(f"🎬 TRUE Two-Stage: {video_path}")
68
+ logger.info(f"📁 Temp: {self.temp_dir}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ green_master = self._stage1_create_greenscreen_video(video_path, trim_seconds)
71
+ final_out = self._stage2_chroma_key_replacement(
72
+ green_master, background_image_path, background_preset
73
+ )
74
 
75
+ status = "✅ Two-stage (GREEN master → Key) completed."
76
+ logger.info(f"{status} Output: {final_out}")
77
+ return final_out, green_master, status
78
 
79
+ except Exception as e:
80
+ logger.error(f"Processor failed: {e}")
81
+ logger.error(traceback.format_exc())
82
+ return None, None, f"Processing failed: {e}"
83
+
84
+ # =========================
85
+ # STAGE 1: GREEN MASTER
86
+ # =========================
87
 
88
+ def _stage1_create_greenscreen_video(self, video_path: str, trim_seconds: float) -> str:
89
+ logger.info("STAGE 1: Creating GREEN master via SAM2 + MatAnyone …")
90
 
91
+ ref_mask = self._create_reference_mask(video_path)
92
+ trimap = self._create_trimap(ref_mask)
93
 
94
+ # Trim source (codec here is fine; this is not the matte)
95
+ trimmed = os.path.join(self.temp_dir, "trimmed_src.mp4")
96
+ with VideoFileClip(video_path) as clip:
97
+ sub = clip.subclip(0, min(trim_seconds, clip.duration))
98
+ sub.write_videofile(trimmed, codec="libx264", audio_codec="aac",
99
+ verbose=False, logger=None)
100
+
101
+ # MatAnyone → alpha PNGs (lossless)
102
  try:
103
+ from .matanyone_adapter import run_matanyone_with_reference_mask
104
  except Exception as e:
105
+ raise ImportError(f"matanyone_adapter missing or broken: {e}")
106
+
107
+ out_dir = os.path.join(self.temp_dir, "matanyone_out")
108
+ os.makedirs(out_dir, exist_ok=True)
109
+
110
+ alpha_dir = run_matanyone_with_reference_mask(
111
+ inference_core=self.matanyone_processor,
112
+ video_path=trimmed,
113
+ reference_mask_path=trimap,
114
+ output_dir=out_dir,
115
+ write_png=True
116
+ )
117
+ if not alpha_dir or not os.path.isdir(alpha_dir):
118
+ raise RuntimeError("MatAnyone adapter did not create alpha PNG sequence")
119
+
120
+ # Convert alpha PNGs + source RGB → GREEN master (safe mp4v)
121
+ green_master = self._alpha_png_to_green_master(trimmed, alpha_dir)
122
+ logger.info(f"✅ GREEN master: {green_master}")
123
+ return green_master
124
+
125
+ def _create_reference_mask(self, video_path: str) -> str:
126
+ cap = cv2.VideoCapture(video_path)
127
+ if not cap.isOpened():
128
+ raise ValueError(f"Cannot open video: {video_path}")
129
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
130
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
131
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
132
+
133
+ masks: List[np.ndarray] = []
134
+ for i in range(min(DEFAULT_REFERENCE_FRAMES, max(1, total))):
135
+ cap.set(cv2.CAP_PROP_POS_FRAMES, i)
136
+ ok, frame_bgr = cap.read()
137
+ if not ok: continue
138
+ frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
139
+ m = self.sam2_handler.create_mask(frame_rgb)
140
+ if m is None: continue
141
+ m = m.astype(np.float32)
142
+ if m.max() > 1.01: m /= 255.0
143
+ m = np.clip(m, 0, 1)
144
+ masks.append(m)
145
+ logger.info(f"Reference frame {i+1} processed")
146
+ cap.release()
147
+
148
+ if not masks:
149
+ # ellipse fallback
150
+ y, x = np.ogrid[:h, :w]
151
+ cy, cx = h//2, w//2
152
+ ry, rx = int(h*0.32), int(w*0.22)
153
+ fallback = (((x-cx)/rx)**2 + ((y-cy)/ry)**2 <= 1).astype(np.float32)
154
+ masks = [fallback]
155
+
156
+ comp = np.mean(np.stack(masks, 0), axis=0).astype(np.float32)
157
+ comp = np.clip(comp, 0, 1)
158
+ path = os.path.join(self.temp_dir, "reference_mask.png")
159
+ cv2.imwrite(path, (comp * 255.0 + 0.5).astype(np.uint8))
160
+ logger.info(f"Reference mask saved: {path}")
161
+ return path
162
+
163
+ def _create_trimap(self, mask_png: str) -> str:
164
+ m = cv2.imread(mask_png, cv2.IMREAD_GRAYSCALE)
165
+ if m is None:
166
+ raise ValueError(f"Mask not found: {mask_png}")
167
+ m01 = (m.astype(np.float32) / 255.0)
168
+
169
+ tri = np.zeros_like(m, dtype=np.uint8)
170
+ tri[m01 > 0.85] = 255
171
+ tri[m01 < 0.15] = 0
172
+ tri[(m01 >= 0.15) & (m01 <= 0.85)] = 128
173
+
174
+ # widen unknown band around edges
175
+ k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
176
+ edges = cv2.Canny(m, 40, 120)
177
+ tri[cv2.dilate(edges, k, 1) > 0] = 128
178
+
179
+ out = os.path.join(self.temp_dir, "trimap.png")
180
+ cv2.imwrite(out, tri)
181
+ logger.info(f"Trimap created: {out}")
182
+ return out
183
+
184
+ def _alpha_png_to_green_master(self, rgb_video: str, alpha_dir: str) -> str:
185
+ # Gather PNGs
186
+ pngs = sorted(p for p in os.listdir(alpha_dir) if p.startswith("alpha_") and p.endswith(".png"))
187
+ if not pngs: raise RuntimeError(f"No alpha PNGs in {alpha_dir}")
188
+
189
+ cap = cv2.VideoCapture(rgb_video)
190
+ if not cap.isOpened(): raise ValueError(f"Cannot open {rgb_video}")
191
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25
192
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
193
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
194
+
195
+ out_path = os.path.join(self.temp_dir, "greenscreen_master.mp4")
196
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v') # safe on Spaces
197
+ out = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
198
+ if not out.isOpened():
199
+ raise RuntimeError("Failed to open VideoWriter for GREEN master (mp4v)")
200
+
201
+ i = 0
202
+ last_alpha = np.ones((h, w), dtype=np.float32)
203
+ while True:
204
+ ok, frame_bgr = cap.read()
205
+ if not ok: break
206
+ # original frame in RGB for math
207
+ frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
208
+
209
+ if i < len(pngs):
210
+ a = cv2.imread(os.path.join(alpha_dir, pngs[i]), cv2.IMREAD_GRAYSCALE)
211
+ if a is not None:
212
+ if (a.shape[1], a.shape[0]) != (w, h):
213
+ a = cv2.resize(a, (w, h), interpolation=cv2.INTER_LINEAR)
214
+ last_alpha = np.clip(a.astype(np.float32)/255.0, 0, 1)
215
+
216
+ a3 = np.dstack([last_alpha]*3)
217
+ green_rgb = np.array(PURE_GREEN_RGB, dtype=np.float32)/255.0
218
+ green_img = np.ones_like(frame_rgb) * green_rgb
219
+
220
+ mixed_rgb = a3*frame_rgb + (1.0 - a3)*green_img
221
+ mixed_bgr = cv2.cvtColor((np.clip(mixed_rgb*255.0,0,255)).astype(np.uint8), cv2.COLOR_RGB2BGR)
222
+
223
+ # Debug probe for "black video" diagnosis (first frame)
224
+ if i == 0:
225
+ cv2.imwrite(os.path.join(self.temp_dir, "probe_green_000.jpg"), mixed_bgr)
226
+ logger.info("Saved probe_green_000.jpg")
227
+
228
+ out.write(mixed_bgr)
229
+ i += 1
230
+
231
+ cap.release()
232
+ out.release()
233
+ logger.info(f"GREEN master written: {out_path}")
234
+ return out_path
235
+
236
+ # =========================
237
+ # STAGE 2: KEY & COMPOSITE
238
+ # =========================
239
+
240
+ def _stage2_chroma_key_replacement(self, green_master: str, bg_image: Optional[str], preset: str) -> str:
241
+ # Background as RGB
242
+ if bg_image and os.path.exists(bg_image):