add basic files
This commit is contained in:
128
main.py
Normal file
128
main.py
Normal file
@@ -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)
|
||||
46
processing.py
Normal file
46
processing.py
Normal file
@@ -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")
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
python-multipart
|
||||
pillow
|
||||
jinja2
|
||||
rembg
|
||||
onnxruntime
|
||||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
340
templates/index.html
Normal file
340
templates/index.html
Normal file
@@ -0,0 +1,340 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>图像处理工具集</title>
|
||||
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
background: linear-gradient(to right, #f0f2f5, #dfe4ea);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
background: white;
|
||||
padding: 30px 40px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
|
||||
animation: fadeIn 0.8s ease-in-out;
|
||||
margin-bottom: 30px; /* 增加卡片之间的间隙 */
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
color: #4a90e2;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.drop-zone.dragover {
|
||||
background: #eef6ff;
|
||||
border-color: #4a90e2;
|
||||
color: #4a90e2;
|
||||
}
|
||||
|
||||
.drop-zone i {
|
||||
font-size: 36px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4a90e2;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #357abd;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 15px;
|
||||
height: 12px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background-color: #4a90e2;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
color: #555;
|
||||
margin-top: 12px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spinner i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.download-link i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<i class="fas fa-image"></i> 边框背景图像调整尺寸
|
||||
</div>
|
||||
<h2>将不同比例的边框图片调整为 800x1000 尺寸</h2>
|
||||
<p class="notice">支持批量上传 PNG / JPG / WebP 图片,推荐使用竖图(比例 9:16 或 3:4)</p>
|
||||
|
||||
<div class="drop-zone" id="dropZone" title="支持拖拽上传图片或点击选择文件">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
拖拽图片到这里,或点击选择
|
||||
</div>
|
||||
|
||||
<form id="uploadForm" enctype="multipart/form-data" style="display: none;">
|
||||
<input type="file" id="fileInput" name="files" multiple accept="image/png, image/jpeg, image/webp">
|
||||
</form>
|
||||
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
</div>
|
||||
|
||||
<div class="spinner" id="loadingSpinner">
|
||||
<i class="fas fa-circle-notch fa-spin"></i> 正在处理,请稍候...
|
||||
</div>
|
||||
|
||||
<div class="download-link" id="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<i class="fas fa-image"></i> 图像背景移除
|
||||
</div>
|
||||
<h2>批量移除图片背景</h2>
|
||||
<p class="notice">支持批量上传 PNG / JPG 图片,移除背景后可下载结果</p>
|
||||
|
||||
<div class="drop-zone" id="bgRemoveDropZone" title="支持拖拽上传图片或点击选择文件">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
拖拽图片到这里,或点击选择
|
||||
</div>
|
||||
|
||||
<form id="bgRemoveForm" enctype="multipart/form-data" style="display: none;">
|
||||
<input type="file" id="bgRemoveFileInput" name="files" multiple accept="image/png, image/jpeg">
|
||||
</form>
|
||||
|
||||
<div class="progress-container" id="bgRemoveProgressContainer">
|
||||
<div class="progress-bar" id="bgRemoveProgressBar"></div>
|
||||
</div>
|
||||
|
||||
<div class="spinner" id="bgRemoveLoadingSpinner">
|
||||
<i class="fas fa-circle-notch fa-spin"></i> 正在处理,请稍候...
|
||||
</div>
|
||||
|
||||
<div class="download-link" id="bgRemoveResult"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const dropZone = document.getElementById("dropZone");
|
||||
const fileInput = document.getElementById("fileInput");
|
||||
const progressBar = document.getElementById("progressBar");
|
||||
const progressContainer = document.getElementById("progressContainer");
|
||||
const resultBox = document.getElementById("result");
|
||||
const spinner = document.getElementById("loadingSpinner");
|
||||
|
||||
dropZone.addEventListener("click", () => fileInput.click());
|
||||
|
||||
dropZone.addEventListener("dragover", e => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("dragleave", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("drop", e => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove("dragover");
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
uploadFiles(fileInput.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener("change", () => {
|
||||
if (fileInput.files.length > 0) {
|
||||
uploadFiles(fileInput.files);
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadFiles(files) {
|
||||
resultBox.innerHTML = "";
|
||||
progressBar.style.width = "0%";
|
||||
progressContainer.style.display = "block";
|
||||
spinner.style.display = "block";
|
||||
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append("files", files[i]);
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/upload", true);
|
||||
|
||||
xhr.upload.onprogress = function (e) {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
progressBar.style.width = percent + "%";
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function () {
|
||||
spinner.style.display = "none";
|
||||
progressContainer.style.display = "none";
|
||||
if (xhr.status === 200) {
|
||||
const res = JSON.parse(xhr.responseText);
|
||||
if (res.download_url) {
|
||||
resultBox.innerHTML = `<i class="fas fa-check-circle" style="color:green;"></i> <a href='${res.download_url}' download>点击下载处理结果</a>`;
|
||||
} else {
|
||||
resultBox.innerHTML = `<i class="fas fa-times-circle" style="color:red;"></i> 上传失败,请检查文件格式。`;
|
||||
}
|
||||
} else {
|
||||
resultBox.innerHTML = `<i class="fas fa-exclamation-circle" style="color:orange;"></i> 上传错误,请稍后重试。`;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
const bgRemoveDropZone = document.getElementById("bgRemoveDropZone");
|
||||
const bgRemoveFileInput = document.getElementById("bgRemoveFileInput");
|
||||
const bgRemoveProgressBar = document.getElementById("bgRemoveProgressBar");
|
||||
const bgRemoveProgressContainer = document.getElementById("bgRemoveProgressContainer");
|
||||
const bgRemoveResultBox = document.getElementById("bgRemoveResult");
|
||||
const bgRemoveSpinner = document.getElementById("bgRemoveLoadingSpinner");
|
||||
|
||||
bgRemoveDropZone.addEventListener("click", () => bgRemoveFileInput.click());
|
||||
|
||||
bgRemoveDropZone.addEventListener("dragover", e => {
|
||||
e.preventDefault();
|
||||
bgRemoveDropZone.classList.add("dragover");
|
||||
});
|
||||
|
||||
bgRemoveDropZone.addEventListener("dragleave", () => {
|
||||
bgRemoveDropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
bgRemoveDropZone.addEventListener("drop", e => {
|
||||
e.preventDefault();
|
||||
bgRemoveDropZone.classList.remove("dragover");
|
||||
bgRemoveFileInput.files = e.dataTransfer.files;
|
||||
uploadBgRemoveFiles(bgRemoveFileInput.files);
|
||||
});
|
||||
|
||||
bgRemoveFileInput.addEventListener("change", () => {
|
||||
if (bgRemoveFileInput.files.length > 0) {
|
||||
uploadBgRemoveFiles(bgRemoveFileInput.files);
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadBgRemoveFiles(files) {
|
||||
bgRemoveResultBox.innerHTML = "";
|
||||
bgRemoveProgressBar.style.width = "0%";
|
||||
bgRemoveProgressContainer.style.display = "block";
|
||||
bgRemoveSpinner.style.display = "block";
|
||||
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append("files", files[i]);
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/remove-bg", true);
|
||||
|
||||
xhr.upload.onprogress = function (e) {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
bgRemoveProgressBar.style.width = percent + "%";
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function () {
|
||||
bgRemoveSpinner.style.display = "none";
|
||||
bgRemoveProgressContainer.style.display = "none";
|
||||
if (xhr.status === 200) {
|
||||
const res = JSON.parse(xhr.responseText);
|
||||
if (res.download_url) {
|
||||
bgRemoveResultBox.innerHTML = `<i class="fas fa-check-circle" style="color:green;"></i> <a href='${res.download_url}' download>点击下载处理结果</a>`;
|
||||
} else {
|
||||
bgRemoveResultBox.innerHTML = `<i class="fas fa-times-circle" style="color:red;"></i> 上传失败,请检查文件格式。`;
|
||||
}
|
||||
} else {
|
||||
bgRemoveResultBox.innerHTML = `<i class="fas fa-exclamation-circle" style="color:orange;"></i> 上传错误,请稍后重试。`;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user