MogensR commited on
Commit
df8bf83
Β·
verified Β·
1 Parent(s): 777d4e3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +319 -288
app.py CHANGED
@@ -1,4 +1,18 @@
1
  #!/usr/bin/env python3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
  High-Quality Video Background Replacement - MAIN APPLICATION
4
  Upload video β†’ Choose professional background β†’ Replace with cinema quality
@@ -6,7 +20,6 @@
6
  cinema-quality processing, lazy loading, and enhanced stability
7
  """
8
 
9
- import os
10
  import sys
11
  import tempfile
12
  import cv2
@@ -25,24 +38,17 @@
25
  import queue
26
  from typing import Optional, Tuple, Dict, Any
27
  import logging
 
28
 
29
- # Import all utilities
 
 
30
  from utilities import *
31
 
32
- # Fix OpenMP threads issue - remove problematic environment variable
33
- try:
34
- if 'OMP_NUM_THREADS' in os.environ:
35
- del os.environ['OMP_NUM_THREADS']
36
- except:
37
- pass
38
-
39
- # Suppress warnings and optimize for quality
40
- import warnings
41
  warnings.filterwarnings("ignore")
42
- os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'max_split_size_mb:1024'
43
- os.environ['CUDA_LAUNCH_BLOCKING'] = '0'
44
 
45
- # Setup logging for debugging
46
  logging.basicConfig(level=logging.INFO)
47
  logger = logging.getLogger(__name__)
48
 
@@ -66,97 +72,163 @@ def patched_get_type(schema):
66
  logger.warning(f"⚠️ Could not apply Gradio monkey patch: {e}")
67
 
68
  # ============================================================================ #
69
-
70
- def load_sam2_predictor(device="cuda", progress=gr.Progress()):
 
71
  """
72
  Loads the SAM2 model and returns a SAM2ImagePredictor instance.
73
- - Tries to load 'sam2_hiera_large' first.
74
- - Falls back to 'sam2_hiera_tiny' if large cannot be loaded. # <--- OBS: Denne linje kan du ogsΓ₯ opdatere!
75
- - Assumes YAML configs are in ./Configs/ (capital C), as required by upstream.
76
  """
77
  import hydra
78
  from omegaconf import OmegaConf
79
- import torch
80
- import logging
81
 
82
- logger = logging.getLogger("SAM2Loader")
83
  configs_dir = os.path.abspath("Configs")
84
- logger.info(f"Looking for SAM2 configs in absolute path: {configs_dir}")
85
 
86
  if not os.path.isdir(configs_dir):
87
- logger.error(f"FATAL: Configs directory not found at '{configs_dir}'. Please ensure the 'Configs' folder is at the root of your repository.")
88
- raise gr.Error(f"FATAL: SAM2 Configs directory not found. Check repository structure.")
 
 
 
89
 
90
  tried = []
91
 
92
- def try_load(config_name, checkpoint_name):
 
 
 
 
 
 
 
93
  try:
94
  checkpoint_path = os.path.join("./checkpoints", checkpoint_name)
95
- logger.info(f"Attempting to use checkpoint: {checkpoint_path}")
96
 
97
  if not os.path.exists(checkpoint_path):
98
- logger.info(f"Downloading {checkpoint_name} from Hugging Face Hub...")
99
- progress(0.1, desc=f"Downloading {checkpoint_name}...")
100
  from huggingface_hub import hf_hub_download
 
101
  checkpoint_path = hf_hub_download(
102
- repo_id=f"facebook/{config_name.replace('.yaml', '')}", # e.g. facebook/sam2_hiera_large
103
  filename=checkpoint_name,
104
  cache_dir="./checkpoints",
105
  local_dir_use_symlinks=False
106
  )
107
- logger.info(f"βœ… Download complete: {checkpoint_path}")
108
 
 
109
  if hydra.core.global_hydra.GlobalHydra.instance().is_initialized():
110
  hydra.core.global_hydra.GlobalHydra.instance().clear()
111
 
112
  hydra.initialize(config_path=os.path.relpath(configs_dir), job_name=f"sam2_load_{int(time.time())}")
113
- cfg = hydra.compose(config_name=config_name)
 
114
 
115
- logger.info(f"Trying to load {config_name} on {device} with checkpoint {checkpoint_path}")
116
- progress(0.3, desc=f"Loading {config_name}...")
117
-
118
  from sam2.build_sam import build_sam2
119
  from sam2.sam2_image_predictor import SAM2ImagePredictor
120
 
121
- sam2_model = build_sam2(config_name, checkpoint_path)
 
 
 
 
122
  sam2_model.to(device)
123
  predictor = SAM2ImagePredictor(sam2_model)
124
- logger.info(f"βœ… Loaded {config_name} successfully on {device}")
125
  return predictor
126
  except Exception as e:
127
- error_msg = f"Failed to load {config_name}: {e}\nTraceback: {traceback.format_exc()}"
128
  tried.append(error_msg)
129
- logger.warning(error_msg)
130
  return None
131
 
132
  predictor = try_load("sam2_hiera_large.yaml", "sam2_hiera_large.pt")
133
- # if predictor is None:
134
- # logger.warning("Could not load large model, falling back to tiny model.")
135
- # predictor = try_load("sam2_hiera_tiny.yaml", "sam2_hiera_tiny.pt")
136
- # if predictor:
137
- # logger.warning("⚠️ Using Tiny model as fallback (less accurate, but faster and lighter).")
138
 
139
  if predictor is None:
140
- error_message = "SAM2 loading failed for large model. Reasons: \n" + "\n".join(tried)
141
- logger.error(f"❌ {error_message}")
142
  raise gr.Error(error_message)
143
 
144
  return predictor
145
 
146
- # -------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
147
 
148
- # Global variables for models (lazy loading)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  sam2_predictor = None
150
  matanyone_model = None
151
  models_loaded = False
152
  loading_lock = threading.Lock()
153
 
154
- # ------- Robust download_and_setup_models() using above loader --------
155
-
156
- def download_and_setup_models(progress=gr.Progress()):
157
  """
158
- Download and setup models (SAM2 and MatAnyone), robust to Hugging Face Spaces and local dev.
159
- Uses local YAML config, falls back to Tiny if Large can't be loaded.
160
  """
161
  global sam2_predictor, matanyone_model, models_loaded
162
 
@@ -166,77 +238,79 @@ def download_and_setup_models(progress=gr.Progress()):
166
  try:
167
  logger.info("πŸ”„ Starting ENHANCED model loading with fallback...")
168
 
169
- # --- Load SAM2 ---
170
  device = "cuda" if torch.cuda.is_available() else "cpu"
171
- sam2_predictor_local = load_sam2_predictor(device, progress)
172
- sam2_predictor = sam2_predictor_local
173
 
174
- # --- Load MatAnyone (your original robust loader logic) ---
175
- matanyone_model_local = None
176
- matanyone_loaded = False
 
 
 
177
  try:
178
- from huggingface_hub import hf_hub_download
179
- from matanyone import InferenceCore
180
- matanyone_model_local = InferenceCore(
181
- "PeiqingYang/MatAnyone-v1.0",
182
- device=device,
183
- cfg={}
184
- )
185
- matanyone_loaded = True
186
- logger.info("βœ… MatAnyone loaded via HuggingFace Hub")
187
  except Exception as e:
188
- logger.warning(f"❌ MatAnyone load failed: {e}")
189
-
190
- if not matanyone_loaded:
191
  raise RuntimeError("MatAnyone model could not be loaded.")
192
 
193
- matanyone_model = matanyone_model_local
194
-
195
  models_loaded = True
196
  logger.info("--- βœ… All models loaded successfully ---")
197
- return "βœ… SAM2 + MatAnyone loaded successfully!"
198
  except Exception as e:
199
  logger.error(f"❌ Enhanced loading failed: {str(e)}")
200
  logger.error(f"Full traceback: {traceback.format_exc()}")
201
  return f"❌ Enhanced loading failed: {str(e)}"
202
- # =======================================================================
203
- # [START REST OF YOUR MAIN APP, UNCHANGED]
204
- # =======================================================================
205
 
206
- def process_video_hq(video_path, background_choice, custom_background_path, progress=gr.Progress()):
 
 
 
 
 
 
 
 
207
  """TWO-STAGE High-quality video processing: Original β†’ Green Screen β†’ Final Background"""
208
  if not models_loaded:
209
  return None, "❌ Models not loaded. Click 'Load Models' first."
210
-
211
  if not video_path:
212
  return None, "❌ No video file provided."
213
-
 
 
 
 
 
 
 
214
  try:
215
- progress(0, desc="🎬 Initializing TWO-STAGE processing...")
216
-
217
  # Validate and read video
218
  if not os.path.exists(video_path):
219
  return None, f"❌ Video file not found: {video_path}"
220
-
221
  cap = cv2.VideoCapture(video_path)
222
  if not cap.isOpened():
223
  return None, "❌ Could not open video file. Please check the format."
224
-
225
  # Get video properties
226
  fps = cap.get(cv2.CAP_PROP_FPS)
227
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
228
  frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
229
  frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
230
-
231
  logger.info(f"Video properties: {frame_width}x{frame_height}, {fps}fps, {total_frames} frames")
232
-
233
  if total_frames == 0:
234
  return None, "❌ Video appears to be empty or corrupted."
235
-
236
  # Prepare final background for Stage 2
237
  background = None
238
  background_name = ""
239
-
240
  if background_choice == "custom" and custom_background_path:
241
  try:
242
  background = cv2.imread(custom_background_path)
@@ -258,125 +332,125 @@ def process_video_hq(video_path, background_choice, custom_background_path, prog
258
  return None, f"❌ Error creating background: {str(e)}"
259
  else:
260
  return None, f"❌ Invalid background selection: {background_choice}"
261
-
262
  if background is None:
263
  return None, "❌ Failed to create background."
264
-
265
  # Setup codec and timestamp
266
  timestamp = int(time.time())
267
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
268
-
269
  # STAGE 1: Create green screen video (Original β†’ Green Screen)
270
- progress(0.1, desc="🟒 STAGE 1: Creating green screen version...")
271
  greenscreen_path = f"/tmp/greenscreen_{timestamp}.mp4"
272
  greenscreen_writer = cv2.VideoWriter(greenscreen_path, fourcc, fps, (frame_width, frame_height))
273
-
274
  if not greenscreen_writer.isOpened():
275
  return None, "❌ Could not create green screen video file."
276
-
277
  frame_count = 0
278
-
279
  # Process original video to green screen
280
  while True:
281
  ret, frame = cap.read()
282
  if not ret:
283
  break
284
-
285
  try:
286
  progress_pct = 0.1 + (frame_count / total_frames) * 0.4
287
- progress(progress_pct, desc=f"🟒 Green screen frame {frame_count + 1}/{total_frames}")
288
-
289
  # Segment person and create green screen frame
290
  mask = segment_person_hq(frame)
291
  refined_mask = refine_mask_hq(frame, mask)
292
  green_screen = create_green_screen_background(frame)
293
  green_screen_frame = replace_background_hq(frame, refined_mask, green_screen)
294
-
295
  greenscreen_writer.write(green_screen_frame)
296
  frame_count += 1
297
-
298
  if frame_count % 100 == 0:
299
  gc.collect()
300
  if torch.cuda.is_available():
301
  torch.cuda.empty_cache()
302
-
303
  except Exception as e:
304
  logger.warning(f"Error in Stage 1 frame {frame_count}: {e}")
305
  greenscreen_writer.write(frame)
306
  frame_count += 1
307
-
308
  greenscreen_writer.release()
309
  cap.release()
310
-
311
  # STAGE 2: Replace green screen with final background (Green Screen β†’ Final)
312
- progress(0.5, desc=f"🎨 STAGE 2: Replacing green screen with {background_name}...")
313
-
314
  final_path = f"/tmp/final_output_{timestamp}.mp4"
315
  final_writer = cv2.VideoWriter(final_path, fourcc, fps, (frame_width, frame_height))
316
-
317
  if not final_writer.isOpened():
318
  return None, "❌ Could not create final output video file."
319
-
320
  # Open green screen video
321
  greenscreen_cap = cv2.VideoCapture(greenscreen_path)
322
  if not greenscreen_cap.isOpened():
323
  return None, "❌ Could not open green screen video."
324
-
325
  frame_count = 0
326
-
327
  # Process green screen video to final background with enhanced green detection
328
  while True:
329
  ret, green_frame = greenscreen_cap.read()
330
  if not ret:
331
  break
332
-
333
  try:
334
  progress_pct = 0.5 + (frame_count / total_frames) * 0.4
335
- progress(progress_pct, desc=f"🎬 Final compositing frame {frame_count + 1}/{total_frames}")
336
-
337
  # Detect green screen with wider detection range
338
  hsv = cv2.cvtColor(green_frame, cv2.COLOR_BGR2HSV)
339
- lower_green = np.array([25, 30, 30]) # More sensitive (default: [35,40,40])
340
- upper_green = np.array([100, 255, 255]) # Wider range (default: [85,255,255])
341
  green_mask = cv2.inRange(hsv, lower_green, upper_green)
342
-
343
  # Additional mask processing for cleaner edges
344
- kernel = np.ones((3,3), np.uint8)
345
  green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_OPEN, kernel)
346
  green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_CLOSE, kernel)
347
  green_mask = 255 - green_mask # Invert mask (person = white, green = black)
348
-
349
  result_frame = replace_background_hq(green_frame, green_mask, background)
350
  final_writer.write(result_frame)
351
  frame_count += 1
352
-
353
  if frame_count % 100 == 0:
354
  gc.collect()
355
  if torch.cuda.is_available():
356
  torch.cuda.empty_cache()
357
-
358
  except Exception as e:
359
  logger.warning(f"Error in Stage 2 frame {frame_count}: {e}")
360
  final_writer.write(green_frame)
361
  frame_count += 1
362
-
363
  greenscreen_cap.release()
364
  final_writer.release()
365
-
366
  # Cleanup intermediate green screen file
367
  try:
368
  os.remove(greenscreen_path)
369
- except:
370
  pass
371
-
372
  if frame_count == 0:
373
  return None, "❌ No frames were processed successfully."
374
-
375
- progress(0.9, desc="🎡 Adding high-quality audio...")
376
-
377
  # Add audio back with high quality settings
378
  final_output = f"/tmp/final_output_hq_{timestamp}.mp4"
379
-
380
  try:
381
  audio_cmd = (
382
  f'ffmpeg -y -i "{final_path}" -i "{video_path}" '
@@ -384,13 +458,12 @@ def process_video_hq(video_path, background_choice, custom_background_path, prog
384
  f'-c:a aac -b:a 192k -ac 2 -ar 48000 '
385
  f'-map 0:v:0 -map 1:a:0? -shortest "{final_output}"'
386
  )
387
-
388
  result = os.system(audio_cmd)
389
-
390
  if result != 0 or not os.path.exists(final_output):
391
  logger.warning("Audio merging failed, using video without audio")
392
  shutil.copy2(final_path, final_output)
393
-
394
  except Exception as e:
395
  logger.warning(f"Audio processing error: {e}, using video without audio")
396
  try:
@@ -398,50 +471,53 @@ def process_video_hq(video_path, background_choice, custom_background_path, prog
398
  except Exception as e2:
399
  logger.error(f"Failed to copy video file: {e2}")
400
  return None, f"❌ Failed to finalize video: {str(e2)}"
401
-
402
  # Save to MyAvatar/My Videos directory
403
  try:
404
  myavatar_path = "/tmp/MyAvatar/My_Videos/"
405
  os.makedirs(myavatar_path, exist_ok=True)
406
-
407
  saved_filename = f"two_stage_bg_replaced_{timestamp}.mp4"
408
  saved_path = os.path.join(myavatar_path, saved_filename)
409
  shutil.copy2(final_output, saved_path)
410
-
411
  logger.info(f"Video saved to: {saved_path}")
412
  except Exception as e:
413
  logger.warning(f"Could not save to MyAvatar directory: {e}")
414
  saved_filename = os.path.basename(final_output)
415
-
416
  # Cleanup temporary files
417
  try:
418
  if os.path.exists(final_path):
419
  os.remove(final_path)
420
- except:
421
  pass
422
-
423
- progress(1.0, desc="βœ… TWO-STAGE processing complete!")
424
-
425
  success_message = (
426
  f"βœ… TWO-STAGE Success!\n"
427
  f"🟒 Stage 1: Original β†’ Green Screen\n"
428
  f"🎬 Stage 2: Green Screen β†’ {background_name}\n"
429
- f"πŸ“Š Processed: {frame_count}/{total_frames} frames\n"
430
  f"πŸ“ Saved: MyAvatar/My Videos/{saved_filename}\n"
431
- f"🎯 Quality: Cinema-grade with SAM2 + MatAnyone\n"
432
  f"πŸš€ Method: Professional two-stage compositing"
433
  )
434
-
435
  return final_output, success_message
436
-
437
  except Exception as e:
438
  error_msg = f"❌ TWO-STAGE Processing Error: {str(e)}"
439
  logger.error(f"Video processing error: {traceback.format_exc()}")
440
  return None, error_msg
441
 
 
 
 
442
  def create_interface():
443
  """Create enhanced Gradio interface with comprehensive features and 4-method background system"""
444
-
445
  def extract_video_path(v):
446
  # Robustly extract file path from input (tuple, list, or string)
447
  if isinstance(v, (tuple, list)) and len(v) > 0:
@@ -449,36 +525,32 @@ def extract_video_path(v):
449
  return v
450
 
451
  with gr.Blocks(
452
- title="ENHANCED High-Quality Video Background Replacement",
453
  theme=gr.themes.Soft(),
454
  css="""
455
- .gradio-container {
456
- max-width: 1200px !important;
457
- }
458
- .progress-bar {
459
- background: linear-gradient(90deg, #3498db, #2ecc71) !important;
460
- }
461
  """
462
  ) as demo:
463
-
464
  # Header
465
  gr.Markdown("# 🎬 Cinema-Quality Video Background Replacement")
466
  gr.Markdown("**Upload a video β†’ Choose a background β†’ Get professional results with AI**")
467
- gr.Markdown("*Powered by SAM2 + MatAnyone with multi-fallback loading for maximum reliability*")
468
  gr.Markdown("---")
469
-
470
  with gr.Row():
471
  # Left column - Input and controls
472
  with gr.Column(scale=1):
473
  gr.Markdown("### πŸ“₯ Step 1: Upload Your Video")
474
  gr.Markdown("*Supports MP4, MOV, AVI, and other common formats*")
475
-
476
  video_input = gr.Video(
477
- label="πŸŽ₯ Drop your video here",
478
  height=300
479
  )
480
 
481
- # ================= ADD VIDEO PREVIEW BLOCK =================
482
  video_preview = gr.Video(
483
  label="πŸ“Ί Preview of Uploaded Video",
484
  height=200,
@@ -489,23 +561,24 @@ def extract_video_path(v):
489
  inputs=video_input,
490
  outputs=video_preview
491
  )
492
- # ================= END VIDEO PREVIEW BLOCK =================
493
 
494
  gr.Markdown("### 🎨 Step 2: Choose Background Method")
495
  gr.Markdown("*Select your preferred background creation method*")
496
-
497
- # 4 Background Methods
498
  background_method = gr.Radio(
499
- choices=[
500
- ("A) πŸ“· Upload Image", "upload"),
501
- ("B) 🎨 Professional Presets", "professional"),
502
- ("C) 🌈 Colors/Gradients", "colors"),
503
- ("D) πŸ€– AI Generated", "ai")
504
- ],
505
  value="professional",
506
  label="Background Method"
507
  )
508
-
 
 
 
 
 
 
 
509
  # Method A: Upload Image
510
  with gr.Group(visible=False) as upload_group:
511
  gr.Markdown("**πŸ“· Upload Your Background Image**")
@@ -513,7 +586,7 @@ def extract_video_path(v):
513
  label="Drop your background image here",
514
  type="filepath"
515
  )
516
-
517
  # Method B: Professional Presets
518
  with gr.Group(visible=True) as professional_group:
519
  gr.Markdown("**🎨 Professional Background Presets**")
@@ -522,114 +595,110 @@ def extract_video_path(v):
522
  value="office_modern",
523
  label="Select Professional Background"
524
  )
525
-
526
  # Method C: Colors/Gradients
527
  with gr.Group(visible=False) as colors_group:
528
  gr.Markdown("**🌈 Custom Colors & Gradients**")
529
-
530
  gradient_type = gr.Dropdown(
531
  choices=["solid", "vertical", "horizontal", "diagonal", "radial", "soft_radial"],
532
  value="vertical",
533
  label="Gradient Type"
534
  )
535
-
536
  with gr.Row():
537
  color1 = gr.ColorPicker(label="🎨 Color 1", value="#3498db")
538
  color2 = gr.ColorPicker(label="🎨 Color 2", value="#2ecc71")
539
-
540
  with gr.Row():
541
  color3 = gr.ColorPicker(label="🎨 Color 3", value="#e74c3c")
542
  use_third_color = gr.Checkbox(label="Use 3rd color", value=False)
543
-
544
  # Method D: AI Generated
545
  with gr.Group(visible=False) as ai_group:
546
  gr.Markdown("**πŸ€– AI Generated Background**")
547
-
548
  ai_prompt = gr.Textbox(
549
  label="Describe your background",
550
  placeholder="e.g., 'modern office with plants', 'sunset over mountains', 'abstract tech pattern'",
551
  lines=2
552
  )
553
-
554
  ai_style = gr.Dropdown(
555
  choices=["photorealistic", "artistic", "abstract", "minimalist", "corporate", "nature"],
556
  value="photorealistic",
557
  label="Style"
558
  )
559
-
560
  with gr.Row():
561
  generate_ai_btn = gr.Button("🎨 Generate Background", variant="secondary")
562
  ai_generated_image = gr.Image(label="Generated Background", type="filepath", visible=False)
563
-
564
  # Background method switching function
565
  def switch_background_method(method):
566
  return (
567
  gr.update(visible=(method == "upload")), # upload_group
568
- gr.update(visible=(method == "professional")), # professional_group
569
  gr.update(visible=(method == "colors")), # colors_group
570
  gr.update(visible=(method == "ai")) # ai_group
571
  )
572
-
573
  background_method.change(
574
  fn=switch_background_method,
575
  inputs=background_method,
576
  outputs=[upload_group, professional_group, colors_group, ai_group]
577
  )
578
-
579
  gr.Markdown("### 🎬 Processing Controls")
580
  gr.Markdown("*First load the AI models, then process your video*")
581
-
582
  with gr.Row():
583
  load_models_btn = gr.Button(
584
- "πŸš€ Step 1: Load AI Models",
585
- variant="secondary",
586
- size="lg"
587
  )
588
  process_btn = gr.Button(
589
- "✨ Step 2: Process Video",
590
- variant="primary",
591
- size="lg"
592
  )
593
-
594
  # System status
595
  status_text = gr.Textbox(
596
- label="πŸ”§ System Status",
597
- value=get_model_status(),
598
  interactive=False,
599
  lines=3
600
  )
601
-
602
  # Right column - Results and preview
603
  with gr.Column(scale=1):
604
  gr.Markdown("### πŸ“€ Your Results")
605
  gr.Markdown("*Processed video will appear here after Step 2*")
606
-
607
  video_output = gr.Video(
608
- label="🎬 Your Processed Video",
609
  height=400
610
  )
611
-
612
  result_text = gr.Textbox(
613
- label="πŸ“Š Processing Results",
614
  interactive=False,
615
  lines=6,
616
  placeholder="Processing status and results will appear here..."
617
  )
618
-
619
  gr.Markdown("### 🎨 Professional Backgrounds Available")
620
-
621
  # Create background preview grid
622
  bg_preview_html = """
623
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 10px; max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 8px;'>
624
  """
625
-
626
  for key, config in PROFESSIONAL_BACKGROUNDS.items():
627
  colors = config["colors"]
628
  if len(colors) >= 2:
629
  gradient = f"linear-gradient(45deg, {colors[0]}, {colors[-1]})"
630
  else:
631
  gradient = colors[0]
632
-
633
  bg_preview_html += f"""
634
  <div style='
635
  padding: 12px 8px;
@@ -648,23 +717,20 @@ def switch_background_method(method):
648
  </div>
649
  </div>
650
  """
651
-
652
  bg_preview_html += "</div>"
653
  gr.HTML(bg_preview_html)
654
-
655
  # AI Background Generation Function
656
  def generate_ai_background(prompt, style):
657
  """Generate AI background using procedural methods"""
658
  if not prompt or not prompt.strip():
659
  return None, "❌ Please enter a prompt"
660
-
661
  try:
662
  # Create procedural background based on prompt
663
  bg_image = create_procedural_background(prompt, style, 1920, 1080)
664
-
665
  if bg_image is not None:
666
- # Save generated image
667
- import tempfile
668
  with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
669
  cv2.imwrite(tmp.name, bg_image)
670
  return tmp.name, f"βœ… Background generated: {prompt[:50]}..."
@@ -673,90 +739,89 @@ def generate_ai_background(prompt, style):
673
  except Exception as e:
674
  logger.error(f"AI generation error: {e}")
675
  return None, f"❌ Generation error: {str(e)}"
676
-
677
  # Enhanced video processing function that handles all 4 methods
678
- def process_video_enhanced(video_path, bg_method, custom_img, prof_choice, grad_type,
679
- color1, color2, color3, use_third, ai_prompt, ai_style, ai_img,
680
- progress=gr.Progress()):
 
 
 
 
 
 
 
681
  """Process video with any of the 4 background methods using TWO-STAGE approach"""
682
-
683
  if not models_loaded:
684
  return None, "❌ Models not loaded. Click 'Load Models' first."
685
-
686
  if not video_path:
687
  return None, "❌ No video file provided."
688
-
689
  try:
690
- progress(0, desc="🎬 Preparing background...")
691
-
692
- # Determine which background to use based on method
693
  if bg_method == "upload":
694
  if custom_img and os.path.exists(custom_img):
695
  return process_video_hq(video_path, "custom", custom_img, progress)
696
  else:
697
  return None, "❌ No image uploaded. Please upload a background image."
698
-
699
  elif bg_method == "professional":
700
  if prof_choice and prof_choice in PROFESSIONAL_BACKGROUNDS:
701
  return process_video_hq(video_path, prof_choice, None, progress)
702
  else:
703
  return None, f"❌ Invalid professional background: {prof_choice}"
704
-
705
  elif bg_method == "colors":
706
- # Create custom gradient as temporary image
707
  try:
708
  colors = [color1 or "#3498db", color2 or "#2ecc71"]
709
  if use_third and color3:
710
  colors.append(color3)
711
-
712
  bg_config = {
713
  "type": "gradient" if grad_type != "solid" else "color",
714
- "colors": colors,
715
  "direction": grad_type if grad_type != "solid" else "vertical"
716
  }
717
-
718
- if grad_type == "solid":
719
- bg_config["colors"] = [colors[0]]
720
-
721
- # Create temporary image for gradient
722
  gradient_bg = create_professional_background(bg_config, 1920, 1080)
723
  temp_path = f"/tmp/gradient_{int(time.time())}.png"
724
  cv2.imwrite(temp_path, gradient_bg)
725
-
726
  return process_video_hq(video_path, "custom", temp_path, progress)
727
  except Exception as e:
728
  return None, f"❌ Error creating gradient: {str(e)}"
729
-
730
  elif bg_method == "ai":
731
  if ai_img and os.path.exists(ai_img):
732
  return process_video_hq(video_path, "custom", ai_img, progress)
733
  else:
734
  return None, "❌ No AI background generated. Click 'Generate Background' first."
735
-
736
  else:
737
  return None, f"❌ Unknown background method: {bg_method}"
738
-
739
  except Exception as e:
740
  logger.error(f"Enhanced processing error: {e}")
741
  return None, f"❌ Processing error: {str(e)}"
742
-
743
- # Connect all the functions
744
  load_models_btn.click(
745
  fn=download_and_setup_models,
746
  outputs=status_text
747
  )
748
-
749
  generate_ai_btn.click(
750
  fn=generate_ai_background,
751
  inputs=[ai_prompt, ai_style],
752
  outputs=[ai_generated_image, status_text]
753
  )
754
-
755
  process_btn.click(
756
  fn=process_video_enhanced,
757
  inputs=[
758
  video_input, # video_path
759
- background_method, # bg_method
760
  custom_background, # custom_img
761
  professional_choice, # prof_choice
762
  gradient_type, # grad_type
@@ -765,92 +830,58 @@ def process_video_enhanced(video_path, bg_method, custom_img, prof_choice, grad_
765
  ],
766
  outputs=[video_output, result_text]
767
  )
768
-
769
- # Comprehensive info section
770
  with gr.Accordion("ℹ️ ENHANCED Quality & Features", open=False):
771
  gr.Markdown("""
772
  ### πŸ† TWO-STAGE Cinema-Quality Features:
773
-
774
- **🎬 Two-Stage Processing:**
775
- - **Stage 1**: Original Video β†’ Green Screen Video (SAM2 + MatAnyone segmentation)
776
- - **Stage 2**: Green Screen Video β†’ Final Background (Professional chroma key replacement)
777
- - **Why Two-Stage?**: Better edge quality, cleaner separation, professional results
778
-
779
- **πŸ€– Advanced AI Models:**
780
- - **SAM2**: State-of-the-art segmentation (Large/Tiny auto-selection)
781
- - **MatAnyone**: CVPR 2025 professional matting technology
782
- - **Multi-Fallback Loading**: 4+ methods each for maximum reliability
783
- - **OpenCV Fallbacks**: Enhanced backup systems for compatibility
784
-
785
- **🎨 4 Background Methods:**
786
- - **A) Upload Image**: Use any custom image as background
787
- - **B) Professional Presets**: 15+ high-quality professional backgrounds
788
- - **C) Colors/Gradients**: Custom color combinations with 6 gradient types
789
- - **D) AI Generated**: Procedural backgrounds from text prompts
790
-
791
- **🎬 Professional Quality:**
792
- - **✨ Edge Feathering**: Smooth, natural transitions
793
- - **🎬 Gamma Correction**: Professional color compositing
794
- - **πŸ” Multi-Point Segmentation**: 7-point strategic person detection
795
- - **🧹 Morphological Processing**: Advanced mask cleanup
796
- - **🟒 Green Screen Intermediate**: Professional chroma key workflow
797
-
798
- **🎡 Audio & Video:**
799
- - **High-Quality Audio**: 192kbps AAC preservation
800
- - **πŸ“Ί H.264 Codec**: CRF 18 for broadcast quality
801
- - **🎞️ Frame Processing**: Advanced error handling
802
- - **πŸ’Ύ Smart Caching**: Optimized memory management
803
-
804
- ### πŸ’‘ Usage Tips:
805
- - Upload videos in common formats (MP4, MOV, AVI)
806
- - For best results, ensure good lighting in original video
807
- - Custom backgrounds work best with high resolution images
808
- - AI prompts: Try "modern office", "sunset mountain", "abstract tech"
809
- - GPU processing is faster but CPU fallback always available
810
- - Two-stage processing gives cinema-quality results
811
  """)
812
-
813
- # Footer
814
  gr.Markdown("---")
815
- gr.Markdown(
816
- "*🎬 Cinema-Quality Video Background Replacement - "
817
- "Enhanced with TWO-STAGE processing and 4-method background system*"
818
- )
819
-
820
  return demo
821
 
 
 
 
822
  def main():
823
  """Main application entry point"""
824
  try:
825
  print("🎬 Cinema-Quality Video Background Replacement")
826
  print("=" * 50)
827
-
828
- # Initialize application
829
  os.makedirs("/tmp/MyAvatar/My_Videos/", exist_ok=True)
830
  os.makedirs(os.path.expanduser("~/.cache/sam2"), exist_ok=True)
831
-
832
  print("πŸš€ Features:")
833
- print(" β€’ SAM2 + MatAnyone AI models")
834
  print(" β€’ TWO-STAGE processing (Original β†’ Green Screen β†’ Final)")
835
  print(" β€’ 4 background methods (Upload/Professional/Colors/AI)")
836
  print(" β€’ Multi-fallback loading system")
837
  print(" β€’ Cinema-quality processing")
838
  print(" β€’ Enhanced stability & error handling")
839
  print("=" * 50)
840
-
841
  # Create and launch interface
842
  logger.info("🌐 Creating Gradio interface...")
843
  demo = create_interface()
844
-
845
  logger.info("πŸš€ Launching application...")
846
-
847
  demo.launch(
848
  server_name="0.0.0.0",
849
  server_port=7860,
850
  share=True,
851
  show_error=True
852
  )
853
-
854
  except KeyboardInterrupt:
855
  logger.info("πŸ›‘ Application stopped by user")
856
  print("\nπŸ›‘ Application stopped by user")
 
1
  #!/usr/bin/env python3
2
+ # ========================= PRE-IMPORT ENV GUARDS =========================
3
+ # Must run BEFORE importing numpy/cv2/torch to avoid libgomp errors.
4
+ import os
5
+
6
+ # Remove invalid OMP setting or tame thread counts
7
+ os.environ.pop("OMP_NUM_THREADS", None) # or set to e.g. "1"
8
+ os.environ.setdefault("MKL_NUM_THREADS", "1")
9
+ os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
10
+ os.environ.setdefault("NUMEXPR_NUM_THREADS", "1")
11
+ # Optional CUDA allocator tuning
12
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "max_split_size_mb:1024")
13
+ os.environ.setdefault("CUDA_LAUNCH_BLOCKING", "0")
14
+ # ========================================================================
15
+
16
  """
17
  High-Quality Video Background Replacement - MAIN APPLICATION
18
  Upload video β†’ Choose professional background β†’ Replace with cinema quality
 
20
  cinema-quality processing, lazy loading, and enhanced stability
21
  """
22
 
 
23
  import sys
24
  import tempfile
25
  import cv2
 
38
  import queue
39
  from typing import Optional, Tuple, Dict, Any
40
  import logging
41
+ import warnings
42
 
43
+ # Import all utilities (must provide: PROFESSIONAL_BACKGROUNDS,
44
+ # create_professional_background, create_procedural_background,
45
+ # segment_person_hq, refine_mask_hq, replace_background_hq, get_model_status)
46
  from utilities import *
47
 
48
+ # Suppress warnings
 
 
 
 
 
 
 
 
49
  warnings.filterwarnings("ignore")
 
 
50
 
51
+ # Setup logging
52
  logging.basicConfig(level=logging.INFO)
53
  logger = logging.getLogger(__name__)
54
 
 
72
  logger.warning(f"⚠️ Could not apply Gradio monkey patch: {e}")
73
 
74
  # ============================================================================ #
75
+ # SAM2 LOADER (Hydra cfg, correct build)
76
+ # ============================================================================ #
77
+ def load_sam2_predictor(device: str = "cuda", progress: Optional[gr.Progress] = None):
78
  """
79
  Loads the SAM2 model and returns a SAM2ImagePredictor instance.
80
+ - Tries to load 'sam2_hiera_large' model config from ./Configs
81
+ - Downloads checkpoint via HF if not present
 
82
  """
83
  import hydra
84
  from omegaconf import OmegaConf
 
 
85
 
86
+ sam_logger = logging.getLogger("SAM2Loader")
87
  configs_dir = os.path.abspath("Configs")
88
+ sam_logger.info(f"Looking for SAM2 configs in absolute path: {configs_dir}")
89
 
90
  if not os.path.isdir(configs_dir):
91
+ sam_logger.error(
92
+ f"FATAL: Configs directory not found at '{configs_dir}'. "
93
+ f"Please ensure the 'Configs' folder exists at repository root."
94
+ )
95
+ raise gr.Error("FATAL: SAM2 Configs directory not found.")
96
 
97
  tried = []
98
 
99
+ def _maybe_progress(pct: float, desc: str):
100
+ if progress is not None:
101
+ try:
102
+ progress(pct, desc=desc)
103
+ except Exception:
104
+ pass
105
+
106
+ def try_load(config_name_with_yaml: str, checkpoint_name: str):
107
  try:
108
  checkpoint_path = os.path.join("./checkpoints", checkpoint_name)
109
+ sam_logger.info(f"Attempting to use checkpoint: {checkpoint_path}")
110
 
111
  if not os.path.exists(checkpoint_path):
112
+ sam_logger.info(f"Downloading {checkpoint_name} from Hugging Face Hub...")
113
+ _maybe_progress(0.1, f"Downloading {checkpoint_name}...")
114
  from huggingface_hub import hf_hub_download
115
+ repo = f"facebook/{config_name_with_yaml.replace('.yaml','')}" # e.g. facebook/sam2_hiera_large
116
  checkpoint_path = hf_hub_download(
117
+ repo_id=repo,
118
  filename=checkpoint_name,
119
  cache_dir="./checkpoints",
120
  local_dir_use_symlinks=False
121
  )
122
+ sam_logger.info(f"βœ… Download complete: {checkpoint_path}")
123
 
124
+ # Reset & init Hydra
125
  if hydra.core.global_hydra.GlobalHydra.instance().is_initialized():
126
  hydra.core.global_hydra.GlobalHydra.instance().clear()
127
 
128
  hydra.initialize(config_path=os.path.relpath(configs_dir), job_name=f"sam2_load_{int(time.time())}")
129
+ cfg_name = config_name_with_yaml.replace(".yaml", "")
130
+ cfg = hydra.compose(config_name=cfg_name)
131
 
 
 
 
132
  from sam2.build_sam import build_sam2
133
  from sam2.sam2_image_predictor import SAM2ImagePredictor
134
 
135
+ sam_logger.info(f"Trying to load {config_name_with_yaml} on {device} with checkpoint {checkpoint_path}")
136
+ _maybe_progress(0.3, f"Loading {config_name_with_yaml}...")
137
+
138
+ # IMPORTANT: pass cfg (not the string)
139
+ sam2_model = build_sam2(cfg, checkpoint_path)
140
  sam2_model.to(device)
141
  predictor = SAM2ImagePredictor(sam2_model)
142
+ sam_logger.info(f"βœ… Loaded {config_name_with_yaml} successfully on {device}")
143
  return predictor
144
  except Exception as e:
145
+ error_msg = f"Failed to load {config_name_with_yaml}: {e}\nTraceback: {traceback.format_exc()}"
146
  tried.append(error_msg)
147
+ sam_logger.warning(error_msg)
148
  return None
149
 
150
  predictor = try_load("sam2_hiera_large.yaml", "sam2_hiera_large.pt")
 
 
 
 
 
151
 
152
  if predictor is None:
153
+ error_message = "SAM2 loading failed for large model. Reasons:\n" + "\n".join(tried)
154
+ sam_logger.error(f"❌ {error_message}")
155
  raise gr.Error(error_message)
156
 
157
  return predictor
158
 
159
+ # ============================================================================ #
160
+ # MatAnyOne LOADER (robust cfg + net + core)
161
+ # ============================================================================ #
162
+ def load_matanyone(device: str):
163
+ """
164
+ Robust MatAnyOne loader:
165
+ - Loads an OmegaConf cfg from common paths or creates a minimal default
166
+ - Builds the network and wraps with InferenceCore
167
+ - Tries multiple import layouts to accommodate different repos
168
+ """
169
+ from omegaconf import OmegaConf
170
 
171
+ ma_logger = logging.getLogger("MatAnyOneLoader")
172
+
173
+ # Try to locate a config file; otherwise minimal default
174
+ cfg_path_candidates = [
175
+ "Configs/matanyone.yaml",
176
+ "configs/matanyone.yaml",
177
+ ]
178
+ cfg = None
179
+ for p in cfg_path_candidates:
180
+ if os.path.exists(p):
181
+ ma_logger.info(f"Loading MatAnyOne cfg: {p}")
182
+ cfg = OmegaConf.load(p)
183
+ break
184
+ if cfg is None:
185
+ ma_logger.warning("No MatAnyOne cfg found, using minimal defaults.")
186
+ cfg = OmegaConf.create({
187
+ "model": {"backbone": "swinB"},
188
+ "inference": {"amp": True},
189
+ "device": device,
190
+ })
191
+
192
+ last_err = None
193
+
194
+ # Layout A (common in forks): separate model + inference modules
195
+ try:
196
+ from matanyone.model.matanyone import MatAnyOne
197
+ from matanyone.inference.inference_core import InferenceCore
198
+ net = MatAnyOne(cfg)
199
+ net.to(device)
200
+ core = InferenceCore(net, cfg)
201
+ ma_logger.info("βœ… MatAnyOne loaded (layout A)")
202
+ return core
203
+ except Exception as e:
204
+ last_err = e
205
+ ma_logger.warning(f"Layout A failed: {e}")
206
+
207
+ # Layout B (single package exposing classes)
208
+ try:
209
+ from matanyone import MatAnyOne, InferenceCore
210
+ net = MatAnyOne(cfg)
211
+ net.to(device)
212
+ core = InferenceCore(net, cfg)
213
+ ma_logger.info("βœ… MatAnyOne loaded (layout B)")
214
+ return core
215
+ except Exception as e:
216
+ last_err = e
217
+ ma_logger.warning(f"Layout B failed: {e}")
218
+
219
+ raise RuntimeError(f"Failed to initialize MatAnyOne/InferenceCore. Last error: {last_err}")
220
+
221
+ # ============================================================================ #
222
+ # GLOBALS & MODEL SETUP
223
+ # ============================================================================ #
224
  sam2_predictor = None
225
  matanyone_model = None
226
  models_loaded = False
227
  loading_lock = threading.Lock()
228
 
229
+ def download_and_setup_models(progress: Optional[gr.Progress] = None):
 
 
230
  """
231
+ Download and setup models (SAM2 and MatAnyOne), robust to HF Spaces and local dev.
 
232
  """
233
  global sam2_predictor, matanyone_model, models_loaded
234
 
 
238
  try:
239
  logger.info("πŸ”„ Starting ENHANCED model loading with fallback...")
240
 
 
241
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
 
242
 
243
+ # --- Load SAM2 ---
244
+ local_sam2 = load_sam2_predictor(device=device, progress=progress)
245
+ # keep global
246
+ sam2_predictor = local_sam2
247
+
248
+ # --- Load MatAnyOne ---
249
  try:
250
+ local_matanyone = load_matanyone(device)
251
+ matanyone_model = local_matanyone
252
+ logger.info("βœ… MatAnyOne loaded")
 
 
 
 
 
 
253
  except Exception as e:
254
+ logger.warning(f"❌ MatAnyOne load failed: {e}")
 
 
255
  raise RuntimeError("MatAnyone model could not be loaded.")
256
 
 
 
257
  models_loaded = True
258
  logger.info("--- βœ… All models loaded successfully ---")
259
+ return "βœ… SAM2 + MatAnyOne loaded successfully!"
260
  except Exception as e:
261
  logger.error(f"❌ Enhanced loading failed: {str(e)}")
262
  logger.error(f"Full traceback: {traceback.format_exc()}")
263
  return f"❌ Enhanced loading failed: {str(e)}"
 
 
 
264
 
265
+ # ============================================================================ #
266
+ # TWO-STAGE PROCESSING PIPELINE
267
+ # ============================================================================ #
268
+ def process_video_hq(
269
+ video_path,
270
+ background_choice,
271
+ custom_background_path,
272
+ progress: Optional[gr.Progress] = None
273
+ ):
274
  """TWO-STAGE High-quality video processing: Original β†’ Green Screen β†’ Final Background"""
275
  if not models_loaded:
276
  return None, "❌ Models not loaded. Click 'Load Models' first."
277
+
278
  if not video_path:
279
  return None, "❌ No video file provided."
280
+
281
+ def _prog(pct: float, desc: str):
282
+ if progress is not None:
283
+ try:
284
+ progress(pct, desc=desc)
285
+ except Exception:
286
+ pass
287
+
288
  try:
289
+ _prog(0.0, "🎬 Initializing TWO-STAGE processing...")
290
+
291
  # Validate and read video
292
  if not os.path.exists(video_path):
293
  return None, f"❌ Video file not found: {video_path}"
294
+
295
  cap = cv2.VideoCapture(video_path)
296
  if not cap.isOpened():
297
  return None, "❌ Could not open video file. Please check the format."
298
+
299
  # Get video properties
300
  fps = cap.get(cv2.CAP_PROP_FPS)
301
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
302
  frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
303
  frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
304
+
305
  logger.info(f"Video properties: {frame_width}x{frame_height}, {fps}fps, {total_frames} frames")
306
+
307
  if total_frames == 0:
308
  return None, "❌ Video appears to be empty or corrupted."
309
+
310
  # Prepare final background for Stage 2
311
  background = None
312
  background_name = ""
313
+
314
  if background_choice == "custom" and custom_background_path:
315
  try:
316
  background = cv2.imread(custom_background_path)
 
332
  return None, f"❌ Error creating background: {str(e)}"
333
  else:
334
  return None, f"❌ Invalid background selection: {background_choice}"
335
+
336
  if background is None:
337
  return None, "❌ Failed to create background."
338
+
339
  # Setup codec and timestamp
340
  timestamp = int(time.time())
341
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
342
+
343
  # STAGE 1: Create green screen video (Original β†’ Green Screen)
344
+ _prog(0.1, "🟒 STAGE 1: Creating green screen version...")
345
  greenscreen_path = f"/tmp/greenscreen_{timestamp}.mp4"
346
  greenscreen_writer = cv2.VideoWriter(greenscreen_path, fourcc, fps, (frame_width, frame_height))
347
+
348
  if not greenscreen_writer.isOpened():
349
  return None, "❌ Could not create green screen video file."
350
+
351
  frame_count = 0
352
+
353
  # Process original video to green screen
354
  while True:
355
  ret, frame = cap.read()
356
  if not ret:
357
  break
358
+
359
  try:
360
  progress_pct = 0.1 + (frame_count / total_frames) * 0.4
361
+ _prog(progress_pct, f"🟒 Green screen frame {frame_count + 1}/{total_frames}")
362
+
363
  # Segment person and create green screen frame
364
  mask = segment_person_hq(frame)
365
  refined_mask = refine_mask_hq(frame, mask)
366
  green_screen = create_green_screen_background(frame)
367
  green_screen_frame = replace_background_hq(frame, refined_mask, green_screen)
368
+
369
  greenscreen_writer.write(green_screen_frame)
370
  frame_count += 1
371
+
372
  if frame_count % 100 == 0:
373
  gc.collect()
374
  if torch.cuda.is_available():
375
  torch.cuda.empty_cache()
376
+
377
  except Exception as e:
378
  logger.warning(f"Error in Stage 1 frame {frame_count}: {e}")
379
  greenscreen_writer.write(frame)
380
  frame_count += 1
381
+
382
  greenscreen_writer.release()
383
  cap.release()
384
+
385
  # STAGE 2: Replace green screen with final background (Green Screen β†’ Final)
386
+ _prog(0.5, f"🎨 STAGE 2: Replacing green screen with {background_name}...")
387
+
388
  final_path = f"/tmp/final_output_{timestamp}.mp4"
389
  final_writer = cv2.VideoWriter(final_path, fourcc, fps, (frame_width, frame_height))
390
+
391
  if not final_writer.isOpened():
392
  return None, "❌ Could not create final output video file."
393
+
394
  # Open green screen video
395
  greenscreen_cap = cv2.VideoCapture(greenscreen_path)
396
  if not greenscreen_cap.isOpened():
397
  return None, "❌ Could not open green screen video."
398
+
399
  frame_count = 0
400
+
401
  # Process green screen video to final background with enhanced green detection
402
  while True:
403
  ret, green_frame = greenscreen_cap.read()
404
  if not ret:
405
  break
406
+
407
  try:
408
  progress_pct = 0.5 + (frame_count / total_frames) * 0.4
409
+ _prog(progress_pct, f"🎬 Final compositing frame {frame_count + 1}/{total_frames}")
410
+
411
  # Detect green screen with wider detection range
412
  hsv = cv2.cvtColor(green_frame, cv2.COLOR_BGR2HSV)
413
+ lower_green = np.array([25, 30, 30])
414
+ upper_green = np.array([100, 255, 255])
415
  green_mask = cv2.inRange(hsv, lower_green, upper_green)
416
+
417
  # Additional mask processing for cleaner edges
418
+ kernel = np.ones((3, 3), np.uint8)
419
  green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_OPEN, kernel)
420
  green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_CLOSE, kernel)
421
  green_mask = 255 - green_mask # Invert mask (person = white, green = black)
422
+
423
  result_frame = replace_background_hq(green_frame, green_mask, background)
424
  final_writer.write(result_frame)
425
  frame_count += 1
426
+
427
  if frame_count % 100 == 0:
428
  gc.collect()
429
  if torch.cuda.is_available():
430
  torch.cuda.empty_cache()
431
+
432
  except Exception as e:
433
  logger.warning(f"Error in Stage 2 frame {frame_count}: {e}")
434
  final_writer.write(green_frame)
435
  frame_count += 1
436
+
437
  greenscreen_cap.release()
438
  final_writer.release()
439
+
440
  # Cleanup intermediate green screen file
441
  try:
442
  os.remove(greenscreen_path)
443
+ except Exception:
444
  pass
445
+
446
  if frame_count == 0:
447
  return None, "❌ No frames were processed successfully."
448
+
449
+ _prog(0.9, "🎡 Adding high-quality audio...")
450
+
451
  # Add audio back with high quality settings
452
  final_output = f"/tmp/final_output_hq_{timestamp}.mp4"
453
+
454
  try:
455
  audio_cmd = (
456
  f'ffmpeg -y -i "{final_path}" -i "{video_path}" '
 
458
  f'-c:a aac -b:a 192k -ac 2 -ar 48000 '
459
  f'-map 0:v:0 -map 1:a:0? -shortest "{final_output}"'
460
  )
 
461
  result = os.system(audio_cmd)
462
+
463
  if result != 0 or not os.path.exists(final_output):
464
  logger.warning("Audio merging failed, using video without audio")
465
  shutil.copy2(final_path, final_output)
466
+
467
  except Exception as e:
468
  logger.warning(f"Audio processing error: {e}, using video without audio")
469
  try:
 
471
  except Exception as e2:
472
  logger.error(f"Failed to copy video file: {e2}")
473
  return None, f"❌ Failed to finalize video: {str(e2)}"
474
+
475
  # Save to MyAvatar/My Videos directory
476
  try:
477
  myavatar_path = "/tmp/MyAvatar/My_Videos/"
478
  os.makedirs(myavatar_path, exist_ok=True)
479
+
480
  saved_filename = f"two_stage_bg_replaced_{timestamp}.mp4"
481
  saved_path = os.path.join(myavatar_path, saved_filename)
482
  shutil.copy2(final_output, saved_path)
483
+
484
  logger.info(f"Video saved to: {saved_path}")
485
  except Exception as e:
486
  logger.warning(f"Could not save to MyAvatar directory: {e}")
487
  saved_filename = os.path.basename(final_output)
488
+
489
  # Cleanup temporary files
490
  try:
491
  if os.path.exists(final_path):
492
  os.remove(final_path)
493
+ except Exception:
494
  pass
495
+
496
+ _prog(1.0, "βœ… TWO-STAGE processing complete!")
497
+
498
  success_message = (
499
  f"βœ… TWO-STAGE Success!\n"
500
  f"🟒 Stage 1: Original β†’ Green Screen\n"
501
  f"🎬 Stage 2: Green Screen β†’ {background_name}\n"
502
+ f"πŸ“Š Processed: {frame_count} frames\n"
503
  f"πŸ“ Saved: MyAvatar/My Videos/{saved_filename}\n"
504
+ f"🎯 Quality: Cinema-grade with SAM2 + MatAnyOne\n"
505
  f"πŸš€ Method: Professional two-stage compositing"
506
  )
507
+
508
  return final_output, success_message
509
+
510
  except Exception as e:
511
  error_msg = f"❌ TWO-STAGE Processing Error: {str(e)}"
512
  logger.error(f"Video processing error: {traceback.format_exc()}")
513
  return None, error_msg
514
 
515
+ # ============================================================================ #
516
+ # GRADIO UI
517
+ # ============================================================================ #
518
  def create_interface():
519
  """Create enhanced Gradio interface with comprehensive features and 4-method background system"""
520
+
521
  def extract_video_path(v):
522
  # Robustly extract file path from input (tuple, list, or string)
523
  if isinstance(v, (tuple, list)) and len(v) > 0:
 
525
  return v
526
 
527
  with gr.Blocks(
528
+ title="ENHANCED High-Quality Video Background Replacement",
529
  theme=gr.themes.Soft(),
530
  css="""
531
+ .gradio-container { max-width: 1200px !important; }
532
+ .progress-bar { background: linear-gradient(90deg, #3498db, #2ecc71) !important; }
 
 
 
 
533
  """
534
  ) as demo:
535
+
536
  # Header
537
  gr.Markdown("# 🎬 Cinema-Quality Video Background Replacement")
538
  gr.Markdown("**Upload a video β†’ Choose a background β†’ Get professional results with AI**")
539
+ gr.Markdown("*Powered by SAM2 + MatAnyOne with multi-fallback loading for maximum reliability*")
540
  gr.Markdown("---")
541
+
542
  with gr.Row():
543
  # Left column - Input and controls
544
  with gr.Column(scale=1):
545
  gr.Markdown("### πŸ“₯ Step 1: Upload Your Video")
546
  gr.Markdown("*Supports MP4, MOV, AVI, and other common formats*")
547
+
548
  video_input = gr.Video(
549
+ label="πŸŽ₯ Drop your video here",
550
  height=300
551
  )
552
 
553
+ # Video preview
554
  video_preview = gr.Video(
555
  label="πŸ“Ί Preview of Uploaded Video",
556
  height=200,
 
561
  inputs=video_input,
562
  outputs=video_preview
563
  )
 
564
 
565
  gr.Markdown("### 🎨 Step 2: Choose Background Method")
566
  gr.Markdown("*Select your preferred background creation method*")
567
+
568
+ # FIXED Radio (flat choices)
569
  background_method = gr.Radio(
570
+ choices=["upload", "professional", "colors", "ai"],
 
 
 
 
 
571
  value="professional",
572
  label="Background Method"
573
  )
574
+ # Labels hint
575
+ gr.Markdown(
576
+ "- **upload** = πŸ“· Upload Image \n"
577
+ "- **professional** = 🎨 Professional Presets \n"
578
+ "- **colors** = 🌈 Colors/Gradients \n"
579
+ "- **ai** = πŸ€– AI Generated"
580
+ )
581
+
582
  # Method A: Upload Image
583
  with gr.Group(visible=False) as upload_group:
584
  gr.Markdown("**πŸ“· Upload Your Background Image**")
 
586
  label="Drop your background image here",
587
  type="filepath"
588
  )
589
+
590
  # Method B: Professional Presets
591
  with gr.Group(visible=True) as professional_group:
592
  gr.Markdown("**🎨 Professional Background Presets**")
 
595
  value="office_modern",
596
  label="Select Professional Background"
597
  )
598
+
599
  # Method C: Colors/Gradients
600
  with gr.Group(visible=False) as colors_group:
601
  gr.Markdown("**🌈 Custom Colors & Gradients**")
602
+
603
  gradient_type = gr.Dropdown(
604
  choices=["solid", "vertical", "horizontal", "diagonal", "radial", "soft_radial"],
605
  value="vertical",
606
  label="Gradient Type"
607
  )
608
+
609
  with gr.Row():
610
  color1 = gr.ColorPicker(label="🎨 Color 1", value="#3498db")
611
  color2 = gr.ColorPicker(label="🎨 Color 2", value="#2ecc71")
612
+
613
  with gr.Row():
614
  color3 = gr.ColorPicker(label="🎨 Color 3", value="#e74c3c")
615
  use_third_color = gr.Checkbox(label="Use 3rd color", value=False)
616
+
617
  # Method D: AI Generated
618
  with gr.Group(visible=False) as ai_group:
619
  gr.Markdown("**πŸ€– AI Generated Background**")
620
+
621
  ai_prompt = gr.Textbox(
622
  label="Describe your background",
623
  placeholder="e.g., 'modern office with plants', 'sunset over mountains', 'abstract tech pattern'",
624
  lines=2
625
  )
626
+
627
  ai_style = gr.Dropdown(
628
  choices=["photorealistic", "artistic", "abstract", "minimalist", "corporate", "nature"],
629
  value="photorealistic",
630
  label="Style"
631
  )
632
+
633
  with gr.Row():
634
  generate_ai_btn = gr.Button("🎨 Generate Background", variant="secondary")
635
  ai_generated_image = gr.Image(label="Generated Background", type="filepath", visible=False)
636
+
637
  # Background method switching function
638
  def switch_background_method(method):
639
  return (
640
  gr.update(visible=(method == "upload")), # upload_group
641
+ gr.update(visible=(method == "professional")), # professional_group
642
  gr.update(visible=(method == "colors")), # colors_group
643
  gr.update(visible=(method == "ai")) # ai_group
644
  )
645
+
646
  background_method.change(
647
  fn=switch_background_method,
648
  inputs=background_method,
649
  outputs=[upload_group, professional_group, colors_group, ai_group]
650
  )
651
+
652
  gr.Markdown("### 🎬 Processing Controls")
653
  gr.Markdown("*First load the AI models, then process your video*")
654
+
655
  with gr.Row():
656
  load_models_btn = gr.Button(
657
+ "πŸš€ Step 1: Load AI Models",
658
+ variant="secondary",
 
659
  )
660
  process_btn = gr.Button(
661
+ "✨ Step 2: Process Video",
662
+ variant="primary",
 
663
  )
664
+
665
  # System status
666
  status_text = gr.Textbox(
667
+ label="πŸ”§ System Status",
668
+ value=get_model_status(),
669
  interactive=False,
670
  lines=3
671
  )
672
+
673
  # Right column - Results and preview
674
  with gr.Column(scale=1):
675
  gr.Markdown("### πŸ“€ Your Results")
676
  gr.Markdown("*Processed video will appear here after Step 2*")
677
+
678
  video_output = gr.Video(
679
+ label="🎬 Your Processed Video",
680
  height=400
681
  )
682
+
683
  result_text = gr.Textbox(
684
+ label="πŸ“Š Processing Results",
685
  interactive=False,
686
  lines=6,
687
  placeholder="Processing status and results will appear here..."
688
  )
689
+
690
  gr.Markdown("### 🎨 Professional Backgrounds Available")
691
+
692
  # Create background preview grid
693
  bg_preview_html = """
694
  <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 10px; max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 8px;'>
695
  """
 
696
  for key, config in PROFESSIONAL_BACKGROUNDS.items():
697
  colors = config["colors"]
698
  if len(colors) >= 2:
699
  gradient = f"linear-gradient(45deg, {colors[0]}, {colors[-1]})"
700
  else:
701
  gradient = colors[0]
 
702
  bg_preview_html += f"""
703
  <div style='
704
  padding: 12px 8px;
 
717
  </div>
718
  </div>
719
  """
 
720
  bg_preview_html += "</div>"
721
  gr.HTML(bg_preview_html)
722
+
723
  # AI Background Generation Function
724
  def generate_ai_background(prompt, style):
725
  """Generate AI background using procedural methods"""
726
  if not prompt or not prompt.strip():
727
  return None, "❌ Please enter a prompt"
728
+
729
  try:
730
  # Create procedural background based on prompt
731
  bg_image = create_procedural_background(prompt, style, 1920, 1080)
732
+
733
  if bg_image is not None:
 
 
734
  with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
735
  cv2.imwrite(tmp.name, bg_image)
736
  return tmp.name, f"βœ… Background generated: {prompt[:50]}..."
 
739
  except Exception as e:
740
  logger.error(f"AI generation error: {e}")
741
  return None, f"❌ Generation error: {str(e)}"
742
+
743
  # Enhanced video processing function that handles all 4 methods
744
+ def process_video_enhanced(
745
+ video_path,
746
+ bg_method,
747
+ custom_img,
748
+ prof_choice,
749
+ grad_type,
750
+ color1, color2, color3, use_third,
751
+ ai_prompt, ai_style, ai_img,
752
+ progress: Optional[gr.Progress] = None
753
+ ):
754
  """Process video with any of the 4 background methods using TWO-STAGE approach"""
755
+
756
  if not models_loaded:
757
  return None, "❌ Models not loaded. Click 'Load Models' first."
758
+
759
  if not video_path:
760
  return None, "❌ No video file provided."
761
+
762
  try:
 
 
 
763
  if bg_method == "upload":
764
  if custom_img and os.path.exists(custom_img):
765
  return process_video_hq(video_path, "custom", custom_img, progress)
766
  else:
767
  return None, "❌ No image uploaded. Please upload a background image."
768
+
769
  elif bg_method == "professional":
770
  if prof_choice and prof_choice in PROFESSIONAL_BACKGROUNDS:
771
  return process_video_hq(video_path, prof_choice, None, progress)
772
  else:
773
  return None, f"❌ Invalid professional background: {prof_choice}"
774
+
775
  elif bg_method == "colors":
 
776
  try:
777
  colors = [color1 or "#3498db", color2 or "#2ecc71"]
778
  if use_third and color3:
779
  colors.append(color3)
780
+
781
  bg_config = {
782
  "type": "gradient" if grad_type != "solid" else "color",
783
+ "colors": colors if grad_type != "solid" else [colors[0]],
784
  "direction": grad_type if grad_type != "solid" else "vertical"
785
  }
786
+
 
 
 
 
787
  gradient_bg = create_professional_background(bg_config, 1920, 1080)
788
  temp_path = f"/tmp/gradient_{int(time.time())}.png"
789
  cv2.imwrite(temp_path, gradient_bg)
790
+
791
  return process_video_hq(video_path, "custom", temp_path, progress)
792
  except Exception as e:
793
  return None, f"❌ Error creating gradient: {str(e)}"
794
+
795
  elif bg_method == "ai":
796
  if ai_img and os.path.exists(ai_img):
797
  return process_video_hq(video_path, "custom", ai_img, progress)
798
  else:
799
  return None, "❌ No AI background generated. Click 'Generate Background' first."
800
+
801
  else:
802
  return None, f"❌ Unknown background method: {bg_method}"
803
+
804
  except Exception as e:
805
  logger.error(f"Enhanced processing error: {e}")
806
  return None, f"❌ Processing error: {str(e)}"
807
+
808
+ # Wire up callbacks
809
  load_models_btn.click(
810
  fn=download_and_setup_models,
811
  outputs=status_text
812
  )
813
+
814
  generate_ai_btn.click(
815
  fn=generate_ai_background,
816
  inputs=[ai_prompt, ai_style],
817
  outputs=[ai_generated_image, status_text]
818
  )
819
+
820
  process_btn.click(
821
  fn=process_video_enhanced,
822
  inputs=[
823
  video_input, # video_path
824
+ background_method, # bg_method
825
  custom_background, # custom_img
826
  professional_choice, # prof_choice
827
  gradient_type, # grad_type
 
830
  ],
831
  outputs=[video_output, result_text]
832
  )
833
+
834
+ # Info
835
  with gr.Accordion("ℹ️ ENHANCED Quality & Features", open=False):
836
  gr.Markdown("""
837
  ### πŸ† TWO-STAGE Cinema-Quality Features:
838
+ **Stage 1**: Original β†’ Green Screen (SAM2 + MatAnyOne)
839
+ **Stage 2**: Green Screen β†’ Final Background (professional chroma key)
840
+
841
+ **Background Methods**: Upload image / Professional presets / Gradients / AI generated
842
+
843
+ **Quality**: Edge feathering, gamma correction, mask cleanup, H.264 CRF 18, AAC 192kbps.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  """)
845
+
 
846
  gr.Markdown("---")
847
+ gr.Markdown("*🎬 Cinema-Quality Video Background Replacement β€” TWO-STAGE pipeline*")
848
+
 
 
 
849
  return demo
850
 
851
+ # ============================================================================ #
852
+ # MAIN
853
+ # ============================================================================ #
854
  def main():
855
  """Main application entry point"""
856
  try:
857
  print("🎬 Cinema-Quality Video Background Replacement")
858
  print("=" * 50)
859
+
860
+ # Initialize application paths
861
  os.makedirs("/tmp/MyAvatar/My_Videos/", exist_ok=True)
862
  os.makedirs(os.path.expanduser("~/.cache/sam2"), exist_ok=True)
863
+
864
  print("πŸš€ Features:")
865
+ print(" β€’ SAM2 + MatAnyOne AI models")
866
  print(" β€’ TWO-STAGE processing (Original β†’ Green Screen β†’ Final)")
867
  print(" β€’ 4 background methods (Upload/Professional/Colors/AI)")
868
  print(" β€’ Multi-fallback loading system")
869
  print(" β€’ Cinema-quality processing")
870
  print(" β€’ Enhanced stability & error handling")
871
  print("=" * 50)
872
+
873
  # Create and launch interface
874
  logger.info("🌐 Creating Gradio interface...")
875
  demo = create_interface()
876
+
877
  logger.info("πŸš€ Launching application...")
 
878
  demo.launch(
879
  server_name="0.0.0.0",
880
  server_port=7860,
881
  share=True,
882
  show_error=True
883
  )
884
+
885
  except KeyboardInterrupt:
886
  logger.info("πŸ›‘ Application stopped by user")
887
  print("\nπŸ›‘ Application stopped by user")