
简介面向计算机视觉与图像分割任务研究者这份资源提供面部眼镜图像分割专用数据集。整体包含约16000张带像素级标注的图片按训练集与测试集划分训练集约11200张图片及对应masks测试集约4800张images与masks一一对应目录结构清晰方便直接接入主流分割框架。标注类别分为背景与眼镜两类既可用于训练语义分割模型也可作为验证分割算法或扩充数据集的素材。资源共2000个文件主体为PNG格式原始图与掩码另附类别说明txt和Python可视化脚本压缩包整体约849MB。脚本会随机挑选一张样本把原始图、GT图以及GT在原图上的蒙版效果同屏展示并保存便于快速核对标注正确性、直观理解数据格式。已有91人浏览学习适合需要眼镜区域精细分割标注的入门及进阶开发者以及人脸解析、可穿戴设备等视觉项目。1. 面部眼镜图像分割数据集先别急着套模型做 AR 虚拟试戴、疲劳驾驶预警、眼镜防雾算法或者医学图像分割里的眼周分析第一步往往不是写网络而是拿到一份能用的图像分割数据集。面前这份「面部眼镜图像分割数据集」规模在 16000 张左右每张都带配套的标签已经属于「够训练一个像样分割模型」的量级。很多从业者拿到数据集后的第一反应是直接丢进 Unet 或者 MMSegmentation 里跑结果往往是被小目标、镜片反光和正负样本失衡三件事反复折磨。这篇笔记会从数据拆解、标签校验、训练管线、超参设置到排错把这条落地路径完整走一遍。适合手里有类似数据、正准备训分割模型的算法工程师和学生看完能直接照做。2. 数据拆解眼镜分割到底在分割什么以及 16k 样本的构成2.1 眼镜目标与通用语义分割的三点差别拿 Cityscapes 或者 ADE20K 的思路来套眼镜分割是第一个翻车点。眼镜目标在整张人脸图上通常占不到 10% 的像素大多数时候只有 2% 到 5%。通用语义分割面对的类别分布相对均衡很多而眼镜分割是典型的极端正负样本失衡任务背景像素轻松超过 95%。如果不做类别加权或者换损失函数模型很快就会学成「把所有像素都预测成背景」训练 loss 看着在降实际输出全黑。第二个差别是边界属性。镜框有非常锐利的强边缘但镜片区域是半透明的反光严重时镜片内部的纹理和肤色混在一起标签本身就会存在歧义。通用分割里那种「物体内部相对致密」的假设在眼镜上不成立镜片中间经常被标注成背景或者反过来被整块标注成前景这取决于标注规范。第三个差别是和身份强相关。同一个人戴同一副眼镜正脸、侧脸、仰头、低头镜片反光区域完全不一样。数据集的 16000 张图看起来数量不小但实际多样性要看人物数量、姿态覆盖和光照条件。我拿到类似数据集的第一件事不是统计文件个数而是写个采样脚本均匀抽出 50 张图连标签一起人工过一遍半小时就能把数据质量的底摸清楚。2.2 标签形态二值 Mask、VOC 与软标签绝大多数眼镜分割数据集采用最简单的标签形式一张图配一个同名 PNG前景像素为 255背景为 0。这种二值 Mask 可读性好标注工具导出方便但要注意文件是单通道还是三通道。有些标注工具导出的是三通道 PNG三个通道值完全一样直接用cv2.imread默认参数读进来是H×W×3后续和单通道标签一拼接就容易出维度错误。也有数据集采用 VOC 格式的调色板 PNG或者 COCO JSON 多边形标注。如果拿到的是 COCO JSON转 Mask 时先cv2.fillPoly再检查孔洞眼镜框中间是空的标注软件里的多边形可能只框外轮廓也可能框完外轮廓再挖掉内圈转换后必须可视化核对。还有一种情况是标签本身就分「镜框」和「镜片」两类像素值为 1 和 2。需要先确认任务是二分类还是多分类不能默认 255 就是唯一前景。这里特别提一下软标签。镜片边缘的像素本来就介于「眼镜」和「非眼镜」之间硬标注 0 或 1 会让模型在边界上反复横跳。常见做法是用 OpenCV 距离变换把边界附近的标签软化import cv2 import numpy as np mask cv2.imread(masks/0001.png, cv2.IMREAD_GRAYSCALE) mask_bin (mask 127).astype(np.uint8) # 计算前景到背景的距离归一化成软标签 dist cv2.distanceTransform(mask_bin, cv2.DIST_L2, 5) # 距离变换的结果是前景内部越深越大边界处接近0 soft_gt np.clip(dist / 10.0, 0, 1).astype(np.float32)软标签可以缓解边界歧义但也有代价模型输出的是连续值验证时要用阈值 0.5 转回二值 MaskIoU 计算也有额外一步。如果数据集本身标注质量不错我一般只在镜片反光严重的子集上用软标签其他部分维持硬标签。2.3 数据集划分按人头划分而不是按图像划分这是新手最容易忽略的点。16000 张图如果直接random_split同一个人可能同时出现在训练集和验证集里。人脸特征高度相似模型很容易记住这个人而不是学会分割眼镜验证集 mIoU 虚高一到真实场景就掉点。正确做法是先按人物 ID 分组再以组为单位切分。文件名一般带有身份编码比如031_0023.jpg这种前缀为人物编号的命名。按组划分的脚本如下import os import random from collections import defaultdict random.seed(42) img_dir images mask_dir masks out_dir splits os.makedirs(out_dir, exist_okTrue) files [f for f in os.listdir(img_dir) if f.endswith(.jpg)] group defaultdict(list) for f in files: person_id f.split(_)[0] group[person_id].append(f) train, val, test [], [], [] for pid, flist in group.items(): random.shuffle(flist) n len(flist) train flist[: int(n * 0.8)] val flist[int(n * 0.8) : int(n * 0.9)] test flist[int(n * 0.9) :] for split, lst in [(train, train), (val, val), (test, test)]: with open(f{out_dir}/{split}.txt, w) as fp: for name in sorted(lst): fp.write(name.replace(.jpg, ) \n)这段脚本的关键在分组逻辑person_id f.split(_)[0]假设前缀是人员编号如果文件名没有这个规律需要先维护一份人员映射表再执行。8:1:1 的划分对 16000 张图是合理起点训练集约 12800 张验证和测试各约 1600 张。注意val flist[int(n*0.8):int(n*0.9)]这一行为当前人物分配了 10% 的样本到验证集不会出现代码截断 0.9 到 0.9 的空区间但最终测试集的量取决于每个人物样本数如果某个人只拍了 3 张可能分不均匀属于正常现象。3. 把数据集跑进训练管线目录约定、Dataset 类与框架对接3.1 先定目录结构和配对校验模型训练前先把这个目录结构定下来后面所有脚本都围绕它展开data/glasses_seg/ ├── images/ # 原图*.jpg ├── masks/ # 标签*.png与 images 同名不同后缀 └── splits/ ├── train.txt ├── val.txt └── test.txtsplits里的每一行是图像 id不含扩展名。加载时统一用f{sid}.jpg和f{sid}.png拼接路径避免在不同脚本里各写一套路径逻辑。拿到数据先跑一遍配对校验检查每张图是否都有对应标签尺寸是否一致for f in images/*.jpg; do b$(basename $f .jpg) if [ ! -f masks/$b.png ]; then echo missing mask: $b fi done很多数据集在打包传输时丢过文件这一条命令能查出所有缺标签的样本。如果检查出来缺了几十张直接过滤掉或者在训练脚本里跳过不要留着让 DataLoader 在 epoch 中途报错。3.2 写一个能直接跑的 PyTorch Dataset 加载器下面这个 Dataset 类是我常用模板兼容二值 PNG 标签同时把数据增强写进同一套管线保证图和 Mask 做完全一致的变换import cv2 import torch import numpy as np from torch.utils.data import Dataset import albumentations as A class GlassesSegDataset(Dataset): def __init__(self, split_file, img_dir, mask_dir, size(512, 512), trainingFalse): with open(split_file) as fp: self.ids [line.strip() for line in fp if line.strip()] self.img_dir img_dir self.mask_dir mask_dir self.size size if training: self.aug A.Compose([ A.RandomResizedCrop(size[0], size[1], scale(0.5, 1.0), p0.8), A.HorizontalFlip(p0.5), A.ColorJitter(brightness0.2, contrast0.2, p0.5), A.CoarseDropout(max_holes2, max_height64, max_width64, p0.3), ]) else: self.aug A.Compose([ A.Resize(size[0], size[1]) ]) def __len__(self): return len(self.ids) def __getitem__(self, idx): sid self.ids[idx] img cv2.imread(f{self.img_dir}/{sid}.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(f{self.mask_dir}/{sid}.png, cv2.IMREAD_GRAYSCALE) mask (mask 127).astype(np.uint8) transformed self.aug(imageimg, maskmask) img transformed[image].astype(np.float32) / 255.0 mask transformed[mask] img torch.from_numpy(img).permute(2, 0, 1) # 语义分割标签用 long mask torch.from_numpy(mask).long() return {image: img, mask: mask, id: sid}这段代码有三个细节要注意。第一mask 127把标签统一归到 0/1忽略原标签可能存在的中间灰度值如果数据集本身含镜框和镜片两类就不能这么做需改成mask.astype(np.int64)保留类别编号。第二permute(2, 0, 1)将 HWC 转成 CHW和 PyTorch 的卷积输入对齐。第三验证集的Resize对 mask 默认用最近邻插值不会因为缩放产生新的中间灰度如果自己用cv2.resize单独处理标签记得显式指定interpolationcv2.INTER_NEAREST。3.3 对接 MMSegmentation 与 YOLOv8-seg不想维护自己的训练循环可以对接现成分割框架。MMSegmentation 是最常用的一个支持 Mask 和 VOC 两种标签格式。最快路径是把数据整理成 VOC 格式JPEGImages放原图SegmentationClass放调色板 PNG 标签然后数据配置写成# configs/_base_/datasets/glasses_voc.py 关键片段 dataset_type VOCDataset data_root data/glasses_seg/voc_format/ img_norm_cfg dict( mean[123.675, 116.28, 103.53], std[58.395, 57.12, 57.375], to_rgbTrue) train_pipeline [ dict(typeLoadImageFromFile), dict(typeLoadAnnotations), dict(typeResize, img_scale(512, 512), ratio_range(0.5, 2.0)), dict(typeRandomFlip, prob0.5), dict(typeNormalize, **img_norm_cfg), dict(typeCollect, keys[img, gt_semantic_seg]), ]LoadAnnotations会自己读调色板索引因此标签 PNG 必须是无压缩调色板格式用cv2.imread读出来是索引值而不是灰度值。如果手里的二值 PNG 是普通灰度图需要先用 PIL 转一次调色板模式再存否则 MMSegmentation 读到的类别索引是错的。如果只做检测式的实例分割YOLOv8-seg 也是一个选项。它要的标签是每个目标的归一化多边形坐标不是 dense mask。把二值 Mask 转 YOLO 多边形标签的常见做法是cv2.findContours提取轮廓再归一化一个 512 分辨率下的镜框轮廓一般要保留 20 到 40 个点少了边界锯齿明显多了训练变慢。YOLOv8 的数据 YAML 配置如下# glasses_seg.yaml path: data/glasses_seg train: images/train val: images/val names: 0: glassesYOLO 格式转换脚本不复杂但要注意轮廓开口问题眼镜框是环状结构findContours默认只取外轮廓镜框内圈容易被忽略导致 mask 覆盖不完整。遇到这种情况我会把二值 Mask 做一次填充孔洞预处理再提取轮廓保证内外圈都能进入标注。4. 让眼镜分割收敛稳损失函数、数据增强与四个超参4.1 损失函数CrossEntropy 之外必须加一个 Dice 项眼镜分割的类别失衡有多严重直接跑一次就能体会。背景像素动辄 95% 以上纯 CrossEntropy 会让模型倾向把所有像素预测成背景mIoU 看起来还行输出 Mask 里眼镜区域全是残缺的。常用做法是 CE 加 Dice Loss把 Dice 作为正则项拉高前景的召回import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): # pred 是未经过 softmax 的 logits形状 [B, C, H, W] prob F.softmax(pred, dim1)[:, 1] # 取眼镜类 prob prob.contiguous().view(-1) target target.contiguous().view(-1).float() intersection (prob * target).sum() return 1 - (2.0 * intersection smooth) / ( prob.sum() target.sum() smooth) ce F.cross_entropy(pred, mask) dice dice_loss(pred, mask) loss ce 0.5 * dice权重 0.5 是个中庸值。Dice 权重太高会让模型在边界附近产生过度激进的预测太低又压不住背景倾向。如果用的网络是 DeepLabV3 这类带 ASPP 的模型我可以接受 0.3 到 0.7 的范围从 0.5 起步看到验证集 mIoU 停滞再微调。另一个思路是在 CE 里加权torch.nn.CrossEntropyLoss(weighttorch.tensor([0.2, 1.0]))效果接近但不如 Dice 平滑。4.2 数据增强针对眼镜的四个具体操作通用的随机翻转和裁剪对眼镜任务不够。眼镜目标集中在人脸中上部裁剪范围太大会把眼镜裁出画面太小又学不到上下文。我常用这一套增强组合import albumentations as A aug A.Compose([ A.OneOf([ A.RandomCrop(480, 480, p0.7), A.RandomResizedCrop(512, 512, scale(0.6, 1.2), p0.3), ]), A.Affine(rotate(-15, 15), scale(0.9, 1.1), p0.5), A.OneOf([ A.MotionBlur(blur_limit5, p0.5), A.GaussianBlur(blur_limit3, p0.5), ]), A.CoarseDropout(max_holes2, max_height64, max_width64, p0.3), ])四项各自有针对性。随机裁剪固定 480 尺寸和输入 512 之间产生尺度扰动让模型对眼镜大小不敏感。仿射变换只旋转 ±15 度因为人脸姿态超过这个范围后眼镜形变严重训练分布过度扩展反而干扰。运动模糊和中心区域遮挡模拟摄像头运动模糊和刘海、手部遮挡。CoarseDropout 的 64 像素空洞尺寸正好覆盖镜片面积强迫模型从镜框上下文推断镜片位置对反光镜片很有效。验证阶段不要用这套组合只用Resize否则验证集结果不具备可比性。4.3 四个超参的起步推荐值参数推荐起步值说明输入分辨率512×512低于 384 会漏掉镜框细边高于 640 训练时间成倍增加Batch size8单卡 16GB 显存低于 4 时 BatchNorm 统计不稳定需配合小学习率学习率1e-4AdamW1e-3 容易在 Dice 项上震荡收敛后换 5e-5 精调Epochs60–100小目标任务收敛偏慢配合验证集早停分辨率是最关键的一项。512×512 在 16:9 的原图上会丢失一些细节但对镜框分割足够如果场景里眼镜占面部比例很小我会先检测人脸并裁剪到 512 再训练而不是直接缩放整张图。学习率用 1e-4 起步几乎不会翻车Dice Loss 的梯度比 CE 更尖锐学习率大了训练 loss 会出现周期性尖峰。5. 眼镜分割最容易翻车的 5 个坑现象、原因、处理5.1 验证集指标很高真实摄像头下一团糟现象在测试集上 mIoU 有 85模型部署到手机摄像头后侧脸、低头、夜间场景集体翻车检测区域要么偏移要么整个丢失。原因数据集主体是正面、室内、正常光照的样本测试集也来自同一分布模型学到的是「人脸正中间有个眼镜」的先验不是真正的眼镜边界。侧脸时镜框投影变化剧烈夜间反光让镜片区域和背景融为一体先验失效。解决划分一个「困难集」单独评估挑出侧脸、暗光、戴帽子的样本用同样的模型跑一遍。我一般会把困难集从训练数据里抽出来冻结不放回只做验证。如果困难集 mIoU 低于正常集 10 个点以上就需要补充对应场景的数据或者在增强里加大仿射旋转幅度和亮度扰动范围。5.2 Mask 缩放后出现锯齿和渐变灰边现象训练集可视化正常predict 时输出的 Mask 边缘有一圈半透明灰边二值化后边缘参差不齐比原标签粗糙很多。原因训练管线里对 mask 用了和 image 相同的缩放方式默认双线性插值会在 0/1 边界插出中间灰度值。虽然训练时被阈值化成 0/1但灰度值污染了模型对边界的判断。解决所有对 mask 的 resize 和仿射变换必须用最近邻插值。在 albumentations 里A.Resize和A.Affine对 mask 默认就是最近邻但自定义脚本里容易忽略。手动cv2.resize时记得写interpolationcv2.INTER_NEAREST。5.3 镜片反光区域被预测成背景现象深色墨镜和透明镜片的表现截然不同透明镜片反光强烈时模型把反光高光区切成了背景镜片中心出现一个大洞。原因标签本身就不一致。标注员在高光区域可能标了背景也可能标了前景模型在高光区域学到的是随机噪声。解决先统计标签里镜片区域的平均灰度分布如果反光区域的标签大量为 0就要么重新标注一批反光样本要么给反光区域单独设一个类别提高标签一致性。最省事的方案是训练时不区分镜框和镜片整体作为前景让模型自己对反光区域做容错。5.4 图像和标签错位字符串排序的坑现象训练 loss 降到 0.2 后验证集 loss 始终降不下去抽了几张图看发现图片和 Mask 完全对不上人物轮廓和眼镜位置都对错了。原因某些脚本用sorted(os.listdir())做配对字符串排序会把img10.jpg排在img9.jpg前面当文件名超过 100 时这种排序会积累大量错位。解决用 split.txt 作为唯一的数据索引不要依赖目录遍历顺序。每次加载数据后先做一次配对校验img cv2.imread(f{img_dir}/{sid}.jpg) mask cv2.imread(f{mask_dir}/{sid}.png, cv2.IMREAD_GRAYSCALE) assert img.shape[:2] mask.shape[:2], fsize mismatch: {sid}尺寸一致还不能完全确认语义对齐更稳妥的方法是在训练前随机抽 10 个样本把 image 和 mask 叠在一起可视化一次。肉眼确认比任何自动校验都可靠。5.5 训练到一半 loss 变成 NaN现象前两个 epoch 正常第三个 epoch 开始 loss 周期性出现 NaN重启训练后随机复现。原因常见原因有三个。Mask 里出现了类别索引超出模型输出通道数输入图像里有全黑或全白异常样本混合精度训练下梯度溢出。解决训练前打印 Mask 的唯一值确认最大类别小于num_classes。全黑或异常的图直接在 Dataset 里跳过if mask.sum() 16: # 前景像素合计少于16个当成坏样本 return self.__getitem__((idx 1) % len(self.ids))混合精度问题可以在前 5 个 epoch 关闭 AMP等训练稳定后再打开。这条经验在分割任务里能省很多排查时间。6. 验证不只是 mIoU三个离线检查与从整镜到部件分割的进阶验证集 mIoU 是必要指标但不足以判断模型能不能用。我习惯在 mIoU 之外加三个离线检查它们能暴露指标掩盖掉的问题。第一个是面积合理性检查。统计预测 Mask 占整图比例眼镜在面部脸部的一般不会超过 30%也不会低于 0.5%。如果某个样本预测出来占比异常说明模型在图片的奇怪位置产生了幻觉区域。第二个是连通域检查。一张图里眼镜正常情况是 1 到 2 个连通域如果在预测 Mask 里数出 5 个以上互相分离的区域大概率是过分割或者背景噪声被误判import cv2 import numpy as np pred (pred_mask 0.5).astype(np.uint8) num_labels, labels cv2.connectedComponents(pred) # 正常约 2~3多于此值说明有碎片第三个是边缘对齐度。把预测 Mask 的边缘像素和输入图像的梯度方向做对比边缘处梯度越大说明贴合得越好。这一步不用写复杂代码简单做法是把边缘叠加到原图上可视化肉眼判断。进阶方向是把「眼镜」整体分割拆成「镜框 镜片」的多部件分割。整个数据集不需要重新标注可以先训一个整体分割模型把预测的眼镜区域裁出来再在这个局部区域上训练一个二分类的镜框/镜片分割小模型。这样做的好处是 AR 试戴场景里可以只替换镜片而不动镜框也可以只换镜框保留镜片光学参数产品价值比单输出一个整体 Mask 高很多。我现在的习惯是拿到任何新的图像分割数据集第一轮先做三件事看标签分布、按人划分数据集、跑一个 batch 过拟合验证管线通不通。这三步做完后面所有训练和排查都顺很多。数据本身不会骗人但数据管线的每一个细节都可能骗你希望你也能少踩这些坑。本文还有配套的精品资源点击获取