16000张面部眼镜图像分割数据集:从清洗到训练全流程解析
简介图像分割数据集面部眼镜图像分割数据集约16000张数据和标签面向图像分割算法学习与模型训练人群可用于人脸佩戴眼镜区域的二分类分割任务背景与眼镜。数据集划分为训练集和测试集训练集约11200张图片及对应掩膜测试集约4800张满足模型训练与评测基本需求。资源共2000个文件以1998张png图片为主含原图与GT掩膜辅以1个类别txt和1个Python可视化脚本压缩包整体849.19MB。可视化脚本可随机抽取图片将原始图、GT图及GT在原图上的蒙板效果显示并保存便于快速查看样本质量与标注效果。目前已有91人学习下载适合中高级图像分割学习者、科研人员及算法工程师直接用于实验、迁移学习或基准对比。1. 面部眼镜图像分割数据集为什么说这 16000 张图是刚需做图像分割的同行应该都有体会找公开数据集不难难的是找一个“任务边界干净、标签可用、规模够训练”的细分数据集。眼镜分割就是这种典型场景——人脸检测、人脸解析、美颜虚化、AR 试戴、活体检测预处理全都绕不开“把眼镜从脸上抠出来”这一步。而这个面部眼镜图像分割数据集约 16000 张图和标签正好卡在学术界和工业界都需要的空档上。这个数据集解决的核心问题很直接眼镜区域是什么、边界在哪、哪些像素属于镜框和镜片。和通用人像分割数据集不一样它把“眼镜”单拎出来做标签省去了自己从 COCO 或 ADE20K 里筛类别、抠 mask、合并背景的脏活。适合谁用第一是做人脸解析和美颜 SDK 的需要稳定的眼镜先验第二是做 AR 试戴眼镜功能的产品需要一个能直接喂进分割模型的训练集第三是研究半监督、域自适应分割的拿这个数据集当干净 benchmark 跑基线非常顺手。接下来我按自己做数据清洗和模型训练的习惯把这个数据集从标签结构、训练流程到踩坑点完整拆一遍。2. 数据集的标签体系与目录结构先看懂再动手2.1 从目录到文件名一套能直接进 DataLoader 的布局拿到数据集后第一件事不是看图片而是看目录。常见做法是训练集、验证集、测试集分文件夹或者所有图片平铺、标签文件同名对应。这个数据集约 16000 张图和标签目录一般长这样face_glasses_seg/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ ├── 000002.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── masks/ │ ├── train/ │ │ ├── 000001.png │ │ ├── 000002.png │ │ └── ... │ ├── val/ │ └── test/ ├── annotations/ │ └── train.json └── train.txt如果你的压缩包解出来不是这个布局而是类似images/xxx.jpg和labels/xxx.png分开平铺那就按上面对应的逻辑重新组织。我习惯先把train.txt里列出的文件名读一遍确认图片和标签一一对应避免后面训练时出现“图有一张、标缺一张”的隐性问题。标签文件常见两种格式一种是单通道 PNG像素值为 0 和 10 是背景1 是眼镜区域另一种是三通道 PNG 但内容相同肉眼看着是黑白图。前者直接当 mask 用后者要做一步降维。我在清洗时会把三通道 PNG 读取后取第一个通道并断言该通道和另外两个通道完全一致防止出现“看似彩色实为灰阶”的隐藏不一致。2.2 像素级标签和实例级标签的区别这个数据集大概率是语义分割图像分割任务里语义分割和实例分割的标签格式差别很大。语义分割只要一张单通道 PNG每个像素值对应一个类别实例分割则要用 COCO 那种 JSON每个眼镜是一个独立 polygon训练时再转成 mask。这个数据集约 16000 张图和标签如果是以 PNG mask 为主那基本就是语义分割设定类别就是背景 0 眼镜 1。有个细节值得注意一张人脸若戴了墨镜和近视镜在语义分割标签里它们都叫“眼镜”不会区分镜框粗细、镜片颜色。这对任务本身影响不大因为大多数下游产品只关心眼镜整体区域。但如果你想做镜框款式分类或者要分别输出镜框和镜片两个通道就需要自己重新标注或二次处理。这一点在项目规划阶段就要定清楚否则训练到一半才发现标签粒度不够返工成本不低。3. 把 16000 张图喂进分割模型从数据清洗到训练配置3.1 清洗脚本过滤坏图、修复标签、统一尺寸拿到数据集后我一般会先跑一个完整的健康检查脚本。这个脚本做四件事检查图片能否正常解码、检查图片和标签是否成对存在、检查标签是否有超出类别集合的像素值、检查图片和标签的尺寸是否一致。16000 张的量级不算大脚本几分钟就能跑完但能省掉后面训练中断的麻烦。import cv2 import numpy as np from pathlib import Path img_root Path(images/train) mask_root Path(masks/train) bad_pairs [] wrong_size [] for img_path in sorted(img_root.glob(*.jpg)): mask_path mask_root / (img_path.stem .png) if not mask_path.exists(): bad_pairs.append(str(img_path)) continue img cv2.imread(str(img_path)) mask cv2.imread(str(mask_path), cv2.IMREAD_UNCHANGED) if img is None or mask is None: bad_pairs.append(str(img_path)) continue if mask.ndim 3 and mask.shape[2] 3: mask mask[:, :, 0] if img.shape[:2] ! mask.shape[:2]: wrong_size.append(str(img_path)) continue uniq np.unique(mask) if not set(uniq).issubset({0, 1}): bad_pairs.append(str(img_path)) print(bad pairs:, len(bad_pairs)) print(wrong size:, len(wrong_size))这段脚本的逻辑要点有三处。第一bad_pairs收集两类问题——标签缺失和图片解码失败这些样本直接踢出训练集。第二mask.ndim 3时取第一个通道这是处理三通道灰阶 PNG 的通用做法比直接cv2.imread默认转 BGR 更安全。第三np.unique检查像素值是否严格属于 {0, 1}如果有人工标注时用了 255 表示前景这一步能立刻暴露出来避免 loss 计算时出现诡异行为。3.2 训练集划分按人头分而不是按图分如果你打算把整个数据集用于微调、做领域自适应实验划分策略会影响最终指标。常见错误是随机打乱后按 8:1:1 划分但如果同一张人脸在连续帧里出现多次随机划分会让人脸泄漏到训练集和验证集导致验证指标虚高。这个数据集如果是单人单图为主风险稍微小一些但仍然值得按人物维度划分。import random from pathlib import Path all_files list(Path(images).glob(*.jpg)) random.seed(42) random.shuffle(all_files) train_ratio 0.8 val_ratio 0.1 split_idx1 int(len(all_files) * train_ratio) split_idx2 int(len(all_files) * (train_ratio val_ratio)) train_files all_files[:split_idx1] val_files all_files[split_idx1:split_idx2] test_files all_files[split_idx2:] with open(train.txt, w) as f: for p in train_files: f.write(str(p) \n) # val.txt / test.txt 同理这里的核心参数是random.seed(42)固定随机种子保证每次复现划分一致。split_idx1和split_idx2用的是比例计算而非硬编码数量方便后面调整比例。注意我把划分结果写进了 txt 文件而不是直接在训练脚本里每次重排——因为每次重排会导致验证集变化模型对比就失去了公平性。写盘这种小而重要的习惯能省下很多复现时的自查时间。3.3 训练配置U-Net 还是 DeepLabV3输入尺寸和 loss 怎么选对于 16000 张的二分类分割任务我一般第一个 baseline 用 U-Net 或 DeepLabV3。尺寸上原始图片如果是 512x512 左右直接缩放到 256x256 或 512x512 都行如果原图更大建议先等比缩放再 center crop保留人脸区域的完整性。Batch size 显存不够就开梯度累积不要强行把图压得太小否则眼镜边缘的细节信息会被磨掉。import torch import torch.nn as nn class SegmentationLoss(nn.Module): def __init__(self, bce_weight0.5, dice_weight0.5): super().__init__() self.bce_weight bce_weight self.dice_weight dice_weight self.bce nn.BCEWithLogitsLoss() def forward(self, pred, mask): bce_loss self.bce(pred, mask) pred_prob torch.sigmoid(pred) intersection (pred_prob * mask).sum(dim(1, 2, 3)) dice (2 * intersection 1e-6) / (pred_prob.sum(dim(1, 2, 3)) mask.sum(dim(1, 2, 3)) 1e-6) dice_loss 1 - dice.mean() return self.bce_weight * bce_loss self.dice_weight * dice_loss这个 loss 是二分类分割的常用组合。BCE 提供像素级梯度Dice loss 缓解正负样本不平衡——眼镜区域的像素占比通常在 5% 到 15% 之间纯 BCE 会让模型倾向预测背景。参数上bce_weight0.5和dice_weight0.5是稳妥起点如果你发现 mask 边缘毛刺多可以把 dice_weight 调到 0.7如果发现小眼镜框经常漏检回调 bce_weight。1e-6是防止分母为零的常数2 * intersection是 Dice 系数的标准写法分子分母都用了平滑项。训练阶段其他参数我一般这样设输入尺寸 512x512batch size 8AdamW 初始学习率 1e-4每 10 个 epoch 乘 0.5 衰减训练到验证集 Dice 不再上升就停。16000 张图在这个配置下单张 RTX 3090 上 30 到 50 个 epoch 就能收敛大约耗时两到三个小时。如果你的显存只有 8Gbatch size 降到 4输入尺寸降到 384效果差距可以接受。4. 二分类分割的评估指标Dice、IoU 和边缘误差怎么算4.1 Dice 和 IoU两个指标一起看避免被单指标骗了分割任务最常用的两个指标是 Dice 和 IoU它们对同一个模型给出的排名可能不同。Dice 对正样本的召回更敏感IoU 对背景像素的精确率更敏感。二分类眼镜分割里模型如果偏好把镜框内部全填满过分割Dice 可能很高但 IoU 会掉得明显反过来模型如果只在镜框边缘外扩一圈IoU 变化小Dice 却会下滑。所以评估脚本里两个指标都要算。def compute_metrics(pred_mask, gt_mask): pred_mask pred_mask 0.5 gt_mask gt_mask 0.5 intersection (pred_mask gt_mask).sum() union (pred_mask | gt_mask).sum() pred_sum pred_mask.sum() gt_sum gt_mask.sum() iou intersection / union if union 0 else 0.0 dice (2 * intersection) / (pred_sum gt_sum) if (pred_sum gt_sum) 0 else 0.0 return dice, iou这个函数里阈值取 0.5 是默认做法但实际模型输出的概率分布往往偏向 0.7 或 0.3你在验证集上扫一遍阈值再定最终值会更稳。pred_mask gt_mask和pred_mask | gt_mask利用了布尔数组的位运算比逐像素循环快得多。还有一个细节union是并集不是pred_mask gt_mask如果两个 mask 有重叠区域算错公式会让 IoU 虚高。4.2 边缘评估轮廓距离和对称性分割模型的分数再高也不等于边缘贴合。眼镜框细、镜腿细哪怕只偏两三个像素视觉效果也会很假。我常用轮廓平均距离Hausdorff distance 的平均版本来补充验证。做法是用 OpenCV 提取预测 mask 和真值 mask 的轮廓再计算两轮廓点集之间的最近距离均值。这个指标对工业生产里“边界是否可接受”的判断非常直接。import cv2 def contour_similarity(pred_mask, gt_mask): pred_contours, _ cv2.findContours(pred_mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) gt_contours, _ cv2.findContours(gt_mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not pred_contours or not gt_contours: return -1 pred_pts np.vstack([c.reshape(-1, 2) for c in pred_contours]) gt_pts np.vstack([c.reshape(-1, 2) for c in gt_contours]) dists [] for p in pred_pts: diffs gt_pts - p dist np.sqrt(np.sum(diffs ** 2, axis1)).min() dists.append(dist) return float(np.mean(dists))这段代码把两个轮廓中的点两两算欧氏距离取最近距离的均值。注意我用的是np.sqrt(np.sum(diffs ** 2, axis1))而不是np.linalg.norm(diffs, axis1)前者在 numpy 版本迁移上更稳后者的axis参数行为在不同版本上偶尔有差异。轮廓距离小于 3 个像素基本能说边缘贴合不错如果大于 5 个像素就需要检查是不是分辨率丢失或标签本身偏了。5. 眼镜分割的常见坑与排查清单这些坑我建议你绕开5.1 训练 loss 降不下去先怀疑标签再怀疑网络现象BCE 和 Dice 组合 loss 前几十个 epoch 一直抖动验证集 Dice 卡在 0.6 上下不去。原因最常踩的坑是 mask 和图片没对齐——有人用 OpenCV 读图默认是 BGR但标签是 RGB 顺序读出来的位置偏移几个像素不易察觉还有的图片原始分辨率不同mask 是从另一个路径生成的尺寸虽然经过检查一致但空间对应关系已经错位了。另一个原因是图片里出现大量“不戴眼镜的人脸”mask 全黑这类样本占比超过 30% 时会严重拖后腿。解决清洗脚本里加一步可视化——把img和mask用cv2.addWeighted叠加后存图抽查 20 张。确认 mask 确实叠在眼镜区域上再训练。全黑 mask 的样本如果占比高单独分一个子集作为背景类不要全混在主训练集里。顺带说一句有些人喜欢把所有黑 mask 删掉但如果测试集里也有一半人不戴眼镜这种做法会让模型在真实场景乱检反而更糟。5.2 验证集指标高、实际场景效果差域差异和分辨率掉坑现象在验证集上 Dice 到 0.93换到手机实拍图上眼镜区域要么漏检要么把眉毛和眼镜框连成一片。原因这个数据集里的图片大多是网图和公开人脸集分辨率、光照、眼镜款式分布相对集中。实际场景里可能出现反光镜片、粗黑框、半框眼镜、戴帽子和口罩的组合这些在数据集里的占比不够。另一个常见问题是训练时把图缩放到 256x256模型学到的纹理分辨率低推理时输入 1080p 原图反而特征对不上。解决训练时做多尺度增强输入尺寸在 384 到 640 之间随机采样推理时用原图比例而非强制 512x512。同时准备一个 100 到 200 张的“域外验证集”——随手拍的几张眼镜图就够了。如果验证集指标很高、域外验证集掉点优先加数据增强随机亮度、对比度、高斯噪声不要盲目换网络结构。5.3 镜框细、镜腿断标注稀疏和类别不平衡现象训练完的 mask 上细镜框中段经常出现断裂镜腿区域几乎消失。原因人工标注在细缝区域容易抖动标签里镜腿部分的边界本身不连续模型学到的边界自然是碎的另外眼镜像素占比低背景样本在 loss 里占绝对主导细长结构被压缩了。解决把 dce_weight 提高或者干脆去掉 BCE 只用 Dice loss逼迫模型更关注前景结构。数据增强里加随机形态学膨胀和腐蚀每次迭代让镜框粗细小幅变化能有效增强细结构的鲁棒性。我还会在后处理里对 mask 做一次闭运算cv2.morphologyEx配合MORPH_CLOSE核大小取 3x3能缝合大部分断裂。5.4 标签和图片数量对不上16 张图的消失事件现象按train.txt读文件时某个文件名在图片文件夹里存在但标签不存在训练中断在mask_path.exists()之外。原因常见于解压时文件系统大小写敏感差异或压缩包自带损坏。Windows 上解压出的JPG后缀和 Linux 上的jpg不一致Path.glob(*.jpg)就匹配不到。解决写清理脚本时不要用glob(*.jpg)改为glob(*)然后检查后缀名转换为小写再做集合匹配。或者在预处理阶段把所有文件名统一改成小写这是最省心的方案。压缩包文件在传输过程中的丢包导致的坏文件清洗脚本里的cv2.imread返回None已经能兜住。5.5 checkpoint 保存和复现标签 mismatch 的隐形元凶现象同一个训练脚本隔几天重跑验证集指标差异超过 2 个点。原因最常见的不是模型变化而是数据顺序被打乱了。随机划分时忘记固定随机种子或者 augmentation 的随机种子在每次运行时不同都会让验证集翻车。另一个隐蔽问题是 PyTorch DataLoader 的num_workers设置过大可能在一次采样时返回了上一个大 batch 的缓存数据。解决固定所有随机源包括random.seed、numpy.random.seed、torch.manual_seed和cudnn.benchmark的状态。num_workers设置为 4 或 8并在 DataLoader 的worker_init_fn里给每个子进程设置独立随机状态。事件和时间相关的拍照数据不要做多线程采样把persistent_workersFalse保持默认。跑完训练后把训练配置、数据划分文件、最终指标存档在同一目录后续回溯时只需要这一份目录。6. 进阶用法用分割结果做边缘拟合和后期配准数据集的边界能更窄模型输出的 mask 不等于可以直接上线的结果尤其是需要接 3D 渲染或 AR 场景时像素级 mask 不够用。这里分享一个进阶技巧把 mask 转成边缘点集再用 RANSAC 拟合椭圆或直线得到更稳定的眼镜框几何参数。主要思路是这样先对 mask 做连通域分析取最大的连通域作为主眼镜区域再对连通域的边缘做轮廓提取最后分镜框和镜腿两段分别用椭圆拟合和直线拟合得到几何参数。这一步能让下游不用直接吃 raw mask而是用少量参数去驱动 UI 或渲染抗噪能力强很多。def fit_glasses_frame(mask): contours, _ cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None largest max(contours, keylambda c: cv2.contourArea(c)) points largest.reshape(-1, 2).astype(np.float32) ellipse cv2.fitEllipse(points) return ellipsecv2.fitEllipse返回的是(中心点, (长轴, 短轴), 旋转角度)在镜框呈椭圆形态时效果不错。但要注意它要求至少 5 个点而且对噪声敏感先 RANSAC 提纯点集再拟合会更稳。对直线型的镜腿可以用cv2.fitLine返回的是一个归一化方向和直线上一点用它们可以算镜腿的朝向和长度。最后把拟合结果按置信度排序只保留面积占比超过整张图 2% 的连通域防止零碎噪声干扰。这套做法的价值在于它把分割模型的输出抽象成了可调参的几何描述。我在做 AR 试戴时先用分割拿到眼镜 mask再拟合出镜框椭圆中心、旋转角度和长短轴并以此锚定虚拟眼镜的位置和缩放。这个数据集的 16000 张图足够支撑这套流程的前半段。另一个可验证的方向是边缘对比度在测试集上计算 mask 和图片边缘的梯度对齐度如果梯度对齐度低于 0.8说明模型在低对比度镜框上不稳定可以考虑在训练集里加入更多的低光照图片增强。数据集的边界也值得再挖一挖如果镜片是透明的mask 会包含镜片内部但镜片区域和脸部的对比度很低这时可以训练一个双分支网络一个分支负责镜框边缘一个分支负责镜片区域。没有现成的双分支数据没关系用这个数据集先拆出两个单通道标签验证各自分支的效果再合并成一个 mask也是一个可落地的实验方向。做模型评估时我建议最后一步总是人工目检一份最难样本我挑的是细框 低光照 转头三个条件叠加的图。自动指标只是均值真正决定产品体验的往往是那 10% 边角案例。我在一次交付前翻车就是因为细边框漏了半边指标却接近满分。那之后我就养成了这个习惯也希望帮到你。本文还有配套的精品资源点击获取