ARTICLE DETAIL

资讯详情

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

面部眼镜图像分割数据集:16000张带掩码图片,助力UNet高效训练

面部眼镜图像分割数据集:16000张带掩码图片,助力UNet高效训练 简介面向图像分割方向的研究者与初学者这份面部眼镜图像分割数据集提供了约一万六千张人脸图像及对应像素级标注可用来完成背景与眼镜两个类别的分割模型训练和评估。数据已按约一万一千二百张训练集和四千八百张测试集划分每张图片均配有独立掩膜图片可直接用于U-Net、DeepLabV3等常见语义分割网络。压缩包共二千个文件除主图像与标注图片外还包含类别说明文本文件以及可视化展示脚本整体体积约八百四十九兆字节。该脚本能够随机抽取样本并在一张画布中同时展示原始图像、真实标注以及标注叠加在原图上的效果便于直观核对标注是否正确。目前已有九十一人学习适合作为入门级分割实验、课程设计或算法对比的数据基础。1. 面部眼镜图像分割数据集自带mask的16000张图省掉的不只是标注时间做图像分割的人最清楚标注的痛人脸眼镜这类细长结构多边形描边动辄几十个点镜腿和镜片边缘稍不仔细就会标歪一张图耗时在五分钟以上。这份面部眼镜图像分割数据集恰好把最耗时的标注环节打包好了。约16000张图片与对应掩码标注类别只有两类——背景和眼镜。训练集约11200张测试集约4800张全部是png格式原始图与掩码同名一一对应。无论你是刚接触语义分割想找一份结构清晰的标注数据跑通UNet图像分割还是做人脸相关项目需要一个眼镜区域的先验来辅助检测或分割这份数据都能直接落地。它还附带一个可视化脚本随机抽一张图就能把原图、GT掩码、GT在原图上的蒙板叠加图一次展示并保存到本地。2. 数据集的目录与文件命名从训练测试划分到sgseg后缀的解读2.1 训练集和测试集的目录对应关系数据集的顶层划分为训练集和测试集两部分每一部分下都有独立的images目录和masks目录。images存放原始采集图像masks存放与图像同名的分割掩码。两张图之间靠文件名作为唯一关联键这种做法在分割数据集里最常见优点是迁移方便PyTorch 的Dataset类里只需拼接两个路径就能完成加载缺点也很直接——任何人删了一张原图必须同步删掉同名掩码否则训练时就会报文件缺失或数据错位。拿到数据后我建议先做一遍完整性核对。下面这段 bash 脚本遍历训练和测试目录检查每张原图是否都有对应掩码for split in train test; do echo $split 集检查 img_count$(ls $split/images/*.png 2/dev/null | wc -l) mask_count$(ls $split/masks/*.png 2/dev/null | wc -l) echo images 数量: $img_count echo masks 数量: $mask_count # 逐一确认同名掩码是否存在 for f in $split/images/*.png; do name$(basename $f) [ -f $split/masks/$name ] || echo 缺失掩码: $split/masks/$name done done这段脚本先分别统计images和masks下 png 文件的数量然后进入第二个循环逐个检查每张原图是否能在masks目录下找到同名文件。若打印出“缺失掩码”条目说明数据不完整需要补文件或从训练清单中剔除。注意两点一是数据集内原图和掩码后缀都是 png这个检查可以直接比较完整文件名二是如果以后自己造数据掩码用 png 单通道存储是惯例jpg 的压缩会引入伪影不要混用。2.2 文件名结构Glass编号、表情标签和场景标识文件名不是随机哈希而是带有清晰的元信息。随便挑一条样本拆解img-Glass002-414-19_cheek_blowing-1-epping_forest_02-024-sgseg.png按连字符切段后每段含义大致如下段示例值含义前缀img图像标记眼镜款式Glass002眼镜款式的编号同款眼镜会有多张图人物编号414采集对象标识同一人物有不同表情帧表情标签19_cheek_blowing姿态或表情描述如鼓腮、闭眼、露齿笑、下颌左移帧序号1同一表情下的帧序号场景标识epping_forest_02采集背景场景的名称拍摄序号024该场景下的连续拍摄序号用途后缀sgseg该图属于分割标注数据集这种命名的价值主要体现在两个地方。第一做子集划分时可以直接解析文件名生成标签想验证模型在不同眼镜款式上的泛化能力按Glass编号分组做 cross-validation想测试模型对陌生场景的鲁棒性按场景标识留出几个未见过的场景做测试。第二表情标签对训练策略有实际影响——grin和cheek_blowing这类表情会让人脸下半部分的皮肤纹理出现明显褶皱眼镜框与脸颊的贴合区域也随之变化这类样本的掩码边界往往比中性表情难学。我一般会在读取数据时就把这些字段解析出来写进一个索引文件后期做数据筛选就不必反复扫描文件系统。2.3 classes文件里的类别映射数据集包含一个classes文件定义分割的类别索引。这属于二分类分割任务标准映射关系是0: background 1: glasses也就是说掩码图中像素值为 0 的位置代表背景像素值为 1 的位置代表人脸佩戴的眼镜区域。训练时输出的特征图通道数设为 2分别对应这两个类别的预测概率而不是 3 或更多。这一步看起来简单但翻车的比例相当高——有人会把掩码当作灰度图读出来之后发现里面全是 0 和 255再去找网络输出通道怎么对不上实际上问题出在读取方式而不是网络结构。classes文件的作用就是把这种映射固化下来训练和评估时都以它为准。3. 眼镜分割的掩码语义与可视化二分类标签的像素级理解3.1 背景与眼镜两类别的标注粒度这个数据集只标两个类别背景和眼镜。这里的“眼镜”指的是完整佩戴在人脸上的眼镜区域包括镜片、镜框和镜腿在可见范围内的部分。掩码是像素级的精细标注不是目标检测那种矩形框所以掩码中眼镜区域的形状会紧贴镜片轮廓镜腿与耳朵接触的位置也有独立像素表达。这类细长结构的标注对分割模型是很好的压力测试——如果模型用下采样倍数过大的编码器比如直接缩到原来的 1/32镜腿这种几个像素宽的区域很容易在特征图上被抹掉。与常见的街景分割、医学图像分割数据集相比这类数据集的类别数很少但目标的几何特性更极端。眼镜在整张图里占的比例通常只有 5% 到 15%大量像素属于背景。这个比例直接决定了损失函数的选择如果直接拿普通交叉熵训练模型很容易把所有像素都预测成背景因为这样做的损失已经很低了。后面第 5 章会专门展开这类问题这里先记住一个结论——类别不平衡是这个数据集训练时的主要矛盾。3.2 掩码图像的存储格式与读取要求掩码以 png 格式存储。PNG 的优势在于无损掩码边缘不会被压缩算法破坏。但需要注意同一份 png 文件可以有不同的读取方式用 OpenCV 的cv2.imread(path, cv2.IMREAD_COLOR)读出来是三个通道取值在 0 到 255 之间用cv2.IMREAD_GRAYSCALE读出来是单通道。对于这份数据掩码的语义值是 0 和 1不是 0 和 255。如果使用三通道读取会得到一个 H×W×3 的数组每个通道相同直接用于损失计算时形状对不上。正确的方式是单通道读取并在送入损失函数前将标签转为torch.long。下面是一个标准的掩码加载片段import cv2 import numpy as np mask cv2.imread(train/masks/img-Glass002-414-19_cheek_blowing-1-epping_forest_02-024-sgseg.png, cv2.IMREAD_GRAYSCALE) print(掩码形状:, mask.shape) # (H, W)单通道 print(像素取值:, np.unique(mask)) # 应输出 [0 1] # 若某些工具误存成 0/255做一次归一到 0/1 if mask.max() 1: mask (mask 127).astype(np.uint8)逻辑说明IMREAD_GRAYSCALE强制把图像按单通道灰度读入形状变成(H, W)np.unique列出全部像素取值判断是否存在 0 和 1 之外的值。mask.max() 1这个条件用于兼容某些标注工具把标签保存成 0 和 255 的情况一旦检测到就按大于 127 的阈值重新二值化。这一步建议放在数据加载的最前面做统一处理而不是在每张图上临时判断——统一处理能避免训练过程中某个批次突然蹦出异常值导致损失变成 NaN。3.3 可视化脚本的三图对比逻辑数据集附带的可视化脚本做的是这样一件事随机抽取一张图像把原始图像、GT 掩码图、GT 蒙板叠加到原图上的效果图并列展示并保存到当前目录。这个脚本对快速体检数据质量非常有用——掩码是否正确贴合眼镜边缘、是否有漏标注或错标注一眼就能看出来。三张图的生成逻辑并不复杂核心操作是用半透明方式把掩码区域叠加到原图上。下面这个函数就是常见的实现方式import cv2 import numpy as np def overlay_mask(image_path, mask_path, alpha0.5, color(0, 255, 0)): image cv2.imread(image_path) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask (mask 0).astype(np.uint8) # 转为 0/1 overlay image.copy() # 只在掩码区域填入指定颜色 overlay[mask 1] color # 加权融合形成半透明叠加 result cv2.addWeighted(image, 1 - alpha, overlay, alpha, 0) return result这段代码先用mask 0把掩码二值化再把选中区域像素替换为绿色或任意指定颜色最后用addWeighted做加权融合。当alpha取 0.5 时叠加区域既能看清眼镜轮廓又不会完全盖住原图纹理。可视化脚本的价值在于它能让你在训练前先建立对 GT 质量的直觉——如果掩码边缘明显粗糙模型大概率会学到同样的噪声边缘如果某些图的掩码整体偏移几个像素那就要回到标注环节排查对齐问题。4. 可视化脚本复现从随机抽样到三图保存的执行流程4.1 脚本的核心抽样与保存逻辑数据包里自带的脚本解决了“随机抽查数据质量”这件事。它的流程分为四步随机抽一张图、读取原图与掩码、生成叠加图、把三张图拼接保存。为了让这个流程可以复用到自己的数据上我把实现拆成可独立运行的版本。下面是一个完整的脚本放在数据集根目录下执行即可import os import random import cv2 import numpy as np train_images train/images train_masks train/masks output_dir visual_check os.makedirs(output_dir, exist_okTrue) # 随机抽取一张图 all_files [f for f in os.listdir(train_images) if f.endswith(.png)] chosen random.choice(all_files) print(抽取样本:, chosen) img_path os.path.join(train_images, chosen) mask_path os.path.join(train_masks, chosen) # 读取原图和掩码 image cv2.imread(img_path) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask_bin (mask 0).astype(np.uint8) # 在原图上叠加掩码半透明绿色 overlay image.copy() overlay[mask_bin 1] (0, 255, 0) blended cv2.addWeighted(image, 0.5, overlay, 0.5, 0) # 横向拼接三张图原图 | GT 掩码 | 叠加图 gt_color cv2.cvtColor(mask_bin * 255, cv2.COLOR_GRAY2BGR) combined np.hstack([image, gt_color, blended]) save_path os.path.join(output_dir, fcheck_{chosen}) cv2.imwrite(save_path, combined) print(可视化结果已保存:, save_path)循环结构上脚本先列出train/images下的全部 png 文件用random.choice抽取一张随后分别读取原图和掩码mask_bin把掩码转为 0/1 二值数组overlay将眼镜区域填充为绿色再与原图按 0.5 的权重融合最后将三张图水平拼接成一张长图保存。gt_color这一步是把单通道掩码转成三通道否则np.hstack会因为通道数不一致报错。这个脚本每次执行抽取一张想连续抽多张就加一层循环或直接塞进批处理里。参数上可以调整的是alpha值0.3 更通透0.7 更强调掩码位置。4.2 脚本输出的三种信息输出文件名为check_加原图文件名里面从左到右依次是原始图像、GT 掩码图、GT 与原图的叠加图。第一张原图提供上下文第二张掩码图展示标注的精确位置第三张叠加图直接暴露标注是否贴合目标边缘。眼镜这类目标有一个特点镜片区域往往是半透明的原图里透过镜片能看到眼部纹理但掩码会把这部分完整覆盖。如果掩码边界与镜框的实际边缘有系统性偏移叠加图上一眼就能发现。这个脚本的价值不在于花哨而在于把“抽象的文件名”转化为“可视化的数据检查”建议训练前先在训练集和测试集各抽 20 张左右过目一遍。5. 用这份数据集训练分割模型的避坑排查指南5.1 掩码尺寸与原图不一致现象训练循环直接报张量形状不匹配collate阶段拼接失败。有的样本原图是 512×512掩码却是 510×510。原因训练脚本里对原图做了 resize掩码路径忘了同步处理或者原图与掩码在采集导出时本身就有几个像素的裁剪偏差。这个数据集的大多数文件是一致的但只要有极少数不一致训练就会崩。解决在 Dataset 的__getitem__里对二者强制做同样的尺寸变换用cv2.resize加固定插值方式掩码用INTER_NEAREST不用线性插值。线性插值会引入中间灰度值破坏 0/1 二值语义。5.2 标签值混用 0/1 与 0/255现象训练输出通道设为 2但内存中的标签最大值为 255损失疯涨。或者反过来掩码读出来全是 0 和 1但代码用 255 做阈值二值化得到全零张量。原因不同标注工具导出的掩码格式不同有的按类别 ID 存 0/1有的按可视化灰度存 0/255。读取时没有统一归一化。解决在数据加载入口处做一次强制规范——单通道读取检测最大值大于 1 就做(mask 127).astype(np.uint8)归一到 0/1。不要在每个 batch 里临时判断加载阶段处理一次后续逻辑全部按 0/1 语义走。5.3 数据增强把原图和掩码切错方向现象训练时 loss 能下降但验证时 IoU 极低可视化增强后的样本发现原图里眼镜在左边掩码却标在右边。原因随机水平翻转时只对原图做了变换掩码没动或二者用了不同的随机种子导致翻转不同步。解决在自定义 Dataset 里对原图和掩码使用同一个随机状态。最简单的方式是生成一个随机数原图和掩码共用这个数决定是否翻转裁剪和缩放同理ROI 坐标必须同时作用于两者。遇到同类问题先查增强代码再看数据集本身。5.4 类别不平衡导致模型全部预测背景现象训练几轮后 loss 下降但 mIoU 始终在 0 附近预测结果全黑没有任何眼镜区域。Val 上的准确率反而很高因为背景占了大头。原因经典的正负样本失衡。眼镜区域占全图比例太低普通交叉熵里背景像素贡献了绝大多数梯度模型学习不到眼镜的判别特征。解决换用 Dice Loss 或 Dice CrossEntropy 的组合损失。Dice Loss 直接优化区域重叠程度对前景占比不敏感。或者给背景类降低权重顺带提高前景类在损失中的贡献。实践里BCEWithLogitsLoss加pos_weight也是一个可用方案。5.5 按字符串排序导致训练测试数据泄漏现象按sorted(os.listdir(...))划分数据时测试集中出现与训练集同一人物、同一场景的连续帧评估指标虚高。原因文件名中Glass002和Glass010按字符串排序时Glass010会排在前面但这种排序方式把同一个采集对象的相邻帧切到了不同集合里。人脸相邻帧的相似度极高模型见过几乎一样的图验证自然好看。解决按文件名中的人物编号或Glass编号做分组划分同一人物的所有帧必须在同一集合内。解析文件名生成分组键再按组做 train/test split才能保证测试集的独立性。6. 把数据集接入 UNet 训练链路自定义 Dataset 与验证习惯6.1 Dataset 类的核心实现把这份眼镜分割数据集跑通 UNet关键在数据加载层把前面的坑都堵住。一个能直接使用的 Dataset 实现如下import os import cv2 import torch from torch.utils.data import Dataset class GlassesSegDataset(Dataset): def __init__(self, img_dir, mask_dir, size(256, 256), augmentFalse): self.img_dir img_dir self.mask_dir mask_dir self.size size self.augment augment self.files [f for f in os.listdir(img_dir) if f.endswith(.png)] def __len__(self): return len(self.files) def __getitem__(self, idx): name self.files[idx] image cv2.imread(os.path.join(self.img_dir, name)) mask cv2.imread(os.path.join(self.mask_dir, name), cv2.IMREAD_GRAYSCALE) # 统一尺寸掩码用最近邻插值 image cv2.resize(image, self.size, interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, self.size, interpolationcv2.INTER_NEAREST) # 标签值归一化到 0/1 mask (mask 127).astype(float32) # 翻转增强原图和掩码共享同一随机状态 if self.augment and torch.rand(1) 0.5: image cv2.flip(image, 1) mask cv2.flip(mask, 1) # 转为 tensor 并调整通道顺序 image torch.from_numpy(image.transpose(2, 0, 1)).float() / 255.0 mask torch.from_numpy(mask).unsqueeze(0).float() return image, mask实现逻辑分四步按文件名加载原图和掩码用固定尺寸做缩放掩码强制最近邻插值防止灰度污染标签统一归一到 0/1翻转增强共用随机数保持同步。mask.unsqueeze(0)把形状变为(1, H, W)一部分损失函数要求这种格式另一部分要求去掉通道维具体看使用习惯。6.2 训练配置与验证习惯用这份数据训 UNet 时我通常会这样配置输入尺寸选 256×256模型输出通道 2配合 Dice Loss优化器用 Adam 配 1e-4 学习率batch size 16。验证指标以 mIoU 为准不看准确率。还有一个习惯值得分享——训练过程中每隔几个 epoch 就把验证集里的预测结果与 GT 拼图保存下来肉眼确认掩码是否真的贴合眼镜边缘。指标再好不如亲眼看两张预测图可信。从那以后我每次拿到新的分割数据集都会强制走一遍同样的流程先跑文件完整性检查再做数据可视化抽样最后才进训练循环。这三个步骤加起来不超过半小时但能避免后面几天的无效训练。这份面部眼镜图像分割数据集结构清晰、标注完整按照上面的流程走一遍UNet 的分割训练链路很快就能跑通。希望帮到你。本文还有配套的精品资源点击获取
返回列表