服装图人像抹除:解决虚拟试衣中模特冲突的实用方案
在我们团队开发虚拟试衣产品的过程中,遇到了一个常见且棘手的问题——用户需上传一张模特图与一张服装图,由AI生成模特试穿服装的效果。逻辑看似正确,但上线后用户反馈却集中爆发:

大量用户提交的服装图中内置有模特图像。您可能会疑惑:生成模型不是已经在提示词中指定“使用第一张图片的模特”了吗?然而,AI模型有时会误将服装图中的模特识别为目标人物,导致合成后的面部完全不符。
用户反馈直指核心:“为什么试衣结果的脸不是我?凭什么给我换了个模特?”
我们最初建议用户多次尝试,但该方案体验较差。问题的根源逐渐明晰:服装图中的模特人像信息干扰了模型的判断,引发了虚拟试衣模特冲突。
核心思路
既然干扰源来自服装图中的模特,那么最直接且高效的方案是:检测服装图是否包含人物,如果存在,则将脸部、颈部、头发等区域抹除,仅保留服装本体信息,再传递给虚拟试衣模型。这样可彻底消除模特冲突。
如此一来,生成模型只能从用户提供的模特图中提取人脸信息,模型误判(幻觉)问题基本得到解决。
技术方案对比与选型
调研了几种主流方案,对比分析如下:
| 方案 | CPU速度 | 精度 | 模型大小 | 说明 |
|---|---|---|---|---|
| SegFormer B0 人体解析 | ~2-3s/张 | 高 | ~50MB | 像素级语义分割,包含Face/Neck/Hair标签 |
| YOLO + MediaPipe | ~50ms | 中 | ~30MB | 脖子边界靠关键点估算,不够精准 |
| SAM2 | ~10-30s | 非常高 | ~2GB | 太慢太重,CPU不适用 |
| RemBG + 人体解析 | ~5-6s | 中 | ~220MB | 两模型串联,延迟翻倍 |
最终选定 SegFormer B0 + ATR 数据集,原因如下:
- 单个模型同时完成检测与分割,无需串联多个模型
- ATR数据集标签体系天然包含Face(11)、Neck(17)、Hair(2)、Hat(1),可直接使用
- B0为最小变体,CPU环境2-3秒性能可接受
- HuggingFace上提供现成预训练模型,开箱即用
至于抹除方式,同样进行了对比:
| 方式 | 效果 | 速度 | 推荐度 |
|---|---|---|---|
| cv2 inpainting (Telea) | 边界自然,修复效果优良 | ~10ms | ⭐⭐⭐ 推荐 |
| 马赛克 | 像素化马赛克效果 | ~5ms | ⭐⭐ 看场景 |
| 纯色填充 | 有明显色块,效果欠佳 | ~1ms | ⭐ 不推荐 |
具体实现步骤
1. 人体解析模型
我们采用 mattmdjaga/segformer_b0_clothes 模型,并通过 HuggingFace transformers 库进行加载:
from transformers import SegformerForSemanticSegmentation, SegformerImageProcessor
model = SegformerForSemanticSegmentation.from_pretrained("mattmdjaga/segformer_b0_clothes")
processor = SegformerImageProcessor.from_pretrained("mattmdjaga/segformer_b0_clothes")
该模型输出18个类别的像素级分割图,我们需要抹除的类别包括:
ERASE_LABELS = {"Hat", "Hair", "Sunglasses", "Face", "Neck"}
2. Mask 生成与膨胀
获取分割图后,我们将需要抹除的类别合并为二值掩码(mask),并执行形态学膨胀操作,确保边缘完整覆盖:
mask = np.isin(seg_map, list(ERASE_IDS)).astype(np.uint8) * 255
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (31, 31))
mask = cv2.dilate(mask, kernel, iterations=1)
膨胀步骤至关重要——分割边界通常与实际区域存在几个像素的偏差,若不膨胀,抹除边缘可能残留头发丝或脸部轮廓。
3. 抹除
三种方式实现都很简单:
# Inpaint 修复(推荐)
result = cv2.inpaint(image, mask, inpaint_radius=10, cv2.INPAINT_TELEA)
# 马赛克
# 逐块计算 mask 区域内平均颜色,用平均色填充
# 纯色填充
result[mask == 255] = (128, 128, 128)
4. API 服务封装
我们使用 FastAPI 将功能封装为服务,接口设计为 base64 输入和 base64 输出,便于虚拟试衣系统直接调用:
POST /api/erase
输入: image(base64) + method + 可选参数
输出: JSON { has_person, detected_parts, image(base64), format, elapsed_seconds }
几个关键设计决策:
- 未检测到人像时原样返回:不进行任何重编码,直接回传原始base64,避免体积变化
- 默认输出JPG格式:PNG对照片类图片体积会膨胀3-4倍
- 支持
data:image/jpeg;base64,前缀:前端传来的base64常携带此前缀,系统自动处理
5. 前端演示页面
我们额外搭建了一个单页应用,支持拖拽上传、三种抹除方式切换、参数调整、原图与结果左右对比、以及结果下载。便于团队内部演示与测试。
最终效果
- 服装图包含模特 → 自动检测并抹除脸部/颈部/头发 → 虚拟试衣模型不再受干扰
- 服装图不含模特 → 原样返回,零额外开销
- CPU环境下处理时间2-3秒/张,作为API服务性能充足
- 抹除后,虚拟试衣结果的模特一致性显著提升
项目结构
vtt-mask/
├── app/
│ ├── main.py # FastAPI 服务,路由定义
│ ├── parser.py # SegFormer B0 人体解析模型
│ ├── eraser.py # 抹除逻辑 (inpaint / mosaic / fill)
│ └── static/
│ └── index.html # 前端演示页面
├── requirements.txt
├── API_DOC.md # API 接口文档
└── README.md
调用方式
import requests, base64
with open("clothing.jpg", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
resp = requests.post("https://localhost:8787/api/erase", data={
"image": b64,
"method": "inpaint",
"output_format": "jpg",
})
result = resp.json()
if result["has_person"]:
print("检测到人像,已抹除:", result["detected_parts"])
else:
print("纯服装图,无需抹除")
# result["image"] 为抹除后图片 base64
遇到的坑
- NumPy 版本兼容性问题:onnxruntime 编译时绑定 NumPy 1.x,若环境安装 NumPy 2.x 则直接崩溃。解决方案:执行 pip install "numpy<2"
- ONNX 导出失败:SegFormer 模型具有动态形状,torch.onnx.export 导出时出现错误。最终放弃 ONNX 加速,直接采用 PyTorch CPU 推理,2-3秒性能可接受
- PNG 体积膨胀:初始版本默认输出 PNG,结果图片体积比原图大3-4倍。原因是 PNG 无损压缩对照片类图片效果不佳。改为默认输出 JPG 后体积与原图相近
- 未检测到人像时的重编码问题:早期版本即便不抹除也会执行 decode → encode 操作,导致 JPEG→PNG 格式变化和体积膨胀。改为直接回传原始字节流后问题得以解决
总结
本方案的核心思路简单而有效:与其期望AI不犯错,不如从输入端消除犯错的可能性。通过抹除服装图中的人像信息,虚拟试衣模型只能选择用户提供的模特图人脸,彻底消除模特冲突。
SegFormer B0 模型轻量且精准,CPU环境即可运行,部署成本极低。整个方案从调研到上线仅需一天,效果立竿见影。
