这个需求从哪来

我们有个"只换模特"的功能,说白了就是给一张衣服场景图和一张目标模特图,让 AI 把场景图里的人换成新模特。但上线后成功率只有 50%——一半情况下 API 返回来的跟原图一模一样,一点没变。

为什么会这样?因为 API 收到的是两张图加一段文字,它自己猜哪里该换、哪里该保留。猜对了就成功,猜错了就返回原图。这个"猜"的动作太不可控了。

我们的解法

即梦 API 本身是支持局部重绘的,叫 i2i_inpainting_edit 接口。这个接口允许你传一张 Mask 图——白色区域代表"这里给我重绘",黑色区域代表"这里原封不动保留"。如果 Mask 足够精准,API 就不用猜了,直接按 Mask 干活,成功率理论上能接近 100%。

所以这个项目的核心就是:给衣服场景图生成分割 Mask,让 Mask 精准覆盖人物的头部(头发、眉毛、眼睛、鼻子、嘴巴、下颌)和皮肤区域,然后传给 API 做局部重绘。

经历了什么

第一步:搭一个 Mask 生成器

我们调研了一圈,决定用三把刀组合:

  1. rembg U-2-Net — 专门做人物分割的模型,把人从背景里抠出来
  2. dlib 68点人脸关键点 — 精确定位面部五官的几百个点位
  3. HSV + YCrCb 肤色检测 — 在色彩空间里把皮肤区域捞出来

三者叠加:rembg 负责圈定"这里有个人",dlib 负责精确定位"五官在哪",肤色检测负责"皮肤区域有哪些"。最后做个交集运算,就得到了一张精准的 Mask 图。

Mask 生成后转成 Base64 字符串,通过 extra_body={"req_key":"i2i_inpainting_edit","mask":...} 传给即梦 API。

第二步:改 TaskManager 支持 Mask

原来的 API 调用是两张图 + prompt,现在要变成两张图 + prompt + mask。我们修改了 _call_doubao_with_prompt 方法,加了一个 extra_mask_base64 参数——有 mask 就传,没有就走原来的普通模式。

_process_change_model_only_scenario 里,我们在 Stage 2 调用 API 之前,先从衣服场景图生成 mask,再塞给 API。

prompt 也改了,原来说"把图1的人换成图2",现在说"只重重绘皮肤和面部,衣服背景100%保留",让模型知道该干什么。

第三步:调错 mask 来源(栽了一个坑)

一开始 mask 一直生成不出来像样的效果,我们反复调 rembg 参数、调肤色阈值、调扩张比例,怎么都不对。

后来才发现:mask 根本上错了图片

我们一开始以为 mask 应该从"目标模特图"生成,因为那是"替换后的目标"。但实际上 mask 应该从"衣服场景图"生成——因为即梦 API 要知道的是:在衣服场景图里,哪些像素需要被替换。如果 mask 画的是目标模特的脸,那 API 根本不知道该动衣服场景图里的哪块区域。

这个错误很致命,浪费了大概两天时间。

第四步:dlib 模型文件找不到

好不容易 mask 能生成了,一跑发现面部检测一直报错:Unable to open shape_predictor_68_face_landmarks.dat

原因是 dlib 有个默认的模型搜索路径,但我们的模型文件放在 ~/.face_recognition_models/ 目录下,dlib 找不到。

我们尝试过几种解法:设环境变量、拷到系统目录、用相对路径……最后用的是在代码里 os.environ["DLIB_MODEL_PATH"] 指向我们存放模型的绝对路径,然后 predictor 构造时用完整绝对路径。

第五步:numpy 2.x 跟 opencv 4.9.0.80 打架

某天突然跑不起来了,一查发现 numpy 升到了 2.x,但 opencv 4.9 编译时用的是 numpy 1.x 的 API,接口对不上。

解决方法是锁版本:numpy>=1.26.0,<2

第六步:setuptools 升级后 pkg_resources 被删

face_recognition 依赖的某个老库调用了 pkg_resources,但 setuptools 82.x 把这个模块移除了,导致 import 就崩。

降级 setuptools 到 <71 版本解决。

第七步:面部 Mask 覆盖不全(最核心的问题)

mask 生成跑起来了,但 API 返回的图一看——头发缺了一块、眉毛没了、嘴巴位置不对。我们盯着 Mask 图研究了半天,发现问题出在 get_face_mask 函数上。

这个函数最初的实现极其简陋:face_recognition 返回一个粗边框,我就往外扩一圈当 mask。dlib 那边稍微好一点,用 68 点做了个凸包填充,但也就盖住五官附近一块小区域。

头发根本不在 mask 里——因为头发不在任何一个人脸关键点上。眉毛、眼睛、鼻子、嘴巴虽然有对应的点位,但凸包连出来的区域很小,根本没扩展到周围去。

用户直接投诉了这句话:"你没有把头发识别进去,也没有把眉毛、眼睛、鼻子、嘴巴生成 mask,这个问题很严重。"

痛定思痛,我们重写了 get_face_mask

  • 头部整体轮廓:用 dlib 68 点凸包算出头部外接椭圆,向上扩展 150% 来覆盖头发区域
  • 眉毛:points[17:27] 各自做凸包填充
  • 眼睛:points[36:48] 做凸包填充
  • 鼻子:points[27:36] 做凸包填充
  • 嘴巴:points[48:68] 做凸包填充
  • 下颌:points[0:17] 做凸包填充

这样整张脸加上头发都覆盖进去了,不是简单的一个矩形。

编辑代码时字符串匹配失败

项目后期我想在 get_face_mask 函数里做一次大的重写,用 Edit 工具传入 old_string,但连续好几次都报错说匹配不上。猜测是文件里有不可见字符或者编码问题。

最后解决方法是:先用 Read 工具精确读出要改的那段代码,然后原封不动地把 Read 到的内容复制为 old_string,这才匹配成功。

总结

这个项目的本质是一个提示词工程 + 图像处理的结合体。最大的坑不在于"怎么调用 API",而在于:

  1. Mask 必须画对位置——画错了图片、画小了区域,API 就不知道该动哪里
  2. Mask 必须覆盖完整——头发、眉毛、眼睛、鼻子、嘴巴缺一不可,否则生成结果会缺零件
  3. 环境依赖很脆弱——numpy 版本、setuptools 版本、dlib 模型路径,任何一个不对都会导致跑不起来

现在回头看,整个项目的核心洞见是:让 AI 执行"只换模特"任务时,不要让它猜,而要精确告诉它"动哪里"和"不动哪里"。 Mask 就是那个精确指令的载体。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐