Nekochu commited on
Commit
bd9731f
·
verified ·
1 Parent(s): 92540b1

off cache

Browse files
Files changed (1) hide show
  1. app.py +724 -733
app.py CHANGED
@@ -1,733 +1,724 @@
1
- """
2
- Background Removal - Image and Video
3
- - BRIA RMBG 1.4: Fast general-purpose background removal
4
- - BRIA RMBG 2.0: High-quality BiRefNet-based (slower, better quality)
5
- - IS-Net/DIS: Ultra-fine edge detection (hair, fur, transparency)
6
- - Robust Video Matting: Video background removal with ONNX Runtime (2x faster!)
7
-
8
- CPU-only HuggingFace Space
9
- """
10
-
11
- import os
12
- import sys
13
- import tempfile
14
- import argparse
15
- from typing import Tuple, Optional
16
-
17
- # Set OMP threads to match cpu-basic (2 vCPU)
18
- os.environ["OMP_NUM_THREADS"] = "2"
19
- os.environ["OMP_WAIT_POLICY"] = "ACTIVE"
20
-
21
- import gradio as gr
22
- import numpy as np
23
- from PIL import Image
24
- import torch
25
- import torch.nn.functional as F
26
- from torchvision.transforms.functional import normalize
27
- from huggingface_hub import hf_hub_download
28
- import cv2
29
- import onnxruntime as ort
30
-
31
- # Force CPU for PyTorch (image model)
32
- device = torch.device("cpu")
33
-
34
- # =============================================================================
35
- # Image Background Removal (BRIA RMBG 1.4)
36
- # =============================================================================
37
-
38
- class BriaRMBGModel:
39
- """Wrapper for BRIA-RMBG 1.4 model"""
40
-
41
- def __init__(self):
42
- self.model = None
43
- self.input_size = [1024, 1024]
44
-
45
- def load_model(self):
46
- """Load the BRIA-RMBG 1.4 model from HuggingFace"""
47
- if self.model is not None:
48
- return
49
-
50
- print("Loading BRIA-RMBG 1.4 model...")
51
-
52
- # Download all required files from BRIA repo
53
- repo_id = "briaai/RMBG-1.4"
54
- model_path = hf_hub_download(repo_id=repo_id, filename="model.pth")
55
- briarmbg_path = hf_hub_download(repo_id=repo_id, filename="briarmbg.py")
56
- myconfig_path = hf_hub_download(repo_id=repo_id, filename="MyConfig.py")
57
-
58
- # Setup package directory
59
- briarmbg_dir = os.path.dirname(briarmbg_path)
60
-
61
- # Create __init__.py to make it a proper package
62
- init_path = os.path.join(briarmbg_dir, "__init__.py")
63
- if not os.path.exists(init_path):
64
- with open(init_path, "w") as f:
65
- f.write("# Auto-generated for package import\n")
66
-
67
- # Add parent directory to path so we can import as package
68
- parent_dir = os.path.dirname(briarmbg_dir)
69
- package_name = os.path.basename(briarmbg_dir)
70
-
71
- if parent_dir not in sys.path:
72
- sys.path.insert(0, parent_dir)
73
-
74
- # Import using the package structure
75
- import importlib
76
- briarmbg_module = importlib.import_module(f"{package_name}.briarmbg")
77
- BriaRMBG = briarmbg_module.BriaRMBG
78
-
79
- self.model = BriaRMBG()
80
- self.model.load_state_dict(torch.load(model_path, map_location=device))
81
- self.model.to(device)
82
- self.model.eval()
83
- print("BRIA-RMBG model loaded!")
84
-
85
- def preprocess(self, image: Image.Image):
86
- """Preprocess image for model input"""
87
- if image.mode != "RGB":
88
- image = image.convert("RGB")
89
-
90
- orig_size = image.size
91
- im = np.array(image)
92
-
93
- if len(im.shape) < 3:
94
- im = im[:, :, np.newaxis]
95
- if im.shape[2] == 1:
96
- im = np.repeat(im, 3, axis=2)
97
-
98
- im_tensor = torch.tensor(im, dtype=torch.float32).permute(2, 0, 1)
99
- im_tensor = F.interpolate(
100
- im_tensor.unsqueeze(0),
101
- self.input_size,
102
- mode="bilinear",
103
- align_corners=False
104
- )
105
-
106
- image_tensor = im_tensor / 255.0
107
- image_tensor = normalize(image_tensor, [0.5, 0.5, 0.5], [1.0, 1.0, 1.0])
108
-
109
- return image_tensor, orig_size
110
-
111
- def postprocess(self, result, orig_size):
112
- """Postprocess model output to mask"""
113
- result = F.interpolate(
114
- result,
115
- size=(orig_size[1], orig_size[0]),
116
- mode="bilinear",
117
- align_corners=False
118
- )
119
-
120
- ma = torch.max(result)
121
- mi = torch.min(result)
122
- result = (result - mi) / (ma - mi + 1e-8)
123
-
124
- mask = (result.squeeze().cpu().numpy() * 255).astype(np.uint8)
125
- return mask
126
-
127
- @torch.no_grad()
128
- def remove_background(self, image: Image.Image):
129
- """Remove background from image"""
130
- self.load_model()
131
-
132
- input_tensor, orig_size = self.preprocess(image)
133
- input_tensor = input_tensor.to(device)
134
-
135
- result = self.model(input_tensor)
136
-
137
- if isinstance(result, (list, tuple)):
138
- result = result[0]
139
- if isinstance(result, (list, tuple)):
140
- result = result[0]
141
-
142
- mask = self.postprocess(result, orig_size)
143
-
144
- pil_mask = Image.fromarray(mask).convert("L")
145
-
146
- if image.mode != "RGB":
147
- image = image.convert("RGB")
148
-
149
- output = image.copy()
150
- output.putalpha(pil_mask)
151
-
152
- return output, pil_mask
153
-
154
-
155
- # =============================================================================
156
- # Image Background Removal (IS-Net/DIS - Ultra-fine edges) - ONNX Runtime
157
- # =============================================================================
158
-
159
- class ISNetModel:
160
- """Wrapper for IS-Net/DIS ONNX model - better for hair, fur, transparency"""
161
-
162
- def __init__(self):
163
- self.session = None
164
- self.input_size = [1024, 1024]
165
-
166
- def load_model(self):
167
- """Load IS-Net ONNX model"""
168
- if self.session is not None:
169
- return
170
-
171
- print("Loading IS-Net/DIS ONNX model...")
172
- model_path = os.path.join(os.path.dirname(__file__), "isnet_dis_fp32.onnx")
173
-
174
- # Setup ONNX Runtime with optimizations
175
- sess_options = ort.SessionOptions()
176
- sess_options.intra_op_num_threads = 2
177
- sess_options.inter_op_num_threads = 1
178
- sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
179
-
180
- self.session = ort.InferenceSession(
181
- model_path,
182
- sess_options,
183
- providers=['CPUExecutionProvider']
184
- )
185
- print("IS-Net/DIS ONNX model loaded!")
186
-
187
- def preprocess(self, image: Image.Image):
188
- """Preprocess image for model input"""
189
- if image.mode != "RGB":
190
- image = image.convert("RGB")
191
-
192
- orig_size = image.size
193
- # Resize to 1024x1024
194
- im_resized = image.resize((1024, 1024), Image.BILINEAR)
195
- im = np.array(im_resized).astype(np.float32)
196
-
197
- # Normalize: (x/255 - 0.5) / 1.0 = x/255 - 0.5
198
- im = im / 255.0
199
- im = (im - 0.5) / 1.0
200
-
201
- # HWC -> NCHW
202
- im = np.transpose(im, (2, 0, 1))[np.newaxis, ...]
203
-
204
- return im.astype(np.float32), orig_size
205
-
206
- def postprocess(self, result, orig_size):
207
- """Postprocess model output to mask"""
208
- # Result is already sigmoid (0-1), resize to original
209
- mask_1024 = result[0, 0] # 1024x1024
210
-
211
- # Resize to original size
212
- mask_pil = Image.fromarray((mask_1024 * 255).astype(np.uint8))
213
- mask_pil = mask_pil.resize(orig_size, Image.BILINEAR)
214
-
215
- return np.array(mask_pil)
216
-
217
- def remove_background(self, image: Image.Image):
218
- """Remove background from image using IS-Net ONNX"""
219
- self.load_model()
220
-
221
- input_tensor, orig_size = self.preprocess(image)
222
-
223
- # Run ONNX inference - d0 is the main output
224
- outputs = self.session.run(None, {'input': input_tensor})
225
- result = outputs[0] # d0: main segmentation mask
226
-
227
- mask = self.postprocess(result, orig_size)
228
-
229
- pil_mask = Image.fromarray(mask).convert("L")
230
-
231
- if image.mode != "RGB":
232
- image = image.convert("RGB")
233
-
234
- output = image.copy()
235
- output.putalpha(pil_mask)
236
-
237
- return output, pil_mask
238
-
239
-
240
- # =============================================================================
241
- # Image Background Removal (BRIA RMBG 2.0 - BiRefNet based, high quality)
242
- # =============================================================================
243
-
244
- class RMBG2Model:
245
- """Wrapper for BRIA-RMBG 2.0 model (BiRefNet architecture) - higher quality, slower"""
246
-
247
- def __init__(self):
248
- self.model = None
249
- self.input_size = 1024 # Native resolution for best quality
250
-
251
- def load_model(self):
252
- """Load RMBG-2.0 model from HuggingFace"""
253
- if self.model is not None:
254
- return
255
-
256
- print("Loading BRIA-RMBG 2.0 model (BiRefNet)...")
257
-
258
- # Download model files from ungated repo
259
- repo_id = "1038lab/RMBG-2.0"
260
- model_path = hf_hub_download(repo_id=repo_id, filename="model.safetensors")
261
- birefnet_path = hf_hub_download(repo_id=repo_id, filename="birefnet.py")
262
- config_path = hf_hub_download(repo_id=repo_id, filename="BiRefNet_config.py")
263
-
264
- # Setup import path
265
- model_dir = os.path.dirname(birefnet_path)
266
- if model_dir not in sys.path:
267
- sys.path.insert(0, model_dir)
268
-
269
- # Import model classes
270
- from safetensors.torch import load_file
271
- import importlib.util
272
-
273
- # Load BiRefNet_config
274
- spec = importlib.util.spec_from_file_location("BiRefNet_config", config_path)
275
- config_module = importlib.util.module_from_spec(spec)
276
- sys.modules["BiRefNet_config"] = config_module
277
- spec.loader.exec_module(config_module)
278
-
279
- # Fix relative import in birefnet.py - read, fix, and exec
280
- with open(birefnet_path, 'r', encoding='utf-8') as f:
281
- birefnet_code = f.read()
282
-
283
- # Replace relative import with absolute
284
- birefnet_code = birefnet_code.replace(
285
- 'from .BiRefNet_config import BiRefNetConfig',
286
- 'from BiRefNet_config import BiRefNetConfig'
287
- )
288
-
289
- # Execute the fixed code
290
- birefnet_module = type(sys)("birefnet")
291
- exec(compile(birefnet_code, birefnet_path, 'exec'), birefnet_module.__dict__)
292
-
293
- BiRefNet = birefnet_module.BiRefNet
294
- BiRefNetConfig = config_module.BiRefNetConfig
295
-
296
- # Create and load model
297
- config = BiRefNetConfig(bb_pretrained=False)
298
- self.model = BiRefNet(config)
299
-
300
- state_dict = load_file(model_path)
301
- self.model.load_state_dict(state_dict)
302
- self.model.to(device)
303
- self.model.eval()
304
-
305
- print(f"BRIA-RMBG 2.0 loaded! ({sum(p.numel() for p in self.model.parameters())/1e6:.0f}M params)")
306
-
307
- def preprocess(self, image: Image.Image):
308
- """Preprocess image for model input"""
309
- if image.mode != "RGB":
310
- image = image.convert("RGB")
311
-
312
- orig_size = image.size
313
-
314
- # Resize to model input size
315
- im_resized = image.resize((self.input_size, self.input_size), Image.BILINEAR)
316
- im = np.array(im_resized).astype(np.float32)
317
-
318
- # Normalize
319
- im = im / 255.0
320
- im = (im - 0.5) / 1.0
321
-
322
- # HWC -> NCHW
323
- im_tensor = torch.from_numpy(im).permute(2, 0, 1).unsqueeze(0)
324
-
325
- return im_tensor.float(), orig_size
326
-
327
- def postprocess(self, result, orig_size):
328
- """Postprocess model output to mask"""
329
- # Get final output (BiRefNet returns list for deep supervision)
330
- if isinstance(result, (list, tuple)):
331
- result = result[-1] # Last output is the refined mask
332
-
333
- # Sigmoid and resize
334
- mask = torch.sigmoid(result).squeeze().cpu().numpy()
335
-
336
- # Resize to original size
337
- mask_pil = Image.fromarray((mask * 255).astype(np.uint8))
338
- mask_pil = mask_pil.resize(orig_size, Image.BILINEAR)
339
-
340
- return np.array(mask_pil)
341
-
342
- @torch.no_grad()
343
- def remove_background(self, image: Image.Image):
344
- """Remove background from image using RMBG-2.0"""
345
- self.load_model()
346
-
347
- input_tensor, orig_size = self.preprocess(image)
348
- input_tensor = input_tensor.to(device)
349
-
350
- result = self.model(input_tensor)
351
- mask = self.postprocess(result, orig_size)
352
-
353
- pil_mask = Image.fromarray(mask).convert("L")
354
-
355
- if image.mode != "RGB":
356
- image = image.convert("RGB")
357
-
358
- output = image.copy()
359
- output.putalpha(pil_mask)
360
-
361
- return output, pil_mask
362
-
363
-
364
- # =============================================================================
365
- # Video Background Removal (Robust Video Matting - ONNX Runtime)
366
- # =============================================================================
367
-
368
- rvm_session = None
369
-
370
-
371
- def load_rvm_onnx():
372
- """Load RobustVideoMatting ONNX model for fast CPU inference."""
373
- global rvm_session
374
- if rvm_session is not None:
375
- return rvm_session
376
-
377
- print("Loading Robust Video Matting ONNX model...")
378
-
379
- # Load ONNX model from local Space directory
380
- model_path = os.path.join(os.path.dirname(__file__), "rvm_mobilenetv3_fp32.onnx")
381
-
382
- # Setup ONNX Runtime with optimizations (2 vCPU on cpu-basic)
383
- sess_options = ort.SessionOptions()
384
- sess_options.intra_op_num_threads = 2
385
- sess_options.inter_op_num_threads = 1
386
- sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
387
-
388
- rvm_session = ort.InferenceSession(
389
- model_path,
390
- sess_options,
391
- providers=['CPUExecutionProvider']
392
- )
393
-
394
- print("RVM ONNX model loaded! (2x faster than PyTorch)")
395
- return rvm_session
396
-
397
-
398
- # Output resolution options
399
- RESOLUTION_OPTIONS = {
400
- "Original": None,
401
- "1080p": 1080,
402
- "720p": 720,
403
- "480p": 480,
404
- }
405
-
406
-
407
- def process_video(input_video, output_type="green_screen", max_resolution="720p", downsample_ratio=0.25, progress=None):
408
- """
409
- Process video to remove background using ONNX Runtime.
410
-
411
- Args:
412
- input_video: Path to input video
413
- output_type: "green_screen" or "foreground_only"
414
- max_resolution: Output video resolution (affects speed)
415
- downsample_ratio: Internal model quality (affects edge accuracy)
416
- progress: Gradio Progress tracker (optional, for web UI)
417
-
418
- Returns:
419
- Path to processed video
420
- """
421
- if input_video is None:
422
- return None
423
-
424
- # Get max height from resolution option
425
- max_h = RESOLUTION_OPTIONS.get(max_resolution)
426
-
427
- session = load_rvm_onnx()
428
- output_names = [o.name for o in session.get_outputs()]
429
-
430
- cap = cv2.VideoCapture(input_video)
431
- fps = cap.get(cv2.CAP_PROP_FPS)
432
- orig_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
433
- orig_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
434
- frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
435
-
436
- # Resize based on quality preset
437
- if max_h and orig_height > max_h:
438
- scale = max_h / orig_height
439
- width = int(orig_width * scale)
440
- height = max_h
441
- print(f"Resizing: {orig_width}x{orig_height} -> {width}x{height}")
442
- else:
443
- width, height = orig_width, orig_height
444
-
445
- with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
446
- output_path = tmp.name
447
-
448
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
449
- out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
450
-
451
- green_bg = np.zeros((height, width, 3), dtype=np.uint8)
452
- green_bg[:] = (0, 255, 0)
453
-
454
- # Initialize recurrent states (zeros for first frame)
455
- r1 = np.zeros((1, 1, 1, 1), dtype=np.float32)
456
- r2 = np.zeros((1, 1, 1, 1), dtype=np.float32)
457
- r3 = np.zeros((1, 1, 1, 1), dtype=np.float32)
458
- r4 = np.zeros((1, 1, 1, 1), dtype=np.float32)
459
-
460
- frame_idx = 0
461
- while True:
462
- ret, frame = cap.read()
463
- if not ret:
464
- break
465
-
466
- # Resize if needed
467
- if frame.shape[0] != height or frame.shape[1] != width:
468
- frame = cv2.resize(frame, (width, height))
469
-
470
- # Preprocess frame
471
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
472
- src = frame_rgb.astype(np.float32) / 255.0
473
- src = np.transpose(src, (2, 0, 1))[np.newaxis, ...] # NCHW
474
-
475
- # ONNX inference
476
- outputs = session.run(output_names, {
477
- 'src': src,
478
- 'r1i': r1,
479
- 'r2i': r2,
480
- 'r3i': r3,
481
- 'r4i': r4,
482
- 'downsample_ratio': np.array([downsample_ratio], dtype=np.float32)
483
- })
484
-
485
- fgr, pha, r1, r2, r3, r4 = outputs
486
-
487
- # Postprocess
488
- fgr_np = np.transpose(fgr[0], (1, 2, 0)) # CHW -> HWC
489
- pha_np = pha[0, 0]
490
-
491
- if fgr_np.shape[:2] != (height, width):
492
- fgr_np = cv2.resize(fgr_np, (width, height))
493
- pha_np = cv2.resize(pha_np, (width, height))
494
-
495
- pha_3ch = np.stack([pha_np] * 3, axis=-1)
496
-
497
- if output_type == "green_screen":
498
- result = (fgr_np * pha_3ch + (green_bg / 255.0) * (1 - pha_3ch))
499
- else:
500
- result = fgr_np * pha_3ch
501
-
502
- result_bgr = cv2.cvtColor((result * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)
503
- out.write(result_bgr)
504
-
505
- frame_idx += 1
506
- if frame_count > 0:
507
- if progress is not None:
508
- progress(frame_idx / frame_count, desc=f"Processing frame {frame_idx}/{frame_count}")
509
- elif frame_idx % 30 == 0:
510
- print(f"Processing frame {frame_idx}/{frame_count}")
511
-
512
- cap.release()
513
- out.release()
514
-
515
- return output_path
516
-
517
-
518
- # =============================================================================
519
- # Gradio Interface
520
- # =============================================================================
521
-
522
- # Initialize image models (lazy loading)
523
- bria_model = BriaRMBGModel()
524
- rmbg2_model = RMBG2Model()
525
- isnet_model = ISNetModel()
526
-
527
- IMAGE_MODELS = {
528
- "BRIA-RMBG 1.4 (fast, general)": bria_model,
529
- "BRIA-RMBG 2.0 (high quality, slow)": rmbg2_model,
530
- "IS-Net/DIS (fine edges, hair)": isnet_model,
531
- }
532
-
533
-
534
- IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.gif', '.tiff'}
535
- VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv'}
536
-
537
-
538
- def detect_file_type(file_path):
539
- """Detect if file is image or video based on extension."""
540
- if file_path is None:
541
- return None
542
- ext = os.path.splitext(file_path)[1].lower()
543
- if ext in IMAGE_EXTENSIONS:
544
- return "image"
545
- elif ext in VIDEO_EXTENSIONS:
546
- return "video"
547
- return None
548
-
549
-
550
- def on_file_upload(file_path):
551
- """Handle file upload - detect type and update preview."""
552
- if file_path is None:
553
- return gr.Image(visible=False), gr.Video(visible=False), None
554
-
555
- file_type = detect_file_type(file_path)
556
- if file_type == "image":
557
- return gr.Image(value=file_path, visible=True), gr.Video(visible=False), "image"
558
- elif file_type == "video":
559
- return gr.Image(visible=False), gr.Video(value=file_path, visible=True), "video"
560
- return gr.Image(visible=False), gr.Video(visible=False), None
561
-
562
-
563
- def process_media(file_path, file_type, image_model_choice, output_type, max_resolution, edge_quality, progress=gr.Progress()):
564
- """Process based on stored file type."""
565
- if file_path is None:
566
- return None, None
567
-
568
- # Auto-detect file type if not provided (for API calls)
569
- if file_type is None:
570
- file_type = detect_file_type(file_path)
571
-
572
- if file_type is None:
573
- return None, None
574
-
575
- if file_type == "image":
576
- pil_image = Image.open(file_path)
577
- model = IMAGE_MODELS.get(image_model_choice, bria_model)
578
- output_img, mask = model.remove_background(pil_image)
579
- return output_img, None
580
-
581
- elif file_type == "video":
582
- output_path = process_video(file_path, output_type, max_resolution, edge_quality, progress)
583
- return None, output_path
584
-
585
- return None, None
586
-
587
-
588
- def build_ui():
589
- """Build compact unified Gradio interface."""
590
- with gr.Blocks(title="Background Removal") as demo:
591
- gr.Markdown("## Background Removal\nUpload image or video. Image models: RMBG 1.4 (fast) | RMBG 2.0 (quality) | IS-Net (fine edges) | Video: RVM")
592
-
593
- # State to track file type
594
- file_type_state = gr.State(value=None)
595
-
596
- with gr.Row():
597
- with gr.Column(scale=1):
598
- input_file = gr.File(
599
- label="Upload Image or Video",
600
- file_types=["image", "video"],
601
- type="filepath"
602
- )
603
- # Preview - only one visible at a time
604
- preview_image = gr.Image(label="Preview", interactive=False, visible=False)
605
- preview_video = gr.Video(label="Preview", interactive=False, visible=False)
606
-
607
- # Image model selector
608
- image_model_dropdown = gr.Dropdown(
609
- choices=list(IMAGE_MODELS.keys()),
610
- value="BRIA-RMBG 1.4 (fast, general)",
611
- label="Image Model",
612
- info="1.4: fast | 2.0: quality (slow) | IS-Net: fine edges"
613
- )
614
-
615
- with gr.Accordion("Video Settings", open=False):
616
- output_type = gr.Radio(
617
- choices=["green_screen", "foreground_only"],
618
- value="green_screen",
619
- label="Output Type"
620
- )
621
- max_res = gr.Dropdown(
622
- choices=list(RESOLUTION_OPTIONS.keys()),
623
- value="720p",
624
- label="Output Resolution"
625
- )
626
- edge_quality = gr.Slider(
627
- minimum=0.1,
628
- maximum=1.0,
629
- value=0.25,
630
- step=0.05,
631
- label="Edge Quality"
632
- )
633
-
634
- process_btn = gr.Button("Remove Background", variant="primary")
635
-
636
- with gr.Column(scale=1):
637
- # Output components
638
- output_image = gr.Image(label="Result (Image)", type="pil")
639
- output_video = gr.Video(label="Result (Video)")
640
-
641
- # Wrapper for examples (auto-detects file type)
642
- def process_example(file_path, image_model_choice, output_type, max_resolution, edge_quality):
643
- file_type = detect_file_type(file_path)
644
- return process_media(file_path, file_type, image_model_choice, output_type, max_resolution, edge_quality)
645
-
646
- # Example files with cached results
647
- gr.Examples(
648
- examples=[
649
- ["examples/cat.jpg", "BRIA-RMBG 1.4 (fast, general)", "green_screen", "720p", 0.25],
650
- ],
651
- inputs=[input_file, image_model_dropdown, output_type, max_res, edge_quality],
652
- outputs=[output_image, output_video],
653
- fn=process_example,
654
- cache_examples=True,
655
- cache_mode="lazy",
656
- label="Examples"
657
- )
658
-
659
- # On upload: detect type and show preview
660
- input_file.change(
661
- fn=on_file_upload,
662
- inputs=[input_file],
663
- outputs=[preview_image, preview_video, file_type_state]
664
- )
665
-
666
- # On process: fill the already-visible output
667
- process_btn.click(
668
- fn=process_media,
669
- inputs=[input_file, file_type_state, image_model_dropdown, output_type, max_res, edge_quality],
670
- outputs=[output_image, output_video]
671
- )
672
-
673
- return demo
674
-
675
-
676
- # =============================================================================
677
- # CLI Interface
678
- # =============================================================================
679
-
680
- def cli_main():
681
- """CLI entry point."""
682
- parser = argparse.ArgumentParser(description="Background Removal - Image and Video")
683
- subparsers = parser.add_subparsers(dest="command", help="Commands")
684
-
685
- # Image command
686
- img_parser = subparsers.add_parser("image", help="Remove background from image")
687
- img_parser.add_argument("input", type=str, help="Input image path")
688
- img_parser.add_argument("--output", "-o", type=str, default="output.png", help="Output path")
689
- img_parser.add_argument("--mask", "-m", type=str, help="Save mask to path")
690
- img_parser.add_argument("--model", type=str, default="BRIA-RMBG 1.4 (fast, general)",
691
- choices=list(IMAGE_MODELS.keys()),
692
- help="Image model: BRIA (fast) or IS-Net (fine edges)")
693
-
694
- # Video command
695
- vid_parser = subparsers.add_parser("video", help="Remove background from video")
696
- vid_parser.add_argument("input", type=str, help="Input video path")
697
- vid_parser.add_argument("--output", "-o", type=str, default="output.mp4", help="Output path")
698
- vid_parser.add_argument("--type", type=str, default="green_screen",
699
- choices=["green_screen", "foreground_only"])
700
- vid_parser.add_argument("--downsample", type=float, default=0.25, help="Downsample ratio")
701
-
702
- args = parser.parse_args()
703
-
704
- if args.command == "image":
705
- print(f"Processing image: {args.input}")
706
- input_img = Image.open(args.input)
707
- # Use selected model (default: BRIA)
708
- model = IMAGE_MODELS.get(args.model, bria_model)
709
- output_img, mask = model.remove_background(input_img)
710
- output_img.save(args.output)
711
- print(f"Saved: {args.output}")
712
- if args.mask:
713
- mask.save(args.mask)
714
- print(f"Saved mask: {args.mask}")
715
-
716
- elif args.command == "video":
717
- print(f"Processing video: {args.input}")
718
- output_path = process_video(args.input, args.type, args.downsample)
719
- if output_path:
720
- import shutil
721
- shutil.move(output_path, args.output)
722
- print(f"Saved: {args.output}")
723
-
724
- else:
725
- parser.print_help()
726
-
727
-
728
- if __name__ == "__main__":
729
- if len(sys.argv) > 1 and sys.argv[1] in ["image", "video", "-h", "--help"]:
730
- cli_main()
731
- else:
732
- demo = build_ui()
733
- demo.launch(theme=gr.themes.Soft())
 
1
+ """
2
+ Background Removal - Image and Video
3
+ - BRIA RMBG 1.4: Fast general-purpose background removal
4
+ - BRIA RMBG 2.0: High-quality BiRefNet-based (slower, better quality)
5
+ - IS-Net/DIS: Ultra-fine edge detection (hair, fur, transparency)
6
+ - Robust Video Matting: Video background removal with ONNX Runtime (2x faster!)
7
+
8
+ CPU-only HuggingFace Space
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import tempfile
14
+ import argparse
15
+ from typing import Tuple, Optional
16
+
17
+ # Set OMP threads to match cpu-basic (2 vCPU)
18
+ os.environ["OMP_NUM_THREADS"] = "2"
19
+ os.environ["OMP_WAIT_POLICY"] = "ACTIVE"
20
+
21
+ import gradio as gr
22
+ import numpy as np
23
+ from PIL import Image
24
+ import torch
25
+ import torch.nn.functional as F
26
+ from torchvision.transforms.functional import normalize
27
+ from huggingface_hub import hf_hub_download
28
+ import cv2
29
+ import onnxruntime as ort
30
+
31
+ # Force CPU for PyTorch (image model)
32
+ device = torch.device("cpu")
33
+
34
+ # =============================================================================
35
+ # Image Background Removal (BRIA RMBG 1.4)
36
+ # =============================================================================
37
+
38
+ class BriaRMBGModel:
39
+ """Wrapper for BRIA-RMBG 1.4 model"""
40
+
41
+ def __init__(self):
42
+ self.model = None
43
+ self.input_size = [1024, 1024]
44
+
45
+ def load_model(self):
46
+ """Load the BRIA-RMBG 1.4 model from HuggingFace"""
47
+ if self.model is not None:
48
+ return
49
+
50
+ print("Loading BRIA-RMBG 1.4 model...")
51
+
52
+ # Download all required files from BRIA repo
53
+ repo_id = "briaai/RMBG-1.4"
54
+ model_path = hf_hub_download(repo_id=repo_id, filename="model.pth")
55
+ briarmbg_path = hf_hub_download(repo_id=repo_id, filename="briarmbg.py")
56
+ myconfig_path = hf_hub_download(repo_id=repo_id, filename="MyConfig.py")
57
+
58
+ # Setup package directory
59
+ briarmbg_dir = os.path.dirname(briarmbg_path)
60
+
61
+ # Create __init__.py to make it a proper package
62
+ init_path = os.path.join(briarmbg_dir, "__init__.py")
63
+ if not os.path.exists(init_path):
64
+ with open(init_path, "w") as f:
65
+ f.write("# Auto-generated for package import\n")
66
+
67
+ # Add parent directory to path so we can import as package
68
+ parent_dir = os.path.dirname(briarmbg_dir)
69
+ package_name = os.path.basename(briarmbg_dir)
70
+
71
+ if parent_dir not in sys.path:
72
+ sys.path.insert(0, parent_dir)
73
+
74
+ # Import using the package structure
75
+ import importlib
76
+ briarmbg_module = importlib.import_module(f"{package_name}.briarmbg")
77
+ BriaRMBG = briarmbg_module.BriaRMBG
78
+
79
+ self.model = BriaRMBG()
80
+ self.model.load_state_dict(torch.load(model_path, map_location=device))
81
+ self.model.to(device)
82
+ self.model.eval()
83
+ print("BRIA-RMBG model loaded!")
84
+
85
+ def preprocess(self, image: Image.Image):
86
+ """Preprocess image for model input"""
87
+ if image.mode != "RGB":
88
+ image = image.convert("RGB")
89
+
90
+ orig_size = image.size
91
+ im = np.array(image)
92
+
93
+ if len(im.shape) < 3:
94
+ im = im[:, :, np.newaxis]
95
+ if im.shape[2] == 1:
96
+ im = np.repeat(im, 3, axis=2)
97
+
98
+ im_tensor = torch.tensor(im, dtype=torch.float32).permute(2, 0, 1)
99
+ im_tensor = F.interpolate(
100
+ im_tensor.unsqueeze(0),
101
+ self.input_size,
102
+ mode="bilinear",
103
+ align_corners=False
104
+ )
105
+
106
+ image_tensor = im_tensor / 255.0
107
+ image_tensor = normalize(image_tensor, [0.5, 0.5, 0.5], [1.0, 1.0, 1.0])
108
+
109
+ return image_tensor, orig_size
110
+
111
+ def postprocess(self, result, orig_size):
112
+ """Postprocess model output to mask"""
113
+ result = F.interpolate(
114
+ result,
115
+ size=(orig_size[1], orig_size[0]),
116
+ mode="bilinear",
117
+ align_corners=False
118
+ )
119
+
120
+ ma = torch.max(result)
121
+ mi = torch.min(result)
122
+ result = (result - mi) / (ma - mi + 1e-8)
123
+
124
+ mask = (result.squeeze().cpu().numpy() * 255).astype(np.uint8)
125
+ return mask
126
+
127
+ @torch.no_grad()
128
+ def remove_background(self, image: Image.Image):
129
+ """Remove background from image"""
130
+ self.load_model()
131
+
132
+ input_tensor, orig_size = self.preprocess(image)
133
+ input_tensor = input_tensor.to(device)
134
+
135
+ result = self.model(input_tensor)
136
+
137
+ if isinstance(result, (list, tuple)):
138
+ result = result[0]
139
+ if isinstance(result, (list, tuple)):
140
+ result = result[0]
141
+
142
+ mask = self.postprocess(result, orig_size)
143
+
144
+ pil_mask = Image.fromarray(mask).convert("L")
145
+
146
+ if image.mode != "RGB":
147
+ image = image.convert("RGB")
148
+
149
+ output = image.copy()
150
+ output.putalpha(pil_mask)
151
+
152
+ return output, pil_mask
153
+
154
+
155
+ # =============================================================================
156
+ # Image Background Removal (IS-Net/DIS - Ultra-fine edges) - ONNX Runtime
157
+ # =============================================================================
158
+
159
+ class ISNetModel:
160
+ """Wrapper for IS-Net/DIS ONNX model - better for hair, fur, transparency"""
161
+
162
+ def __init__(self):
163
+ self.session = None
164
+ self.input_size = [1024, 1024]
165
+
166
+ def load_model(self):
167
+ """Load IS-Net ONNX model"""
168
+ if self.session is not None:
169
+ return
170
+
171
+ print("Loading IS-Net/DIS ONNX model...")
172
+ model_path = os.path.join(os.path.dirname(__file__), "isnet_dis_fp32.onnx")
173
+
174
+ # Setup ONNX Runtime with optimizations
175
+ sess_options = ort.SessionOptions()
176
+ sess_options.intra_op_num_threads = 2
177
+ sess_options.inter_op_num_threads = 1
178
+ sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
179
+
180
+ self.session = ort.InferenceSession(
181
+ model_path,
182
+ sess_options,
183
+ providers=['CPUExecutionProvider']
184
+ )
185
+ print("IS-Net/DIS ONNX model loaded!")
186
+
187
+ def preprocess(self, image: Image.Image):
188
+ """Preprocess image for model input"""
189
+ if image.mode != "RGB":
190
+ image = image.convert("RGB")
191
+
192
+ orig_size = image.size
193
+ # Resize to 1024x1024
194
+ im_resized = image.resize((1024, 1024), Image.BILINEAR)
195
+ im = np.array(im_resized).astype(np.float32)
196
+
197
+ # Normalize: (x/255 - 0.5) / 1.0 = x/255 - 0.5
198
+ im = im / 255.0
199
+ im = (im - 0.5) / 1.0
200
+
201
+ # HWC -> NCHW
202
+ im = np.transpose(im, (2, 0, 1))[np.newaxis, ...]
203
+
204
+ return im.astype(np.float32), orig_size
205
+
206
+ def postprocess(self, result, orig_size):
207
+ """Postprocess model output to mask"""
208
+ # Result is already sigmoid (0-1), resize to original
209
+ mask_1024 = result[0, 0] # 1024x1024
210
+
211
+ # Resize to original size
212
+ mask_pil = Image.fromarray((mask_1024 * 255).astype(np.uint8))
213
+ mask_pil = mask_pil.resize(orig_size, Image.BILINEAR)
214
+
215
+ return np.array(mask_pil)
216
+
217
+ def remove_background(self, image: Image.Image):
218
+ """Remove background from image using IS-Net ONNX"""
219
+ self.load_model()
220
+
221
+ input_tensor, orig_size = self.preprocess(image)
222
+
223
+ # Run ONNX inference - d0 is the main output
224
+ outputs = self.session.run(None, {'input': input_tensor})
225
+ result = outputs[0] # d0: main segmentation mask
226
+
227
+ mask = self.postprocess(result, orig_size)
228
+
229
+ pil_mask = Image.fromarray(mask).convert("L")
230
+
231
+ if image.mode != "RGB":
232
+ image = image.convert("RGB")
233
+
234
+ output = image.copy()
235
+ output.putalpha(pil_mask)
236
+
237
+ return output, pil_mask
238
+
239
+
240
+ # =============================================================================
241
+ # Image Background Removal (BRIA RMBG 2.0 - BiRefNet based, high quality)
242
+ # =============================================================================
243
+
244
+ class RMBG2Model:
245
+ """Wrapper for BRIA-RMBG 2.0 model (BiRefNet architecture) - higher quality, slower"""
246
+
247
+ def __init__(self):
248
+ self.model = None
249
+ self.input_size = 1024 # Native resolution for best quality
250
+
251
+ def load_model(self):
252
+ """Load RMBG-2.0 model from HuggingFace"""
253
+ if self.model is not None:
254
+ return
255
+
256
+ print("Loading BRIA-RMBG 2.0 model (BiRefNet)...")
257
+
258
+ # Download model files from ungated repo
259
+ repo_id = "1038lab/RMBG-2.0"
260
+ model_path = hf_hub_download(repo_id=repo_id, filename="model.safetensors")
261
+ birefnet_path = hf_hub_download(repo_id=repo_id, filename="birefnet.py")
262
+ config_path = hf_hub_download(repo_id=repo_id, filename="BiRefNet_config.py")
263
+
264
+ # Setup import path
265
+ model_dir = os.path.dirname(birefnet_path)
266
+ if model_dir not in sys.path:
267
+ sys.path.insert(0, model_dir)
268
+
269
+ # Import model classes
270
+ from safetensors.torch import load_file
271
+ import importlib.util
272
+
273
+ # Load BiRefNet_config
274
+ spec = importlib.util.spec_from_file_location("BiRefNet_config", config_path)
275
+ config_module = importlib.util.module_from_spec(spec)
276
+ sys.modules["BiRefNet_config"] = config_module
277
+ spec.loader.exec_module(config_module)
278
+
279
+ # Fix relative import in birefnet.py - read, fix, and exec
280
+ with open(birefnet_path, 'r', encoding='utf-8') as f:
281
+ birefnet_code = f.read()
282
+
283
+ # Replace relative import with absolute
284
+ birefnet_code = birefnet_code.replace(
285
+ 'from .BiRefNet_config import BiRefNetConfig',
286
+ 'from BiRefNet_config import BiRefNetConfig'
287
+ )
288
+
289
+ # Execute the fixed code
290
+ birefnet_module = type(sys)("birefnet")
291
+ exec(compile(birefnet_code, birefnet_path, 'exec'), birefnet_module.__dict__)
292
+
293
+ BiRefNet = birefnet_module.BiRefNet
294
+ BiRefNetConfig = config_module.BiRefNetConfig
295
+
296
+ # Create and load model
297
+ config = BiRefNetConfig(bb_pretrained=False)
298
+ self.model = BiRefNet(config)
299
+
300
+ state_dict = load_file(model_path)
301
+ self.model.load_state_dict(state_dict)
302
+ self.model.to(device)
303
+ self.model.eval()
304
+
305
+ print(f"BRIA-RMBG 2.0 loaded! ({sum(p.numel() for p in self.model.parameters())/1e6:.0f}M params)")
306
+
307
+ def preprocess(self, image: Image.Image):
308
+ """Preprocess image for model input"""
309
+ if image.mode != "RGB":
310
+ image = image.convert("RGB")
311
+
312
+ orig_size = image.size
313
+
314
+ # Resize to model input size
315
+ im_resized = image.resize((self.input_size, self.input_size), Image.BILINEAR)
316
+ im = np.array(im_resized).astype(np.float32)
317
+
318
+ # Normalize
319
+ im = im / 255.0
320
+ im = (im - 0.5) / 1.0
321
+
322
+ # HWC -> NCHW
323
+ im_tensor = torch.from_numpy(im).permute(2, 0, 1).unsqueeze(0)
324
+
325
+ return im_tensor.float(), orig_size
326
+
327
+ def postprocess(self, result, orig_size):
328
+ """Postprocess model output to mask"""
329
+ # Get final output (BiRefNet returns list for deep supervision)
330
+ if isinstance(result, (list, tuple)):
331
+ result = result[-1] # Last output is the refined mask
332
+
333
+ # Sigmoid and resize
334
+ mask = torch.sigmoid(result).squeeze().cpu().numpy()
335
+
336
+ # Resize to original size
337
+ mask_pil = Image.fromarray((mask * 255).astype(np.uint8))
338
+ mask_pil = mask_pil.resize(orig_size, Image.BILINEAR)
339
+
340
+ return np.array(mask_pil)
341
+
342
+ @torch.no_grad()
343
+ def remove_background(self, image: Image.Image):
344
+ """Remove background from image using RMBG-2.0"""
345
+ self.load_model()
346
+
347
+ input_tensor, orig_size = self.preprocess(image)
348
+ input_tensor = input_tensor.to(device)
349
+
350
+ result = self.model(input_tensor)
351
+ mask = self.postprocess(result, orig_size)
352
+
353
+ pil_mask = Image.fromarray(mask).convert("L")
354
+
355
+ if image.mode != "RGB":
356
+ image = image.convert("RGB")
357
+
358
+ output = image.copy()
359
+ output.putalpha(pil_mask)
360
+
361
+ return output, pil_mask
362
+
363
+
364
+ # =============================================================================
365
+ # Video Background Removal (Robust Video Matting - ONNX Runtime)
366
+ # =============================================================================
367
+
368
+ rvm_session = None
369
+
370
+
371
+ def load_rvm_onnx():
372
+ """Load RobustVideoMatting ONNX model for fast CPU inference."""
373
+ global rvm_session
374
+ if rvm_session is not None:
375
+ return rvm_session
376
+
377
+ print("Loading Robust Video Matting ONNX model...")
378
+
379
+ # Load ONNX model from local Space directory
380
+ model_path = os.path.join(os.path.dirname(__file__), "rvm_mobilenetv3_fp32.onnx")
381
+
382
+ # Setup ONNX Runtime with optimizations (2 vCPU on cpu-basic)
383
+ sess_options = ort.SessionOptions()
384
+ sess_options.intra_op_num_threads = 2
385
+ sess_options.inter_op_num_threads = 1
386
+ sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
387
+
388
+ rvm_session = ort.InferenceSession(
389
+ model_path,
390
+ sess_options,
391
+ providers=['CPUExecutionProvider']
392
+ )
393
+
394
+ print("RVM ONNX model loaded! (2x faster than PyTorch)")
395
+ return rvm_session
396
+
397
+
398
+ # Output resolution options
399
+ RESOLUTION_OPTIONS = {
400
+ "Original": None,
401
+ "1080p": 1080,
402
+ "720p": 720,
403
+ "480p": 480,
404
+ }
405
+
406
+
407
+ def process_video(input_video, output_type="green_screen", max_resolution="720p", downsample_ratio=0.25, progress=None):
408
+ """
409
+ Process video to remove background using ONNX Runtime.
410
+
411
+ Args:
412
+ input_video: Path to input video
413
+ output_type: "green_screen" or "foreground_only"
414
+ max_resolution: Output video resolution (affects speed)
415
+ downsample_ratio: Internal model quality (affects edge accuracy)
416
+ progress: Gradio Progress tracker (optional, for web UI)
417
+
418
+ Returns:
419
+ Path to processed video
420
+ """
421
+ if input_video is None:
422
+ return None
423
+
424
+ # Get max height from resolution option
425
+ max_h = RESOLUTION_OPTIONS.get(max_resolution)
426
+
427
+ session = load_rvm_onnx()
428
+ output_names = [o.name for o in session.get_outputs()]
429
+
430
+ cap = cv2.VideoCapture(input_video)
431
+ fps = cap.get(cv2.CAP_PROP_FPS)
432
+ orig_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
433
+ orig_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
434
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
435
+
436
+ # Resize based on quality preset
437
+ if max_h and orig_height > max_h:
438
+ scale = max_h / orig_height
439
+ width = int(orig_width * scale)
440
+ height = max_h
441
+ print(f"Resizing: {orig_width}x{orig_height} -> {width}x{height}")
442
+ else:
443
+ width, height = orig_width, orig_height
444
+
445
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
446
+ output_path = tmp.name
447
+
448
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
449
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
450
+
451
+ green_bg = np.zeros((height, width, 3), dtype=np.uint8)
452
+ green_bg[:] = (0, 255, 0)
453
+
454
+ # Initialize recurrent states (zeros for first frame)
455
+ r1 = np.zeros((1, 1, 1, 1), dtype=np.float32)
456
+ r2 = np.zeros((1, 1, 1, 1), dtype=np.float32)
457
+ r3 = np.zeros((1, 1, 1, 1), dtype=np.float32)
458
+ r4 = np.zeros((1, 1, 1, 1), dtype=np.float32)
459
+
460
+ frame_idx = 0
461
+ while True:
462
+ ret, frame = cap.read()
463
+ if not ret:
464
+ break
465
+
466
+ # Resize if needed
467
+ if frame.shape[0] != height or frame.shape[1] != width:
468
+ frame = cv2.resize(frame, (width, height))
469
+
470
+ # Preprocess frame
471
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
472
+ src = frame_rgb.astype(np.float32) / 255.0
473
+ src = np.transpose(src, (2, 0, 1))[np.newaxis, ...] # NCHW
474
+
475
+ # ONNX inference
476
+ outputs = session.run(output_names, {
477
+ 'src': src,
478
+ 'r1i': r1,
479
+ 'r2i': r2,
480
+ 'r3i': r3,
481
+ 'r4i': r4,
482
+ 'downsample_ratio': np.array([downsample_ratio], dtype=np.float32)
483
+ })
484
+
485
+ fgr, pha, r1, r2, r3, r4 = outputs
486
+
487
+ # Postprocess
488
+ fgr_np = np.transpose(fgr[0], (1, 2, 0)) # CHW -> HWC
489
+ pha_np = pha[0, 0]
490
+
491
+ if fgr_np.shape[:2] != (height, width):
492
+ fgr_np = cv2.resize(fgr_np, (width, height))
493
+ pha_np = cv2.resize(pha_np, (width, height))
494
+
495
+ pha_3ch = np.stack([pha_np] * 3, axis=-1)
496
+
497
+ if output_type == "green_screen":
498
+ result = (fgr_np * pha_3ch + (green_bg / 255.0) * (1 - pha_3ch))
499
+ else:
500
+ result = fgr_np * pha_3ch
501
+
502
+ result_bgr = cv2.cvtColor((result * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)
503
+ out.write(result_bgr)
504
+
505
+ frame_idx += 1
506
+ if frame_count > 0:
507
+ if progress is not None:
508
+ progress(frame_idx / frame_count, desc=f"Processing frame {frame_idx}/{frame_count}")
509
+ elif frame_idx % 30 == 0:
510
+ print(f"Processing frame {frame_idx}/{frame_count}")
511
+
512
+ cap.release()
513
+ out.release()
514
+
515
+ return output_path
516
+
517
+
518
+ # =============================================================================
519
+ # Gradio Interface
520
+ # =============================================================================
521
+
522
+ # Initialize image models (lazy loading)
523
+ bria_model = BriaRMBGModel()
524
+ rmbg2_model = RMBG2Model()
525
+ isnet_model = ISNetModel()
526
+
527
+ IMAGE_MODELS = {
528
+ "BRIA-RMBG 1.4 (fast, general)": bria_model,
529
+ "BRIA-RMBG 2.0 (high quality, slow)": rmbg2_model,
530
+ "IS-Net/DIS (fine edges, hair)": isnet_model,
531
+ }
532
+
533
+
534
+ IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.gif', '.tiff'}
535
+ VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv'}
536
+
537
+
538
+ def detect_file_type(file_path):
539
+ """Detect if file is image or video based on extension."""
540
+ if file_path is None:
541
+ return None
542
+ ext = os.path.splitext(file_path)[1].lower()
543
+ if ext in IMAGE_EXTENSIONS:
544
+ return "image"
545
+ elif ext in VIDEO_EXTENSIONS:
546
+ return "video"
547
+ return None
548
+
549
+
550
+ def on_file_upload(file_path):
551
+ """Handle file upload - detect type and update preview."""
552
+ if file_path is None:
553
+ return gr.Image(visible=False), gr.Video(visible=False), None
554
+
555
+ file_type = detect_file_type(file_path)
556
+ if file_type == "image":
557
+ return gr.Image(value=file_path, visible=True), gr.Video(visible=False), "image"
558
+ elif file_type == "video":
559
+ return gr.Image(visible=False), gr.Video(value=file_path, visible=True), "video"
560
+ return gr.Image(visible=False), gr.Video(visible=False), None
561
+
562
+
563
+ def process_media(file_path, file_type, image_model_choice, output_type, max_resolution, edge_quality, progress=gr.Progress()):
564
+ """Process based on stored file type."""
565
+ if file_path is None:
566
+ return None, None
567
+
568
+ # Auto-detect file type if not provided (for API calls)
569
+ if file_type is None:
570
+ file_type = detect_file_type(file_path)
571
+
572
+ if file_type is None:
573
+ return None, None
574
+
575
+ if file_type == "image":
576
+ pil_image = Image.open(file_path)
577
+ model = IMAGE_MODELS.get(image_model_choice, bria_model)
578
+ output_img, mask = model.remove_background(pil_image)
579
+ return output_img, None
580
+
581
+ elif file_type == "video":
582
+ output_path = process_video(file_path, output_type, max_resolution, edge_quality, progress)
583
+ return None, output_path
584
+
585
+ return None, None
586
+
587
+
588
+ def build_ui():
589
+ """Build compact unified Gradio interface."""
590
+ with gr.Blocks(title="Background Removal") as demo:
591
+ gr.Markdown("## Background Removal\nUpload image or video. Image models: RMBG 1.4 (fast) | RMBG 2.0 (quality) | IS-Net (fine edges) | Video: RVM")
592
+
593
+ # State to track file type
594
+ file_type_state = gr.State(value=None)
595
+
596
+ with gr.Row():
597
+ with gr.Column(scale=1):
598
+ input_file = gr.File(
599
+ label="Upload Image or Video",
600
+ file_types=["image", "video"],
601
+ type="filepath"
602
+ )
603
+ # Preview - only one visible at a time
604
+ preview_image = gr.Image(label="Preview", interactive=False, visible=False)
605
+ preview_video = gr.Video(label="Preview", interactive=False, visible=False)
606
+
607
+ # Image model selector
608
+ image_model_dropdown = gr.Dropdown(
609
+ choices=list(IMAGE_MODELS.keys()),
610
+ value="BRIA-RMBG 1.4 (fast, general)",
611
+ label="Image Model",
612
+ info="1.4: fast | 2.0: quality (slow) | IS-Net: fine edges"
613
+ )
614
+
615
+ with gr.Accordion("Video Settings", open=False):
616
+ output_type = gr.Radio(
617
+ choices=["green_screen", "foreground_only"],
618
+ value="green_screen",
619
+ label="Output Type"
620
+ )
621
+ max_res = gr.Dropdown(
622
+ choices=list(RESOLUTION_OPTIONS.keys()),
623
+ value="720p",
624
+ label="Output Resolution"
625
+ )
626
+ edge_quality = gr.Slider(
627
+ minimum=0.1,
628
+ maximum=1.0,
629
+ value=0.25,
630
+ step=0.05,
631
+ label="Edge Quality"
632
+ )
633
+
634
+ process_btn = gr.Button("Remove Background", variant="primary")
635
+
636
+ with gr.Column(scale=1):
637
+ # Output components
638
+ output_image = gr.Image(label="Result (Image)", type="pil")
639
+ output_video = gr.Video(label="Result (Video)")
640
+
641
+ # Example files (click to populate inputs, then press button)
642
+ gr.Examples(
643
+ examples=[
644
+ ["examples/cat.jpg", "BRIA-RMBG 1.4 (fast, general)", "green_screen", "720p", 0.25],
645
+ ],
646
+ inputs=[input_file, image_model_dropdown, output_type, max_res, edge_quality],
647
+ label="Examples"
648
+ )
649
+
650
+ # On upload: detect type and show preview
651
+ input_file.change(
652
+ fn=on_file_upload,
653
+ inputs=[input_file],
654
+ outputs=[preview_image, preview_video, file_type_state]
655
+ )
656
+
657
+ # On process: fill the already-visible output
658
+ process_btn.click(
659
+ fn=process_media,
660
+ inputs=[input_file, file_type_state, image_model_dropdown, output_type, max_res, edge_quality],
661
+ outputs=[output_image, output_video]
662
+ )
663
+
664
+ return demo
665
+
666
+
667
+ # =============================================================================
668
+ # CLI Interface
669
+ # =============================================================================
670
+
671
+ def cli_main():
672
+ """CLI entry point."""
673
+ parser = argparse.ArgumentParser(description="Background Removal - Image and Video")
674
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
675
+
676
+ # Image command
677
+ img_parser = subparsers.add_parser("image", help="Remove background from image")
678
+ img_parser.add_argument("input", type=str, help="Input image path")
679
+ img_parser.add_argument("--output", "-o", type=str, default="output.png", help="Output path")
680
+ img_parser.add_argument("--mask", "-m", type=str, help="Save mask to path")
681
+ img_parser.add_argument("--model", type=str, default="BRIA-RMBG 1.4 (fast, general)",
682
+ choices=list(IMAGE_MODELS.keys()),
683
+ help="Image model: BRIA (fast) or IS-Net (fine edges)")
684
+
685
+ # Video command
686
+ vid_parser = subparsers.add_parser("video", help="Remove background from video")
687
+ vid_parser.add_argument("input", type=str, help="Input video path")
688
+ vid_parser.add_argument("--output", "-o", type=str, default="output.mp4", help="Output path")
689
+ vid_parser.add_argument("--type", type=str, default="green_screen",
690
+ choices=["green_screen", "foreground_only"])
691
+ vid_parser.add_argument("--downsample", type=float, default=0.25, help="Downsample ratio")
692
+
693
+ args = parser.parse_args()
694
+
695
+ if args.command == "image":
696
+ print(f"Processing image: {args.input}")
697
+ input_img = Image.open(args.input)
698
+ # Use selected model (default: BRIA)
699
+ model = IMAGE_MODELS.get(args.model, bria_model)
700
+ output_img, mask = model.remove_background(input_img)
701
+ output_img.save(args.output)
702
+ print(f"Saved: {args.output}")
703
+ if args.mask:
704
+ mask.save(args.mask)
705
+ print(f"Saved mask: {args.mask}")
706
+
707
+ elif args.command == "video":
708
+ print(f"Processing video: {args.input}")
709
+ output_path = process_video(args.input, args.type, args.downsample)
710
+ if output_path:
711
+ import shutil
712
+ shutil.move(output_path, args.output)
713
+ print(f"Saved: {args.output}")
714
+
715
+ else:
716
+ parser.print_help()
717
+
718
+
719
+ if __name__ == "__main__":
720
+ if len(sys.argv) > 1 and sys.argv[1] in ["image", "video", "-h", "--help"]:
721
+ cli_main()
722
+ else:
723
+ demo = build_ui()
724
+ demo.launch(theme=gr.themes.Soft())