Spaces:
Running on Zero
Running on Zero
File size: 14,494 Bytes
80d3fc8 e7d8d0b 02cae92 80d3fc8 e7d8d0b 80d3fc8 ba403cb 74237c6 02cae92 74237c6 02cae92 80d3fc8 bfc7d6a aceffa8 bfc7d6a aceffa8 80d3fc8 7d2b21b e7d8d0b 80d3fc8 ba403cb bfc7d6a 80d3fc8 bfc7d6a 80d3fc8 bfc7d6a 80d3fc8 7d2b21b 80d3fc8 ca186c4 84cab1e ca186c4 84cab1e ca186c4 84cab1e 80d3fc8 e7d8d0b 80d3fc8 84cab1e 80d3fc8 e7d8d0b 80d3fc8 ba403cb e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e7d8d0b 80d3fc8 e05655a e7d8d0b 80d3fc8 e7d8d0b 02cae92 e7d8d0b 02cae92 e7d8d0b e05655a 02cae92 aceffa8 bfc7d6a aceffa8 80d3fc8 02cae92 80d3fc8 e7d8d0b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 | """
سرویس حذف پسزمینه — نسخهی اجرا روی Hugging Face Spaces (ZeroGPU، رایگان).
معماری نهایی (با آزمون و خطای واقعی روی همین Space):
* سختافزار ZeroGPU **خودش** یک سرور Gradio روی پورت پیشفرض بالا میآورد؛ پس
اجرای uvicorn خودمان ممکن نیست (`address already in use`) و مسیرهای سفارشی
هم زیر Gradio گم میشوند (۴۰۴).
* بنابراین ارتباط افزونه از طریق **API خود Gradio** انجام میشود و برای اینکه
هیچ فایل/مسیر موقتی لازم نباشد، ورودی و خروجی **base64** است:
POST /gradio_api/call/api_cutout {"data": ["<image b64>", "silueta", "<token>"]}
→ {"event_id": "..."}
GET /gradio_api/call/api_cutout/<event_id>
→ {"data": ["<png b64>", "0.5068"]}
* سختافزار ZeroGPU بدون وجود یک تابع `@spaces.GPU` بالا نمیآید
(«No @spaces.GPU function detected during startup»)؛ یک تابع تشریفاتی داریم
که هرگز صدا زده نمیشود و سهمیهی GPU مصرف نمیکند.
تلههای مدل که در کد لحاظ شده:
* پیشپردازش **فقط /255** (بدون نرمالسازی ImageNet؛ وگرنه خروجی صفر و ماسک
خالی میشود و کل تصویر پاک میگردد).
* نرمالسازی خروجی کمینه-بیشینه (سیگموید ماسک بد میدهد).
* `intra_op_num_threads` محدود، تا روی کانتینر مشترک پایدار بماند.
"""
import base64
import io
import os
import threading
import urllib.request
import gradio as gr
import numpy as np
import onnxruntime as ort
from PIL import Image, ImageFilter
try: # pragma: no cover
import spaces # noqa: F401
except Exception:
spaces = None
if spaces is not None:
@spaces.GPU(duration=1)
def _gpu_probe() -> bool:
"""فقط برای الزام ZeroGPU؛ هرگز اجرا نمیشود (سهمیهی GPU مصرف نمیکند)."""
return True
else:
def _gpu_probe() -> bool:
return True
# کلید مشترک با وردپرس؛ در Space → Settings → Variables and secrets ست میشود.
TOKEN = os.environ.get("PCMO_TOKEN", "")
MODELS = {
# نام: (مسیر فایل، ابعاد ورودی، آدرس دانلود، نرمالسازی ImageNet؟)
#
# نکتهی مهم: مدلهای خانوادهی u2net/isnet **فقط** تقسیم بر ۲۵۵ میخواهند،
# ولی BiRefNet نرمالسازی ImageNet هم میخواهد (طبق preprocessor_config خودش).
# اشتباه گرفتن این دو، خروجی صفر و ماسک خالی میدهد.
"silueta": (
"models/silueta.onnx",
320,
"https://github.com/danielgatis/rembg/releases/download/v0.0.0/silueta.onnx",
False,
),
"u2netp": (
"models/u2netp.onnx",
320,
"https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2netp.onnx",
False,
),
"isnet": (
"models/isnet-general-use.onnx",
1024,
"https://github.com/danielgatis/rembg/releases/download/v0.0.0/isnet-general-use.onnx",
False,
),
# «مدل دقیق» — همان مدلی که رقیب استفاده میکند. سنگین است (۴۶۷ مگابایت) و
# در اولین استفاده دانلود میشود؛ برای عکسهای شلوغ و پسزمینهی بافتدار
# (چوب، پارچه) بهطور محسوس بهتر از isnet است.
"birefnet": (
"models/birefnet-fp16.onnx",
1024,
"https://huggingface.co/onnx-community/BiRefNet-ONNX/resolve/main/onnx/model_fp16.onnx",
True,
),
}
DEFAULT_MODEL = os.environ.get("PCMO_MODEL", "silueta")
# مدلهایی که خروجیشان logit است و باید سیگموید شود (BiRefNet).
# بقیهی مدلها (u2net/isnet) خروجیشان نسبتاً نرمال است و کمینه-بیشینه کافی است.
SIGMOID_MODELS = {"birefnet"}
MAX_BYTES = int(os.environ.get("PCMO_MAX_MB", "15")) * 1024 * 1024
_lock = threading.Lock()
_sessions: dict[str, tuple[ort.InferenceSession, int]] = {}
def ensure_model(name: str) -> str:
"""فایل مدل را (یکبار در عمر کانتینر) دانلود میکند."""
path, _, url, _ = MODELS[name]
if os.path.exists(path) and os.path.getsize(path) > 1_000_000:
return path
os.makedirs("models", exist_ok=True)
partial = path + ".part"
print(f"[pcmo] downloading model {name} …", flush=True)
with urllib.request.urlopen(url, timeout=600) as response, open(partial, "wb") as handle:
while True:
chunk = response.read(1024 * 256)
if not chunk:
break
handle.write(chunk)
os.replace(partial, path)
print(f"[pcmo] model ready: {os.path.getsize(path) // 1048576} MB", flush=True)
return path
def session_for(name: str) -> tuple[ort.InferenceSession, int]:
with _lock:
if name not in _sessions:
path = ensure_model(name)
options = ort.SessionOptions()
options.intra_op_num_threads = int(os.environ.get("PCMO_THREADS", "2"))
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
_sessions[name] = (
ort.InferenceSession(path, sess_options=options, providers=["CPUExecutionProvider"]),
MODELS[name][1],
)
return _sessions[name]
def build_alpha(image: Image.Image, name: str) -> Image.Image:
session, size = session_for(name)
normalize = MODELS[name][3]
rgb = image.convert("RGB").resize((size, size), Image.LANCZOS)
pixels = np.asarray(rgb, dtype=np.float32) / 255.0
# فقط مدلهای ViT-محور (BiRefNet) نرمالسازی ImageNet میخواهند.
if normalize:
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
pixels = (pixels - mean) / std
tensor = np.transpose(pixels, (2, 0, 1))[None].astype(np.float32)
raw = session.run(None, {session.get_inputs()[0].name: tensor})[0]
raw_mask = raw[0, 0] if raw.ndim == 4 else raw[0]
if name in SIGMOID_MODELS:
# خروجی این مدلها logit است (مثلاً -18 تا +5) و باید سیگموید شود.
# با کمینه-بیشینه، همهی مقادیر نزدیک هم به «سوژه» تبدیل میشدند و پوشش
# به ۹۹.۹۵٪ میرسید (کل تصویر دستنخورده میماند).
mask = 1.0 / (1.0 + np.exp(-np.clip(raw_mask.astype(np.float32), -30.0, 30.0)))
else:
span = float(raw_mask.max() - raw_mask.min())
mask = (raw_mask - raw_mask.min()) / (span if span > 1e-6 else 1.0)
# ۱) آستانهی نرم.
#
# ماسک خام مدل، مقادیر ضعیفی (۰.۲ تا ۰.۵) در اطراف سوژه دارد که بعد از بزرگ
# شدن به شکل «هالهی رنگی» و «لکههای نیمهشفاف پسزمینه» دیده میشود. با یک
# باند نرم، این مقادیر صفر میشوند و لبهی سوژه تمیز ولی نرم میماند.
threshold = float(os.environ.get("PCMO_MASK_THRESHOLD", "0.45"))
band = max(1e-6, float(os.environ.get("PCMO_MASK_BAND", "0.12")))
mask = np.clip((mask - threshold) / band, 0.0, 1.0)
alpha = Image.fromarray((mask * 255).astype(np.uint8), mode="L")
# ۲) پاکسازی نقطههای ریز: «باز کردن» (فرسایش سپس گسترش) لکههای کوچک را
# حذف میکند و به برگ و شاخهی نازک کاری ندارد (کرنل کوچک انتخاب شد).
alpha = alpha.filter(ImageFilter.MinFilter(3)).filter(ImageFilter.MaxFilter(3))
# ۳) بازگشت به ابعاد تصویر با درونیابی نرم.
#
# LANCZOS روی ماسک، لبههای پلهای و «رینگ» رنگی میسازد؛ BILINEAR تمیزتر است.
return alpha.resize(image.size, Image.BILINEAR)
def cut_out(image: Image.Image, name: str, feather: int = 0) -> tuple[Image.Image, float]:
alpha = build_alpha(image, name)
# کمی نرمی روی لبه (برای چاپ DTF بهتر است) + نرمی اختیاری بیشتر از پارامتر.
blur = max(0.6, float(feather))
alpha = alpha.filter(ImageFilter.GaussianBlur(blur))
output = image.convert("RGBA")
output.putalpha(alpha)
coverage = float((np.asarray(alpha) > 128).mean())
return output, coverage
def ui_cutout(image, model_name):
"""رابط کاربری مرورگر (برای تست دستی)."""
if image is None:
return None
output, _ = cut_out(image, model_name or DEFAULT_MODEL, 0)
return output
def api_cutout(image_b64: str, model_name: str, token: str):
"""
اندپوینت افزونهی وردپرس: تصویر base64 میگیرد و PNG شفاف base64 + پوشش
ماسک برمیگرداند. توکن برای جلوگیری از استفادهی ناخواسته بررسی میشود.
"""
if TOKEN and (token or "").strip() != TOKEN:
return "", "ERR:token"
name = (model_name or DEFAULT_MODEL).strip()
if name not in MODELS:
return "", "ERR:model"
payload = (image_b64 or "").strip()
if payload.startswith("data:"):
payload = payload.split(",", 1)[-1]
if not payload:
return "", "ERR:empty"
try:
raw_bytes = base64.b64decode(payload, validate=False)
except Exception:
return "", "ERR:decode"
if len(raw_bytes) > MAX_BYTES:
return "", "ERR:big"
try:
image = Image.open(io.BytesIO(raw_bytes))
image.load()
except Exception:
return "", "ERR:image"
output, coverage = cut_out(image, name, 0)
buffer = io.BytesIO()
output.save(buffer, format="PNG", optimize=True)
return base64.b64encode(buffer.getvalue()).decode("ascii"), f"{coverage:.4f}"
def probe_model(model_name: str) -> str:
"""
اطلاعات تشخیصی مدل: نام/شکل/نوع ورودیها و خروجیها + آمار خروجی خام.
چرا لازم شد: BiRefNet خروجیاش با isnet/u2net فرق دارد و بدون دیدن آمار
واقعی (کمینه/بیشینه/میانه) نمیتوان تصمیم گرفت سیگموید لازم است یا نه.
"""
import json as _json
name = (model_name or DEFAULT_MODEL).strip()
if name not in MODELS:
return "ERR:model"
session, size = session_for(name)
info = {
"model": name,
"normalize": bool(MODELS[name][3]),
"size": size,
"inputs": [{"name": i.name, "shape": list(i.shape), "type": i.type} for i in session.get_inputs()],
"outputs": [{"name": o.name, "shape": list(o.shape), "type": o.type} for o in session.get_outputs()],
}
pixels = np.full((size, size, 3), 0.5, dtype=np.float32)
if MODELS[name][3]:
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
pixels = (pixels - mean) / std
tensor = np.transpose(pixels, (2, 0, 1))[None].astype(np.float32)
outputs = session.run(None, {session.get_inputs()[0].name: tensor})
stats = []
for index, raw in enumerate(outputs):
flat = np.asarray(raw).reshape(-1).astype(np.float32)
stats.append(
{
"index": index,
"dims": list(np.asarray(raw).shape),
"min": round(float(flat.min()), 4),
"max": round(float(flat.max()), 4),
"mean": round(float(flat.mean()), 4),
"p50": round(float(np.percentile(flat, 50)), 4),
"p90": round(float(np.percentile(flat, 90)), 4),
}
)
info["stats"] = stats
return _json.dumps(info, ensure_ascii=False)
with gr.Blocks(title="حذف پسزمینه") as demo:
gr.Markdown("# حذف پسزمینه\nسرویس افزونهی چاپ سفارشی وردپرس.")
with gr.Row():
ui_image = gr.Image(type="pil", label="تصویر")
ui_out = gr.Image(type="pil", label="بدون پسزمینه")
ui_model = gr.Dropdown(choices=list(MODELS), value=DEFAULT_MODEL, label="مدل")
ui_btn = gr.Button("حذف پسزمینه", variant="primary")
ui_btn.click(ui_cutout, inputs=[ui_image, ui_model], outputs=[ui_out], api_name="ui_cutout")
gr.Markdown("---\n### اندپوینت API افزونه (base64)")
api_in = gr.Textbox(label="ورودی base64")
api_model = gr.Dropdown(choices=list(MODELS), value=DEFAULT_MODEL, label="مدل")
api_token = gr.Textbox(label="توکن")
api_btn = gr.Button("اجرا")
api_out = gr.Textbox(label="خروجی base64")
api_cov = gr.Textbox(label="پوشش ماسک")
api_btn.click(api_cutout, inputs=[api_in, api_model, api_token], outputs=[api_out, api_cov], api_name="api_cutout")
probe_btn = gr.Button("اطلاعات مدل (تشخیصی)")
probe_btn.click(probe_model, inputs=[api_model], outputs=[api_out], api_name="probe_model")
def _warm() -> None:
"""
آمادهسازی مدلها پیش از اولین درخواست کاربر.
`isnet` (۱۷۰ مگابایت) در اولین استفاده دانلود میشود؛ اگر همینجا و در پسزمینه
انجام شود، اولین کلیک مشتری معطل دانلود نمیماند.
"""
for name in ("silueta", "isnet", "birefnet"):
try:
session_for(name)
print(f"[pcmo] warm: {name} ready", flush=True)
except Exception as error: # noqa: BLE001
print(f"[pcmo] warm-up failed for {name}: {error}", flush=True)
threading.Thread(target=_warm, daemon=True).start()
if __name__ == "__main__":
demo.launch()
|