israellaguan commited on
Commit
5d7cec7
·
verified ·
1 Parent(s): b008d42
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ birefnet_portrait.trt filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,3 +1,115 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BiRefNet Portrait - TensorRT
2
+
3
+ Fast background removal for portrait images using BiRefNet with NVIDIA TensorRT acceleration.
4
+
5
+ ## Features
6
+
7
+ - **5.3x speedup** over PyTorch (RTX 3060, 1024x1024)
8
+ - **123ms** median inference time with TensorRT FP16
9
+ - Simple CLI interface
10
+ - Python API for programmatic usage
11
+
12
+ ## Requirements
13
+
14
+ - CUDA 12.0+
15
+ - TensorRT 10.x
16
+ - Python 3.8+
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install -r requirements.txt
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### CLI
27
+
28
+ Process a single image:
29
+
30
+ ```bash
31
+ python -m rmbg.cli process input.jpg -o output/
32
+ ```
33
+
34
+ Process a directory:
35
+
36
+ ```bash
37
+ python -m rmbg.cli process input_folder/ -o output/
38
+ ```
39
+
40
+ Options:
41
+
42
+ ```bash
43
+ python -m rmbg.cli process input.jpg -o output/ --verbose --warmup
44
+ ```
45
+
46
+ - `--verbose, -v`: Show detailed timing information
47
+ - `--warmup, -w`: Warmup backend before processing (benchmark mode)
48
+ - `--size, -s`: Input size (default: 1024)
49
+ - `--format`: Output format: png, jpg, webp (default: png)
50
+
51
+ ### Python API
52
+
53
+ ```python
54
+ from rmbg.tools import Pipeline
55
+
56
+ # Create pipeline
57
+ pipeline = Pipeline(backend="tensorrt", size=1024)
58
+
59
+ # Process single image
60
+ result = pipeline.run("input.jpg", output_path="output/")
61
+
62
+ # Process directory
63
+ results = pipeline.run("input_folder/", output_path="output/")
64
+
65
+ # Cleanup
66
+ pipeline.unload()
67
+ ```
68
+
69
+ ### Advanced Usage
70
+
71
+ ```python
72
+ from rmbg.tools import BackgroundRemover
73
+
74
+ # Direct background remover access
75
+ remover = BackgroundRemover(backend_name="tensorrt")
76
+
77
+ from PIL import Image
78
+ image = Image.open("input.jpg").convert('RGB')
79
+ result = remover.process(image)
80
+ result.save("output.png")
81
+
82
+ remover.unload()
83
+ ```
84
+
85
+ ## Model
86
+
87
+ - **Architecture**: BiRefNet (Bilateral Reference Network)
88
+ - **Input**: RGB images, resized to 1024x1024
89
+ - **Output**: PNG with transparent background
90
+ - **Format**: TensorRT engine (.trt)
91
+ - **Precision**: FP16
92
+
93
+ ## Performance
94
+
95
+ | Runtime | Median | FPS | Speedup |
96
+ |---------------|--------|-----|----------|
97
+ | TensorRT FP16 | 123ms | 8.1 | **5.3x** |
98
+ | PyTorch | 653ms | 1.5 | 1.0x |
99
+
100
+ Tested on RTX 3060, CUDA 12.0, TensorRT 10.8
101
+
102
+ ## License
103
+
104
+ This model is based on BiRefNet. See original repository for license details.
105
+
106
+ ## Citation
107
+
108
+ ```bibtex
109
+ @article{biRefNet2024,
110
+ title={BiRefNet: Bilateral Reference Network for High-Resolution Dichotomous Image Segmentation},
111
+ author={Zheng, Peng and Gao, Dehong and Fan, Guolei and Li, Sheng and Sarkar, Berihun},
112
+ journal={arXiv preprint},
113
+ year={2024}
114
+ }
115
+ ```
birefnet_portrait.trt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b59bebf986283190e3fa9405b1f59530829ee792d6546176b979d09c42cda055
3
+ size 645133036
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BiRefNet Portrait - TensorRT Runtime Dependencies
2
+ # Install with: pip install -r requirements.txt
3
+
4
+ # Core ML
5
+ torch>=2.5.0
6
+ torchvision>=0.20.0
7
+ tensorrt>=10.0.0
8
+
9
+ # CLI and UI
10
+ typer>=0.9.0
11
+ rich>=13.0.0
12
+
13
+ # Image processing
14
+ pillow>=9.0.0
15
+ numpy<2
16
+
17
+ # Optional: for HuggingFace integration
18
+ huggingface-hub>=0.25.0
rmbg/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """RMBG - Background Removal Pipeline CLI."""
2
+
3
+ __version__ = "0.1.0"
rmbg/backends/__init__.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backend implementations for RMBG."""
2
+
3
+ from .base import BaseBackend
4
+ from .tensorrt import TensorRTBackend
5
+ from .huggingface import HuggingFaceBackend
6
+
7
+ __all__ = ["BaseBackend", "TensorRTBackend", "HuggingFaceBackend", "get_available_backends", "get_backend", "get_fastest_backend"]
8
+
9
+ def get_available_backends() -> list[type[BaseBackend]]:
10
+ """Get list of available backends, sorted by priority."""
11
+ backends = [TensorRTBackend, HuggingFaceBackend]
12
+ available = []
13
+ for backend_class in backends:
14
+ try:
15
+ instance = backend_class()
16
+ if instance.is_available():
17
+ available.append(backend_class)
18
+ except Exception:
19
+ pass
20
+ return sorted(available, key=lambda b: b().priority)
21
+
22
+ def get_backend(name: str) -> BaseBackend:
23
+ """Get backend by name."""
24
+ backends = {
25
+ "tensorrt": TensorRTBackend,
26
+ "trt": TensorRTBackend,
27
+ "hf": HuggingFaceBackend,
28
+ "huggingface": HuggingFaceBackend,
29
+ "pytorch": HuggingFaceBackend,
30
+ }
31
+ if name.lower() not in backends:
32
+ raise ValueError(f"Unknown backend: {name}. Available: {list(backends.keys())}")
33
+ return backends[name.lower()]()
34
+
35
+ def get_fastest_backend() -> BaseBackend:
36
+ """Get the fastest available backend."""
37
+ available = get_available_backends()
38
+ if not available:
39
+ raise RuntimeError("No backends available")
40
+ return available[0]()
rmbg/backends/base.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backend interface for RMBG."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Union
5
+ import numpy as np
6
+ import torch
7
+
8
+
9
+ class BaseBackend(ABC):
10
+ """Abstract base class for background removal backends."""
11
+
12
+ def __init__(self, model_name: str = "briaai/RMBG-2.0"):
13
+ self.model_name = model_name
14
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
15
+ self._model = None
16
+
17
+ @property
18
+ @abstractmethod
19
+ def name(self) -> str:
20
+ """Backend name identifier."""
21
+ pass
22
+
23
+ @property
24
+ @abstractmethod
25
+ def priority(self) -> int:
26
+ """Priority for auto-selection (lower = faster/higher priority)."""
27
+ pass
28
+
29
+ @abstractmethod
30
+ def load(self) -> None:
31
+ """Load the model."""
32
+ pass
33
+
34
+ @abstractmethod
35
+ def predict(self, image: Union[np.ndarray, torch.Tensor]) -> Union[np.ndarray, torch.Tensor]:
36
+ """Run inference and return alpha mask."""
37
+ pass
38
+
39
+ def is_available(self) -> bool:
40
+ """Check if backend is available on this system."""
41
+ return True
42
+
43
+ def unload(self) -> None:
44
+ """Unload model to free memory."""
45
+ self._model = None
46
+ if torch.cuda.is_available():
47
+ torch.cuda.empty_cache()
rmbg/backends/huggingface.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace backend for RMBG using PyTorch."""
2
+
3
+ import torch
4
+ from torchvision import transforms
5
+ from transformers import AutoModelForImageSegmentation
6
+ from PIL import Image
7
+ import numpy as np
8
+
9
+ from .base import BaseBackend
10
+
11
+
12
+ class HuggingFaceBackend(BaseBackend):
13
+ """HuggingFace/PyTorch backend for quality inference."""
14
+
15
+ def __init__(self, model_name: str = "briaai/RMBG-2.0", input_size: int = 1024):
16
+ super().__init__(model_name)
17
+ self.input_size = input_size
18
+
19
+ @property
20
+ def name(self) -> str:
21
+ return "huggingface"
22
+
23
+ @property
24
+ def priority(self) -> int:
25
+ return 2 # Lower priority (slower) than TensorRT
26
+
27
+ def is_available(self) -> bool:
28
+ """Always available if PyTorch is installed."""
29
+ try:
30
+ import transformers
31
+ return True
32
+ except ImportError:
33
+ return False
34
+
35
+ def load(self) -> None:
36
+ """Load model from HuggingFace."""
37
+ if self._model is not None:
38
+ return
39
+
40
+ print(f"Loading {self.model_name} from HuggingFace...")
41
+ self._model = AutoModelForImageSegmentation.from_pretrained(
42
+ self.model_name,
43
+ trust_remote_code=True
44
+ ).eval().to(self.device)
45
+ print("Model loaded")
46
+
47
+ def predict(self, image) -> torch.Tensor:
48
+ """Run inference on preprocessed image."""
49
+ if self._model is None:
50
+ self.load()
51
+
52
+ with torch.no_grad():
53
+ if isinstance(image, Image.Image):
54
+ # Preprocess PIL image
55
+ transform = transforms.Compose([
56
+ transforms.Resize((self.input_size, self.input_size)),
57
+ transforms.ToTensor(),
58
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
59
+ ])
60
+ input_tensor = transform(image).unsqueeze(0).to(self.device)
61
+ elif isinstance(image, torch.Tensor):
62
+ input_tensor = image.to(self.device)
63
+ if input_tensor.dim() == 3:
64
+ input_tensor = input_tensor.unsqueeze(0)
65
+ else:
66
+ raise ValueError(f"Unsupported image type: {type(image)}")
67
+
68
+ # RMBG returns tuple, take last element
69
+ output = self._model(input_tensor)
70
+ if isinstance(output, tuple):
71
+ output = output[-1]
72
+
73
+ return output.sigmoid()
74
+
75
+ def unload(self) -> None:
76
+ """Unload model to free memory."""
77
+ self._model = None
78
+ super().unload()
rmbg/backends/tensorrt.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TensorRT backend for RMBG using native TensorRT API."""
2
+
3
+ import time
4
+ import torch
5
+ import numpy as np
6
+ from torchvision import transforms
7
+ from PIL import Image
8
+ from pathlib import Path
9
+
10
+ try:
11
+ import tensorrt as trt
12
+ import pycuda.driver as cuda
13
+ import pycuda.autoinit
14
+ HAS_TENSORRT = True
15
+ except ImportError:
16
+ HAS_TENSORRT = False
17
+
18
+ from .base import BaseBackend
19
+
20
+
21
+ class TensorRTBackend(BaseBackend):
22
+ """TensorRT backend for fast inference using native TensorRT API."""
23
+
24
+ def __init__(self, engine_path: str = None, input_size: int = 1024):
25
+ super().__init__("birefnet_portrait")
26
+ # Default: look in current directory first (for HF repo), then fall back to weights/
27
+ if engine_path is None:
28
+ current_dir = Path.cwd()
29
+ weights_dir = current_dir / "weights"
30
+ if (current_dir / "birefnet_portrait.trt").exists():
31
+ engine_path = current_dir / "birefnet_portrait.trt"
32
+ else:
33
+ engine_path = weights_dir / "birefnet_portrait.trt"
34
+ self.engine_path = Path(engine_path)
35
+ self.input_size = input_size
36
+ self._engine = None
37
+ self._context = None
38
+ self._input_name = None
39
+ self._output_name = None
40
+ self._warmup_done = False
41
+
42
+ @property
43
+ def name(self) -> str:
44
+ return "tensorrt"
45
+
46
+ @property
47
+ def priority(self) -> int:
48
+ return 1 # Highest priority (fastest)
49
+
50
+ def is_available(self) -> bool:
51
+ """Check if TensorRT is available and engine exists."""
52
+ if not HAS_TENSORRT:
53
+ return False
54
+ if not torch.cuda.is_available():
55
+ return False
56
+ # Check if engine file exists
57
+ return self.engine_path.exists()
58
+
59
+ def load(self, warmup: bool = False) -> None:
60
+ """Load pre-compiled TensorRT engine."""
61
+ if self._engine is not None:
62
+ return
63
+
64
+ if not self.engine_path.exists():
65
+ raise FileNotFoundError(f"TensorRT engine not found: {self.engine_path}")
66
+
67
+ print(f"Loading TensorRT engine from: {self.engine_path}")
68
+
69
+ logger = trt.Logger(trt.Logger.ERROR)
70
+ with open(self.engine_path, 'rb') as f:
71
+ runtime = trt.Runtime(logger)
72
+ self._engine = runtime.deserialize_cuda_engine(f.read())
73
+
74
+ self._context = self._engine.create_execution_context()
75
+
76
+ # Get tensor names
77
+ self._input_name = self._engine.get_tensor_name(0)
78
+ self._output_name = self._engine.get_tensor_name(1)
79
+
80
+ print(f" Engine loaded: {self._engine.name}")
81
+ print(f" Input: {self._input_name}")
82
+ print(f" Output: {self._output_name}")
83
+
84
+ if warmup:
85
+ self.warmup()
86
+
87
+ def warmup(self, num_runs: int = 3) -> None:
88
+ """Warmup with dummy inferences."""
89
+ if self._warmup_done:
90
+ return
91
+
92
+ if self._engine is None:
93
+ self.load()
94
+
95
+ print(f"Warming up with {num_runs} dummy inferences...")
96
+ dummy_input = np.random.randn(1, 3, self.input_size, self.input_size).astype(np.float32)
97
+ dummy_input = np.ascontiguousarray(dummy_input)
98
+
99
+ for _ in range(num_runs):
100
+ # Allocate device memory
101
+ d_input = cuda.mem_alloc(dummy_input.nbytes)
102
+
103
+ # Set input shape
104
+ self._context.set_input_shape(self._input_name, dummy_input.shape)
105
+
106
+ # Get output shape
107
+ output_shape = self._context.get_tensor_shape(self._output_name)
108
+ output_np = np.empty(output_shape, dtype=np.float32)
109
+ d_output = cuda.mem_alloc(output_np.nbytes)
110
+
111
+ # Set tensor addresses
112
+ self._context.set_tensor_address(self._input_name, int(d_input))
113
+ self._context.set_tensor_address(self._output_name, int(d_output))
114
+
115
+ # Copy input to device
116
+ cuda.memcpy_htod(d_input, dummy_input)
117
+
118
+ # Run inference
119
+ self._context.execute_async_v3(stream_handle=0)
120
+
121
+ # Copy output to host
122
+ cuda.memcpy_dtoh(output_np, d_output)
123
+
124
+ # Cleanup
125
+ d_input.free()
126
+ d_output.free()
127
+
128
+ cuda.Context.synchronize()
129
+ self._warmup_done = True
130
+ print("Warmup complete.")
131
+
132
+ def predict(self, image, return_time: bool = False):
133
+ """Run inference on preprocessed image tensor."""
134
+ if self._engine is None:
135
+ self.load()
136
+
137
+ # Preprocess image to tensor
138
+ if isinstance(image, Image.Image):
139
+ transform = transforms.Compose([
140
+ transforms.Resize((self.input_size, self.input_size)),
141
+ transforms.ToTensor(),
142
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
143
+ ])
144
+ input_tensor = transform(image).unsqueeze(0)
145
+ elif isinstance(image, torch.Tensor):
146
+ input_tensor = image
147
+ if input_tensor.dim() == 3:
148
+ input_tensor = input_tensor.unsqueeze(0)
149
+ else:
150
+ raise ValueError(f"Unsupported image type: {type(image)}")
151
+
152
+ # Convert to numpy array (NCHW format)
153
+ input_np = input_tensor.cpu().numpy().astype(np.float32)
154
+ input_np = np.ascontiguousarray(input_np)
155
+
156
+ # Allocate device memory
157
+ d_input = cuda.mem_alloc(input_np.nbytes)
158
+
159
+ # Set input shape
160
+ self._context.set_input_shape(self._input_name, input_np.shape)
161
+
162
+ # Get output shape
163
+ output_shape = self._context.get_tensor_shape(self._output_name)
164
+ output_np = np.empty(output_shape, dtype=np.float32)
165
+ d_output = cuda.mem_alloc(output_np.nbytes)
166
+
167
+ # Set tensor addresses
168
+ self._context.set_tensor_address(self._input_name, int(d_input))
169
+ self._context.set_tensor_address(self._output_name, int(d_output))
170
+
171
+ # Copy input to device
172
+ cuda.memcpy_htod(d_input, input_np)
173
+
174
+ # Synchronize before timing
175
+ cuda.Context.synchronize()
176
+ start = time.perf_counter()
177
+
178
+ # Run inference
179
+ self._context.execute_async_v3(stream_handle=0)
180
+
181
+ # Synchronize after inference
182
+ cuda.Context.synchronize()
183
+ elapsed = time.perf_counter() - start
184
+
185
+ # Copy output to host
186
+ cuda.memcpy_dtoh(output_np, d_output)
187
+
188
+ # Cleanup
189
+ d_input.free()
190
+ d_output.free()
191
+
192
+ # Convert to torch tensor and apply sigmoid
193
+ output_tensor = torch.from_numpy(output_np)
194
+ result = output_tensor.sigmoid()
195
+
196
+ if return_time:
197
+ return result, elapsed
198
+ return result
199
+
200
+ def unload(self) -> None:
201
+ """Unload model to free memory."""
202
+ self._engine = None
203
+ self._context = None
204
+ self._warmup_done = False
205
+ super().unload()
rmbg/cli.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RMBG CLI - Background Removal Pipeline."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+
9
+ from .tools import Pipeline, run_pipeline
10
+ from .backends import get_available_backends, get_backend
11
+
12
+ app = typer.Typer(
13
+ name="rmbg",
14
+ help="Background removal pipeline with TensorRT and HuggingFace backends",
15
+ rich_markup_mode="rich",
16
+ )
17
+ console = Console()
18
+
19
+
20
+ @app.command()
21
+ def process(
22
+ input: Path = typer.Argument(..., help="Input image or directory"),
23
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output directory"),
24
+ backend: Optional[str] = typer.Option(None, "--backend", "-b", help="Backend: tensorrt, hf"),
25
+ quality: bool = typer.Option(False, "--quality", "-q", help="Quality mode (HuggingFace)"),
26
+ fast: bool = typer.Option(False, "--fast", "-f", help="Fast mode (TensorRT)"),
27
+ size: int = typer.Option(1024, "--size", "-s", help="Input size"),
28
+ format: str = typer.Option("png", "--format", help="Output format: png, jpg, webp"),
29
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
30
+ warmup: bool = typer.Option(False, "--warmup", "-w", help="Warmup backend before processing"),
31
+ ):
32
+ """
33
+ Remove background from images.
34
+
35
+ Default uses fastest available backend (TensorRT > HuggingFace).
36
+ Use --quality for best quality, --fast for maximum speed.
37
+ """
38
+ if not input.exists():
39
+ console.print(f"[red]Error: Input not found: {input}[/red]")
40
+ raise typer.Exit(1)
41
+
42
+ # Show available backends
43
+ if verbose:
44
+ available = get_available_backends()
45
+ console.print(f"[dim]Available backends: {[b().name for b in available]}[/dim]")
46
+
47
+ # Run pipeline
48
+ try:
49
+ with Progress(
50
+ SpinnerColumn(),
51
+ TextColumn("[progress.description]{task.description}"),
52
+ console=console,
53
+ transient=True,
54
+ ) as progress:
55
+ task = progress.add_task("Processing...", total=None)
56
+
57
+ results = run_pipeline(
58
+ input_path=input,
59
+ output_path=output,
60
+ backend=backend,
61
+ size=size,
62
+ format=format,
63
+ quality=quality,
64
+ fast=fast,
65
+ verbose=verbose,
66
+ warmup=warmup,
67
+ )
68
+
69
+ progress.update(task, completed=True)
70
+
71
+ if output:
72
+ console.print(f"[green]Results saved to: {output}[/green]")
73
+ else:
74
+ console.print(f"[green]Processed {len(results)} image(s)[/green]")
75
+
76
+ except Exception as e:
77
+ console.print(f"[red]Error: {e}[/red]")
78
+ raise typer.Exit(1)
79
+
80
+
81
+ @app.command()
82
+ def backends(
83
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed info"),
84
+ ):
85
+ """List available backends and their status."""
86
+ available = get_available_backends()
87
+
88
+ console.print("\n[bold]Available Backends:[/bold]")
89
+ for i, backend_class in enumerate(available, 1):
90
+ backend = backend_class()
91
+ status = "[green]✓ Available[/green]"
92
+ console.print(f" {i}. {backend.name} {status}")
93
+ if verbose:
94
+ console.print(f" Priority: {backend.priority}")
95
+ console.print(f" Device: {backend.device}")
96
+
97
+ if not available:
98
+ console.print(" [red]No backends available[/red]")
99
+ else:
100
+ console.print(f"\n[dim]Default: {available[0]().name} (fastest)[/dim]")
101
+ console.print()
102
+
103
+
104
+ @app.command()
105
+ def preprocess(
106
+ input: Path = typer.Argument(..., help="Input image or directory"),
107
+ output: Path = typer.Option(..., "--output", "-o", help="Output directory"),
108
+ size: int = typer.Option(1024, "--size", "-s", help="Target size"),
109
+ ):
110
+ """Preprocess images (resize, normalize)."""
111
+ from .tools import preprocess_images
112
+
113
+ if not input.exists():
114
+ console.print(f"[red]Error: Input not found: {input}[/red]")
115
+ raise typer.Exit(1)
116
+
117
+ console.print(f"Preprocessing to {size}x{size}...")
118
+ preprocess_images(input, output, size=size)
119
+ console.print(f"[green]Results: {output}[/green]")
120
+
121
+
122
+ @app.command()
123
+ def remove_bg(
124
+ input: Path = typer.Argument(..., help="Input image or directory"),
125
+ output: Path = typer.Option(..., "--output", "-o", help="Output directory"),
126
+ backend: str = typer.Option("tensorrt", "--backend", "-b", help="Backend to use"),
127
+ ):
128
+ """Remove background (pipeline stage 2)."""
129
+ from .tools import remove_background
130
+
131
+ if not input.exists():
132
+ console.print(f"[red]Error: Input not found: {input}[/red]")
133
+ raise typer.Exit(1)
134
+
135
+ remove_background(input, output, backend=backend)
136
+
137
+
138
+ @app.command()
139
+ def postprocess(
140
+ input: Path = typer.Argument(..., help="Input image or directory"),
141
+ output: Path = typer.Option(..., "--output", "-o", help="Output directory"),
142
+ format: str = typer.Option("png", "--format", "-f", help="Output format"),
143
+ quality: int = typer.Option(95, "--quality", "-q", help="JPEG/WebP quality"),
144
+ ):
145
+ """Postprocess images (format, resize)."""
146
+ from .tools import postprocess_images
147
+
148
+ if not input.exists():
149
+ console.print(f"[red]Error: Input not found: {input}[/red]")
150
+ raise typer.Exit(1)
151
+
152
+ postprocess_images(input, output, format=format, quality=quality)
153
+ console.print(f"[green]Results: {output}[/green]")
154
+
155
+
156
+ def main():
157
+ """Entry point for CLI."""
158
+ app()
159
+
160
+
161
+ if __name__ == "__main__":
162
+ main()
rmbg/tools/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pipeline tools for RMBG."""
2
+
3
+ from .preprocess import Preprocessor, preprocess_images
4
+ from .remove_bg import BackgroundRemover, remove_background
5
+ from .postprocess import Postprocessor, postprocess_images
6
+ from .pipeline import Pipeline, run_pipeline
7
+
8
+ __all__ = [
9
+ "Preprocessor",
10
+ "preprocess_images",
11
+ "BackgroundRemover",
12
+ "remove_background",
13
+ "Postprocessor",
14
+ "postprocess_images",
15
+ "Pipeline",
16
+ "run_pipeline",
17
+ ]
rmbg/tools/pipeline.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pipeline orchestration for RMBG."""
2
+
3
+ from pathlib import Path
4
+ from typing import Union, Optional, List
5
+ from PIL import Image
6
+ import time
7
+
8
+ from .preprocess import Preprocessor
9
+ from .remove_bg import BackgroundRemover
10
+ from .postprocess import Postprocessor
11
+ from ..backends import get_fastest_backend, get_backend
12
+
13
+
14
+ class Pipeline:
15
+ """End-to-end background removal pipeline."""
16
+
17
+ def __init__(
18
+ self,
19
+ backend: Optional[str] = None,
20
+ size: int = 1024,
21
+ output_format: str = "png",
22
+ quality: bool = False,
23
+ fast: bool = False,
24
+ verbose: bool = False,
25
+ warmup: bool = False,
26
+ ):
27
+ """
28
+ Initialize pipeline.
29
+
30
+ Args:
31
+ backend: Force specific backend ('tensorrt', 'huggingface', 'hf')
32
+ size: Input size for model
33
+ output_format: Output image format
34
+ quality: Use quality mode (HuggingFace backend)
35
+ fast: Force fast mode (TensorRT backend)
36
+ verbose: Show detailed logs and timing
37
+ warmup: Warmup backend before processing
38
+ """
39
+ self.verbose = verbose
40
+ self.warmup = warmup
41
+
42
+ # Determine backend
43
+ if quality:
44
+ self.backend_name = "huggingface"
45
+ elif fast or backend == "tensorrt":
46
+ self.backend_name = "tensorrt"
47
+ elif backend:
48
+ self.backend_name = backend
49
+ else:
50
+ # Auto-select fastest
51
+ fastest = get_fastest_backend()
52
+ self.backend_name = fastest.name
53
+
54
+ self.size = size
55
+ self.output_format = output_format
56
+
57
+ # Initialize stages
58
+ self.preprocessor = Preprocessor(size=size)
59
+ self.remover = BackgroundRemover(backend_name=self.backend_name, verbose=verbose, warmup=warmup)
60
+ self.postprocessor = Postprocessor(output_format=output_format)
61
+
62
+ def run(
63
+ self,
64
+ input_path: Union[str, Path, Image.Image],
65
+ output_path: Optional[Union[str, Path]] = None,
66
+ ) -> Union[Image.Image, List[Image.Image]]:
67
+ """
68
+ Run full pipeline on input.
69
+
70
+ Args:
71
+ input_path: Input image, directory, or PIL Image
72
+ output_path: Output directory (optional, returns images if not provided)
73
+
74
+ Returns:
75
+ Processed image(s) - either saved to disk or returned
76
+ """
77
+ if isinstance(input_path, Image.Image):
78
+ return self._process_single(input_path)
79
+
80
+ input_path = Path(input_path)
81
+
82
+ if input_path.is_file():
83
+ start = time.perf_counter()
84
+ result, inference_time = self._process_single(Image.open(input_path).convert('RGB'), return_time=True)
85
+ elapsed = time.perf_counter() - start
86
+ if output_path:
87
+ output_path = Path(output_path)
88
+ output_path.mkdir(parents=True, exist_ok=True)
89
+ output_file = output_path / f"{input_path.stem}.{self.output_format}"
90
+ self.postprocessor.save(result, output_file)
91
+ print(f"Saved: {output_file}")
92
+ if self.verbose:
93
+ print(f" Total: {elapsed*1000:.1f}ms, Inference: {inference_time*1000:.1f}ms")
94
+ return result
95
+ else:
96
+ return self._process_directory(input_path, output_path)
97
+
98
+ def _process_single(self, image: Image.Image, return_time: bool = False):
99
+ """Process single image through pipeline."""
100
+ # Preprocess
101
+ tensor, original = self.preprocessor.process(image)
102
+
103
+ # Remove background with timing
104
+ if return_time:
105
+ result, inference_time = self.remover.process(original, return_time=True)
106
+ else:
107
+ result = self.remover.process(original)
108
+ inference_time = None
109
+
110
+ # Postprocess
111
+ result = self.postprocessor.process(result, original_size=original.size)
112
+
113
+ if return_time:
114
+ return result, inference_time
115
+ return result
116
+
117
+ def _process_directory(
118
+ self,
119
+ input_dir: Path,
120
+ output_dir: Optional[Path],
121
+ ) -> List[Path]:
122
+ """Process all images in directory."""
123
+ # Get all image files
124
+ files = []
125
+ for ext in ('*.jpg', '*.jpeg', '*.png', '*.webp', '*.bmp'):
126
+ files.extend(input_dir.glob(ext))
127
+
128
+ if output_dir:
129
+ output_dir = Path(output_dir)
130
+ output_dir.mkdir(parents=True, exist_ok=True)
131
+
132
+ results = []
133
+ total_time = 0
134
+ total_inference_time = 0
135
+ processed_count = 0
136
+
137
+ if self.verbose:
138
+ print(f"Processing {len(files)} images with {self.backend_name} backend...")
139
+
140
+ for i, file in enumerate(files, 1):
141
+ try:
142
+ # Load
143
+ image = Image.open(file).convert('RGB')
144
+
145
+ # Run pipeline with timing only if verbose
146
+ if self.verbose:
147
+ start = time.perf_counter()
148
+ result, inference_time = self._process_single(image, return_time=True)
149
+ elapsed = time.perf_counter() - start
150
+ total_time += elapsed
151
+ if inference_time:
152
+ total_inference_time += inference_time
153
+ print(f" [{i}/{len(files)}] {file.name}: total={elapsed*1000:.1f}ms, inference={inference_time*1000:.1f}ms")
154
+ else:
155
+ result = self._process_single(image, return_time=False)
156
+
157
+ # Save or collect
158
+ if output_dir:
159
+ ext = self.output_format if self.output_format != "jpg" else "jpeg"
160
+ output_file = output_dir / f"{file.stem}.{ext}"
161
+ self.postprocessor.save(result, output_file)
162
+ results.append(output_file)
163
+ if not self.verbose:
164
+ print(f"Saved: {output_file}")
165
+ processed_count += 1
166
+ else:
167
+ results.append(result)
168
+ except Exception as e:
169
+ if self.verbose:
170
+ print(f" Error processing {file.name}: {e}")
171
+
172
+ if output_dir and len(files) > 0 and processed_count > 0:
173
+ if total_time > 0:
174
+ avg_time = total_time / processed_count * 1000
175
+ print(f"Average: {avg_time:.1f}ms per image")
176
+
177
+ return results
178
+
179
+ def unload(self):
180
+ """Unload models to free memory."""
181
+ self.remover.unload()
182
+
183
+
184
+ def run_pipeline(
185
+ input_path: Union[str, Path],
186
+ output_path: Optional[Union[str, Path]] = None,
187
+ backend: Optional[str] = None,
188
+ size: int = 1024,
189
+ format: str = "png",
190
+ quality: bool = False,
191
+ fast: bool = False,
192
+ verbose: bool = False,
193
+ warmup: bool = False,
194
+ ):
195
+ """Run full pipeline with CLI-friendly interface."""
196
+ pipeline = Pipeline(
197
+ backend=backend,
198
+ size=size,
199
+ output_format=format,
200
+ quality=quality,
201
+ fast=fast,
202
+ verbose=verbose,
203
+ warmup=warmup,
204
+ )
205
+
206
+ try:
207
+ results = pipeline.run(input_path, output_path)
208
+ return results
209
+ finally:
210
+ pipeline.unload()
rmbg/tools/postprocess.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Postprocessing tool for RMBG pipeline."""
2
+
3
+ from pathlib import Path
4
+ from typing import Union, List, Optional, Tuple
5
+ from PIL import Image
6
+ import numpy as np
7
+
8
+
9
+ class Postprocessor:
10
+ """Postprocessing for background removal results."""
11
+
12
+ def __init__(
13
+ self,
14
+ output_format: str = "png",
15
+ resize_to_original: bool = True,
16
+ matte_edges: bool = False,
17
+ ):
18
+ self.output_format = output_format.lower()
19
+ self.resize_to_original = resize_to_original
20
+ self.matte_edges = matte_edges
21
+
22
+ def process(
23
+ self,
24
+ image: Union[Image.Image, str, Path],
25
+ original_size: Optional[Tuple[int, int]] = None,
26
+ ) -> Image.Image:
27
+ """
28
+ Postprocess result image.
29
+
30
+ Args:
31
+ image: Result image with alpha channel
32
+ original_size: Original size to resize to
33
+
34
+ Returns:
35
+ Processed image
36
+ """
37
+ if isinstance(image, (str, Path)):
38
+ image = Image.open(image)
39
+
40
+ # Ensure RGBA mode
41
+ if image.mode != 'RGBA':
42
+ image = image.convert('RGBA')
43
+
44
+ # Resize to original if needed
45
+ if original_size and self.resize_to_original and image.size != original_size:
46
+ image = image.resize(original_size, Image.LANCZOS)
47
+
48
+ # Edge matting (optional refinement)
49
+ if self.matte_edges:
50
+ image = self._apply_edge_matting(image)
51
+
52
+ return image
53
+
54
+ def _apply_edge_matting(self, image: Image.Image) -> Image.Image:
55
+ """Apply edge matting for smoother edges."""
56
+ # Simple edge refinement - could be enhanced with more sophisticated algorithms
57
+ r, g, b, a = image.split()
58
+
59
+ # Apply slight blur to alpha for smoother edges
60
+ a = a.filter(Image.GaussianBlur(radius=0.5))
61
+
62
+ return Image.merge('RGBA', (r, g, b, a))
63
+
64
+ def save(
65
+ self,
66
+ image: Image.Image,
67
+ output_path: Union[str, Path],
68
+ quality: int = 95,
69
+ ) -> None:
70
+ """Save image with appropriate format settings."""
71
+ output_path = Path(output_path)
72
+
73
+ if self.output_format == "png":
74
+ image.save(output_path, 'PNG', optimize=True)
75
+ elif self.output_format in ("jpg", "jpeg"):
76
+ # Remove alpha for JPEG
77
+ rgb_image = Image.new('RGB', image.size, (255, 255, 255))
78
+ rgb_image.paste(image, mask=image.split()[3]) # Use alpha as mask
79
+ rgb_image.save(output_path, 'JPEG', quality=quality)
80
+ elif self.output_format == "webp":
81
+ image.save(output_path, 'WEBP', quality=quality, lossless=False)
82
+ else:
83
+ image.save(output_path)
84
+
85
+
86
+ def postprocess_images(
87
+ input_path: Union[str, Path],
88
+ output_path: Union[str, Path],
89
+ format: str = "png",
90
+ quality: int = 95,
91
+ ) -> None:
92
+ """
93
+ Postprocess images from input directory to output directory.
94
+
95
+ Args:
96
+ input_path: Path to input image or directory
97
+ output_path: Path to output directory
98
+ format: Output format (png, jpg, webp)
99
+ quality: Quality for lossy formats (1-100)
100
+ """
101
+ input_path = Path(input_path)
102
+ output_path = Path(output_path)
103
+ output_path.mkdir(parents=True, exist_ok=True)
104
+
105
+ postprocessor = Postprocessor(output_format=format)
106
+
107
+ # Get input files
108
+ if input_path.is_file():
109
+ files = [input_path]
110
+ else:
111
+ files = list(input_path.glob("*.png")) + list(input_path.glob("*.jpg")) + list(input_path.glob("*.webp"))
112
+
113
+ for file in files:
114
+ try:
115
+ result = postprocessor.process(file)
116
+ ext = format if format != "jpg" else "jpeg"
117
+ output_file = output_path / f"{file.stem}.{ext}"
118
+ postprocessor.save(result, output_file, quality=quality)
119
+ print(f" Saved: {output_file.name}")
120
+ except Exception as e:
121
+ print(f" Error processing {file.name}: {e}")
122
+
123
+
124
+ if __name__ == "__main__":
125
+ import sys
126
+ if len(sys.argv) < 3:
127
+ print("Usage: python postprocess.py <input> <output> [--format png|jpg|webp] [--quality 95]")
128
+ sys.exit(1)
129
+
130
+ input_arg = sys.argv[1]
131
+ output_arg = sys.argv[2]
132
+ format = "png"
133
+ quality = 95
134
+
135
+ if "--format" in sys.argv:
136
+ idx = sys.argv.index("--format")
137
+ format = sys.argv[idx + 1]
138
+
139
+ if "--quality" in sys.argv:
140
+ idx = sys.argv.index("--quality")
141
+ quality = int(sys.argv[idx + 1])
142
+
143
+ postprocess_images(input_arg, output_arg, format=format, quality=quality)
rmbg/tools/preprocess.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Preprocessing tool for RMBG pipeline."""
2
+
3
+ from pathlib import Path
4
+ from typing import Union, List, Tuple
5
+ from PIL import Image
6
+ import torch
7
+ from torchvision import transforms
8
+ import numpy as np
9
+
10
+
11
+ class Preprocessor:
12
+ """Image preprocessing for background removal."""
13
+
14
+ def __init__(self, size: int = 1024, normalize: bool = True):
15
+ self.size = size
16
+ self.normalize = normalize
17
+
18
+ # Build transform pipeline
19
+ transform_list = [
20
+ transforms.Resize((size, size)),
21
+ transforms.ToTensor(),
22
+ ]
23
+ if normalize:
24
+ transform_list.append(
25
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
26
+ )
27
+ self.transform = transforms.Compose(transform_list)
28
+
29
+ def process(self, image: Union[str, Path, Image.Image]) -> Tuple[torch.Tensor, Image.Image]:
30
+ """
31
+ Preprocess image for inference.
32
+
33
+ Returns:
34
+ Tuple of (tensor for inference, original PIL image)
35
+ """
36
+ if isinstance(image, (str, Path)):
37
+ image = Image.open(image).convert('RGB')
38
+
39
+ original = image.copy()
40
+ tensor = self.transform(image)
41
+ return tensor, original
42
+
43
+ def process_batch(self, images: List[Union[str, Path, Image.Image]]) -> Tuple[torch.Tensor, List[Image.Image]]:
44
+ """Preprocess batch of images."""
45
+ tensors = []
46
+ originals = []
47
+
48
+ for img in images:
49
+ tensor, original = self.process(img)
50
+ tensors.append(tensor)
51
+ originals.append(original)
52
+
53
+ return torch.stack(tensors), originals
54
+
55
+
56
+ def preprocess_images(
57
+ input_path: Union[str, Path],
58
+ output_path: Union[str, Path],
59
+ size: int = 1024,
60
+ normalize: bool = True,
61
+ ) -> None:
62
+ """
63
+ Preprocess images from input directory to output directory.
64
+
65
+ Args:
66
+ input_path: Path to input image or directory
67
+ output_path: Path to output directory
68
+ size: Target size for resizing
69
+ normalize: Whether to apply ImageNet normalization
70
+ """
71
+ input_path = Path(input_path)
72
+ output_path = Path(output_path)
73
+ output_path.mkdir(parents=True, exist_ok=True)
74
+
75
+ preprocessor = Preprocessor(size=size, normalize=normalize)
76
+
77
+ # Get input files
78
+ if input_path.is_file():
79
+ files = [input_path]
80
+ else:
81
+ files = list(input_path.glob("*.jpg")) + list(input_path.glob("*.jpeg")) + list(input_path.glob("*.png"))
82
+
83
+ for file in files:
84
+ try:
85
+ tensor, original = preprocessor.process(file)
86
+ # Save preprocessed tensor as numpy for pipeline
87
+ np.save(output_path / f"{file.stem}_tensor.npy", tensor.numpy())
88
+ # Save original for reference
89
+ original.save(output_path / f"{file.stem}_original.png")
90
+ print(f" Preprocessed: {file.name}")
91
+ except Exception as e:
92
+ print(f" Error processing {file.name}: {e}")
93
+
94
+
95
+ if __name__ == "__main__":
96
+ import sys
97
+ if len(sys.argv) < 3:
98
+ print("Usage: python preprocess.py <input> <output> [--size 1024]")
99
+ sys.exit(1)
100
+
101
+ input_arg = sys.argv[1]
102
+ output_arg = sys.argv[2]
103
+ size = 1024
104
+
105
+ if "--size" in sys.argv:
106
+ idx = sys.argv.index("--size")
107
+ size = int(sys.argv[idx + 1])
108
+
109
+ preprocess_images(input_arg, output_arg, size=size)
rmbg/tools/remove_bg.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Background removal tool for RMBG pipeline."""
2
+
3
+ from pathlib import Path
4
+ from typing import Union, List, Optional, Tuple
5
+ from PIL import Image
6
+ import torch
7
+ import numpy as np
8
+
9
+ from ..backends import BaseBackend, get_fastest_backend, get_backend
10
+
11
+
12
+ class BackgroundRemover:
13
+ """Background removal with configurable backend."""
14
+
15
+ def __init__(self, backend: Optional[BaseBackend] = None, backend_name: Optional[str] = None, verbose: bool = False, warmup: bool = False):
16
+ """
17
+ Initialize background remover.
18
+
19
+ Args:
20
+ backend: Pre-initialized backend instance
21
+ backend_name: Name of backend to use ('tensorrt', 'huggingface', 'hf')
22
+ verbose: Show detailed logs
23
+ warmup: Warmup backend before processing
24
+ """
25
+ self.verbose = verbose
26
+
27
+ if backend is not None:
28
+ self.backend = backend
29
+ elif backend_name is not None:
30
+ self.backend = get_backend(backend_name)
31
+ else:
32
+ self.backend = get_fastest_backend()
33
+
34
+ # Load backend with warmup only if both verbose and warmup are True
35
+ if hasattr(self.backend, 'load'):
36
+ if verbose and warmup:
37
+ self.backend.load(warmup=True)
38
+ else:
39
+ self.backend.load(warmup=False)
40
+ else:
41
+ self.backend.load()
42
+
43
+ def process(
44
+ self,
45
+ image: Union[Image.Image, torch.Tensor, np.ndarray, str, Path],
46
+ return_mask: bool = False,
47
+ return_time: bool = False,
48
+ ) -> Union[Image.Image, Tuple[Image.Image, Image.Image], Tuple[Image.Image, float], Tuple[Image.Image, Image.Image, float]]:
49
+ """
50
+ Remove background from image.
51
+
52
+ Args:
53
+ image: Input image (PIL, tensor, numpy array, or path)
54
+ return_mask: If True, also return the mask
55
+ return_time: If True, also return inference time
56
+
57
+ Returns:
58
+ Image with transparent background, optionally with mask and/or time
59
+ """
60
+ # Handle path input
61
+ if isinstance(image, (str, Path)):
62
+ image = Image.open(image).convert('RGB')
63
+
64
+ # Get original size for PIL images
65
+ if isinstance(image, Image.Image):
66
+ original_size = image.size
67
+ else:
68
+ original_size = None
69
+
70
+ # Get prediction from backend with timing if supported
71
+ inference_time = None
72
+ if hasattr(self.backend, 'predict') and return_time:
73
+ try:
74
+ pred, inference_time = self.backend.predict(image, return_time=True)
75
+ except TypeError:
76
+ # Backend doesn't support return_time
77
+ pred = self.backend.predict(image)
78
+ else:
79
+ pred = self.backend.predict(image)
80
+
81
+ # Convert prediction to mask
82
+ if isinstance(pred, torch.Tensor):
83
+ mask = pred.squeeze().cpu().numpy()
84
+ else:
85
+ mask = pred.squeeze()
86
+
87
+ # Scale to 0-255
88
+ mask = (mask * 255).astype(np.uint8)
89
+ mask_img = Image.fromarray(mask, mode='L')
90
+
91
+ # Resize mask to original size if needed
92
+ if original_size and mask_img.size != original_size:
93
+ mask_img = mask_img.resize(original_size, Image.BILINEAR)
94
+
95
+ # Apply mask to original image
96
+ if isinstance(image, Image.Image):
97
+ result = image.convert('RGBA')
98
+ result.putalpha(mask_img)
99
+ else:
100
+ raise ValueError("For tensor/numpy input, provide original PIL image separately")
101
+
102
+ # Build return tuple based on flags
103
+ if return_mask and return_time:
104
+ return result, mask_img, inference_time
105
+ elif return_mask:
106
+ return result, mask_img
107
+ elif return_time:
108
+ return result, inference_time
109
+ return result
110
+
111
+ def process_batch(
112
+ self,
113
+ images: List[Union[Image.Image, str, Path]],
114
+ ) -> List[Image.Image]:
115
+ """Remove background from batch of images."""
116
+ results = []
117
+ for img in images:
118
+ try:
119
+ result = self.process(img)
120
+ results.append(result)
121
+ except Exception as e:
122
+ print(f" Error processing image: {e}")
123
+ results.append(None)
124
+ return results
125
+
126
+ def unload(self):
127
+ """Unload backend to free memory."""
128
+ self.backend.unload()
129
+
130
+
131
+ def remove_background(
132
+ input_path: Union[str, Path],
133
+ output_path: Union[str, Path],
134
+ backend: Optional[str] = None,
135
+ save_masks: bool = False,
136
+ ) -> None:
137
+ """
138
+ Remove background from images.
139
+
140
+ Args:
141
+ input_path: Path to input image or directory
142
+ output_path: Path to output directory
143
+ backend: Backend name ('tensorrt', 'huggingface', 'hf')
144
+ save_masks: Whether to also save individual mask files
145
+ """
146
+ input_path = Path(input_path)
147
+ output_path = Path(output_path)
148
+ output_path.mkdir(parents=True, exist_ok=True)
149
+
150
+ # Initialize remover
151
+ backend_name = backend or "tensorrt"
152
+ print(f"Using backend: {backend_name}")
153
+ remover = BackgroundRemover(backend_name=backend_name)
154
+
155
+ # Get input files
156
+ if input_path.is_file():
157
+ files = [input_path]
158
+ else:
159
+ files = list(input_path.glob("*.jpg")) + list(input_path.glob("*.jpeg")) + list(input_path.glob("*.png"))
160
+
161
+ print(f"Processing {len(files)} images...")
162
+ for file in files:
163
+ try:
164
+ result = remover.process(file)
165
+ output_file = output_path / f"{Path(file).stem}.png"
166
+ result.save(output_file)
167
+ print(f" Saved: {output_file.name}")
168
+ except Exception as e:
169
+ print(f" Error processing {file.name}: {e}")
170
+
171
+ remover.unload()
172
+ print(f"\nResults saved to: {output_path}")
173
+
174
+
175
+ if __name__ == "__main__":
176
+ import sys
177
+ if len(sys.argv) < 3:
178
+ print("Usage: python remove_bg.py <input> <output> [--backend tensorrt|hf]")
179
+ sys.exit(1)
180
+
181
+ input_arg = sys.argv[1]
182
+ output_arg = sys.argv[2]
183
+ backend = None
184
+
185
+ if "--backend" in sys.argv:
186
+ idx = sys.argv.index("--backend")
187
+ backend = sys.argv[idx + 1]
188
+
189
+ remove_background(input_arg, output_arg, backend=backend)
rmbg/utils/__init__.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility functions for RMBG."""
2
+
3
+ from pathlib import Path
4
+ from typing import List, Union
5
+ from PIL import Image
6
+
7
+
8
+ def get_image_files(path: Union[str, Path]) -> List[Path]:
9
+ """Get list of image files from path."""
10
+ path = Path(path)
11
+
12
+ if path.is_file():
13
+ return [path]
14
+
15
+ extensions = ('*.jpg', '*.jpeg', '*.png', '*.webp', '*.bmp', '*.gif')
16
+ files = []
17
+ for ext in extensions:
18
+ files.extend(path.glob(ext))
19
+
20
+ return sorted(files)
21
+
22
+
23
+ def load_image(path: Union[str, Path]) -> Image.Image:
24
+ """Load image and convert to RGB."""
25
+ return Image.open(path).convert('RGB')
26
+
27
+
28
+ def save_image(
29
+ image: Image.Image,
30
+ path: Union[str, Path],
31
+ format: str = "png",
32
+ quality: int = 95,
33
+ ) -> None:
34
+ """Save image with specified format and quality."""
35
+ path = Path(path)
36
+ path.parent.mkdir(parents=True, exist_ok=True)
37
+
38
+ if format.lower() in ("jpg", "jpeg"):
39
+ # Handle RGBA -> RGB conversion for JPEG
40
+ if image.mode == 'RGBA':
41
+ rgb = Image.new('RGB', image.size, (255, 255, 255))
42
+ rgb.paste(image, mask=image.split()[3])
43
+ image = rgb
44
+ image.save(path, 'JPEG', quality=quality, optimize=True)
45
+ elif format.lower() == "png":
46
+ image.save(path, 'PNG', optimize=True)
47
+ elif format.lower() == "webp":
48
+ image.save(path, 'WEBP', quality=quality, lossless=False)
49
+ else:
50
+ image.save(path)