Files changed (7) hide show
  1. .gitattributes +34 -0
  2. .gitignore +33 -2
  3. Dockerfile +0 -25
  4. README.md +10 -24
  5. app.py +3 -337
  6. main.py +53 -0
  7. requirements.txt +4 -5
.gitattributes ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tflite filter=lfs diff=lfs merge=lfs -text
29
+ *.tgz filter=lfs diff=lfs merge=lfs -text
30
+ *.wasm filter=lfs diff=lfs merge=lfs -text
31
+ *.xz filter=lfs diff=lfs merge=lfs -text
32
+ *.zip filter=lfs diff=lfs merge=lfs -text
33
+ *.zst filter=lfs diff=lfs merge=lfs -text
34
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,4 +1,35 @@
1
- .env
2
  __pycache__/
3
  *.py[cod]
4
- .venv/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
  __pycache__/
3
  *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+ *.swo
32
+
33
+ # OS specific
34
+ .DS_Store
35
+ Thumbs.db
Dockerfile DELETED
@@ -1,25 +0,0 @@
1
- FROM python:3.11-slim
2
-
3
- ENV PYTHONDONTWRITEBYTECODE=1 \
4
- PYTHONUNBUFFERED=1 \
5
- U2NET_HOME=/models
6
-
7
- WORKDIR /app
8
-
9
- RUN apt-get update \
10
- && apt-get install -y --no-install-recommends \
11
- ca-certificates \
12
- libgl1 \
13
- libglib2.0-0 \
14
- libgomp1 \
15
- && rm -rf /var/lib/apt/lists/* \
16
- && mkdir -p /models
17
-
18
- COPY requirements.txt .
19
- RUN pip install --no-cache-dir --upgrade pip \
20
- && pip install --no-cache-dir -r requirements.txt
21
-
22
- COPY app.py .
23
-
24
- EXPOSE 7860
25
- CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--threads", "2", "--timeout", "180", "app:app"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,28 +1,14 @@
1
  ---
2
- title: Background Studio
3
- emoji: ✂️
4
- colorFrom: purple
5
- colorTo: blue
6
- sdk: docker
7
- app_port: 7860
 
8
  pinned: false
 
 
9
  ---
10
 
11
- # Background Studio
12
-
13
- An advanced background-removal web app powered by `rembg`.
14
-
15
- ## Features
16
-
17
- - Drag-and-drop image upload
18
- - Quality, balanced, fast, people, and anime models
19
- - Optional alpha matting for hair, fur, and fine edges
20
- - Before/after comparison slider
21
- - Transparent PNG, WEBP, or JPG output
22
- - Custom JPG background color
23
- - 12 MB upload limit and basic rate limiting
24
- - Optional `API_KEY` Space Secret for API protection
25
-
26
- Open `/` for the web interface or call `POST /remove-bg` with a multipart `image` field.
27
-
28
- For a public Space, add `API_KEY` under **Settings → Variables and secrets** if you want the processing endpoint protected. Do not commit `.env` files or credentials.
 
1
  ---
2
+ title: RemBG API
3
+ emoji: 📈
4
+ colorFrom: blue
5
+ colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: 5.34.0
8
+ app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: rembg-api
12
  ---
13
 
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,338 +1,4 @@
1
- import hmac
2
- import io
3
- import os
4
- import time
5
- from threading import Lock
6
 
7
- from dotenv import load_dotenv
8
- from flask import Flask, jsonify, request, send_file
9
- from PIL import Image, ImageColor, ImageOps, UnidentifiedImageError
10
- from rembg import new_session, remove
11
-
12
- load_dotenv()
13
-
14
- app = Flask(__name__)
15
- app.config["MAX_CONTENT_LENGTH"] = 12 * 1024 * 1024 # 12 MB upload limit
16
-
17
- MODELS = {
18
- "isnet-general-use": "Quality · ISNet General",
19
- "u2net": "Balanced · U2Net",
20
- "u2netp": "Fast · U2NetP",
21
- "u2net_human_seg": "People · Human Segmentation",
22
- "isnet-anime": "Anime · ISNet Anime",
23
- }
24
- FORMATS = {"png", "jpg", "webp"}
25
- SESSIONS = {}
26
- SESSION_LOCK = Lock()
27
- RATE_LOCK = Lock()
28
- RATE_BUCKETS = {}
29
- RATE_LIMIT = 8
30
- RATE_WINDOW = 60
31
-
32
- INDEX_HTML = r"""
33
- <!doctype html>
34
- <html lang="en">
35
- <head>
36
- <meta charset="utf-8">
37
- <meta name="viewport" content="width=device-width, initial-scale=1">
38
- <title>Background Studio</title>
39
- <style>
40
- :root { color-scheme: dark; --bg:#090d18; --panel:#121a2e; --panel2:#18233d; --line:rgba(148,163,184,.2); --text:#f7f9ff; --muted:#95a5c2; --cyan:#43ddc6; --blue:#8298ff; --pink:#ff8eaa; --shadow:0 26px 80px rgba(0,0,0,.32); }
41
- * { box-sizing:border-box; }
42
- body { min-height:100vh; margin:0; color:var(--text); font:15px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; background:radial-gradient(circle at 12% 0%,rgba(130,152,255,.24),transparent 34rem),radial-gradient(circle at 94% 17%,rgba(67,221,198,.13),transparent 30rem),var(--bg); }
43
- button,input,select { font:inherit; }
44
- button { cursor:pointer; }
45
- .shell { width:min(1180px,calc(100% - 34px)); margin:0 auto; padding:30px 0 58px; }
46
- .topbar { display:flex; align-items:center; justify-content:space-between; gap:20px; }
47
- .brand { display:flex; align-items:center; gap:11px; font-weight:850; letter-spacing:.05em; }
48
- .brand-mark { display:grid; place-items:center; width:39px; height:39px; border-radius:13px; color:#06111b; background:linear-gradient(135deg,var(--cyan),var(--blue)); box-shadow:0 9px 28px rgba(67,221,198,.2); }
49
- .brand small { display:block; color:var(--muted); font-weight:500; letter-spacing:0; }
50
- .system { display:inline-flex; align-items:center; gap:8px; color:#b7c6df; font-size:13px; }
51
- .dot { width:8px; height:8px; border-radius:50%; background:var(--cyan); box-shadow:0 0 13px var(--cyan); }
52
- .hero { max-width:770px; margin:72px 0 34px; }
53
- .eyebrow { margin:0 0 13px; color:var(--cyan); font-size:12px; font-weight:850; letter-spacing:.15em; text-transform:uppercase; }
54
- h1 { margin:0; font-size:clamp(2.7rem,7vw,5.5rem); line-height:.93; letter-spacing:-.075em; }
55
- .hero p { max-width:650px; margin:22px 0 0; color:var(--muted); font-size:18px; }
56
- .studio { display:grid; grid-template-columns:380px minmax(0,1fr); gap:18px; align-items:start; }
57
- .card { border:1px solid var(--line); border-radius:23px; background:rgba(18,26,46,.82); box-shadow:var(--shadow); backdrop-filter:blur(16px); }
58
- .controls { padding:22px; position:sticky; top:18px; }
59
- .card-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:20px; font-weight:800; }
60
- .pill { padding:5px 9px; border:1px solid rgba(67,221,198,.25); border-radius:999px; color:var(--cyan); background:rgba(67,221,198,.08); font-size:11px; font-weight:800; }
61
- .dropzone { display:grid; place-items:center; min-height:166px; padding:22px; border:1px dashed rgba(130,152,255,.55); border-radius:17px; color:var(--muted); background:rgba(8,13,28,.45); text-align:center; transition:.18s; }
62
- .dropzone.drag { border-color:var(--cyan); background:rgba(67,221,198,.08); transform:scale(1.01); }
63
- .upload-icon { display:grid; place-items:center; width:44px; height:44px; margin-bottom:10px; border-radius:14px; color:var(--cyan); background:rgba(67,221,198,.1); font-size:21px; }
64
- .dropzone strong { display:block; color:var(--text); }
65
- .dropzone span { display:block; margin-top:4px; font-size:12px; }
66
- .file-name { overflow:hidden; max-width:100%; margin-top:10px; color:#b8c8ff; text-overflow:ellipsis; white-space:nowrap; }
67
- .browse { display:inline-block; margin-top:13px; padding:8px 12px; border:1px solid var(--line); border-radius:9px; color:var(--text); background:rgba(130,152,255,.12); cursor:pointer; font-size:12px; font-weight:750; }
68
- #file { display:none; }
69
- label { display:block; margin:18px 0 8px; color:#c9d5ec; font-size:11px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; }
70
- select,input[type=password],input[type=color] { width:100%; padding:11px 12px; border:1px solid var(--line); border-radius:11px; outline:0; color:var(--text); background:rgba(7,12,25,.62); }
71
- select:focus,input:focus { border-color:var(--blue); box-shadow:0 0 0 4px rgba(130,152,255,.13); }
72
- input[type=color] { height:42px; padding:5px; }
73
- .row { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
74
- .check { display:flex; align-items:center; gap:9px; margin-top:17px; color:#c5d2e9; font-size:13px; }
75
- .check input { width:16px; height:16px; accent-color:var(--cyan); }
76
- details { margin-top:14px; color:var(--muted); font-size:12px; }
77
- summary { cursor:pointer; color:#b8c8ff; font-weight:750; }
78
- .advanced { display:grid; grid-template-columns:1fr 1fr 1fr; gap:9px; margin-top:10px; }
79
- .advanced input { padding:8px; font-size:12px; }
80
- .advanced label { margin:0 0 5px; font-size:10px; }
81
- .run { width:100%; margin-top:22px; padding:14px 16px; border:0; border-radius:12px; color:#06111b; background:linear-gradient(100deg,var(--cyan),#91a7ff); box-shadow:0 12px 30px rgba(67,221,198,.15); font-weight:850; }
82
- .run:disabled { opacity:.55; cursor:wait; }
83
- .status { min-height:21px; margin:13px 0 0; color:var(--muted); font-size:12px; }
84
- .status.success { color:var(--cyan); }
85
- .status.error { color:var(--pink); }
86
- .result { min-width:0; padding:22px; }
87
- .result-head { display:flex; align-items:center; justify-content:space-between; gap:14px; margin-bottom:18px; }
88
- .result-head h2 { margin:0; font-size:19px; }
89
- .result-head span { color:var(--muted); font-size:12px; }
90
- .empty { display:grid; place-items:center; min-height:500px; border:1px dashed var(--line); border-radius:17px; color:#687995; text-align:center; }
91
- .empty-icon { margin-bottom:9px; font-size:32px; opacity:.7; }
92
- .compare-wrap { display:none; }
93
- .compare-wrap.show { display:block; }
94
- .compare { position:relative; overflow:hidden; min-height:430px; border:1px solid var(--line); border-radius:17px; background-color:#edf2fa; background-image:linear-gradient(45deg,#dfe6f2 25%,transparent 25%),linear-gradient(-45deg,#dfe6f2 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#dfe6f2 75%),linear-gradient(-45deg,transparent 75%,#dfe6f2 75%); background-position:0 0,0 11px,11px -11px,-11px 0; background-size:22px 22px; }
95
- .compare img { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; }
96
- .after-clip { position:absolute; inset:0; clip-path:inset(0 50% 0 0); }
97
- .divider { position:absolute; top:0; bottom:0; left:50%; width:2px; background:white; box-shadow:0 0 8px rgba(0,0,0,.4); pointer-events:none; }
98
- .tag { position:absolute; top:13px; z-index:2; padding:5px 8px; border-radius:7px; color:white; background:rgba(5,10,22,.65); font-size:11px; font-weight:800; letter-spacing:.06em; text-transform:uppercase; }
99
- .tag.before { left:13px; }
100
- .tag.after { right:13px; }
101
- .range { width:100%; margin:18px 0 0; accent-color:var(--blue); cursor:ew-resize; }
102
- .result-actions { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:15px; color:var(--muted); font-size:12px; }
103
- .download { display:none; padding:8px 11px; border:1px solid rgba(67,221,198,.28); border-radius:9px; color:var(--cyan); text-decoration:none; font-weight:800; }
104
- .download.show { display:inline-block; }
105
- .hint { margin-top:16px; color:#6e7e9b; font-size:12px; }
106
- @media (max-width:850px) { .studio{grid-template-columns:1fr;} .controls{position:static;} }
107
- @media (max-width:540px) { .shell{width:min(100% - 22px,1180px);padding-top:20px;} .system{display:none;} .hero{margin-top:55px;} .row,.advanced{grid-template-columns:1fr;} .compare{min-height:300px;} }
108
- </style>
109
- </head>
110
- <body>
111
- <main class="shell">
112
- <header class="topbar">
113
- <div class="brand"><span class="brand-mark">✦</span><div>BACKGROUND STUDIO<small>Advanced cutout workspace</small></div></div>
114
- <div class="system"><span class="dot"></span> CPU processing ready</div>
115
- </header>
116
- <section class="hero">
117
- <p class="eyebrow">Clean edges. Better cutouts.</p>
118
- <h1>Make the subject stand out.</h1>
119
- <p>Upload an image, choose the right segmentation model, fine-tune the edges, and download a transparent result.</p>
120
- </section>
121
- <section class="studio">
122
- <aside class="card controls">
123
- <div class="card-head"><span>Removal settings</span><span class="pill">ADVANCED</span></div>
124
- <form id="form">
125
- <div class="dropzone" id="dropzone">
126
- <div class="upload-icon">↑</div>
127
- <strong>Drop an image here</strong>
128
- <span>PNG, JPG, WEBP · up to 12 MB</span>
129
- <label class="browse" for="file">Choose image</label>
130
- <input id="file" type="file" accept="image/png,image/jpeg,image/webp">
131
- <div class="file-name" id="file-name">No file selected</div>
132
- </div>
133
- <label for="model">Segmentation model</label>
134
- <select id="model">
135
- <option value="isnet-general-use">Quality · ISNet General</option>
136
- <option value="u2net">Balanced · U2Net</option>
137
- <option value="u2netp">Fast · U2NetP</option>
138
- <option value="u2net_human_seg">People · Human Segmentation</option>
139
- <option value="isnet-anime">Anime · ISNet Anime</option>
140
- </select>
141
- <div class="row">
142
- <div><label for="format">Output</label><select id="format"><option value="png">PNG · transparent</option><option value="webp">WEBP · smaller</option><option value="jpg">JPG · solid background</option></select></div>
143
- <div><label for="background">JPG background</label><input id="background" type="color" value="#ffffff" title="Used when JPG output is selected"></div>
144
- </div>
145
- <label class="check"><input id="matting" type="checkbox"> Enable alpha matting for finer edges</label>
146
- <details><summary>Fine-tune alpha matting</summary><div class="advanced"><div><label for="foreground">Foreground</label><input id="foreground" type="number" min="0" max="255" value="240"></div><div><label for="threshold">Background</label><input id="threshold" type="number" min="0" max="255" value="10"></div><div><label for="erode">Erode</label><input id="erode" type="number" min="0" max="40" value="10"></div></div></details>
147
- <label for="api-key">API key <span style="color:var(--muted);font-weight:500;text-transform:none;letter-spacing:0">(optional)</span></label>
148
- <input id="api-key" type="password" autocomplete="off" placeholder="Only needed if the Space secret is enabled">
149
- <button class="run" id="run" type="submit">Remove background →</button>
150
- <div class="status" id="status">Choose an image to begin.</div>
151
- </form>
152
- </aside>
153
- <section class="card result">
154
- <div class="result-head"><h2>Preview</h2><span id="meta">Before and after comparison</span></div>
155
- <div class="empty" id="empty"><div><div class="empty-icon">◌</div>Upload an image and your result will appear here.</div></div>
156
- <div class="compare-wrap" id="compare-wrap">
157
- <div class="compare" id="compare"><span class="tag before">Original</span><span class="tag after">Cutout</span><img id="before" alt="Original image"><div class="after-clip" id="after-clip"><img id="after" alt="Background removed image"></div><div class="divider" id="divider"></div></div>
158
- <input class="range" id="range" type="range" min="0" max="100" value="50" aria-label="Compare original and cutout">
159
- <div class="result-actions"><span id="details">Transparent PNG ready</span><a class="download" id="download" download="background-removed.png">Download result ↓</a></div>
160
- </div>
161
- <p class="hint">Tip: use alpha matting when hair, fur, glass, or fine object edges need extra refinement.</p>
162
- </section>
163
- </section>
164
- </main>
165
- <script>
166
- const form = document.getElementById('form');
167
- const fileInput = document.getElementById('file');
168
- const dropzone = document.getElementById('dropzone');
169
- const fileName = document.getElementById('file-name');
170
- const run = document.getElementById('run');
171
- const status = document.getElementById('status');
172
- const empty = document.getElementById('empty');
173
- const wrap = document.getElementById('compare-wrap');
174
- const before = document.getElementById('before');
175
- const after = document.getElementById('after');
176
- const clip = document.getElementById('after-clip');
177
- const divider = document.getElementById('divider');
178
- const range = document.getElementById('range');
179
- const download = document.getElementById('download');
180
- const details = document.getElementById('details');
181
- const meta = document.getElementById('meta');
182
- let originalUrl = null;
183
- let resultUrl = null;
184
- function setFile(file) { if (!file) return; fileInput.files = (() => { const dt = new DataTransfer(); dt.items.add(file); return dt.files; })(); fileName.textContent = file.name; status.textContent = 'Ready to process.'; status.className = 'status'; if (originalUrl) URL.revokeObjectURL(originalUrl); originalUrl = URL.createObjectURL(file); before.src = originalUrl; }
185
- fileInput.addEventListener('change', () => setFile(fileInput.files[0]));
186
- ['dragenter','dragover'].forEach((event) => dropzone.addEventListener(event, (e) => { e.preventDefault(); dropzone.classList.add('drag'); }));
187
- ['dragleave','drop'].forEach((event) => dropzone.addEventListener(event, (e) => { e.preventDefault(); dropzone.classList.remove('drag'); }));
188
- dropzone.addEventListener('drop', (e) => setFile(e.dataTransfer.files[0]));
189
- function updateCompare() { const value = Number(range.value); clip.style.clipPath = `inset(0 ${100 - value}% 0 0)`; divider.style.left = `${value}%`; }
190
- range.addEventListener('input', updateCompare);
191
- form.addEventListener('submit', async (event) => {
192
- event.preventDefault();
193
- const file = fileInput.files[0];
194
- if (!file) { status.textContent = 'Choose an image first.'; status.className = 'status error'; return; }
195
- run.disabled = true; status.className = 'status'; status.textContent = 'Processing… the first model run may take longer.'; meta.textContent = 'Working on your cutout';
196
- const outputFormat = document.getElementById('format').value;
197
- const body = new FormData(); body.append('image', file); body.append('model', document.getElementById('model').value); body.append('format', outputFormat); body.append('background', outputFormat === 'jpg' ? document.getElementById('background').value : 'transparent'); body.append('alpha_matting', document.getElementById('matting').checked ? 'true' : 'false'); body.append('foreground_threshold', document.getElementById('foreground').value); body.append('background_threshold', document.getElementById('threshold').value); body.append('erode_size', document.getElementById('erode').value);
198
- const apiKey = document.getElementById('api-key').value.trim();
199
- try {
200
- const headers = apiKey ? { 'X-API-Key': apiKey } : {};
201
- const response = await fetch('/remove-bg', { method:'POST', headers, body });
202
- if (!response.ok) { const data = await response.json().catch(() => ({})); throw new Error(data.error || `Request failed: ${response.status}`); }
203
- const blob = await response.blob();
204
- if (resultUrl) URL.revokeObjectURL(resultUrl); resultUrl = URL.createObjectURL(blob); after.src = resultUrl; download.href = resultUrl; download.classList.add('show'); empty.style.display = 'none'; wrap.classList.add('show'); range.value = 50; updateCompare();
205
- const model = document.getElementById('model').value; const format = outputFormat.toUpperCase(); details.textContent = `${model} · ${format} ready`; meta.textContent = 'Drag the slider to compare'; status.className = 'status success'; status.textContent = 'Background removed successfully.';
206
- } catch (error) { status.className = 'status error'; status.textContent = error.message; }
207
- finally { run.disabled = false; }
208
- });
209
- </script>
210
- </body>
211
- </html>
212
- """
213
-
214
-
215
- def _client_key():
216
- forwarded = request.headers.get("X-Forwarded-For", "")
217
- return forwarded.split(",")[0].strip() or request.remote_addr or "unknown"
218
-
219
-
220
- def _within_rate_limit():
221
- now = time.time()
222
- key = _client_key()
223
- with RATE_LOCK:
224
- recent = [stamp for stamp in RATE_BUCKETS.get(key, []) if now - stamp < RATE_WINDOW]
225
- if len(recent) >= RATE_LIMIT:
226
- RATE_BUCKETS[key] = recent
227
- return False
228
- recent.append(now)
229
- RATE_BUCKETS[key] = recent
230
- return True
231
-
232
-
233
- def _check_api_key():
234
- configured = os.getenv("API_KEY", "").strip()
235
- if not configured:
236
- return True
237
- supplied = request.headers.get("X-API-Key", "")
238
- return bool(supplied) and hmac.compare_digest(supplied, configured)
239
-
240
-
241
- def _int_field(name, default, low, high):
242
- try:
243
- value = int(request.form.get(name, default))
244
- except (TypeError, ValueError):
245
- raise ValueError(f"{name} must be a number")
246
- return max(low, min(high, value))
247
-
248
-
249
- def _get_session(model):
250
- if model not in MODELS:
251
- raise ValueError("Unknown segmentation model")
252
- with SESSION_LOCK:
253
- if model not in SESSIONS:
254
- SESSIONS[model] = new_session(model)
255
- return SESSIONS[model]
256
-
257
-
258
- def _composite(image, color):
259
- try:
260
- rgb = ImageColor.getrgb(color)
261
- except ValueError as exc:
262
- raise ValueError("Invalid background color") from exc
263
- background = Image.new("RGBA", image.size, rgb + (255,))
264
- background.alpha_composite(image.convert("RGBA"))
265
- return background
266
-
267
-
268
- @app.get("/")
269
- def home():
270
- return INDEX_HTML
271
-
272
-
273
- @app.get("/health")
274
- def health():
275
- return jsonify({"status": "healthy", "service": "background-studio"}), 200
276
-
277
-
278
- @app.post("/remove-bg")
279
- def remove_background():
280
- if not _within_rate_limit():
281
- return jsonify({"error": "Rate limit reached. Please wait a minute."}), 429
282
- if not _check_api_key():
283
- return jsonify({"error": "Invalid or missing API key"}), 401
284
- if "image" not in request.files:
285
- return jsonify({"error": "Choose an image file"}), 400
286
-
287
- uploaded = request.files["image"]
288
- if not uploaded.filename:
289
- return jsonify({"error": "No image selected"}), 400
290
-
291
- model = request.form.get("model", "isnet-general-use")
292
- output_format = request.form.get("format", "png").lower()
293
- background = request.form.get("background", "#ffffff")
294
- alpha_matting = request.form.get("alpha_matting", "false").lower() == "true"
295
- if output_format not in FORMATS:
296
- return jsonify({"error": "Unsupported output format"}), 400
297
-
298
- try:
299
- image = ImageOps.exif_transpose(Image.open(uploaded.stream))
300
- image.load()
301
- if image.width * image.height > 25_000_000:
302
- return jsonify({"error": "Image dimensions are too large"}), 400
303
- image = image.convert("RGBA")
304
- session = _get_session(model)
305
- output = remove(
306
- image,
307
- session=session,
308
- alpha_matting=alpha_matting,
309
- alpha_matting_foreground_threshold=_int_field("foreground_threshold", 240, 0, 255),
310
- alpha_matting_background_threshold=_int_field("background_threshold", 10, 0, 255),
311
- alpha_matting_erode_size=_int_field("erode_size", 10, 0, 40),
312
- ).convert("RGBA")
313
-
314
- if output_format == "jpg" or background.lower() != "transparent":
315
- output = _composite(output, background if background.lower() != "transparent" else "#ffffff")
316
- if output_format == "jpg":
317
- output = output.convert("RGB")
318
-
319
- result = io.BytesIO()
320
- if output_format == "jpg":
321
- output.save(result, format="JPEG", quality=95, optimize=True)
322
- elif output_format == "webp":
323
- output.save(result, format="WEBP", quality=95, method=6)
324
- else:
325
- output.save(result, format="PNG", optimize=True)
326
- result.seek(0)
327
- return send_file(result, mimetype=f"image/{'jpeg' if output_format == 'jpg' else output_format}", as_attachment=True, download_name=f"background-removed.{output_format}")
328
- except (UnidentifiedImageError, OSError):
329
- return jsonify({"error": "The uploaded file is not a readable image"}), 400
330
- except ValueError as exc:
331
- return jsonify({"error": str(exc)}), 400
332
- except Exception:
333
- app.logger.exception("Background removal failed")
334
- return jsonify({"error": "Background removal failed. Try a smaller image or another model."}), 500
335
-
336
-
337
- if __name__ == "__main__":
338
- app.run(host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
 
1
+ from app.main import app
 
 
 
 
2
 
3
+ # This file is needed for Hugging Face Spaces compatibility
4
+ # The app is imported from app.main
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
main.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.responses import StreamingResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import io
5
+ from PIL import Image
6
+ from rembg import remove
7
+
8
+ app = FastAPI(title="Background Removal API")
9
+
10
+ # Add CORS middleware to allow requests from any origin
11
+ app.add_middleware(
12
+ CORSMiddleware,
13
+ allow_origins=["*"],
14
+ allow_credentials=True,
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ )
18
+
19
+ @app.get("/")
20
+ def read_root():
21
+ return {"message": "Background Removal API is running. Use POST /remove-bg to remove background from an image."}
22
+
23
+ @app.post("/remove-bg")
24
+ async def remove_background(file: UploadFile = File(...)):
25
+ # Check if the file is an image
26
+ if not file.content_type.startswith("image/"):
27
+ raise HTTPException(status_code=400, detail="File must be an image")
28
+
29
+ try:
30
+ # Read the image
31
+ contents = await file.read()
32
+ input_image = Image.open(io.BytesIO(contents))
33
+
34
+ # Remove the background
35
+ output_image = remove(input_image)
36
+
37
+ # Convert to bytes
38
+ img_byte_arr = io.BytesIO()
39
+ output_image.save(img_byte_arr, format="PNG")
40
+ img_byte_arr.seek(0)
41
+
42
+ # Return the processed image
43
+ return StreamingResponse(
44
+ content=img_byte_arr,
45
+ media_type="image/png"
46
+ )
47
+
48
+ except Exception as e:
49
+ raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
50
+
51
+ if __name__ == "__main__":
52
+ import uvicorn
53
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,5 +1,4 @@
1
- Flask>=3.0,<4
2
- Pillow>=10,<12
3
- python-dotenv>=1,<2
4
- rembg[cpu]>=2.0.50,<3
5
- gunicorn>=22,<24
 
1
+ rembg
2
+ onnxruntime
3
+ fastapi
4
+ uvicorn