| 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 |
|
|
|
|
| |
| sess_options = rt.SessionOptions() |
| sess_options.intra_op_num_threads = 2 |
| 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, |
| ) |
| 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_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)) |
|
|
| |
| 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 |
|
|
| |
| 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("请至少选择一张图片") |
|
|
| |
| cached_results = {} |
| to_process = [] |
|
|
| 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") |
|
|