
简介面向计算机视觉与语义分割学习者的面部眼镜图像分割数据集覆盖背景与眼镜两个语义类别包含约一万六千张原始图像及对应像素级掩码其中训练集约一万一千二百张、测试集约四千八百张便于直接用于分割模型的训练与评估。样本涵盖多种人脸姿态、表情变化以及不同光照和场景可支撑人脸解析、驾驶员注意力监测、智能眼镜佩戴检测、AR/VR交互等实际应用。资源包共两千个文件以PNG图像与标签文件为主另含类别定义文本和Python可视化脚本脚本可随机抽取一张样本将原始图片、真实掩码以及掩码叠加在原图上的效果同屏展示并保存帮助快速审核标注质量和直观理解分割结果。压缩包整体约849.19MB目录结构清晰便于本地管理与复用。目前已有91人学习适合需要高质量带标注人脸眼镜数据集的开发者与研究人员。1. 眼镜分割数据集是什么16000张到底够不够用如果产品经理突然说“要上线一个在线试戴眼镜的功能”你第一时间要解决的问题不是模型结构而是“图像分割数据集”。面部眼镜图像分割数据集约16000张数据和标签解决的就是这件事把约16000张人脸图像和对应的像素级标签打包好让模型能学会把眼镜从背景、皮肤、头发里分出来。对图像分割团队来说这个量级在单一类别任务上已经足够启动训练但真正决定它值不值得投入的是标签语义怎么设计、数据怎么划分、训练参数怎么调。这篇笔记从数据集怎么看、标签怎么转、模型怎么训到常见的翻车点逐一拆开面向想把这套数据真正跑出效果的算法工程师、研究者和做AR/VR产品落地的人。2. 面部眼镜图像分割数据集的内部结构标签格式与类别分布拿到任何一个分割数据集都不要直接丢进训练脚本。我一般会先用几分钟把数据盘一遍图像长宽、标注文件类型、掩码类别数、标签边界是否贴合。约16000张的规模不算小但足够多的脏数据会抵消数量优势这一步做得越细后面返工越少。2.1 拿到手先做三件事看尺寸、查格式、可视化标签先看目录组织。常见的数据集结构是把人脸原图和一个同名掩码分开存少数会提供 JSON 多边形或 COCO 格式。不要假设一定是哪种先列文件。data/ ├── images/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... └── masks/ ├── 000001.png ├── 000002.png └── ...上面的 masks 目录存放单通道 PNG0 是背景1 是眼镜前景。如果数据集提供的是 COCO JSON就需要在训练前转成掩码下一章会写转换脚本。先做这一步是为了确认掩码不是三通道彩色图、不是黑白颠倒也不是只标注了部分样本。接下来统计图像尺寸、掩码类别和眼镜面积占比。我习惯先抽前 100 张做快速检查from PIL import Image from pathlib import Path import numpy as np image_dir Path(images) mask_dir Path(masks) img_paths sorted(image_dir.glob(*.jpg))[:100] for img_path in img_paths: mask_path mask_dir / img_path.name.replace(.jpg, .png) img np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path).convert(L)) uniq, counts np.unique(mask, return_countsTrue) glasses_ratio counts[1] / mask.size if 1 in uniq else 0 print(img_path.name, img.shape, mask.shape, uniq, round(glasses_ratio, 4))这段代码把图像和掩码读成 NumPy 数组然后统计掩码里的像素类别。mask是单通道灰度图类别值直接从np.unique里能看到glasses_ratio表示眼镜区域占全图的比例。如果比值长期低于 0.01模型的损失函数就一定要处理类别不平衡。如果你看到掩码里出现 255 这类值说明标签被存成了带颜色的 PNG需要先转成 0/1 索引图。再花一分钟可视化叠加结果这一步能发现标签错位和漏标import matplotlib.pyplot as plt fig, axes plt.subplots(1, 2, figsize(9, 4)) axes[0].imshow(img) overlay img.copy() overlay[mask 1] (0, 255, 0) axes[1].imshow(overlay) plt.show()把绿色叠加层直接盖在原图上能直观看出多边形标注是否沿着镜框边缘走。我见过不少“切分错误”问题图像和掩码文件名对不上模型训练时用错标签可视化能立刻暴露。2.2 语义分割标签还是实例分割标签眼镜框、镜片与镜腿怎么分这个数据集要支撑的最终用途决定了标签体系。最常见的方案是二分类语义分割背景 0眼镜整体为 1。适合遮挡检测、人脸预处理和基础试戴。另一种是把眼镜拆成镜框、镜片、镜腿三类用于精细化换镜框或光学分析。两种方案并不冲突但一开始不确认团队内部很容易出现标注口径不一致。我习惯先跑一个二分类基线。原因是眼镜分割属于典型的“细长目标”任务镜腿可能只占图像宽度的一小段再拆成多个类别会让小目标样本更难学。如果产品确实需要区分镜框和镜片再在二分类掩码基础上做类别细化也不迟。实际项目里人类标注员对“镜框下边缘和镜片的边界”都可能产生分歧这种噪声会直接影响模型上限。标签方案适用场景优点风险二分类背景/眼镜遮挡检测、人脸预处理、试戴标注简单噪声小收敛快后续还要再做结构拆分三分割框/片/腿精细化换镜、光学分析一次到位省后处理类别不平衡严重边缘标注争议大如果是三分割损失函数建议给镜腿这一类更高的权重。眼镜腿像素面积常常只有镜框的 1/3不处理的话模型会优先把资源花在背景和镜框上。另一个容易被忽略的点是透明镜片无色镜片在图像里接近背景如果你希望把它也算进前景标签规范里必须写清楚并且训练时增加对“透明区域”的增强否则模型会把它学成背景。这里的核心结论是先确定语义分割标签要分几类再决定其余所有训练细节。3. 把原始标签转成统一训练格式JSON转PNG掩码与数据集划分很多面部眼镜分割数据集不是直接给你一堆 PNG 掩码而是给 COCO JSON 或 VGG JSON。JSON 适合存储多边形和多实例标注但训练 U-Net 时每次都解析 JSON 不仅慢还容易出现边界自交、空洞等图形错误。我一般会先统一转成 PNG 掩码后续读取和可视化都方便。3.1 写一个转换脚本把COCO多边形变成单通道mask下面的脚本接收一个 COCO 格式 JSON输出同名 PNG 掩码。假设 JSON 里segmentation可能是多边形坐标也可能是 RLE 编码。import json import numpy as np import cv2 from pathlib import Path from pycocotools import mask as coco_mask def coco_to_png(json_path, img_root, out_root): out_root Path(out_root) out_root.mkdir(parentsTrue, exist_okTrue) with open(json_path) as f: coco json.load(f) cat_id_to_label {cat[id]: i 1 for i, cat in enumerate(coco[categories])} for img_info in coco[images]: img_id img_info[id] height, width img_info[height], img_info[width] merged_mask np.zeros((height, width), dtypenp.uint8) for ann in coco[annotations]: if ann[image_id] ! img_id: continue seg ann[segmentation] label cat_id_to_label[ann[category_id]] if isinstance(seg, list): # 多边形点阵用 fillPoly 填充 polygons [ np.array(p, dtypenp.int32).reshape(-1, 2) for p in seg ] cv2.fillPoly(merged_mask, polygons, label) elif isinstance(seg, dict): rle_obj coco_mask.frPyObjects(seg, height, width) ann_mask coco_mask.decode(rle_obj) mask_uint8 (ann_mask 0).astype(np.uint8) * label merged_mask np.maximum(merged_mask, mask_uint8) out_name Path(img_info[file_name]).stem .png cv2.imwrite(str(out_root / out_name), merged_mask)这段代码的核心逻辑是先遍历 COCO 的images再匹配annotations。cv2.fillPoly负责处理多边形坐标RLE 则交给 pycocotools 解码然后通过np.maximum把多个实例的掩码合并到同一张图中。如果同一张图里有两副眼镜最后得到的merged_mask上每个像素的类别值只保留最大的标签值。参数说明cat_id_to_label把 COCO 里的原始分类 ID 映射到从 1 开始的连续值避免出现标签值为 5、8 这种断层img_root这个参数在实际转换时用于验证原图是否存在于file_name对应路径。注意保存格式一定是 PNG不要用 JPG否则压缩会引入伪边缘相当于给标签主动加噪声。3.2 按身份而不是按文件随机划分同人同镜是验证集虚高的最大元凶如果你直接把 16000 张图随机划分成 train 和 val验证指标看起来会非常漂亮但换一批新用户立刻现原形。原因很简单同一个人的多张脸图或者同一副眼镜在不同角度的照片可能同时出现在训练集和验证集模型其实在“背”这副眼镜而不是在“学习”分割眼镜的结构。我建议用人物身份分组后再划分。很多数据集的文件名会带人物前缀比如face_023_01.jpg、face_023_02.jpg。可以先解析前缀作为分组依据。import random from pathlib import Path random.seed(42) img_paths list(Path(images).glob(*.jpg)) def group_key(path): # 根据实际命名规则提取身份ID parts path.stem.split(_) return f{parts[0]}_{parts[1]} groups {} for p in img_paths: groups.setdefault(group_key(p), []).append(p) group_names list(groups.keys()) random.shuffle(group_names) val_ratio 0.15 val_count int(len(group_names) * val_ratio) train_groups group_names[val_count:] val_groups group_names[:val_count] train_files [p for g in train_groups for p in groups[g]] val_files [p for g in val_groups for p in groups[g]] print(len(train_files), len(val_files))核心是最后两行先把组名打乱再按组取文件而不是按单张图片取文件。如果直接用train_test_split就会出现同人泄露。参数说明val_ratio取 0.15 在 16000 张规模下大约能得到 2400 张验证图但如果每个身份组只有三四张图验证集的多样性会受限。遇到这种情况我会额外检查验证集里是否覆盖了不同的眼镜类型方框、圆形、无框、墨镜、女性大框、男士粗框至少各出现一部分。验证集样本可以少但分布不能偏。4. 训练眼镜分割模型图像分割算法选型与U-Net参数实践标签和划分准备好以后进入模型选择。语义分割的公开模型很多但眼镜任务有它的特殊性目标细长、类别少、边界要求高。不同图像分割算法在这个任务上的表现差距主要来自对边界细节的保持程度。4.1 为什么先试U-Net而不是DeepLabV3小目标、边界和单卡显存U-Net 结构是编码器逐层下采样解码器逐步恢复分辨率最后输出的特征图尺寸和输入一致。这种设计对镜框、镜腿这样的细长结构非常友好因为上采样路径能保留高分辨率位置信息。DeepLabV3 的优势在大尺度多类别分割它的空洞卷积对“细线”不算友好16 倍下采样后一条只有 10 像素宽的镜腿可能会在特征图里消失。下面是一个实际选型对比模型典型显存512x512边界保留部署难度适用场景U-Net ResNet344-6 GB好低单类别分割、中等规模数据集DeepLabV3 MobileNetV33-5 GB中中移动端轻量化SegFormer/BiSeNet4-7 GB中高偏高需要更强上下文建模在只有人脸和眼镜的二维图像里上下文信息并不复杂。U-Net 的归纳偏置足以把镜框和头发、眉毛区分开。所以第一版模型我会直接选 U-NetBackbone 用 ResNet34训练时间短单卡能跑后续换轻量 Backbone 也容易。4.2 训练参数怎么定损失函数、输入尺寸、数据增强与学习率先给一个我常用的训练配置# train_config.py IMG_SIZE 512 BATCH_SIZE 16 NUM_CLASSES 2 LR 1e-4 EPOCHS 60 LOSS dice ce AUGMENT [ RandomHorizontalFlip, Affine(rotate10, scale0.1), ColorJitter(brightness0.2, contrast0.2), RandomGamma ]IMG_SIZE不能设成 256。原因很直接眼镜腿在 512 原图上可能是 8-10 像素缩到 256 后只剩四五个像素U-Net 下采样两层后基本就消失了。BATCH_SIZE为 16 在 24G 显存上可以跑 ResNet34 Backbone如果显存只有 12G优先降 batch 到 8不要先降分辨率。损失函数我这里写一版经典 Dice Lossimport torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, pred, target): pred torch.sigmoid(pred) inter (pred * target).sum() union pred.sum() target.sum() return 1 - (2 * inter self.smooth) / (union self.smooth)这个实现适合二分类输出pred是模型未过 sigmoid 的 logitstarget是 float 类型的掩码。与交叉熵混合使用时我习惯把 Dice Loss 的权重设为 0.7CE 为 0.3避免 Dice 在梯度上过于激进。数据增强里ColorJitter和RandomGamma直接针对暗光和偏色场景。眼镜是强反射物体镜片反光会造成局部高亮色阶增强能让模型对这类干扰更鲁棒。水平翻转对左右对称的眼镜是安全的垂直翻转不建议因为人脸结构并不是上下对称的随意翻转会让模型学到错误的位置先验。如果你更熟悉 YOLOv8 训练自己的数据集也可以把掩码转成 YOLOv8-seg 的格式跑一个快速基线。但对这种边界精度要求高的任务U-Net 的像素级损失更可控。5. 常见问题排查眼镜分割训练里的五个翻车现场把 16000 张数据真正跑起来之后你大概率会遇到下面这些问题。每一条我都按现象、原因、解决写清楚都是实际项目里反复出现的坑。5.1 验证mIoU很高换一批人脸就崩现象训练验证指标能到 0.90 以上第一次做灰度测试时用户随手拍一张证件照模型把下巴轮廓也圈进了眼镜区域。原因绝大多数情况是数据划分泄漏。随机划分让同一个人的不同表情、角度同时出现在训练集和验证集模型记住了人物特征而没学会眼镜特征。另外标注里可能把镜框和眉毛之间紧贴的阴影也标了进去。解决先用第 3.2 节的按身份分组划分重做训练然后手动检查验证集里是否出现和训练集同款眼镜。如果这两步都做了还有问题就是标签噪声需要把置信度最低的 50 个训练样本可视化挑出边界画错的图重新修正。5.2 镜框预测断成一截一截现象掩码在镜框和镜腿连接处出现空洞鼻梁处也经常断视觉上像虚线。原因输入尺寸不够或者 U-Net 解码器恢复的边界信息不足。眼镜框只有几个像素宽如果在编码器最深层已经无法分辨解码器再怎么上采样也补不回来。还有一种可能是原始标注多边形本身就存在未闭合的线段。解决把IMG_SIZE提到 640batch size 相应降到 8训练完成后对预测结果做一次形态学闭运算import cv2 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2)闭运算可以连接细小的断缝但它不能替代模型能力。如果断线是普遍问题优先查标签原图看标注员是否漏了镜腿的末端。5.3 透明镜片被当成背景现象无框眼镜或透明镜片眼镜预测掩码只包含镜框镜片区域整片丢失。原因镜片在图像中接近背景色许多标注工具里也看不清到底该不该标。如果数据集最初的标签规范没有说明模型自然会把接近背景的像素归为背景。解决先在标签规范里明确“透明镜片属于前景”然后重新生成掩码把镜片区域补上。如果不想重新标注可以在训练时对验证集单独统计“透明镜片样本比例”确认模型在关键场景上到底漏了多少。对于产品需求透明镜片是否重要取决于你是做人脸反遮挡还是做试戴前者必须保证轮廓完整。5.4 损失在降mIoU不动现象训练到第 30 轮Dice Loss 继续下降但类别 IoU 一直停留在 0.2 左右。原因当背景占 95% 以上像素时模型很容易陷入“全部预测为背景”的假收敛。只看损失曲线会发现它很低但前景完全没被预测出来mIoU 自然上不去。解决换 Dice CE 混合损失并单独输出前景 IoU。Dice Loss 相当于给前景强加权能逼着模型把注意力放到眼镜区域。此外检查标签里是否存在全背景图如果训练集混入几十张没有眼镜的图模型会被它们带着走。5.5 暗光、阴影下误检严重现象夜间或侧面光的人脸图模型把阴影区域当成眼镜的一部分掩码边缘不规则。原因眼镜在低光下与眉毛、眼窝阴影融合而训练集大多以均匀光照为主增强里没有覆盖这类场景。解决训练时加入RandomGamma和 CLAHE 预处理。更直接的手段是在训练集里加入部分“人工调暗”的图把亮度降低 30% 后再写入增强管线。推理阶段我一般会在输入模型前先做一次简单的图像亮度归一化避免模型对绝对亮度敏感。6. 进阶用边界IoU、后处理修边和轻量化部署压榨数据集的剩余价值当 mIoU 已经跑到 0.85 以上再盯着整体 IoU 调参收获不大。眼镜这种细长目标更需要看边界误差。我建议额外计算一个“边界 IoU”只取真实边界内外各 5 像素的窄带在这个带内计算预测和真实的交集占比。边界 IoU 比全局 mIoU 更敏感能直接反映镜框边缘画得够不够干净。部署到移动端时一个实用技巧是把 U-Net 的 Backbone 换成 MobileNetV3再用 ONNX 导出并做 int8 量化。模型体积可以从几十 MB 压缩到 5 MB 左右边界会有少量损失但对试戴贴图来说完全可接受。量化后如果发现边缘锯齿追加一次cv2.morphologyEx开运算通常能磨平杂点。还有一个值得做的进阶技巧利用掩码生成眼镜贴图替换。分割出眼镜区域后把新眼镜图像按仿射变换对齐到旧眼镜的质心和角度再用掩码作为透明度权重做 alpha blending。这也是试戴产品里最常用的落地方式。这个流程做完你会发现数据集的真正价值不在 mIoU 多高而在它能不能支持你把一副新眼镜自然“戴”到任意人脸上去。我习惯每次调完模型都把那十几张最差的预测图放大到 100% 盯着看看边框是不是比人眼还顺。这一步比调学习率更值。希望帮到你。本文还有配套的精品资源点击获取