
简介面部眼镜图像分割数据集包含约16000张配对图像与标签按2类背景与眼镜进行像素级标注适合用于训练和评估深度学习分割模型特别适合人脸相关视觉项目与入门语义分割实践。资源内含清晰的训练集与测试集划分其中训练集约11200张图片及对应mask测试集约4800张并附带一个Python可视化脚本可随机抽取样本展示原始图、GT图及蒙版叠加效果方便快速检查数据质量。压缩包共2000个文件以PNG格式的图片和mask为主另有类别说明txt和可视化脚本py整体大小约849MB。该数据集已按常见分割任务格式整理可直接用于模型训练与评估目前已有91人学习下载适合需要现成数据集或想熟悉分割任务流程的开发者使用。1. 图像分割数据集这批面部眼镜数据真能直接开训图像分割数据集很多但真正能直接用的不多。这批面部眼镜图像分割数据集约16000张图片和对应mask训练集11200张、测试集4800张类别只有背景和眼镜两类。它解决的实际问题很明确任何人脸眼镜区域抠图任务比如眼镜佩戴检测、美颜特效、人脸属性编辑都能拿它做初版训练或验证分割流程。适合正在找一份不用清洗、分好训练和测试集、还自带可视化脚本的干净二分类分割数据的人。拿到手先别急着写模型把目录结构和mask格式摸清后面训练会省很多麻烦。2. 数据集目录与文件命名从一张文件名看穿全套数据这个数据集的组织方式对新手很友好train/test、images/masks分得清清楚楚但如果你只看一眼就开训容易忽略几个关键细节。资源自带的classes文件和可视化脚本是我觉得比一堆散图更有价值的部分。下面我从目录、文件名、mask三个角度把它拆开。2.1 目录结构train/test 分得清清楚楚解压后你会看到类似下面的结构. ├── train │ ├── images │ │ ├── img-Glass002-414-19_cheek_blowing-1-epping_forest_02-024-sgseg.png │ │ └── ... │ └── masks │ ├── img-Glass002-414-19_cheek_blowing-1-epping_forest_02-024-sgseg.png │ └── ... ├── test │ ├── images │ │ └── ... │ └── masks │ └── ... ├── classes.txt └── visualize.py # 脚本名可能不同以解压后为准train 和 test 下分别有 images 和 masks图片名和 mask 名一一对应。classes.txt里写的就是两类背景、眼镜用文本编辑器打开就能看到。我一般会先跑一条命令确认图片和 mask 数量是否一致find train/images -type f | wc -l find train/masks -type f | wc -l如果两个数字对不上说明有文件缺失直接去补或过滤。正常情况train下面应该都是11200左右test下面4800左右。我还会额外检查文件名是否完全匹配防止少了一个扩展名或多了空格ls train/images | sed s/.png$// /tmp/img_ids.txt ls train/masks | sed s/.png$// /tmp/mask_ids.txt diff /tmp/img_ids.txt /tmp/mask_ids.txt | headdiff 无输出说明一一对应。这里有一个容易忽略的点有些压缩包解压后会产生._开头的隐藏文件macOS 上尤为常见。如果你的find数量比预期多多半是混进了._img-...png这种垃圾文件加载时一定要过滤掉。目录结构本身没太多玄机但建议你保留原始目录名不要重命名成data或labels之类的。因为可视化脚本里通常写死了相对路径你改目录名之后脚本可能报错而且报错信息还不直观。我实际操作时就因为把train/images改成了images导致脚本找不到文件后来用grep翻了脚本源码才定位到路径。所以拿到手先按原结构存放跑通再想优化。2.2 文件名编码规则眼镜款式、表情、场景全写在名字里文件名的信息量非常大比如img-Glass002-414-19_cheek_blowing-1-epping_forest_02-024-sgseg.png。拆开来大概能读出这些含义片段含义猜测Glass002眼镜款式编号414人物或姿态编号19可能是人脸关键点编号cheek_blowing / dimpler / grin面部动作或表情标签1 / 0 / -3 这些表情严重程度或标签位epping_forest_02 / autumn_ground背景场景名024 / 170序列号sgseg分割数据集标识这些附加标签很有意思但注意它们和分割任务没有直接关系。分割任务只需要 RGB 图和对应的 mask文件名里的表情、场景不能当监督信号。我见过有人想把cheek_blowing提取出来当多任务学习结果发现同一个表情下的眼镜形状千差万别反而干扰主干特征提取。所以常规做法是忽略文件名只取图片和 mask。不过文件名也不是完全没用。至少两个场景可以用到一是数据去重如果你怀疑某些图片来自同一段视频的连续帧可以通过Glass002-414-19_cheek_blowing-1-epping_forest_02-024这种连续编号快速定位相邻序列二是检查分布均衡性统计每个Glass编号下的图片数量看看是不是某一款眼镜占了半壁江山。统计命令ls train/images | awk -F- {print $2} | sort | uniq -c如果发现某个款式数量特别少训练时要在损失权重里稍微给这些稀有样本加权重否则模型学到的眼镜形状会偏向主流款式。还有一点cheek_blowing这类表情标签可能来自人脸属性标注表情不同会影响脸部肌肉拉伸导致眼镜和脸部的相对位置略有变化但这不是分割要解决的本质问题。把注意力放在眼镜轮廓和镜片透明度上而不是这些辅助标签。2.3 mask 格式与可视化脚本的作用mask 是 PNG 格式尺寸和原图一样。但第一坑就是 mask 的像素值不一定是干净的 0 和 255。有的 mask 边缘带了抗锯齿灰度有的甚至把镜片反光标成了半透明。所以加载时最好统一做二值化不要直接拿 0/255 当 target。用一条 Python 命令就能检查from PIL import Image import numpy as np mask np.array(Image.open(train/masks/img-Glass002-414-19_cheek_blowing-1-epping_forest_02-024-sgseg.png).convert(L)) print(np.unique(mask))如果输出[0 255]说明是纯二值图如果出现中间值就得在 Dataset 里加阈值。我见过这套数据里同时存在两种编码风格所以统一阈值处理是最稳的。数据集附带的可视化脚本算是雪中送炭。它随机抽一张图把原始图像、GTGround Truth图像、GT 在原图上叠加的效果三张图拼在一起保存到当前目录。我在标注质量存疑时第一件事不是写验证代码而是跑这个脚本多看几十张确认眼镜轮廓标得是否贴合镜框。如果你的场景是户外、阴影、侧脸也要重点看这些图因为分割模型最容易被边缘锯齿和遮挡带偏。另外这个可视化脚本的产物通常是一张横向拼接的大图用普通图片查看器就能打开。我建议抽检时不要只看一张而是把脚本改造成循环跑多张或者干脆批量抽取 20 张拼成网格。下面是一个快速抽检脚本的雏形import matplotlib.pyplot as plt from PIL import Image import numpy as np names [img-Glass002-414-19_cheek_blowing-1-epping_forest_02-024-sgseg.png, img-Glass003-431-20_eye_closed-0-ostrich_road-170-sgseg.png] fig, axes plt.subplots(len(names), 3, figsize(12, 4*len(names))) for i, name in enumerate(names): img Image.open(ftrain/images/{name}) mask Image.open(ftrain/masks/{name}).convert(L) axes[i,0].imshow(img); axes[i,0].set_title(original) axes[i,1].imshow(mask, cmapgray); axes[i,1].set_title(mask) axes[i,2].imshow(img); axes[i,2].imshow(mask, alpha0.5, cmapjet); axes[i,2].set_title(overlay) plt.tight_layout() plt.savefig(check_overlay.png, dpi150)注意这里mask直接以灰度图叠加如果 mask 有灰度值叠加后边缘会很糊。你可以在加载时先做(mask 0)再转 uint8。这个脚本能帮你肉眼确认图片和 mask 是否对应、mask 标注是否完整。2.4 数据分布与划分为什么我说别把测试集当验证集训练集 11200 张、测试集 4800 张这个比例大约 7:3看起来没问题。但这里有个常见误区测试集是用来最终评估的不是用来调参的。如果你在训练过程中频繁拿测试集看结果你会不自觉地根据测试集表现改超参数最后报出来的 mIoU 会虚高。正确做法是从训练集里再切一个小验证集比如按 9:1 切 1120 张出来专门用来做早停和调参。import random from pathlib import Path train_img_dir Path(train/images) all_ids [p.stem for p in train_img_dir.glob(*.png)] random.seed(42) random.shuffle(all_ids) val_ids set(all_ids[:1120]) # 取约10%然后 Dataset 初始化时传入一个ids参数只加载指定划分。这样训练集、验证集、测试集三者完全隔离测试集只在最终评估时碰一次。另外数据集的场景分布值得关注。从文件名可以看出场景名很多样比如epping_forest_02、railway_bridges、beach_parking说明数据采集覆盖了室外、室内、自然光、人造光等。但还是建议统计一下场景名数量如果某些场景数量特别多模型可能会把场景特征学进去导致在没见过的场景上分割效果下降。用awk -F- {print $(NF-1)}这种命令提取场景段统计即可。我在实际使用中遇到过头灯、眩光等极端光照这批数据里不一定覆盖全所以最终上线前还是要补拍一些真实场景图。3. 用 PyTorch 把数据集喂进分割模型加载器、Dice 损失与训练参数写分割训练代码最怕的就是自定义 Dataset 写不对训练流程跑起来之后才发现数据读取有问题。我习惯先把数据加载器写稳再搭模型和损失函数。这一章给你一份能直接用的 PyTorch 加载、增强、训练和评估的标准做法。3.1 自定义 Dataset二值化 mask 是关键我习惯用 PyTorch 写分割训练。第一步是写一个 Dataset 类把图片读进来、mask 二值化然后做统一的尺寸变换。下面这个代码可以直接抄import os import numpy as np from PIL import Image from torch.utils.data import Dataset class GlassSegDataset(Dataset): def __init__(self, images_dir, masks_dir, idsNone, transformNone): self.images_dir images_dir self.masks_dir masks_dir # 过滤隐藏文件和非png文件 all_ids [ f[:-4] for f in os.listdir(images_dir) if f.endswith(.png) and not f.startswith(.) ] self.ids ids if ids is not None else all_ids self.transform transform def __len__(self): return len(self.ids) def __getitem__(self, idx): name self.ids[idx] img_path os.path.join(self.images_dir, name .png) mask_path os.path.join(self.masks_dir, name .png) image Image.open(img_path).convert(RGB) mask Image.open(mask_path).convert(L) # 转 numpy 做二值化 mask np.array(mask) # 只要像素大于0就算眼镜区域 mask (mask 0).astype(np.float32) return image, mask这里最核心的是(mask 0).astype(np.float32)。如果 mask 只有 0 和 255这一步会把 255 变成 1如果 mask 有灰度渐变也能通过阈值统一。阈值设多少我一般用 0 而不是 127因为标注边缘即使有半透明只要不是纯背景就应当算作前景。某些数据集里 mask 的纯黑部分像素值也可能不是 0 而是 0 附近的小值这种情况下用mask 10更稳妥但先看数据再调。另外这个 Dataset 返回的是 PIL 图像和 numpy 数组后面需要用 transform 把它们转成 tensor。如果你不想依赖外部库可以用torchvision.transforms但几何增强要自己写或者用RandomCrop保证图片和 mask 同步裁剪稍麻烦。所以我建议直接用 albumentations。3.2 数据增强几何变换和颜色变换要分开对待分割任务里mask 必须跟着图片做同样的几何变换但颜色增强不能作用到 mask 上。我一般用 albumentations 库它天然对 image 和 mask 同步处理。下面是一组适合这份数据集的增强参数import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.Resize(256, 256), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.3), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit10, p0.5), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2() ])注意几点Resize 用了 256如果你的显卡显存够大可以上 512对眼镜这种细小目标更友好ShfitScaleRotate 的 rotate_limit 不要超过 15 度因为戴眼镜的人脸如果大角度旋转眼镜可能部分超出人脸区域导致 mask 边界被裁剪产生假标注。训练集比较充足所以 HorizontalFlip 设为 0.5 是合理的。验证集不要加任何几何增强只做 Resize 和 Normalize。val_transform A.Compose([ A.Resize(256, 256), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2() ])RandomBrightnessContrast只影响图片不影响 maskalbumentations 会保证 mask 原样通过。我通常会额外加一个GaussNoise模拟弱光环境但对这个数据集不是必须因为样本本身已经覆盖了多种室内外场景。如果要用把p设到 0.2 即可太大容易把眼镜腿的细小结构破坏掉。使用 albumentations 时调用方式要和普通 transform 区分augmented train_transform(imagenp.array(image), maskmask) image_tensor augmented[image] mask_tensor augmented[mask]这里mask必须是 numpy 数组且形状为(H, W)不能是(H, W, 1)的二维。ToTensorV2 会自动把 mask 变成(1, H, W)的 float tensor。如果你把 mask 转成了(H, W, 1)通道维度会影响后续损失计算所以保持单通道最省事。3.3 损失函数BCE 加 Dice比单独用 BCE 稳太多眼镜分割是典型的类别不平衡问题背景像素占比可能超过 90%前景眼镜只有一小块。单独用 BCEWithLogitsLoss 会让模型倾向全部预测背景loss 下降很快但 mIoU 很低。常见的做法是 BCE 加 Dice 损失。我一般这样实现import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): # pred 是未经过 sigmoid 的 logits pred torch.sigmoid(pred) intersection (pred * target).sum() return 1 - (2 * intersection smooth) / (pred.sum() target.sum() smooth) def combined_loss(pred, target): bce F.binary_cross_entropy_with_logits(pred, target) dice dice_loss(pred, target) return bce dice参数解读smooth 是平滑项防止分母为 0一般取 1.0 即可pred形状是[B,1,H,W]target形状一样且已经二值化成 float32。如果你的网络输出是[B,2,H,W]两类那就只用第一类或第二类的通道或者改成多分类 Dice。我这里把两类问题当作单通道二分类处理对应网络最后一层卷积输出 1 个 channel。训练时我习惯用 Adam初始学习率 1e-4batch size 16256 分辨率下 8G 显存够用。如果 loss 长时间不降先把学习率降到 1e-5同时检查数据加载是不是没有 shuffle。还有一个容易忽略的点ToTensorV2会把 float32 图像转成 0~1 范围和Normalize的顺序不能反否则 Normalize 的 mean/std 会对 0~255 的像素做操作效果完全不同。我在第一次用 albumentations 时就把Normalize放在了ToTensorV2后面结果模型训练了 10 个 epoch 都没有收敛。3.4 评估指标别只看 accuracy要看 IoU二分类分割里 accuracy 几乎没有参考价值。假设背景占 95%模型全预测背景也能有 95% accuracy但眼镜区域一塌糊涂。我通常在验证集上计算 mIoU代码片段import numpy as np def compute_iou(pred_mask, true_mask): # pred_mask 和 true_mask 都是二值 numpy 数组 intersection np.logical_and(pred_mask, true_mask).sum() union np.logical_or(pred_mask, true_mask).sum() if union 0: return 1.0 return intersection / union对每个 batch 把 pred 用0.5阈值转成二值再和 true_mask 算 IoU最后对所有样本平均。这份数据集眼镜区域相对小如果 mIoU 能到 0.85 以上模型基本可用。除了 mIoU我还会看眼镜类别的 IoU因为背景 IoU 总是很高会被平均掩盖。用 per-class IoU 才能反映真实性能。计算时只需要单独把前景类的 intersection 和 union 拿出来代入公式其他与 mIoU 一致。我在项目里用一个循环统计所有类别的 IoU最后打印一个表格这样能直观看到哪个类别拖后腿。4. 避坑手册这份数据集最常见的 5 个坑这份数据集的坑不在目录结构上而在标签编码、文件命名、跨平台兼容性这些细节里。下面是我跑完整个流程后踩过的 5 个坑按影响程度排序。前三个坑会直接让你训练失败或指标失真后两个坑影响你的开发效率和真实场景表现。4.1 现象mask 加载后像素值不是 0/255训练 loss 震荡第一次跑训练时我直接用 PIL 读取 mask 并转成L模式拿原始 0~255 的值当回归目标结果 loss 一直不收敛预测输出全是灰色。打印np.unique(mask)发现值有[0, 128, 255]甚至还有1这种接近 0 的像素。原因mask 是 PNG 格式部分样本的像素值有抗锯齿灰度尤其眼镜腿边缘值可能是 100、180 这类中间值另外还有少数 mask 是 0/1 的 PNG 索引图读进来只有 0 和 1。不同样本的标签表示不统一模型就懵了。解决加载时统一做mask 0二值化把非零值全部变成 1。最好是写进 Dataset不要让训练循环里每步再处理。具体修改mask np.array(mask) mask (mask 0).astype(np.uint8)astype(np.uint8)是因为后续计算损失时PyTorch 的binary_cross_entropy_with_logits需要 float但这步先存成 uint8 更省内存使用时再转 float。如果你用float32也可以但要注意mask 0返回 bool直接astype(np.float32)也能行。关键在于不要保留原灰度值。这个坑属于数据质量层面的第一道防线不解决后面所有指标都不可信。还有一点测试集和训练集的mask格式可能也不完全一致所以验证阶段也要做同样的二值化。4.2 现象文件名里的表情标签被误当成类别我用cheek_blowing、grin这些标签想做一个多任务输出在分割网络后面接了一个分类头结果验证集 mIoU 暴跌到 0.5 左右单独看分类准确率却很高。原因这些表情标签和眼镜分割的依赖关系很弱大笑和鼓腮时眼镜形状没有本质变化而且数据集里表情分布不均衡有些表情只有几百张强制学习会引入噪声。解决如果你真想用这些标签最多做弱监督的辅助分类且需要单独统计分布不要把它和主分割分支混合在一起。我后来还是选择完全忽略文件名。辅助分支不仅在结构上增加复杂度还让损失函数里多了一个超参数需要调得不偿失。如果实在要统计表情分布用一条命令ls train/images | awk -F- {print $4} | sort | uniq -c | sort -nr这样能看到cheek_blowing有多少张、eye_closed有多少张。我统计完发现cheek_blowing数量只有grin的三分之一这种不均衡会导致分类头学偏进而干扰共享编码器。所以最稳妥的做法还是只做单任务分割。4.3 现象训练时 loss 不降原来是数据顺序没 shuffle我在自定义 Dataset 里没有设置 shuffle直接把所有图片按字典序排序后的顺序传入 DataLoader导致一个 batch 里全是同一个眼镜款式、同一个场景的图片模型反复学同一类样本验证 loss 几乎不下降。解决DataLoader 里shuffleTrue或者在我自己的训练循环里手动打乱 idx。这是一个很基础但经常翻车的点尤其在文件命名有强规律如前缀都是 img-Glass002时shuffle 必须放在随机增强之前。另外即使设置了 shuffle如果 Dataset 的__getitem__里返回的图片路径始终不变shuffle 仍然有效因为它是对索引打乱。我还会在第一个 epoch 打印每个 batch 的图片名片段确认一个 batch 内确实来自不同款式。打印代码for i in range(0, len(dataset), batch_size): batch_ids dataset.ids[i:ibatch_size] print([x.split(-)[1] for x in batch_ids])如果输出全是Glass002说明顺序没打乱。正常情况一个 batch 里应该均衡出现几种款式。这个检查只花几秒钟但能避免浪费一晚上的训练时间。这个坑容易在换数据集时复发每次换数据都要重新检查。4.4 现象可视化脚本在 Windows 上报 FileNotFoundError数据集附带的可视化脚本在 Linux 下没问题但在 Windows 上跑会报错Traceback 末尾通常是FileNotFoundError: [WinError 3] 系统找不到指定的路径。原因脚本里可能用了/拼接路径而 Windows 需要\或者脚本一启动就去找某个不存在的目录。解决用pathlib.Path重写路径拼接from pathlib import Path base Path(r你的数据集根目录) img_path base / train / images / filename mask_path base / train / masks / filename注意Path的/运算符在不同操作系统上都会生成正确的分隔符。如果不想改脚本可以手动把脚本里的所有分隔符替换成os.path.join。另外注意classes.txt路径不同操作系统的换行符也可能导致读取到空行最好用strip()处理。这个坑其实不是数据集的问题是跨平台脚本的通病但如果你在 Windows 下复现十有八九会遇到。我自己的习惯是拿到任何项目先把os.path.join统一成pathlib.Path一劳永逸。4.5 现象测试集 mIoU 很高但是自定义图像上预测效果很差我把训练好的模型在测试集上跑出 0.9 的 mIoU心想稳了结果拿手机拍了几张戴眼镜照片一测眼镜边缘全是毛刺。原因这个数据集的测试集和训练集来自相似的数据生成管线场景分布接近但真实世界有镜片反光、粗黑框、半框、金丝镜腿等多种样式模型没见过。解决把测试集里随机抽 100 张做人工检查发现测试集里也大多是高清证件照风格和手机自拍差异大。我的做法是额外收集 20~50 张不同光源下、不同拍摄角度的眼镜照片手动标注后当验证集用这个验证集来调阈值和做早停。同时预测时把输出概率的阈值从 0.5 调到 0.4 或 0.3可能让边缘更完整但也会引入一些背景噪声需要权衡。标注 50 张图大概两小时但带来的可靠性提升远超训练一个晚上的成本。总的来说自己补几张真实图比迷信测试集指标强得多。5. 进阶用法用自带脚本抽检再导成 COCO 格式5.1 跑通可视化脚本快速发现标注脏数据我拿到数据第一晚就是跑这个脚本抽了 50 张图发现有几张眼镜边缘明显没贴住镜框还有两张把镜片反光当成了眼镜区域。幸好有脚本一目了然否则这种脏数据混进去模型会被带偏。脚本运行方式一般是在命令行直接执行比如python visualize.py它会在当前目录生成三张一组的对比图。我建议你改一下脚本让它可以指定图片编号不要每次都随机这样复查特定样本更快。改法很简单把随机取样改成传参即可具体实现不需要依赖额外库用sys.argv接收一个文件名参数就行。5.2 把二值 mask 导出成 COCO JSON接上 mmdetection如果你想用现成目标检测/分割框架比如 mmdetection 或 Detectron2需要把 PNG mask 转成 COCO 格式的 RLE 编码。转换代码很简单import numpy as np from pycocotools import mask as cocomask import json def convert_mask_to_rle(binary_mask): # 转换为 Fortran 连续数组 fortran_mask np.asfortranarray(binary_mask.astype(np.uint8)) rle cocomask.encode(fortran_mask) rle[counts] rle[counts].decode(ascii) return rle注意binary_mask必须是 0/1 的 uint8 数组并且np.asfortranarray不能少否则 RLE 编码结果会错。然后遍历所有训练集 mask为每张图生成一个 annotationcategory_id 设为 1对应眼镜bbox和area可以由 RLE 生成。这样导出的 JSON 可以直接被 mmdetection 的 CocoDataset 读取省去自己写 Dataset 的功夫。如果你只需要训练分割也可以直接用我的 Dataset不必导 COCO。但如果你后续要接检测、实例分割、关键点等任务这个转换就很有价值。从那以后我每次训练前都会强制走一遍「随机抽 9 张图看叠加效果 → 打开一张 mask 看像素值是否二值化 → 再进训练」这三个动作加起来不到五分钟但能救下好几个小时的无效训练。希望帮到你。本文还有配套的精品资源点击获取