MogensR commited on
Commit
d70cd4e
·
1 Parent(s): ee38ee4

Update utilities.py

Browse files
Files changed (1) hide show
  1. utilities.py +720 -223
utilities.py CHANGED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
  """
3
- utilities.py - Helper functions and utilities for Video Background Replacement
4
- CRITICAL FIX: Fixed transparency issue by ensuring mask is properly normalized
5
  """
6
 
7
  import os
@@ -11,271 +11,691 @@
11
  from PIL import Image, ImageDraw
12
  import logging
13
  import time
 
 
14
 
15
  # Setup logging
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger(__name__)
18
 
19
- # Professional background templates
20
  PROFESSIONAL_BACKGROUNDS = {
21
  "office_modern": {
22
  "name": "Modern Office",
23
  "type": "gradient",
24
  "colors": ["#f8f9fa", "#e9ecef", "#dee2e6"],
25
  "direction": "diagonal",
26
- "description": "Clean, contemporary office environment"
 
 
27
  },
28
  "studio_blue": {
29
  "name": "Professional Blue",
30
  "type": "gradient",
31
  "colors": ["#1e3c72", "#2a5298", "#3498db"],
32
  "direction": "radial",
33
- "description": "Broadcast-quality blue studio"
 
 
34
  },
35
  "studio_green": {
36
  "name": "Broadcast Green",
37
  "type": "color",
38
  "colors": ["#00b894"],
39
  "chroma_key": True,
40
- "description": "Professional green screen replacement"
 
 
41
  },
42
  "minimalist": {
43
  "name": "Minimalist White",
44
  "type": "gradient",
45
  "colors": ["#ffffff", "#f1f2f6", "#ddd"],
46
  "direction": "soft_radial",
47
- "description": "Clean, minimal background"
 
 
48
  },
49
  "warm_gradient": {
50
  "name": "Warm Sunset",
51
  "type": "gradient",
52
  "colors": ["#ff7675", "#fd79a8", "#fdcb6e"],
53
  "direction": "diagonal",
54
- "description": "Warm, inviting atmosphere"
 
 
55
  },
56
  "tech_dark": {
57
  "name": "Tech Dark",
58
  "type": "gradient",
59
  "colors": ["#0c0c0c", "#2d3748", "#4a5568"],
60
  "direction": "vertical",
61
- "description": "Modern tech/gaming setup"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
  }
64
 
65
- def segment_person_hq(image, predictor):
66
- """High-quality person segmentation using provided SAM2 predictor"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  try:
68
- predictor.set_image(image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  h, w = image.shape[:2]
70
 
71
- # Strategic point placement for person detection
72
  points = np.array([
73
- [w//2, h//4], # Top-center (head)
74
- [w//2, h//2], # Center (torso)
75
- [w//2, 3*h//4], # Bottom-center (legs)
76
- [w//4, h//2], # Left-side (arm)
77
- [3*w//4, h//2], # Right-side (arm)
78
- ])
79
- labels = np.ones(len(points))
80
-
81
- masks, scores, _ = predictor.predict(
82
- point_coords=points,
83
- point_labels=labels,
84
- multimask_output=True
85
- )
86
-
87
- # Select best mask
88
- best_idx = np.argmax(scores)
89
- best_mask = masks[best_idx]
90
-
91
- # CRITICAL FIX: Ensure mask is properly normalized to 0-255
92
- if len(best_mask.shape) > 2:
93
- best_mask = best_mask.squeeze()
94
-
95
- # Check if mask is in 0-1 range and convert to 0-255
96
- if best_mask.max() <= 1.0:
97
- best_mask = (best_mask * 255).astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  else:
99
- best_mask = best_mask.astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
100
 
101
- # Post-process mask
102
- kernel = np.ones((5, 5), np.uint8)
103
- best_mask = cv2.morphologyEx(best_mask, cv2.MORPH_CLOSE, kernel)
104
- best_mask = cv2.morphologyEx(best_mask, cv2.MORPH_OPEN, kernel, iterations=1)
 
 
 
 
 
 
105
 
106
- # Ensure mask is binary and clean
107
- _, best_mask = cv2.threshold(best_mask, 127, 255, cv2.THRESH_BINARY)
 
 
108
 
109
- logger.info(f"Mask after segmentation - shape: {best_mask.shape}, range: {best_mask.min()}-{best_mask.max()}")
 
110
 
111
- return best_mask
112
 
113
  except Exception as e:
114
- logger.error(f"Segmentation error: {e}")
115
- # Fallback to simple center mask
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  h, w = image.shape[:2]
117
- fallback_mask = np.zeros((h, w), dtype=np.uint8)
118
- x1, y1 = w//4, h//6
119
- x2, y2 = 3*w//4, 5*h//6
120
- fallback_mask[y1:y2, x1:x2] = 255
121
- logger.warning("Using fallback mask due to segmentation error")
122
- return fallback_mask
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- def refine_mask_hq(image, mask, matanyone_processor):
125
- """Cinema-quality mask refinement using provided MatAnyone processor"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  try:
127
- # Ensure mask is 0-255 range
128
- if mask.max() <= 1.0:
129
- mask = (mask * 255).astype(np.uint8)
130
 
131
  # Try MatAnyone if available
132
  if matanyone_processor is not None:
133
  try:
134
- refined_mask = matanyone_processor.infer(image, mask)
135
- if refined_mask is not None:
136
- if len(refined_mask.shape) == 3:
137
- refined_mask = cv2.cvtColor(refined_mask, cv2.COLOR_BGR2GRAY)
138
- # Ensure proper range
139
- if refined_mask.max() <= 1.0:
140
- refined_mask = (refined_mask * 255).astype(np.uint8)
141
  return refined_mask
142
- except:
143
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
- # Fallback to OpenCV refinement
146
- logger.warning("Using OpenCV fallback for mask refinement")
147
- return enhance_mask_opencv(image, mask)
 
148
 
149
  except Exception as e:
150
- logger.error(f"Mask refinement error: {e}")
151
- return enhance_mask_opencv(image, mask)
152
 
153
- def enhance_mask_opencv(image, mask):
154
- """Enhanced mask refinement using OpenCV techniques"""
 
 
155
  try:
156
  if len(mask.shape) == 3:
157
  mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
158
 
159
- # Ensure mask is 0-255
160
  if mask.max() <= 1.0:
161
  mask = (mask * 255).astype(np.uint8)
162
 
163
- # Bilateral filtering for edge preservation
 
 
164
  refined_mask = cv2.bilateralFilter(mask, 9, 75, 75)
165
 
166
- # Morphological operations for cleaner edges
167
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
168
- refined_mask = cv2.morphologyEx(refined_mask, cv2.MORPH_CLOSE, kernel)
169
- refined_mask = cv2.morphologyEx(refined_mask, cv2.MORPH_OPEN, kernel)
170
 
171
- # Ensure binary mask
172
- _, refined_mask = cv2.threshold(refined_mask, 127, 255, cv2.THRESH_BINARY)
 
 
 
 
173
 
174
- # Smooth edges
175
- refined_mask = cv2.GaussianBlur(refined_mask, (3, 3), 1.0)
 
 
 
176
 
177
  return refined_mask
178
 
179
  except Exception as e:
180
- logger.warning(f"Enhanced mask refinement error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  return mask
182
 
183
- # ============================================================================ #
184
- # CRITICAL FIX: Fixed transparency issue in background replacement
185
- # ============================================================================ #
186
- def replace_background_hq(frame, mask, background):
187
- """High-quality background replacement with FIXED transparency handling"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  try:
189
  # Resize background to match frame
190
- background = cv2.resize(background, (frame.shape[1], frame.shape[0]), interpolation=cv2.INTER_LANCZOS4)
 
191
 
192
- # Ensure mask is single channel
193
  if len(mask.shape) == 3:
194
  mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
195
 
196
- # CRITICAL FIX: Ensure mask is in 0-255 range
197
  if mask.dtype != np.uint8:
198
  mask = mask.astype(np.uint8)
199
 
200
  if mask.max() <= 1.0:
201
- logger.warning("Mask appears to be normalized 0-1, converting to 0-255")
202
  mask = (mask * 255).astype(np.uint8)
203
 
204
- # Log mask statistics for debugging
205
- logger.info(f"Mask stats before threshold - min: {mask.min()}, max: {mask.max()}, mean: {mask.mean():.2f}")
 
 
 
 
 
 
 
 
 
 
206
 
207
- # Create binary mask with adjusted threshold
208
- threshold = 100 # Lower threshold to catch more of the person
 
 
 
 
 
 
 
 
 
 
 
 
209
  _, mask_binary = cv2.threshold(mask, threshold, 255, cv2.THRESH_BINARY)
210
 
211
- # Clean up mask with morphological operations
212
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
213
- mask_binary = cv2.morphologyEx(mask_binary, cv2.MORPH_CLOSE, kernel) # Fill holes
214
- mask_binary = cv2.morphologyEx(mask_binary, cv2.MORPH_OPEN, kernel) # Remove noise
215
 
216
- # Create smooth edges with minimal feathering
217
  mask_smooth = cv2.GaussianBlur(mask_binary.astype(np.float32), (5, 5), 1.0)
218
- mask_smooth = mask_smooth / 255.0 # Normalize to 0-1 for blending
219
 
220
- # Ensure opaque center by applying curve adjustment
221
- mask_smooth = np.power(mask_smooth, 0.8) # Makes transition sharper
222
 
223
- # Apply threshold to ensure solid center
224
  mask_smooth = np.where(mask_smooth > 0.5,
225
- np.minimum(mask_smooth * 1.1, 1.0), # Boost high values
226
- mask_smooth * 0.9) # Slightly reduce low values
 
 
 
227
 
228
- # Create 3-channel mask for blending
229
- mask_3channel = np.stack([mask_smooth] * 3, axis=2)
230
 
231
- # Perform compositing
232
- frame_float = frame.astype(np.float32)
233
  background_float = background.astype(np.float32)
234
 
235
- result = frame_float * mask_3channel + background_float * (1 - mask_3channel)
 
236
  result = np.clip(result, 0, 255).astype(np.uint8)
237
 
238
- # Log final statistics
239
- logger.info(f"Final mask stats - min: {mask_smooth.min():.3f}, max: {mask_smooth.max():.3f}, mean: {mask_smooth.mean():.3f}")
240
-
241
  return result
242
 
243
  except Exception as e:
244
- logger.error(f"Background replacement error: {e}")
245
- # Simple fallback
246
- try:
247
- background = cv2.resize(background, (frame.shape[1], frame.shape[0]))
248
- if len(mask.shape) == 3:
249
- mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
250
- if mask.max() <= 1.0:
251
- mask = (mask * 255).astype(np.uint8)
252
- _, mask_binary = cv2.threshold(mask, 100, 255, cv2.THRESH_BINARY)
253
- mask_norm = mask_binary.astype(np.float32) / 255.0
254
- mask_3ch = np.stack([mask_norm] * 3, axis=2)
255
- result = frame * mask_3ch + background * (1 - mask_3ch)
256
- return result.astype(np.uint8)
257
- except:
258
  return frame
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
- def create_professional_background(bg_config, width, height):
261
- """Create professional background based on configuration"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  try:
263
  if bg_config["type"] == "color":
264
- color_hex = bg_config["colors"][0].lstrip('#')
265
- color_rgb = tuple(int(color_hex[i:i+2], 16) for i in (0, 2, 4))
266
- color_bgr = color_rgb[::-1]
267
- background = np.full((height, width, 3), color_bgr, dtype=np.uint8)
268
  elif bg_config["type"] == "gradient":
269
- background = create_gradient_background(bg_config, width, height)
270
  else:
 
271
  background = np.full((height, width, 3), (128, 128, 128), dtype=np.uint8)
 
 
 
 
272
  return background
 
273
  except Exception as e:
274
  logger.error(f"Background creation error: {e}")
275
  return np.full((height, width, 3), (128, 128, 128), dtype=np.uint8)
276
 
277
- def create_gradient_background(bg_config, width, height):
278
- """Create high-quality gradient backgrounds"""
 
 
 
 
 
 
 
279
  try:
280
  colors = bg_config["colors"]
281
  direction = bg_config.get("direction", "vertical")
@@ -290,106 +710,183 @@ def create_gradient_background(bg_config, width, height):
290
  if not rgb_colors:
291
  rgb_colors = [(128, 128, 128)]
292
 
293
- # Create PIL image for gradient
294
- pil_img = Image.new('RGB', (width, height))
295
- draw = ImageDraw.Draw(pil_img)
296
-
297
- def interpolate_color(colors, progress):
298
- if len(colors) == 1:
299
- return colors[0]
300
- elif len(colors) == 2:
301
- r = int(colors[0][0] + (colors[1][0] - colors[0][0]) * progress)
302
- g = int(colors[0][1] + (colors[1][1] - colors[0][1]) * progress)
303
- b = int(colors[0][2] + (colors[1][2] - colors[0][2]) * progress)
304
- return (r, g, b)
305
- else:
306
- segment = progress * (len(colors) - 1)
307
- idx = int(segment)
308
- local_progress = segment - idx
309
- if idx >= len(colors) - 1:
310
- return colors[-1]
311
- c1, c2 = colors[idx], colors[idx + 1]
312
- r = int(c1[0] + (c2[0] - c1[0]) * local_progress)
313
- g = int(c1[1] + (c2[1] - c1[1]) * local_progress)
314
- b = int(c1[2] + (c2[2] - c1[2]) * local_progress)
315
- return (r, g, b)
316
-
317
- # Generate gradient based on direction
318
  if direction == "vertical":
319
- for y in range(height):
320
- progress = y / height if height > 0 else 0
321
- color = interpolate_color(rgb_colors, progress)
322
- draw.line([(0, y), (width, y)], fill=color)
323
  elif direction == "horizontal":
324
- for x in range(width):
325
- progress = x / width if width > 0 else 0
326
- color = interpolate_color(rgb_colors, progress)
327
- draw.line([(x, 0), (x, height)], fill=color)
328
  elif direction == "diagonal":
329
- max_distance = width + height
330
- for y in range(height):
331
- for x in range(width):
332
- progress = (x + y) / max_distance if max_distance > 0 else 0
333
- progress = min(1.0, progress)
334
- color = interpolate_color(rgb_colors, progress)
335
- pil_img.putpixel((x, y), color)
336
  elif direction in ["radial", "soft_radial"]:
337
- center_x, center_y = width // 2, height // 2
338
- max_distance = np.sqrt(center_x**2 + center_y**2)
339
- for y in range(height):
340
- for x in range(width):
341
- distance = np.sqrt((x - center_x)**2 + (y - center_y)**2)
342
- progress = distance / max_distance if max_distance > 0 else 0
343
- progress = min(1.0, progress)
344
- if direction == "soft_radial":
345
- progress = progress**0.7
346
- color = interpolate_color(rgb_colors, progress)
347
- pil_img.putpixel((x, y), color)
348
-
349
- # Convert to OpenCV format
350
- background = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
351
- return background
352
 
353
  except Exception as e:
354
  logger.error(f"Gradient creation error: {e}")
355
- background = np.full((height, width, 3), (128, 128, 128), dtype=np.uint8)
356
- return background
357
 
358
- def create_procedural_background(prompt, style, width, height):
359
- """Create procedural background based on text prompt"""
360
- # Simplified version - full implementation would be too long
361
- color_map = {
362
- 'blue': ['#1e3c72', '#2a5298', '#3498db'],
363
- 'green': ['#27ae60', '#2ecc71', '#58d68d'],
364
- 'red': ['#e74c3c', '#c0392b', '#ff7675'],
365
- 'purple': ['#6c5ce7', '#a29bfe', '#fd79a8'],
366
- }
367
 
368
- selected_colors = ['#3498db', '#2ecc71', '#e74c3c'] # Default
369
- for keyword, colors in color_map.items():
370
- if keyword in prompt.lower():
371
- selected_colors = colors
372
- break
373
 
374
- bg_config = {
375
- "type": "gradient",
376
- "colors": selected_colors[:2],
377
- "direction": "diagonal"
378
- }
379
- return create_gradient_background(bg_config, width, height)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- def validate_video_file(video_path):
382
- """Validate video file format and basic properties"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  if not video_path or not os.path.exists(video_path):
384
  return False, "Video file not found"
 
385
  try:
 
 
 
 
 
 
 
 
 
386
  cap = cv2.VideoCapture(video_path)
387
  if not cap.isOpened():
388
  return False, "Cannot open video file"
 
389
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
390
- if frame_count == 0:
391
- return False, "Video appears to be empty"
 
 
392
  cap.release()
393
- return True, "Video file valid"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  except Exception as e:
395
  return False, f"Error validating video: {str(e)}"
 
1
  #!/usr/bin/env python3
2
  """
3
+ Enhanced utilities.py - Core computer vision functions with improved error handling
4
+ Fixed transparency issues, added fallback strategies, and enhanced memory management
5
  """
6
 
7
  import os
 
11
  from PIL import Image, ImageDraw
12
  import logging
13
  import time
14
+ from typing import Optional, Dict, Any, Tuple
15
+ from pathlib import Path
16
 
17
  # Setup logging
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
 
21
+ # Professional background templates with enhanced configurations
22
  PROFESSIONAL_BACKGROUNDS = {
23
  "office_modern": {
24
  "name": "Modern Office",
25
  "type": "gradient",
26
  "colors": ["#f8f9fa", "#e9ecef", "#dee2e6"],
27
  "direction": "diagonal",
28
+ "description": "Clean, contemporary office environment",
29
+ "brightness": 0.95,
30
+ "contrast": 1.1
31
  },
32
  "studio_blue": {
33
  "name": "Professional Blue",
34
  "type": "gradient",
35
  "colors": ["#1e3c72", "#2a5298", "#3498db"],
36
  "direction": "radial",
37
+ "description": "Broadcast-quality blue studio",
38
+ "brightness": 0.9,
39
+ "contrast": 1.2
40
  },
41
  "studio_green": {
42
  "name": "Broadcast Green",
43
  "type": "color",
44
  "colors": ["#00b894"],
45
  "chroma_key": True,
46
+ "description": "Professional green screen replacement",
47
+ "brightness": 1.0,
48
+ "contrast": 1.0
49
  },
50
  "minimalist": {
51
  "name": "Minimalist White",
52
  "type": "gradient",
53
  "colors": ["#ffffff", "#f1f2f6", "#ddd"],
54
  "direction": "soft_radial",
55
+ "description": "Clean, minimal background",
56
+ "brightness": 0.98,
57
+ "contrast": 0.9
58
  },
59
  "warm_gradient": {
60
  "name": "Warm Sunset",
61
  "type": "gradient",
62
  "colors": ["#ff7675", "#fd79a8", "#fdcb6e"],
63
  "direction": "diagonal",
64
+ "description": "Warm, inviting atmosphere",
65
+ "brightness": 0.85,
66
+ "contrast": 1.15
67
  },
68
  "tech_dark": {
69
  "name": "Tech Dark",
70
  "type": "gradient",
71
  "colors": ["#0c0c0c", "#2d3748", "#4a5568"],
72
  "direction": "vertical",
73
+ "description": "Modern tech/gaming setup",
74
+ "brightness": 0.7,
75
+ "contrast": 1.3
76
+ },
77
+ "corporate_blue": {
78
+ "name": "Corporate Blue",
79
+ "type": "gradient",
80
+ "colors": ["#667eea", "#764ba2", "#f093fb"],
81
+ "direction": "diagonal",
82
+ "description": "Professional corporate background",
83
+ "brightness": 0.88,
84
+ "contrast": 1.1
85
+ },
86
+ "nature_blur": {
87
+ "name": "Soft Nature",
88
+ "type": "gradient",
89
+ "colors": ["#a8edea", "#fed6e3", "#d299c2"],
90
+ "direction": "radial",
91
+ "description": "Soft blurred nature effect",
92
+ "brightness": 0.92,
93
+ "contrast": 0.95
94
  }
95
  }
96
 
97
+ class SegmentationError(Exception):
98
+ """Custom exception for segmentation failures"""
99
+ pass
100
+
101
+ class MaskRefinementError(Exception):
102
+ """Custom exception for mask refinement failures"""
103
+ pass
104
+
105
+ class BackgroundReplacementError(Exception):
106
+ """Custom exception for background replacement failures"""
107
+ pass
108
+
109
+ def segment_person_hq(image: np.ndarray, predictor: Any, fallback_enabled: bool = True) -> np.ndarray:
110
+ """
111
+ High-quality person segmentation with enhanced error handling and fallback strategies
112
+
113
+ Args:
114
+ image: Input image (H, W, 3)
115
+ predictor: SAM2 predictor instance
116
+ fallback_enabled: Whether to use fallback segmentation if AI fails
117
+
118
+ Returns:
119
+ Binary mask (H, W) with values 0-255
120
+
121
+ Raises:
122
+ SegmentationError: If segmentation fails and fallback is disabled
123
+ """
124
+ if image is None or image.size == 0:
125
+ raise SegmentationError("Invalid input image")
126
+
127
  try:
128
+ # Validate predictor
129
+ if predictor is None:
130
+ if fallback_enabled:
131
+ logger.warning("SAM2 predictor not available, using fallback")
132
+ return _fallback_segmentation(image)
133
+ else:
134
+ raise SegmentationError("SAM2 predictor not available")
135
+
136
+ # Set image for prediction
137
+ try:
138
+ predictor.set_image(image)
139
+ except Exception as e:
140
+ logger.error(f"Failed to set image in predictor: {e}")
141
+ if fallback_enabled:
142
+ return _fallback_segmentation(image)
143
+ else:
144
+ raise SegmentationError(f"Predictor setup failed: {e}")
145
+
146
  h, w = image.shape[:2]
147
 
148
+ # Enhanced strategic point placement for better person detection
149
  points = np.array([
150
+ [w//2, h//4], # Head center
151
+ [w//2, h//2], # Torso center
152
+ [w//2, 3*h//4], # Lower body
153
+ [w//3, h//2], # Left side
154
+ [2*w//3, h//2], # Right side
155
+ [w//2, h//6], # Upper head
156
+ [w//4, 2*h//3], # Left leg area
157
+ [3*w//4, 2*h//3], # Right leg area
158
+ ], dtype=np.float32)
159
+
160
+ labels = np.ones(len(points), dtype=np.int32)
161
+
162
+ # Perform prediction with error handling
163
+ try:
164
+ with torch.no_grad():
165
+ masks, scores, _ = predictor.predict(
166
+ point_coords=points,
167
+ point_labels=labels,
168
+ multimask_output=True
169
+ )
170
+ except Exception as e:
171
+ logger.error(f"SAM2 prediction failed: {e}")
172
+ if fallback_enabled:
173
+ return _fallback_segmentation(image)
174
+ else:
175
+ raise SegmentationError(f"Prediction failed: {e}")
176
+
177
+ # Validate prediction results
178
+ if masks is None or len(masks) == 0:
179
+ logger.warning("SAM2 returned no masks")
180
+ if fallback_enabled:
181
+ return _fallback_segmentation(image)
182
+ else:
183
+ raise SegmentationError("No masks generated")
184
+
185
+ if scores is None or len(scores) == 0:
186
+ logger.warning("SAM2 returned no scores")
187
+ best_mask = masks[0]
188
+ else:
189
+ # Select best mask based on score
190
+ best_idx = np.argmax(scores)
191
+ best_mask = masks[best_idx]
192
+ logger.debug(f"Selected mask {best_idx} with score {scores[best_idx]:.3f}")
193
+
194
+ # Process mask to ensure correct format
195
+ mask = _process_mask(best_mask)
196
+
197
+ # Validate mask quality
198
+ if not _validate_mask_quality(mask, image.shape[:2]):
199
+ logger.warning("Mask quality validation failed")
200
+ if fallback_enabled:
201
+ return _fallback_segmentation(image)
202
+ else:
203
+ raise SegmentationError("Poor mask quality")
204
+
205
+ logger.debug(f"Segmentation successful - mask range: {mask.min()}-{mask.max()}")
206
+ return mask
207
+
208
+ except SegmentationError:
209
+ raise
210
+ except Exception as e:
211
+ logger.error(f"Unexpected segmentation error: {e}")
212
+ if fallback_enabled:
213
+ return _fallback_segmentation(image)
214
  else:
215
+ raise SegmentationError(f"Unexpected error: {e}")
216
+
217
+ def _process_mask(mask: np.ndarray) -> np.ndarray:
218
+ """Process raw mask to ensure correct format and range"""
219
+ try:
220
+ # Handle different input formats
221
+ if len(mask.shape) > 2:
222
+ mask = mask.squeeze()
223
+
224
+ if len(mask.shape) > 2:
225
+ mask = mask[:, :, 0] if mask.shape[2] > 0 else mask.sum(axis=2)
226
 
227
+ # Ensure proper data type and range
228
+ if mask.dtype == bool:
229
+ mask = mask.astype(np.uint8) * 255
230
+ elif mask.dtype == np.float32 or mask.dtype == np.float64:
231
+ if mask.max() <= 1.0:
232
+ mask = (mask * 255).astype(np.uint8)
233
+ else:
234
+ mask = np.clip(mask, 0, 255).astype(np.uint8)
235
+ else:
236
+ mask = mask.astype(np.uint8)
237
 
238
+ # Post-process for cleaner edges
239
+ kernel = np.ones((3, 3), np.uint8)
240
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
241
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
242
 
243
+ # Ensure binary threshold
244
+ _, mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
245
 
246
+ return mask
247
 
248
  except Exception as e:
249
+ logger.error(f"Mask processing failed: {e}")
250
+ # Return a basic fallback mask
251
+ h, w = mask.shape[:2] if len(mask.shape) >= 2 else (256, 256)
252
+ fallback = np.zeros((h, w), dtype=np.uint8)
253
+ fallback[h//4:3*h//4, w//4:3*w//4] = 255
254
+ return fallback
255
+
256
+ def _validate_mask_quality(mask: np.ndarray, image_shape: Tuple[int, int]) -> bool:
257
+ """Validate that the mask meets quality criteria"""
258
+ try:
259
+ h, w = image_shape
260
+ mask_area = np.sum(mask > 127)
261
+ total_area = h * w
262
+
263
+ # Check if mask area is reasonable (5% to 80% of image)
264
+ area_ratio = mask_area / total_area
265
+ if area_ratio < 0.05 or area_ratio > 0.8:
266
+ logger.warning(f"Suspicious mask area ratio: {area_ratio:.3f}")
267
+ return False
268
+
269
+ # Check if mask is not just a blob in corner
270
+ mask_binary = mask > 127
271
+ mask_center_y, mask_center_x = np.where(mask_binary)
272
+
273
+ if len(mask_center_y) == 0:
274
+ logger.warning("Empty mask")
275
+ return False
276
+
277
+ center_y = np.mean(mask_center_y)
278
+ center_x = np.mean(mask_center_x)
279
+
280
+ # Person should be roughly centered
281
+ if center_y < h * 0.2 or center_y > h * 0.9:
282
+ logger.warning(f"Mask center too far from expected person location: y={center_y/h:.2f}")
283
+ return False
284
+
285
+ return True
286
+
287
+ except Exception as e:
288
+ logger.warning(f"Mask validation error: {e}")
289
+ return True # Default to accepting mask if validation fails
290
+
291
+ def _fallback_segmentation(image: np.ndarray) -> np.ndarray:
292
+ """Fallback segmentation when AI models fail"""
293
+ try:
294
+ logger.info("Using fallback segmentation strategy")
295
  h, w = image.shape[:2]
296
+
297
+ # Try background subtraction approach
298
+ try:
299
+ # Simple background subtraction
300
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
301
+
302
+ # Assume background is around the edges
303
+ edge_pixels = np.concatenate([
304
+ gray[0, :], gray[-1, :], gray[:, 0], gray[:, -1]
305
+ ])
306
+ bg_color = np.median(edge_pixels)
307
+
308
+ # Create mask based on difference from background
309
+ diff = np.abs(gray.astype(float) - bg_color)
310
+ mask = (diff > 30).astype(np.uint8) * 255
311
+
312
+ # Morphological operations to clean up
313
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
314
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
315
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
316
+
317
+ # If mask looks reasonable, use it
318
+ if _validate_mask_quality(mask, image.shape[:2]):
319
+ logger.info("Background subtraction fallback successful")
320
+ return mask
321
+
322
+ except Exception as e:
323
+ logger.warning(f"Background subtraction fallback failed: {e}")
324
+
325
+ # Simple geometric fallback
326
+ mask = np.zeros((h, w), dtype=np.uint8)
327
+
328
+ # Create an elliptical mask in center assuming person location
329
+ center_x, center_y = w // 2, h // 2
330
+ radius_x, radius_y = w // 3, h // 2.5
331
+
332
+ y, x = np.ogrid[:h, :w]
333
+ mask_ellipse = ((x - center_x) / radius_x) ** 2 + ((y - center_y) / radius_y) ** 2 <= 1
334
+ mask[mask_ellipse] = 255
335
+
336
+ logger.info("Using geometric fallback mask")
337
+ return mask
338
+
339
+ except Exception as e:
340
+ logger.error(f"All fallback strategies failed: {e}")
341
+ # Last resort: simple center rectangle
342
+ h, w = image.shape[:2]
343
+ mask = np.zeros((h, w), dtype=np.uint8)
344
+ mask[h//6:5*h//6, w//4:3*w//4] = 255
345
+ return mask
346
 
347
+ def refine_mask_hq(image: np.ndarray, mask: np.ndarray, matanyone_processor: Any,
348
+ fallback_enabled: bool = True) -> np.ndarray:
349
+ """
350
+ Enhanced mask refinement with MatAnyone and robust fallbacks
351
+
352
+ Args:
353
+ image: Input image (H, W, 3)
354
+ mask: Input mask (H, W) with values 0-255
355
+ matanyone_processor: MatAnyone processor instance
356
+ fallback_enabled: Whether to use fallback refinement if MatAnyone fails
357
+
358
+ Returns:
359
+ Refined mask (H, W) with values 0-255
360
+
361
+ Raises:
362
+ MaskRefinementError: If refinement fails and fallback is disabled
363
+ """
364
+ if image is None or mask is None:
365
+ raise MaskRefinementError("Invalid input image or mask")
366
+
367
  try:
368
+ # Ensure mask is in correct format
369
+ mask = _process_mask(mask)
 
370
 
371
  # Try MatAnyone if available
372
  if matanyone_processor is not None:
373
  try:
374
+ logger.debug("Attempting MatAnyone refinement")
375
+ refined_mask = _matanyone_refine(image, mask, matanyone_processor)
376
+
377
+ if refined_mask is not None and _validate_mask_quality(refined_mask, image.shape[:2]):
378
+ logger.debug("MatAnyone refinement successful")
 
 
379
  return refined_mask
380
+ else:
381
+ logger.warning("MatAnyone produced poor quality mask")
382
+
383
+ except Exception as e:
384
+ logger.warning(f"MatAnyone refinement failed: {e}")
385
+
386
+ # Fallback to enhanced OpenCV refinement
387
+ if fallback_enabled:
388
+ logger.debug("Using enhanced OpenCV refinement")
389
+ return enhance_mask_opencv_advanced(image, mask)
390
+ else:
391
+ raise MaskRefinementError("MatAnyone failed and fallback disabled")
392
+
393
+ except MaskRefinementError:
394
+ raise
395
+ except Exception as e:
396
+ logger.error(f"Unexpected mask refinement error: {e}")
397
+ if fallback_enabled:
398
+ return enhance_mask_opencv_advanced(image, mask)
399
+ else:
400
+ raise MaskRefinementError(f"Unexpected error: {e}")
401
+
402
+ def _matanyone_refine(image: np.ndarray, mask: np.ndarray, processor: Any) -> Optional[np.ndarray]:
403
+ """Attempt MatAnyone mask refinement"""
404
+ try:
405
+ # Different possible MatAnyone interfaces
406
+ if hasattr(processor, 'infer'):
407
+ refined_mask = processor.infer(image, mask)
408
+ elif hasattr(processor, 'process'):
409
+ refined_mask = processor.process(image, mask)
410
+ elif callable(processor):
411
+ refined_mask = processor(image, mask)
412
+ else:
413
+ logger.warning("Unknown MatAnyone interface")
414
+ return None
415
+
416
+ if refined_mask is None:
417
+ return None
418
 
419
+ # Process the refined mask
420
+ refined_mask = _process_mask(refined_mask)
421
+
422
+ return refined_mask
423
 
424
  except Exception as e:
425
+ logger.warning(f"MatAnyone processing error: {e}")
426
+ return None
427
 
428
+ def enhance_mask_opencv_advanced(image: np.ndarray, mask: np.ndarray) -> np.ndarray:
429
+ """
430
+ Advanced OpenCV-based mask enhancement with multiple techniques
431
+ """
432
  try:
433
  if len(mask.shape) == 3:
434
  mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
435
 
436
+ # Ensure proper range
437
  if mask.max() <= 1.0:
438
  mask = (mask * 255).astype(np.uint8)
439
 
440
+ # Multi-stage refinement
441
+
442
+ # 1. Bilateral filtering for edge preservation
443
  refined_mask = cv2.bilateralFilter(mask, 9, 75, 75)
444
 
445
+ # 2. Edge-aware smoothing using guided filter approximation
446
+ refined_mask = _guided_filter_approx(image, refined_mask, radius=8, eps=0.2)
 
 
447
 
448
+ # 3. Morphological operations for structure
449
+ kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
450
+ refined_mask = cv2.morphologyEx(refined_mask, cv2.MORPH_CLOSE, kernel_close)
451
+
452
+ kernel_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
453
+ refined_mask = cv2.morphologyEx(refined_mask, cv2.MORPH_OPEN, kernel_open)
454
 
455
+ # 4. Final smoothing
456
+ refined_mask = cv2.GaussianBlur(refined_mask, (3, 3), 0.8)
457
+
458
+ # 5. Ensure binary output
459
+ _, refined_mask = cv2.threshold(refined_mask, 127, 255, cv2.THRESH_BINARY)
460
 
461
  return refined_mask
462
 
463
  except Exception as e:
464
+ logger.warning(f"Enhanced OpenCV refinement failed: {e}")
465
+ # Simple fallback
466
+ return cv2.GaussianBlur(mask, (5, 5), 1.0)
467
+
468
+ def _guided_filter_approx(guide: np.ndarray, mask: np.ndarray, radius: int = 8, eps: float = 0.2) -> np.ndarray:
469
+ """Approximation of guided filter for edge-aware smoothing"""
470
+ try:
471
+ guide_gray = cv2.cvtColor(guide, cv2.COLOR_BGR2GRAY) if len(guide.shape) == 3 else guide
472
+ guide_gray = guide_gray.astype(np.float32) / 255.0
473
+ mask_float = mask.astype(np.float32) / 255.0
474
+
475
+ # Box filter approximation
476
+ kernel_size = 2 * radius + 1
477
+
478
+ # Mean filters
479
+ mean_guide = cv2.boxFilter(guide_gray, -1, (kernel_size, kernel_size))
480
+ mean_mask = cv2.boxFilter(mask_float, -1, (kernel_size, kernel_size))
481
+ corr_guide_mask = cv2.boxFilter(guide_gray * mask_float, -1, (kernel_size, kernel_size))
482
+
483
+ # Covariance
484
+ cov_guide_mask = corr_guide_mask - mean_guide * mean_mask
485
+ mean_guide_sq = cv2.boxFilter(guide_gray * guide_gray, -1, (kernel_size, kernel_size))
486
+ var_guide = mean_guide_sq - mean_guide * mean_guide
487
+
488
+ # Coefficients
489
+ a = cov_guide_mask / (var_guide + eps)
490
+ b = mean_mask - a * mean_guide
491
+
492
+ # Apply coefficients
493
+ mean_a = cv2.boxFilter(a, -1, (kernel_size, kernel_size))
494
+ mean_b = cv2.boxFilter(b, -1, (kernel_size, kernel_size))
495
+
496
+ output = mean_a * guide_gray + mean_b
497
+ output = np.clip(output * 255, 0, 255).astype(np.uint8)
498
+
499
+ return output
500
+
501
+ except Exception as e:
502
+ logger.warning(f"Guided filter approximation failed: {e}")
503
  return mask
504
 
505
+ def replace_background_hq(frame: np.ndarray, mask: np.ndarray, background: np.ndarray,
506
+ fallback_enabled: bool = True) -> np.ndarray:
507
+ """
508
+ Enhanced background replacement with comprehensive error handling and quality improvements
509
+
510
+ Args:
511
+ frame: Input frame (H, W, 3)
512
+ mask: Binary mask (H, W) with values 0-255
513
+ background: Background image (H, W, 3)
514
+ fallback_enabled: Whether to use fallback if main method fails
515
+
516
+ Returns:
517
+ Composited frame (H, W, 3)
518
+
519
+ Raises:
520
+ BackgroundReplacementError: If replacement fails and fallback is disabled
521
+ """
522
+ if frame is None or mask is None or background is None:
523
+ raise BackgroundReplacementError("Invalid input frame, mask, or background")
524
+
525
  try:
526
  # Resize background to match frame
527
+ background = cv2.resize(background, (frame.shape[1], frame.shape[0]),
528
+ interpolation=cv2.INTER_LANCZOS4)
529
 
530
+ # Process mask
531
  if len(mask.shape) == 3:
532
  mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
533
 
 
534
  if mask.dtype != np.uint8:
535
  mask = mask.astype(np.uint8)
536
 
537
  if mask.max() <= 1.0:
538
+ logger.debug("Converting normalized mask to 0-255 range")
539
  mask = (mask * 255).astype(np.uint8)
540
 
541
+ # Enhanced compositing with multiple techniques
542
+ try:
543
+ result = _advanced_compositing(frame, mask, background)
544
+ logger.debug("Advanced compositing successful")
545
+ return result
546
+
547
+ except Exception as e:
548
+ logger.warning(f"Advanced compositing failed: {e}")
549
+ if fallback_enabled:
550
+ return _simple_compositing(frame, mask, background)
551
+ else:
552
+ raise BackgroundReplacementError(f"Advanced compositing failed: {e}")
553
 
554
+ except BackgroundReplacementError:
555
+ raise
556
+ except Exception as e:
557
+ logger.error(f"Unexpected background replacement error: {e}")
558
+ if fallback_enabled:
559
+ return _simple_compositing(frame, mask, background)
560
+ else:
561
+ raise BackgroundReplacementError(f"Unexpected error: {e}")
562
+
563
+ def _advanced_compositing(frame: np.ndarray, mask: np.ndarray, background: np.ndarray) -> np.ndarray:
564
+ """Advanced compositing with edge feathering and color correction"""
565
+ try:
566
+ # Create high-quality alpha mask
567
+ threshold = 100 # Lower threshold for better person extraction
568
  _, mask_binary = cv2.threshold(mask, threshold, 255, cv2.THRESH_BINARY)
569
 
570
+ # Clean up mask
571
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
572
+ mask_binary = cv2.morphologyEx(mask_binary, cv2.MORPH_CLOSE, kernel)
573
+ mask_binary = cv2.morphologyEx(mask_binary, cv2.MORPH_OPEN, kernel)
574
 
575
+ # Create smooth alpha channel with edge feathering
576
  mask_smooth = cv2.GaussianBlur(mask_binary.astype(np.float32), (5, 5), 1.0)
577
+ mask_smooth = mask_smooth / 255.0
578
 
579
+ # Apply gamma correction for better blending
580
+ mask_smooth = np.power(mask_smooth, 0.8)
581
 
582
+ # Enhance edges - boost values near 1.0, reduce values near 0.0
583
  mask_smooth = np.where(mask_smooth > 0.5,
584
+ np.minimum(mask_smooth * 1.1, 1.0),
585
+ mask_smooth * 0.9)
586
+
587
+ # Color matching between foreground and background
588
+ frame_adjusted = _color_match_edges(frame, background, mask_smooth)
589
 
590
+ # Create 3-channel alpha mask
591
+ alpha_3ch = np.stack([mask_smooth] * 3, axis=2)
592
 
593
+ # Perform high-quality compositing
594
+ frame_float = frame_adjusted.astype(np.float32)
595
  background_float = background.astype(np.float32)
596
 
597
+ # Alpha blending with gamma correction
598
+ result = frame_float * alpha_3ch + background_float * (1 - alpha_3ch)
599
  result = np.clip(result, 0, 255).astype(np.uint8)
600
 
 
 
 
601
  return result
602
 
603
  except Exception as e:
604
+ logger.error(f"Advanced compositing error: {e}")
605
+ raise
606
+
607
+ def _color_match_edges(frame: np.ndarray, background: np.ndarray, alpha: np.ndarray) -> np.ndarray:
608
+ """Subtle color matching at edges to reduce halos"""
609
+ try:
610
+ # Find edge regions (transition areas)
611
+ edge_mask = cv2.Sobel(alpha, cv2.CV_64F, 1, 1, ksize=3)
612
+ edge_mask = np.abs(edge_mask)
613
+ edge_mask = (edge_mask > 0.1).astype(np.float32)
614
+
615
+ # Calculate color difference in edge regions
616
+ edge_areas = edge_mask > 0
617
+ if not np.any(edge_areas):
618
  return frame
619
+
620
+ # Subtle color adjustment
621
+ frame_adjusted = frame.copy().astype(np.float32)
622
+ background_float = background.astype(np.float32)
623
+
624
+ # Apply very subtle color shift towards background in edge areas
625
+ adjustment_strength = 0.1
626
+ for c in range(3):
627
+ frame_adjusted[:, :, c] = np.where(
628
+ edge_areas,
629
+ frame_adjusted[:, :, c] * (1 - adjustment_strength) +
630
+ background_float[:, :, c] * adjustment_strength,
631
+ frame_adjusted[:, :, c]
632
+ )
633
+
634
+ return np.clip(frame_adjusted, 0, 255).astype(np.uint8)
635
+
636
+ except Exception as e:
637
+ logger.warning(f"Color matching failed: {e}")
638
+ return frame
639
 
640
+ def _simple_compositing(frame: np.ndarray, mask: np.ndarray, background: np.ndarray) -> np.ndarray:
641
+ """Simple fallback compositing method"""
642
+ try:
643
+ logger.info("Using simple compositing fallback")
644
+
645
+ # Resize background
646
+ background = cv2.resize(background, (frame.shape[1], frame.shape[0]))
647
+
648
+ # Process mask
649
+ if len(mask.shape) == 3:
650
+ mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
651
+ if mask.max() <= 1.0:
652
+ mask = (mask * 255).astype(np.uint8)
653
+
654
+ # Simple binary threshold
655
+ _, mask_binary = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
656
+
657
+ # Create alpha mask
658
+ mask_norm = mask_binary.astype(np.float32) / 255.0
659
+ mask_3ch = np.stack([mask_norm] * 3, axis=2)
660
+
661
+ # Simple alpha blending
662
+ result = frame * mask_3ch + background * (1 - mask_3ch)
663
+ return result.astype(np.uint8)
664
+
665
+ except Exception as e:
666
+ logger.error(f"Simple compositing failed: {e}")
667
+ # Last resort: return original frame
668
+ return frame
669
+
670
+ def create_professional_background(bg_config: Dict[str, Any], width: int, height: int) -> np.ndarray:
671
+ """Enhanced professional background creation with quality improvements"""
672
  try:
673
  if bg_config["type"] == "color":
674
+ background = _create_solid_background(bg_config, width, height)
 
 
 
675
  elif bg_config["type"] == "gradient":
676
+ background = _create_gradient_background_enhanced(bg_config, width, height)
677
  else:
678
+ # Fallback to neutral gray
679
  background = np.full((height, width, 3), (128, 128, 128), dtype=np.uint8)
680
+
681
+ # Apply brightness and contrast adjustments
682
+ background = _apply_background_adjustments(background, bg_config)
683
+
684
  return background
685
+
686
  except Exception as e:
687
  logger.error(f"Background creation error: {e}")
688
  return np.full((height, width, 3), (128, 128, 128), dtype=np.uint8)
689
 
690
+ def _create_solid_background(bg_config: Dict[str, Any], width: int, height: int) -> np.ndarray:
691
+ """Create solid color background"""
692
+ color_hex = bg_config["colors"][0].lstrip('#')
693
+ color_rgb = tuple(int(color_hex[i:i+2], 16) for i in (0, 2, 4))
694
+ color_bgr = color_rgb[::-1]
695
+ return np.full((height, width, 3), color_bgr, dtype=np.uint8)
696
+
697
+ def _create_gradient_background_enhanced(bg_config: Dict[str, Any], width: int, height: int) -> np.ndarray:
698
+ """Create enhanced gradient background with better quality"""
699
  try:
700
  colors = bg_config["colors"]
701
  direction = bg_config.get("direction", "vertical")
 
710
  if not rgb_colors:
711
  rgb_colors = [(128, 128, 128)]
712
 
713
+ # Use NumPy for better performance on large images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  if direction == "vertical":
715
+ background = _create_vertical_gradient(rgb_colors, width, height)
 
 
 
716
  elif direction == "horizontal":
717
+ background = _create_horizontal_gradient(rgb_colors, width, height)
 
 
 
718
  elif direction == "diagonal":
719
+ background = _create_diagonal_gradient(rgb_colors, width, height)
 
 
 
 
 
 
720
  elif direction in ["radial", "soft_radial"]:
721
+ background = _create_radial_gradient(rgb_colors, width, height, direction == "soft_radial")
722
+ else:
723
+ background = _create_vertical_gradient(rgb_colors, width, height)
724
+
725
+ return cv2.cvtColor(background, cv2.COLOR_RGB2BGR)
 
 
 
 
 
 
 
 
 
 
726
 
727
  except Exception as e:
728
  logger.error(f"Gradient creation error: {e}")
729
+ return np.full((height, width, 3), (128, 128, 128), dtype=np.uint8)
 
730
 
731
+ def _create_vertical_gradient(colors: list, width: int, height: int) -> np.ndarray:
732
+ """Create vertical gradient using NumPy for performance"""
733
+ gradient = np.zeros((height, width, 3), dtype=np.uint8)
 
 
 
 
 
 
734
 
735
+ for y in range(height):
736
+ progress = y / height if height > 0 else 0
737
+ color = _interpolate_color(colors, progress)
738
+ gradient[y, :] = color
 
739
 
740
+ return gradient
741
+
742
+ def _create_horizontal_gradient(colors: list, width: int, height: int) -> np.ndarray:
743
+ """Create horizontal gradient using NumPy for performance"""
744
+ gradient = np.zeros((height, width, 3), dtype=np.uint8)
745
+
746
+ for x in range(width):
747
+ progress = x / width if width > 0 else 0
748
+ color = _interpolate_color(colors, progress)
749
+ gradient[:, x] = color
750
+
751
+ return gradient
752
+
753
+ def _create_diagonal_gradient(colors: list, width: int, height: int) -> np.ndarray:
754
+ """Create diagonal gradient using vectorized operations"""
755
+ y_coords, x_coords = np.mgrid[0:height, 0:width]
756
+ max_distance = width + height
757
+ progress = (x_coords + y_coords) / max_distance
758
+ progress = np.clip(progress, 0, 1)
759
+
760
+ # Vectorized color interpolation
761
+ gradient = np.zeros((height, width, 3), dtype=np.uint8)
762
+ for c in range(3):
763
+ gradient[:, :, c] = _vectorized_color_interpolation(colors, progress, c)
764
+
765
+ return gradient
766
+
767
+ def _create_radial_gradient(colors: list, width: int, height: int, soft: bool = False) -> np.ndarray:
768
+ """Create radial gradient using vectorized operations"""
769
+ center_x, center_y = width // 2, height // 2
770
+ max_distance = np.sqrt(center_x**2 + center_y**2)
771
+
772
+ y_coords, x_coords = np.mgrid[0:height, 0:width]
773
+ distances = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
774
+ progress = distances / max_distance
775
+ progress = np.clip(progress, 0, 1)
776
+
777
+ if soft:
778
+ progress = np.power(progress, 0.7)
779
+
780
+ # Vectorized color interpolation
781
+ gradient = np.zeros((height, width, 3), dtype=np.uint8)
782
+ for c in range(3):
783
+ gradient[:, :, c] = _vectorized_color_interpolation(colors, progress, c)
784
+
785
+ return gradient
786
 
787
+ def _vectorized_color_interpolation(colors: list, progress: np.ndarray, channel: int) -> np.ndarray:
788
+ """Vectorized color interpolation for performance"""
789
+ if len(colors) == 1:
790
+ return np.full_like(progress, colors[0][channel], dtype=np.uint8)
791
+
792
+ num_segments = len(colors) - 1
793
+ segment_progress = progress * num_segments
794
+ segment_indices = np.floor(segment_progress).astype(int)
795
+ segment_indices = np.clip(segment_indices, 0, num_segments - 1)
796
+ local_progress = segment_progress - segment_indices
797
+
798
+ # Get start and end colors for each pixel
799
+ start_colors = np.array([colors[i][channel] for i in range(len(colors))])
800
+ end_colors = np.array([colors[min(i + 1, len(colors) - 1)][channel] for i in range(len(colors))])
801
+
802
+ start_vals = start_colors[segment_indices]
803
+ end_vals = end_colors[segment_indices]
804
+
805
+ result = start_vals + (end_vals - start_vals) * local_progress
806
+ return np.clip(result, 0, 255).astype(np.uint8)
807
+
808
+ def _interpolate_color(colors: list, progress: float) -> tuple:
809
+ """Interpolate between multiple colors"""
810
+ if len(colors) == 1:
811
+ return colors[0]
812
+ elif len(colors) == 2:
813
+ r = int(colors[0][0] + (colors[1][0] - colors[0][0]) * progress)
814
+ g = int(colors[0][1] + (colors[1][1] - colors[0][1]) * progress)
815
+ b = int(colors[0][2] + (colors[1][2] - colors[0][2]) * progress)
816
+ return (r, g, b)
817
+ else:
818
+ segment = progress * (len(colors) - 1)
819
+ idx = int(segment)
820
+ local_progress = segment - idx
821
+ if idx >= len(colors) - 1:
822
+ return colors[-1]
823
+ c1, c2 = colors[idx], colors[idx + 1]
824
+ r = int(c1[0] + (c2[0] - c1[0]) * local_progress)
825
+ g = int(c1[1] + (c2[1] - c1[1]) * local_progress)
826
+ b = int(c1[2] + (c2[2] - c1[2]) * local_progress)
827
+ return (r, g, b)
828
+
829
+ def _apply_background_adjustments(background: np.ndarray, bg_config: Dict[str, Any]) -> np.ndarray:
830
+ """Apply brightness and contrast adjustments to background"""
831
+ try:
832
+ brightness = bg_config.get("brightness", 1.0)
833
+ contrast = bg_config.get("contrast", 1.0)
834
+
835
+ if brightness != 1.0 or contrast != 1.0:
836
+ background = background.astype(np.float32)
837
+ background = background * contrast * brightness
838
+ background = np.clip(background, 0, 255).astype(np.uint8)
839
+
840
+ return background
841
+
842
+ except Exception as e:
843
+ logger.warning(f"Background adjustment failed: {e}")
844
+ return background
845
+
846
+ def validate_video_file(video_path: str) -> Tuple[bool, str]:
847
+ """Enhanced video file validation with detailed checks"""
848
  if not video_path or not os.path.exists(video_path):
849
  return False, "Video file not found"
850
+
851
  try:
852
+ # Check file size
853
+ file_size = os.path.getsize(video_path)
854
+ if file_size == 0:
855
+ return False, "Video file is empty"
856
+
857
+ if file_size > 2 * 1024 * 1024 * 1024: # 2GB limit
858
+ return False, "Video file too large (>2GB)"
859
+
860
+ # Check with OpenCV
861
  cap = cv2.VideoCapture(video_path)
862
  if not cap.isOpened():
863
  return False, "Cannot open video file"
864
+
865
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
866
+ fps = cap.get(cv2.CAP_PROP_FPS)
867
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
868
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
869
+
870
  cap.release()
871
+
872
+ # Validation checks
873
+ if frame_count == 0:
874
+ return False, "Video appears to be empty (0 frames)"
875
+
876
+ if fps <= 0 or fps > 120:
877
+ return False, f"Invalid frame rate: {fps}"
878
+
879
+ if width <= 0 or height <= 0:
880
+ return False, f"Invalid resolution: {width}x{height}"
881
+
882
+ if width > 4096 or height > 4096:
883
+ return False, f"Resolution too high: {width}x{height} (max 4096x4096)"
884
+
885
+ duration = frame_count / fps
886
+ if duration > 300: # 5 minutes
887
+ return False, f"Video too long: {duration:.1f}s (max 300s)"
888
+
889
+ return True, f"Valid video: {width}x{height}, {fps:.1f}fps, {duration:.1f}s"
890
+
891
  except Exception as e:
892
  return False, f"Error validating video: {str(e)}"