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 +21 -0
- bgeraser-0.1.0/PKG-INFO +131 -0
- bgeraser-0.1.0/README.md +102 -0
- bgeraser-0.1.0/pyproject.toml +44 -0
- bgeraser-0.1.0/setup.cfg +4 -0
- bgeraser-0.1.0/src/bgeraser/__init__.py +26 -0
- bgeraser-0.1.0/src/bgeraser/cli.py +99 -0
- bgeraser-0.1.0/src/bgeraser/core.py +162 -0
- bgeraser-0.1.0/src/bgeraser/templates/index.html +271 -0
- bgeraser-0.1.0/src/bgeraser/web.py +69 -0
- bgeraser-0.1.0/src/bgeraser.egg-info/PKG-INFO +131 -0
- bgeraser-0.1.0/src/bgeraser.egg-info/SOURCES.txt +15 -0
- bgeraser-0.1.0/src/bgeraser.egg-info/dependency_links.txt +1 -0
- bgeraser-0.1.0/src/bgeraser.egg-info/entry_points.txt +2 -0
- bgeraser-0.1.0/src/bgeraser.egg-info/requires.txt +10 -0
- bgeraser-0.1.0/src/bgeraser.egg-info/top_level.txt +1 -0
- bgeraser-0.1.0/tests/test_bgeraser.py +84 -0
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.
|
bgeraser-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
bgeraser-0.1.0/README.md
ADDED
|
@@ -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"]
|
bgeraser-0.1.0/setup.cfg
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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
|