ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

眼镜分割数据集实战:从16000张标注图到Unet与YOLOv8模型落地

眼镜分割数据集实战:从16000张标注图到Unet与YOLOv8模型落地 简介面向面部图像理解与分割任务这是一套包含约16000张图像及对应像素级标注的眼镜分割数据集适合计算机视觉学习者、算法工程师及科研人员用于训练和评估眼镜区域分割模型。数据按训练集与测试集划分训练集约11200张测试集约4800张每部分均含原始图像与mask模板目录类别仅背景与眼镜两类便于直接开展二分类语义分割实验。资源包共2000个文件以png图像与掩码为主体附带classes.txt类别说明及可视化脚本py压缩后约849MB结构清晰、即下即用。除数据外脚本支持随机抽取样本并合成展示原图、GT及叠加结果方便快速核对标注质量与模型效果整体可作为语义分割入门或面部部件识别研究的优质训练样本。目前已有91人学习下载适合需要眼镜区域标注数据或进行分割基准测试的开发者参考使用。1. 眼镜分割数据集约16000张标注图能做什么、值得不值得投入图像分割数据集里人脸方向的不少但专门把眼镜作为分割目标的很少。这个面部眼镜图像分割数据集约16000张数据和标签解决的是在人脸图像中逐像素抠出眼镜区域的任务。对做人脸识别前处理、AR 虚拟试戴、驾驶员疲劳检测的工程师来说最耗时间的不是模型而是标注眼镜腿和头发挨着、镜片反光、墨镜全黑这些边界让标注员也头疼。适合两类人一是刚入门想跑通图像分割完整流程的新手拿一份现成标签练手最划算二是手头有模型但缺高质量掩码的团队直接从这里开始清洗、训练和验证。数据本身不解决所有问题但能帮你跳过最枯燥的几个星期。2. 拆开数据集标注格式、目录结构与数据体检2.1 标注格式对比mask 文件夹、COCO JSON 还是 RLE 压缩拿到数据集先别急着找模型第一步是弄清楚标签的组织方式。常见做法是两种一种是 image 和 mask 各一个目录mask 是 PNG 灰度图背景 0、前景 255另一种是 COCO 风格所有标注汇总到一个 annotations.json 里用多边形坐标或 RLE 编码存储。这两种格式对后续训练的路线影响很大。格式存储方式优点常见坑PNG mask 目录每张图片对应一张灰度 PNG直观、可用任意分割框架文件名对不齐、灰度取值不统一COCO JSON多边形坐标 RLE适合检测/分割统一训练解析复杂多边形有孔洞时难还原RLE 单图压缩编码存文本省空间解码麻烦肉眼无法直接检查不管标题里说约16000张你拿到手第一件事就是确认这个数字以及目录层级是否符合预期# 解压后先看两层目录结构确认 image 和 label/mask 的存放位置 find . -maxdepth 2 -type d | head -20 # 统计图片文件总数和标题声称的约 16000 对一下 find . -type f \( -name *.jpg -o -name *.png -o -name *.jpeg \) | wc -l # 查看是否有独立的 label/mask 目录以及文件后缀 ls -la ./images | head -5 ls -la ./masks | head -5我一般会把这个输出保存下来作为数据版本的第一条记录。注意find命令里的-o表示或三种常见扩展名都要统计如果实际数量与标题差的远先看是不是有子目录嵌套有些数据集把 train/val/test 分开根目录下统计不全。一个容易被忽略的点是有的数据集 mask 是 PNG 但原图是 JPGfind按后缀统计会漏掉跨格式匹配所以后面 Python 体检时还要按文件名 stem 再对一次。2.2 用 Python 做数据体检图像尺寸、标签取值与文件对齐命令行只能看数量真正决定训练是否顺利的是标签质量。我做分割数据集的第一步永远是写一个体检脚本跑三件事核对文件名一一对应、统计 mask 的取值分布、记录图像尺寸范围。这三件事能挡住后续 80% 的翻车。from pathlib import Path from PIL import Image import numpy as np import collections img_dir Path(images) mask_dir Path(masks) # 以文件名 stem 为 key 做对齐忽略后缀差异 img_stems {p.stem: p for p in img_dir.iterdir() if p.suffix.lower() in (.jpg, .png, .jpeg)} mask_stems {p.stem: p for p in mask_dir.iterdir() if p.suffix.lower() in (.png, .jpg)} print(f原图数量: {len(img_stems)}) print(f掩码数量: {len(mask_stems)}) print(f有图无掩码: {len(set(img_stems) - set(mask_stems))}) print(f有掩码无图: {len(set(mask_stems) - set(img_stems))}) # 标签取值分布取前 300 张掩码统计 counter collections.Counter() for stem, path in list(mask_stems.items())[:300]: arr np.array(Image.open(path).convert(L)) counter.update(np.unique(arr).tolist()) print(掩码像素取值分布:, dict(counter))逻辑说明convert(L)强制转灰度避免三通道 PNG 读进来变成 (H, W, 3) 导致np.unique的结果变成数组。取值分布这一步非常关键——有的标注工具把背景导出成 255、前景导出成 0和常见情形正好相反有的 mask 里混入 127 这种过渡值说明标注皮肤时开了抗锯齿。正常情况下应该只出现 0 和 255或 0 和 1。参数说明前 300 张是我常用的抽样量能覆盖大多数脏数据模式且不会太慢。约16000张的数据全量统计也不慢但读 PNG 非常吃磁盘 IO抽样先看一轮更高效。发现取值里只有 0 和 1 时训练时不用做阈值处理发现 255 时一定要在数据加载代码里加/255这是训练 loss 不下降的经典隐性原因之一。2.3 可视化抽查叠加 mask 看边界是否可信数值体检通过了不代表标注没问题。眼镜分割标签有它的特殊性镜框和眉毛、头发在灰度图上几乎融为一体标注员稍不留神就会把眉毛画进 mask。所以必须做可视化抽查随机抽几十张原图和 mask 叠加看。import matplotlib.pyplot as plt def show_overlay(img_path, mask_path, alpha0.6): img np.array(Image.open(img_path).convert(RGB)) m np.array(Image.open(mask_path).convert(L)) # 生成红色半透明叠加层mask 0 的位置标红 overlay img.copy() overlay[m 0] (255, 0, 0) blended (img * (1 - alpha) overlay * alpha).astype(np.uint8) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img) axes[0].set_title(原图) axes[1].imshow(m, cmapgray) axes[1].set_title(mask) axes[2].imshow(blended) axes[2].set_title(叠加) plt.tight_layout() plt.show()逻辑说明overlay[m 0] (255, 0, 0)直接把原图上对应像素改为纯红alpha 混合让边界看得清又不遮挡细节。这里特意用m 0而不是m 255就是为了兼容前一步体检发现的各种取值情况。参数说明alpha0.6是我试过比较舒服的值太高看不清原图纹理太低红色不显眼。抽查时重点看三类问题一是 mask 是否把眉毛、睫毛包进去了二是眼镜腿的末端是否准确结束在脸侧而不是一路画到耳朵三是同一人不同角度的两张图mask 大小是否和图中眼镜的实际尺寸一致。这一步不需要写代码自动判断人眼扫几十张就能建立对数据集质量的直觉后面调参才有底气。3. 训练前的数据准备按人划分、格式转换与增强边界3.1 按人物身份划分训练集和验证集避免同人串集人脸相关的数据翻车最多的不是网络结构而是划分方式。约16000张图如果来自有限数量的拍摄对象同一人的不同照片往往高度相似——光照、角度、背景接近。如果随机划分训练集和验证集里会出现同一个人的照片模型记住这个人就足够了验证指标虚高部署到真实场景换个陌生人效果断崖式下跌。from sklearn.model_selection import GroupShuffleSplit from pathlib import Path img_files sorted(Path(images).glob(*.jpg)) # 假设文件名形如 person_001_002.jpg取前两段作为人物 ID groups [p.stem.rsplit(_, 1)[0] for p in img_files] split GroupShuffleSplit(n_splits1, test_size0.15, random_state42) train_idx, val_idx next(split.split(img_files, groupsgroups)) print(f总人物数: {len(set(groups))}) print(f训练集图片: {len(train_idx)}, 验证集图片: {len(val_idx)})逻辑说明GroupShuffleSplit和普通train_test_split的区别在于它接受groups参数保证同一个 group 的所有样本只进训练或只进验证。关键在rsplit(_, 1)[0]怎么取人物 ID——这取决于数据集的文件名规则有的叫face_001_01.jpg有的叫id_001_img_02.jpg必须先抽样打印文件名确认分隔符和字段位置。参数说明test_size0.15对约16000张的数据集意味着验证集约 2400 张足够稳定评估分割指标如果后面要做早停或模型选择可以提到 0.2。random_state42固定下来方便不同实验之间对比——这一步很多人忽略随机种子不固定两次实验之差还不如随机划分的开销大。如果数据集没有提供人物 ID 信息文件名也无法推断还有一个近似办法对所有人脸区域做特征向量聚类把同一簇分到同一组。用人脸识别模型提特征、按余弦相似度聚类虽然不是精确分组但能有效降低串集程度。3.2 把 PNG mask 转成 YOLO 分割格式polygon 与归一化坐标用 YOLOv8 训练自己的数据集已经是图像分割落地场景里的常见路线但 yolo 系列的分割格式不是像素 mask而是归一化多边形坐标所以必须做格式转换。约16000张 mask转换脚本要跑得干净、可重复我一般这样写import cv2 import numpy as np def mask_to_yolo(mask_path, class_id0): m cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 统一成二值图无论标签是 0/255 还是 0/1都能处理 _, binary cv2.threshold(m, 127, 255, cv2.THRESH_BINARY) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8)) # RETR_EXTERNAL 只取最外层轮廓避免内轮廓重复标注 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) h, w binary.shape lines [] for cnt in contours: # 点数太多会拖慢训练且 loss 波动用多边形近似压点数 epsilon 0.001 * cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, epsilon, True) pts approx.reshape(-1, 2).astype(np.float64) pts[:, 0] / w pts[:, 1] / h coords .join(f{x:.4f} {y:.4f} for x, y in pts) lines.append(f{class_id} {coords}) return lines逻辑说明cv2.threshold把 mask 变成严格二值图避免过渡值变成多边形里的锯齿。MORPH_CLOSE闭运算把细小空洞补上对眼镜这种内部可能有镜片反光空洞的 mask 很实用。RETR_EXTERNAL只提取最外层轮廓换成RETR_LIST会把镜框内部的孔洞也当成轮廓输出训练时同一张图出现重叠标签。参数说明epsilon0.001 * arcLength是我常用的压缩系数换算成点数大概是每个轮廓保留 20~60 个点。眼镜腿是细长结构epsilon 太大会把腿压成直线太小则每个 mask 动辄几百个点训练代码组装 batch 时会明显变慢。转完后必须抽几张图把多边形画回原图对比肉眼确认轮廓贴合度这一步不能省多边形近似造成的精度损失是可见的。3.3 数据增强的边界眼镜是刚性物体翻转旋转要克制数据增强是分割任务提升泛化能力最有效的手段之一但人脸眼镜场景有自己的特殊约束。眼镜是刚性物体水平翻转是合理的镜像对称垂直翻转则完全错误——没有人倒着戴眼镜。旋转角度也要克制人脸检测之后接分割时图里的人脸通常已做过对齐旋转超过 30 度就开始偏离真实分布。import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.3), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, p0.3), A.Affine(scale(0.9, 1.1), rotate(-15, 15), translate_percent(-0.05, 0.05), p0.5), A.Resize(512, 512), ])逻辑说明albumentations的分割版本会在内部自动同步原图和 mask 的变换不需要手动把同样的随机参数传给 mask。这里没有加随机裁剪因为眼镜在整张人脸中占比不大随机裁剪如果切掉眼镜区域等于喂了一批没有目标的负样本如果一定要裁剪应配合人脸区域约束或检测框信息。参数说明rotate(-15, 15)对正视人脸是合理范围如果数据集里有大量侧脸可以放宽到(-25, 25)。scale(0.9, 1.1)模拟距离变化不要小于 0.8否则眼镜的细节纹理被插值抹平。亮度增强对有反光的镜片尤其重要——很多实际图片里镜片白花花一片训练时见过这种变化推理时才不会把反光区域全部判成背景。4. 从约16000张到可用模型Unet 与 YOLOv8-seg 两条落地路线4.1 Unet 路线小显存也能跑通的最小配置Unet 图像分割是语义分割的经典架构在医学图像分割领域被验证过无数次拿来做眼镜分割同样合适尤其是想严格控制参数量和推理延迟的时候。约16000张数据对 Unet 来说不算多用 ImageNet 预训练编码器做迁移学习是关键。习惯上我会用 segmentation_models_pytorch 这套封装省去手写 decoder 的麻烦。import torch import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels3, classes1, # 二分类背景 眼镜 activationNone, # 不在模型里加 sigmoid方便和 loss 组合 ) loss_dice smp.losses.DiceLoss(modebinary) loss_bce torch.nn.BCEWithLogitsLoss() # 组合 lossdice 对小目标稳定bce 对梯度回传平滑 loss_fn lambda pred, target: loss_dice(pred, target) 0.5 * loss_bce(pred, target) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5)逻辑说明activationNone让模型输出 logitsBCEWithLogitsLoss 内部会做 sigmoid数值上比手动 sigmoid 再接 BCELoss 更稳定这个细节影响收敛但常被忽略。DiceLoss(modebinary)在类别不平衡时比纯 BCE 好得多——眼镜区域在整张图中通常只占 3%~8%纯 BCE 会让模型学会输出全背景。参数说明resnet34是平衡点比 resnet18 精度好、比 resnet50 省显存512 输入下 batch size 8 大约占 6~7G 显存。lr1e-4是迁移学习的常见起点配合ReduceLROnPlateau在验证 loss 停滞时降为 0.1 倍。Dice 和 BCE 的权重配比 1:0.5 是我调出来的稳健组合如果发现边界粗糙可以调成 1:1 让 BCE 多发挥一点。训练过程中我建议每 5 个 epoch 保存一次 checkpoint并同时记录验证集 IoU。Unet 的训练曲线存在 loss 平了但 mask 边界还在抖的假平稳期单看 loss 容易提前停掉。4.2 YOLOv8-seg 路线格式转换后的训练命令与参数如果后续要接目标检测、跟踪或者部署到边缘设备YOLOv8-seg 是比 Unet 更省心的选择。约16000张的数据集经过第 3 章格式转换后组织成 yolo 要求的目录结构就能直接训练。很多人处理数据集用于 yolo 训练时前两次都会因为 yaml 路径或类别编号问题报错这里给出完整配置。# glasses_seg.yaml path: ./glasses_seg # 数据集根目录相对路径或绝对路径均可 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 names: 0: glasses # 类别编号必须和转换脚本里的 class_id 对应# 训练命令nano 模型起步先验证流程能不能跑通 yolo segment train \ dataglasses_seg.yaml \ modelyolov8n-seg.pt \ epochs80 \ imgsz640 \ batch16 \ device0 \ patience15逻辑说明yaml 里的names编号必须和 mask_to_yolo 转换脚本写入 txt 时的class_id一一对应。训练命令里modelyolov8n-seg.pt表示从官方预训练权重继续训练比从头训练收敛快得多patience15表示验证集指标连续 15 个 epoch 不上升就早停约16000张的训练量通常用不到 80 个 epoch早停能省不少时间。参数说明imgsz640是 yolo 系列的默认输入眼镜这类细节较多的目标如果觉得分割边缘不够细可调到 768 或 1024显存占用大约按平方关系上涨batch16在 8G 显存上刚好能跑显存不足时先降 batch 而不是降 imgszbatch 影响梯度稳定性imgsz 影响分割精度。patience15不要设太小分割任务的验证指标波动比检测大太激进会在局部波动处停掉。4.3 验证指标怎么组合着看IoU、Dice 与边界误差训练完怎么判断模型好坏只看一个指标会被骗。约16000张的数据集我建议至少同时记录三个指标IoU、Dice 系数和边界平均距离误差。它们各有盲区——IoU 对小块漏检敏感Dice 对整体重叠敏感边界误差直接反映分割结果的可用性。指标计算方式主要盲区适用场景IoU交集/并集小目标漏检时下降不明显整体评估Dice2倍交集/(预测真值)对目标形状变化不敏感类别不平衡时更稳边界距离预测边界与真值边界平均像素距离无法反映内部空洞眼镜腿细长结构评估def compute_iou(pred_mask, gt_mask, threshold0.5): pred (pred_mask threshold).astype(np.uint8) gt (gt_mask 0).astype(np.uint8) intersection np.logical_and(pred, gt).sum() union np.logical_or(pred, gt).sum() return intersection / (union 1e-6)逻辑说明threshold0.5是常用分割阈值如果推理时误检偏多可以在验证集上扫一遍 0.3~0.7 之间的阈值找到最优点并沿用这是分割项目通用的后处理步骤。union 1e-6防止除零——某些极端图中没有眼镜真值为全 0预测也为全 0 时 IoU 定义上是 1但多数验证脚本会跳过这种图统计时要把规则写清楚。参数说明边界距离需要先用cv2.findContours提取预测边界和真值边界再逐点算最近距离。我一般只对验证集里眼镜区域占比大于 5% 的图片计算边界误差目标太小时该指标数值波动很大看整体均值没有意义。5. 眼镜分割避坑指南5 个让人返工的细节5.1 现象训练 loss 降得很顺利可视化输出却全黑或全白原因绝大多数情况下是训练时用了BCEWithLogitsLoss或DiceLoss但推理时直接把模型输出当成概率没有做 sigmoid。模型输出是 logits正值代表前景、负值代表背景直接阈值化时如果整体输出偏移就会出现全黑所有 logits 为负或全白所有 logits 为正。解决推理前统一对输出做torch.sigmoid(pred)再与 0.5 比较。同时检查数据加载时 mask 是否做了归一化——如果标签是 0/255 而代码里没除以 255目标值是 255模型会拼命把 logits 往大推最终输出全白。这类问题在拿到新数据集的第一个训练循环里最容易出现先验证单张图的 forward 输出再跑全量能省一晚上。5.2 现象眼镜腿总是断分割结果只有镜片没有镜腿原因眼镜腿在整张图中只占几百个像素是典型的细长小目标。Unet 或 yolo 的下采样会把 640 输入压到 20 或 40眼镜腿宽度只有几个像素时在深层特征图里已经消失Dice loss 按像素占比加权镜腿贡献的 loss 太小模型倾向于放弃它。解决三个手段叠加。一是提高输入分辨率到 768从根本上保住镜腿像素二是 loss 层面把 Dice 权重调高或换成TverskyLoss这类能对假阴性加权的 loss三是训练后处理时对分割结果做一次形态学闭运算cv2.morphologyEx(pred, cv2.MORPH_CLOSE, kernel)能把镜腿细小断裂补上。代价是闭运算会让边界膨胀 1~2 像素对边缘精度要求高时只对镜腿邻近区域做闭运算更精细。5.3 现象验证集 IoU 有 0.9换到实际拍的照片效果很差原因这是人脸类数据集最经典的陷阱——同人串集。随机划分时同一人的多张照片同时进入训练集和验证集模型记住的是这个人而不是眼镜的形状。另外数据集里如果大量是白墙背景、正面光验证集继承这种单一分布真实场景里的侧光、杂乱背景直接让模型失效。解决严格按人物 ID 分组划分验证集里的脸绝不能出现在训练集。检查方法很简单随机抽验证集 10 张图看里面的人脸在训练集中能否找到同一张脸。没有人物 ID 时用 3.1 节的特征向量聚类来分组。部署前再准备一个外部陌生人人脸测试集数量不用多50 张就足够暴露问题。5.4 现象墨镜和透明镜片的标签语义混在一起原因约16000张数据集如果来自多个标注员很容易出现规则不一致有人把墨镜的深色镜片标成前景有人把透明镜片标成背景只标镜框有人把镜片和镜框合成一整块有人把两者分开。训练时语义混乱模型输出的边界就会抖动。解决第一步按 mask 的面积分布做离群检测——同一类目标面积应相对集中如果分布出现明显双峰大概率是标注规则分裂。第二步明确标签规则面向眼镜整体分割时把镜框和镜片合并为同一类面向镜框追踪时把透明镜片视为背景只分割框体。规则定好后用脚本剔除或改写不符合规则的 mask再重新跑数据体检。5.5 现象数据集里混入低分辨率头像和漫画人脸原因人脸图像分割数据集通常会清洗图片来源但 16000 张规模难免有漏网之鱼常见的有社交软件头像缩略图、网络漫画或插画以及分辨率低到眼镜无法辨认的图。这些样本对训练是纯噪声模型会学到错误的纹理和边界特征。解决按图像尺寸和清晰度做两轮过滤。第一轮过滤尺寸小于 200×200 的图第二轮用cv2.Laplacian算子计算方差方差低于阈值时判定为模糊图直接剔除阈值我常用 50~80视数据集整体清晰度调整。过滤后重新跑第 2 章的体检脚本再做一次可视化抽查。整个过程落成脚本后续换数据集可以复用。6. 让数据集增值从二值掩码到多属性标签与边缘部署约16000张数据加上训练好的分割模型其实只完成了一半。眼镜分割在实际项目里很少单独使用它往往是更大系统的前置模块。我自己的做法是把二值掩码继续往上叠加业务价值其中一个最有用的方向是拿到分割结果后提取眼镜区域的最小外接矩形和长宽比做镜框类型分类。无框眼镜的宽高比通常在 2.8~3.2 之间墨镜通常更宽圆形镜框的外接矩形比例接近 1.5依靠这个特征搭配一个简单分类器就能达到实用精度不需要重新训练一个检测模型。另一个值得做的是把训练好的分割模型导出到端侧部署格式。Unet 转 ONNX 的代码很短但有几步必须盯紧import torch model.eval() dummy torch.randn(1, 3, 512, 512) # opset_version 11 以上才能覆盖主流边缘推理框架dynamic_axes 支持输入尺寸动态变化 torch.onnx.export( model, dummy, glasses_seg.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch, 2: height, 3: width}}, opset_version11, )导出后我用 onnxruntime 加载 ONNX 文件喂一张训练时没见过的图对比 PyTorch 输出与 ONNX 输出最大误差超过 0.01 就得排查算子兼容问题。约16000张的数据在这个阶段的优势就体现出来了不会担心微调样本不够可以放心地把一部分数据留给蒸馏和量化校准。INT8 量化时用 500 张有代表性的图片做校准集眼镜分割的精度损失通常能控制在 1 个点以内。最后我保留一个习惯每次拿到新数据集都先写一遍第 2 章那样的体检脚本不管换什么分割任务都先跑一遍。这个动作帮我避开过太多次训练到半夜才发现标签问题的返工与其在训练中段怀疑人生不如在第一天花 20 分钟把数据看透。希望帮到你。本文还有配套的精品资源点击获取
返回列表