diff --git a/main.py b/main.py new file mode 100644 index 0000000..e73b5e6 --- /dev/null +++ b/main.py @@ -0,0 +1,128 @@ +from fastapi import FastAPI, File, UploadFile, Form +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.requests import Request +from typing import List +from pathlib import Path +from processing import process_image +from zipfile import ZipFile +import shutil +import uuid +from rembg import remove +import os + +app = FastAPI() + +# 文件路径设置 +BASE_DIR = Path(__file__).resolve().parent +STATIC_DIR = BASE_DIR / "static" +OUTPUT_DIR = STATIC_DIR / "output" +UPLOAD_DIR = STATIC_DIR / "uploads" +TEMPLATE_DIR = BASE_DIR / "templates" + +# 确保目录存在 +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + +# 挂载静态目录和模板 +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +templates = Jinja2Templates(directory=TEMPLATE_DIR) + + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + + +@app.post("/upload") +async def upload_images(files: List[UploadFile] = File(...)): + session_id = str(uuid.uuid4()) + session_upload_dir = UPLOAD_DIR / session_id + session_output_dir = OUTPUT_DIR / session_id + session_zip_path = OUTPUT_DIR / f"{session_id}.zip" + + session_upload_dir.mkdir(parents=True) + session_output_dir.mkdir(parents=True) + + for file in files: + contents = await file.read() + input_path = session_upload_dir / file.filename + with open(input_path, "wb") as f: + f.write(contents) + + output_path = session_output_dir / file.filename + process_image(input_path, output_path) + + # 打包为 zip + with ZipFile(session_zip_path, 'w') as zipf: + for image_file in session_output_dir.iterdir(): + zipf.write(image_file, arcname=image_file.name) + + # 清理上传目录(可选) + shutil.rmtree(session_upload_dir) + shutil.rmtree(session_output_dir) + + return {"download_url": f"/static/output/{session_zip_path.name}"} + + +@app.get("/download/{zip_filename}") +async def download_zip(zip_filename: str): + file_path = OUTPUT_DIR / zip_filename + if file_path.exists(): + return FileResponse(path=file_path, filename=zip_filename, media_type='application/zip') + return {"error": "File not found"} + + +@app.post("/remove-bg") +async def remove_bg(files: List[UploadFile] = File(...)): + session_id = str(uuid.uuid4()) + session_upload_dir = UPLOAD_DIR / session_id + session_output_dir = OUTPUT_DIR / session_id + session_zip_path = OUTPUT_DIR / f"{session_id}.zip" + + session_upload_dir.mkdir(parents=True, exist_ok=True) + session_output_dir.mkdir(parents=True, exist_ok=True) + + result_files = [] + + for file in files: + if file.filename == '': + return JSONResponse(content={"error": "No selected file"}, status_code=400) + + input_path = session_upload_dir / file.filename + output_path = session_output_dir / file.filename + + contents = await file.read() + with open(input_path, "wb") as f: + f.write(contents) + + with open(input_path, 'rb') as input_file: + with open(output_path, 'wb') as output_file: + input_data = input_file.read() + output_data = remove( + input_data, + alpha_matting=True, + alpha_matting_erode_size=15, + alpha_matting_background_threshold=5, + alpha_matting_foreground_threshold=250, + ) + output_file.write(output_data) + + result_files.append(f'/static/output/{session_id}/{file.filename}') + + # 打包为 zip 文件 + with ZipFile(session_zip_path, 'w') as zipf: + for image_file in session_output_dir.iterdir(): + zipf.write(image_file, arcname=image_file.name) + + # 清理临时目录 + shutil.rmtree(session_upload_dir) + shutil.rmtree(session_output_dir) + + return JSONResponse(content={"download_url": f"/static/output/{session_zip_path.name}"}) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="127.0.0.1", port=7310) diff --git a/processing.py b/processing.py new file mode 100644 index 0000000..76a6845 --- /dev/null +++ b/processing.py @@ -0,0 +1,46 @@ +from PIL import Image, ImageDraw +from pathlib import Path + + +def create_soft_edge_mask(width: int, height: int, transition_ratio: float = 0.02) -> Image: + transition_height = int(height * transition_ratio) + mask = Image.new("L", (width, height), 255) # 默认全白(不透明) + draw = ImageDraw.Draw(mask) + draw.rectangle([0, 0, width, height // 2 - transition_height // 2], fill=0) # 上半黑 + draw.rectangle([0, height // 2 + transition_height // 2, width, height], fill=255) # 下半白 + + # 顶部和底部之间 2% 的区域渐变 + for i in range(transition_height): + alpha = int(255 * (i / transition_height)) + y = height // 2 - transition_height // 2 + i + draw.line([(0, y), (width, y)], fill=alpha) + + return mask + + +def process_image(input_path: str | Path, output_path: str | Path): + base_width, base_height = 800, 1000 + original = Image.open(input_path).convert("RGBA") + + # 缩放图像宽度适配底图宽度 + scale_ratio = base_width / original.width + new_height = int(original.height * scale_ratio) + resized = original.resize((base_width, new_height), Image.LANCZOS) + + # 创建底图并粘贴第一个图像(对齐顶部) + base_image = Image.new("RGBA", (base_width, base_height), "white") + base_image.paste(resized, (0, 0), resized) + + # 创建 A2 图层(对齐底部) + A2 = resized.copy() + A2_image = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0)) + A2_y = base_height - A2.height + A2_image.paste(A2, (0, A2_y), A2) + + # 创建遮罩并加为 alpha 通道 + mask = create_soft_edge_mask(base_width, base_height, 0.02) + A2_image.putalpha(mask) + + # 合成 + final = Image.alpha_composite(base_image, A2_image) + final.convert("RGB").save(output_path, format="PNG") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..97ab4f9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +python-multipart +pillow +jinja2 +rembg +onnxruntime \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..0c535f4 Binary files /dev/null and b/static/favicon.ico differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5688455 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,340 @@ + + + + + 图像处理工具集 + + + + + +
+ +

将不同比例的边框图片调整为 800x1000 尺寸

+

支持批量上传 PNG / JPG / WebP 图片,推荐使用竖图(比例 9:16 或 3:4)

+ +
+ + 拖拽图片到这里,或点击选择 +
+ + + +
+
+
+ +
+ 正在处理,请稍候... +
+ + +
+ +
+ +

批量移除图片背景

+

支持批量上传 PNG / JPG 图片,移除背景后可下载结果

+ +
+ + 拖拽图片到这里,或点击选择 +
+ + + +
+
+
+ +
+ 正在处理,请稍候... +
+ + +
+ + + +