Spaces:
Sleeping
Sleeping
File size: 8,183 Bytes
a3953cb | 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 | """
bgremove.py β POST /bgremove/upload and GET /bgremove/result/{job_id}
INDEPENDENT of the LaMa/watermark-removal pipeline, Real-ESRGAN upscale pipeline,
and object-removal pipeline.
Do not import from jobs_store.py, upscale_jobs_store.py, schemas.py, generate.py,
inpaint_engine.py, upscale_engine.py, or any other pipeline module.
Design
------
Background removal, like upscaling, has no user input after the image is
chosen β so upload and inference kick-off are combined into a single POST
/bgremove/upload endpoint.
Flow
----
1. POST /bgremove/upload (multipart file)
a. Validate content-type.
b. EXIF-correct and convert to RGB via Pillow.
c. Assign UUID job_id; create storage/bgremove/{folder_name}/.
d. Save EXIF-corrected image as original.jpg.
e. Persist BgRemoveJob(status="pending") to bgremove_jobs.json.
f. Immediately advance status to "processing" and enqueue BackgroundTask.
g. Return {"job_id": job_id} immediately (inference runs in background).
2. GET /bgremove/result/{job_id} (polling)
Returns {"job_id", "status", "result_image_url", "error_message"}.
result_image_url is a /storage/... URL served by the existing StaticFiles
mount in main.py β no extra route needed.
3. run_bgremove_job (background worker)
Runs after the POST response is sent; has no access to the Request object.
Receives the engine as an explicit argument.
Updates job status to "completed" or "failed"; never raises.
Storage layout (kept entirely separate from all other storage paths)
---------------------------------------------------------------------
storage/
bgremove/
{folder_name}/
original.jpg β EXIF-corrected upload (JPEG)
result.png β RGBA background-removed output (PNG, for alpha channel)
These are served as:
/storage/bgremove/{folder_name}/original.jpg
/storage/bgremove/{folder_name}/result.png
via the existing app.mount("/storage", StaticFiles(...)) in main.py.
Why PNG for result?
-------------------
JPEG does not support an alpha channel. The background removal output is RGBA
(foreground pixels are opaque/semi-transparent, background pixels are
transparent). PNG losslessly encodes all four channels, so result.png
preserves the transparency that makes background removal useful.
"""
from __future__ import annotations
import io
import traceback
import uuid
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, File, HTTPException, Request, UploadFile
from PIL import Image, ImageOps
from app.config import STORAGE_DIR
from app.utils import job_folder_name
from app.bgremove_jobs_store import BgRemoveJob, create_job, get_job, update_job
from app.services.bgremove_engine import BaseBackgroundRemovalEngine
router = APIRouter()
# Background-removal jobs live in a dedicated sub-directory of storage/ so they
# can never collide with watermark-removal, upscale, or object-removal folders.
_BGREMOVE_STORAGE = STORAGE_DIR / "bgremove"
# ---------------------------------------------------------------------------
# Background worker
# ---------------------------------------------------------------------------
def run_bgremove_job(
job_id: str,
folder_name: str,
original_path: Path,
engine: BaseBackgroundRemovalEngine,
) -> None:
"""Run BiRefNet background removal and save result.png; update job status.
Called by FastAPI BackgroundTasks after the /bgremove/upload response is sent.
Has no access to the Request object β engine is passed as an explicit arg.
Updates status to "completed" or "failed"; never raises.
Pipeline
--------
original.jpg (RGB) β engine.remove_background() β RGBA PIL β result.png (PNG)
The result is saved as PNG (not JPEG) because JPEG cannot encode the alpha
channel that carries the transparency mask.
"""
job_dir = _BGREMOVE_STORAGE / folder_name
result_path = job_dir / "result.png"
try:
pil_img = Image.open(original_path).convert("RGB")
# Run background removal β returns RGBA PIL Image
rgba_img = engine.remove_background(pil_img)
# Save as PNG to preserve the alpha channel
rgba_img.save(str(result_path), format="PNG")
# Build the URL path relative to the /storage mount.
rel = result_path.relative_to(STORAGE_DIR)
update_job(job_id, status="completed", result_image=f"/{rel.as_posix()}")
except Exception:
update_job(
job_id,
status="failed",
error_message=traceback.format_exc(),
)
# ---------------------------------------------------------------------------
# POST /bgremove/upload
# ---------------------------------------------------------------------------
@router.post("/upload")
async def bgremove_upload(
file: UploadFile,
background_tasks: BackgroundTasks,
request: Request,
) -> dict:
"""Accept an image, EXIF-correct it, persist it, and kick off background removal.
Form fields
-----------
file : image file (multipart)
Returns ``{"job_id": <str>}`` immediately; background removal runs in the
background. Poll GET /bgremove/result/{job_id} for completion.
"""
# --- Content-type guard --------------------------------------------------
if not (file.content_type or "").startswith("image/"):
raise HTTPException(
status_code=415,
detail=f"Expected an image file, got content-type '{file.content_type}'.",
)
# --- Read and EXIF-correct -----------------------------------------------
raw_bytes = await file.read()
if not raw_bytes:
raise HTTPException(status_code=400, detail="Uploaded file is empty.")
try:
pil_img = Image.open(io.BytesIO(raw_bytes))
pil_img = ImageOps.exif_transpose(pil_img)
pil_img = pil_img.convert("RGB")
except Exception as exc:
raise HTTPException(status_code=422, detail=f"Could not decode image: {exc}")
# --- Persist original image ----------------------------------------------
job_id = str(uuid.uuid4())
created_at = datetime.now(timezone.utc)
folder = job_folder_name(job_id, created_at)
job_dir = _BGREMOVE_STORAGE / folder
job_dir.mkdir(parents=True, exist_ok=True)
original_path = job_dir / "original.jpg"
pil_img.save(str(original_path), format="JPEG", quality=95)
# --- Create job record ---------------------------------------------------
orig_rel = original_path.relative_to(STORAGE_DIR)
job = BgRemoveJob(
id = job_id,
original_image = f"/{orig_rel.as_posix()}",
status = "processing",
created_at = created_at.isoformat(),
)
create_job(job)
# --- Enqueue background removal ------------------------------------------
engine: BaseBackgroundRemovalEngine = request.app.state.bgremove_engine
background_tasks.add_task(
run_bgremove_job,
job_id = job_id,
folder_name = folder,
original_path = original_path,
engine = engine,
)
return {"job_id": job_id}
# ---------------------------------------------------------------------------
# GET /bgremove/result/{job_id}
# ---------------------------------------------------------------------------
@router.get("/result/{job_id}")
def bgremove_result(job_id: str) -> dict:
"""Return the current status of a background removal job.
Response fields:
job_id : str
status : "pending" | "processing" | "completed" | "failed"
result_image_url : str | null β /storage/bgremove/{folder}/result.png
error_message : str | null
"""
job = get_job(job_id)
if job is None:
raise HTTPException(
status_code=404,
detail=f"Background removal job '{job_id}' not found.",
)
return {
"job_id": job.id,
"status": job.status,
"result_image_url": job.result_image,
"error_message": job.error_message,
}
|