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

资讯详情

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

小样本药品检测:110张板蓝根图像跑通YOLOv8检测流水线

小样本药品检测:110张板蓝根图像跑通YOLOv8检测流水线 简介这是一个面向目标检测实战的药品包装检测数据集聚焦板蓝根颗粒袋装与999感冒灵颗粒两类常见药品包装的识别定位适合入门级目标检测项目练习、模型快速验证以及药品货架/包装识别场景测试。数据包共335个文件以111张jpg原图、VOC格式xml标注文件和YOLO格式txt标注文件为主压缩包整体仅3.71MB使用labelImg人工标注文件体量轻、便于快速下载与迭代。标注信息完整且类别区分清晰总计134个目标框其中板蓝根颗粒袋85个、999感冒灵49个可用于训练和评估基于VOC/YOLO格式的目标检测模型。资源已有121人学习关注适合正在学习YOLO、Fast R-CNN等目标检测算法或需要少量实测数据进行流程测试的数据科学初学者、算法实习生与高校实验课程使用。1. 药品板蓝根颗粒检测数据集只有110张为什么还值得跑一遍检测流水线药品外包装质检里板蓝根颗粒的破损、异物和装量差异本质上是一个目标检测问题。手头这套药品板蓝根颗粒检测数据集总共110张图同时给了VOC和YOLO两种格式量小得可怜但做过检测流水线的人都清楚小样本不是死刑标注错位才是。110张足够你把数据体检、格式对齐、YOLOv8训练和错检回灌跑完一个闭环用一轮迭代去判断质检方案可不可行再决定要不要投入更大规模的采集。它适合正在做医药外观检测、以及准备用YOLO接手小批量质检场景的工程师。数据集的直接价值在格式齐、场景单一不在数量大。2. 拆开zip看门道VOC与YOLO两套目录结构与标注文件的对应关系拿到zip第一步不是解压就训练而是先确认两套格式的布局。很多平台的标注导出都是VOC和YOLO同时给如果不把目录结构理清后面写脚本会反复返工。这一章的结论一句话VOC用像素坐标的xmlYOLO用归一化坐标的txt两者描述同一个目标坐标空间完全不同对齐工作比训练本身更花时间。维度VOC格式YOLO格式标注文件xmltxt坐标含义左上角xmin、ymin和右下角xmax、ymax像素值中心点x_center、y_center和宽高width、height归一化类别写法object节点下的name字符串每行开头的整数编号对应names顺序一个图片对应一个同名xml一个同名txt2.1 标准VOC布局JPEGImages、Annotations与ImageSets/Main我见过的大多数VOC风格数据集会按Pascal VOC的约定排列解压后三个目录、一个文本组。JPEGImages放原图Annotations放同名xmlImageSets/Main放的是按空格分隔的图片名列表用来划分训练、验证和测试。xml里重点看四个节点filename、size下的width和height、object下的name和bndbox。bndbox给的是左上角xmin、ymin和右下角xmax、ymax单位是像素不是归一化数值。这个像素坐标是VOC和YOLO最大的分水岭后续所有转换都从它出发。用Python读这种xml不算复杂ElementTree就够用。下面这个脚本遍历Annotations目录把每个xml的类别和bndbox打印出来同时核对filename字段和实际文件名是否一致import xml.etree.ElementTree as ET from pathlib import Path annot_dir Path(Annotations) for xml_path in sorted(annot_dir.glob(*.xml)): tree ET.parse(xml_path) root tree.getroot() filename root.findtext(filename) size root.find(size) img_w, img_h int(size.findtext(width)), int(size.findtext(height)) boxes [] for obj in root.findall(object): name obj.findtext(name) bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) boxes.append((name, xmin, ymin, xmax, ymax)) print(f{xml_path.stem}: {img_w}x{img_h}, {len(boxes)} objects, filename{filename}) for box in boxes[:3]: print( , box)逻辑是先按文件名排序逐个解析size和object节点再打印前三个框确认坐标范围。参数说明img_w和img_h在转YOLO格式时当分母如果xml里的size和真实图片分辨率不一致转化出来的坐标全错这是最容易忽略的一环。建议打印结束后随机挑两张图把坐标用OpenCV画上去肉眼核对一遍比写一百行校验代码都管用。画框用rectangle坐标直接用xmin那一串不要先归一化再还原多一次变换多一个出错点。2.2 YOLO格式的labels目录txt归一化坐标与类别编号YOLO训练不读xml只读txt。常规布局是images目录下放jpglabels目录下放同名txt文件名一致但扩展名不同。txt每行一个目标五个字段class_id、x_center、y_center、width、height。前四个都要归一化中心点x除以图宽、中心点y除以图高、目标宽除以图宽、目标高除以图高。class_id从0开始编号对应data.yaml里的names列表顺序。为什么YOLO强制归一化因为训练时输入会resize到640乘640目标在640坐标系里的位置随原图分辨率变化归一化之后才能在不同尺寸输入下保持同一个语义。从VOC转过来时最容易出错的不是公式而是把左上角坐标当成中心点来算。VOC转YOLO的标准公式x_center (xmin xmax) / 2 / img_wy_center (ymin ymax) / 2 / img_hwidth (xmax - xmin) / img_wheight (ymax - ymin) / img_h注意宽高是目标真实宽高除以整图宽高不是除以目标框宽高也不是除以640。我把这个公式直接写成转换函数输入xml路径和图片尺寸输出txt内容import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_w, img_h): root ET.parse(xml_path).getroot() lines [] class_map {granule: 0} # 按实际类名调整多类别时继续追加编号 for obj in root.findall(object): name obj.findtext(name) if name not in class_map: continue bndbox obj.find(bndbox) xmin int(float(bndbox.findtext(xmin))) ymin int(float(bndbox.findtext(ymin))) xmax int(float(bndbox.findtext(xmax))) ymax int(float(bndbox.findtext(ymax))) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h lines.append(f{class_map[name]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) return \n.join(lines)参数说明class_map这行是关键txt里的第一个数字不是类别名称而是类别在names列表里的索引。很多人把类别名直接写进txt训练时类别数变成字符数加一这种错误通常在loss正常但val的mAP为0时才暴露。另一个容易翻车的地方xmin加xmax除以2得到的是中心点如果误写成xmax减xmin除以2就得到宽度的一半框全偏左上角。这里的img_w、img_h必须用原始图片的宽高而不是xml里size字段之外的任何值。2.3 双格式交叉校验用Python脚本对齐图片、标注与IDVOC和YOLO两套标注可能由不同工具或平台导出经常出现xml有框但txt漏框、或者图片改名后两边不同步。直接训练YOLO会跳过没有txt的图val集合悄悄变小精度数字看着还行落地一推理全是漏检。我一般会先做一次交叉校验把三个目录的stem集合拿来做差集from pathlib import Path jpg_dir Path(JPEGImages) xml_dir Path(Annotations) txt_dir Path(labels) jpgs {p.stem for p in jpg_dir.glob(*.jpg)} xmls {p.stem for p in xml_dir.glob(*.xml)} txts {p.stem for p in txt_dir.glob(*.txt)} print(缺xml的图:, jpgs - xmls) print(缺txt的图:, jpgs - txts) print(有txt没图的:, txts - jpgs) for stem in sorted(jpgs txts): txt txt_dir / f{stem}.txt for i, line in enumerate(txt.read_text(encodingutf-8).splitlines()): parts line.split() if len(parts) ! 5: print(f{stem} 第{i}行格式错误: {line}) continue _, xc, yc, w, h map(float, parts) if xc - w/2 -0.02 or xc w/2 1.02 or yc - h/2 -0.02 or yc h/2 1.02: print(f{stem} 第{i}行越界: {line})逻辑是三步收集三个目录的文件主名做集合差集再逐行检查txt格式和越界。参数说明编码用utf-8而不是默认的gbkWindows下txt经常以gbk写入读出来报UnicodeDecodeError行数不是5说明这行被回车截断或混入空行。越界容差0.02低于这个不报因为边缘物体的标注本来就容易探出边界。集合差集输出空是正常情况一旦有输出就要回到标注工具里补。还有一个更直接的检查抽三张图用txt坐标画框和原图叠在一起看一眼颗粒是圆形框应该贴合边缘如果框明显偏移多半是除以了错误的图宽高。3. 训练前强制体检110张小数据的3个必查项与可用检查脚本小样本数据集最怕的不是数量少而是坏数据占比高。110张图如果有一半标注框越界或漏标模型会把背景学进特征里去。训练前体检不是玄学是标准的工业流程。这一章给三个必查项框质量、类别分布、数据划分每个都有可复制的脚本。3.1 框质量接近零尺寸、越界与宽高比异常标注工具缩放到小图时很容易产生只有两三个像素的框。这种框在原始分辨率下肉眼看不出来在640输入下更是一个噪声点YOLO会为它输出一个近乎随机的小框。逐条检查越界和过小的问题用一段小脚本读目录下全部txtfrom pathlib import Path txt_dir Path(labels) imgsz 640 bad [] for txt in txt_dir.glob(*.txt): for i, line in enumerate(txt.read_text(encodingutf-8).splitlines()): parts line.split() if len(parts) ! 5: bad.append(f{txt.name}:{i} 列数异常) continue xc, yc, w, h map(float, parts[1:]) if w * imgsz 5 or h * imgsz 5: bad.append(f{txt.name} 目标过小 w{w:.4f} h{h:.4f}) if xc - w/2 -0.02 or xc w/2 1.02: bad.append(f{txt.name} 中心x越界 {xc:.4f}) if yc - h/2 -0.02 or yc h/2 1.02: bad.append(f{txt.name} 中心y越界 {yc:.4f}) for item in bad: print(item)逻辑是把txt按行拆开每行解析出5个字段再换算成640尺度下的像素宽度。参数说明w乘以imgsz是换算公式imgsz取训练时的输入尺寸缺省640小于5像素的框直接列为过小因为颗粒在640图上至少有十来个像素5像素以下基本是误标。越界判定里的0.02是容差某些数据集的边缘物体标注本来就会出边界容差内先放行等人工复核。这里不直接删框只打印名单删框是不可逆操作先把问题暴露出来再逐个决定。3.2 类别与每图目标数分布板蓝根颗粒检测场景相对集中但也要确认类别到底是一个还是多个。统计每张图的目标数和类别编号分布能提前判断两个问题一是是否有类别只在训练集里出现一两次二是是否有大量图片一个框都没有。第二种最隐蔽YOLO会把空图当成纯背景来压低误检但空图占比过大模型会倾向保守漏检。统计脚本只需要一个Counterfrom collections import Counter from pathlib import Path txt_dir Path(labels) cls_counter Counter() per_img_counts [] for txt in txt_dir.glob(*.txt): lines [l for l in txt.read_text(encodingutf-8).splitlines() if l.strip()] cls_counter.update(int(line.split()[0]) for line in lines) per_img_counts.append(len(lines)) print(类别统计:, cls_counter) print(每图目标数分布:, Counter(per_img_counts)) print(空图张数:, sum(1 for c in per_img_counts if c 0))逻辑是把每个txt的行数视作该图的目标数量再统计类别编号和空图张数。参数说明如果空图超过图片总数的15%建议先补标一批有目标的图而不是直接训练如果某个类别编号只出现两三次单独开一个检测头去学它结果基本靠运气。我一般不用现成的可视化工具直接看终端输出就够了数字本身已经能揭示不平衡程度。还要注意类别编号必须从0开始连续排列如果出现了class_id5但中间缺了编号data.yaml里的names顺序就有一半是错位。3.3 划分策略按组切分而不是随机打乱110张图划分训练和验证至少不能简单随机打乱后按比例切。同一批次拍摄的板蓝根颗粒图光照和背景几乎一样随机打乱会让验证集分布和训练集高度重叠模型mAP虚高一到现场就崩。我常用的做法是先按拍摄批次或文件名前缀分组以组为单位切分让val里的场景是train没见过的。给出按文件名前缀分组划分的脚本from pathlib import Path import random, re jpgs sorted(Path(JPEGImages).glob(*.jpg)) def group_key(stem): parts re.split(r[_-], stem) return parts[0] if len(parts) 1 else stem[:2] groups {} for p in jpgs: groups.setdefault(group_key(p.stem), []).append(p.stem) random.seed(42) names list(groups) random.shuffle(names) train, val [], [] for g in names: if len(train) / len(jpgs) 0.8: train.extend(groups[g]) else: val.extend(groups[g]) print(train:, len(train), val:, len(val))逻辑是先把文件名按下划线或中划线拆出批次前缀相同前缀归到同一组再以组为单位切分到train和val。参数说明0.8是train的目标占比110张对应约88张train和22张valseed固定成42保证每次运行划分一致。如果文件名没有批次信息脚本会自动退化成按前两个字符分组此时相当于半随机效果比纯随机好一点但仍建议人工先看一眼哪些图是同一场景。划分完把train和val的图片清单各自存成txt后续训练和换模型都复用同一份划分否则不同实验之间没有可比性。另外如果val里的目标总数不足20个验证mAP方差大得没法看可以把比例调成9:1照样是小样本常规操作。4. 用YOLOv8把110张图训练成板蓝根颗粒检测器最小命令与关键参数体检完就该训练了。选YOLOv8还是更晚的版本看团队环境我的经验是别盲目追新先让一个稳定版本跑通流程。110张图属于极小样本网络结构、增强策略、优化器三个地方都要调整不是把图片丢进去等结果就行。提示训练前先跑一遍第3章的体检脚本能省掉后面大半的排查时间。4.1 安装与数据组织YOLO官方目录约定安装ultralytics包一行pip。数据目录按YOLO约定的布局来摆train和val下各放images和labels图片在images下txt在labels下两个目录的文件主名必须完全一致dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml里写路径、类别数和类别名path: /home/user/dataset train: images/train val: images/val nc: 1 names: 0: granule参数说明path用绝对路径训练脚本和数据集不在同一目录时相对路径容易解析到当前工作目录折腾一次就不想再有第二次。nc必须和txt里的最大class_id加一相等names顺序要和txt里的编号一一对应。类名写成granule只是示意实际以标注里的类名为准YOLO不要求英文名符合语义。安装命令pip install ultralytics装完可以用python -c import ultralytics; print(ultralytics.version)验证。数据集目录不要放在中文路径下某些版本的第三方库对中文路径处理有历史遗留问题容易在读取图片时崩掉。4.2 训练命令与5个必调参数训练命令本身很短难在参数怎么设。110张图的数据量参数设不好训练一小时白费。去掉大模型和复杂trick实际起作用的就这几个yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ optimizerAdamW \ lr00.001 \ patience30 \ mosaic0.5 \ seed42逐个解释。modelyolov8n.pt用的是预训练权重不要用yolov8l或者更大的模型110张图加大模型等于把过拟合写死在参数里。epochs150配合patience30意思是150轮封顶但连续30轮val mAP不涨就提前停小样本普遍在80到120轮之间收敛。batch16是显存和稳定性的折中batch太小梯度噪声大AdamW都救不回来batch太大又受显存限制。imgsz640是默认输入尺寸如果板蓝根颗粒在图里小于32像素建议改成768代价是训练和推理都变慢。optimizerAdamW配上lr00.001是我在小样本检测上最稳的组合SGD配0.01对这个小数据量容易在前期震荡。mosaic0.5把默认的马赛克增强概率减半原因下一小节细说。seed42让整个训练过程可复现换机器重跑结果一致排查问题方便得多。有人习惯在命令后面加plotsTrue把P-R曲线和混淆矩阵存到runs/detect/train目录下这不影响训练结果但能让收敛趋势看得更清楚。训练结束后自动生成weights/best.pt和weights/last.ptbest是按val mAP评出来的实际部署用best不要手滑用了last。4.3 数据增强救小样本YOLOv8默认增强与人工增强的取舍YOLOv8默认把马赛克增强开到接近1.0即几乎每个训练batch都合成四宫格同时对每张图做HSV扰动和随机翻转。对1000张以上的数据集这套组合非常强。但110张的小样本马赛克会把颗粒裁剪得只剩半颗模型学出来的特征偏向边缘而非整体形状在验证集上看到的现象是mAP漂亮换成整袋颗粒就漏检。常见做法是把mosaic降到0.5mixup降到0保留fliplr0.5和HSV小幅扰动再补一点scale和translate让模型对尺度变化不敏感。把这些直接写进训练命令别放到data.yaml里yolo detect train \ datadata.yaml modelyolov8n.pt \ epochs150 imgsz640 batch16 mosaic0.5 mixup0 \ hsv_h0.015 hsv_s0.5 hsv_v0.4 \ scale0.3 translate0.1 fliplr0.5参数说明scale0.3允许目标在0.7到1.3倍尺度之间随机缩放颗粒是小物体稍微放大一点对漏检的改善比平移明显。translate0.1控制目标在图中最多平移10%避免颗粒总是贴在图片中心。hsv三个值分别控制色调、饱和度、亮度的扰动幅度板蓝根颗粒是棕色系色相扰动太大容易让颗粒变成灰色或绿色我把hsv_h压到0.015保留一点变化但不失真。mixup0直接关掉样本混合对几十张的小样本mixup会稀释颗粒的纹理特征收益不如损失多。这组参数是血泪经验换来的先用全默认跑一遍看loss曲线哪个增强明显让val loss反弹就关掉哪个。5. 小样本训练避坑5条从标注、格式到收敛的真实踩坑记录小样本训练翻车的地方不在模型结构而在数据链路。我把带人做检测项目时最高频的几类问题整理成5条每条按现象、原因、解决展开你能直接对上自己的报错或诡异曲线。5.1 现象loss正常下降val的mAP一直是0原因几乎总是类别编号和names列表对不上。txt里写了class_id3data.yaml里nc2模型把类别索引当超过边界处理推理时输出全被过滤。另一种情况是data.yaml的names顺序和标注工具导出顺序不一致比如工具里颗粒类排第0你写配置文件时把别的类放第0loss照样能收敛但val的mAP就是0。解决训练前用3.2节的Counter脚本打印txt里的类别编号再打开data.yaml核对names顺序。还有个自测办法训练完用best.pt对任意一张val图推理打印结果框的类别号和置信度如果类别号全为0而图里明明有别的类就是编号问题。5.2 现象loss先降后涨val mAP震荡得像心跳原因在小数据集上通常是三件事叠加batch太小、mosaic全开、学习率偏高。batch8时每个batch只有8张图的统计量梯度方向不稳定mosaic合成图让目标上下文频繁切换lr00.01的SGD在小样本上相当于在loss曲面里反复横跳。解决直接改成batch16起步mosaic降到0.5optimizer换成AdamW并把lr0压到0.001。改完如果还在震荡再把epochs从150提到300早停阈值放宽到40轮让模型在最低点附近多游一会儿。这套组合在多个小样本检测任务里都有效我一般先按它跑一遍再微调。如果显存实在不够batch8也可以用但要记得把lr0降到0.0005让梯度更新步子小一点震荡幅度会低一些。5.3 现象VOC转YOLO后画框全偏到左上角原因公式写错。最常见的是把x_center算成(xmax - xmin) / 2 / img_w算出来的是目标宽度的一半不是中心点还有人把width算成(xmax - xmin) / 2认为宽高也要除以2结果框只有实际一半大小。解决把公式抄对之后用三个文件做人工验证挑一张颗粒靠右下角的图转完txt后和xml画在同一画布上对比或者把txt坐标还原成像素坐标画框代码就几行import cv2 img cv2.imread(JPEGImages/xxx.jpg) with open(labels/xxx.txt, encodingutf-8) as f: for line in f: _, xc, yc, w, h line.split() xc, yc, w, h float(xc), float(yc), float(w), float(h) x1 int((xc - w / 2) * img.shape[1]) y1 int((yc - h / 2) * img.shape[0]) x2 int((xc w / 2) * img.shape[1]) y2 int((yc h / 2) * img.shape[0]) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(check.jpg, img)逻辑是把归一化的中心点和宽高还原成左上、右下像素坐标再画绿框。参数说明img.shape[1]是宽img.shape[0]是高两者不能写反还原后框应该和xml映射的框重合任何一厘米的偏移都说明转换脚本里有bug。注意cv2.rectangle要求坐标是整数先转成int再画否则OpenCV会报类型错误。5.4 现象模型把包装袋纹理或桌面背景当颗粒框出来原因110张图里多数是近景颗粒图背景几乎不变模型偷懒学了一个背景分类器而不是形状分类器。val里恰好有桌面背景图模型就输出一堆假阳性框。解决不是继续调参数而是先做两件事。一是给训练数据加8到10张没有颗粒的负样本图例如空包装袋、桌面、白底txt写成空文件让模型知道这类背景不该出框。二是在增强里把scale开到0.3让颗粒在图上被放大缩小弱化和背景的关联。负样本图不要乱加加太多模型会变得保守训练完先看val的假阳率再做增删一般负样本占总量5%到8%就够。5.5 现象zip解压后文件名乱码或txt内容乱码原因zip打包工具在Windows下默认用GBK压缩文件名Python的zipfile默认按UTF-8解压文件名读取失败txt标注文件如果被Excel编辑过再另存成txt内部编码可能变成GBK或带BOM。解决解压时指定编码txt一律用Python的encodingutf-8或gbk去读读出来后再统一存成utf-8。给一条最省事的检查命令file labels/*.txt | head输出里看到UTF-8 Unicode text就是正常的看到ISO-8859或with CRLF line terminators就要处理。CRLF本身YOLO能容忍但如果每行末尾多出\r某些版本的读取代码会把\r算进class_id里训练时直接报标签格式错误。解决用sed -i s/\r$// labels/*.txt批量清理或者直接在Python读取时用line.strip()。这个小问题一旦触发排查路径能让你浪费一上午先查编码再查坐标。6. 迁移学习与错检回灌把110张数据沉淀成可复用的评估基线6.1 先冻结再微调的实验顺序小样本用预训练权重几乎是不用讨论的默认操作但冻结backbone不一定好。颗粒外形简单YOLOv8n的backbone在ImageNet和COCO上学到的纹理特征已经够用冻结反而限制了对颗粒边缘的适应。我一般先全量fine-tune跑一次如果val mAP低于预期再试freeze10只训练后面几层。要比较两个方案必须用同一份划分、同一份命令只改freeze参数否则对比出来的差异是数据扰动造成的。跑完看runs/detect/train目录下的results.png如果冻结版的P曲线上不去了就不要省那点显存。6.2 错检回灌把验证集变成数据采集清单训练结束不是终点质检项目真正值钱的是错检回灌流程。跑一批推理把置信度在0.3到0.6之间、且有误检或漏检的图导出来集中看一遍。常见误判有三种把阴影当颗粒、把颗粒叠影当多个目标、把包装内气泡当异物。每确认一种收集对应场景的图补进数据集一两周把110张扩到300张模型mAP往往能涨三到五个点比调任何参数都管用。导出低置信度样本的脚本from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) res model.predict(sourceval/, saveFalse) for r in res: for box in r.boxes: if box.conf 0.6: print(r.path, box.cls, box.conf)逻辑是对验证集逐张推理把低置信度框和对应图片路径打出来。参数说明0.6这个阈值是经验值低于它的框往往处在边界情况人工复查成本低如果任务容错更严可以放到0.8。拿到输出文件名后我到现场把对应图像截出来整理成补标清单回到标注工具里把缺的框补齐。这条路我已经迭代过好几个质检项目最后留下的不是说那些调参技巧而是每次接手新数据第一件事跑一遍体检脚本这个习惯少它不行。希望帮到你。本文还有配套的精品资源点击获取
返回列表