前阵子做红外场景的视觉方案时,遇到一个很实际的需求:手里有一批红外光图像,要在图上把人脸的五官部件一个个分割出来,眼睛、鼻子、嘴巴、头发,分别抠出对应区域,之后还要基于这些区域做温度统计和状态判断。最初以为这项工作跟平时的可见光人脸解析差不多,真正上手才发现红外图像的特性和普通照片差异很大,标注方案、格式处理、训练策略都要重新调一轮。今天就把我整理的一套“红外光图像人脸部件分割数据集”完整复盘一遍,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文件动辄几MB,886张下来体积很大,存储和之后的批量读取都非常蠢。用--nodata后,JSON中只有imagePath引用,体积可以压到几十KB,训练前再按路径去读图,流程干净很多。
第三,标注顺序也统一掉:先标头发,再标皮肤,然后按鼻子、嘴巴、左眼、右眼的顺序标五官。这个顺序不是为了好看,而是为了方便检查。因为后标的小部件通常会叠加在大区域上,如果在标注时不小心把五官画到了皮肤外面,后期检查时一眼就能看出来。另外,在红外面部轮廓模糊的情况下,先画一个大范围的皮肤区域,再在里面分割五官,比直接先画眼睛再补皮肤要稳定得多。
标注过程中常用的操作:画多边形时单击鼠标左键添加锚点,闭合时双击或按回车完成;标注完一个部件后按Ctrl+W保存当前文件。如果发现前一个点放歪了,可以按Ctrl+Z撤销;对已经完成的部件,用鼠标选中后拖动画点调整,或者直接删除重画。这些操作效率高不高,很大程度取决于个人习惯,但无论如何,保持每个部件的锚点数适中很重要。锚点太少,圆弧形边界会被直线割裂;锚点太多,标注时间暴涨,多边形还会出现锯齿。我的经验是眼睛这类小部件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.png,JSON却叫ir_0001.json;有的图像被resize过,但JSON里的imageWidth和imageHeight还是原始值。如果直接遍历JSON文件读图,很容易尺寸和坐标全部错位。
所以文件对齐放在最前面。我统一建了一个目录结构:images/存放原图,labels/存放JSON,processed/存放生成的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', encoding='utf-8') as f: data = json.load(f) mask = np.zeros(image_size, dtype=np.uint8) for label in DRAW_ORDER: for shape in data['shapes']: if shape['label'] != label: continue pts = np.array(shape['points'], dtype=np.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_ok=True) 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', encoding='utf-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(f'image 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(f'processed {idx + 1}/{len(json_names)}')这里写入的Mask是PNG灰度索引图,每个像素取值0到6。cv2.imwrite保存PNG是无损压缩,灰度值不会失真。但绝对不要用JPEG保存Mask,JPEG有损压缩会污染类别值,训练时会出现大量错误的类别混叠。
3.3 转换结果怎么自查
转换完成后,不能直接丢给训练,先做一轮可视化和统计。我常用的自查手段有三个。
第一个是逐张叠加可视化。把原红外图和对应的灰度Mask叠加显示,或者把Mask不同类别染成不同颜色,肉眼检查五官位置是否正确。这一步虽然土,但非常有效。当时抽查了100张,就发现有几张图眼睛和嘴巴位置完全不对,原因就是标注时对左右眼约定理解不一致,最后通过复查统一修正。
第二个是统计各类别像素占比。用一个脚本统计886张Mask中每个类别的像素总数和占比,输出类似下表:
| 类别 | 像素占比 |
|---|---|
| background | 52.3% |
| skin | 30.6% |
| hair | 9.8% |
| nose | 3.1% |
| left_eye | 1.4% |
| right_eye | 1.5% |
| mouth | 1.3% |
看到占比就知道眼睛和嘴巴这类小目标加起来只有几个百分点,如果不做损失平衡,模型大概率只学会预测背景和皮肤,对后续的部件级应用是致命的。
第三个是检查类别像素值是否越界。有时候JSON里混入一个未在CATEGORIES里定义的label,比如有人把眼睛标成了eye,转换脚本如果没处理,会直接跳过该多边形,导致该样本Mask里缺失这个类别。批量检查时用np.unique统计每个Mask的类别值,凡是出现大于6的像素值或者缺类别的样本,一律打回重新标注。
4. 拿886张红外图训练分割模型的关键配置
4.1 模型怎么选:轻量还是高精度
数据量只有886张时,模型结构的复杂度和最终效果之间要做取舍。首选是DeepLabV3+,Backbone用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 * DiceLoss
Dice损失对前景小目标的敏感性比IoU稍强,两个损失叠加,CE保持训练稳定,Dice把边界和细节拉回来。
关于类别权重的计算,给一条经验算式。假设某个类别像素占比为p,权重可以设为1 / sqrt(p),然后统一归一化。这样背景权重不会过大,眼睛这类小类别也不会被推得太高。如果直接用1/p,背景的权重很小,眼睛权重可能到几百倍,训练时梯度爆炸几乎是必然的。
4.4 训练过程、指标和结果怎么看
886张数据按大约7:2:1切分为训练、验证、测试,切分前先做“人员分组”,避免同一个人的多张图像同时进入训练集和验证集。红外人脸数据经常出现一个人的样本特别多、其他人只有几张的情况,如果随机切分,验证集里大概率混入训练集里出现过的人脸,指标会虚高。我采用的简单分组方式是:先统计每个人脸主体的样本数,把每个人作为一组,按组拆分。这和单张随机切分相比更接近真实使用场景。
训练时输入尺寸选384,batch size 8,优化器用SGD配合poly学习率衰减,初始学习率1e-2。DeepLabV3+用SGD普遍比Adam更稳。训练100个epoch后,主要观察mIoU,同时单独打印每个类别的IoU,尤其是left_eye和mouth的IoU。
实测下来,整体模型在测试集上的结果大约是:
| 指标 | 数值范围 |
|---|---|
| mean IoU | 0.72 ~ 0.78 |
| background IoU | 0.90以上 |
| skin IoU | 0.84 ~ 0.86 |
| hair IoU | 0.80 ~ 0.83 |
| nose IoU | 0.70 ~ 0.73 |
| left_eye IoU | 0.55 ~ 0.60 |
| right_eye IoU | 0.58 ~ 0.62 |
| mouth IoU | 0.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,没有转成int,cv2.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 |
| 验证指标忽高忽低 | 归一化方式不统一 | 归一化放进pipeline |
| loss曲线居高不下 | 训练初期就加入Dice | 先跑纯WeightedCE再切组合损失 |
| 戴眼镜样本全崩 | 眼镜区域干扰眼部特征 | 单列眼镜类别或剔除样本 |
6. 我对这套数据集的几点延伸体会
把886张红外人脸数据从标注做到能训练,整个过程走下来,我最大的感受是:数据量小不等于不能做,关键在于把每一张样本的质量和一致性控住。比起盲目收集更多图像,多花时间在标注规范、边界约定、格式合规上,对最终模型效果的提升更直接。
这批数据除了用于直接训练分割模型,之后还可以做几类扩展。一是基于分割结果提取每个部件的平均温度和温场分布,用于人体健康状态的统计;二是把分割结果作为先验输入给检测网络,做人脸对齐和小部件检测;三是通过合并左右眼、皮肤区域,把7类Mask降维成二分类或多分类的输入,用于疲劳状态判断。红外分割这个方向相对小众,公开数据集少,能用886张搭起一个可工作的pipeline,已经能解决不少实际需求了。
最后分享一个我在反复调参中觉得最划算的做法:训练之前,把所有Mask按类别随机填充成不同的颜色,在屏幕上逐张播放,同时对照原图用肉眼看一遍。不要觉得这个土,它能发现一大批格式和标注问题,而且往往比任何训练曲线都直观。这套流程后来我在别的分割项目里也一直在用,对数据集质量的把控,它就是最简单也最有效的杀手锏。