资讯动态

红外人脸部件分割数据集:886张图+labelme标注到训练全流程

发布时间:2026/10/22 4:35:50 来源:尧图企业网站定制
前阵子做红外场景的视觉方案时遇到一个很实际的需求手里有一批红外光图像要在图上把人脸的五官部件一个个分割出来眼睛、鼻子、嘴巴、头发分别抠出对应区域之后还要基于这些区域做温度统计和状态判断。最初以为这项工作跟平时的可见光人脸解析差不多真正上手才发现红外图像的特性和普通照片差异很大标注方案、格式处理、训练策略都要重新调一轮。今天就把我整理的一套“红外光图像人脸部件分割数据集”完整复盘一遍886张图像、7个类别、labelme格式标注从数据集怎么设计、标注规范怎么定到JSON转Mask的完整流程再到小样本训练的关键配置和坑点记录一路走下来的经验都写在这里。不管是自己准备搞红外分割数据还是想拿现成数据集练手的人脸方向同学这篇都值得扫一眼。1. 红外人脸部件分割这件事到底难在哪1.1 红外图像和普通照片不是一回事红外热像仪捕捉的是物体表面热辐射的差异输出的灰度值本质上是温度的映射。人脸区域因为血液供应充沛温度普遍比环境高面部轮廓在热图像里通常能看出来但我们习惯用来区分五官的纹理、颜色信息全部没有了。眼睛和嘴巴周围的温度分布与脸颊有差异可在灰度图上这些差异经常是一两个灰度级的事肉眼看起来非常模糊。所以标注的时候不能像看可见光照片那样轻松地描边界必须盯着温度梯度来推断。这种特性也直接影响模型。常规的RGB预训练模型第一层卷积学到的是颜色边缘和纹理组合红外图只有单通道灰度颜色信息被直接剥离网络很难把预训练权重里的颜色特征用上。我一般会先把单通道图像复制成三通道喂给预训练骨干同时也单独训练过从头开始的轻量网络发现后者在小数据集上反而更容易过拟合前者加数据增强、加早停实际效果更稳。1.2 886张、7个类别这个规模能做什么很多人一听到886张第一反应是“太小了”尤其在语义分割任务里动辄几万张的公开数据集才算正常。但实际做垂直场景时886张经过严格标注的红外人脸数据足够完成一些有意义的事。如果任务是做二分类的“人脸/背景”分割这个量级甚至有点浪费但要覆盖面部的五个部件加头发每个类别能分到的有效样本并不宽裕。从形态上说这批数据主要由三种来源构成一部分是近景正面人脸一部分是带偏转角度的半侧面还有少量是多人同框场景。多人的情况标注时只标注主目标人物其他人物不标这个约定在数据说明里写清楚不然转换时会漏掉一些shapes。如果后续想扩展成多目标分割这种“一人标注”的约定就要重做。886张的构成包含性别、年龄段等自然分布的差异但没有刻意做配额平衡如果模型要用在特定人群上比如儿童或老年人后续需要再补样本。这个规模最适合的做法是微调。选择一个在通用分割任务上预训练好的模型比如DeepLabV3或者带预训练Encoder的UNet把骨干冻结或者用较小学习率微调在886张上训练几十到一百个epoch能得到稳定可用的效果。如果目标是部署在嵌入式设备上做实时推理这批数据也足够用来蒸馏一个小模型或者做剪枝量化前的基线评估。简单说886张不属于“训练出奇迹”的量级但属于“验证可行、打磨流程、跑通pipeline”的合适量级。2. 类别怎么定、标注怎么做、labelme格式怎么解开2.1 7个类别的边界约定7个类别最终定为背景、脸部皮肤、头发、左眼、右眼、鼻子、嘴巴。这套划分参考了常见的人脸解析类别体系同时砍掉了眉毛和耳朵。眉毛在红外图上温度梯度非常弱和皮肤区域很难区分强行标注会导致不同标注员之间的主观差异极大耳朵则经常被头发或背景遮住样本的一致性也很难保证干脆不放进核心类别里。每个类别的边界需要事先写成标注规范不然几个人一起标最后合数据时一定会对不上。我按一套统一的边界规则来执行脸部皮肤从发际线开始沿面部外轮廓延伸到下颌左右到耳朵根部耳朵不纳入皮肤区域下沿到下巴底部。头发覆盖头皮的所有毛发区域包括发际线外扩散的碎发和披在脸侧的头发。头发稀疏或露出头皮时仍然按毛发覆盖区域标注透出的头皮不单独画。左眼观察者的左侧即图像中的左半边。标注范围是上下眼睑之间的可见区域包含眼球、睫毛和闭合时的眼线不含眉毛。右眼同左眼规则镜像处理。鼻子从鼻根眉心稍下方到鼻尖横向覆盖鼻梁和两侧鼻翼不包括鼻翼与脸颊之间的过渡带。嘴巴上下唇轮廓及口裂区域张嘴时包含口腔可见部分但牙齿不单独成类算在嘴巴内部。这里要特别强调左右眼不要标反。如果图像没有经过水平翻转标注时按观察者的左右来理解。但很多同学在标注时是按自己的左右手去判断最后左右颠倒。这个问题在可见光数据集里常见红外图因为看不清人脸朝向更容易错。我的办法是统一在标注前给每张图加一个朝向备注或者用眼睛的相对位置判断左眼在图像中一般位于鼻子的左侧观察者视角这个约定一旦定了就不要改。2.2 用什么工具、按什么流程标注标注工具直接用labelme这是一个使用非常广泛的开源标注工具支持多边形标注保存结果为JSON。安装和启动命令很简单pip install labelme然后在终端里打开labelme。它支持标注后直接保存为JSON不需要额外转换标注格式。在开始标注之前我做几件可以减少后期返工的事情。第一准备一个labels.txt文件按固定顺序列出7个类别名background、skin、hair、left_eye、right_eye、nose、mouth。labelme启动时通过--labels参数传入标注时下拉列表只会出现这些标准名称避免不同人把“皮肤”标成“skin”、“face”之类的别名。注意background不在shapes里画它只是占位真正标注时用“未标注区域默认为背景”这个约定即可。第二通过--nodata参数启动labelme。这个参数的作用是保存JSON时不再把原图的base64数据嵌入JSON文件里。如果不对JSON做任何处理默认会把整张640x480的红外图以base64字符串存进去一个JSON文件动辄几MB886张下来体积很大存储和之后的批量读取都非常蠢。用--nodata后JSON中只有imagePath引用体积可以压到几十KB训练前再按路径去读图流程干净很多。第三标注顺序也统一掉先标头发再标皮肤然后按鼻子、嘴巴、左眼、右眼的顺序标五官。这个顺序不是为了好看而是为了方便检查。因为后标的小部件通常会叠加在大区域上如果在标注时不小心把五官画到了皮肤外面后期检查时一眼就能看出来。另外在红外面部轮廓模糊的情况下先画一个大范围的皮肤区域再在里面分割五官比直接先画眼睛再补皮肤要稳定得多。标注过程中常用的操作画多边形时单击鼠标左键添加锚点闭合时双击或按回车完成标注完一个部件后按CtrlW保存当前文件。如果发现前一个点放歪了可以按CtrlZ撤销对已经完成的部件用鼠标选中后拖动画点调整或者直接删除重画。这些操作效率高不高很大程度取决于个人习惯但无论如何保持每个部件的锚点数适中很重要。锚点太少圆弧形边界会被直线割裂锚点太多标注时间暴涨多边形还会出现锯齿。我的经验是眼睛这类小部件10到20个点足够皮肤和头发这类大区域控制在80个点以内。2.3 labelme的JSON字段逐个拆标注完成后每张图像会生成一个同名JSON文件。下面是一张480x640红外图的JSON核心结构{ version: 5.3.1, flags: {}, shapes: [ { label: hair, points: [[210.3, 88.1], [266.2, 71.6], [310.5, 92.4]], group_id: null, description: , shape_type: polygon, flags: {} }, { label: skin, points: [[172.1, 130.5], [295.8, 108.2], [312.4, 210.7]], shape_type: polygon } ], imagePath: ir_0001.png, imageData: null, imageHeight: 480, imageWidth: 640 }重点关注三块。imagePath是图像文件名转换脚本要从这里读图imageData如果是null就说明用了--nodata如果不为null则是一长串base64字符串后续处理时要先从JSON中删掉它shapes是标注的核心每个元素代表一个多边形label字段是类别名points是二维坐标列表每个点是一个[x, y]的浮点数组。有一个容易被忽略的点labelme保存点的坐标是浮点数而cv2.fillPoly这类函数要求整数坐标转换时要么四舍五入要么直接转成int类型。单独看无所谓但当检查最终Mask的类别边界时偶尔能看到一条细缝原因就是坐标转换的取整方式不一致导致的。所以我在转换脚本里统一用四舍五入再转整数并且所有类别都用同一个取整函数保证边界缝隙不会累积。3. 从JSON到训练Mask一套能跑的转换流程3.1 先解决图像和标注的对齐问题拿到886个JSON和886张红外图像之后第一步不是急着转Mask而是先做文件对齐。不同来源的数据经常会出现名称不一致有的图像叫IR_0001.pngJSON却叫ir_0001.json有的图像被resize过但JSON里的imageWidth和imageHeight还是原始值。如果直接遍历JSON文件读图很容易尺寸和坐标全部错位。所以文件对齐放在最前面。我统一建了一个目录结构images/存放原图labels/存放JSONprocessed/存放生成的Mask。转换脚本以labels目录为准遍历JSON再从images里读原图出现任何缺失都会在批量转换时打印出来。尺寸方面我的方案是保持原图尺寸训练不提前缩小。因为眼睛、嘴巴这类小部件在原图里可能只有50x60像素一旦缩到224x224细节就丢失了标注的精度也跟着浪费。如果图像太大比如超过1024可以切patch训练或者在标注时就规划好固定采集尺寸。3.2 批量转换的核心脚本转换的本质是把每个JSON里的多个polygon画到一张全零的uint8矩阵上每个类别对应一个固定的像素值。这里的关键不是画多边形本身而是绘制顺序。如果先画皮肤再画左眼左眼区域会被皮肤覆盖Mask里就只剩下皮肤了反过来先画左眼再画皮肤左眼又会压在其他区域上面。因为背景是默认0其他类别像素值空间不重叠所以合理顺序是从外围类别开始依次往内画。我按这套规则写了一个可复用脚本核心函数如下import json import numpy as np import cv2 CATEGORIES { background: 0, hair: 1, skin: 2, nose: 3, left_eye: 4, right_eye: 5, mouth: 6, } # 优先级从低到高低的先画 DRAW_ORDER [hair, skin, nose, left_eye, right_eye, mouth] def json_to_mask(json_path, image_size): with open(json_path, r, encodingutf-8) as f: data json.load(f) mask np.zeros(image_size, dtypenp.uint8) for label in DRAW_ORDER: for shape in data[shapes]: if shape[label] ! label: continue pts np.array(shape[points], dtypenp.float32) # 统一取整避免边界缝隙 pts np.round(pts).astype(np.int32) # 单点或退化多边形直接跳过 if len(pts) 3: continue cv2.fillPoly(mask, [pts], CATEGORIES[label]) return mask这个函数有几个细节要说明。CATEGORIES里的背景值0不参与画图因为整个矩阵初始就是0如果某张图整张都是背景Mask也天然成立。DRAW_ORDER顺序是头发、皮肤、鼻子、左眼、右眼、嘴巴。我把眼睛放在鼻子后面画嘴巴放在最后因为嘴巴在张开状态下会压到皮肤和周边区域最后画可以把细节保留。左眼和右眼之间没有覆盖关系画哪个都行但为了对称习惯先左后右。批量转换时还有个通用main函数import os def convert_all(json_dir, image_dir, output_dir): os.makedirs(output_dir, exist_okTrue) json_names [n for n in os.listdir(json_dir) if n.endswith(.json)] for idx, name in enumerate(json_names): json_path os.path.join(json_dir, name) with open(json_path, r, encodingutf-8) as f: data json.load(f) img_path os.path.join(image_dir, data[imagePath]) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: print(fimage not found: {img_path}) continue mask json_to_mask(json_path, img.shape[:2]) base os.path.splitext(name)[0] cv2.imwrite(os.path.join(output_dir, base _mask.png), mask) if (idx 1) % 100 0: print(fprocessed {idx 1}/{len(json_names)})这里写入的Mask是PNG灰度索引图每个像素取值0到6。cv2.imwrite保存PNG是无损压缩灰度值不会失真。但绝对不要用JPEG保存MaskJPEG有损压缩会污染类别值训练时会出现大量错误的类别混叠。3.3 转换结果怎么自查转换完成后不能直接丢给训练先做一轮可视化和统计。我常用的自查手段有三个。第一个是逐张叠加可视化。把原红外图和对应的灰度Mask叠加显示或者把Mask不同类别染成不同颜色肉眼检查五官位置是否正确。这一步虽然土但非常有效。当时抽查了100张就发现有几张图眼睛和嘴巴位置完全不对原因就是标注时对左右眼约定理解不一致最后通过复查统一修正。第二个是统计各类别像素占比。用一个脚本统计886张Mask中每个类别的像素总数和占比输出类似下表类别像素占比background52.3%skin30.6%hair9.8%nose3.1%left_eye1.4%right_eye1.5%mouth1.3%看到占比就知道眼睛和嘴巴这类小目标加起来只有几个百分点如果不做损失平衡模型大概率只学会预测背景和皮肤对后续的部件级应用是致命的。第三个是检查类别像素值是否越界。有时候JSON里混入一个未在CATEGORIES里定义的label比如有人把眼睛标成了eye转换脚本如果没处理会直接跳过该多边形导致该样本Mask里缺失这个类别。批量检查时用np.unique统计每个Mask的类别值凡是出现大于6的像素值或者缺类别的样本一律打回重新标注。4. 拿886张红外图训练分割模型的关键配置4.1 模型怎么选轻量还是高精度数据量只有886张时模型结构的复杂度和最终效果之间要做取舍。首选是DeepLabV3Backbone用ResNet50这是分割任务里稳定且工程化程度非常高的组合。为什么不用ResNet101因为数据规模撑不起来ResNet50在这个量级上已经足够表达面部的局部细节再往上参数翻倍随着训练轮数增加验证集loss往往会先下降后反弹衰退得非常快。如果目标是嵌入式实时推理我会换成BiSeNetV2或者带MobileNetV3骨干的LR-ASPP这类轻量网络在中低端设备上也能跑到实时缺点是精细边缘略差。实际操作中我先用DeepLabV3微调等精度验证可行后再用大模型去蒸馏轻量模型蒸馏之后的边缘质量会明显好于从886张小样本直接训练的轻量网络。这个“先大后小、蒸馏兜底”的路线在样本量不足时非常实用。U-Net也是选择之一在医学图像分割中表现很好对边缘细节敏感。但U-Net在小样本场景下如果Encoder部分用了预训练权重效果也不错如果从零开始训练我遇到过严重的过拟合。所以同样是886张U-Net的适用前提是有预训练Encoder或者搭配很强但合理的数据增强。4.2 数据增强要围绕红外特性来设计红外图像没有颜色信息所以不能用颜色抖动。我的增强分成几何和灰度两部分。基础几何增强包括左右翻转、随机旋转正负5度、随机缩放0.9到1.1、随机裁剪到固定尺寸。水平翻转在这里有一个坑翻转后左眼和右眼必须交换不能简单地把label保持原样直接训练。在pipeline里处理label翻转时把4和5两个像素值做一个交换操作同时镜像整张Mask增强后的图像和Mask才能保持语义一致。灰度变换这块我用的不是普通的亮度抖动而是模拟红外相机响应变化对整张图乘一个0.8到1.2的随机系数加一个正负10的偏置再对局部区域做高斯模糊模拟镜头离焦。之所以强调响应变化是因为红外成像受环境温度影响很大同一张人脸在不同环境温度下灰度分布会整体偏移。在训练时引入这种全局偏移模型对温度漂移的鲁棒性会提升不少。红外图的噪声通常是椒盐噪声和可见光常见的高斯噪声不同。增强时加入少量椒盐噪声模拟传感器干扰效果比高斯噪声更贴近真实。这个细节不调整模型在实际传感器数据上的精度会打折。这里还要提醒一句不要把增强叠加得过于激进尤其是旋转和裁剪同时使用时面部边缘很容易被裁掉眼睛嘴巴这类小部件甚至会跑到画面外。我在调试时遇到过增强太强训练集loss很低但验证集反而更差的情况因为模型学会了处理“残缺人脸”却对完整人脸不敏感了。后来把旋转限制在正负5度以内裁剪比例不低于0.85问题才消失。4.3 损失函数怎么平衡极端类别前面统计过眼睛和嘴巴的像素占比可能只有1%上下常规交叉熵损失在这么极端的不平衡下会完全失效。我这里的做法是分阶段调整损失。第一阶段用带类别权重的交叉熵类别权重根据训练集统计的像素占比来计算但要做截断避免背景权重过低、眼睛权重过高导致训练初期震荡。第二阶段在loss里加入Dice损失最终使用组合损失L 0.6 * WeightedCE 0.4 * DiceLossDice损失对前景小目标的敏感性比IoU稍强两个损失叠加CE保持训练稳定Dice把边界和细节拉回来。关于类别权重的计算给一条经验算式。假设某个类别像素占比为p权重可以设为1 / sqrt(p)然后统一归一化。这样背景权重不会过大眼睛这类小类别也不会被推得太高。如果直接用1/p背景的权重很小眼睛权重可能到几百倍训练时梯度爆炸几乎是必然的。4.4 训练过程、指标和结果怎么看886张数据按大约7:2:1切分为训练、验证、测试切分前先做“人员分组”避免同一个人的多张图像同时进入训练集和验证集。红外人脸数据经常出现一个人的样本特别多、其他人只有几张的情况如果随机切分验证集里大概率混入训练集里出现过的人脸指标会虚高。我采用的简单分组方式是先统计每个人脸主体的样本数把每个人作为一组按组拆分。这和单张随机切分相比更接近真实使用场景。训练时输入尺寸选384batch size 8优化器用SGD配合poly学习率衰减初始学习率1e-2。DeepLabV3用SGD普遍比Adam更稳。训练100个epoch后主要观察mIoU同时单独打印每个类别的IoU尤其是left_eye和mouth的IoU。实测下来整体模型在测试集上的结果大约是指标数值范围mean IoU0.72 ~ 0.78background IoU0.90以上skin IoU0.84 ~ 0.86hair IoU0.80 ~ 0.83nose IoU0.70 ~ 0.73left_eye IoU0.55 ~ 0.60right_eye IoU0.58 ~ 0.62mouth IoU0.52 ~ 0.57背景和皮肤的IoU可以到0.85以上眼睛、嘴巴的IoU在0.55到0.65之间这就是小样本加红外模态下的正常水平。如果嘴巴IoU长时间低于0.4基本是标注问题而非模型问题该回头检查Mask了。5. 标注、转换、训练中的常见问题记录5.1 标注阶段的坑红外图上五官边界模糊是最大的坑。让几个标注同学同时标同一张图眼睛外轮廓的框选范围差异能达到十几个像素。解决办法是在正式标注前抽10张图做一轮试标由一个人制定标准其他人跟着标然后统计每个人与标准Mask的IoU低于阈值的一律返工。听起来繁琐但对886张数据来说花一个下午统一标准可以省下后期几倍的对齐时间。另一个容易被忽略的问题是眼镜。在红外图像中镜片会大幅衰减眼周热辐射导致眼睛区域变成一团低温暗区和周围皮肤的温度梯度很弱标注者靠肉眼判断时经常把镜片边缘当成眼睛边界。如果数据里有戴眼镜的样本最好的办法是单独建立一个眼镜类别或者在数据说明中明确剔除。否则要么眼睛分割效果很差要么模型会把镜片边缘学成眼睛特征。我在第一版数据里没有处理这一点测试时发现戴眼镜样本的眼睛IoU直接掉到0.2以下后来剔除了这类样本才恢复正常。标注阶段还要注意汇总检查。定期扫描所有JSON把shapes里的label去重后和标准7类做差集差值非空就输出文件名和异常标签。如果漏掉这一步训练时发现混入了整体轮廓之类的多余label模型会学到很奇怪的特征。5.2 转换阶段的坑与速查转换阶段最常见的坑一个是路径问题一个是类型问题。路径问题指的是JSON里的imagePath字段保存的是带目录的相对路径如果目录结构变了脚本直接用data[imagePath]去拼路径就会找不到图。解决方法是让脚本只依赖JSON所在目录下的同文件名单不信任JSON内部的路径。类型问题指的是points坐标是float没有转成intcv2.fillPoly直接报错或者画出错位图形。还有一个很隐蔽的问题是重复标注。我遇到过一张图hair被标了两遍一个覆盖很完整另一个只有一个残破的多边形转换出来后残破多边形覆盖在完整多边形上导致头发区域出现一块空洞。这个用统计特征就能发现就是那张图的hair像素占比比同类图明显偏低。修这个问题的办法是在转换脚本里加一步同一个label出现多个shape时把面积最大的作为主标注其他面积明显偏小的重叠区域直接丢弃或者人工复查该样本。现象可能原因处理办法某张Mask出现空洞同一label重复标注残破polygon覆盖主区域面积最大的polygon优先重叠交集丢弃图片路径找不到JSON内imagePath带相对路径不信任内部路径按labels目录同名匹配cv2.fillPoly报错coordinates是float统一转换成int类型图片和标注错位图像被resize但JSON尺寸未更新转换前做尺寸和路径对齐检查5.3 训练阶段的坑与速查训练阶段先说显存和输入尺寸。红外图是单通道如果直接用灰度图训练需要把单通道复制成三通道以匹配预训练骨干的输入层。复制后数据量比可见光图小很多但为了尽可能保存眼部细节输入尺寸不要低于256否则眼睛在特征图里可能只有一两个像素分割无从谈起。我试过把输入缩到224眼睛的IoU明显下降改回384后恢复。再说验证集和训练集统计不一致的问题。如果不统一归一化方式比如训练集用全图均值和标准差归一化验证集用单图归一化那么验证指标会非常不稳定一次验证一个结果。解决方案很简单把归一化做成训练pipeline的一部分而不是放在数据预处理脚本里。还有一个在loss上容易踩的坑如果在训练初期就加入Dice损失类别严重不平衡时Dice损失对背景的梯度响应很强会造成loss曲线居高不下看起来像不收敛。我以前也踩过后来改成先跑50个epoch的纯WeightedCE再切换组合损失最后测试指标反而更均衡。这在公开代码里不常看到属于自己试出来的经验。现象可能原因处理办法眼睛几乎学不出来输入尺寸太小输入尺寸至少256最好384验证指标忽高忽低归一化方式不统一归一化放进pipelineloss曲线居高不下训练初期就加入Dice先跑纯WeightedCE再切组合损失戴眼镜样本全崩眼镜区域干扰眼部特征单列眼镜类别或剔除样本6. 我对这套数据集的几点延伸体会把886张红外人脸数据从标注做到能训练整个过程走下来我最大的感受是数据量小不等于不能做关键在于把每一张样本的质量和一致性控住。比起盲目收集更多图像多花时间在标注规范、边界约定、格式合规上对最终模型效果的提升更直接。这批数据除了用于直接训练分割模型之后还可以做几类扩展。一是基于分割结果提取每个部件的平均温度和温场分布用于人体健康状态的统计二是把分割结果作为先验输入给检测网络做人脸对齐和小部件检测三是通过合并左右眼、皮肤区域把7类Mask降维成二分类或多分类的输入用于疲劳状态判断。红外分割这个方向相对小众公开数据集少能用886张搭起一个可工作的pipeline已经能解决不少实际需求了。最后分享一个我在反复调参中觉得最划算的做法训练之前把所有Mask按类别随机填充成不同的颜色在屏幕上逐张播放同时对照原图用肉眼看一遍。不要觉得这个土它能发现一大批格式和标注问题而且往往比任何训练曲线都直观。这套流程后来我在别的分割项目里也一直在用对数据集质量的把控它就是最简单也最有效的杀手锏。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑