“ling”
单张和批量共享同一个缓存目录
2a5c58e
Raw
History Blame Contribute Delete
11 kB
import gradio as gr
import huggingface_hub
import onnxruntime as rt
import numpy as np
import cv2
import os
import time
import threading
import hashlib
import tempfile
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
# ---------- CPU 优化推理配置 ----------
sess_options = rt.SessionOptions()
sess_options.intra_op_num_threads = 2 # 匹配 2 核 CPU
sess_options.inter_op_num_threads = 2
sess_options.graph_optimization_level = rt.GraphOptimizationLevel.ORT_ENABLE_ALL
sess_options.enable_cpu_mem_arena = True
from onnxruntime.quantization import quantize_static, QuantType, CalibrationMethod
from onnxruntime.quantization import CalibrationDataReader as _CalibReaderBase
providers = ["CPUExecutionProvider"]
model_path = huggingface_hub.hf_hub_download("skytnt/anime-seg", "isnetis.onnx")
model_dir = os.path.dirname(model_path)
quantized_path = os.path.join(model_dir, "isnetis_int8.onnx")
class _CalibReader(_CalibReaderBase):
"""用项目自带的示例图片做静态量化校准,生成 QLinearConv 算子"""
def __init__(self):
self._data = []
for i in range(1, 4):
path = os.path.join("examples", f"{i:02d}.jpg")
if not os.path.exists(path):
continue
img_bgr = cv2.imread(path)
if img_bgr is None:
continue
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
img = (img_rgb / 255).astype(np.float32)
h, w = img.shape[:2]
s = 1024
h2, w2 = (s, int(s * w / h)) if h > w else (int(s * h / w), s)
ph, pw = s - h2, s - w2
inp = np.zeros((s, s, 3), dtype=np.float32)
inp[ph // 2:ph // 2 + h2, pw // 2:pw // 2 + w2] = cv2.resize(img, (w2, h2))
inp = np.transpose(inp, (2, 0, 1))[np.newaxis, :]
self._data.append({"img": inp})
self._idx = 0
def get_next(self):
if self._idx < len(self._data):
val = self._data[self._idx]
self._idx += 1
return val
return None
if not os.path.exists(quantized_path):
orig_mb = os.path.getsize(model_path) / 1024 / 1024
print(f"[量化] 原始模型 {orig_mb:.0f}MB,正在生成 INT8 量化版(~5 秒)...")
try:
calib_reader = _CalibReader()
quantize_static(
model_path,
quantized_path,
calibration_data_reader=calib_reader,
weight_type=QuantType.QInt8,
activation_type=QuantType.QInt8,
calibrate_method=CalibrationMethod.MinMax,
per_channel=False, # per_channel=True 需要新版 ONNX Runtime 的 DequantizeLinear axis 属性
)
q_mb = os.path.getsize(quantized_path) / 1024 / 1024
print(f"[量化] ✓ 成功!量化后 {q_mb:.0f}MB(压缩 {orig_mb / q_mb:.1f}x)")
except Exception as e:
print(f"[量化] ✗ 量化过程失败:{e},回退到原始 FP32 模型")
quantized_path = model_path
else:
print(f"[量化] 发现已缓存的量化模型 {os.path.getsize(quantized_path) / 1024 / 1024:.0f}MB")
# 尝试加载量化模型,失败则删掉缓存重新量化
try:
rmbg_model = rt.InferenceSession(quantized_path, sess_options=sess_options, providers=providers)
except Exception as e:
if quantized_path != model_path:
print(f"[量化] ⚠ 缓存模型加载失败 ({e}),删除并重新量化...")
os.remove(quantized_path)
try:
calib_reader = _CalibReader()
quantize_static(
model_path,
quantized_path,
calibration_data_reader=calib_reader,
weight_type=QuantType.QInt8,
activation_type=QuantType.QInt8,
calibrate_method=CalibrationMethod.MinMax,
per_channel=False,
)
q_mb = os.path.getsize(quantized_path) / 1024 / 1024
print(f"[量化] ✓ 重新量化成功!{q_mb:.0f}MB")
rmbg_model = rt.InferenceSession(quantized_path, sess_options=sess_options, providers=providers)
except Exception as e2:
print(f"[量化] ✗ 重新量化也失败:{e2},回退 FP32")
rmbg_model = rt.InferenceSession(model_path, sess_options=sess_options, providers=providers)
else:
print(f"[量化] ✗ FP32 模型加载也失败:{e}")
raise
# 结果缓存目录
CACHE_DIR = "cache"
os.makedirs(CACHE_DIR, exist_ok=True)
# 每个线程独立的输入缓冲区(thread-safe)
_thread_local = threading.local()
_INPUT_SIZE = 1024
def get_mask(img, s=1024):
# 获取当前线程的私有缓冲区
buf = getattr(_thread_local, "input_buf", None)
if buf is None:
buf = np.empty((_INPUT_SIZE, _INPUT_SIZE, 3), dtype=np.float32)
_thread_local.input_buf = buf
img = (img / 255).astype(np.float32)
h, w = h0, w0 = img.shape[:-1]
h, w = (s, int(s * w / h)) if h > w else (int(s * h / w), s)
ph, pw = s - h, s - w
buf.fill(0)
buf[ph // 2:ph // 2 + h, pw // 2:pw // 2 + w] = cv2.resize(img, (w, h))
# CHW + batch dim
img_input = np.ascontiguousarray(buf.transpose(2, 0, 1))[np.newaxis, :]
mask = rmbg_model.run(None, {"img": img_input})[0][0]
mask = np.transpose(mask, (1, 2, 0))
mask = mask[ph // 2:ph // 2 + h, pw // 2:pw // 2 + w]
mask = cv2.resize(mask, (w0, h0))[:, :, np.newaxis]
return mask
def rmbg_fn(img):
# 计算输入图片的像素哈希(用于磁盘缓存)
img_hash = hashlib.md5(img.tobytes()).hexdigest()
cache_path = os.path.join(CACHE_DIR, f"{img_hash}.png")
# 命中缓存
if os.path.exists(cache_path):
cached = cv2.imread(cache_path, cv2.IMREAD_UNCHANGED)
if cached is not None:
result_rgba = cv2.cvtColor(cached, cv2.COLOR_BGRA2RGBA)
mask_gray = result_rgba[:, :, 3]
mask_rgb = np.stack([mask_gray] * 3, axis=2)
print(f"[单张] 命中缓存 ✓")
return mask_rgb, result_rgba
t0 = time.perf_counter()
mask = get_mask(img)
img = (mask * img + 255 * (1 - mask)).astype(np.uint8)
mask = (mask * 255).astype(np.uint8)
img = np.concatenate([img, mask], axis=2, dtype=np.uint8)
mask = mask.repeat(3, axis=2)
elapsed = time.perf_counter() - t0
# 写入缓存(BGRA,与 batch_process 格式一致)
cv2.imwrite(cache_path, cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA))
print(f"[单张] 推理耗时: {elapsed:.2f}s")
return mask, img
def batch_process(files):
"""批量处理多张图片(并行推理 + 磁盘缓存)"""
if not files:
raise gr.Error("请至少选择一张图片")
# ---- 第 1 步:读入内存 + 检查缓存 ----
cached_results = {} # idx → cache_path
to_process = [] # (img_rgb, stem, idx, file_hash)
for idx, f in enumerate(files):
img_path = f.name if hasattr(f, "name") else str(f)
with open(img_path, "rb") as fh:
raw = fh.read()
file_hash = hashlib.md5(raw).hexdigest()
cache_path = os.path.join(CACHE_DIR, f"{file_hash}.png")
if os.path.exists(cache_path):
cached_results[idx] = cache_path
print(f" [{idx+1}] {Path(img_path).stem}: 命中缓存 ✓")
continue
img_bgr = cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)
if img_bgr is None:
continue
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
stem = Path(img_path).stem
to_process.append((img_rgb, stem, idx, file_hash))
if not to_process and not cached_results:
raise gr.Error("没有有效的图片")
tmp_dir = tempfile.mkdtemp()
results = [None] * len(files)
# 填充缓存命中结果
for idx, path in cached_results.items():
dest = os.path.join(tmp_dir, f"cached_{idx}.png")
cv2.imwrite(dest, cv2.imread(path, cv2.IMREAD_UNCHANGED))
results[idx] = dest
if to_process:
print(f"[批量] 开始并行推理 {len(to_process)} 张图片(2 线程)...")
t_start = time.perf_counter()
def _infer_one(img_rgb, stem, idx, file_hash):
t0 = time.perf_counter()
mask, result_rgba = rmbg_fn(img_rgb)
result_bgr = cv2.cvtColor(result_rgba, cv2.COLOR_RGBA2BGRA)
# 写入磁盘缓存
cv2.imwrite(os.path.join(CACHE_DIR, f"{file_hash}.png"), result_bgr)
out_path = os.path.join(tmp_dir, f"{stem}_rmbg.png")
cv2.imwrite(out_path, result_bgr)
elapsed = time.perf_counter() - t0
print(f" [{idx+1}/{len(files)}] {stem}: {elapsed:.2f}s")
return idx, out_path
with ThreadPoolExecutor(max_workers=2) as pool:
futures = {
pool.submit(_infer_one, img, stem, idx, fh): idx
for img, stem, idx, fh in to_process
}
for future in as_completed(futures):
idx, path = future.result()
results[idx] = path
t_total = time.perf_counter() - t_start
print(f"[批量] 完成!推理 {len(to_process)} 张,缓存命中 {len(cached_results)} 张,总耗时 {t_total:.2f}s")
return results
with gr.Blocks(title="Anime Remove Background") as app:
gr.Markdown(
"# Anime Remove Background\n\n"
"基于 [skytnt/anime-segmentation](https://github.com/SkyTNT/anime-segmentation/) 的动漫图片去背景工具"
)
with gr.Tab("单张处理"):
with gr.Row():
with gr.Column():
input_img = gr.Image(label="输入图片")
examples_data = [[f"examples/{x:02d}.jpg"] for x in range(1, 4)]
examples = gr.Dataset(components=[input_img], samples=examples_data)
with gr.Column():
run_btn = gr.Button("去除背景", variant="primary")
output_mask = gr.Image(label="蒙版")
output_img = gr.Image(label="结果(RGBA)", image_mode="RGBA")
examples.click(lambda x: x[0], [examples], [input_img])
run_btn.click(rmbg_fn, [input_img], [output_mask, output_img], api_name="rmbg_fn")
with gr.Tab("批量处理"):
with gr.Row():
with gr.Column():
input_files = gr.File(
label="上传多张图片",
file_count="multiple",
file_types=["image"],
)
batch_btn = gr.Button("批量去背景", variant="primary")
with gr.Column():
output_gallery = gr.Gallery(label="去背景结果", columns=3)
batch_btn.click(
batch_process,
[input_files],
[output_gallery],
api_name="batch_rmbg",
)
if __name__ == "__main__":
app.launch(server_name="0.0.0.0")