ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于FaceX-Zoo的人脸口罩合成:实时数据增强与识别优化

基于FaceX-Zoo的人脸口罩合成:实时数据增强与识别优化 这几年做人脸识别相关项目躲不开一个需求给训练集里的人脸批量加上口罩。原因很简单现实场景里戴口罩的人太多了而公开数据集里几乎清一色是露脸的照片直接用这些数据训练模型一遇到口罩就崩。所以当时我盯上了FaceX-Zoo这个 PyTorch 人脸识别工具箱把Face Mask Adding人脸戴口罩合成做成了训练管线里一个标准环节。这个项目本身不复杂核心就是先检测人脸关键点再把口罩模板旋转、缩放、贴到正确位置但里面值得抠的细节不少。这篇博文我会从思路、环境、完整脚本到踩坑记录逐段展开不管你是想快速给数据集“戴”上口罩还是想把这套逻辑变成训练时的实时数据增强看完基本都能直接上手。1. 项目概述与核心思路1.1 这个项目到底做了什么先把任务说清楚避免有人被FaceX-Zoo这个拗口的名字吓住。Face Mask Adding 翻译成人话就是“给人脸戴上口罩”输入一张无口罩的人脸照片输出一张同样人脸但戴着口罩的照片而且口罩的位置、大小、角度都要贴合真实人脸不能飘在半空也不能歪到耳朵上。实现路径大致分三步。第一步用 MTCNN 或 RetinaFace 检测出人脸框以及 5 个关键点左眼、右眼、鼻尖、左嘴角、右嘴角。第二步准备一张带透明通道的口罩模板 PNG。第三步根据关键点计算口罩的旋转角度、缩放比例和摆放位置用 OpenCV 的warpAffine做仿射变换再用 alpha 融合把口罩贴上去。听起来不复杂真正做起来需要抠的细节全在第三步锚点选在哪里、口罩宽度按哪个距离换算、角度按双眼还是按嘴角算这些都会直接影响最终效果。为什么挂在 FaceX-Zoo 上做而不是自己从零写一套因为这个项目的目标不只是“合成几张图”而是要融入人脸识别模型的训练链路。FaceX-Zoo 已经把数据加载、backbone、head、损失函数、评测环节都封装好了我只需要把口罩添加逻辑做成一个 transform在训练时实时生成口罩样本。这么做既省了大量工程时间又能方便地在同一套代码里对比“戴口罩数据增强前后”对识别精度的影响实验做起来干净利落。1.2 FaceX-Zoo 为什么适合做这件事这里多说一句 FaceX-Zoo。它算是人脸识别领域比较系统的框架目录结构很清晰train部分管理训练流程configure里放配置文件util里是各种工具函数包括数据加载、关键点检测、评测指标等。和本项目关系最密切的部分是它对检测器的适配——FaceX-Zoo 默认把 MTCNN、RetinaFace 这类检测器封装好返回的人脸框和关键点坐标格式统一可以直接喂给后续数据处理流程。对人脸识别任务来说数据集质量基本决定了模型上限。公开数据集如 MS1M、WebFace 虽然量很大但几乎不含口罩样本。真要自己重新采集带口罩的人脸数据成本高、授权难、类间分布还不均衡。用 Face Mask Adding 做合成本质上是一种针对遮挡的数据增强它解决的是“训练分布和测试分布不一致”的问题。我实测下来在戴口罩识别场景下加入 30% 左右的合成口罩样本模型误拒率能有明显下降如果完全不加入模型几乎没法在真实口罩场景用。1.3 Face Mask Adding 的典型应用场景除了训练戴口罩人脸识别模型Face Mask Adding 还有几个常见用途。第一个是鲁棒性评测。把测试集里的人脸都合成上口罩再拿现有模型跑一遍看 top1 精度掉了多少可以快速判断模型对遮挡的敏感程度。这块我在项目里做得比较多它比临时找真实口罩照片测试要省事得多而且可以控制口罩颜色、大小、角度做得很细。第二个是故障复现。线上有用户反馈戴口罩总识别失败线下往往很难拿到真实的“识别失败”现场照片这时候用合成口罩去复现问题定位效率高很多。我记得有一次排查误拒问题就是用合成口罩数据发现是模型对下巴区域特征的依赖太重一旦遮挡就废问题定位非常快。第三个场景可能很多人没想到数据合规脱敏。有些客户要求展示人脸识别效果但又不能直接公开真实人脸照片加个口罩既能保留一定身份特征又起到遮挡作用。当然真要严格脱敏还要配合其他手段但口罩合成可以作为第一步。最后这个技术也能用在偏消费类的玩法上比如给相册里的人像试戴不同花色口罩、生成社交头像等。这些应用听起来轻量底层算法逻辑完全一样学会一套就够用了。2. 环境准备与核心组件解析2.1 环境搭建先把 FaceX-Zoo 跑起来环境部分不想写得太啰嗦但有几个坑要提前说。FaceX-Zoo 基于 PyTorch建议在 Linux 上跑GPU 内存 8G 以上比较舒服。基本安装流程如下git clone https://github.com/JDAI-CV/FaceX-Zoo.git cd FaceX-Zoo # 建议新建虚拟环境 conda create -n facex python3.8 conda activate facex pip install -r requirements.txtrequirements 里主要依赖 torch、torchvision、numpy、opencv-python、easydict、tqdm 这些。如果要用 MTCNN 检测人脸还需要额外装一下相关依赖或者直接用 FaceX-Zoo util 里的检测器封装。我的建议是第一次跑不要折腾自定义环境先把 requirements 装齐跑通官方 demo 后再动手改。有一个经验之谈FaceX-Zoo 的不同分支对 PyTorch 版本有要求老代码在 PyTorch 2.x 下偶尔会碰到 API 兼容问题。我实际踩过的坑是torchvision.transforms在接口上的小变化所以 clone 之后先看一下官方 README 标注的版本要求稳妥起见直接用 requirements 里锁定的版本。老项目配老环境这是我能给的最真诚建议。装好之后先跑一次数据加载和前向推理确认环境没问题再进正题。不要一上来就训练完整模型那样太慢只要确认检测器能出框、数据能过 pipeline 就行。2.2 核心组件人脸关键点检测与口罩模板口罩要戴得自然最关键的是知道“往哪儿戴”这就依赖人脸关键点检测。关键点规模有 5 点、68 点、106 点等不同选择Face Mask Adding 一般用 5 点就够了左眼、右眼、鼻尖、左嘴角、右嘴角。为什么不用更多因为口罩覆盖范围主要取决于眼睛到下巴这片区域5 个点已经能确定宽度、角度和中心位置再多点并不会显著提升贴合效果反而增加计算量。举个生活化的例子给一张照片里的脸戴口罩和你自己照着镜子戴口罩是一个道理。你会先看镜子里的自己找到鼻子和嘴的位置再把口罩提上去、对好角度、拉下来盖住下巴。程序做的事一模一样只不过它靠的是关键点坐标而不是眼睛和手。MTCNN 返回的 5 个关键点就是它的“镜子”。口罩模板建议用 PNG 格式、带 alpha 通道的图像。alpha 通道就是透明度信息黑色表示完全透明、白色表示完全不透明这样贴图的时候才能只把口罩本身显示出来而不是把整个矩形白块怼上去。模板分辨率最好在 300x300 以上太小了缩放之后会糊。颜色上至少准备两种白色医疗口罩和蓝色医用口罩也可以准备黑色口罩方便后面做多颜色数据增强。我后来为了测试模型对口罩颜色的鲁棒性还加了粉色、印花这类模板效果很不错。2.3 核心几何操作仿射变换与 alpha 融合拿到关键点、准备好模板后需要做两件事把口罩摆正、缩放到人脸大小然后和人脸合成到一起。第一步用仿射变换第二步用 alpha 融合。仿射变换可以理解成“把图片整体平移、旋转、缩放”。口罩从模板里的原始姿态变换到和目标人脸姿态一致的状态靠的就是这个东西。OpenCV 的getRotationMatrix2D里可以指定旋转中心、旋转角度和缩放比例一次调用就能得到变换矩阵再配合warpAffine就能得到变换后的口罩图像。这中间要注意变换之后图像的有效区域旋转会让图像边缘出现空洞必须用BORDER_CONSTANT填充透明像素不然会残留黑边。alpha 融合则是逐像素加权混合公式长这样result img * (1 - alpha) mask * alpha其中alpha是口罩模板透明度归一化到 0 到 1 的值。人脸区域原本是什么颜色混合后就往口罩颜色偏口罩外部 alpha 接近 0保持原图不动。这一步非常直观但注意边界不能硬切直接硬切会看到明显的矩形边缘。后面我会专门讲怎么用羽化处理这个问题。3. 实操用 FaceX-Zoo 给照片批量生成口罩人脸3.1 准备数据与口罩模板动手之前先把原料备齐。你需要三样东西一批无口罩的人脸图片、一个能输出关键点的检测器、一张或几张口罩模板。人脸图片建议用正规公开数据集或者自己合规采集的照片。图片格式无所谓jpg、png 都行但最好大小统一方便批量处理。检测器用 MTCNN因为它快、轻量对正面脸效果很好如果图片里有大角度侧脸建议换成 RetinaFace关键点更稳当然速度也会慢一些。口罩模板建议这样准备找一张干净的口罩图抠图得到透明底 PNG或者用程序生成一张带 alpha 通道的模板。重点是要保证 alpha 通道干净边缘不能有残留的半透明杂色否则贴到脸上会留下脏印。我第一次做的时候偷懒直接找了一张带白色背景的口罩 JPG 当模板结果口罩周围一圈白底全都贴上去了效果惨不忍睹。后来改成抠好的 PNG 透明底图问题才彻底解决。这里郑重提醒一定要用带 alpha 通道的 PNG不要用 JPG。3.2 核心脚本人脸口罩合成下面这段代码是整个项目的核心。它做的事情是读取一张人脸图用 MTCNN 检测关键点再读取口罩模板根据嘴部宽度确定缩放、根据嘴角连线确定旋转角度最后把口罩图层叠加到原图上。考虑到批量处理时人脸可能出现在图片任意位置我特意把口罩画到一张与原图同尺寸的透明图层上这样能避免边界裁剪的问题代码更稳健。import cv2 import numpy as np from mtcnn.mtcnn import MTCNN detector MTCNN() def add_mask(img_bgr, mask_bgra, kps): 将口罩模板贴合到人脸 :param img_bgr: 原图 BGR :param mask_bgra: 口罩模板 BGRA带透明通道 :param kps: 关键点 dict包含 left_eye, right_eye, nose, mouth_left, mouth_right :return: 合成后的 BGR 图像 h_img, w_img img_bgr.shape[:2] # 1. 计算口罩宽度以左右嘴角距为基准乘 1.4 让口罩盖住面部两侧 mouth_width np.linalg.norm( np.array(kps[mouth_right]) - np.array(kps[mouth_left]) ) scale mouth_width * 1.4 / mask_bgra.shape[1] # 2. 计算旋转角度以嘴角连线与水平方向的夹角为准 angle_deg np.degrees(np.arctan2( kps[mouth_right][1] - kps[mouth_left][1], kps[mouth_right][0] - kps[mouth_left][0] )) # 3. 对口罩模板做仿射变换旋转 缩放 h, w mask_bgra.shape[:2] rot_mat cv2.getRotationMatrix2D((w / 2, h / 2), angle_deg, scale) mask_warped cv2.warpAffine( mask_bgra, rot_mat, (w, h), flagscv2.INTER_LINEAR, borderModecv2.BORDER_CONSTANT, borderValue(0, 0, 0, 0) # 填充全透明防止黑边 ) # 4. 确定摆放位置anchor 取鼻尖与嘴角中心连线的中点 nose np.array(kps[nose]) mouth_center (np.array(kps[mouth_left]) np.array(kps[mouth_right])) / 2 anchor (nose mouth_center) / 2 mh, mw mask_warped.shape[:2] top int(anchor[1] - mh / 2) left int(anchor[0] - mw / 2) # 5. 创建与原图同尺寸的口罩图层避免切片越界 mask_layer np.zeros((h_img, w_img, 4), dtypenp.uint8) y0, y1 max(top, 0), min(top mh, h_img) x0, x1 max(left, 0), min(left mw, w_img) if y0 y1 or x0 x1: return img_bgr mask_layer[y0:y1, x0:x1] mask_warped[y0 - top:y1 - top, x0 - left:x1 - left] # 6. alpha 融合 alpha mask_layer[:, :, 3] / 255.0 alpha alpha[:, :, np.newaxis] mask_rgb mask_layer[:, :, :3] blended img_bgr * (1 - alpha) mask_rgb * alpha return blended.astype(np.uint8) def process(img_path, mask_path, save_path): img cv2.imread(img_path) mask cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) if mask is None or mask.shape[2] ! 4: raise ValueError(口罩模板必须是带 alpha 通道的 PNG) result detector.detect_faces(img) if len(result) 0: print(f未检测到人脸: {img_path}) return face result[0] kp face[keypoints] kps { left_eye: kp[left_eye], right_eye: kp[right_eye], nose: kp[nose], mouth_left: kp[mouth_left], mouth_right: kp[mouth_right], } out add_mask(img, mask, kps) cv2.imwrite(save_path, out) if __name__ __main__: process(face.jpg, mask_template.png, face_with_mask.jpg)这个脚本里有两个小细节要解释。第一scale取 1.4意思是口罩宽度是嘴部宽度的 1.4 倍这样才能盖住脸颊两侧而不是只盖住嘴。如果发现口罩窄了优先调这个系数。第二anchor 取鼻尖和嘴角中心的中间点目的是让口罩上边缘落到鼻子中段下边缘盖住下巴这符合人实际戴口罩的位置。再说说“基于 FaceX-Zoo”体现在哪。这段合成逻辑本身不绑定框架但实际项目中我是把它作为 FaceX-Zoo 数据管线里的一个 transform 挂进去的。FaceX-Zoo 的 data loader 在加载样本时会先做人脸检测、对齐再送进网络我只需要在加载阶段加一个概率判断比如 50% 的概率调用add_mask就能让模型在训练时动态看到带口罩的样本。这样合成图像不会额外占用磁盘空间而且模型每轮都可能看到新的口罩样本增强效果比一次性离线生成要好。3.3 从“能戴”到“戴得自然”的参数调优脚本能跑通只是第一步想让合成效果经得起肉眼检查还有几个参数要调。首先是口罩宽度系数。不同人脸结构差别很大嘴部距离一样的人脸宽可能差出两三个码。只按嘴角距离定宽会在宽脸上出现口罩盖不住腮帮的问题。我的做法是在关键点检测之外额外用两眼距离做个加权口罩宽度 max(1.4 * 嘴宽, 1.8 * 眼距)。这样更稳。其次是旋转中心。getRotationMatrix2D默认绕模板中心旋转这在把口罩放到 anchor 位置时一般够用但如果你想精细控制让口罩上边缘精确贴到鼻梁建议把 anchor 作为旋转后的参考点而不是直接套模板中心。实际操作中可以先旋转缩放好口罩再手动计算 anchor 与口罩中心的偏移并做修正。第三是边缘羽化。直接 alpha 融合在贴图边界处容易产生“硬边”尤其是原图像素较亮的时候特别明显。处理手法是给 alpha 通道做一次高斯模糊比如cv2.GaussianBlur(alpha, (5, 5), 0)让边界从 1 平滑过渡到 0。如果想让效果更真实还可以用 OpenCV 的seamlessClone做泊松融合。它的好处是能根据周围皮肤的亮度颜色自动调整口罩边缘的过渡让合成结果更像真实物理遮挡。缺点也很明显速度比 alpha 融合慢很多不适合超大批量。我的用法是只在最终精修或小范围验证时用。4. 常见问题与排查技巧实录4.1 口罩完全戴歪问题多半在关键点检测这是出现频率最高的坑。口罩歪绝大多数情况不是融合代码写错了而是关键点检测本身就偏了。特别是用 MTCNN 处理侧脸、眨眼、表情夸张的照片时嘴角关键点经常会飘嘴角连线角度一旦算错口罩自然跟着歪。排查方法很直接把检测到的关键点画到原图上可视化一眼就能看出问题。如果确实是关键点出错按优先级处理换成 RetinaFace 提升关键点稳定性过滤掉侧脸角度过大的图比如鼻子相对两眼中心的偏移超过阈值就直接跳过适当调检测器置信度阈值宁可漏检也不要拿错误关键点硬合成。这里有取舍漏检只是少一张样本硬合成进去的是脏数据会影响整个数据集质量。另外有个常见陷阱MTCNN 返回的关键点顺序可能因版本而异。官方文档里keypoints字段是left_eye、right_eye、nose、mouth_left、mouth_right但有的封装库返回的顺序是乱的。写代码之前一定要先打印出关键点坐标核对一遍不要默认顺序是对的。这个错误我犯过一次一晚上合成的图全是歪的后来才发现是键名映射问题。4.2 口罩边缘生硬像贴图而不是“戴”上去硬边是第二个高频问题。根源在于 alpha 通道在边界处直接从 0 跳到 1或者变换后的模板边缘有锯齿。两个解决办法第一个办法是对 alpha 做高斯模糊制造羽化效果。这是最省事也见效最快的方式适合批处理场景。第二个办法是用泊松融合我一般只在需要精修时用。此外还有一个容易被忽略的细节口罩模板本身的边缘。很多抠图模板边缘会残留一圈白色或青色半透明像素这在深色皮肤人脸时尤其明显。处理方法是先把模板 alpha 通道做一次二值化清理alpha 大于 200 的置 255小于 50 的置 0中间做平滑过渡。模板干净了后面融合才干净。4.3 批处理速度太慢怎么塞进训练管线如果要给 10 万张图离线加口罩Python 逐张跑 MTCNN 加warpAffine速度会非常感人。我在实践中做了三个优化。第一是缓存关键点。人的五官位置在单张图上是固定的离线生成数据集时先跑一遍检测把所有关键点存成 npy 或 json。后续不管换多少种口罩模板都不用再检测一次能省掉大头时间。第二是批量处理时复用口罩变换参数同一个角度、同一种人脸尺度用同一套旋转矩阵不要每张都重新算一遍角度和缩放。第三是如果目标只是训练模型不需要离线存图那就用我前面提到的在线 transform 方案把口罩添加放到 FaceX-Zoo 的训练 pipeline 里训练时实时生成省掉磁盘读写和预处理时间。还有一个工程小建议合成数据不是越多越好。我做过对比实验口罩样本比例在 30%-50% 时识别模型最稳比例太高反而会让模型对无口罩人脸的表现下降。如果口罩样本和无口罩样本混在一起训练batch 内部也要注意保证无口罩样本占一定比例否则会引入新的数据不平衡。5. 应用扩展与个人实操体会5.1 从数据增强到更多应用方向Face Mask Adding 这个技术点能延伸出去的方向还挺多的。最直接的是口罩佩戴检测把合成口罩图和原图配对天然就是一组带标签的数据可以用来训练“有没有戴口罩”的分类器比人工标注省太多事。我在一个边缘设备项目里就干过这事用合成数据初始化模型再用少量真实数据微调效果和全用真实数据标注训练的差不多成本却低了一截。另一个方向是人脸防伪。不少攻击样本会伪造戴口罩的人脸合成口罩数据可以用来训练活体检测模型对口罩区域的敏感度。再往后走同样的思路换一下模板就能做很多事给照片加眼镜、加装饰物、换发型遮挡甚至做人脸局部编辑。核心都是“关键点定位 模板仿射变换 alpha 融合”这三板斧学会 Face Mask Adding等于掌握了一套可复用的图像合成方法论换个素材就是新功能。5.2 我对 FaceX-Zoo 和整套方案的几点评价最后聊点主观感受。FaceX-Zoo 给我最大的价值不是某个具体算法有多强而是它把“训练一个人脸识别模型”这件事变成了一条标准流水线数据、backbone、head、损失、评测全部模块化。你不需要纠结怎么组织代码只需把心思花在任务本身。对做工程的人来说这种省心感很重要。Face Mask Adding 这套方案也有它的局限性。合成口罩和真实口罩之间始终存在域差异真口罩有褶皱、有绳子勒痕、有呼吸导致的水汽合成数据很难模拟得一模一样。所以它适合作为数据增强手段但替代不了真实场景数据的采集。真正严谨的项目应该是合成数据打底再配合少量真实口罩数据做微调两条腿走路。我自己大概用这套流程处理过 6 万多张人脸图前前后后改了不少版本。最开始的代码把所有逻辑堆在一个函数里口罩角度、位置全靠估效果飘忽不定后来把关键点检测、仿射变换、alpha 融合拆开每一步都能单独验证稳定度一下就上来了。这算是我在整个项目里最深的体会图像合成这类任务bug 往往不在最后融合那一步而是前面的几何关系没理清楚。口罩贴合、边缘处理、数据比例这几个关键点多试几次你也会找到最适合自己数据的那组参数。
返回列表