File size: 10,971 Bytes
11f0a1f 94a437a d02c09b 28d0efc 94a437a a158b20 94a437a a158b20 94a437a 3bde59a a648bef 94a437a a648bef 3bde59a a648bef 3bde59a a648bef 3bde59a 9c4eba5 3bde59a 9c4eba5 3bde59a 9c4eba5 3dd3ce4 28d0efc d02c09b 7110843 11f0a1f d02c09b 11f0a1f d02c09b 7110843 d02c09b 94a437a 11f0a1f c2ef1f3 69656f2 c2ef1f3 2a5c58e d02c09b c2ef1f3 d02c09b 2a5c58e d02c09b c2ef1f3 3592f83 11f0a1f 372d479 28d0efc 94a437a 28d0efc 8548b4b 7110843 28d0efc 7110843 94a437a 7110843 28d0efc 7110843 28d0efc 7110843 28d0efc 8548b4b 94a437a c2ef1f3 94a437a c2ef1f3 94a437a c2ef1f3 94a437a 8548b4b 94a437a 8548b4b 94a437a | 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 | 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")
|