尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

16000张眼镜分割数据集:从清洗到训练的完整指南

16000张眼镜分割数据集:从清洗到训练的完整指南 简介这是一份面向图像分割入门与实战的面部眼镜分割数据集包含约16000张人脸图像及对应像素级标签标注类别为背景与眼镜两类适合用于训练语义分割模型、验证分割算法效果。数据集按训练集与测试集划分训练集约11200张图片及masks模板测试集约4800张图片及对应模板并附带classes类别说明文件。压缩包共2000个文件以png图像为主1998个另含txt类别定义与一个Python可视化脚本总大小约849.19MB。该脚本可随机抽取样本将原始图像、GT标签及原图叠加蒙版效果展示并保存便于快速直观检查数据质量与标注效果。目前已有91人学习适合计算机视觉学习者、科研人员及竞赛选手作为眼镜分割任务的基准数据与调试工具。1. 面部眼镜图像分割数据集约16000张图到底能解决什么问题当你准备做一个眼镜分割模型第一个拦路虎往往不是算法而是数据。公开人脸数据集不少但带像素级眼镜标签的很少有些甚至只有几十张。这个约16000张的面部眼镜图像分割数据集正好卡在“够启动”和“需要认真清洗”的中间。它解决的是细分场景数据稀缺的问题适合做人脸解析、AR试戴、眼镜遮挡分析、眼镜检测等任务的算法工程师。和通用图像分割数据集不同眼镜区域小、反光多、边界细16000张图足够撑起一个能看的分割基线但前提是你得读对标签、做对清洗。很多新手拿到这类数据集后直接丢进模型训练结果发现mIoU很高但画出来一团糟十有八九是标签读取或转换环节出了问题。下面按“数据长什么样→怎么清洗→怎么转换→怎么避坑→怎么继续提点”的顺序走一遍完整流程。默认你用的是Python和PyTorchOpenCV做图像处理所有代码在CPU上都能先跑通再上GPU。2. 拆解数据集目录结构、标注格式与类别分布统计2.1 常见目录组织与命名规律拿到约16000张图的眼镜分割数据集第一步不是急着写模型而是先弄清楚文件是怎么摆放的。最常见的是images目录放原图labels或masks目录放掩码也有一个annotations目录下全部是JSON或COCO导出的文件。命名上通常是“同名不同后缀”例如000001.jpg对应000001.png但也常见原图叫face_0001.jpg、掩码叫face_0001_mask.png的情况。先把两个目录的文件列出来做对比ls images | head -5 ls labels | head -5如果前五个名字能一一对应说明命名大概率是同步的。再看总数量ls images | wc -l ls labels | wc -l数量不对等是最容易发现的坑。如果images数量比labels多出几十张往往是标注过程中丢弃了模糊或重复图但原图没有同步清理。我一般的做法是把没有掩码的原图直接移出训练目录而不是强行补一个全黑掩码。16000张里缺几十张不影响分布错误的标签反而会污染模型。还有一种情况是两种文件的扩展名不一致比如原图是.jpg掩码是.png。这不一定是问题但会在统一读取时增加一个判断分支。我习惯用一个小函数做“查表法”配对import os img_dir images mask_dir labels img_names os.listdir(img_dir) mask_names set(os.listdir(mask_dir)) matched [] for name in img_names: stem os.path.splitext(name)[0] if stem .png in mask_names or stem _mask.png in mask_names: matched.append(name) print(matched:, len(matched), total:, len(img_names))这段代码只做一件事确认每个原图是否能找到掩码。这里把_mask.png这种常见后缀也纳入匹配范围避免因为命名习惯不同而误判。2.2 标签的三种常见编码PNG掩码、JSON多边形与RLE眼镜分割数据集的标签格式远不止一种。我遇到过三种主流编码分别对应不同标注工具和数据集发布习惯。第一种是PNG灰度掩码。每张掩码是单通道图片像素值0表示背景1或255表示眼镜区域。读取时最容易踩的坑是忘了加灰度模式导致图像被读成三通道像素值混乱。第二种是JSON多边形常见于LabelMe或VIA工具导出的结果。每个标注对象是一个多边形轮廓需要自行栅格化成掩码。第三种是COCO RLE格式用pycocotools的maskUtils解码优点是紧凑缺点是新手容易被数组维度绕晕。先写一个探测脚本自动判断标签属于哪一种import os, json import cv2 import numpy as np labels_dir labels sample os.listdir(labels_dir)[0] path os.path.join(labels_dir, sample) if sample.endswith(.json): with open(path) as f: data json.load(f) print(JSON keys:, list(data.keys())) else: img cv2.imread(path, cv2.IMREAD_GRAYSCALE) print(shape:, img.shape, unique values:, np.unique(img))通过文件后缀和唯一像素值可以快速定位。unique values只有[0,1]说明是二类掩码有[0,255]需要做归一化如果有多个值比如0,1,2说明标签里可能区分了镜框、镜片、镜腿等子类别。如果你拿到的是JSON多边形需要把多边形转成掩码再参与训练import json import numpy as np import cv2 with open(labels/sample.json) as f: data json.load(f) # 假设图像尺寸已知 h, w 512, 512 mask np.zeros((h, w), dtypenp.uint8) for shape in data[shapes]: points np.array(shape[points], dtypenp.int32) label shape[label] # 给不同类别分配不同灰度值 color 1 if label in [glasses, lens] else 2 cv2.fillPoly(mask, [points], color) print(unique:, np.unique(mask))这里把label名映射为索引polygon通过cv2.fillPoly填充成掩码。注意多边形点坐标可能是浮点需要先转成整数如果图像尺寸和原图不一致还要先按比例缩放坐标。这一步很麻烦但做一次后面所有训练脚本都能受益。RLE格式的读取方式更标准from pycocotools import mask as maskUtils # annotation中的segmentation字段是RLE字典 rle annotation[segmentation] binary_mask maskUtils.decode(rle) print(binary_mask.shape, binary_mask.dtype)decode返回的数组是HxW的uint8类型像素1表示前景。它的好处是直接变成二值掩码不需要手动处理灰度值。2.3 用Python做第一次体检统计类别、尺寸与损坏文件无论标签是什么格式拿到数据集后我都强烈建议做一次全量体检。约16000张图写个脚本几分钟能跑完能避免训练到一半才发现大量标签错位。体检项包括图像尺寸是否统一、图片与标签是否一一对应、掩码内是否有异常像素值、有没有损坏的文件。PIL读取时不会立即加载全部数据但可以用load()强制读完捕捉截断文件。import os from PIL import Image import numpy as np img_dir images mask_dir labels imgs sorted(os.listdir(img_dir)) masks sorted(os.listdir(mask_dir)) print(images:, len(imgs), masks:, len(masks)) broken [] for name in imgs: p os.path.join(img_dir, name) try: with Image.open(p) as im: im.load() except Exception: broken.append(name) print(broken images:, len(broken)) # 统计前200张图尺寸 sizes set() for name in imgs[:200]: with Image.open(os.path.join(img_dir, name)) as im: sizes.add(im.size) print(sizes in first 200:, sizes)如果sizes只有一个元素说明图片尺寸统一可以直接固定输入尺寸如果有多个后面需要统一处理。对于损坏文件我一般先尝试重装图像库或换读图方式如果还是打不开直接删掉对应图片和标签因为这种损坏会在训练时随机爆出异常。再往下是统计掩码里各类别占比这决定了损失函数设计import cv2 pixel_ratios [] for name in os.listdir(mask_dir): m cv2.imread(os.path.join(mask_dir, name), cv2.IMREAD_GRAYSCALE) if m is None: continue m (m 0).astype(np.uint8) pixel_ratios.append(m.mean()) pixel_ratios np.array(pixel_ratios) print(mean fg ratio:, pixel_ratios.mean())眼镜区域占比通常不超过5%。如果统计结果比这个更低比如小于1%那你得考虑是否在数据清洗时把大尺度人脸照都删了只留了眼镜特写。否则模型会极度偏向背景。3. 把原始数据变成可训练样本清洗、裁剪与样本均衡3.1 图像尺寸统一与归一化选保持长宽比还是直接resize分割模型对输入尺寸很敏感。约16000张图中人脸近景、半身、全身照的比例差异可能很大眼镜在整图中的占比从0.5%到15%都有可能。直接拉伸resize到固定方形如512x512会让镜腿变成亚像素边缘严重失真。我一般会先统计长宽比分布再决定策略。如果90%以上图片的长宽比在4:3到3:4之间直接resize问题不大如果比例差异明显优先用letterbox也就是保持原比例缩放四周填充灰色。训练时用letterbox预测时也要用同样的letterbox参数否则坐标对不齐。import cv2 import numpy as np def letterbox(im, size512): h, w im.shape[:2] scale min(size / w, size / h) nw, nh int(w * scale), int(h * scale) resized cv2.resize(im, (nw, nh), interpolationcv2.INTER_LINEAR) canvas np.full((size, size, 3), 114, dtypenp.uint8) x0 (size - nw) // 2 y0 (size - nh) // 2 canvas[y0:y0nh, x0:x0nw] resized return canvas, scale, x0, y0letterbox返回缩放系数和偏移量这两个值必须保存。后面无论是把预测mask映射回原图还是把原图上的标注框映射到网络输入都要用到它们。很多人只做了resize不记录偏移导致评估时预测结果和原图对不上。常见做法是把四个值一起返回例如返回项含义canvas填充后的图像scale原始图像缩放比例x0原始图在canvas上的水平偏移y0原始图在canvas上的垂直偏移归一化方面如果打算用ImageNet预训练权重就用ImageNet的mean和std如果从头训练可以把RGB缩放到[0,1]。掩码不要做减均值保持整数0/1。这是我见过最常被忽略的细节掩码一旦被归一化到[-1,1]所有阈值都会失效。3.2 处理样本不均衡眼镜区域占比统计与损失函数选择眼镜在整张人脸图里占比很小带来的直接问题是网络很容易把背景全预测成正确分类因为背景占比太高。交叉熵损失会被背景主导前景的错误几乎不影响loss。这时候需要两个手段一是统计前景占比二是换损失函数。前面已经写了统计占比的代码这里继续往下说。如果前景平均占比低于5%建议用Dice Loss或Focal Loss。Dice Loss优化的是区域重叠度适合前景小的情况Focal Loss通过调制因子让模型聚焦难分类像素。import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) intersection (pred * target).sum() return 1 - (2.0 * intersection smooth) / (pred.sum() target.sum() smooth)这段代码输出一个标量可以直接加到总loss里。实际使用时我会把交叉熵和Dice按0.50.5比例混合纯DiceLoss收敛不稳定纯交叉熵前景被淹没。混合以后效果通常更好。针对前景区域极小的情况还可以在采样上做文章。将图像按前景占比降序排列每个epoch先取前景占比最高的那批图训练几个step再打乱随机训练。这个做法来自我在一个医疗分割项目里的经验让模型先看到“目标大而清晰”的样本建立基本认知再学难样本。对眼镜分割同样有效。3.3 数据增强哪些操作对眼镜分割友好眼镜分割既依赖镜框边缘也依赖镜片反光增强操作要非常克制。通用检测里常用的随机擦除、大幅旋转放到眼镜上很容易让标签语义失真。我按推荐度排序增强操作增强操作推荐度理由水平翻转高人脸对称但标签分左右眼时禁用轻度旋转±10度中改善姿态泛化过大让镜片反光不真实颜色抖动中提升对光照变化的鲁棒性随机裁剪低容易切掉眼镜仅在特写图上可用代码里我会用albumentations库它会同步处理图像和掩码省去很多麻烦import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.Rotate(limit10, p0.5), A.ColorJitter(brightness0.2, contrast0.2, saturation0.1, p0.3), A.Resize(512, 512), ])这里把RandomCrop去掉了因为我发现眼镜区域本身占比就小再裁剪很容易把镜框切没。如果硬要加我会把裁剪尺寸设成原始图的一半以上并且确保裁剪区域仍包含标注中心。更好的做法是用Mosaic增强把四张图拼在一起但这需要额外处理标签坐标不适合小团队快速实验。另外一个值得注意的点是镜片反光区域在颜色抖动下会变得不真实。如果你发现验证集上透明镜片总是分割失败可以考虑把ColorJitter的saturation调低到0.05或者只在亮度通道增强不做饱和度抖动。4. 格式转换与训练避坑从PNG掩码到YOLO分割格式4.1 把掩码转成YOLO分割格式的完整协议很多人拿到约16000张的眼镜分割数据集是想喂给YOLOv8训练实例分割模型。YOLO分割格式不是像素掩码而是归一化多边形坐标每个目标一行开头是类别ID后面是成对的x,y坐标所有坐标除以图像宽高。如果一张图里有多个目标比如两个人戴眼镜就有多行。从PNG掩码转成YOLO格式的关键是提取轮廓。做法是先用findContours找外部轮廓再用approxPolyDP减少点数最后归一化。为什么不用RETR_TREE因为眼镜框内部如果有空洞比如镜片镂空内部轮廓会被当成独立目标YOLO训练时会增加错误样本。大多数数据集标注的是整个眼镜区域所以RETR_EXTERNAL够用。import cv2 import numpy as np def mask_to_yolo(mask_path, img_w, img_h): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask (mask 0).astype(np.uint8) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) lines [] for cnt in contours: epsilon 0.001 * cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, epsilon, True) pts approx.reshape(-1, 2).astype(np.float32) pts[:, 0] / img_w pts[:, 1] / img_h line [0] pts.reshape(-1).tolist() lines.append( .join(f{v:.6f} for v in line)) return lines参数epsilon越小轮廓越精细但会引入大量冗余点导致训练变慢。我一般用0.002对眼镜这种小目标够用。如果发现转换后mask和原掩码形状差异大可以把epsilon调小到0.0005。另外多类别标签要把class_id从0换成对应类别不要全写0否则YOLO会把镜框镜片混成一类。转换后建议做一次回读验证把生成的YOLO多边形重新填充成掩码和原掩码计算IoU。如果IoU低于0.95说明轮廓点数或坐标转换出了问题。这个验证代码很简单但能避免你拿脏数据训练几十轮。4.2 避坑一掩码灰度值不等于类别索引现象用cv2.imread读掩码后显示像素值有0和255直接当成类别索引1算loss结果所有预测都乱掉。原因标注工具导出时把类别ID写成了255而不是1。很多数据集发布者不会统一做像素值归一化灰度图里255只是“可见白色”不代表类别索引。解决在读取掩码后立刻执行mask (mask 0).astype(np.int64)。这是最可靠的归一化手段。不要用mask // 255因为0除以255是01除以255是0等于全背景。每次读数据都现算不要提前存中间文件避免中间文件又被其他脚本误用。4.3 避坑二EXIF旋转信息导致标签错位现象原图在手机或数码相机里带了EXIF Orientation比如方向标签是6。用PIL的Image.open读图时自动应用旋转但用cv2.imread读不会导致掩码和图像差了一个90度或180度。原因两个库对EXIF的处理策略不一致。PIL会自动做方向修正OpenCV完全忽略。解决统一用PIL读取图像和掩码并在读取时手动处理方向。这里给一个我常用的函数from PIL import Image def read_with_exif(path): im Image.open(path) exif im.getexif() orientation exif.get(274) if orientation 6: im im.rotate(-90, expandTrue) elif orientation 8: im im.rotate(90, expandTrue) elif orientation 3: im im.rotate(180, expandTrue) return im如果你的数据集来源是标准人脸库一般不会有EXIF问题。但当你用手机拍扩充样本时这个坑几乎必踩。训练脚本的数据读取阶段必须统一旋转策略否则前几十个epoch的loss会反复震荡。4.4 避坑三显存不够时不要急着改batch size现象用512输入尺寸和8的batch size显存直接爆掉。于是把batch size改成2模型训练变慢而且效果明显变差。原因batch size缩小后BatchNorm的均值方差估计不稳定梯度更新噪声变大。分割网络对batch size比分类网络更敏感。解决优先把输入尺寸降到384或320保持batch size不变。好多人不知道从512降到384能省将近一半的显存对眼镜这种小物体影响很小。如果尺寸不能再降就开梯度累加每4步或8步更新一次优化器等效扩大batch size。optimizer.zero_grad() loss.backward() if step % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这个办法不需要改动模型结构只是牺牲一点训练速度。另外torch.cuda.amp.autocast()也能在不降batch的情况下省大约30%显存尤其在T4或V100上效果明显。混精度训练偶尔会遇上某些算子不支持半精度我一般只用标准Unet或DeepLab系列自定义注意力模块容易报错。4.5 避坑四mIoU虚高边缘效果差现象验证集mIoU达到0.92但随机抽样看预测图镜腿边缘毛刺严重透明镜片内部出现黑洞。原因mIoU是按像素统计的眼镜区域小少量边缘误差对整个IoU影响不大。更严重的是很多人为了跑得快评估时把预测和真值都缩放到64x64这算出来的IoU几乎没有参考价值。解决除了mIoU增加边界指标比如BF Score也可以算Hausdorff距离。更重要的是可视化。我每次训练完都从验证集抽16张图把原图、真值、预测拼成三联图肉眼过一遍。坏case往往集中在深色镜框、透明镜片、侧脸镜腿三类。def visualize(img, pred, target, save_path): import cv2 h, w img.shape[:2] pred (pred 0.5).astype(np.uint8) * 255 target (target 0.5).astype(np.uint8) * 255 combined np.hstack([img, cv2.cvtColor(target, cv2.COLOR_GRAY2BGR), cv2.cvtColor(pred, cv2.COLOR_GRAY2BGR)]) cv2.imwrite(save_path, combined)这个拼接脚本虽然简单但在项目汇报时很有说服力。指标可以骗人图片不会。如果发现某些bad case集中比如透明镜片总被预测为背景可以考虑在数据增强里加入“局部亮度扰动”模拟反光强、透明度高的场景。4.6 训练参数建议学习率、批量大小与多尺度最后给一套我常用的训练基线。模型用DeepLabV3或U-Netbackbone选ResNet50输入尺寸512batch size 8学习率1e-4配合CosineAnnealingloss用0.5CrossEntropy0.5Dice。epochs在80到120之间16000张图的数据量不需要更久。训练命令示意python train.py --model deeplabv3plus --backbone resnet50 \ --data_root ./face_glasses --size 512 --batch_size 8 \ --lr 1e-4 --epochs 100 --loss ce_dice学习率不要照搬检测任务常用的1e-3分割模型的梯度更稀疏1e-3很容易发散。如果发现loss冲高先把学习率降到3e-5再把batch size调大通常能稳住。多尺度预测可以留到测试阶段训练时固定尺寸反而更容易收敛。5. 进阶用法用伪标签和半监督策略把16000张数据继续挤出效果5.1 用训练好的模型给新数据打伪标签当你已经用约16000张数据训练出一个收敛的模型下一步最划算的不是更换网络结构而是扩充训练数据。手动标分割掩码太贵常见做法是用现有模型推理一批无标签的人脸图像生成伪标签再混入训练集做一轮自训练。这种方法对眼镜这种边界清晰的目标效果不错但必须过滤低质量预测。过滤逻辑分两层像素级和图像级。像素级设置置信度阈值0.9只有预测概率大于该值的前景像素才保留图像级要求伪标签中的眼镜面积占比在1%到95%之间否则整张丢弃。面积过小大概率是误检面积过大可能是模型把背景也标成了眼镜。import torch import torch.nn.functional as F model.eval() with torch.no_grad(): logits model(images) prob F.softmax(logits, dim1) pseudo (prob[:, 1] 0.9).float() area_ratio pseudo.sum(dim(1, 2)) / pseudo.shape[1] / pseudo.shape[2] valid (area_ratio 0.01) (area_ratio 0.95) pseudo pseudo[valid]这个过滤策略能防止模型用自己的错误反复强化。伪标签占整体训练样本的比例最好控制在30%以内否则模型会被自己的“自信错误”带偏。这叫自我训练也叫半监督选干净样本是最重要的一步。5.2 软标签让模型在不确定区域保留连续性如果你觉得硬过滤丢掉太多中低置信度信息可以试试软标签。所谓软标签就是把网络预测的概率图不为0/1掩码而是直接当作回归目标。这样在镜片反光、半透明镜腿等模糊区域模型学到的是一个连续过渡而不是被强行二值化后的抖动边界。loss F.binary_cross_entropy_with_logits(logits, soft_target)其中soft_target是只用置信度高于0.4的预测概率低于0.4的置0。这样做可以减少标签噪声带来的震荡。我用下来的体会是软标签适合最后20个epoch做微调不适合从头训练。从头训练时模型还没有建立基本轮廓软目标会让它学得十分缓慢。5.3 验证可视化对比与边界指标最后一步是建立固定的验证流程。我每次训练完都会跑一组固定样本生成四联图原图、真值、预测、预测叠加在原图上。然后计算三个指标mIoU、Dice、BF Score。BF Score衡量预测边界与真值边界匹配程度对眼镜这种细结构比IoU更敏感。from scipy.ndimage import binary_erosion pred_b pred 0.5 true_b target 0.5 edge_pred binary_erosion(pred_b) ! pred_b edge_true binary_erosion(true_b) ! true_b tp (edge_pred edge_true).sum() fp (edge_pred ~edge_true).sum() fn (~edge_pred edge_true).sum() precision tp / (tp fp 1e-6) recall tp / (tp fn 1e-6) bf_score 2 * precision * recall / (precision recall 1e-6)这个脚本可以写进验证函数每次训练完自动输出。我的习惯是先看可视化图再看边界指标最后才看mIoU。因为mIoU在背景占比极大的眼镜分割里几乎总是很高不能真实反映边界质量。把这个验证流程固定下来以后无论是换模型还是换增强你都能快速看出真正的变化。这几年我做过的分割项目只要数据规模在万张左右流程基本一样先体检、再清洗、然后转换格式、训练时盯边界指标、最后用伪标签提点。眼镜分割比一般物体分割更依赖细边界所以每一步都不能跳过。希望这套方法能帮你在自己的项目里少踩几个坑。本文还有配套的精品资源点击获取
返回列表