ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于2948张图像的小数据集YOLO眼镜检测实战:从数据准备到模型调优

基于2948张图像的小数据集YOLO眼镜检测实战:从数据准备到模型调优 简介这份资源面向从事计算机视觉与目标检测的开发者、学生及算法工程师提供一套可直接用于训练的眼镜检测数据集解决玻璃镜片与眼镜目标识别中样本获取与标注成本高的问题。压缩包共2000个文件以xml标注文件为主同时配套yolo格式txt标签与data.yaml配置文件整体约127.93MB已按训练集与验证集划分完毕可无缝接入yolov5、yolov8、yolov9、yolov7、yolov10及yolo11等主流框架。数据集包含2948张图像覆盖两种标签格式yolo格式以类别索引与归一化中心点、宽高比例记录目标框voc格式则保留标准xml结构便于不同训练流程灵活切换。目前已有128人学习下载适合希望快速验证眼镜检测模型、开展课程设计或进行算法对比实验的读者能省去数据采集与标注环节直接进入模型训练与效果评估阶段。1. 眼镜检测数据集与 YOLO 落地2948 张带标签图像能撑起什么手上拿到一个标注好的数据集第一反应往往不是兴奋而是怀疑2948 张图像、带标签、玻璃镜片场景这个量级到底能不能训出一个能用的 YOLO 眼镜检测模型我做过好几个类似规模的小数据集项目结论是——能但前提是你得把「数据集的真实边界」摸清楚而不是直接yolo train一把梭。眼镜检测这个任务看着简单实际坑不少镜框细长、镜片反光、玻璃材质和背景对比度低、侧脸和遮挡场景下框会飘这些都会让一个在 COCO 上表现良好的预训练模型直接翻车。这个数据集的核心价值在于「场景聚焦」2948 张图像全部围绕眼镜/玻璃镜片这一类目标标注是目标检测框格式适合直接喂给 YOLOv5、YOLOv8 或 YOLO11 系列。它解决的不是「通用目标检测」问题而是「在有限样本下把单一品类检测做稳」的问题。适合谁适合做智能眼镜门店客流分析、驾驶疲劳监测里的眼镜佩戴判断、AR/VR 头显佩戴检测、以及安防场景下「是否戴眼镜」这类二分类衍生任务的工程师。如果你手上正好有类似需求又不想从零标注几千张图这个数据集能帮你省掉最贵的那部分人力成本。但我要先把话说在前面2948 张不是「随便训训就 SOTA」的量级。它属于典型的小数据集训练策略、划分方式、增强手段的选择比模型结构本身更决定成败。下面几章我会按「先看清数据 → 再跑通训练 → 再调参 → 再避坑 → 最后讲进阶」的顺序把这个数据集怎么用讲透。2. 先验数据2948 张眼镜图像在训练前必须做的四件事拿到压缩包解压之后别急着写训练脚本。小数据集的成败70% 在进模型之前就决定了。这一章讲的是数据侧的准备包括格式核对、划分策略、类别分布检查和可视化抽检。2.1 解压后的目录结构与标签格式核对常见做法是数据集里图像和标签分开存放标签为 YOLO 格式的.txt每行class_id x_center y_center width height坐标归一化到 0~1。先跑一段脚本确认这件事而不是假设它一定对。import os from pathlib import Path from PIL import Image root Path(glasses_dataset) # 解压后的根目录按实际改 img_dir root / images lbl_dir root / labels bad [] for img_path in img_dir.glob(*.jpg): lbl_path lbl_dir / f{img_path.stem}.txt if not lbl_path.exists(): bad.append((missing_label, img_path.name)) continue with Image.open(img_path) as im: w, h im.size for line in lbl_path.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: bad.append((bad_format, img_path.name, line)) continue cls, x, y, bw, bh parts vals list(map(float, [x, y, bw, bh])) if any(v 0 or v 1 for v in vals): bad.append((out_of_range, img_path.name, line)) if float(bw) 0 or float(bh) 0: bad.append((zero_size, img_path.name, line)) print(f总图像数: {len(list(img_dir.glob(*.jpg)))}) print(f问题条目: {len(bad)}) for b in bad[:20]: print(b)这段脚本做三件事检查每张图是否有对应标签、检查每行是否为 5 个字段、检查归一化坐标是否越界或宽高为零。参数上img_dir和lbl_dir按你解压后的实际路径改如果图像是.png把glob(*.jpg)换掉。跑完如果问题条目是 0说明格式干净如果有out_of_range大概率是标注工具导出时用了像素坐标没归一化需要整体除以图像宽高修复。2.2 训练/验证/测试划分小数据集别用 8:1:12948 张如果按 8:1:1 划分验证集只有不到 300 张测试集不到 300 张。眼镜检测里侧脸、遮挡、反光这些「难样本」本来就少随机划分很容易让验证集里一个难样本都没有导致训练指标虚高、上线翻车。我一般用 7:2:1并且做分层抽样——按「目标数量」和「图像亮度」两个维度分层。import random import numpy as np from pathlib import Path from PIL import Image random.seed(42) img_dir Path(glasses_dataset/images) lbl_dir Path(glasses_dataset/labels) records [] for img_path in img_dir.glob(*.jpg): lbl_path lbl_dir / f{img_path.stem}.txt lines lbl_path.read_text().strip().splitlines() n_obj len(lines) with Image.open(img_path) as im: gray np.array(im.convert(L)) brightness gray.mean() records.append((img_path.name, n_obj, brightness)) # 按目标数量分桶桶内按亮度排序后轮转分配保证分布均匀 records.sort(keylambda r: (r[1], r[2])) train, val, test [], [], [] for i, rec in enumerate(records): if i % 10 7: train.append(rec[0]) elif i % 10 9: val.append(rec[0]) else: test.append(rec[0]) print(len(train), len(val), len(test))这里的关键不是代码本身而是「排序后轮转」这个动作先按目标数量、再按亮度排序然后按 7:2:1 的节奏切分等价于在二维特征上做了均匀采样。参数seed42保证可复现i % 10的阈值决定比例。如果你的数据集里单图目标数差异很大比如有的图 1 副眼镜、有的图 5 副这个划分能避免验证集全是单目标图。2.3 类别分布与框尺寸分布决定 anchor 和增强策略眼镜检测通常只有 1~2 类眼镜、可能还有太阳镜或护目镜。先统计每类实例数和框的宽高分布这直接决定你要不要调 anchor、要不要用 Mosaic。from collections import Counter import numpy as np cls_counter Counter() wh_list [] for lbl_path in lbl_dir.glob(*.txt): for line in lbl_path.read_text().strip().splitlines(): c, x, y, w, h line.split() cls_counter[int(c)] 1 wh_list.append((float(w), float(h))) wh np.array(wh_list) print(类别实例数:, dict(cls_counter)) print(宽高均值:, wh.mean(axis0)) print(宽高 5%/95% 分位:, np.percentile(wh, [5, 95], axis0))如果 95% 分位的宽高都小于 0.1归一化后说明目标偏小YOLO 默认 anchor 可能偏大需要重新聚类或直接换用 YOLOv8 这种 anchor-free 的版本。如果类别实例数严重不均衡比如眼镜 5000 个、太阳镜 200 个训练时要用cls损失加权或在数据增强里对少数类过采样。这一步不做后面调参就是盲调。2.4 可视化抽检用脚本把框画回图上数字检查完一定要肉眼过一遍。抽 20~30 张把标签框画回原图看有没有框偏、漏标、错标。import random from PIL import Image, ImageDraw from pathlib import Path img_dir Path(glasses_dataset/images) lbl_dir Path(glasses_dataset/labels) samples random.sample(list(img_dir.glob(*.jpg)), 24) canvas Image.new(RGB, (4*320, 6*320), (30, 30, 30)) for idx, img_path in enumerate(samples): im Image.open(img_path).convert(RGB).resize((320, 320)) draw ImageDraw.Draw(im) w, h im.size for line in (lbl_dir / f{img_path.stem}.txt).read_text().strip().splitlines(): c, x, y, bw, bh map(float, line.split()) x1, y1 (x - bw/2)*w, (y - bh/2)*h x2, y2 (x bw/2)*w, (y bh/2)*h draw.rectangle([x1, y1, x2, y2], outline(0, 255, 0), width2) canvas.paste(im, ((idx%4)*320, (idx//4)*320)) canvas.save(check_labels.jpg)跑完打开check_labels.jpg重点看三类问题框是否只框住镜框而漏掉镜腿、反光镜片是否被标成整块、侧脸时框是否明显偏移。发现系统性偏移说明标注规范有问题要么修要么在训练时接受这个噪声并靠增强缓解。3. 用 YOLOv8 在本地跑通眼镜检测的最小训练链路数据确认干净之后进入训练环节。这一章以 YOLOv8 为例因为它在小数据集上的默认表现比较稳且 anchor-free 设计省掉了调 anchor 的麻烦。如果你用 YOLOv5 或 YOLO11流程类似差异我会在参数说明里点出来。3.1 环境安装与数据配置文件先建环境。我一般用 conda 隔离Python 3.10 是当前比较稳的选择。conda create -n glasses python3.10 -y conda activate glasses pip install ultralytics8.2.0 opencv-python pillow numpy装完确认版本yolo checks然后写数据配置文件glasses.yamlpath: /abs/path/to/glasses_dataset train: images/train val: images/val test: images/test names: 0: glasses注意path必须是绝对路径train/val/test是相对path的子目录。如果你的数据集划分后图像还在原目录需要先按 2.2 的结果把文件移动到images/train、images/val、images/test标签同步移动到labels/train等。这一步用shutil.move写个脚本即可别手动拖。3.2 从预训练权重启动训练命令与关键参数最小训练命令yolo detect train \ dataglasses.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience30 \ mosaic1.0 \ mixup0.1 \ degrees10 \ translate0.1 \ scale0.5 \ fliplr0.5 \ cacheTrue \ projectruns/glasses \ namev8s_baseline参数逐个说清楚。modelyolov8s.pt是预训练权重小数据集必须从预训练启动从零训基本没戏。epochs150配合patience30意思是 30 轮验证指标不升就早停避免过拟合。imgsz640是默认值如果 2.3 统计出目标偏小可以提到 800 或 960但显存要够。batch16在 8G 显存上跑 640 分辨率基本安全显存不够就降到 8 并开ampTrue。lr00.01是初始学习率小数据集我一般降到 0.005~0.01太大容易震荡。mosaic1.0是 YOLOv8 默认开启的增强对眼镜这种细长目标有帮助但如果你的图像里眼镜占比很大、Mosaic 拼接后目标被切碎可以降到 0.5。mixup0.1轻微混叠防止过拟合。degrees10小角度旋转模拟头部倾斜。cacheTrue把图像缓存到内存2948 张 640 分辨率大概占 3~4G 内存能开就开训练速度提升明显。3.3 训练过程看什么loss 曲线与 mAP 的读法训练启动后runs/glasses/v8s_baseline/下会有results.csv和weights/。重点看三个信号第一train/box_loss和val/box_loss的差距。如果 train 持续下降而 val 在 30 轮后走平甚至上升就是过拟合该早停或加增强。第二metrics/mAP50-95的爬升节奏。小数据集上前 50 轮通常能到 0.5 左右150 轮能到 0.6~0.7 就算不错。如果 50 轮还不到 0.3先回去查数据别急着调模型。第三metrics/precision和metrics/recall的平衡。眼镜检测里 recall 通常比 precision 重要——漏检一副眼镜比误检一个框代价大如果 recall 明显低于 precision可以调低置信度阈值或增加正样本增强。import pandas as pd df pd.read_csv(runs/glasses/v8s_baseline/results.csv) df.columns [c.strip() for c in df.columns] print(df[[epoch, train/box_loss, val/box_loss, metrics/mAP50(B), metrics/mAP50-95(B)]].tail(10))这段代码读训练日志看最后 10 轮的指标。如果mAP50-95在最后 10 轮波动小于 0.005说明已经收敛可以停。3.4 推理验证单图、批量与视频三种方式训练完先用验证集跑一遍官方评估yolo detect val \ modelruns/glasses/v8s_baseline/weights/best.pt \ dataglasses.yaml \ imgsz640 \ batch16然后单图推理看效果yolo detect predict \ modelruns/glasses/v8s_baseline/weights/best.pt \ sourceglasses_dataset/images/test \ conf0.25 \ iou0.5 \ saveTrue \ projectruns/glasses \ namepred_testconf0.25是置信度阈值眼镜检测里如果漏检多就降到 0.15误检多就提到 0.4。iou0.5是 NMS 的 IoU 阈值眼镜框重叠少0.5 够用如果一张图里有多副眼镜且靠得近可以提到 0.6 减少误抑制。批量推理结果在runs/glasses/pred_test/下肉眼过一遍重点看侧脸和反光场景。4. 小数据集调参眼镜检测里真正影响 mAP 的五个参数基线跑通之后别急着换模型。2948 张的量级调参的收益远大于换 backbone。这一章讲五个我实测下来对眼镜检测影响最大的参数以及怎么判断该往哪个方向调。4.1 图像分辨率 imgsz小目标眼镜的取舍眼镜在整图里占比通常不大尤其是监控视角下。imgsz640时一副眼镜可能只占 40~80 像素YOLO 的 P3 特征图80x80勉强能覆盖。如果 2.3 统计出 95% 分位宽高小于 0.08建议把imgsz提到 800 或 960。代价是显存和训练时间增加800 比 640 大约多 50% 显存。实测在眼镜数据集上640 到 800 通常能带来 3~5 个点的 mAP50-95 提升800 到 960 收益递减到 1~2 个点。所以我的建议是先 640 跑基线如果 recall 上不去且显存有余升到 800。4.2 学习率与 warmup小数据集别用大 lrlr00.01是 YOLOv8 默认但小数据集上我一般降到 0.005并开warmup_epochs5。原因是 2948 张的梯度噪声大大学习率容易在前期把预训练权重带偏。lrf0.01是最终学习率系数配合余弦退火让训练后期稳定收敛。如果你发现 loss 曲线前期剧烈震荡先把lr0减半别急着改模型。4.3 数据增强组合Mosaic、MixUp、Copy-Paste 的取舍眼镜检测的增强要围绕「姿态变化」和「光照变化」做文章。degrees10模拟头部倾斜translate0.1和scale0.5模拟距离变化fliplr0.5水平翻转眼镜左右对称安全。mosaic1.0在小数据集上通常有益但如果你的图像背景单一比如全是白底产品图Mosaic 拼接会引入不自然的边界这时降到 0.5。mixup0.1轻微使用太高会让细长镜框糊掉。Copy-Paste 增强 YOLOv8 默认没开需要自己写对眼镜这种小目标收益明显但实现成本高建议基线稳定后再加。4.4 置信度与 NMS IoU推理侧的两个旋钮训练侧的 mAP 是一回事上线后的实际表现是另一回事。conf和iou这两个推理参数我一般用验证集画 P-R 曲线来定。做法是跑yolo detect val时加plotsTrue生成的PR_curve.png里找 recall 和 precision 的平衡点。眼镜检测里如果业务允许一定误检conf可以压到 0.15~0.2 换 recall如果误检代价高比如自动门禁提到 0.4~0.5。iou在眼镜场景下 0.5~0.6 都行取决于同图多目标的重叠程度。4.5 冻结层与微调策略什么时候该冻 backbone如果数据集和预训练数据COCO差异大——比如你的眼镜全是红外或灰度图——前 10 轮可以冻 backbonefreeze10只训 head让 head 先适应新类别再解冻全量微调。如果图像是常规 RGB直接全量微调即可。实测在眼镜数据集上冻结策略带来的差异在 1~2 个点以内不是关键变量但能加快前期收敛。5. 避坑与排查眼镜检测训练里最常见的五类翻车这一章是我自己踩过和帮别人排查过的坑按「现象 → 原因 → 解决」写。每一条都对应真实训练日志里能看到的信号。5.1 现象mAP 卡在 0.2 上不去loss 也不降原因标签格式错了但没报错。YOLO 对越界坐标会静默截断如果标注是像素坐标没归一化所有框都被压到图像左上角模型学不到东西。解决回到 2.1 的脚本检查out_of_range条目。如果确认是像素坐标写脚本批量除以图像宽高修复重新训练。5.2 现象训练集 mAP 0.9验证集只有 0.4原因过拟合且验证集划分有偏。2948 张里如果验证集恰好抽到的都是简单样本训练集里的难样本没被验证覆盖。解决按 2.2 的分层抽样重新划分并加增强mosaic1.0、mixup0.15、scale0.6同时把patience降到 20 早停。5.3 现象侧脸和反光镜片的框明显偏移原因训练集里这类样本太少模型没见过。解决统计侧脸可以用关键点或简单的人脸角度分类器筛和强反光样本对这两类做过采样或者在增强里加hsv_v0.5模拟反光变化、perspective0.0005模拟角度。如果样本实在少考虑用 Copy-Paste 把眼镜抠出来贴到不同背景上。5.4 现象训练到一半 loss 突然变 NaN原因学习率太大或某批数据里有异常框宽高为 0 或极大。解决先把lr0降到 0.001 重跑如果还 NaN用 2.1 的脚本查zero_size条目。另外ampTrue在部分显卡上会引入 NaN可以临时关掉 AMP 定位问题。5.5 现象推理时同一副眼镜出多个框原因NMS 的iou阈值太高或者模型对同一目标输出了多个高置信度框。解决把推理iou从 0.7 降到 0.5如果还多检查训练时box损失是否正常下降。少数情况下是 anchor 或特征图问题但 YOLOv8 anchor-free 下少见优先调 NMS。6. 从 2948 张到可上线模型三个我常用的进阶技巧基线稳定之后如果还想再压榨几个点或者要把模型推到实际业务里下面三个技巧是我反复用过的。第一个是「难样本回捞」。用训练好的模型在验证集和测试集上跑推理把conf在 0.1~0.3 之间的框导出来人工复核。这些是模型「犹豫」的样本把它们加回训练集修正标签后再跑一轮微调通常能带来 2~4 个点的 recall 提升。代码上就是读predict输出的labels/目录按置信度筛。from pathlib import Path pred_dir Path(runs/glasses/pred_test/labels) hard [] for txt in pred_dir.glob(*.txt): for line in txt.read_text().strip().splitlines(): parts line.split() if len(parts) 6: conf float(parts[5]) if 0.1 conf 0.3: hard.append((txt.stem, conf)) print(f难样本数: {len(hard)})第二个是「模型集成」。YOLOv8s 和 YOLOv8m 各训一个推理时用 WBF加权框融合合并结果。眼镜检测里两个模型的错误往往不重叠集成能稳定提升 1~3 个点。WBF 可以用ensemble-boxes库或者自己写简单的 IoU 合并。第三个是「导出与部署」。训练完用yolo export formatonnx opset12 simplifyTrue导出 ONNX再用 ONNX Runtime 或 TensorRT 部署。眼镜检测如果跑在边缘设备上TensorRT FP16 通常能比 PyTorch 快 2~3 倍精度损失在 0.5 个点以内。导出后一定要用同一批测试图对比 PyTorch 和 ONNX 的输出确认没有算子不兼容导致的精度漂移。技巧预期收益成本适用阶段难样本回捞recall 2~4人工复核 1~2 天基线稳定后模型集成mAP 1~3训练时间翻倍追求极致指标ONNX/TensorRT 导出速度 2~3 倍半天调试上线前最后说个我自己的习惯每次训完一个模型我都会把best.pt、glasses.yaml、results.csv和当时的 git commit 一起归档到一个带日期的目录里。小数据集的调参过程很玄学同一个参数换个随机种子结果可能差 2 个点没有归档两周后你根本复现不出当时的最好结果。这个习惯帮我省过好几次「后悔药」。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进