
简介面向需要开展头发图像语义分割研究的开发者该资源包提供约2200张已完成预处理的头发图片与配套标签按2类别分割任务组织可直接用于模型训练。数据集已划分妥当训练集包含约1570张原图及对应mask验证集约670张目录结构清晰适合作为图像分割算法入门练习或调优基准数据。包内另附一个Python可视化脚本可随机抽取样本并同屏展示原始图、GT标签及GT蒙版叠加效果方便快速核对标注质量与观察预测结果。整个压缩包共2000个文件以png标签图、jpg原图为主辅以py脚本整体大小52.9MB中小规模数据下载与搬运均较便捷。目前已有52人学习下载适合计算机视觉方向的学生、算法工程师或科研人员用于快速搭建头发分割实验并验证改进模型效果。1. 头发语义分割2000多张图能训出能用的分割模型吗头发语义分割是计算机视觉里一个非常具体的图像分割任务输入一张人像照片模型逐像素判断它属于头发还是非头发输出一张与输入同尺寸的掩膜。它和医学图像分割、人体分割一样属于语义分割的细分场景只区分类别不区分个体。手里有2000多张带标签的数据集已经足够从头训练一个能用的分割模型前提是标签格式、数据划分、模型选型和训练参数都踩在点上。这篇文章写给三类人做课程大作业的学生、刚转入视觉方向想练手语义分割算法的开发者以及要做发型推荐或换装类小工具的一线工程师。下面按我实际跑这个场景的顺序把数据、模型、参数和坑一次讲完。2. 先把数据盘明白2000多张图的标签格式、数据划分与增强拿到一个带标签的数据集先别急着上模型。头发分割属于数据敏感型任务标注质量、掩膜格式、图像尺寸分布这些细节直接决定后面是顺利收敛还是反复返工。这一章把数据侧的四件事讲透数据形态、标签格式转换、划分策略和增强手段。2.1 头发分割数据长什么样原图、掩膜和占比统计常见的数据包里每一对样本由一张原图和一个同尺寸的掩膜组成。原图是普通的 JPG 或 PNG 人像掩膜是黑白 PNG白色区域表示头发黑色表示背景、皮肤、衣服等非头发区域。做这个任务的第一步不是看几张图觉得“挺清晰”就开工而是写一个小脚本把整份数据普查一遍。import os from PIL import Image import numpy as np root dataset for name in sorted(os.listdir(root)): img_path os.path.join(root, name, image.jpg) mask_path os.path.join(root, name, mask.png) img np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path).convert(L)) h, w mask.shape[:2] hair_ratio (mask 127).mean() print(f{name}: image{img.shape}, mask{mask.shape}, hair_ratio{hair_ratio:.3f})这段脚本做三件事确认所有图像的尺寸是否一致确认掩膜尺寸是否与对应原图一致统计每张图里头发像素占比。逻辑很简单但作用很实在。hair_ratio 不是只看个热闹它决定后面要不要做类别加权、裁剪策略怎么定。如果一批数据里头发占比从 0.05 到 0.6 分布极不均匀训练时的损失曲线会明显震荡加权系数就不能按固定值拍脑袋。这里有两个细节容易翻车。一是convert(L)必须写。有些数据包的掩膜是三通道的 RGB 黑白图不转灰度直接参与计算会在损失函数里报 shape 不匹配。二是阈值要用 127 而不是 0。部分标注工具导出的 PNG 掩膜并非严格只有 0 和 255非头发区域可能残留 10 到 30 的暗色噪点直接用 0 做阈值会把背景噪点误判成头发。2.2 标签格式三种常见存法以及多边形转掩膜的脚本头发分割数据集的标签格式市面上常见三种第一种是单通道 PNG 掩膜名字和原图一一对应这是最省事的形式第二种是 VOC 风格原图为 JPG、掩膜为同名 PNG第三种是从标注平台导出的 JSON里面存的是头发轮廓的多边形顶点坐标需要先转成掩膜再进训练流程。第三种格式最值得单独处理。JSON 里的多边形可能是坐落在原图像素坐标系上的浮点坐标也可能经过了相对坐标归一化。读取之后必须第一时间确认坐标系的基准很多标注平台导出的是相对原图宽高的归一化值直接当作像素坐标会画出完全错位的掩膜。转换代码很简单import cv2 import numpy as np def polygons_to_mask(polygons, height, width): mask np.zeros((height, width), dtypenp.uint8) for poly in polygons: pts np.array(poly, dtypenp.int32).reshape(-1, 1, 2) cv2.fillPoly(mask, [pts], 1) return mask # 返回 0/1 掩膜保存时乘以 255这个函数的要点是dtypenp.int32和fillPoly的组合。OpenCV 的 fillPoly 不接受浮点坐标必须先取整而取整本身会让多边形边缘出现半像素误差对于头发这种细长结构顶点数量不够的多边形转出来会有明显折线感。如果发现转换后的掩膜边缘棱角太生硬优先回到标注数据里补点而不是依赖后处理去磨边。另外无论拿到哪种格式都要做一个掩膜与原图的对齐检查尺寸必须一致文件名必须一一对应不能存在只有原图没有掩膜或者反过来只有掩膜没有原图的情况。这个检查写成脚本一遍跑掉别手工翻目录。2.3 数据划分按人头还是按文件随机分结果差很多2000多张数据看着不少但如果划分方式不对整个实验的可信度都会打折扣。最典型的错误是直接对所有文件做随机划分train、val、test 各自分配 70%、20%、10%。表面上看没问题实际上一旦同一人物的多张照片同时落入训练集和验证集验证集指标会被严重虚高——因为模型见过这个人的脸型和发型验证时等于在“开卷考试”。正确做法是按人物主体划分。以人名为单位先把所有样本归到对应的身份 ID 下再对 ID 做随机划分保证同一个人的所有照片只在某一个集合里出现。这个逻辑用文字说明就够了但建议写成一个校验函数防止后面加数据时又把同一个人拆散。划分比例上我一般用 8:1:1。2000多张的规模下训练集大概 1600 张验证集 200 张测试集 200 张。如果数据量降到 1000 张以下就把比例调成 7:2:1但必须保证验证集至少 150 张否则 val 曲线的抖动会让你没法判断模型是否真的在变好。2.4 数据增强哪些该做哪些会让头发边界变糊头发分割的增强策略和通用分割任务有区别核心原因是头发是细长结构对空间变形特别敏感。我常用的增强管线基于 albumentations训练和推理共用同一套配置import albumentations as A import cv2 train_transform A.Compose([ A.HorizontalFlip(p0.5), A.Rotate(limit15, border_modecv2.BORDER_CONSTANT, value0), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.Resize(512, 512), ])水平翻转永远安全头发左右不对称但语义不变。旋转角度限制在 15 度以内是很重要的参数旋转过大会把头顶或发梢切出画面模型被迫学习“头发被截断”的错误模式。旋转后留下的空白区域用BORDER_CONSTANT填 0对应 mask 里的背景这是分割任务的常规默契。亮度对比度扰动幅度控制在 0.1够用了过强的颜色扰动会让深色头发和深色背景之间的边界更难分辨。不建议做的增强有两个。一是RandomSizedCrop这类大范围随机裁剪容易把刘海和头皮切掉一半模型学到的是残缺的头发语义二是强缩放头发丝在缩放过程中会产生锯齿等于给标签主动注入噪声。如果需要一定尺度泛化能力用Resize配合 0.15 以内的随机尺度缩放比大裁剪稳妥得多。3. 模型选型与训练U-Net 先行参数与评估指标一起定下来数据准备好之后进入模型环节。头发语义分割在模型选型上不需要追赶新潮U-Net 及其变体是这个任务的可靠起点配合合理的训练参数就能拿到可用的效果。这一章讲清选型理由、训练脚本、评估指标和可视化验证。3.1 为什么先选 U-Net语义分割和实例分割别混用先明确一个边界头发分割是语义分割任务不是实例分割。语义分割只回答“这个像素是不是头发”不关心它是属于第几个人的第几缕。这意味着不需要 Mask R-CNN 或 YOLO 系里的实例分割分支那些方案要额外处理实例聚类对头发这种大面积不规则区域属于绕远路。当然如果你想做“给每缕头发编号”这种更细的需求才需要考虑实例分割。在这个前提下U-Net 是性价比最高的选择。它的编码器用预训练的 ResNet34 或 ResNet50 提取特征解码器通过跳跃连接恢复空间细节。头发分割最关键的信息是边界而 U-Net 的跳跃连接恰好把浅层的高频细节传到了深层比纯 encoder-decoder 结构更贴合任务。DeepLabv3 也是常见选择它的 ASPP 模块擅长捕捉多尺度上下文。但在 2000多张数据量下DeepLabv3 的感受野优势并不明显训练速度和显存占用反而不如 U-Net 友好。我的习惯是先用 U-Net 跑通全流程如果后续发现边界模糊或大块区域漏检再换 DeepLabv3 做对比实验。3.2 训练脚本骨架损失、优化器与关键超参训练入口通常是一个 train.py核心骨架并不复杂难在参数匹配。以下是我整理的一套可供直接修改的训练循环逻辑import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset import cv2 import numpy as np class HairDataset(Dataset): def __init__(self, items, transformNone): self.items items # list of (img_path, mask_path) self.transform transform def __getitem__(self, i): img cv2.imread(self.items[i][0]) # BGR mask cv2.imread(self.items[i][1], 0) # 灰度 mask (mask 127).astype(np.float32) if self.transform: aug self.transform(imageimg, maskmask) img, mask aug[image], aug[mask] img torch.from_numpy(img.transpose(2, 0, 1) / 255.0) mask torch.from_numpy(mask).unsqueeze(0) return img, mask def dice_loss(pred, target): pred torch.sigmoid(pred) inter (pred * target).sum() denom pred.sum() target.sum() 1e-6 return 1 - 2 * inter / denom criterion lambda pred, target: nn.BCEWithLogitsLoss()(pred, target) dice_loss(pred, target) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100)这段代码里的关键参数值得逐个说明。输入尺寸 512x512头发是细长结构256 会让发丝边界糊成一团768 则对显存要求过高单张 3060 或 2080Ti 跑 512 是合理平衡点。batch_size 按显存设在 8 到 16 之间再大收益有限。学习率 1e-4 是我在分割任务上的常用起点AdamW 配 2e-3 容易在初期震荡配 1e-5 又收敛太慢1e-4 配合余弦退火最稳。损失函数用 BCE 加 Dice 的组合BCE 提供稳定的梯度信号Dice 直接优化背景和头发之间的不平衡。weight_decay 设 1e-4对 2000多张数据量是有必要的约束。如果你拿到的是单类别掩膜输出层用 1 个通道加 Sigmoid而不是 2 个通道加 Softmax。这是语义分割里最容易写错的细节之一二分类分割用 2 通道输出在数学上等价但训练稳定性差不少。3.3 评估指标mIoU、Dice 和像素精度该看哪个训练完看指标最常见的新手操作是盯着 accuracy但这在分割任务里几乎没有参考价值。头发在一张图中往往只占 15% 到 30% 的像素模型全预测成背景像素精度也有 80% 以上给人“效果不错”的错觉。正确指标是 mIoU 和 Dice。def compute_metrics(pred_mask, gt_mask): pred (pred_mask 0.5) gt (gt_mask 0.5) inter (pred gt).sum() union (pred | gt).sum() iou inter / (union 1e-6) dice 2 * inter / (pred.sum() gt.sum() 1e-6) pa (pred gt).mean() return iou, dice, pamIoU 衡量预测区域和真实区域的交集占并集的比例对类别不平衡敏感是语义分割的通用指标。Dice 和 mIoU 在数值上高度相关Dice 略高一些两者都应当记录。我在 2000多张数据上的经验是U-Net 训到收敛后验证集 mIoU 在 0.85 到 0.90 之间属于正常范围Dice 在 0.90 到 0.94 左右。低于 0.80 说明大概率存在数据问题或训练参数问题盲目堆网络深度解决不了。像素精度PA只在附录里报一下不要用它做早停依据。3.4 可视化验证把预测 mask 叠回原图边界问题一眼看出指标是数字数字不会告诉你边界是平滑的还是带毛刺的。每次训练结束我会随机抽 30 张验证集图片把预测 mask 用叠加图形式保存下来人工过一遍。import cv2 import numpy as np def vis_result(img_path, pred_mask, save_path): img cv2.imread(img_path) overlay img.copy() color np.array([0, 255, 0], dtypenp.uint8) overlay[pred_mask 0] overlay[pred_mask 0] * 0.5 color * 0.5 cv2.imwrite(save_path, np.hstack([img, overlay]))绿色半透明区域是模型预测的头发。这种可视化能直接暴露三类问题刘海和头皮的交界处有没有糊成一片发辫的细长部分有没有断成两截深色头发和深色背景之间是不是被漏检。这些问题在 mIoU 上可能只差 0.02但上线后用户一眼就会看出不对。训练阶段每 5 个 epoch 输出一批叠加图比只看 loss 曲线更可靠。4. 避坑指南头发分割翻车的五个常见原因与排查路径这一章集中写我在跑头发分割时踩过的坑每一条都是“现象 → 原因 → 解决”的结构。新手按这个顺序排查能省下大量试错时间。4.1 边界锯齿和格子感标注边缘太硬现象是模型输出的 mask 边界有马赛克感头发丝边缘像砖块堆出来的放大看全是直角。原因是标注掩膜的边缘是硬二值化的而模型在编码器下采样过程中丢失了高频信息解码器又无法凭空恢复。U-Net 的跳跃连接能缓解但无法根治。解决办法有三种。一是给边缘像素更高的损失权重在损失函数里对距离边界 3 像素以内的区域乘以 2 到 3 的权重强制模型关注边界。二是训练时对 mask 做轻微高斯模糊sigma 取 0.5 到 1把硬边缘软化出一个过渡带模型学到的边界自然平滑。三是推理后处理用高斯模糊加阈值对预测结果做轻微修整。注意这套方法里只有软化 mask 是在“改标签”模糊过大反而会让预测边界向外扩散有的头发与皮肤交界会被吞掉sigma 别超过 1。4.2 预测大面积全黑类别不平衡在作祟现象是损失值在正常下降但一次 epoch 结束保存的可视化里预测 mask 几乎全黑只有零星几个像素被预测成头发。原因是头发像素占比太低BCE 损失被背景主导模型早期梯度全部指向“预测成背景更安全”的方向于是输出了全局最优但毫无用处的全背景解。解决思路是给正类加权。可以按全局统计计算w_pos n_neg / n_pos在损失里把正类的 BCE 项乘以这个权重更直接的是把 Dice 损失加上去它在数学上就是为不平衡设计的。还有一条辅助经验把初始学习率降到 5e-5 跑 10 个 epoch 观察如果预测依然全黑问题基本在损失配置而不是学习率。Focal Loss 也能用但实现复杂度高对头发场景收益不如 Dice 明显。4.3 训练集很好、验证集崩过拟合比你想来得早现象是训练集 Dice 一路涨到 0.95验证集 Dice 涨到 0.72 就不动了再往后 train 继续升、val 开始掉。2000多张图对于 ResNet 编码器的分割模型来说只算入门规模过拟合在第 40 个 epoch 前后就会显现并不需要模型特别大。解决办法按优先级排序第一加早停patience 设 15 到 20 个 epochval 不改善就停第二增强里的空间变换加码比如把旋转角度从 15 放宽到 20加入 Scale 范围 0.9 到 1.1 的随机缩放第三编码器换成 ImageNet 预训练权重这通常能带来 3 到 5 个点的 val mIoU 提升第四在解码器每一层后加 Dropout概率 0.1 即可。不要一上来就换更大的模型DeepLabv3 在这种规模的数据上过拟合只会更快。4.4 换一批新图效果骤降预处理管线不一致现象是在验证集上表现优秀把模型接到新拍的测试图上效果明显变差边界偏移、漏检增多。常见原因是推理代码和训练代码的预处理不一致训练时输入做了 resize 和归一化推理时没有或者训练用 RGB推理用了 BGR又或者归一化把像素除以 255而推理时忘了这一步。这个问题最可靠的规避方式是把图像读取、resize、归一化、通道转换封装成一个独立函数训练脚本和推理脚本都调用同一个函数不要各写一遍。另一个隐蔽的坑是灰度图输入有些用户上传的照片是单通道直接进入模型会报错或产生不可预测的结果推理管线里要统一先cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)。4.5 mIoU 好看但发丝断掉只看指标不看图的验收错觉现象是验证集 mIoU 0.88叠回原图却发现发缝断裂、刘海和皮肤糊在一起、发辫从中间断成两截。原因是 IoU 只衡量区域重叠程度对细长结构是否连续并不敏感一根头发断了 20%对 IoU 的影响可能只有 1%。这是我在真实项目里吃过亏的地方当时只盯着指标就准备上线结果被测试用户一眼看出问题。解决路径是双轨验收指标只看趋势决策靠可视化。每次训练结束抽 30 张不同发型的验证图人工看一眼重点检查发缝、刘海、长直发和卷发的边界。如果可视化经常出问题再引入边界 F1 这类对边缘敏感的指标做辅助评估它和肉眼观感的相关性比 mIoU 高得多。5. 落地最后一公里批量推理脚本与边界质量检查习惯训练跑通只是第一步真正交付时要能对一整批新图快速产出掩膜。这章给一个可直接使用的批量推理脚本以及一个我坚持至今的验收习惯。import os import cv2 import torch model.eval() with torch.no_grad(): for name in os.listdir(test_images): path os.path.join(test_images, name) img cv2.imread(path) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) x torch.from_numpy(rgb.transpose(2, 0, 1) / 255.0).unsqueeze(0).cuda() x torch.nn.functional.interpolate(x, size(512, 512), modebilinear) pred torch.sigmoid(model(x))[0, 0].cpu().numpy() pred cv2.resize(pred, (img.shape[1], img.shape[0])) cv2.imwrite(os.path.join(output_masks, name), (pred 0.5) * 255)这段脚本的逻辑是逐目录读图BGR 转 RGB归一化后 resize 到 512 输入模型输出概率图再 resize 回原图尺寸阈值 0.5 二值化保存。这里有一个容易被忽略的点概率图经过cv2.resize放大回原尺寸后边缘会再次软化此时阈值必须保持在 0.5 附近。如果你为了“更干净”把阈值提到 0.7长直发的发梢会大面积断裂这个坑值得记住。最后分享一个习惯跑完任何一轮训练我不会只看 val 指标而是随机抽 30 张图生成叠加图按“边界是否顺滑、发缝是否连续、深色头发是否漏检”三个标准过一遍再把模型交给下游。第一次做头发分割时我就是只盯 mIoU结果真实用户照片上发辫断成两截回头重新调损失又耗了一周。从那以后先看图、再谈指标成了我的固定流程这个动作救过我很多次。希望帮到你。本文还有配套的精品资源点击获取