☰
眼镜分割数据集实战:从标签处理到U-Net训练全指南
2026/10/9 3:15:14 网站建设 项目流程

简介:面向计算机视觉与语义分割学习者的面部眼镜图像分割数据集,覆盖背景与眼镜两个语义类别,包含约一万六千张原始图像及对应像素级掩码,其中训练集约一万一千二百张、测试集约四千八百张,便于直接用于分割模型的训练与评估。样本涵盖多种人脸姿态、表情变化以及不同光照和场景,可支撑人脸解析、驾驶员注意力监测、智能眼镜佩戴检测、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 目录存放单通道 PNG,0 是背景,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_counts=True) 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(parents=True, exist_ok=True) 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), dtype=np.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, dtype=np.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-Net,Backbone 用 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(rotate=10, scale=0.1)", "ColorJitter(brightness=0.2, contrast=0.2)", "RandomGamma" ]

IMG_SIZE不能设成 256。原因很直接:眼镜腿在 512 原图上可能是 8-10 像素,缩到 256 后只剩四五个像素,U-Net 下采样两层后基本就消失了。BATCH_SIZE为 16 在 24G 显存上可以跑 ResNet34 Backbone;如果显存只有 12G,优先降 batch 到 8,不要先降分辨率。

损失函数我这里写一版经典 Dice Loss:

import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth=1.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 的 logits,target是 float 类型的掩码。与交叉熵混合使用时,我习惯把 Dice Loss 的权重设为 0.7,CE 为 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提到 640,batch size 相应降到 8;训练完成后对预测结果做一次形态学闭运算:

import cv2 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)

闭运算可以连接细小的断缝,但它不能替代模型能力。如果断线是普遍问题,优先查标签原图,看标注员是否漏了镜腿的末端。

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% 盯着看,看边框是不是比人眼还顺。这一步比调学习率更值。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询