MogensR commited on
Commit
353d7b5
Β·
1 Parent(s): 61b8bbf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +313 -156
app.py CHANGED
@@ -1,98 +1,210 @@
1
- # HuggingFace Space - Video Background Replacement
2
- # STREAMLIT VERSION - Memory optimized
 
 
 
3
 
4
  import streamlit as st
5
  import cv2
6
  import numpy as np
7
- from PIL import Image
8
  import tempfile
9
- import traceback
10
  import os
11
- import gc
 
 
 
12
 
13
- # Environment setup
14
- os.environ["OMP_NUM_THREADS"] = "4"
 
15
 
16
- # Page config
17
- st.set_page_config(
18
- page_title="🎬 Video Background Replacement",
19
- page_icon="🎬",
20
- layout="wide"
21
- )
 
 
 
22
 
23
- # Global model cache
24
- MODEL_CACHE = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- def load_models():
27
- """Load required models with caching"""
28
- global MODEL_CACHE
 
 
29
 
30
- if 'models_loaded' in MODEL_CACHE:
31
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
 
 
 
 
33
  try:
34
- # Try to load SAM2 if available
35
- try:
36
- from sam2.sam2_image_predictor import SAM2ImagePredictor
37
- predictor = SAM2ImagePredictor.from_pretrained("facebook/sam2-hiera-small")
38
- MODEL_CACHE['sam2_predictor'] = predictor
39
- st.success("βœ… SAM2 model loaded")
40
- except ImportError:
41
- st.warning("⚠️ SAM2 not available, using fallback method")
42
- MODEL_CACHE['sam2_predictor'] = None
43
 
44
- MODEL_CACHE['models_loaded'] = True
45
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  except Exception as e:
48
- st.error(f"❌ Error loading models: {e}")
49
- return False
50
 
51
- def apply_simple_background_replacement(frame, background):
52
- """Simple background replacement using color segmentation"""
53
  try:
54
- # Convert to HSV
55
- hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
56
 
57
- # Create mask for person (basic skin tone detection)
58
  lower_skin = np.array([0, 20, 70])
59
  upper_skin = np.array([20, 255, 255])
60
- mask1 = cv2.inRange(hsv, lower_skin, upper_skin)
61
 
62
- # Additional mask for different skin tones
63
- lower_skin2 = np.array([160, 20, 70])
64
- upper_skin2 = np.array([180, 255, 255])
65
- mask2 = cv2.inRange(hsv, lower_skin2, upper_skin2)
66
 
67
- # Combine masks
68
- mask = cv2.bitwise_or(mask1, mask2)
 
 
69
 
70
- # Clean up mask
71
- kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
72
- mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
73
- mask = cv2.GaussianBlur(mask, (5, 5), 0)
74
 
75
- # Apply background replacement
76
- mask_norm = mask.astype(np.float32) / 255.0
77
- mask_3ch = np.stack([mask_norm] * 3, axis=-1)
 
 
 
 
 
 
 
 
 
 
78
 
79
- result = frame * mask_3ch + background * (1 - mask_3ch)
80
- return result.astype(np.uint8)
81
 
82
  except Exception as e:
83
- st.error(f"Error in background replacement: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  return frame
85
 
86
- def process_video(input_video, background_image=None):
87
- """Process video with background replacement"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  try:
89
- if not os.path.exists(input_video):
90
- return None, "❌ Input video not found"
91
 
92
- # Load video
93
- cap = cv2.VideoCapture(input_video)
94
- if not cap.isOpened():
95
- return None, "❌ Could not open video"
96
 
97
  # Get video properties
98
  fps = int(cap.get(cv2.CAP_PROP_FPS))
@@ -100,144 +212,189 @@ def process_video(input_video, background_image=None):
100
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
101
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
102
 
103
- st.info(f"πŸ“Š Video: {width}x{height}, {fps} FPS, {total_frames} frames")
104
-
105
- # Load background
106
- if background_image and os.path.exists(background_image):
107
- bg_img = cv2.imread(background_image)
108
- bg_img = cv2.resize(bg_img, (width, height))
109
- else:
110
- # Green screen default
111
- bg_img = np.zeros((height, width, 3), dtype=np.uint8)
112
- bg_img[:, :] = [0, 255, 0]
113
-
114
- # Create output video
115
  output_path = tempfile.mktemp(suffix='.mp4')
116
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
117
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
118
 
119
- # Progress bar
120
- progress_bar = st.progress(0)
121
- status_text = st.empty()
122
-
123
  frame_count = 0
124
- processed_count = 0
125
 
126
- # Process frames
127
  while True:
128
  ret, frame = cap.read()
129
  if not ret:
130
  break
131
 
132
- frame_count += 1
 
133
 
134
- # Process frame
135
- processed_frame = apply_simple_background_replacement(frame, bg_img)
136
- out.write(processed_frame)
137
- processed_count += 1
 
 
 
138
 
139
- # Update progress
140
- progress = frame_count / total_frames
141
- progress_bar.progress(progress)
142
- status_text.text(f"Processing frame {frame_count}/{total_frames}")
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- # Memory cleanup every 100 frames
145
- if frame_count % 100 == 0:
146
- gc.collect()
 
147
 
 
148
  cap.release()
149
  out.release()
150
 
151
- # Final cleanup
152
- gc.collect()
153
 
154
- if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
155
- return output_path, f"βœ… Processed {processed_count} frames successfully!"
156
- else:
157
- return None, "❌ Failed to create output video"
158
-
159
  except Exception as e:
160
- return None, f"❌ Error: {str(e)}"
 
161
 
162
- # Streamlit UI
163
  def main():
164
- st.title("🎬 AI Video Background Replacement")
165
- st.markdown("Upload a video and optionally a background image to replace the background using AI.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- # Load models
168
- if not load_models():
169
- st.error("Failed to load required models")
170
- return
171
 
 
172
  col1, col2 = st.columns(2)
173
 
174
  with col1:
175
- st.subheader("πŸ“Ή Input Video")
176
  uploaded_video = st.file_uploader(
177
- "Choose a video file",
178
  type=['mp4', 'avi', 'mov', 'mkv'],
179
  help="Upload the video you want to process"
180
  )
181
 
182
  if uploaded_video:
 
 
 
183
  # Save uploaded video
184
- with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_video:
185
- tmp_video.write(uploaded_video.read())
186
- video_path = tmp_video.name
187
 
188
- st.video(video_path)
 
189
 
190
  with col2:
191
- st.subheader("πŸ–ΌοΈ Background Image")
192
- uploaded_bg = st.file_uploader(
193
- "Choose background image (optional)",
194
- type=['jpg', 'jpeg', 'png'],
195
- help="Leave empty for green screen background"
 
 
 
 
 
 
 
 
 
196
  )
197
 
198
- bg_path = None
199
- if uploaded_bg:
200
- # Save uploaded background
201
- with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_bg:
202
- tmp_bg.write(uploaded_bg.read())
203
- bg_path = tmp_bg.name
204
-
205
- st.image(bg_path, caption="Background Image")
206
- else:
207
- st.info("πŸ’š Using green screen background")
208
 
209
  # Process button
210
- if st.button("πŸš€ Process Video", type="primary"):
211
- if uploaded_video:
212
- with st.spinner("Processing video..."):
213
- result_path, message = process_video(video_path, bg_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- if result_path:
216
- st.success(message)
217
- st.subheader("✨ Processed Video")
218
- st.video(result_path)
219
-
220
- # Download button
221
- with open(result_path, 'rb') as f:
222
- st.download_button(
223
- label="πŸ“₯ Download Processed Video",
224
- data=f.read(),
225
- file_name="processed_video.mp4",
226
- mime="video/mp4"
227
- )
228
- else:
229
- st.error(message)
230
- else:
231
- st.warning("Please upload a video first!")
 
 
 
232
 
233
- # Status info
234
  st.markdown("---")
 
235
  st.markdown("""
236
- ### πŸ“Š Current Status:
237
- - βœ… **Video processing**: Active
238
- - 🎨 **Background replacement**: Working
239
- - πŸ’Ύ **Memory optimization**: Enabled
240
- - πŸ”„ **Fallback processing**: Available
 
241
  """)
242
 
243
  if __name__ == "__main__":
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ BackgroundFX - Video Background Replacement with Green Screen Workflow
4
+ Hugging Face Space Implementation with SAM2 + MatAnyone
5
+ """
6
 
7
  import streamlit as st
8
  import cv2
9
  import numpy as np
 
10
  import tempfile
 
11
  import os
12
+ from PIL import Image
13
+ import requests
14
+ from io import BytesIO
15
+ import logging
16
 
17
+ # Configure logging
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
 
21
+ # Try to import SAM2 and MatAnyone
22
+ try:
23
+ from sam2.build_sam import build_sam2_video_predictor
24
+ from sam2.sam2_image_predictor import SAM2ImagePredictor
25
+ SAM2_AVAILABLE = True
26
+ logger.info("βœ… SAM2 loaded successfully")
27
+ except ImportError as e:
28
+ SAM2_AVAILABLE = False
29
+ logger.warning(f"⚠️ SAM2 not available: {e}")
30
 
31
+ try:
32
+ import matanyone
33
+ MATANYONE_AVAILABLE = True
34
+ logger.info("βœ… MatAnyone loaded successfully")
35
+ except ImportError as e:
36
+ MATANYONE_AVAILABLE = False
37
+ logger.warning(f"⚠️ MatAnyone not available: {e}")
38
+
39
+ def load_background_image(background_url):
40
+ """Load background image from URL"""
41
+ try:
42
+ response = requests.get(background_url)
43
+ response.raise_for_status()
44
+ image = Image.open(BytesIO(response.content))
45
+ return np.array(image.convert('RGB'))
46
+ except Exception as e:
47
+ logger.error(f"Failed to load background image: {e}")
48
+ # Return default brick wall background
49
+ return create_default_background()
50
 
51
+ def create_default_background():
52
+ """Create a default brick wall background"""
53
+ # Create a simple brick pattern
54
+ height, width = 720, 1280
55
+ background = np.ones((height, width, 3), dtype=np.uint8) * 150
56
 
57
+ # Add brick pattern
58
+ brick_height, brick_width = 40, 80
59
+ for y in range(0, height, brick_height):
60
+ for x in range(0, width, brick_width):
61
+ # Alternate brick offset
62
+ offset = brick_width // 2 if (y // brick_height) % 2 else 0
63
+ x_pos = (x + offset) % width
64
+
65
+ # Draw brick
66
+ cv2.rectangle(background,
67
+ (x_pos, y),
68
+ (min(x_pos + brick_width - 2, width), min(y + brick_height - 2, height)),
69
+ (180, 120, 80), -1)
70
+ cv2.rectangle(background,
71
+ (x_pos, y),
72
+ (min(x_pos + brick_width - 2, width), min(y + brick_height - 2, height)),
73
+ (120, 80, 40), 2)
74
 
75
+ return background
76
+
77
+ def segment_person_sam2(frame):
78
+ """Segment person using SAM2"""
79
  try:
80
+ # Initialize SAM2 predictor
81
+ predictor = SAM2ImagePredictor.from_pretrained("facebook/sam2-hiera-large")
 
 
 
 
 
 
 
82
 
83
+ # Set image
84
+ predictor.set_image(frame)
85
+
86
+ # Use center point as prompt (assuming person is in center)
87
+ h, w = frame.shape[:2]
88
+ center_point = np.array([[w//2, h//2]])
89
+ center_label = np.array([1])
90
+
91
+ # Predict mask
92
+ masks, scores, _ = predictor.predict(
93
+ point_coords=center_point,
94
+ point_labels=center_label,
95
+ multimask_output=False
96
+ )
97
+
98
+ return masks[0] if len(masks) > 0 else None
99
 
100
  except Exception as e:
101
+ logger.error(f"SAM2 segmentation failed: {e}")
102
+ return None
103
 
104
+ def segment_person_fallback(frame):
105
+ """Fallback person segmentation using color-based method"""
106
  try:
107
+ # Convert to HSV for better skin detection
108
+ hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV)
109
 
110
+ # Define skin color range
111
  lower_skin = np.array([0, 20, 70])
112
  upper_skin = np.array([20, 255, 255])
 
113
 
114
+ # Create mask for skin tones
115
+ skin_mask = cv2.inRange(hsv, lower_skin, upper_skin)
 
 
116
 
117
+ # Morphological operations to clean up mask
118
+ kernel = np.ones((5, 5), np.uint8)
119
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
120
+ skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)
121
 
122
+ # Find largest contour (assumed to be person)
123
+ contours, _ = cv2.findContours(skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
 
124
 
125
+ if contours:
126
+ # Get largest contour
127
+ largest_contour = max(contours, key=cv2.contourArea)
128
+
129
+ # Create mask from contour
130
+ mask = np.zeros(frame.shape[:2], dtype=np.uint8)
131
+ cv2.fillPoly(mask, [largest_contour], 255)
132
+
133
+ # Expand mask to include more of the person
134
+ kernel = np.ones((20, 20), np.uint8)
135
+ mask = cv2.dilate(mask, kernel, iterations=2)
136
+
137
+ return mask.astype(bool)
138
 
139
+ return None
 
140
 
141
  except Exception as e:
142
+ logger.error(f"Fallback segmentation failed: {e}")
143
+ return None
144
+
145
+ def insert_green_screen(frame, person_mask):
146
+ """Insert green screen background while preserving person"""
147
+ try:
148
+ # Create green background
149
+ green_background = np.zeros_like(frame)
150
+ green_background[:, :] = [0, 255, 0] # Pure green (RGB)
151
+
152
+ # Combine person with green background
153
+ # Where mask is True (person), keep original frame
154
+ # Where mask is False (background), use green
155
+ result = np.where(person_mask[..., None], frame, green_background)
156
+
157
+ return result
158
+
159
+ except Exception as e:
160
+ logger.error(f"Green screen insertion failed: {e}")
161
  return frame
162
 
163
+ def chroma_key_replacement(green_screen_frame, new_background):
164
+ """Replace green screen with new background using chroma key"""
165
+ try:
166
+ # Resize background to match frame
167
+ h, w = green_screen_frame.shape[:2]
168
+ background_resized = cv2.resize(new_background, (w, h))
169
+
170
+ # Convert to HSV for better green detection
171
+ hsv = cv2.cvtColor(green_screen_frame, cv2.COLOR_RGB2HSV)
172
+
173
+ # Define green color range for chroma key
174
+ lower_green = np.array([40, 50, 50])
175
+ upper_green = np.array([80, 255, 255])
176
+
177
+ # Create mask for green pixels
178
+ green_mask = cv2.inRange(hsv, lower_green, upper_green)
179
+
180
+ # Smooth the mask
181
+ kernel = np.ones((3, 3), np.uint8)
182
+ green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_CLOSE, kernel)
183
+ green_mask = cv2.GaussianBlur(green_mask, (5, 5), 0)
184
+
185
+ # Normalize mask to 0-1 range
186
+ mask_normalized = green_mask.astype(float) / 255
187
+
188
+ # Apply chroma key replacement
189
+ result = green_screen_frame.copy()
190
+ for c in range(3):
191
+ result[:, :, c] = (green_screen_frame[:, :, c] * (1 - mask_normalized) +
192
+ background_resized[:, :, c] * mask_normalized)
193
+
194
+ return result.astype(np.uint8)
195
+
196
+ except Exception as e:
197
+ logger.error(f"Chroma key replacement failed: {e}")
198
+ return green_screen_frame
199
+
200
+ def process_video_with_green_screen(video_path, background_url, progress_callback=None):
201
+ """Process video with proper green screen workflow"""
202
  try:
203
+ # Load background image
204
+ background_image = load_background_image(background_url)
205
 
206
+ # Open video
207
+ cap = cv2.VideoCapture(video_path)
 
 
208
 
209
  # Get video properties
210
  fps = int(cap.get(cv2.CAP_PROP_FPS))
 
212
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
213
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
214
 
215
+ # Create output video writer
 
 
 
 
 
 
 
 
 
 
 
216
  output_path = tempfile.mktemp(suffix='.mp4')
217
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
218
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
219
 
 
 
 
 
220
  frame_count = 0
 
221
 
 
222
  while True:
223
  ret, frame = cap.read()
224
  if not ret:
225
  break
226
 
227
+ # Convert BGR to RGB
228
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
229
 
230
+ # Step 1: Segment person
231
+ if SAM2_AVAILABLE:
232
+ person_mask = segment_person_sam2(frame_rgb)
233
+ method_used = "SAM2"
234
+ else:
235
+ person_mask = segment_person_fallback(frame_rgb)
236
+ method_used = "Fallback"
237
 
238
+ if person_mask is not None:
239
+ # Step 2: Insert green screen
240
+ green_screen_frame = insert_green_screen(frame_rgb, person_mask)
241
+
242
+ # Step 3: Chroma key replacement
243
+ final_frame = chroma_key_replacement(green_screen_frame, background_image)
244
+ else:
245
+ # If segmentation fails, use original frame
246
+ final_frame = frame_rgb
247
+ method_used = "No segmentation"
248
+
249
+ # Convert back to BGR for video writer
250
+ final_frame_bgr = cv2.cvtColor(final_frame, cv2.COLOR_RGB2BGR)
251
+ out.write(final_frame_bgr)
252
+
253
+ frame_count += 1
254
 
255
+ # Update progress
256
+ if progress_callback:
257
+ progress = frame_count / total_frames
258
+ progress_callback(progress, f"Processing frame {frame_count}/{total_frames} ({method_used})")
259
 
260
+ # Release resources
261
  cap.release()
262
  out.release()
263
 
264
+ return output_path
 
265
 
 
 
 
 
 
266
  except Exception as e:
267
+ logger.error(f"Video processing failed: {e}")
268
+ return None
269
 
 
270
  def main():
271
+ """Streamlit main function"""
272
+ st.set_page_config(
273
+ page_title="BackgroundFX - Video Background Replacement",
274
+ page_icon="🎬",
275
+ layout="wide"
276
+ )
277
+
278
+ st.title("🎬 BackgroundFX - Video Background Replacement")
279
+ st.markdown("**Professional video background replacement with green screen workflow**")
280
+
281
+ # Show available methods
282
+ methods = []
283
+ if SAM2_AVAILABLE:
284
+ methods.append("βœ… SAM2 (AI Segmentation)")
285
+ if MATANYONE_AVAILABLE:
286
+ methods.append("βœ… MatAnyone (Advanced Processing)")
287
+ methods.append("βœ… Fallback Method (Color-based)")
288
 
289
+ st.sidebar.markdown("### Available Methods")
290
+ for method in methods:
291
+ st.sidebar.markdown(method)
 
292
 
293
+ # File upload
294
  col1, col2 = st.columns(2)
295
 
296
  with col1:
297
+ st.markdown("### πŸ“Ή Upload Video")
298
  uploaded_video = st.file_uploader(
299
+ "Choose a video file",
300
  type=['mp4', 'avi', 'mov', 'mkv'],
301
  help="Upload the video you want to process"
302
  )
303
 
304
  if uploaded_video:
305
+ # Display video info
306
+ st.success(f"βœ… Video uploaded: {uploaded_video.name}")
307
+
308
  # Save uploaded video
309
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
310
+ tmp_file.write(uploaded_video.read())
311
+ video_path = tmp_file.name
312
 
313
+ # Show video preview
314
+ st.video(uploaded_video)
315
 
316
  with col2:
317
+ st.markdown("### πŸ–ΌοΈ Background Image")
318
+
319
+ # Default background options
320
+ background_options = {
321
+ "Brick Wall": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1280&h=720&fit=crop",
322
+ "Office": "https://images.unsplash.com/photo-1497366216548-37526070297c?w=1280&h=720&fit=crop",
323
+ "Nature": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1280&h=720&fit=crop",
324
+ "City": "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=1280&h=720&fit=crop"
325
+ }
326
+
327
+ selected_background = st.selectbox(
328
+ "Choose background",
329
+ options=list(background_options.keys()),
330
+ index=0
331
  )
332
 
333
+ background_url = background_options[selected_background]
334
+
335
+ # Show background preview
336
+ try:
337
+ background_image = load_background_image(background_url)
338
+ st.image(background_image, caption=f"Background: {selected_background}", use_column_width=True)
339
+ except:
340
+ st.error("Failed to load background image")
 
 
341
 
342
  # Process button
343
+ if uploaded_video and st.button("🎬 Process Video", type="primary"):
344
+
345
+ with st.spinner("Processing video with green screen workflow..."):
346
+ # Create progress bar
347
+ progress_bar = st.progress(0)
348
+ status_text = st.empty()
349
+
350
+ def update_progress(progress, message):
351
+ progress_bar.progress(progress)
352
+ status_text.text(message)
353
+
354
+ # Process video
355
+ output_path = process_video_with_green_screen(
356
+ video_path,
357
+ background_url,
358
+ progress_callback=update_progress
359
+ )
360
+
361
+ if output_path and os.path.exists(output_path):
362
+ st.success("βœ… Video processing completed!")
363
+
364
+ # Display processed video
365
+ st.markdown("### πŸŽ‰ Processed Video")
366
 
367
+ with open(output_path, 'rb') as video_file:
368
+ video_bytes = video_file.read()
369
+ st.video(video_bytes)
370
+
371
+ # Download button
372
+ st.download_button(
373
+ label="πŸ“₯ Download Processed Video",
374
+ data=video_bytes,
375
+ file_name=f"backgroundfx_{uploaded_video.name}",
376
+ mime="video/mp4"
377
+ )
378
+
379
+ # Cleanup
380
+ try:
381
+ os.unlink(video_path)
382
+ os.unlink(output_path)
383
+ except:
384
+ pass
385
+ else:
386
+ st.error("❌ Video processing failed. Please try again.")
387
 
388
+ # Footer
389
  st.markdown("---")
390
+ st.markdown("### πŸ”§ Technical Details")
391
  st.markdown("""
392
+ **Green Screen Workflow:**
393
+ 1. **Person Segmentation** - AI identifies the person in each frame
394
+ 2. **Green Screen Insert** - Replaces background with pure green
395
+ 3. **Chroma Key Replacement** - Replaces green with new background
396
+
397
+ This ensures clean edges and professional results.
398
  """)
399
 
400
  if __name__ == "__main__":