MogensR commited on
Commit
92735bf
Β·
1 Parent(s): 82a2981

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +670 -270
app.py CHANGED
@@ -1,416 +1,816 @@
1
  #!/usr/bin/env python3
2
  """
3
- SAM2 (Segment Anything Model 2) for Video
4
- Correct implementation with dynamic model loading
5
- Optimized for video processing
6
  """
7
 
 
 
 
 
8
  import os
 
 
 
 
 
9
  import torch
10
- import numpy as np
11
- import streamlit as st
12
  from pathlib import Path
13
- import logging
14
- import requests
15
  from tqdm import tqdm
16
- import cv2
17
 
 
 
18
  logger = logging.getLogger(__name__)
19
 
20
  # ============================================
21
- # SAM2 DYNAMIC LOADER FOR VIDEO
22
  # ============================================
23
 
24
- @st.cache_resource(show_spinner=False)
25
- def load_sam2_model_dynamic():
26
- """
27
- Download and load SAM2 model dynamically
28
- SAM2 is specifically designed for video segmentation
29
- """
30
  try:
31
- # Import SAM2 (not SAM1!)
32
- from sam2.build_sam import build_sam2
33
- from sam2.sam2_image_predictor import SAM2ImagePredictor
34
- from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
35
-
36
- # SAM2 Model URLs (these are the NEW video-optimized models)
37
- MODEL_URLS = {
38
- 'sam2_hiera_large': {
39
- 'config': 'sam2_hiera_l.yaml',
40
- 'checkpoint': 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_large.pt',
41
- 'size': '897MB',
42
- 'quality': 'Best for video'
43
- },
44
- 'sam2_hiera_base_plus': {
45
- 'config': 'sam2_hiera_b+.yaml',
46
- 'checkpoint': 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_base_plus.pt',
47
- 'size': '323MB',
48
- 'quality': 'Balanced'
49
- },
50
- 'sam2_hiera_small': {
51
- 'config': 'sam2_hiera_s.yaml',
52
- 'checkpoint': 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_small.pt',
53
- 'size': '155MB',
54
- 'quality': 'Fast'
55
- },
56
- 'sam2_hiera_tiny': {
57
- 'config': 'sam2_hiera_t.yaml',
58
- 'checkpoint': 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_tiny.pt',
59
- 'size': '77MB',
60
- 'quality': 'Fastest'
61
- }
62
- }
63
-
64
- # Choose model based on GPU
65
  if torch.cuda.is_available():
 
66
  gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1024**3
67
- if gpu_memory > 20: # L4 has 24GB
68
- model_name = 'sam2_hiera_large'
69
- elif gpu_memory > 10:
70
- model_name = 'sam2_hiera_base_plus'
71
- elif gpu_memory > 6:
72
- model_name = 'sam2_hiera_small'
73
- else:
74
- model_name = 'sam2_hiera_tiny'
 
 
 
 
 
 
 
 
 
 
75
  else:
76
- model_name = 'sam2_hiera_tiny' # CPU = smallest
77
-
78
- logger.info(f"Selected SAM2 model: {model_name} ({MODEL_URLS[model_name]['quality']})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- # Setup cache directory
81
- cache_dir = Path("/tmp/sam2_models")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  cache_dir.mkdir(exist_ok=True)
83
 
84
- model_path = cache_dir / f"{model_name}.pt"
85
- config_name = MODEL_URLS[model_name]['config']
86
 
87
- # Download if not cached
88
  if not model_path.exists():
89
- logger.info(f"Downloading SAM2 {model_name} ({MODEL_URLS[model_name]['size']})...")
90
-
91
- # Show progress in Streamlit
92
- progress_text = st.empty()
93
- progress_bar = st.progress(0)
94
-
95
- # Download with progress
96
- response = requests.get(MODEL_URLS[model_name]['checkpoint'], stream=True)
97
- total_size = int(response.headers.get('content-length', 0))
98
-
99
- with open(model_path, 'wb') as f:
100
- downloaded = 0
101
- for chunk in response.iter_content(chunk_size=8192):
102
- f.write(chunk)
103
- downloaded += len(chunk)
104
-
105
- if total_size > 0:
106
- progress = downloaded / total_size
107
- progress_bar.progress(progress)
108
- progress_text.text(f"Downloading SAM2: {downloaded/(1024**2):.1f}MB / {total_size/(1024**2):.1f}MB")
109
-
110
- progress_text.empty()
111
- progress_bar.empty()
112
-
113
- logger.info(f"βœ… SAM2 model downloaded to {model_path}")
114
- else:
115
- logger.info(f"βœ… Using cached SAM2 model from {model_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- # Build SAM2 model
118
- device = 'cuda' if torch.cuda.is_available() else 'cpu'
 
119
 
120
- sam2_model = build_sam2(
121
- config_file=config_name,
122
- ckpt_path=str(model_path),
123
- device=device,
124
- apply_postprocessing=True
125
- )
126
 
127
- # Create predictor for frame-by-frame processing
128
- predictor = SAM2ImagePredictor(sam2_model)
129
 
130
- logger.info(f"βœ… SAM2 loaded successfully on {device}")
131
- return predictor, model_name
 
 
 
 
 
132
 
133
- except ImportError as e:
134
- logger.error(f"SAM2 not installed. Install with: pip install sam-2")
135
- return None, None
136
- except Exception as e:
137
- logger.error(f"Failed to load SAM2 model: {e}")
138
- return None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  # ============================================
141
- # SAM2 VIDEO PROCESSOR
142
  # ============================================
143
 
144
- class SAM2VideoProcessor:
145
- """
146
- SAM2 optimized for video processing
147
- Handles temporal consistency across frames
148
- """
149
 
150
  def __init__(self):
151
  self.predictor = None
152
- self.model_name = None
153
  self.loaded = False
154
  self.previous_mask = None
155
- self.frame_count = 0
156
 
157
- def load_model(self):
158
- """Load SAM2 model if not already loaded"""
159
- if not self.loaded:
160
- with st.spinner("🎬 Loading SAM2 Video Model..."):
161
- self.predictor, self.model_name = load_sam2_model_dynamic()
162
- self.loaded = True
163
- if self.predictor:
164
- logger.info(f"SAM2 Video Processor ready with {self.model_name}")
165
- return self.predictor is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- def segment_frame(self, frame, use_previous=True):
168
- """
169
- Segment a single frame with temporal consistency
170
-
171
- Args:
172
- frame: Input frame (H, W, 3) numpy array
173
- use_previous: Use previous frame's mask for consistency
174
-
175
- Returns:
176
- mask: Segmentation mask (H, W) float32
177
- """
178
- if not self.load_model():
179
  return None
180
-
181
  try:
182
- # Set the image
183
  self.predictor.set_image(frame)
184
 
185
  h, w = frame.shape[:2]
186
 
187
- # Generate point prompts
188
- if use_previous and self.previous_mask is not None:
189
- # Use previous mask to guide current segmentation
190
- # Find center of mass of previous mask
191
  y_coords, x_coords = np.where(self.previous_mask > 0.5)
192
  if len(y_coords) > 0:
193
  center_y = int(np.mean(y_coords))
194
  center_x = int(np.mean(x_coords))
195
-
196
- # Add points around previous center
197
- point_coords = np.array([
198
- [center_x, center_y],
199
- [center_x, center_y - h//8], # Above
200
- [center_x, center_y + h//8], # Below
201
- ])
202
  else:
203
- # Fallback to center points
204
- point_coords = np.array([
205
- [w//2, h//2],
206
- [w//2, h//3],
207
- [w//2, 2*h//3]
208
- ])
209
  else:
210
- # Initial frame - use center points
211
- point_coords = np.array([
212
- [w//2, h//2], # Center
213
- [w//2, h//3], # Upper (head)
214
- [w//2, 2*h//3], # Lower (body)
215
- [w//3, h//2], # Left
216
- [2*w//3, h//2], # Right
217
- ])
218
-
219
- point_labels = np.ones(len(point_coords)) # All foreground
220
-
221
- # Generate masks with SAM2
222
- masks, scores, logits = self.predictor.predict(
223
  point_coords=point_coords,
224
  point_labels=point_labels,
225
- multimask_output=True,
226
- return_logits=True
227
  )
228
 
229
- # Select best mask
230
- best_idx = np.argmax(scores)
231
- mask = masks[best_idx].astype(np.float32)
232
 
233
- # Apply temporal smoothing if we have previous mask
234
- if use_previous and self.previous_mask is not None:
235
- # Blend with previous mask for temporal consistency
236
- alpha = 0.3 # Smoothing factor
237
- mask = (1 - alpha) * mask + alpha * self.previous_mask
238
- mask = np.clip(mask, 0, 1)
239
 
240
- # Post-processing for better quality
241
- # Morphological operations
242
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
243
  mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
244
- mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
245
-
246
- # Gaussian blur for smooth edges
247
- mask = cv2.GaussianBlur(mask, (7, 7), 0)
248
-
249
- # Store for next frame
250
- self.previous_mask = mask.copy()
251
- self.frame_count += 1
252
 
 
253
  return mask
254
 
255
  except Exception as e:
256
- logger.error(f"SAM2 segmentation failed: {e}")
257
  return None
258
 
259
  def reset(self):
260
- """Reset temporal state for new video"""
261
  self.previous_mask = None
262
- self.frame_count = 0
263
- logger.info("SAM2 Video Processor reset for new video")
264
 
265
  # ============================================
266
- # LAZY LOADER FOR SAM2
267
  # ============================================
268
 
269
- class SAM2LazyLoader:
270
- """
271
- Lazy loading for SAM2 - only loads when needed
272
- """
273
- def __init__(self):
274
- self.processor = SAM2VideoProcessor()
275
 
276
- def segment_frame(self, frame, use_temporal=True):
277
- """
278
- Segment frame with lazy loading
279
- Model loads on first call
280
- """
281
- return self.processor.segment_frame(frame, use_previous=use_temporal)
282
 
283
- def reset(self):
284
- """Reset for new video"""
285
- self.processor.reset()
286
 
287
- @property
288
- def is_available(self):
289
- """Check if SAM2 can be loaded"""
290
- try:
291
- import sam2
292
- return True
293
- except ImportError:
294
- return False
295
 
296
- @property
297
- def is_loaded(self):
298
- """Check if model is already loaded"""
299
- return self.processor.loaded
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
  # ============================================
302
- # INTEGRATION WITH VIDEO PROCESSING
303
  # ============================================
304
 
305
- # Global SAM2 instance
306
- SAM2_VIDEO = SAM2LazyLoader()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- def process_video_with_sam2(video_path, background_image, progress_callback=None):
309
- """
310
- Process video using SAM2 with temporal consistency
311
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  try:
 
 
 
313
  # Open video
314
  cap = cv2.VideoCapture(video_path)
315
-
316
- # Get video properties
317
  fps = int(cap.get(cv2.CAP_PROP_FPS))
318
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
319
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
320
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
321
 
322
- # Create output writer
323
- output_path = '/tmp/output_sam2.mp4'
 
 
324
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
325
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
326
 
327
  # Resize background
328
  background_resized = cv2.resize(background_image, (width, height))
329
 
330
- # Reset SAM2 for new video
331
- SAM2_VIDEO.reset()
 
332
 
333
  frame_count = 0
 
334
 
335
  while True:
336
  ret, frame = cap.read()
337
  if not ret:
338
  break
339
 
 
 
340
  # Convert BGR to RGB
341
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
342
 
343
- # Segment with SAM2 (with temporal consistency)
344
- mask = SAM2_VIDEO.segment_frame(frame_rgb, use_temporal=(frame_count > 0))
 
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  if mask is not None:
347
- # Apply mask
348
  if mask.ndim == 2:
349
  mask = np.expand_dims(mask, axis=2)
350
 
351
- # Composite
352
- composite = frame_rgb * mask + background_resized * (1 - mask)
353
- composite = composite.astype(np.uint8)
354
 
355
- # Convert back to BGR
356
- composite_bgr = cv2.cvtColor(composite, cv2.COLOR_RGB2BGR)
357
  else:
358
- composite_bgr = frame
359
 
 
 
360
  out.write(composite_bgr)
 
 
 
 
 
361
  frame_count += 1
362
 
363
- # Progress callback
364
  if progress_callback:
365
  progress = frame_count / total_frames
366
- progress_callback(progress, f"SAM2 Processing: {frame_count}/{total_frames}")
 
 
 
 
 
367
 
368
- # Memory cleanup every 50 frames
369
- if frame_count % 50 == 0 and torch.cuda.is_available():
370
  torch.cuda.empty_cache()
371
 
 
372
  cap.release()
373
  out.release()
374
 
375
- logger.info(f"βœ… SAM2 video processing complete: {frame_count} frames")
 
 
 
 
 
 
376
  return output_path
377
 
378
  except Exception as e:
379
- logger.error(f"SAM2 video processing failed: {e}")
380
  return None
381
 
382
  # ============================================
383
- # EXAMPLE USAGE
384
  # ============================================
385
 
386
  def main():
387
- st.title("🎬 Video Background Replacer with SAM2")
 
 
 
 
 
388
 
389
- # Status display
390
- col1, col2, col3 = st.columns(3)
 
 
 
 
391
 
392
  with col1:
393
- if SAM2_VIDEO.is_available:
394
- if SAM2_VIDEO.is_loaded:
395
- st.success("βœ… SAM2 Loaded")
396
- else:
397
- st.info("🎯 SAM2 Ready (loads on demand)")
398
  else:
399
- st.error("❌ SAM2 not installed")
400
 
401
- # Process button
402
- if st.button("Process with SAM2"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  if uploaded_video:
404
- # This triggers model download on first use
405
- result = process_video_with_sam2(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  video_path,
407
- background_image,
 
408
  progress_callback=update_progress
409
  )
410
 
411
- if result:
412
- st.success("βœ… Video processed with SAM2!")
413
- st.video(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
  if __name__ == "__main__":
416
  main()
 
1
  #!/usr/bin/env python3
2
  """
3
+ BackgroundFX - Professional Video Background Replacement
4
+ Priority: MatAnyone > SAM2 > Rembg > OpenCV
5
+ Optimized for HuggingFace Spaces L4 GPU
6
  """
7
 
8
+ import streamlit as st
9
+ import cv2
10
+ import numpy as np
11
+ import tempfile
12
  import os
13
+ from PIL import Image
14
+ import requests
15
+ from io import BytesIO
16
+ import logging
17
+ import gc
18
  import torch
19
+ import time
 
20
  from pathlib import Path
 
 
21
  from tqdm import tqdm
 
22
 
23
+ # Configure logging
24
+ logging.basicConfig(level=logging.INFO)
25
  logger = logging.getLogger(__name__)
26
 
27
  # ============================================
28
+ # GPU SETUP AND INITIALIZATION
29
  # ============================================
30
 
31
+ def setup_gpu_environment():
32
+ """Setup GPU environment with optimal settings for L4"""
33
+ os.environ['CUDA_VISIBLE_DEVICES'] = '0'
34
+ os.environ['TORCH_CUDA_ARCH_LIST'] = '8.9' # L4 architecture
35
+ os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:512'
36
+
37
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  if torch.cuda.is_available():
39
+ gpu_name = torch.cuda.get_device_name(0)
40
  gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1024**3
41
+
42
+ logger.info(f"πŸš€ GPU Detected: {gpu_name} ({gpu_memory:.1f}GB)")
43
+
44
+ # Initialize CUDA
45
+ torch.cuda.init()
46
+ torch.cuda.set_device(0)
47
+
48
+ # Enable TF32 for L4
49
+ torch.backends.cuda.matmul.allow_tf32 = True
50
+ torch.backends.cudnn.allow_tf32 = True
51
+ torch.backends.cudnn.benchmark = True
52
+
53
+ # Warm up
54
+ dummy = torch.randn(512, 512, device='cuda')
55
+ del dummy
56
+ torch.cuda.empty_cache()
57
+
58
+ return True, gpu_name, gpu_memory
59
  else:
60
+ logger.warning("⚠️ CUDA not available - running in CPU mode")
61
+ return False, None, 0
62
+ except Exception as e:
63
+ logger.error(f"GPU setup failed: {e}")
64
+ return False, None, 0
65
+
66
+ # Initialize GPU
67
+ CUDA_AVAILABLE, GPU_NAME, GPU_MEMORY = setup_gpu_environment()
68
+ DEVICE = 'cuda' if CUDA_AVAILABLE else 'cpu'
69
+
70
+ # ============================================
71
+ # MATANYONE - PRIMARY METHOD (BEST QUALITY)
72
+ # ============================================
73
+
74
+ class MatAnyoneProcessor:
75
+ """MatAnyone for superior video matting with temporal consistency"""
76
+
77
+ def __init__(self):
78
+ self.model = None
79
+ self.predictor = None
80
+ self.loaded = False
81
+ self.previous_alpha = None
82
+ self.previous_trimap = None
83
+ self.frame_count = 0
84
 
85
+ @st.cache_resource
86
+ def load_model(_self):
87
+ """Load MatAnyone model with caching"""
88
+ try:
89
+ # Try to import MatAnyone
90
+ from matanyone import MatAnyoneModel, MatAnyonePredictor
91
+
92
+ # Download model if needed
93
+ model_path = _self._download_model_if_needed()
94
+
95
+ # Load model
96
+ model = MatAnyoneModel.from_pretrained(
97
+ model_path,
98
+ device=DEVICE,
99
+ fp16=(DEVICE == 'cuda')
100
+ )
101
+
102
+ # Create predictor
103
+ predictor = MatAnyonePredictor(
104
+ model,
105
+ enable_temporal=True,
106
+ enable_refinement=True,
107
+ alpha_quality='high'
108
+ )
109
+
110
+ logger.info("βœ… MatAnyone loaded successfully")
111
+ return model, predictor, True
112
+
113
+ except ImportError:
114
+ logger.warning("⚠️ MatAnyone not installed, falling back to other methods")
115
+ return None, None, False
116
+ except Exception as e:
117
+ logger.error(f"❌ MatAnyone loading failed: {e}")
118
+ return None, None, False
119
+
120
+ def _download_model_if_needed(self):
121
+ """Download MatAnyone model dynamically"""
122
+ cache_dir = Path("/tmp/matanyone_models")
123
  cache_dir.mkdir(exist_ok=True)
124
 
125
+ model_path = cache_dir / "matanyone_video.pth"
 
126
 
 
127
  if not model_path.exists():
128
+ # MatAnyone model URL
129
+ model_url = "https://huggingface.co/matanyone/matanyone-video/resolve/main/model.pth"
130
+
131
+ with st.spinner("Downloading MatAnyone model (first time only)..."):
132
+ response = requests.get(model_url, stream=True)
133
+ total_size = int(response.headers.get('content-length', 0))
134
+
135
+ progress_bar = st.progress(0)
136
+ with open(model_path, 'wb') as f:
137
+ downloaded = 0
138
+ for chunk in response.iter_content(chunk_size=8192):
139
+ f.write(chunk)
140
+ downloaded += len(chunk)
141
+ if total_size > 0:
142
+ progress_bar.progress(downloaded / total_size)
143
+
144
+ progress_bar.empty()
145
+
146
+ return str(model_path)
147
+
148
+ def process_frame(self, frame, use_temporal=True):
149
+ """Process frame with MatAnyone"""
150
+ if not self.loaded:
151
+ self.model, self.predictor, self.loaded = self.load_model()
152
+
153
+ if not self.loaded or self.predictor is None:
154
+ return None
155
+
156
+ try:
157
+ # Generate or update trimap
158
+ if use_temporal and self.previous_trimap is not None:
159
+ trimap = self._update_trimap(self.previous_trimap, frame)
160
+ else:
161
+ trimap = self._generate_trimap(frame)
162
+
163
+ # Process with temporal consistency
164
+ if use_temporal and self.previous_alpha is not None:
165
+ alpha = self.predictor.predict(
166
+ image=frame,
167
+ trimap=trimap,
168
+ previous_alpha=self.previous_alpha,
169
+ temporal_weight=0.3
170
+ )
171
+ else:
172
+ alpha = self.predictor.predict(image=frame, trimap=trimap)
173
+
174
+ # Refine alpha
175
+ alpha = self._refine_alpha(alpha, frame)
176
+
177
+ # Store for next frame
178
+ self.previous_alpha = alpha.copy()
179
+ self.previous_trimap = trimap.copy()
180
+ self.frame_count += 1
181
+
182
+ return alpha
183
+
184
+ except Exception as e:
185
+ logger.error(f"MatAnyone processing failed: {e}")
186
+ return None
187
+
188
+ def _generate_trimap(self, frame):
189
+ """Generate initial trimap"""
190
+ h, w = frame.shape[:2]
191
+ trimap = np.zeros((h, w), dtype=np.uint8)
192
 
193
+ # Create center region as unknown
194
+ center_x, center_y = w // 2, h // 2
195
+ radius_x, radius_y = w // 3, h // 2
196
 
197
+ y, x = np.ogrid[:h, :w]
198
+ mask = ((x - center_x)**2 / radius_x**2 + (y - center_y)**2 / radius_y**2) <= 1
199
+ trimap[mask] = 128 # Unknown
 
 
 
200
 
201
+ inner_mask = ((x - center_x)**2 / (radius_x*0.5)**2 + (y - center_y)**2 / (radius_y*0.5)**2) <= 1
202
+ trimap[inner_mask] = 255 # Foreground
203
 
204
+ return trimap
205
+
206
+ def _update_trimap(self, prev_trimap, frame):
207
+ """Update trimap with motion compensation"""
208
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
209
+ unknown = (prev_trimap == 128).astype(np.uint8)
210
+ unknown = cv2.dilate(unknown, kernel, iterations=1)
211
 
212
+ trimap = prev_trimap.copy()
213
+ trimap[unknown == 1] = 128
214
+
215
+ return trimap
216
+
217
+ def _refine_alpha(self, alpha, frame):
218
+ """Refine alpha matte"""
219
+ # Guided filter if available
220
+ try:
221
+ alpha = cv2.ximgproc.guidedFilter(frame, alpha, 5, 1e-4)
222
+ except:
223
+ # Fallback to Gaussian blur
224
+ alpha = cv2.GaussianBlur(alpha, (5, 5), 0)
225
+
226
+ return np.clip(alpha, 0, 1)
227
+
228
+ def reset(self):
229
+ """Reset for new video"""
230
+ self.previous_alpha = None
231
+ self.previous_trimap = None
232
+ self.frame_count = 0
233
 
234
  # ============================================
235
+ # SAM2 - SECONDARY METHOD (VIDEO OPTIMIZED)
236
  # ============================================
237
 
238
+ class SAM2Processor:
239
+ """SAM2 for video segmentation"""
 
 
 
240
 
241
  def __init__(self):
242
  self.predictor = None
 
243
  self.loaded = False
244
  self.previous_mask = None
 
245
 
246
+ @st.cache_resource
247
+ def load_model(_self):
248
+ """Load SAM2 model dynamically"""
249
+ try:
250
+ from sam2.build_sam import build_sam2
251
+ from sam2.sam2_image_predictor import SAM2ImagePredictor
252
+
253
+ # Model configurations
254
+ models = {
255
+ 'large': ('sam2_hiera_l.yaml', 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_large.pt', 897),
256
+ 'base': ('sam2_hiera_b+.yaml', 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_base_plus.pt', 323),
257
+ 'small': ('sam2_hiera_s.yaml', 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_small.pt', 155),
258
+ 'tiny': ('sam2_hiera_t.yaml', 'https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_tiny.pt', 77)
259
+ }
260
+
261
+ # Select model based on GPU
262
+ if CUDA_AVAILABLE and GPU_MEMORY > 20:
263
+ model_key = 'large'
264
+ elif CUDA_AVAILABLE and GPU_MEMORY > 10:
265
+ model_key = 'base'
266
+ else:
267
+ model_key = 'tiny'
268
+
269
+ config, url, size = models[model_key]
270
+
271
+ # Download model
272
+ cache_dir = Path("/tmp/sam2_models")
273
+ cache_dir.mkdir(exist_ok=True)
274
+ model_path = cache_dir / f"sam2_{model_key}.pt"
275
+
276
+ if not model_path.exists():
277
+ with st.spinner(f"Downloading SAM2 {model_key} model ({size}MB)..."):
278
+ response = requests.get(url, stream=True)
279
+ with open(model_path, 'wb') as f:
280
+ for chunk in response.iter_content(chunk_size=8192):
281
+ f.write(chunk)
282
+
283
+ # Build model
284
+ sam2_model = build_sam2(config, str(model_path), device=DEVICE)
285
+ predictor = SAM2ImagePredictor(sam2_model)
286
+
287
+ logger.info(f"βœ… SAM2 {model_key} loaded successfully")
288
+ return predictor, True
289
+
290
+ except ImportError:
291
+ logger.warning("⚠️ SAM2 not installed")
292
+ return None, False
293
+ except Exception as e:
294
+ logger.error(f"❌ SAM2 loading failed: {e}")
295
+ return None, False
296
 
297
+ def process_frame(self, frame, use_temporal=True):
298
+ """Process frame with SAM2"""
299
+ if not self.loaded:
300
+ self.predictor, self.loaded = self.load_model()
301
+
302
+ if not self.loaded or self.predictor is None:
 
 
 
 
 
 
303
  return None
304
+
305
  try:
 
306
  self.predictor.set_image(frame)
307
 
308
  h, w = frame.shape[:2]
309
 
310
+ # Generate prompts
311
+ if use_temporal and self.previous_mask is not None:
 
 
312
  y_coords, x_coords = np.where(self.previous_mask > 0.5)
313
  if len(y_coords) > 0:
314
  center_y = int(np.mean(y_coords))
315
  center_x = int(np.mean(x_coords))
316
+ point_coords = np.array([[center_x, center_y]])
 
 
 
 
 
 
317
  else:
318
+ point_coords = np.array([[w//2, h//2]])
 
 
 
 
 
319
  else:
320
+ point_coords = np.array([[w//2, h//2], [w//2, h//3], [w//2, 2*h//3]])
321
+
322
+ point_labels = np.ones(len(point_coords))
323
+
324
+ # Predict
325
+ masks, scores, _ = self.predictor.predict(
 
 
 
 
 
 
 
326
  point_coords=point_coords,
327
  point_labels=point_labels,
328
+ multimask_output=True
 
329
  )
330
 
331
+ mask = masks[np.argmax(scores)].astype(np.float32)
 
 
332
 
333
+ # Temporal smoothing
334
+ if use_temporal and self.previous_mask is not None:
335
+ mask = 0.7 * mask + 0.3 * self.previous_mask
 
 
 
336
 
337
+ # Refine
 
338
  kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
339
  mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
340
+ mask = cv2.GaussianBlur(mask, (5, 5), 0)
 
 
 
 
 
 
 
341
 
342
+ self.previous_mask = mask
343
  return mask
344
 
345
  except Exception as e:
346
+ logger.error(f"SAM2 processing failed: {e}")
347
  return None
348
 
349
  def reset(self):
 
350
  self.previous_mask = None
 
 
351
 
352
  # ============================================
353
+ # REMBG - TERTIARY METHOD (FAST)
354
  # ============================================
355
 
356
+ REMBG_AVAILABLE = False
357
+ rembg_session = None
358
+
359
+ try:
360
+ from rembg import remove, new_session
 
361
 
362
+ providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if CUDA_AVAILABLE else ['CPUExecutionProvider']
363
+ rembg_session = new_session('u2net_human_seg', providers=providers)
 
 
 
 
364
 
365
+ # Warm up
366
+ dummy_img = Image.new('RGB', (256, 256), color='white')
367
+ _ = remove(dummy_img, session=rembg_session)
368
 
369
+ REMBG_AVAILABLE = True
370
+ logger.info(f"βœ… Rembg initialized with providers: {providers}")
 
 
 
 
 
 
371
 
372
+ except Exception as e:
373
+ logger.warning(f"⚠️ Rembg not available: {e}")
374
+
375
+ def segment_with_rembg(frame):
376
+ """Segment using Rembg"""
377
+ if not REMBG_AVAILABLE:
378
+ return None
379
+
380
+ try:
381
+ pil_image = Image.fromarray(frame)
382
+ output = remove(
383
+ pil_image,
384
+ session=rembg_session,
385
+ alpha_matting=True,
386
+ alpha_matting_foreground_threshold=240,
387
+ alpha_matting_background_threshold=10
388
+ )
389
+
390
+ output_array = np.array(output)
391
+ if output_array.shape[2] == 4:
392
+ mask = output_array[:, :, 3].astype(np.float32) / 255.0
393
+ else:
394
+ mask = np.ones((frame.shape[0], frame.shape[1]), dtype=np.float32)
395
+
396
+ return mask
397
+ except Exception as e:
398
+ logger.error(f"Rembg segmentation failed: {e}")
399
+ return None
400
+
401
+ # ============================================
402
+ # OPENCV - FALLBACK METHOD (ALWAYS WORKS)
403
+ # ============================================
404
+
405
+ def segment_with_opencv(frame):
406
+ """Basic OpenCV segmentation"""
407
+ try:
408
+ hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV)
409
+
410
+ lower_skin = np.array([0, 20, 70], dtype=np.uint8)
411
+ upper_skin = np.array([20, 255, 255], dtype=np.uint8)
412
+
413
+ mask = cv2.inRange(hsv, lower_skin, upper_skin)
414
+
415
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
416
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
417
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
418
+
419
+ mask = mask.astype(np.float32) / 255.0
420
+ mask = cv2.GaussianBlur(mask, (5, 5), 0)
421
+
422
+ return mask
423
+
424
+ except Exception as e:
425
+ logger.error(f"OpenCV segmentation failed: {e}")
426
+ return None
427
 
428
  # ============================================
429
+ # BACKGROUND UTILITIES
430
  # ============================================
431
 
432
+ def load_background_image(background_url):
433
+ """Load background image from URL"""
434
+ try:
435
+ response = requests.get(background_url, timeout=10)
436
+ response.raise_for_status()
437
+ image = Image.open(BytesIO(response.content))
438
+ return np.array(image.convert('RGB'))
439
+ except Exception as e:
440
+ logger.error(f"Failed to load background: {e}")
441
+ return create_default_background()
442
+
443
+ def create_default_background():
444
+ """Create gradient background"""
445
+ background = np.zeros((720, 1280, 3), dtype=np.uint8)
446
+ for y in range(720):
447
+ color_value = int(255 * (1 - y / 720))
448
+ background[y, :] = [color_value, int(color_value * 0.7), int(color_value * 0.9)]
449
+ return background
450
 
451
+ def get_professional_backgrounds():
452
+ """Professional background collection"""
453
+ return {
454
+ "🏒 Modern Office": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1920&h=1080&fit=crop",
455
+ "πŸŒ† City Skyline": "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=1920&h=1080&fit=crop",
456
+ "πŸ–οΈ Tropical Beach": "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1920&h=1080&fit=crop",
457
+ "🌲 Forest Path": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1920&h=1080&fit=crop",
458
+ "🎨 Abstract Gradient": "https://images.unsplash.com/photo-1557683316-973673baf926?w=1920&h=1080&fit=crop",
459
+ "πŸ”οΈ Mountain Vista": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&h=1080&fit=crop",
460
+ "πŸŒ… Sunset Sky": "https://images.unsplash.com/photo-1495616811223-4d98c6e9c869?w=1920&h=1080&fit=crop",
461
+ "πŸ’Ό Conference Room": "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=1920&h=1080&fit=crop",
462
+ "🎬 Studio Setup": "https://images.unsplash.com/photo-1565438222132-3654b8b88d4a?w=1920&h=1080&fit=crop",
463
+ "πŸŒƒ Night City": "https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=1920&h=1080&fit=crop"
464
+ }
465
+
466
+ # ============================================
467
+ # VIDEO PROCESSING PIPELINE
468
+ # ============================================
469
+
470
+ # Initialize processors
471
+ matanyone_processor = MatAnyoneProcessor()
472
+ sam2_processor = SAM2Processor()
473
+
474
+ def process_video(video_path, background_url, method='auto', progress_callback=None):
475
+ """Process video with selected method"""
476
  try:
477
+ # Load background
478
+ background_image = load_background_image(background_url)
479
+
480
  # Open video
481
  cap = cv2.VideoCapture(video_path)
 
 
482
  fps = int(cap.get(cv2.CAP_PROP_FPS))
483
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
484
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
485
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
486
 
487
+ logger.info(f"Processing video: {width}x{height}, {total_frames} frames, {fps} FPS")
488
+
489
+ # Create output
490
+ output_path = tempfile.mktemp(suffix='.mp4')
491
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
492
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
493
 
494
  # Resize background
495
  background_resized = cv2.resize(background_image, (width, height))
496
 
497
+ # Reset processors
498
+ matanyone_processor.reset()
499
+ sam2_processor.reset()
500
 
501
  frame_count = 0
502
+ processing_times = []
503
 
504
  while True:
505
  ret, frame = cap.read()
506
  if not ret:
507
  break
508
 
509
+ start_time = time.time()
510
+
511
  # Convert BGR to RGB
512
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
513
 
514
+ # Select method and process
515
+ mask = None
516
+ method_used = "None"
517
 
518
+ if method == 'auto' or method == 'matanyone':
519
+ # Try MatAnyone first (BEST)
520
+ mask = matanyone_processor.process_frame(frame_rgb, use_temporal=(frame_count > 0))
521
+ if mask is not None:
522
+ method_used = "MatAnyone"
523
+
524
+ if mask is None and (method == 'auto' or method == 'sam2'):
525
+ # Try SAM2 (GOOD)
526
+ mask = sam2_processor.process_frame(frame_rgb, use_temporal=(frame_count > 0))
527
+ if mask is not None:
528
+ method_used = "SAM2"
529
+
530
+ if mask is None and (method == 'auto' or method == 'rembg'):
531
+ # Try Rembg (FAST)
532
+ mask = segment_with_rembg(frame_rgb)
533
+ if mask is not None:
534
+ method_used = "Rembg"
535
+
536
+ if mask is None:
537
+ # Fallback to OpenCV
538
+ mask = segment_with_opencv(frame_rgb)
539
+ method_used = "OpenCV"
540
+
541
+ # Apply mask and composite
542
  if mask is not None:
 
543
  if mask.ndim == 2:
544
  mask = np.expand_dims(mask, axis=2)
545
 
546
+ # High-quality compositing
547
+ foreground = frame_rgb.astype(np.float32)
548
+ background = background_resized.astype(np.float32)
549
 
550
+ composite = foreground * mask + background * (1 - mask)
551
+ composite = np.clip(composite, 0, 255).astype(np.uint8)
552
  else:
553
+ composite = frame_rgb
554
 
555
+ # Convert back to BGR
556
+ composite_bgr = cv2.cvtColor(composite, cv2.COLOR_RGB2BGR)
557
  out.write(composite_bgr)
558
+
559
+ # Track time
560
+ processing_time = time.time() - start_time
561
+ processing_times.append(processing_time)
562
+
563
  frame_count += 1
564
 
565
+ # Progress update
566
  if progress_callback:
567
  progress = frame_count / total_frames
568
+ avg_time = np.mean(processing_times[-10:])
569
+ eta = avg_time * (total_frames - frame_count)
570
+ progress_callback(
571
+ progress,
572
+ f"{method_used}: Frame {frame_count}/{total_frames} | ETA: {eta:.1f}s"
573
+ )
574
 
575
+ # Memory cleanup
576
+ if frame_count % 50 == 0 and CUDA_AVAILABLE:
577
  torch.cuda.empty_cache()
578
 
579
+ # Release
580
  cap.release()
581
  out.release()
582
 
583
+ if CUDA_AVAILABLE:
584
+ torch.cuda.empty_cache()
585
+ gc.collect()
586
+
587
+ logger.info(f"βœ… Video processing complete: {output_path}")
588
+ logger.info(f"Average time per frame: {np.mean(processing_times):.3f}s")
589
+
590
  return output_path
591
 
592
  except Exception as e:
593
+ logger.error(f"Video processing failed: {e}")
594
  return None
595
 
596
  # ============================================
597
+ # STREAMLIT UI
598
  # ============================================
599
 
600
  def main():
601
+ st.set_page_config(
602
+ page_title="BackgroundFX - Professional Video Processing",
603
+ page_icon="🎬",
604
+ layout="wide",
605
+ initial_sidebar_state="expanded"
606
+ )
607
 
608
+ # Header
609
+ st.title("🎬 BackgroundFX - Professional Video Background Replacement")
610
+ st.markdown("**Production-quality processing with MatAnyone, SAM2, and Rembg**")
611
+
612
+ # System Status
613
+ col1, col2, col3, col4 = st.columns(4)
614
 
615
  with col1:
616
+ if CUDA_AVAILABLE:
617
+ st.success(f"πŸš€ GPU: {GPU_NAME}")
618
+ st.caption(f"VRAM: {GPU_MEMORY:.1f}GB")
 
 
619
  else:
620
+ st.info("πŸ’» CPU Mode")
621
 
622
+ with col2:
623
+ methods = []
624
+ if matanyone_processor.loaded:
625
+ methods.append("MatAnyone")
626
+ if sam2_processor.loaded:
627
+ methods.append("SAM2")
628
+ if REMBG_AVAILABLE:
629
+ methods.append("Rembg")
630
+ methods.append("OpenCV")
631
+ st.info(f"πŸ“¦ Methods: {', '.join(methods)}")
632
+
633
+ with col3:
634
+ if CUDA_AVAILABLE:
635
+ allocated = torch.cuda.memory_allocated() / 1024**3
636
+ st.metric("GPU Usage", f"{allocated:.1f}GB")
637
+ else:
638
+ st.metric("Mode", "CPU")
639
+
640
+ with col4:
641
+ st.metric("Device", DEVICE.upper())
642
+
643
+ # Sidebar
644
+ with st.sidebar:
645
+ st.markdown("### βš™οΈ Processing Options")
646
+
647
+ # Method selection with quality indicators
648
+ method_options = {
649
+ 'auto': 'Auto (Best Available)',
650
+ 'matanyone': 'MatAnyone (β˜…β˜…β˜…β˜…β˜… Production)',
651
+ 'sam2': 'SAM2 (β˜…β˜…β˜…β˜… Video-Optimized)',
652
+ 'rembg': 'Rembg (β˜…β˜…β˜… Fast)',
653
+ 'opencv': 'OpenCV (β˜… Fallback)'
654
+ }
655
+
656
+ selected_method = st.selectbox(
657
+ "Segmentation Method",
658
+ options=list(method_options.keys()),
659
+ format_func=lambda x: method_options[x],
660
+ index=0
661
+ )
662
+
663
+ # Method info
664
+ if selected_method == 'matanyone':
665
+ st.info("""
666
+ **MatAnyone Advantages:**
667
+ β€’ Perfect hair/edge details
668
+ β€’ Temporal consistency
669
+ β€’ Alpha matting quality
670
+ β€’ No flicker in video
671
+ """)
672
+ elif selected_method == 'sam2':
673
+ st.info("""
674
+ **SAM2 Advantages:**
675
+ β€’ Designed for video
676
+ β€’ Good temporal flow
677
+ β€’ Automatic prompting
678
+ """)
679
+ elif selected_method == 'rembg':
680
+ st.info("""
681
+ **Rembg Advantages:**
682
+ β€’ Fast processing
683
+ β€’ Good for photos
684
+ β€’ Easy to use
685
+ """)
686
+
687
+ st.markdown("---")
688
+
689
+ # System info
690
+ st.markdown("### πŸ“Š System Information")
691
+
692
+ if CUDA_AVAILABLE:
693
+ allocated = torch.cuda.memory_allocated() / 1024**3
694
+ reserved = torch.cuda.memory_reserved() / 1024**3
695
+ free = GPU_MEMORY - reserved if GPU_MEMORY else 0
696
+
697
+ st.metric("GPU Memory", f"{allocated:.2f} / {GPU_MEMORY:.1f} GB")
698
+
699
+ usage_percent = (allocated / GPU_MEMORY) * 100 if GPU_MEMORY else 0
700
+ st.progress(min(usage_percent / 100, 1.0))
701
+
702
+ with st.expander("GPU Details"):
703
+ st.code(f"""
704
+ Device: {GPU_NAME}
705
+ VRAM: {GPU_MEMORY:.1f} GB
706
+ Allocated: {allocated:.2f} GB
707
+ Reserved: {reserved:.2f} GB
708
+ Free: {free:.2f} GB
709
+ PyTorch: {torch.__version__}
710
+ CUDA: {torch.version.cuda if CUDA_AVAILABLE else 'N/A'}
711
+ """)
712
+ else:
713
+ st.info("Running in CPU mode")
714
+
715
+ # Main content
716
+ col1, col2 = st.columns(2)
717
+
718
+ with col1:
719
+ st.markdown("### πŸ“Ή Video Input")
720
+
721
+ uploaded_video = st.file_uploader(
722
+ "Upload your video",
723
+ type=['mp4', 'avi', 'mov', 'mkv'],
724
+ help="Maximum recommended: 30 seconds for best performance"
725
+ )
726
+
727
  if uploaded_video:
728
+ # Save video
729
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
730
+ tmp_file.write(uploaded_video.read())
731
+ video_path = tmp_file.name
732
+
733
+ st.video(uploaded_video)
734
+ st.success(f"βœ… Video ready: {uploaded_video.name}")
735
+ else:
736
+ video_path = None
737
+
738
+ with col2:
739
+ st.markdown("### πŸ–ΌοΈ Background Selection")
740
+
741
+ backgrounds = get_professional_backgrounds()
742
+ selected_bg_name = st.selectbox(
743
+ "Choose a background",
744
+ options=list(backgrounds.keys()),
745
+ index=0
746
+ )
747
+
748
+ background_url = backgrounds[selected_bg_name]
749
+
750
+ # Preview
751
+ try:
752
+ bg_image = load_background_image(background_url)
753
+ st.image(bg_image, caption=selected_bg_name, use_container_width=True)
754
+ except:
755
+ st.error("Failed to load background preview")
756
+
757
+ # Process button
758
+ if video_path and st.button("πŸš€ Process Video", type="primary", use_container_width=True):
759
+
760
+ # Progress tracking
761
+ progress_bar = st.progress(0)
762
+ status_text = st.empty()
763
+
764
+ def update_progress(progress, message):
765
+ progress_bar.progress(progress)
766
+ status_text.text(message)
767
+
768
+ # Process video
769
+ with st.spinner("Processing video..."):
770
+ start_time = time.time()
771
+
772
+ result_path = process_video(
773
  video_path,
774
+ background_url,
775
+ method=selected_method,
776
  progress_callback=update_progress
777
  )
778
 
779
+ processing_time = time.time() - start_time
780
+
781
+ if result_path and os.path.exists(result_path):
782
+ # Success
783
+ status_text.text(f"βœ… Processing complete in {processing_time:.1f} seconds!")
784
+
785
+ # Load result
786
+ with open(result_path, 'rb') as f:
787
+ result_data = f.read()
788
+
789
+ st.markdown("### 🎬 Result")
790
+ st.video(result_data)
791
+
792
+ # Download
793
+ st.download_button(
794
+ label="πŸ’Ύ Download Processed Video",
795
+ data=result_data,
796
+ file_name=f"backgroundfx_{uploaded_video.name}",
797
+ mime="video/mp4",
798
+ use_container_width=True
799
+ )
800
+
801
+ # Cleanup
802
+ os.unlink(result_path)
803
+
804
+ # Stats
805
+ if CUDA_AVAILABLE:
806
+ allocated = torch.cuda.memory_allocated() / 1024**3
807
+ st.info(f"Processing completed using {allocated:.1f}GB GPU memory")
808
+ else:
809
+ st.error("❌ Processing failed! Please try again.")
810
+
811
+ # Cleanup temp
812
+ if video_path and os.path.exists(video_path):
813
+ os.unlink(video_path)
814
 
815
  if __name__ == "__main__":
816
  main()