MogensR commited on
Commit
383230b
Β·
1 Parent(s): fe45f40

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +345 -292
app.py CHANGED
@@ -1,8 +1,8 @@
1
  #!/usr/bin/env python3
2
  """
3
- BackgroundFX - Video Background Replacement with Green Screen Workflow
4
- Fixed for Hugging Face Space - Handles video preview issues
5
- FIXED: Video display issue by properly handling file stream
6
  """
7
 
8
  import streamlit as st
@@ -14,50 +14,80 @@
14
  import requests
15
  from io import BytesIO
16
  import logging
17
- import base64
18
 
19
  # Configure logging
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
- # Try to import SAM2 and MatAnyone
 
 
 
 
 
 
 
24
  try:
25
- from sam2.build_sam import build_sam2_video_predictor
26
- from sam2.sam2_image_predictor import SAM2ImagePredictor
27
- SAM2_AVAILABLE = True
28
- logger.info("βœ… SAM2 loaded successfully")
29
- except ImportError as e:
30
- SAM2_AVAILABLE = False
31
- logger.warning(f"⚠️ SAM2 not available: {e}")
32
 
 
33
  try:
34
- import matanyone
35
  MATANYONE_AVAILABLE = True
36
- logger.info("βœ… MatAnyone loaded successfully")
37
- except ImportError as e:
38
  MATANYONE_AVAILABLE = False
39
- logger.warning(f"⚠️ MatAnyone not available: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  def get_video_info(video_path):
42
- """Get video information and first frame"""
43
  try:
44
  cap = cv2.VideoCapture(video_path)
45
  if not cap.isOpened():
46
  return None, None
47
 
48
- # Get video properties
49
  fps = int(cap.get(cv2.CAP_PROP_FPS))
50
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
51
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
52
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
53
  duration = total_frames / fps if fps > 0 else 0
54
 
55
- # Get first frame for thumbnail
56
  ret, first_frame = cap.read()
57
  cap.release()
58
 
59
  if ret:
60
- # Convert BGR to RGB
61
  first_frame_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
62
  return {
63
  'fps': fps,
@@ -68,192 +98,174 @@ def get_video_info(video_path):
68
  }, first_frame_rgb
69
 
70
  return None, None
71
-
72
  except Exception as e:
73
- logger.error(f"Error getting video info: {e}")
74
  return None, None
75
 
76
- def load_background_image(background_url):
77
- """Load background image from URL"""
78
- try:
79
- response = requests.get(background_url)
80
- response.raise_for_status()
81
- image = Image.open(BytesIO(response.content))
82
- return np.array(image.convert('RGB'))
83
- except Exception as e:
84
- logger.error(f"Failed to load background image: {e}")
85
- return create_default_background()
86
-
87
- def create_default_background():
88
- """Create a default brick wall background"""
89
- height, width = 720, 1280
90
- background = np.ones((height, width, 3), dtype=np.uint8) * 150
91
-
92
- # Add brick pattern
93
- brick_height, brick_width = 40, 80
94
- for y in range(0, height, brick_height):
95
- for x in range(0, width, brick_width):
96
- offset = brick_width // 2 if (y // brick_height) % 2 else 0
97
- x_pos = (x + offset) % width
98
-
99
- cv2.rectangle(background,
100
- (x_pos, y),
101
- (min(x_pos + brick_width - 2, width), min(y + brick_height - 2, height)),
102
- (180, 120, 80), -1)
103
- cv2.rectangle(background,
104
- (x_pos, y),
105
- (min(x_pos + brick_width - 2, width), min(y + brick_height - 2, height)),
106
- (120, 80, 40), 2)
107
-
108
  return background
109
 
110
- def segment_person_sam2(frame):
111
- """Segment person using SAM2"""
112
  try:
113
- predictor = SAM2ImagePredictor.from_pretrained("facebook/sam2-hiera-large")
114
- predictor.set_image(frame)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
 
116
  h, w = frame.shape[:2]
117
- center_point = np.array([[w//2, h//2]])
118
- center_label = np.array([1])
119
-
120
- masks, scores, _ = predictor.predict(
121
- point_coords=center_point,
122
- point_labels=center_label,
123
- multimask_output=False
124
- )
125
-
126
- return masks[0] if len(masks) > 0 else None
127
-
128
- except Exception as e:
129
- logger.error(f"SAM2 segmentation failed: {e}")
130
- return None
131
-
132
- def segment_person_fallback(frame):
133
- """Fallback person segmentation using color-based method"""
134
- try:
135
- hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV)
136
-
137
- lower_skin = np.array([0, 20, 70])
138
- upper_skin = np.array([20, 255, 255])
139
-
140
- skin_mask = cv2.inRange(hsv, lower_skin, upper_skin)
141
-
142
- kernel = np.ones((5, 5), np.uint8)
143
- skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
144
- skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)
145
-
146
- contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
147
 
148
- if contours:
149
- largest_contour = max(contours, key=cv2.contourArea)
150
- mask = np.zeros(frame.shape[:2], dtype=np.uint8)
151
- cv2.fillPoly(mask, [largest_contour], 255)
152
- kernel = np.ones((20, 20), np.uint8)
153
- mask = cv2.dilate(mask, kernel, iterations=2)
154
- return mask.astype(bool)
155
-
156
- return None
157
-
158
- except Exception as e:
159
- logger.error(f"Fallback segmentation failed: {e}")
160
- return None
161
-
162
- def insert_green_screen(frame, person_mask):
163
- """Insert green screen background while preserving person"""
164
- try:
165
- green_background = np.zeros_like(frame)
166
- green_background[:, :] = [0, 255, 0] # Pure green
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
- result = np.where(person_mask[..., None], frame, green_background)
169
  return result
170
 
171
  except Exception as e:
172
- logger.error(f"Green screen insertion failed: {e}")
173
  return frame
174
 
175
- def replace_background(frame, person_mask, background):
176
- """Replace background with new image"""
 
177
  try:
178
- # Resize background to match frame dimensions
179
- h, w = frame.shape[:2]
180
- background_resized = cv2.resize(background, (w, h))
181
-
182
- # Create smooth edges
183
- mask_float = person_mask.astype(np.float32)
184
- kernel = np.ones((5, 5), np.float32) / 25
185
- mask_float = cv2.filter2D(mask_float, -1, kernel)
186
-
187
- # Composite the images
188
- result = np.zeros_like(frame)
189
- for c in range(3):
190
- result[:, :, c] = (mask_float * frame[:, :, c] +
191
- (1 - mask_float) * background_resized[:, :, c])
192
-
193
- return result.astype(np.uint8)
194
 
195
- except Exception as e:
196
- logger.error(f"Background replacement failed: {e}")
197
- return frame
198
-
199
- def process_video(video_path, output_path, background_image, use_green_screen=True, progress_callback=None):
200
- """Process video with background replacement"""
201
- try:
202
  cap = cv2.VideoCapture(video_path)
203
 
204
- # Get video properties
205
  fps = int(cap.get(cv2.CAP_PROP_FPS))
206
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
207
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
208
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
209
 
210
- # Setup video writer
 
 
 
211
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
212
- out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
213
 
214
  frame_count = 0
 
215
 
216
  while True:
217
  ret, frame = cap.read()
218
  if not ret:
219
  break
220
 
221
- # Convert BGR to RGB for processing
222
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
223
 
224
- # Segment person
225
- if SAM2_AVAILABLE:
226
- person_mask = segment_person_sam2(frame_rgb)
227
- else:
228
- person_mask = segment_person_fallback(frame_rgb)
229
 
230
- if person_mask is not None:
231
- if use_green_screen:
232
- # First create green screen
233
- frame_rgb = insert_green_screen(frame_rgb, person_mask)
234
- # Then replace green with background
235
- # Create green mask
236
- lower_green = np.array([0, 200, 0])
237
- upper_green = np.array([100, 255, 100])
238
- green_mask = cv2.inRange(frame_rgb, lower_green, upper_green)
239
- green_mask = green_mask.astype(bool)
240
- # Replace green areas with background
241
- frame_rgb = replace_background(frame_rgb, ~green_mask, background_image)
242
- else:
243
- # Direct background replacement
244
- frame_rgb = replace_background(frame_rgb, person_mask, background_image)
245
 
246
- # Convert back to BGR for writing
247
- frame_bgr = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
248
  out.write(frame_bgr)
249
 
250
- frame_count += 1
251
- if progress_callback:
252
- progress_callback(frame_count / total_frames)
 
 
253
 
254
  cap.release()
255
  out.release()
256
 
 
 
 
 
257
  return True
258
 
259
  except Exception as e:
@@ -263,185 +275,226 @@ def process_video(video_path, output_path, background_image, use_green_screen=Tr
263
  # Streamlit UI
264
  def main():
265
  st.set_page_config(
266
- page_title="BackgroundFX",
267
- page_icon="🎬",
268
  layout="wide"
269
  )
270
 
271
- st.title("🎬 BackgroundFX - Video Background Replacement")
272
- st.markdown("Replace video backgrounds with AI-powered segmentation")
273
 
274
- # Check dependencies
275
- if not SAM2_AVAILABLE:
276
- st.warning("⚠️ SAM2 not available - using fallback segmentation method")
277
- if not MATANYONE_AVAILABLE:
278
- st.info("ℹ️ MatAnyone not available - using standard matting")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
- # Sidebar for settings
281
- with st.sidebar:
282
- st.header("βš™οΈ Settings")
283
-
284
- use_green_screen = st.checkbox(
285
- "Use Green Screen Workflow",
286
- value=True,
287
- help="First create green screen, then replace with background"
 
 
 
288
  )
289
 
290
- st.subheader("πŸ“Έ Background Options")
291
- bg_option = st.radio(
292
- "Choose background source:",
293
- ["Default Brick Wall", "Custom URL", "Upload Image"]
 
 
294
  )
295
 
296
- background_image = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
- if bg_option == "Custom URL":
299
- bg_url = st.text_input(
300
- "Background Image URL",
301
- value="https://images.unsplash.com/photo-1557683316-973673baf926",
302
- help="Enter a direct image URL"
303
- )
304
- if bg_url:
305
- with st.spinner("Loading background..."):
306
- background_image = load_background_image(bg_url)
307
- if background_image is not None:
308
- st.success("βœ… Background loaded")
309
- st.image(background_image, caption="Background Preview", use_column_width=True)
310
-
311
- elif bg_option == "Upload Image":
312
- uploaded_bg = st.file_uploader(
313
- "Upload Background Image",
314
- type=['jpg', 'jpeg', 'png'],
315
- help="Upload your own background image"
316
- )
317
- if uploaded_bg is not None:
318
- background_image = np.array(Image.open(uploaded_bg).convert('RGB'))
319
- st.success("βœ… Background uploaded")
320
- st.image(background_image, caption="Background Preview", use_column_width=True)
321
-
322
- else: # Default Brick Wall
323
- background_image = create_default_background()
324
- st.info("Using default brick wall background")
325
- st.image(background_image, caption="Default Background", use_column_width=True)
326
-
327
- # Main content area
328
- col1, col2 = st.columns(2)
329
-
330
- with col1:
331
- st.header("πŸ“Ή Input Video")
332
  uploaded_video = st.file_uploader(
333
- "Upload your video",
334
  type=['mp4', 'avi', 'mov', 'mkv'],
335
- help="Upload a video to process"
336
  )
337
 
338
  if uploaded_video is not None:
339
- # FIXED: Read bytes once and reuse
340
  video_bytes = uploaded_video.read()
341
 
342
- # Save to temporary file for processing
343
  with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
344
  tmp_file.write(video_bytes)
345
- temp_video_path = tmp_file.name
346
 
347
- # Display video using the original bytes (not consuming the stream)
348
  st.video(video_bytes)
349
 
350
- # Get video info using the temp file
351
- video_info, first_frame = get_video_info(temp_video_path)
352
-
353
- if video_info and first_frame is not None:
354
- st.success(f"βœ… Video loaded: {video_info['width']}x{video_info['height']}, "
355
- f"{video_info['fps']} fps, {video_info['duration']:.1f}s")
356
 
357
- # Show first frame as thumbnail
358
- st.image(first_frame, caption="First Frame Preview", use_column_width=True)
 
359
 
360
- # Store paths in session state
361
- if 'temp_video_path' not in st.session_state:
362
- st.session_state.temp_video_path = temp_video_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  else:
364
- st.error("Failed to read video information")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
  with col2:
367
- st.header("🎯 Output")
368
 
369
- if uploaded_video is not None and background_image is not None:
370
- if st.button("πŸš€ Process Video", type="primary"):
 
 
 
 
 
 
 
371
  try:
372
- # Create output path
373
  output_path = tempfile.mktemp(suffix='.mp4')
374
 
375
- # Process video with progress bar
376
- progress_bar = st.progress(0)
377
- status_text = st.empty()
378
 
379
- def update_progress(progress):
380
- progress_bar.progress(progress)
381
- status_text.text(f"Processing: {int(progress * 100)}%")
382
 
383
- status_text.text("Starting video processing...")
384
 
385
- # Use the temp file path we saved
386
- success = process_video(
387
- st.session_state.temp_video_path,
388
  output_path,
389
- background_image,
390
- use_green_screen,
391
- update_progress
 
 
392
  )
393
 
394
  if success and os.path.exists(output_path):
395
- status_text.text("βœ… Processing complete!")
396
 
397
- # Read the processed video
398
  with open(output_path, 'rb') as f:
399
- processed_video = f.read()
400
 
401
- # Display processed video
402
- st.video(processed_video)
403
 
404
- # Download button
405
  st.download_button(
406
- label="πŸ“₯ Download Processed Video",
407
- data=processed_video,
408
- file_name="backgroundfx_output.mp4",
409
- mime="video/mp4"
 
410
  )
411
 
412
- # Cleanup
 
 
 
413
  os.unlink(output_path)
414
  else:
415
- st.error("❌ Video processing failed")
416
-
417
  except Exception as e:
418
- st.error(f"Error during processing: {str(e)}")
419
  logger.error(f"Processing error: {e}")
420
-
421
- elif uploaded_video is None:
422
- st.info("πŸ‘ˆ Please upload a video to begin")
423
- elif background_image is None:
424
- st.info("πŸ‘ˆ Please select or upload a background image")
425
-
426
- # Cleanup temporary files on session end
427
- if 'temp_video_path' in st.session_state and os.path.exists(st.session_state.temp_video_path):
428
- try:
429
- os.unlink(st.session_state.temp_video_path)
430
- del st.session_state.temp_video_path
431
- except:
432
- pass
433
 
434
- # Footer
435
  st.markdown("---")
436
- st.markdown(
437
- """
438
- <div style='text-align: center'>
439
- <p>BackgroundFX v1.0 | AI-Powered Video Background Replacement</p>
440
- <p>Using SAM2 for person segmentation and green screen technology</p>
441
- </div>
442
- """,
443
- unsafe_allow_html=True
444
- )
445
 
446
  if __name__ == "__main__":
447
  main()
 
1
  #!/usr/bin/env python3
2
  """
3
+ BackgroundFX - Fast Video Background Replacement
4
+ Optimized with Rembg for immediate deployment on HF Space with T4 GPU
5
+ Ready to run in 30 minutes!
6
  """
7
 
8
  import streamlit as st
 
14
  import requests
15
  from io import BytesIO
16
  import logging
17
+ import torch
18
 
19
  # Configure logging
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
+ # Check GPU
24
+ CUDA_AVAILABLE = torch.cuda.is_available()
25
+ if CUDA_AVAILABLE:
26
+ logger.info(f"βœ… GPU: {torch.cuda.get_device_name(0)}")
27
+ else:
28
+ logger.warning("⚠️ Running on CPU")
29
+
30
+ # Import rembg - the main workhorse
31
  try:
32
+ from rembg import remove, new_session
33
+ REMBG_AVAILABLE = True
34
+ logger.info("βœ… Rembg loaded")
35
+ except ImportError:
36
+ REMBG_AVAILABLE = False
37
+ st.error("❌ Please install rembg: pip install rembg")
 
38
 
39
+ # Import MatAnyone for better matting
40
  try:
41
+ from matanyone import MatAnyone
42
  MATANYONE_AVAILABLE = True
43
+ logger.info("βœ… MatAnyone loaded for edge refinement")
44
+ except ImportError:
45
  MATANYONE_AVAILABLE = False
46
+ logger.info("ℹ️ MatAnyone not available - using standard matting")
47
+
48
+ # Global session cache
49
+ @st.cache_resource
50
+ def load_rembg_model():
51
+ """Load and cache the Rembg model"""
52
+ if REMBG_AVAILABLE:
53
+ # u2net_human_seg is specifically for people
54
+ session = new_session('u2net_human_seg')
55
+ logger.info("βœ… U2NET Human Segmentation model loaded")
56
+ return session
57
+ return None
58
+
59
+ @st.cache_resource
60
+ def load_matanyone_model():
61
+ """Load and cache MatAnyone model for edge refinement"""
62
+ if MATANYONE_AVAILABLE:
63
+ try:
64
+ model = MatAnyone()
65
+ if CUDA_AVAILABLE:
66
+ model = model.cuda()
67
+ logger.info("βœ… MatAnyone model loaded for edge refinement")
68
+ return model
69
+ except Exception as e:
70
+ logger.warning(f"Failed to load MatAnyone: {e}")
71
+ return None
72
+ return None
73
 
74
  def get_video_info(video_path):
75
+ """Get video information"""
76
  try:
77
  cap = cv2.VideoCapture(video_path)
78
  if not cap.isOpened():
79
  return None, None
80
 
 
81
  fps = int(cap.get(cv2.CAP_PROP_FPS))
82
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
83
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
84
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
85
  duration = total_frames / fps if fps > 0 else 0
86
 
 
87
  ret, first_frame = cap.read()
88
  cap.release()
89
 
90
  if ret:
 
91
  first_frame_rgb = cv2.cvtColor(first_frame, cv2.COLOR_BGR2RGB)
92
  return {
93
  'fps': fps,
 
98
  }, first_frame_rgb
99
 
100
  return None, None
 
101
  except Exception as e:
102
+ logger.error(f"Error: {e}")
103
  return None, None
104
 
105
+ def create_gradient_background(width=1280, height=720, color1=(70, 130, 180), color2=(255, 140, 90)):
106
+ """Create a nice gradient background"""
107
+ background = np.zeros((height, width, 3), dtype=np.uint8)
108
+ for y in range(height):
109
+ blend = y / height
110
+ color = [
111
+ int(color1[0] * (1 - blend) + color2[0] * blend),
112
+ int(color1[1] * (1 - blend) + color2[1] * blend),
113
+ int(color1[2] * (1 - blend) + color2[2] * blend)
114
+ ]
115
+ background[y, :] = color
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  return background
117
 
118
+ def process_frame_rembg(frame, session, background, use_green_screen=False, matanyone_model=None):
119
+ """Process a single frame with Rembg and optional MatAnyone refinement"""
120
  try:
121
+ # Get RGBA output from rembg
122
+ frame_pil = Image.fromarray(frame)
123
+ output = remove(frame_pil, session=session, alpha_matting=True) # Enable alpha matting for better edges
124
+ output_np = np.array(output)
125
+
126
+ # Extract alpha channel
127
+ if output_np.shape[2] == 4:
128
+ alpha = output_np[:, :, 3].astype(float) / 255.0
129
+ person_rgb = output_np[:, :, :3]
130
+ else:
131
+ alpha = np.ones(frame.shape[:2])
132
+ person_rgb = output_np
133
+
134
+ # Apply MatAnyone for edge refinement if available
135
+ if matanyone_model is not None and MATANYONE_AVAILABLE:
136
+ try:
137
+ # MatAnyone expects torch tensors
138
+ import torch
139
+
140
+ # Convert to tensor
141
+ frame_tensor = torch.from_numpy(frame).float().permute(2, 0, 1).unsqueeze(0) / 255.0
142
+ alpha_tensor = torch.from_numpy(alpha).float().unsqueeze(0).unsqueeze(0)
143
+
144
+ if CUDA_AVAILABLE:
145
+ frame_tensor = frame_tensor.cuda()
146
+ alpha_tensor = alpha_tensor.cuda()
147
+
148
+ # Refine alpha with MatAnyone
149
+ with torch.no_grad():
150
+ refined_alpha = matanyone_model.refine_alpha(frame_tensor, alpha_tensor)
151
+
152
+ # Convert back to numpy
153
+ alpha = refined_alpha.squeeze().cpu().numpy()
154
+
155
+ logger.info("βœ… Applied MatAnyone edge refinement")
156
+ except Exception as e:
157
+ logger.warning(f"MatAnyone refinement failed, using original alpha: {e}")
158
 
159
+ # Resize background
160
  h, w = frame.shape[:2]
161
+ bg_resized = cv2.resize(background, (w, h))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ if use_green_screen:
164
+ # Green screen workflow with refined alpha
165
+ green = np.zeros_like(frame)
166
+ green[:, :] = [0, 255, 0]
167
+
168
+ # Composite person on green with smooth alpha
169
+ green_composite = np.zeros_like(frame, dtype=np.float32)
170
+ for c in range(3):
171
+ green_composite[:, :, c] = alpha * person_rgb[:, :, c] + (1 - alpha) * green[:, :, c]
172
+
173
+ green_composite = green_composite.astype(np.uint8)
174
+
175
+ # Replace green with background
176
+ lower_green = np.array([0, 200, 0])
177
+ upper_green = np.array([100, 255, 100])
178
+ green_mask = cv2.inRange(green_composite, lower_green, upper_green)
179
+ green_mask_inv = cv2.bitwise_not(green_mask)
180
+
181
+ result = cv2.bitwise_and(green_composite, green_composite, mask=green_mask_inv)
182
+ bg_part = cv2.bitwise_and(bg_resized, bg_resized, mask=green_mask)
183
+ result = cv2.add(result, bg_part)
184
+ else:
185
+ # Direct composite with refined alpha (faster and usually better)
186
+ # Apply slight edge smoothing to alpha
187
+ alpha_smooth = cv2.GaussianBlur(alpha.astype(np.float32), (3, 3), 0)
188
+
189
+ result = np.zeros_like(frame, dtype=np.float32)
190
+ for c in range(3):
191
+ result[:, :, c] = alpha_smooth * person_rgb[:, :, c] + (1 - alpha_smooth) * bg_resized[:, :, c]
192
+
193
+ result = result.astype(np.uint8)
194
 
 
195
  return result
196
 
197
  except Exception as e:
198
+ logger.error(f"Frame processing error: {e}")
199
  return frame
200
 
201
+ def process_video_fast(video_path, output_path, background, progress_callback=None,
202
+ skip_frames=1, use_green_screen=False, use_matanyone=True):
203
+ """Fast video processing with Rembg and optional MatAnyone"""
204
  try:
205
+ # Load models once
206
+ session = load_rembg_model()
207
+ if session is None:
208
+ st.error("Failed to load Rembg model")
209
+ return False
210
+
211
+ # Load MatAnyone if requested and available
212
+ matanyone_model = None
213
+ if use_matanyone and MATANYONE_AVAILABLE:
214
+ matanyone_model = load_matanyone_model()
215
+ if matanyone_model:
216
+ st.info("✨ Using MatAnyone for edge refinement")
 
 
 
 
217
 
 
 
 
 
 
 
 
218
  cap = cv2.VideoCapture(video_path)
219
 
 
220
  fps = int(cap.get(cv2.CAP_PROP_FPS))
221
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
222
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
223
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
224
 
225
+ # Adjust FPS if skipping frames
226
+ output_fps = max(fps // skip_frames, 15) # Minimum 15 fps
227
+
228
+ # Use MP4V codec for compatibility
229
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
230
+ out = cv2.VideoWriter(output_path, fourcc, output_fps, (width, height))
231
 
232
  frame_count = 0
233
+ processed_count = 0
234
 
235
  while True:
236
  ret, frame = cap.read()
237
  if not ret:
238
  break
239
 
240
+ frame_count += 1
 
241
 
242
+ # Skip frames for speed
243
+ if skip_frames > 1 and (frame_count - 1) % skip_frames != 0:
244
+ continue
 
 
245
 
246
+ # Process frame with MatAnyone if available
247
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
248
+ processed_frame = process_frame_rembg(
249
+ frame_rgb, session, background, use_green_screen, matanyone_model
250
+ )
 
 
 
 
 
 
 
 
 
 
251
 
252
+ # Write frame
253
+ frame_bgr = cv2.cvtColor(processed_frame, cv2.COLOR_RGB2BGR)
254
  out.write(frame_bgr)
255
 
256
+ processed_count += 1
257
+
258
+ if progress_callback and frame_count % 5 == 0: # Update progress every 5 frames
259
+ progress = frame_count / total_frames
260
+ progress_callback(progress)
261
 
262
  cap.release()
263
  out.release()
264
 
265
+ # Clear GPU memory
266
+ if CUDA_AVAILABLE:
267
+ torch.cuda.empty_cache()
268
+
269
  return True
270
 
271
  except Exception as e:
 
275
  # Streamlit UI
276
  def main():
277
  st.set_page_config(
278
+ page_title="BackgroundFX - Fast",
279
+ page_icon="πŸš€",
280
  layout="wide"
281
  )
282
 
283
+ st.title("πŸš€ BackgroundFX - Fast Background Replacement")
284
+ st.markdown("**Optimized for speed** - Using Rembg U2NET for human segmentation")
285
 
286
+ # Quick status check
287
+ cols = st.columns(4)
288
+ with cols[0]:
289
+ if CUDA_AVAILABLE:
290
+ st.success(f"βœ… GPU Active")
291
+ else:
292
+ st.warning("⚠️ CPU Mode")
293
+ with cols[1]:
294
+ if REMBG_AVAILABLE:
295
+ st.success("βœ… Rembg Ready")
296
+ else:
297
+ st.error("❌ Install rembg")
298
+ with cols[2]:
299
+ if MATANYONE_AVAILABLE:
300
+ st.success("βœ… MatAnyone")
301
+ else:
302
+ st.info("ℹ️ Basic Matting")
303
+ with cols[3]:
304
+ st.info("⚑ Fast Mode")
305
 
306
+ # Two columns layout
307
+ col1, col2 = st.columns(2)
308
+
309
+ with col1:
310
+ st.header("πŸ“Ή Upload Video")
311
+
312
+ # Speed preset at the top for visibility
313
+ speed_mode = st.select_slider(
314
+ "⚑ Speed Mode",
315
+ options=["Quality", "Balanced", "Fast", "Ultra Fast"],
316
+ value="Fast"
317
  )
318
 
319
+ # MatAnyone edge refinement option
320
+ use_matanyone = st.checkbox(
321
+ "✨ Use MatAnyone Edge Refinement",
322
+ value=MATANYONE_AVAILABLE,
323
+ disabled=not MATANYONE_AVAILABLE,
324
+ help="Improves edges around hair and clothing (adds ~20% processing time)"
325
  )
326
 
327
+ # Set parameters based on mode
328
+ if speed_mode == "Ultra Fast":
329
+ skip_frames = 3
330
+ use_green = False
331
+ st.caption("⚑ Every 3rd frame, direct compositing")
332
+ elif speed_mode == "Fast":
333
+ skip_frames = 2
334
+ use_green = False
335
+ st.caption("⚑ Every 2nd frame, direct compositing")
336
+ elif speed_mode == "Balanced":
337
+ skip_frames = 1
338
+ use_green = False
339
+ st.caption("⚑ All frames, direct compositing")
340
+ else: # Quality
341
+ skip_frames = 1
342
+ use_green = True
343
+ st.caption("⚑ All frames, green screen + edge refinement")
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  uploaded_video = st.file_uploader(
346
+ "Choose video file",
347
  type=['mp4', 'avi', 'mov', 'mkv'],
348
+ help="For best results, use videos under 30 seconds"
349
  )
350
 
351
  if uploaded_video is not None:
352
+ # Save video
353
  video_bytes = uploaded_video.read()
354
 
 
355
  with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
356
  tmp_file.write(video_bytes)
357
+ temp_path = tmp_file.name
358
 
 
359
  st.video(video_bytes)
360
 
361
+ # Get info
362
+ info, first_frame = get_video_info(temp_path)
363
+ if info:
364
+ st.success(f"βœ… Ready: {info['duration']:.1f}s @ {info['fps']}fps")
 
 
365
 
366
+ # Time estimate
367
+ process_time = (info['duration'] / skip_frames) * 0.5 # Rough estimate
368
+ st.info(f"⏱️ Estimated time: {process_time:.0f} seconds")
369
 
370
+ st.session_state.video_path = temp_path
371
+ st.session_state.video_info = info
372
+
373
+ # Background selection
374
+ st.subheader("🎨 Background")
375
+ bg_type = st.radio("Choose:", ["Gradient", "Color", "Image URL", "Upload"])
376
+
377
+ background = None
378
+
379
+ if bg_type == "Gradient":
380
+ col_a, col_b = st.columns(2)
381
+ with col_a:
382
+ color1 = st.color_picker("Top", "#4682B4")
383
+ with col_b:
384
+ color2 = st.color_picker("Bottom", "#FF8C5A")
385
+
386
+ # Convert hex to RGB
387
+ c1 = tuple(int(color1[i:i+2], 16) for i in (1, 3, 5))
388
+ c2 = tuple(int(color2[i:i+2], 16) for i in (1, 3, 5))
389
+
390
+ if 'video_info' in st.session_state:
391
+ w = st.session_state.video_info['width']
392
+ h = st.session_state.video_info['height']
393
+ background = create_gradient_background(w, h, c1, c2)
394
  else:
395
+ background = create_gradient_background(1280, 720, c1, c2)
396
+
397
+ elif bg_type == "Color":
398
+ color = st.color_picker("Pick color", "#00FF00")
399
+ rgb = tuple(int(color[i:i+2], 16) for i in (1, 3, 5))
400
+ background = np.full((720, 1280, 3), rgb, dtype=np.uint8)
401
+
402
+ elif bg_type == "Image URL":
403
+ url = st.text_input("Image URL", "https://images.unsplash.com/photo-1557683316-973673baf926")
404
+ if url:
405
+ try:
406
+ response = requests.get(url)
407
+ img = Image.open(BytesIO(response.content))
408
+ background = np.array(img.convert('RGB'))
409
+ st.image(background, caption="Background", use_column_width=True)
410
+ except:
411
+ st.error("Failed to load image")
412
+
413
+ else: # Upload
414
+ uploaded_bg = st.file_uploader("Upload image", type=['jpg', 'jpeg', 'png'])
415
+ if uploaded_bg:
416
+ img = Image.open(uploaded_bg)
417
+ background = np.array(img.convert('RGB'))
418
+ st.image(background, caption="Background", use_column_width=True)
419
 
420
  with col2:
421
+ st.header("🎬 Result")
422
 
423
+ if uploaded_video and background is not None:
424
+ if st.button("πŸš€ Process Video", type="primary", use_container_width=True):
425
+
426
+ # Check if rembg is available
427
+ if not REMBG_AVAILABLE:
428
+ st.error("Please install rembg first!")
429
+ st.code("pip install rembg", language="bash")
430
+ return
431
+
432
  try:
 
433
  output_path = tempfile.mktemp(suffix='.mp4')
434
 
435
+ progress = st.progress(0)
436
+ status = st.empty()
 
437
 
438
+ def update_progress(value):
439
+ progress.progress(value)
440
+ status.text(f"Processing: {int(value * 100)}%")
441
 
442
+ status.text("πŸ”„ Starting processing...")
443
 
444
+ success = process_video_fast(
445
+ st.session_state.video_path,
 
446
  output_path,
447
+ background,
448
+ update_progress,
449
+ skip_frames,
450
+ use_green_screen=use_green,
451
+ use_matanyone=use_matanyone
452
  )
453
 
454
  if success and os.path.exists(output_path):
455
+ status.text("βœ… Done!")
456
 
 
457
  with open(output_path, 'rb') as f:
458
+ result_video = f.read()
459
 
460
+ st.video(result_video)
 
461
 
 
462
  st.download_button(
463
+ "πŸ’Ύ Download Result",
464
+ data=result_video,
465
+ file_name=f"backgroundfx_{speed_mode.lower()}.mp4",
466
+ mime="video/mp4",
467
+ use_container_width=True
468
  )
469
 
470
+ # Show stats
471
+ size_mb = len(result_video) / (1024 * 1024)
472
+ st.success(f"βœ… Output size: {size_mb:.1f} MB")
473
+
474
  os.unlink(output_path)
475
  else:
476
+ st.error("Processing failed!")
477
+
478
  except Exception as e:
479
+ st.error(f"Error: {str(e)}")
480
  logger.error(f"Processing error: {e}")
481
+ else:
482
+ if not uploaded_video:
483
+ st.info("πŸ‘ˆ Upload a video to start")
484
+ else:
485
+ st.info("πŸ‘ˆ Select a background")
 
 
 
 
 
 
 
 
486
 
487
+ # Footer with tips
488
  st.markdown("---")
489
+ with st.expander("πŸ’‘ Quick Tips"):
490
+ st.markdown("""
491
+ - **Ultra Fast**: Best for quick previews (3x faster)
492
+ - **Fast**: Good balance of speed and quality (2x faster)
493
+ - **Balanced**: Full quality, still fast
494
+ - **Quality**: Best edges with green screen workflow
495
+ - Videos under 30 seconds process fastest
496
+ - Gradient backgrounds render instantly
497
+ """)
498
 
499
  if __name__ == "__main__":
500
  main()