bgeraser 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bgeraser-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tavrej (Yonzon Studio)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: bgeraser
3
+ Version: 0.1.0
4
+ Summary: Remove image backgrounds locally — CLI, Python library and a tiny web UI. No uploads, no API keys.
5
+ Author-email: "Tavrej (Yonzon Studio)" <tavrejstudio@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/guesttavrej/bgeraser
8
+ Project-URL: Issues, https://github.com/guesttavrej/bgeraser/issues
9
+ Keywords: background removal,remove background,rembg,image,transparent png,cli
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Multimedia :: Graphics
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: rembg>=2.0.50
21
+ Requires-Dist: onnxruntime>=1.16
22
+ Requires-Dist: pillow>=10.0
23
+ Provides-Extra: web
24
+ Requires-Dist: flask>=3.0; extra == "web"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Requires-Dist: flask>=3.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # bgeraser
31
+
32
+ **Remove image backgrounds on your own computer.** No uploads, no API keys, no watermarks, no limits.
33
+
34
+ Works three ways:
35
+
36
+ | | |
37
+ |---|---|
38
+ | **Command line** | `bgeraser photo.jpg` → `photo-nobg.png` |
39
+ | **Python library** | `remove_background("photo.jpg")` |
40
+ | **Local web page** | `bgeraser serve` → drag & drop in your browser |
41
+
42
+ Powered by open-source [U²-Net](https://github.com/xuebinqin/U-2-Net) / ISNet models through [rembg](https://github.com/danielgatis/rembg).
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install bgeraser # CLI + library
48
+ pip install "bgeraser[web]" # also the drag & drop web page
49
+ ```
50
+
51
+ Needs Python 3.9+. The first run downloads the model (4 MB for the default `u2netp`; larger models download when you choose them).
52
+
53
+ ## Command line
54
+
55
+ ```bash
56
+ # one image → transparent PNG next to it
57
+ bgeraser photo.jpg
58
+
59
+ # choose the output name
60
+ bgeraser photo.jpg -o clean.png
61
+
62
+ # solid background instead of transparent (product photos, ID photos)
63
+ bgeraser photo.jpg --bg white
64
+ bgeraser photo.jpg --bg "#00ff00"
65
+
66
+ # a whole folder → ./products/nobg/
67
+ bgeraser ./products
68
+ bgeraser ./products -o ./out --recursive
69
+
70
+ # better model for people
71
+ bgeraser --model u2net_human_seg me.png
72
+
73
+ # finer edges for hair / fur (slower)
74
+ bgeraser photo.jpg --matting
75
+
76
+ # list models
77
+ bgeraser models
78
+ ```
79
+
80
+ ## Web page (drag & drop)
81
+
82
+ ```bash
83
+ bgeraser serve
84
+ ```
85
+
86
+ Then open <http://127.0.0.1:5000>. Drop, paste, or pick an image; download the PNG. Everything stays on your machine.
87
+
88
+ `bgeraser serve --port 8080 --model isnet-general-use` to change port or model.
89
+
90
+ ## Python
91
+
92
+ ```python
93
+ from bgeraser import remove_background, remove_background_bytes, process_folder
94
+
95
+ remove_background("photo.jpg") # -> photo-nobg.png
96
+ remove_background("photo.jpg", "out.png", bg_color="white")
97
+ remove_background("photo.jpg", model="isnet-general-use", alpha_matting=True)
98
+
99
+ png_bytes = remove_background_bytes(open("photo.jpg", "rb").read()) # in-memory
100
+
101
+ process_folder("./products", "./products/out") # batch
102
+ ```
103
+
104
+ ## Models
105
+
106
+ | name | best for |
107
+ |---|---|
108
+ | `u2netp` *(default)* | fast, small, everyday use |
109
+ | `u2net` | general, higher quality |
110
+ | `isnet-general-use` | often the cleanest edges |
111
+ | `u2net_human_seg` | people / portraits |
112
+ | `isnet-anime` | anime & illustrations |
113
+ | `u2net_cloth_seg` | clothing segmentation |
114
+ | `silueta` | u2net quality, smaller file |
115
+
116
+ ## Why
117
+
118
+ Online background removers are slow, watermark the result, charge per image, or make you upload customer photos to a stranger's server. This does the same job offline, for free, in one command.
119
+
120
+ ## Contributing
121
+
122
+ Issues and pull requests welcome. Run tests with:
123
+
124
+ ```bash
125
+ pip install -e ".[dev]"
126
+ pytest
127
+ ```
128
+
129
+ ## License
130
+
131
+ MIT © Tavrej / Yonzon Studio
@@ -0,0 +1,102 @@
1
+ # bgeraser
2
+
3
+ **Remove image backgrounds on your own computer.** No uploads, no API keys, no watermarks, no limits.
4
+
5
+ Works three ways:
6
+
7
+ | | |
8
+ |---|---|
9
+ | **Command line** | `bgeraser photo.jpg` → `photo-nobg.png` |
10
+ | **Python library** | `remove_background("photo.jpg")` |
11
+ | **Local web page** | `bgeraser serve` → drag & drop in your browser |
12
+
13
+ Powered by open-source [U²-Net](https://github.com/xuebinqin/U-2-Net) / ISNet models through [rembg](https://github.com/danielgatis/rembg).
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install bgeraser # CLI + library
19
+ pip install "bgeraser[web]" # also the drag & drop web page
20
+ ```
21
+
22
+ Needs Python 3.9+. The first run downloads the model (4 MB for the default `u2netp`; larger models download when you choose them).
23
+
24
+ ## Command line
25
+
26
+ ```bash
27
+ # one image → transparent PNG next to it
28
+ bgeraser photo.jpg
29
+
30
+ # choose the output name
31
+ bgeraser photo.jpg -o clean.png
32
+
33
+ # solid background instead of transparent (product photos, ID photos)
34
+ bgeraser photo.jpg --bg white
35
+ bgeraser photo.jpg --bg "#00ff00"
36
+
37
+ # a whole folder → ./products/nobg/
38
+ bgeraser ./products
39
+ bgeraser ./products -o ./out --recursive
40
+
41
+ # better model for people
42
+ bgeraser --model u2net_human_seg me.png
43
+
44
+ # finer edges for hair / fur (slower)
45
+ bgeraser photo.jpg --matting
46
+
47
+ # list models
48
+ bgeraser models
49
+ ```
50
+
51
+ ## Web page (drag & drop)
52
+
53
+ ```bash
54
+ bgeraser serve
55
+ ```
56
+
57
+ Then open <http://127.0.0.1:5000>. Drop, paste, or pick an image; download the PNG. Everything stays on your machine.
58
+
59
+ `bgeraser serve --port 8080 --model isnet-general-use` to change port or model.
60
+
61
+ ## Python
62
+
63
+ ```python
64
+ from bgeraser import remove_background, remove_background_bytes, process_folder
65
+
66
+ remove_background("photo.jpg") # -> photo-nobg.png
67
+ remove_background("photo.jpg", "out.png", bg_color="white")
68
+ remove_background("photo.jpg", model="isnet-general-use", alpha_matting=True)
69
+
70
+ png_bytes = remove_background_bytes(open("photo.jpg", "rb").read()) # in-memory
71
+
72
+ process_folder("./products", "./products/out") # batch
73
+ ```
74
+
75
+ ## Models
76
+
77
+ | name | best for |
78
+ |---|---|
79
+ | `u2netp` *(default)* | fast, small, everyday use |
80
+ | `u2net` | general, higher quality |
81
+ | `isnet-general-use` | often the cleanest edges |
82
+ | `u2net_human_seg` | people / portraits |
83
+ | `isnet-anime` | anime & illustrations |
84
+ | `u2net_cloth_seg` | clothing segmentation |
85
+ | `silueta` | u2net quality, smaller file |
86
+
87
+ ## Why
88
+
89
+ Online background removers are slow, watermark the result, charge per image, or make you upload customer photos to a stranger's server. This does the same job offline, for free, in one command.
90
+
91
+ ## Contributing
92
+
93
+ Issues and pull requests welcome. Run tests with:
94
+
95
+ ```bash
96
+ pip install -e ".[dev]"
97
+ pytest
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT © Tavrej / Yonzon Studio
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bgeraser"
7
+ version = "0.1.0"
8
+ description = "Remove image backgrounds locally — CLI, Python library and a tiny web UI. No uploads, no API keys."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Tavrej (Yonzon Studio)", email = "tavrejstudio@gmail.com" }]
13
+ keywords = ["background removal", "remove background", "rembg", "image", "transparent png", "cli"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Multimedia :: Graphics",
22
+ ]
23
+ dependencies = [
24
+ "rembg>=2.0.50",
25
+ "onnxruntime>=1.16",
26
+ "pillow>=10.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ web = ["flask>=3.0"]
31
+ dev = ["pytest>=7", "flask>=3.0"]
32
+
33
+ [project.scripts]
34
+ bgeraser = "bgeraser.cli:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/guesttavrej/bgeraser"
38
+ Issues = "https://github.com/guesttavrej/bgeraser/issues"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.setuptools.package-data]
44
+ bgeraser = ["templates/*.html"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ """bgeraser — remove image backgrounds locally.
2
+
3
+ >>> from bgeraser import remove_background
4
+ >>> remove_background("photo.jpg", "photo-nobg.png")
5
+
6
+ Everything runs on your machine using open-source U²-Net / ISNet models
7
+ via rembg. Nothing is uploaded anywhere.
8
+ """
9
+
10
+ from .core import (
11
+ MODELS,
12
+ DEFAULT_MODEL,
13
+ remove_background,
14
+ remove_background_bytes,
15
+ process_folder,
16
+ )
17
+
18
+ __version__ = "0.1.0"
19
+ __all__ = [
20
+ "MODELS",
21
+ "DEFAULT_MODEL",
22
+ "remove_background",
23
+ "remove_background_bytes",
24
+ "process_folder",
25
+ "__version__",
26
+ ]
@@ -0,0 +1,99 @@
1
+ """Command-line interface for bgeraser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from . import __version__
11
+ from .core import DEFAULT_MODEL, MODELS, process_folder, remove_background
12
+
13
+
14
+ def _build_parser() -> argparse.ArgumentParser:
15
+ p = argparse.ArgumentParser(
16
+ prog="bgeraser",
17
+ description="Remove image backgrounds locally. Works on a single file or a whole folder.",
18
+ epilog=(
19
+ "examples:\n"
20
+ " bgeraser photo.jpg -> photo-nobg.png\n"
21
+ " bgeraser photo.jpg -o clean.png\n"
22
+ " bgeraser photo.jpg --bg white -> white background instead of transparent\n"
23
+ " bgeraser ./products -o ./products/out -> every image in the folder\n"
24
+ " bgeraser --model u2net_human_seg me.png\n"
25
+ " bgeraser serve -> open a local web page (drag & drop)\n"
26
+ " bgeraser models -> list available models"
27
+ ),
28
+ formatter_class=argparse.RawDescriptionHelpFormatter,
29
+ )
30
+ p.add_argument("input", nargs="?", help="image file or folder, or 'serve' / 'models'")
31
+ p.add_argument("-o", "--output", help="output file (single image) or output folder")
32
+ p.add_argument(
33
+ "-m", "--model", default=DEFAULT_MODEL, choices=list(MODELS),
34
+ help=f"segmentation model (default: {DEFAULT_MODEL})",
35
+ )
36
+ p.add_argument("--bg", metavar="COLOR", help="replace background with a color, e.g. white, #00ff00, 255,0,0")
37
+ p.add_argument("--matting", action="store_true", help="alpha matting for finer edges (hair, fur). Slower")
38
+ p.add_argument("--post", action="store_true", help="post-process the mask for smoother results")
39
+ p.add_argument("-r", "--recursive", action="store_true", help="include sub-folders when input is a folder")
40
+ p.add_argument("--host", default="127.0.0.1", help="host for 'serve' (default 127.0.0.1)")
41
+ p.add_argument("--port", type=int, default=5000, help="port for 'serve' (default 5000)")
42
+ p.add_argument("-q", "--quiet", action="store_true", help="no progress output")
43
+ p.add_argument("-V", "--version", action="version", version=f"bgeraser {__version__}")
44
+ return p
45
+
46
+
47
+ def main(argv: list[str] | None = None) -> int:
48
+ parser = _build_parser()
49
+ args = parser.parse_args(argv)
50
+
51
+ if not args.input:
52
+ parser.print_help()
53
+ return 1
54
+
55
+ if args.input == "models":
56
+ for name, desc in MODELS.items():
57
+ mark = " (default)" if name == DEFAULT_MODEL else ""
58
+ print(f"{name:18s} {desc}{mark}")
59
+ return 0
60
+
61
+ if args.input == "serve":
62
+ from .web import serve
63
+
64
+ serve(host=args.host, port=args.port, model=args.model)
65
+ return 0
66
+
67
+ opts = dict(
68
+ model=args.model,
69
+ bg_color=args.bg,
70
+ alpha_matting=args.matting,
71
+ post_process=args.post,
72
+ )
73
+ src = Path(args.input)
74
+ if not src.exists():
75
+ print(f"error: {src} does not exist", file=sys.stderr)
76
+ return 2
77
+
78
+ t0 = time.time()
79
+ if src.is_dir():
80
+ def progress(i, o):
81
+ if not args.quiet:
82
+ print(f" {i.name} -> {o}")
83
+
84
+ written = process_folder(src, args.output, recursive=args.recursive, on_progress=progress, **opts)
85
+ if not written:
86
+ print("no images found", file=sys.stderr)
87
+ return 1
88
+ if not args.quiet:
89
+ print(f"done: {len(written)} image(s) in {time.time() - t0:.1f}s")
90
+ return 0
91
+
92
+ out = remove_background(src, args.output, **opts)
93
+ if not args.quiet:
94
+ print(f"{src.name} -> {out} ({time.time() - t0:.1f}s)")
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__": # pragma: no cover
99
+ sys.exit(main())
@@ -0,0 +1,162 @@
1
+ """Core background-removal functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ from pathlib import Path
7
+ from typing import Callable, Iterable, Optional, Union
8
+
9
+ from PIL import Image, ImageColor
10
+
11
+ PathLike = Union[str, Path]
12
+
13
+ #: Models available through rembg, smallest/fastest first.
14
+ MODELS = {
15
+ "u2netp": "Fast, small (4 MB). Good default for quick jobs.",
16
+ "u2net": "General purpose, higher quality (170 MB).",
17
+ "u2net_human_seg": "Tuned for people / portraits.",
18
+ "u2net_cloth_seg": "Segments clothing (upper, lower, full body).",
19
+ "isnet-general-use": "Newer general model, often best edges.",
20
+ "isnet-anime": "For anime / illustration characters.",
21
+ "silueta": "u2net quality at ~43 MB.",
22
+ "sam": "Segment Anything (needs prompts, advanced).",
23
+ }
24
+ DEFAULT_MODEL = "u2netp"
25
+
26
+ IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
27
+
28
+ _sessions: dict = {}
29
+
30
+
31
+ def _get_session(model: str):
32
+ """Load (and cache) a rembg model session."""
33
+ if model not in MODELS:
34
+ raise ValueError(f"Unknown model '{model}'. Choose from: {', '.join(MODELS)}")
35
+ if model not in _sessions:
36
+ from rembg import new_session # imported lazily: slow import
37
+
38
+ _sessions[model] = new_session(model)
39
+ return _sessions[model]
40
+
41
+
42
+ def _parse_color(color: Optional[str]):
43
+ """'#ffffff', 'white', '255,0,0' or None -> RGB tuple or None."""
44
+ if not color:
45
+ return None
46
+ color = color.strip()
47
+ if "," in color:
48
+ parts = [int(p) for p in color.split(",")]
49
+ return tuple(parts[:3])
50
+ return ImageColor.getrgb(color)
51
+
52
+
53
+ def remove_background_image(
54
+ image: Image.Image,
55
+ *,
56
+ model: str = DEFAULT_MODEL,
57
+ bg_color: Optional[str] = None,
58
+ alpha_matting: bool = False,
59
+ post_process: bool = False,
60
+ ) -> Image.Image:
61
+ """Remove the background from a PIL image and return an RGBA (or RGB) image.
62
+
63
+ Args:
64
+ image: Source image.
65
+ model: One of :data:`MODELS`.
66
+ bg_color: Optional replacement background ("white", "#00ff00", "255,0,0").
67
+ If given, the result is flattened onto that color.
68
+ alpha_matting: Refine edges (hair, fur). Slower.
69
+ post_process: Apply rembg's mask post-processing (smoother mask).
70
+ """
71
+ from rembg import remove # lazy
72
+
73
+ session = _get_session(model)
74
+ result = remove(
75
+ image,
76
+ session=session,
77
+ alpha_matting=alpha_matting,
78
+ post_process_mask=post_process,
79
+ )
80
+ if not isinstance(result, Image.Image): # very old rembg returned bytes
81
+ result = Image.open(io.BytesIO(result))
82
+ result = result.convert("RGBA")
83
+
84
+ rgb = _parse_color(bg_color)
85
+ if rgb is not None:
86
+ background = Image.new("RGBA", result.size, rgb + (255,))
87
+ background.alpha_composite(result)
88
+ result = background.convert("RGB")
89
+ return result
90
+
91
+
92
+ def remove_background(
93
+ input_path: PathLike,
94
+ output_path: Optional[PathLike] = None,
95
+ **kwargs,
96
+ ) -> Path:
97
+ """Remove the background of the image at ``input_path``.
98
+
99
+ Writes a PNG (transparent unless ``bg_color`` is set) and returns its path.
100
+ If ``output_path`` is omitted, ``<name>-nobg.png`` is written next to the input.
101
+ """
102
+ input_path = Path(input_path)
103
+ if output_path is None:
104
+ output_path = input_path.with_name(f"{input_path.stem}-nobg.png")
105
+ output_path = Path(output_path)
106
+ output_path.parent.mkdir(parents=True, exist_ok=True)
107
+
108
+ with Image.open(input_path) as img:
109
+ img.load()
110
+ out = remove_background_image(img, **kwargs)
111
+
112
+ fmt = "PNG"
113
+ if output_path.suffix.lower() in {".jpg", ".jpeg"}:
114
+ fmt = "JPEG"
115
+ if out.mode == "RGBA": # JPEG cannot hold transparency
116
+ flat = Image.new("RGB", out.size, (255, 255, 255))
117
+ flat.paste(out, mask=out.split()[3])
118
+ out = flat
119
+ elif output_path.suffix.lower() == ".webp":
120
+ fmt = "WEBP"
121
+ out.save(output_path, fmt)
122
+ return output_path
123
+
124
+
125
+ def remove_background_bytes(data: bytes, **kwargs) -> bytes:
126
+ """Same as :func:`remove_background` but in-memory. Returns PNG bytes."""
127
+ with Image.open(io.BytesIO(data)) as img:
128
+ img.load()
129
+ out = remove_background_image(img, **kwargs)
130
+ buf = io.BytesIO()
131
+ out.save(buf, "PNG")
132
+ return buf.getvalue()
133
+
134
+
135
+ def iter_images(folder: PathLike, recursive: bool = False) -> Iterable[Path]:
136
+ folder = Path(folder)
137
+ pattern = "**/*" if recursive else "*"
138
+ for p in sorted(folder.glob(pattern)):
139
+ if p.is_file() and p.suffix.lower() in IMAGE_EXTS and not p.stem.endswith("-nobg"):
140
+ yield p
141
+
142
+
143
+ def process_folder(
144
+ input_dir: PathLike,
145
+ output_dir: Optional[PathLike] = None,
146
+ *,
147
+ recursive: bool = False,
148
+ on_progress: Optional[Callable[[Path, Path], None]] = None,
149
+ **kwargs,
150
+ ) -> list[Path]:
151
+ """Remove backgrounds from every image in a folder. Returns output paths."""
152
+ input_dir = Path(input_dir)
153
+ output_dir = Path(output_dir) if output_dir else input_dir / "nobg"
154
+ written: list[Path] = []
155
+ for src in iter_images(input_dir, recursive=recursive):
156
+ rel = src.relative_to(input_dir)
157
+ dst = (output_dir / rel).with_suffix(".png")
158
+ remove_background(src, dst, **kwargs)
159
+ written.append(dst)
160
+ if on_progress:
161
+ on_progress(src, dst)
162
+ return written
@@ -0,0 +1,271 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>bgeraser — remove image backgrounds, offline</title>
7
+ <meta name="color-scheme" content="dark light">
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%235b8cff'/%3E%3Cpath d='M9 22 L19 9 l4 3 -10 13z' fill='%23fff'/%3E%3Crect x='8' y='23' width='16' height='2' rx='1' fill='%23fff' opacity='.7'/%3E%3C/svg%3E">
9
+ <style>
10
+ :root {
11
+ --bg:#0b0d12; --bg2:#10131a; --card:#151922; --card2:#1b2030; --line:#262c3a; --line2:#334;
12
+ --text:#eef0f5; --muted:#8d94a6; --dim:#5f6678;
13
+ --accent:#5b8cff; --accent2:#8b5cf6; --ok:#34d399; --err:#fb7185;
14
+ --shadow:0 20px 60px -20px rgba(0,0,0,.6);
15
+ --check-a:#2a3040; --check-b:#1c2130;
16
+ }
17
+ @media (prefers-color-scheme: light) {
18
+ :root {
19
+ --bg:#f6f7fb; --bg2:#eef0f6; --card:#ffffff; --card2:#f2f4f9; --line:#e3e6ee; --line2:#cfd4e0;
20
+ --text:#11131a; --muted:#5b6172; --dim:#9aa0b0;
21
+ --shadow:0 20px 50px -24px rgba(20,30,60,.25);
22
+ --check-a:#e2e5ee; --check-b:#f7f8fb;
23
+ }
24
+ }
25
+ * { box-sizing:border-box; }
26
+ html, body { margin:0; }
27
+ body {
28
+ font:15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
29
+ background:
30
+ radial-gradient(900px 500px at 10% -10%, rgba(91,140,255,.18), transparent 60%),
31
+ radial-gradient(700px 400px at 100% 0%, rgba(139,92,246,.14), transparent 60%),
32
+ var(--bg);
33
+ color:var(--text); min-height:100vh; -webkit-font-smoothing:antialiased;
34
+ }
35
+ a { color:inherit; }
36
+ .wrap { max-width:1080px; margin:0 auto; padding:28px 20px 60px; }
37
+
38
+ /* header */
39
+ header { display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:28px; }
40
+ .brand { display:flex; align-items:center; gap:10px; font-weight:700; font-size:18px; letter-spacing:-.01em; }
41
+ .logo { width:30px; height:30px; border-radius:9px; background:linear-gradient(135deg,var(--accent),var(--accent2)); display:grid; place-items:center; box-shadow:0 6px 18px -6px var(--accent); }
42
+ .logo svg { width:18px; height:18px; }
43
+ .pill { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--muted); border:1px solid var(--line); background:var(--card); padding:5px 10px; border-radius:999px; }
44
+ .pill i { width:7px; height:7px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 3px rgba(52,211,153,.18); }
45
+
46
+ /* hero */
47
+ .hero { text-align:center; margin:8px 0 26px; }
48
+ .hero h1 { font-size:clamp(28px,4.6vw,44px); line-height:1.1; margin:0 0 10px; letter-spacing:-.03em; font-weight:800; }
49
+ .hero h1 span { background:linear-gradient(90deg,var(--accent),var(--accent2)); -webkit-background-clip:text; background-clip:text; color:transparent; }
50
+ .hero p { margin:0 auto; color:var(--muted); max-width:560px; font-size:16px; }
51
+
52
+ /* drop zone */
53
+ .drop {
54
+ position:relative; display:block; border:1.5px dashed var(--line2); border-radius:18px; padding:38px 20px;
55
+ text-align:center; cursor:pointer; background:var(--card); transition:border-color .15s, background .15s, transform .15s;
56
+ box-shadow:var(--shadow);
57
+ }
58
+ .drop:hover, .drop.over { border-color:var(--accent); background:var(--card2); }
59
+ .drop.over { transform:scale(1.005); }
60
+ .drop input { display:none; }
61
+ .drop .ic { width:52px; height:52px; margin:0 auto 12px; border-radius:14px; background:linear-gradient(135deg,rgba(91,140,255,.18),rgba(139,92,246,.18)); display:grid; place-items:center; }
62
+ .drop .ic svg { width:26px; height:26px; stroke:var(--accent); }
63
+ .drop strong { font-size:17px; }
64
+ .drop .sub { color:var(--muted); margin-top:4px; font-size:14px; }
65
+ .drop kbd { font:12px ui-monospace, SFMono-Regular, Menlo, monospace; border:1px solid var(--line2); border-radius:6px; padding:1px 6px; color:var(--muted); }
66
+
67
+ /* options */
68
+ .opts { display:flex; flex-wrap:wrap; gap:10px 18px; align-items:center; margin:18px 0 6px; }
69
+ .opts label { display:flex; align-items:center; gap:8px; color:var(--muted); font-size:14px; }
70
+ select, input[type=text] {
71
+ font:inherit; background:var(--card); color:var(--text); border:1px solid var(--line); border-radius:10px; padding:8px 12px; outline:none;
72
+ }
73
+ select:focus, input[type=text]:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(91,140,255,.18); }
74
+ .swatches { display:flex; gap:6px; align-items:center; }
75
+ .sw { width:28px; height:28px; border-radius:8px; border:2px solid var(--line); cursor:pointer; padding:0; position:relative; background:var(--card); }
76
+ .sw.on { border-color:var(--accent); box-shadow:0 0 0 3px rgba(91,140,255,.18); }
77
+ .sw[data-bg=""] { background: repeating-conic-gradient(var(--check-a) 0% 25%, var(--check-b) 0% 50%) 50% / 12px 12px; }
78
+ .sw input[type=color] { position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%; }
79
+ .toggle { position:relative; width:38px; height:22px; background:var(--line2); border-radius:999px; transition:.15s; flex:none; }
80
+ .toggle::after { content:""; position:absolute; top:3px; left:3px; width:16px; height:16px; border-radius:50%; background:#fff; transition:.15s; }
81
+ input:checked + .toggle { background:var(--accent); }
82
+ input:checked + .toggle::after { transform:translateX(16px); }
83
+ .opts input[type=checkbox] { display:none; }
84
+
85
+ .status { min-height:24px; margin:10px 0 14px; color:var(--muted); font-size:14px; display:flex; align-items:center; gap:10px; }
86
+ .status.err { color:var(--err); }
87
+ .status.ok { color:var(--ok); }
88
+ .spin { width:16px; height:16px; border:2px solid var(--line2); border-top-color:var(--accent); border-radius:50%; animation:sp .8s linear infinite; }
89
+ @keyframes sp { to { transform:rotate(360deg); } }
90
+
91
+ /* result */
92
+ .result { display:none; }
93
+ .result.show { display:block; animation:up .35s ease; }
94
+ @keyframes up { from { opacity:0; transform:translateY(10px); } }
95
+ .cmp {
96
+ position:relative; border-radius:18px; overflow:hidden; border:1px solid var(--line); box-shadow:var(--shadow);
97
+ background: repeating-conic-gradient(var(--check-a) 0% 25%, var(--check-b) 0% 50%) 50% / 22px 22px;
98
+ user-select:none; touch-action:none; width:fit-content; max-width:100%; margin:0 auto;
99
+ }
100
+ .cmp img { display:block; width:auto; max-width:100%; height:auto; max-height:70vh; }
101
+ .cmp .before { position:absolute; inset:0; overflow:hidden; background:var(--card); }
102
+ .cmp .before img { position:absolute; left:0; top:0; max-width:none; max-height:none; }
103
+ .cmp .handle { position:absolute; top:0; bottom:0; width:2px; background:#fff; box-shadow:0 0 0 1px rgba(0,0,0,.25); }
104
+ .cmp .knob { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:40px; height:40px; border-radius:50%; background:#fff; display:grid; place-items:center; box-shadow:0 6px 20px rgba(0,0,0,.35); color:#111; }
105
+ .cmp .tag { position:absolute; top:12px; font-size:11px; font-weight:700; letter-spacing:.08em; padding:4px 9px; border-radius:6px; background:rgba(0,0,0,.55); color:#fff; backdrop-filter:blur(6px); }
106
+ .cmp .tag.l { left:12px; } .cmp .tag.r { right:12px; }
107
+ .actions { display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-top:14px; }
108
+ .btn { display:inline-flex; align-items:center; gap:8px; font:inherit; font-weight:600; border:0; cursor:pointer; padding:11px 18px; border-radius:12px; text-decoration:none; transition:.15s; }
109
+ .btn.p { background:linear-gradient(135deg,var(--accent),var(--accent2)); color:#fff; box-shadow:0 10px 24px -10px var(--accent); }
110
+ .btn.p:hover { transform:translateY(-1px); filter:brightness(1.05); }
111
+ .btn.s { background:var(--card); color:var(--text); border:1px solid var(--line); }
112
+ .btn.s:hover { border-color:var(--line2); }
113
+ .btn svg { width:16px; height:16px; }
114
+ .meta { color:var(--dim); font-size:13px; margin-left:auto; }
115
+
116
+ /* features */
117
+ .feats { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-top:34px; }
118
+ .feat { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:16px; }
119
+ .feat b { display:block; margin-bottom:4px; }
120
+ .feat span { color:var(--muted); font-size:14px; }
121
+ footer { margin-top:34px; color:var(--dim); font-size:13px; text-align:center; }
122
+ footer a { color:var(--muted); }
123
+ @media (max-width:720px) { .feats { grid-template-columns:1fr; } .pill span { display:none; } .wrap { padding:20px 16px 40px; } .drop { padding:28px 14px; } .meta { margin-left:0; width:100%; } }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div class="wrap">
128
+ <header>
129
+ <div class="brand">
130
+ <div class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18 15 5l4 3L9 21H5z"/><path d="M4 21h16"/></svg></div>
131
+ bgeraser
132
+ </div>
133
+ <span class="pill"><i></i> offline<span> · nothing leaves this computer</span></span>
134
+ </header>
135
+
136
+ <section class="hero">
137
+ <h1>Remove backgrounds.<br><span>Offline. Free. Unlimited.</span></h1>
138
+ <p>Drop a photo and get a clean transparent PNG in seconds — no uploads, no watermarks, no sign-up.</p>
139
+ </section>
140
+
141
+ <label class="drop" id="drop">
142
+ <input type="file" id="file" accept="image/*">
143
+ <div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4M7 9l5-5 5 5"/><path d="M4 16v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3"/></svg></div>
144
+ <strong>Drop an image here</strong> or click to choose
145
+ <div class="sub">JPG · PNG · WEBP · up to 25 MB — or press <kbd>Ctrl</kbd>+<kbd>V</kbd> to paste</div>
146
+ </label>
147
+
148
+ <div class="opts">
149
+ <label>Model
150
+ <select id="model">
151
+ {% for name, desc in models.items() %}
152
+ <option value="{{ name }}" {% if name == default_model %}selected{% endif %}>{{ name }} — {{ desc }}</option>
153
+ {% endfor %}
154
+ </select>
155
+ </label>
156
+ <label>Background
157
+ <span class="swatches" id="swatches">
158
+ <button type="button" class="sw on" data-bg="" title="Transparent"></button>
159
+ <button type="button" class="sw" data-bg="#ffffff" style="background:#fff" title="White"></button>
160
+ <button type="button" class="sw" data-bg="#000000" style="background:#000" title="Black"></button>
161
+ <button type="button" class="sw" data-bg="#00ff00" style="background:#00ff00" title="Green screen"></button>
162
+ <span class="sw" id="customSw" title="Custom color" style="background:conic-gradient(red,yellow,lime,cyan,blue,magenta,red)"><input type="color" id="customColor" value="#5b8cff"></span>
163
+ </span>
164
+ </label>
165
+ <label><input type="checkbox" id="matting"><span class="toggle"></span> Fine edges (hair, fur — slower)</label>
166
+ </div>
167
+
168
+ <div class="status" id="status">Best on photos with one clear subject — a person, product, pet or logo.</div>
169
+
170
+ <section class="result" id="result">
171
+ <div class="cmp" id="cmp" style="--w:50%">
172
+ <img id="after" alt="Result">
173
+ <div class="before" id="beforeWrap" style="width:50%"><img id="before" alt="Original"></div>
174
+ <div class="handle" id="handle" style="left:50%"><div class="knob"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M8 7l-5 5 5 5M16 7l5 5-5 5"/></svg></div></div>
175
+ <span class="tag l">BEFORE</span><span class="tag r">AFTER</span>
176
+ </div>
177
+ <div class="actions">
178
+ <a class="btn p" id="dl" download><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v12M7 11l5 5 5-5"/><path d="M4 20h16"/></svg>Download PNG</a>
179
+ <button class="btn s" id="again" type="button">Another image</button>
180
+ <span class="meta" id="meta"></span>
181
+ </div>
182
+ </section>
183
+
184
+ <section class="feats">
185
+ <div class="feat"><b>100% private</b><span>The AI model runs on this machine. Your photos never touch the internet.</span></div>
186
+ <div class="feat"><b>No limits</b><span>Process one image or a thousand. No credits, no watermark, no subscription.</span></div>
187
+ <div class="feat"><b>Batch from the terminal</b><span><code>bgeraser ./folder</code> clears every photo in a folder in one command.</span></div>
188
+ </section>
189
+
190
+ <footer>Open source · MIT · <a href="https://github.com/guesttavrej/bgeraser" target="_blank" rel="noopener">github.com/guesttavrej/bgeraser</a></footer>
191
+ </div>
192
+
193
+ <script>
194
+ const $ = id => document.getElementById(id);
195
+ const drop = $('drop'), file = $('file'), status = $('status'), result = $('result');
196
+ const before = $('before'), after = $('after'), beforeWrap = $('beforeWrap'), handle = $('handle'), cmp = $('cmp');
197
+ const dl = $('dl'), meta = $('meta');
198
+ let bg = '', currentFile = null, busy = false;
199
+
200
+ /* background swatches */
201
+ $('swatches').addEventListener('click', e => {
202
+ const b = e.target.closest('.sw'); if (!b || b.id === 'customSw') return;
203
+ document.querySelectorAll('.sw').forEach(s => s.classList.remove('on')); b.classList.add('on');
204
+ bg = b.dataset.bg; if (currentFile && !busy) run(currentFile);
205
+ });
206
+ $('customColor').addEventListener('input', e => {
207
+ document.querySelectorAll('.sw').forEach(s => s.classList.remove('on')); $('customSw').classList.add('on');
208
+ $('customSw').style.background = e.target.value; bg = e.target.value;
209
+ });
210
+ $('customColor').addEventListener('change', () => { if (currentFile && !busy) run(currentFile); });
211
+ $('model').addEventListener('change', () => { if (currentFile && !busy) run(currentFile); });
212
+ $('matting').addEventListener('change', () => { if (currentFile && !busy) run(currentFile); });
213
+
214
+ /* input methods */
215
+ ['dragenter','dragover'].forEach(t => document.addEventListener(t, e => { e.preventDefault(); drop.classList.add('over'); }));
216
+ ['dragleave','drop'].forEach(t => document.addEventListener(t, e => { e.preventDefault(); if (t === 'drop' || e.target === document.documentElement) drop.classList.remove('over'); }));
217
+ document.addEventListener('drop', e => { const f = e.dataTransfer.files[0]; if (f) run(f); });
218
+ file.addEventListener('change', () => { if (file.files[0]) run(file.files[0]); file.value = ''; });
219
+ window.addEventListener('paste', e => { const f = [...(e.clipboardData?.files || [])][0]; if (f) run(f); });
220
+ $('again').addEventListener('click', () => { result.classList.remove('show'); currentFile = null; setStatus('Drop the next image.'); file.click(); });
221
+
222
+ /* comparison slider */
223
+ function setSplit(x) {
224
+ const r = cmp.getBoundingClientRect(); const p = Math.max(0, Math.min(1, (x - r.left) / r.width)) * 100;
225
+ beforeWrap.style.width = p + '%'; handle.style.left = p + '%';
226
+ }
227
+ let dragging = false;
228
+ cmp.addEventListener('pointerdown', e => { dragging = true; cmp.setPointerCapture(e.pointerId); setSplit(e.clientX); });
229
+ cmp.addEventListener('pointermove', e => { if (dragging) setSplit(e.clientX); });
230
+ cmp.addEventListener('pointerup', () => dragging = false);
231
+ cmp.addEventListener('pointercancel', () => dragging = false);
232
+ function syncBefore() { before.style.width = after.clientWidth + 'px'; before.style.height = after.clientHeight + 'px'; }
233
+ window.addEventListener('resize', syncBefore);
234
+
235
+ function setStatus(msg, cls) { status.className = 'status' + (cls ? ' ' + cls : ''); status.innerHTML = msg; }
236
+
237
+ async function run(f) {
238
+ if (!f.type.startsWith('image/')) { setStatus('That is not an image file.', 'err'); return; }
239
+ currentFile = f; busy = true;
240
+ setStatus('<span class="spin"></span> Removing background…');
241
+ const slow = setTimeout(() => setStatus('<span class="spin"></span> Still working — large images and bigger models take longer.'), 8000);
242
+ const fd = new FormData();
243
+ fd.append('image', f); fd.append('model', $('model').value); fd.append('bg', bg);
244
+ if ($('matting').checked) fd.append('matting', 'on');
245
+ const t0 = performance.now();
246
+ try {
247
+ const r = await fetch('/remove', { method: 'POST', body: fd });
248
+ if (!r.ok) { const j = await r.json().catch(() => ({})); throw new Error(j.error || r.statusText); }
249
+ const blob = await r.blob(); const url = URL.createObjectURL(blob);
250
+ before.src = URL.createObjectURL(f); after.src = url;
251
+ await Promise.all([
252
+ new Promise(res => { after.complete && after.naturalWidth ? res() : (after.onload = res); }),
253
+ new Promise(res => { before.complete && before.naturalWidth ? res() : (before.onload = res); }),
254
+ ]);
255
+ result.classList.add('show');
256
+ syncBefore(); beforeWrap.style.width = '50%'; handle.style.left = '50%';
257
+ result.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
258
+ dl.href = url; dl.download = f.name.replace(/\.[^.]+$/, '') + '-nobg.png';
259
+ const s = ((performance.now() - t0) / 1000).toFixed(1);
260
+ meta.textContent = `${after.naturalWidth}×${after.naturalHeight} · ${(blob.size/1024).toFixed(0)} KB · ${s}s`;
261
+ setStatus(`Done in ${s}s — drag the slider to compare.`, 'ok');
262
+ } catch (e) {
263
+ const m = e.message === 'Failed to fetch'
264
+ ? 'Could not reach the server. Is the “bgeraser serve” window still open? Restart it and reload this page.'
265
+ : e.message;
266
+ setStatus('Error: ' + m, 'err');
267
+ } finally { clearTimeout(slow); busy = false; }
268
+ }
269
+ </script>
270
+ </body>
271
+ </html>
@@ -0,0 +1,69 @@
1
+ """Tiny local web UI: `bgeraser serve` then open http://127.0.0.1:5000."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from .core import DEFAULT_MODEL, MODELS, remove_background_bytes
8
+
9
+ TEMPLATE = (Path(__file__).parent / "templates" / "index.html").read_text(encoding="utf-8")
10
+
11
+
12
+ def create_app(model: str = DEFAULT_MODEL):
13
+ try:
14
+ from flask import Flask, Response, render_template_string, request
15
+ except ImportError as e: # pragma: no cover
16
+ raise SystemExit("The web UI needs Flask: pip install 'bgeraser[web]'") from e
17
+
18
+ app = Flask(__name__)
19
+ app.config["MAX_CONTENT_LENGTH"] = 25 * 1024 * 1024 # 25 MB
20
+
21
+ @app.get("/")
22
+ def index():
23
+ return render_template_string(TEMPLATE, models=MODELS, default_model=model)
24
+
25
+ @app.post("/remove")
26
+ def remove():
27
+ f = request.files.get("image")
28
+ if not f:
29
+ return {"error": "no image uploaded"}, 400
30
+ chosen = request.form.get("model") or model
31
+ bg = request.form.get("bg") or None
32
+ matting = request.form.get("matting") == "on"
33
+ try:
34
+ png = remove_background_bytes(f.read(), model=chosen, bg_color=bg, alpha_matting=matting)
35
+ except ValueError as e:
36
+ return {"error": str(e)}, 400
37
+ except Exception as e: # model download failure, bad image, etc.
38
+ msg = f"{type(e).__name__}: {e}"
39
+ print(f"[bgeraser] error: {msg}", flush=True)
40
+ return {"error": msg}, 500
41
+ stem = Path(f.filename or "image").stem
42
+ return Response(
43
+ png,
44
+ mimetype="image/png",
45
+ headers={"Content-Disposition": f'inline; filename="{stem}-nobg.png"'},
46
+ )
47
+
48
+ @app.get("/health")
49
+ def health():
50
+ return {"ok": True, "model": model}
51
+
52
+ return app
53
+
54
+
55
+ def serve(host: str = "127.0.0.1", port: int = 5000, model: str = DEFAULT_MODEL) -> None:
56
+ from .core import _get_session
57
+
58
+ print(f"loading model '{model}' (downloads on first run, please wait)...", flush=True)
59
+ try:
60
+ _get_session(model)
61
+ except Exception as e:
62
+ print(f"could not load model: {type(e).__name__}: {e}\n"
63
+ f"check your internet connection and run again.", flush=True)
64
+ raise SystemExit(1)
65
+ print("model ready", flush=True)
66
+
67
+ app = create_app(model)
68
+ print(f"bgeraser web UI -> http://{host}:{port} (model: {model}, Ctrl+C to stop)", flush=True)
69
+ app.run(host=host, port=port, debug=False, threaded=True)
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: bgeraser
3
+ Version: 0.1.0
4
+ Summary: Remove image backgrounds locally — CLI, Python library and a tiny web UI. No uploads, no API keys.
5
+ Author-email: "Tavrej (Yonzon Studio)" <tavrejstudio@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/guesttavrej/bgeraser
8
+ Project-URL: Issues, https://github.com/guesttavrej/bgeraser/issues
9
+ Keywords: background removal,remove background,rembg,image,transparent png,cli
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Multimedia :: Graphics
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: rembg>=2.0.50
21
+ Requires-Dist: onnxruntime>=1.16
22
+ Requires-Dist: pillow>=10.0
23
+ Provides-Extra: web
24
+ Requires-Dist: flask>=3.0; extra == "web"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Requires-Dist: flask>=3.0; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # bgeraser
31
+
32
+ **Remove image backgrounds on your own computer.** No uploads, no API keys, no watermarks, no limits.
33
+
34
+ Works three ways:
35
+
36
+ | | |
37
+ |---|---|
38
+ | **Command line** | `bgeraser photo.jpg` → `photo-nobg.png` |
39
+ | **Python library** | `remove_background("photo.jpg")` |
40
+ | **Local web page** | `bgeraser serve` → drag & drop in your browser |
41
+
42
+ Powered by open-source [U²-Net](https://github.com/xuebinqin/U-2-Net) / ISNet models through [rembg](https://github.com/danielgatis/rembg).
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install bgeraser # CLI + library
48
+ pip install "bgeraser[web]" # also the drag & drop web page
49
+ ```
50
+
51
+ Needs Python 3.9+. The first run downloads the model (4 MB for the default `u2netp`; larger models download when you choose them).
52
+
53
+ ## Command line
54
+
55
+ ```bash
56
+ # one image → transparent PNG next to it
57
+ bgeraser photo.jpg
58
+
59
+ # choose the output name
60
+ bgeraser photo.jpg -o clean.png
61
+
62
+ # solid background instead of transparent (product photos, ID photos)
63
+ bgeraser photo.jpg --bg white
64
+ bgeraser photo.jpg --bg "#00ff00"
65
+
66
+ # a whole folder → ./products/nobg/
67
+ bgeraser ./products
68
+ bgeraser ./products -o ./out --recursive
69
+
70
+ # better model for people
71
+ bgeraser --model u2net_human_seg me.png
72
+
73
+ # finer edges for hair / fur (slower)
74
+ bgeraser photo.jpg --matting
75
+
76
+ # list models
77
+ bgeraser models
78
+ ```
79
+
80
+ ## Web page (drag & drop)
81
+
82
+ ```bash
83
+ bgeraser serve
84
+ ```
85
+
86
+ Then open <http://127.0.0.1:5000>. Drop, paste, or pick an image; download the PNG. Everything stays on your machine.
87
+
88
+ `bgeraser serve --port 8080 --model isnet-general-use` to change port or model.
89
+
90
+ ## Python
91
+
92
+ ```python
93
+ from bgeraser import remove_background, remove_background_bytes, process_folder
94
+
95
+ remove_background("photo.jpg") # -> photo-nobg.png
96
+ remove_background("photo.jpg", "out.png", bg_color="white")
97
+ remove_background("photo.jpg", model="isnet-general-use", alpha_matting=True)
98
+
99
+ png_bytes = remove_background_bytes(open("photo.jpg", "rb").read()) # in-memory
100
+
101
+ process_folder("./products", "./products/out") # batch
102
+ ```
103
+
104
+ ## Models
105
+
106
+ | name | best for |
107
+ |---|---|
108
+ | `u2netp` *(default)* | fast, small, everyday use |
109
+ | `u2net` | general, higher quality |
110
+ | `isnet-general-use` | often the cleanest edges |
111
+ | `u2net_human_seg` | people / portraits |
112
+ | `isnet-anime` | anime & illustrations |
113
+ | `u2net_cloth_seg` | clothing segmentation |
114
+ | `silueta` | u2net quality, smaller file |
115
+
116
+ ## Why
117
+
118
+ Online background removers are slow, watermark the result, charge per image, or make you upload customer photos to a stranger's server. This does the same job offline, for free, in one command.
119
+
120
+ ## Contributing
121
+
122
+ Issues and pull requests welcome. Run tests with:
123
+
124
+ ```bash
125
+ pip install -e ".[dev]"
126
+ pytest
127
+ ```
128
+
129
+ ## License
130
+
131
+ MIT © Tavrej / Yonzon Studio
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/bgeraser/__init__.py
5
+ src/bgeraser/cli.py
6
+ src/bgeraser/core.py
7
+ src/bgeraser/web.py
8
+ src/bgeraser.egg-info/PKG-INFO
9
+ src/bgeraser.egg-info/SOURCES.txt
10
+ src/bgeraser.egg-info/dependency_links.txt
11
+ src/bgeraser.egg-info/entry_points.txt
12
+ src/bgeraser.egg-info/requires.txt
13
+ src/bgeraser.egg-info/top_level.txt
14
+ src/bgeraser/templates/index.html
15
+ tests/test_bgeraser.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bgeraser = bgeraser.cli:main
@@ -0,0 +1,10 @@
1
+ rembg>=2.0.50
2
+ onnxruntime>=1.16
3
+ pillow>=10.0
4
+
5
+ [dev]
6
+ pytest>=7
7
+ flask>=3.0
8
+
9
+ [web]
10
+ flask>=3.0
@@ -0,0 +1 @@
1
+ bgeraser
@@ -0,0 +1,84 @@
1
+ import io
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ from PIL import Image, ImageDraw
6
+
7
+ from bgeraser import remove_background, remove_background_bytes, process_folder, MODELS, DEFAULT_MODEL
8
+ from bgeraser.cli import main
9
+
10
+
11
+ @pytest.fixture
12
+ def sample(tmp_path: Path) -> Path:
13
+ """Red circle on a white background — an easy subject for the model."""
14
+ img = Image.new("RGB", (160, 160), "white")
15
+ ImageDraw.Draw(img).ellipse((30, 30, 130, 130), fill=(220, 30, 30))
16
+ p = tmp_path / "circle.jpg"
17
+ img.save(p, quality=95)
18
+ return p
19
+
20
+
21
+ def test_default_model_listed():
22
+ assert DEFAULT_MODEL in MODELS
23
+
24
+
25
+ def test_remove_creates_transparent_png(sample: Path):
26
+ out = remove_background(sample)
27
+ assert out.name == "circle-nobg.png"
28
+ with Image.open(out) as im:
29
+ assert im.mode == "RGBA"
30
+ assert im.size == (160, 160)
31
+ alpha = im.split()[3]
32
+ assert alpha.getpixel((2, 2)) < 40 # corner should be (mostly) transparent
33
+ assert alpha.getpixel((80, 80)) > 200 # centre of circle should be opaque
34
+
35
+
36
+ def test_bg_color_flattens(sample: Path, tmp_path: Path):
37
+ out = remove_background(sample, tmp_path / "flat.png", bg_color="#00ff00")
38
+ with Image.open(out) as im:
39
+ assert im.mode == "RGB"
40
+ assert im.getpixel((2, 2)) == (0, 255, 0)
41
+
42
+
43
+ def test_bytes_roundtrip(sample: Path):
44
+ png = remove_background_bytes(sample.read_bytes())
45
+ with Image.open(io.BytesIO(png)) as im:
46
+ assert im.format == "PNG" and im.mode == "RGBA"
47
+
48
+
49
+ def test_unknown_model(sample: Path):
50
+ with pytest.raises(ValueError):
51
+ remove_background(sample, model="does-not-exist")
52
+
53
+
54
+ def test_process_folder(sample: Path, tmp_path: Path):
55
+ (tmp_path / "second.png").write_bytes(sample.read_bytes())
56
+ written = process_folder(tmp_path, tmp_path / "out")
57
+ assert sorted(p.name for p in written) == ["circle.png", "second.png"]
58
+ assert all(p.exists() for p in written)
59
+
60
+
61
+ def test_cli_single(sample: Path, tmp_path: Path, capsys):
62
+ rc = main([str(sample), "-o", str(tmp_path / "cli.png"), "-q"])
63
+ assert rc == 0 and (tmp_path / "cli.png").exists()
64
+
65
+
66
+ def test_cli_models(capsys):
67
+ assert main(["models"]) == 0
68
+ assert DEFAULT_MODEL in capsys.readouterr().out
69
+
70
+
71
+ def test_cli_missing_file(tmp_path: Path):
72
+ assert main([str(tmp_path / "nope.jpg")]) == 2
73
+
74
+
75
+ def test_web_app(sample: Path):
76
+ flask = pytest.importorskip("flask")
77
+ from bgeraser.web import create_app
78
+
79
+ client = create_app().test_client()
80
+ assert client.get("/health").json["ok"] is True
81
+ assert b"bgeraser" in client.get("/").data
82
+ r = client.post("/remove", data={"image": (io.BytesIO(sample.read_bytes()), "circle.jpg")})
83
+ assert r.status_code == 200 and r.mimetype == "image/png"
84
+ assert client.post("/remove", data={}).status_code == 400