MogensR commited on
Commit
fd66920
·
1 Parent(s): 58a43ef

Update utils/cv_processing.py

Browse files
Files changed (1) hide show
  1. utils/cv_processing.py +140 -129
utils/cv_processing.py CHANGED
@@ -1,13 +1,13 @@
1
  #!/usr/bin/env python3
2
  """
3
- cv_processing.py · FIXED VERSION with proper SAM2 handling
4
  """
5
 
6
  from __future__ import annotations
7
 
8
  import logging
9
  from pathlib import Path
10
- from typing import Any, Dict, Optional, Tuple
11
 
12
  import cv2
13
  import numpy as np
@@ -37,6 +37,24 @@ def _ensure_rgb(img: np.ndarray) -> np.ndarray:
37
  return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
38
  return img
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  def _to_mask01(m: np.ndarray) -> np.ndarray:
41
  if m is None:
42
  return None
@@ -47,6 +65,36 @@ def _to_mask01(m: np.ndarray) -> np.ndarray:
47
  m = m / 255.0
48
  return np.clip(m, 0.0, 1.0)
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def _feather(mask01: np.ndarray, k: int = 2) -> np.ndarray:
51
  if mask01.ndim == 3:
52
  mask01 = mask01[..., 0]
@@ -90,38 +138,29 @@ def create_professional_background(key_or_cfg: Any, width: int, height: int) ->
90
  def _simple_person_segmentation(frame_bgr: np.ndarray) -> np.ndarray:
91
  """Basic fallback segmentation using color detection"""
92
  h, w = frame_bgr.shape[:2]
93
-
94
- # Convert to HSV for better color detection
95
  hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)
96
-
97
- # Detect skin tones (basic person detection)
98
  lower_skin = np.array([0, 20, 70], dtype=np.uint8)
99
  upper_skin = np.array([20, 255, 255], dtype=np.uint8)
100
  skin_mask = cv2.inRange(hsv, lower_skin, upper_skin)
101
-
102
- # Also detect non-green/non-white areas as potential person
103
  lower_green = np.array([40, 40, 40], dtype=np.uint8)
104
  upper_green = np.array([80, 255, 255], dtype=np.uint8)
105
  green_mask = cv2.inRange(hsv, lower_green, upper_green)
106
-
107
- # Assume person is NOT green screen
108
  person_mask = cv2.bitwise_not(green_mask)
109
-
110
- # Combine with skin detection
111
  person_mask = cv2.bitwise_or(person_mask, skin_mask)
112
-
113
- # Clean up the mask
114
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
115
  person_mask = cv2.morphologyEx(person_mask, cv2.MORPH_CLOSE, kernel, iterations=2)
116
  person_mask = cv2.morphologyEx(person_mask, cv2.MORPH_OPEN, kernel, iterations=1)
117
-
118
- # Find largest contour (assume it's the person)
119
  contours, _ = cv2.findContours(person_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
120
  if contours:
121
  largest_contour = max(contours, key=cv2.contourArea)
122
  person_mask = np.zeros_like(person_mask)
123
  cv2.drawContours(person_mask, [largest_contour], -1, 255, -1)
124
-
125
  return (person_mask.astype(np.float32) / 255.0)
126
 
127
  def segment_person_hq(
@@ -135,50 +174,32 @@ def segment_person_hq(
135
  High-quality person segmentation with proper SAM2 handling
136
  """
137
  h, w = frame.shape[:2]
138
-
139
- # Skip SAM2 if explicitly disabled
140
  if use_sam2 is False:
141
  return _simple_person_segmentation(frame)
142
-
143
- # Try SAM2 if available
144
  if predictor is not None:
145
  try:
146
- # Ensure we have the right methods
147
  if hasattr(predictor, "set_image") and hasattr(predictor, "predict"):
148
- # Convert to RGB for SAM2
149
  rgb = _ensure_rgb(frame)
150
-
151
- # Set the image
152
  predictor.set_image(rgb)
153
-
154
- # Generate multiple prompt points for better coverage
155
  points = []
156
  labels = []
157
-
158
- # Add center point
159
- points.append([w // 2, h // 2])
160
- labels.append(1) # Foreground
161
-
162
- # Add points for head area (upper center)
163
- points.append([w // 2, h // 4])
164
- labels.append(1)
165
-
166
- # Add body points
167
- points.append([w // 2, h // 2 + h // 8])
168
- labels.append(1)
169
-
170
- # Convert to numpy arrays
171
  point_coords = np.array(points, dtype=np.float32)
172
  point_labels = np.array(labels, dtype=np.int32)
173
-
174
- # Predict with multiple masks
175
  result = predictor.predict(
176
  point_coords=point_coords,
177
  point_labels=point_labels,
178
  multimask_output=True
179
  )
180
-
181
- # Extract masks and scores
182
  if isinstance(result, dict):
183
  masks = result.get("masks", None)
184
  scores = result.get("scores", None)
@@ -187,118 +208,121 @@ def segment_person_hq(
187
  else:
188
  masks = result
189
  scores = None
190
-
191
- # Validate and process masks
192
  if masks is not None:
193
  masks = np.array(masks)
194
-
195
- if masks.size > 0: # Check if not empty
196
- # Handle different mask shapes
197
  if masks.ndim == 3 and masks.shape[0] > 0:
198
- # Multiple masks - choose best one
199
  if scores is not None and len(scores) > 0:
200
  best_idx = np.argmax(scores)
201
  mask = masks[best_idx]
202
  else:
203
- # Use first mask if no scores
204
  mask = masks[0]
205
  elif masks.ndim == 2:
206
- # Single mask
207
  mask = masks
208
  else:
209
  logger.warning(f"Unexpected mask shape from SAM2: {masks.shape}")
210
  mask = None
211
-
212
  if mask is not None:
213
- # Convert to proper format
214
  mask = _to_mask01(mask)
215
-
216
- # Validate mask has actual content
217
- if mask.max() > 0.1: # At least 10% confidence somewhere
218
  return mask
219
  else:
220
  logger.warning("SAM2 mask too weak, using fallback")
221
  else:
222
  logger.warning("SAM2 returned no masks")
223
-
224
  except Exception as e:
225
  logger.warning(f"SAM2 segmentation error: {e}")
226
-
227
- # Fallback to simple segmentation
228
  if fallback_enabled:
229
  logger.debug("Using fallback segmentation")
230
  return _simple_person_segmentation(frame)
231
  else:
232
- # Return full mask if no fallback
233
  return np.ones((h, w), dtype=np.float32)
234
 
235
  segment_person_hq_original = segment_person_hq
236
 
237
  # ----------------------------------------------------------------------------
238
- # MatAnyone Refinement (Fixed)
239
  # ----------------------------------------------------------------------------
240
  def refine_mask_hq(
241
  frame: np.ndarray,
242
  mask: np.ndarray,
243
- matanyone: Optional[Any] = None,
 
 
244
  fallback_enabled: bool = True,
245
  use_matanyone: Optional[bool] = None,
246
  **_compat_kwargs,
247
  ) -> np.ndarray:
248
  """
249
- Refine mask with MatAnyone - with proper handling
 
 
 
 
 
 
 
 
 
250
  """
251
- # Convert mask to proper format
252
  mask01 = _to_mask01(mask)
253
-
254
- # Skip MatAnyone if explicitly disabled
255
  if use_matanyone is False:
256
  return mask01
257
-
258
- # Try MatAnyone if available
259
- if matanyone is not None:
260
  try:
261
- # Try different MatAnyone interfaces
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  refined = None
263
-
264
- # Method 1: Direct callable
265
- if callable(matanyone):
266
- try:
267
- refined = matanyone(frame, mask01)
268
- if refined is not None:
269
- refined = _to_mask01(np.array(refined))
270
- except Exception as e:
271
- logger.debug(f"MatAnyone callable failed: {e}")
272
-
273
- # Method 2: step method
274
  if refined is None and hasattr(matanyone, 'step'):
275
  try:
276
- refined = matanyone.step(frame, mask01)
277
- if refined is not None:
278
- refined = _to_mask01(np.array(refined))
279
  except Exception as e:
280
  logger.debug(f"MatAnyone step failed: {e}")
281
-
282
- # Method 3: process method
283
  if refined is None and hasattr(matanyone, 'process'):
284
  try:
285
- refined = matanyone.process(frame, mask01)
286
- if refined is not None:
287
- refined = _to_mask01(np.array(refined))
288
  except Exception as e:
289
  logger.debug(f"MatAnyone process failed: {e}")
290
-
291
- # Use refined mask if successful
292
  if refined is not None and refined.max() > 0.1:
293
- # Apply post-processing
294
- refined = _postprocess_mask(refined)
295
- return refined
296
  else:
297
  logger.warning("MatAnyone refinement failed or produced empty mask")
298
-
299
  except Exception as e:
300
  logger.warning(f"MatAnyone error: {e}")
301
-
302
  # Fallback refinement
303
  if fallback_enabled:
304
  return _fallback_refine(mask01)
@@ -307,39 +331,31 @@ def refine_mask_hq(
307
 
308
  def _postprocess_mask(mask01: np.ndarray) -> np.ndarray:
309
  """Post-process mask to clean edges and remove artifacts"""
310
- # Convert to uint8
311
  mask_uint8 = (mask01 * 255).astype(np.uint8)
312
-
313
- # Remove small holes
314
  kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
315
  mask_uint8 = cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel_close)
316
-
317
- # Smooth edges
318
  mask_uint8 = cv2.GaussianBlur(mask_uint8, (3, 3), 0)
319
-
320
- # Threshold to clean up
321
  _, mask_uint8 = cv2.threshold(mask_uint8, 127, 255, cv2.THRESH_BINARY)
322
-
323
- # Final smooth
324
  mask_uint8 = cv2.GaussianBlur(mask_uint8, (5, 5), 1)
325
-
326
  return mask_uint8.astype(np.float32) / 255.0
327
 
328
  def _fallback_refine(mask01: np.ndarray) -> np.ndarray:
329
  """Simple fallback refinement"""
330
  mask_uint8 = (mask01 * 255).astype(np.uint8)
331
-
332
- # Bilateral filter for edge-preserving smoothing
333
  mask_uint8 = cv2.bilateralFilter(mask_uint8, 9, 75, 75)
334
-
335
- # Morphological operations
336
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
337
  mask_uint8 = cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel)
338
  mask_uint8 = cv2.morphologyEx(mask_uint8, cv2.MORPH_OPEN, kernel)
339
-
340
- # Edge feathering
341
  mask_uint8 = cv2.GaussianBlur(mask_uint8, (5, 5), 1)
342
-
343
  return mask_uint8.astype(np.float32) / 255.0
344
 
345
  # ----------------------------------------------------------------------------
@@ -355,25 +371,20 @@ def replace_background_hq(
355
  """High-quality background replacement with alpha blending"""
356
  try:
357
  H, W = frame.shape[:2]
358
-
359
- # Resize background if needed
360
  if background.shape[:2] != (H, W):
361
  background = cv2.resize(background, (W, H), interpolation=cv2.INTER_LANCZOS4)
362
-
363
- # Ensure mask is properly formatted
364
- m = _to_mask01(mask01)
365
-
366
- # Apply slight feather for smooth edges
367
  m = _feather(m, k=1)
368
-
369
- # Convert to 3-channel for multiplication
370
  m3 = np.repeat(m[:, :, None], 3, axis=2)
371
-
372
- # Alpha blending
373
  comp = frame.astype(np.float32) * m3 + background.astype(np.float32) * (1.0 - m3)
374
-
375
  return np.clip(comp, 0, 255).astype(np.uint8)
376
-
377
  except Exception as e:
378
  if fallback_enabled:
379
  logger.warning(f"Compositing failed ({e}) – returning original frame")
@@ -432,4 +443,4 @@ def validate_video_file(video_path: str) -> Tuple[bool, str]:
432
  "create_professional_background",
433
  "validate_video_file",
434
  "PROFESSIONAL_BACKGROUNDS",
435
- ]
 
1
  #!/usr/bin/env python3
2
  """
3
+ cv_processing.py · FIXED VERSION with proper SAM2 handling + MatAnyone stateful integration
4
  """
5
 
6
  from __future__ import annotations
7
 
8
  import logging
9
  from pathlib import Path
10
+ from typing import Any, Dict, Optional, Tuple, Callable
11
 
12
  import cv2
13
  import numpy as np
 
37
  return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
38
  return img
39
 
40
+ def _ensure_rgb01(frame_bgr: np.ndarray) -> np.ndarray:
41
+ """
42
+ Convert BGR uint8 [H,W,3] to RGB float32 in [0,1].
43
+ Accepts a variety of layouts and coerces safely to HWC.
44
+ """
45
+ if frame_bgr is None:
46
+ raise ValueError("frame_bgr is None")
47
+ x = frame_bgr
48
+ if x.ndim == 2:
49
+ x = np.stack([x, x, x], axis=-1) # gray -> 3ch
50
+ # channels-first -> HWC
51
+ if x.ndim == 3 and x.shape[0] in (1, 3, 4) and x.shape[-1] not in (1, 3, 4):
52
+ x = np.transpose(x, (1, 2, 0))
53
+ if x.dtype != np.uint8:
54
+ x = np.clip(x, 0, 255).astype(np.uint8)
55
+ rgb = cv2.cvtColor(x, cv2.COLOR_BGR2RGB)
56
+ return (rgb.astype(np.float32) / 255.0).copy()
57
+
58
  def _to_mask01(m: np.ndarray) -> np.ndarray:
59
  if m is None:
60
  return None
 
65
  m = m / 255.0
66
  return np.clip(m, 0.0, 1.0)
67
 
68
+ def _mask_to_2d(mask: np.ndarray) -> np.ndarray:
69
+ """
70
+ Reduce any mask to 2-D float32 [H,W], contiguous, in [0,1].
71
+ Handles HWC/CHW/B1HW/1HW/HW, etc.
72
+ """
73
+ m = np.asarray(mask)
74
+ # channels-first 1xHxW
75
+ if m.ndim == 3 and m.shape[0] == 1 and (m.shape[1] > 1 and m.shape[2] > 1):
76
+ m = m[0]
77
+ # channels-last HxWx1
78
+ if m.ndim == 3 and m.shape[-1] == 1:
79
+ m = m[..., 0]
80
+ # multi-channel -> take first channel
81
+ if m.ndim == 3:
82
+ m = m[..., 0] if m.shape[-1] in (1, 3, 4) else m[0]
83
+ # squeeze anything left
84
+ m = np.squeeze(m)
85
+ if m.ndim != 2:
86
+ h = int(m.shape[-2]) if m.ndim >= 2 else 512
87
+ w = int(m.shape[-1]) if m.ndim >= 2 else 512
88
+ logger.warning(f"_mask_to_2d: unexpected shape {mask.shape}, creating neutral mask.")
89
+ m = np.full((h, w), 0.5, dtype=np.float32)
90
+ # dtype/range
91
+ if m.dtype == np.uint8:
92
+ m = m.astype(np.float32) / 255.0
93
+ elif m.dtype != np.float32:
94
+ m = m.astype(np.float32)
95
+ m = np.clip(m, 0.0, 1.0)
96
+ return np.ascontiguousarray(m)
97
+
98
  def _feather(mask01: np.ndarray, k: int = 2) -> np.ndarray:
99
  if mask01.ndim == 3:
100
  mask01 = mask01[..., 0]
 
138
  def _simple_person_segmentation(frame_bgr: np.ndarray) -> np.ndarray:
139
  """Basic fallback segmentation using color detection"""
140
  h, w = frame_bgr.shape[:2]
 
 
141
  hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)
142
+
 
143
  lower_skin = np.array([0, 20, 70], dtype=np.uint8)
144
  upper_skin = np.array([20, 255, 255], dtype=np.uint8)
145
  skin_mask = cv2.inRange(hsv, lower_skin, upper_skin)
146
+
 
147
  lower_green = np.array([40, 40, 40], dtype=np.uint8)
148
  upper_green = np.array([80, 255, 255], dtype=np.uint8)
149
  green_mask = cv2.inRange(hsv, lower_green, upper_green)
150
+
 
151
  person_mask = cv2.bitwise_not(green_mask)
 
 
152
  person_mask = cv2.bitwise_or(person_mask, skin_mask)
153
+
 
154
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
155
  person_mask = cv2.morphologyEx(person_mask, cv2.MORPH_CLOSE, kernel, iterations=2)
156
  person_mask = cv2.morphologyEx(person_mask, cv2.MORPH_OPEN, kernel, iterations=1)
157
+
 
158
  contours, _ = cv2.findContours(person_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
159
  if contours:
160
  largest_contour = max(contours, key=cv2.contourArea)
161
  person_mask = np.zeros_like(person_mask)
162
  cv2.drawContours(person_mask, [largest_contour], -1, 255, -1)
163
+
164
  return (person_mask.astype(np.float32) / 255.0)
165
 
166
  def segment_person_hq(
 
174
  High-quality person segmentation with proper SAM2 handling
175
  """
176
  h, w = frame.shape[:2]
177
+
 
178
  if use_sam2 is False:
179
  return _simple_person_segmentation(frame)
180
+
 
181
  if predictor is not None:
182
  try:
 
183
  if hasattr(predictor, "set_image") and hasattr(predictor, "predict"):
 
184
  rgb = _ensure_rgb(frame)
 
 
185
  predictor.set_image(rgb)
186
+
 
187
  points = []
188
  labels = []
189
+
190
+ points.append([w // 2, h // 2]); labels.append(1)
191
+ points.append([w // 2, h // 4]); labels.append(1)
192
+ points.append([w // 2, h // 2 + h // 8]); labels.append(1)
193
+
 
 
 
 
 
 
 
 
 
194
  point_coords = np.array(points, dtype=np.float32)
195
  point_labels = np.array(labels, dtype=np.int32)
196
+
 
197
  result = predictor.predict(
198
  point_coords=point_coords,
199
  point_labels=point_labels,
200
  multimask_output=True
201
  )
202
+
 
203
  if isinstance(result, dict):
204
  masks = result.get("masks", None)
205
  scores = result.get("scores", None)
 
208
  else:
209
  masks = result
210
  scores = None
211
+
 
212
  if masks is not None:
213
  masks = np.array(masks)
214
+ if masks.size > 0:
 
 
215
  if masks.ndim == 3 and masks.shape[0] > 0:
 
216
  if scores is not None and len(scores) > 0:
217
  best_idx = np.argmax(scores)
218
  mask = masks[best_idx]
219
  else:
 
220
  mask = masks[0]
221
  elif masks.ndim == 2:
 
222
  mask = masks
223
  else:
224
  logger.warning(f"Unexpected mask shape from SAM2: {masks.shape}")
225
  mask = None
226
+
227
  if mask is not None:
 
228
  mask = _to_mask01(mask)
229
+ if mask.max() > 0.1:
 
 
230
  return mask
231
  else:
232
  logger.warning("SAM2 mask too weak, using fallback")
233
  else:
234
  logger.warning("SAM2 returned no masks")
235
+
236
  except Exception as e:
237
  logger.warning(f"SAM2 segmentation error: {e}")
238
+
 
239
  if fallback_enabled:
240
  logger.debug("Using fallback segmentation")
241
  return _simple_person_segmentation(frame)
242
  else:
 
243
  return np.ones((h, w), dtype=np.float32)
244
 
245
  segment_person_hq_original = segment_person_hq
246
 
247
  # ----------------------------------------------------------------------------
248
+ # MatAnyone Refinement (Stateful-capable)
249
  # ----------------------------------------------------------------------------
250
  def refine_mask_hq(
251
  frame: np.ndarray,
252
  mask: np.ndarray,
253
+ matanyone: Optional[Callable] = None,
254
+ *,
255
+ frame_idx: Optional[int] = None,
256
  fallback_enabled: bool = True,
257
  use_matanyone: Optional[bool] = None,
258
  **_compat_kwargs,
259
  ) -> np.ndarray:
260
  """
261
+ Refine mask with MatAnyone.
262
+
263
+ Modes:
264
+ • Stateful (preferred): provide `frame_idx`. On frame_idx==0, the session encodes with the mask.
265
+ On subsequent frames, the session propagates without a mask.
266
+ • Backward-compat (stateless): if `frame_idx` is None, we try callable/step/process with (frame, mask)
267
+ like before.
268
+
269
+ Returns:
270
+ 2-D float32 alpha [H,W], contiguous, in [0,1] (OpenCV-safe).
271
  """
 
272
  mask01 = _to_mask01(mask)
273
+
 
274
  if use_matanyone is False:
275
  return mask01
276
+
277
+ if matanyone is not None and callable(matanyone):
 
278
  try:
279
+ rgb01 = _ensure_rgb01(frame)
280
+
281
+ # Stateful path (preferred)
282
+ if frame_idx is not None:
283
+ if frame_idx == 0:
284
+ refined = matanyone(rgb01, mask01) # encode + first-frame predict inside
285
+ else:
286
+ refined = matanyone(rgb01) # propagate without mask
287
+ refined = _mask_to_2d(refined)
288
+ if refined.max() > 0.1:
289
+ return _postprocess_mask(refined)
290
+ logger.warning("MatAnyone stateful refinement produced empty/weak mask; falling back.")
291
+
292
+ # Backward-compat (stateless) path
293
  refined = None
294
+
295
+ # Method 1: Direct callable with (frame, mask)
296
+ try:
297
+ refined = matanyone(rgb01, mask01)
298
+ refined = _mask_to_2d(refined)
299
+ except Exception as e:
300
+ logger.debug(f"MatAnyone callable failed: {e}")
301
+
302
+ # Method 2: step(image, mask)
 
 
303
  if refined is None and hasattr(matanyone, 'step'):
304
  try:
305
+ refined = matanyone.step(rgb01, mask01)
306
+ refined = _mask_to_2d(refined)
 
307
  except Exception as e:
308
  logger.debug(f"MatAnyone step failed: {e}")
309
+
310
+ # Method 3: process(image, mask)
311
  if refined is None and hasattr(matanyone, 'process'):
312
  try:
313
+ refined = matanyone.process(rgb01, mask01)
314
+ refined = _mask_to_2d(refined)
 
315
  except Exception as e:
316
  logger.debug(f"MatAnyone process failed: {e}")
317
+
 
318
  if refined is not None and refined.max() > 0.1:
319
+ return _postprocess_mask(refined)
 
 
320
  else:
321
  logger.warning("MatAnyone refinement failed or produced empty mask")
322
+
323
  except Exception as e:
324
  logger.warning(f"MatAnyone error: {e}")
325
+
326
  # Fallback refinement
327
  if fallback_enabled:
328
  return _fallback_refine(mask01)
 
331
 
332
  def _postprocess_mask(mask01: np.ndarray) -> np.ndarray:
333
  """Post-process mask to clean edges and remove artifacts"""
 
334
  mask_uint8 = (mask01 * 255).astype(np.uint8)
335
+
 
336
  kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
337
  mask_uint8 = cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel_close)
338
+
 
339
  mask_uint8 = cv2.GaussianBlur(mask_uint8, (3, 3), 0)
340
+
 
341
  _, mask_uint8 = cv2.threshold(mask_uint8, 127, 255, cv2.THRESH_BINARY)
342
+
 
343
  mask_uint8 = cv2.GaussianBlur(mask_uint8, (5, 5), 1)
344
+
345
  return mask_uint8.astype(np.float32) / 255.0
346
 
347
  def _fallback_refine(mask01: np.ndarray) -> np.ndarray:
348
  """Simple fallback refinement"""
349
  mask_uint8 = (mask01 * 255).astype(np.uint8)
350
+
 
351
  mask_uint8 = cv2.bilateralFilter(mask_uint8, 9, 75, 75)
352
+
 
353
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
354
  mask_uint8 = cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel)
355
  mask_uint8 = cv2.morphologyEx(mask_uint8, cv2.MORPH_OPEN, kernel)
356
+
 
357
  mask_uint8 = cv2.GaussianBlur(mask_uint8, (5, 5), 1)
358
+
359
  return mask_uint8.astype(np.float32) / 255.0
360
 
361
  # ----------------------------------------------------------------------------
 
371
  """High-quality background replacement with alpha blending"""
372
  try:
373
  H, W = frame.shape[:2]
374
+
 
375
  if background.shape[:2] != (H, W):
376
  background = cv2.resize(background, (W, H), interpolation=cv2.INTER_LANCZOS4)
377
+
378
+ m = _mask_to_2d(_to_mask01(mask01))
379
+
 
 
380
  m = _feather(m, k=1)
381
+
 
382
  m3 = np.repeat(m[:, :, None], 3, axis=2)
383
+
 
384
  comp = frame.astype(np.float32) * m3 + background.astype(np.float32) * (1.0 - m3)
385
+
386
  return np.clip(comp, 0, 255).astype(np.uint8)
387
+
388
  except Exception as e:
389
  if fallback_enabled:
390
  logger.warning(f"Compositing failed ({e}) – returning original frame")
 
443
  "create_professional_background",
444
  "validate_video_file",
445
  "PROFESSIONAL_BACKGROUNDS",
446
+ ]