ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

YOLOv8药盒药品识别系统:从数据集构建到Docker部署全流程

YOLOv8药盒药品识别系统:从数据集构建到Docker部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生及初学者的YOLOv8药品识别实战资源,可用于毕业设计、课程设计、大作业或教学演示。系统基于YOLOv8目标检测框架,支持图片与视频输入,自动定位并识别药盒内常见药品,内置yolov8n.pt与训练好的best.pt权重,配套训练脚本与实时视频流检测脚本,可视化界面可查看检测结果、置信度框选、标签分布图,以及F1分数曲线、精确率-召回率曲线、混淆矩阵等评估图表。压缩包共11个文件,约15.92MB,以3个pt权重文件、3个py脚本、3个txt说明文档为主,另含gitignore与inscode配置,数据集已标注整理,覆盖多角度、不同光照下的药盒图像。部署说明覆盖环境配置、依赖安装、权重加载与界面启动全流程,Windows与Linux均可运行,README提供清晰操作指引。目前已有171人学习,适合快速上手目标检测项目并完成完整实践闭环。

1. 药盒识别为什么总在真实货架上翻车

做过零售视觉的人多半有过这种体验:在自建数据集上 mAP 跑到 0.95,信心满满搬到门店货架前,模型立刻变成“睁眼瞎”。YOLOv8 药盒药品识别系统这个方向,真正难的不是把 YOLOv8 跑起来,而是让它在真实药盒场景里稳定输出。药盒本身是极难的检测目标:长宽比极端(有的细长如牙膏盒,有的接近正方形)、表面大面积白底反光、文字密集且字号极小、同类药品不同规格包装高度相似、货架摆放密集遮挡严重。这些特性叠加在一起,直接导致通用检测模型在药盒场景下召回率断崖式下跌。

这套系统要解决的核心问题很明确:给定一张药盒货架或单盒照片,输出药品类别、位置框和置信度,并提供一个可视化界面供非技术人员使用。它适合三类人:一是做智慧药房、零售盘点、药品分拣的工程团队;二是想拿一个完整闭环项目练手 YOLOv8 全流程的算法同学;三是需要快速验证药盒识别可行性的产品负责人。整条链路包含数据集构建、模型训练、界面封装和一键部署四个环节,缺任何一环都只是 demo,不是系统。

2. 数据集构建与标注:药盒识别的地基怎么打

2.1 药盒数据集的采集策略与类别划分

药盒识别的数据集不能随便拍。我一般按“场景分层 + 类别分层”两条线采集。场景分层指:单盒正面平铺、单盒多角度、货架密集排列、手持遮挡、不同光照(冷白灯、暖黄灯、自然光)。类别分层指:按药品通用名划分,而不是按包装颜色或厂商划分,因为同一种药可能换包装,但通用名不变。类别数量建议控制在 30 到 80 之间,太少没有泛化价值,太多标注成本爆炸且长尾类别样本不足。

采集时有个血泪经验:每个类别至少保证 200 张有效样本,其中货架密集场景占比不低于 40%。很多团队前期只拍单盒平铺图,训练出来的模型在货架上直接崩掉,因为密集遮挡下的特征分布和单盒完全不同。另外,药盒表面的反光区域要刻意覆盖,否则模型会把反光当成类别特征,换一个光源就翻车。

2.2 用 LabelImg 标注药盒并转成 YOLO 格式

标注工具用 LabelImg 或 AnyLabeling 都行,格式选 YOLO txt。标注时框要贴紧药盒外边缘,不要框到货架背景,也不要漏掉被遮挡但可辨认的盒子。下面是把 LabelImg 的 XML 转成 YOLO txt 的脚本:

import os import xml.etree.ElementTree as ET # 类别映射,必须与训练时的 data.yaml 顺序一致 CLASSES = ["box_a", "box_b", "box_c"] # 替换为你的药品类别名 def convert_annotation(xml_path, img_w, img_h, out_txt): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in CLASSES: continue cls_id = CLASSES.index(cls_name) bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # YOLO 格式:中心点 x,y 和宽高,全部归一化到 0-1 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt, "w") as f: f.write("\n".join(lines)) # 批量转换示例 for xml_file in os.listdir("annotations"): if xml_file.endswith(".xml"): # 这里需要读取对应图片尺寸,实际使用时用 PIL 或 cv2 获取 img_w, img_h = 1920, 1080 # 替换为真实尺寸 convert_annotation( os.path.join("annotations", xml_file), img_w, img_h, os.path.join("labels", xml_file.replace(".xml", ".txt")) )

这段脚本的关键点有三个:类别顺序必须和后续 data.yaml 里的 names 完全一致,否则训练出来的类别全是错的;归一化坐标要保留六位小数,避免小目标精度损失;被遮挡但可辨认的药盒要标,完全不可辨认的不要标,否则模型学到模糊特征。转换完成后,按 8:1:1 划分训练集、验证集、测试集,测试集里必须包含至少一个完整货架场景,用来验证真实部署效果。

2.3 data.yaml 配置与数据增强参数

data.yaml 是 YOLOv8 训练的入口配置,路径和类别名写错是最常见的低级翻车点:

path: /data/medicine_box # 数据集根目录 train: images/train val: images/val test: images/test names: 0: box_a 1: box_b 2: box_c

数据增强方面,药盒场景我一般开 mosaic=1.0、mixup=0.1、degrees=10、translate=0.1、scale=0.3。mosaic 对密集小目标提升明显,但 mixup 不要开太大,药盒文字密集,mixup 过度会让文字特征糊掉。另外 hsv_h、hsv_s、hsv_v 分别设 0.015、0.7、0.4,用来对抗不同药房的灯光色温差异。如果货架遮挡特别严重,可以额外开 copy_paste=0.1,把药盒实例粘贴到不同背景上,提升遮挡鲁棒性。

3. YOLOv8 训练调参与模型导出

3.1 从预训练权重开始训练的最小命令

不要从零训练,药盒数据集规模通常不足以支撑随机初始化。用 COCO 预训练权重做迁移学习,收敛快且小目标召回更好:

yolo detect train \ data=/data/medicine_box/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=30 \ device=0 \ project=runs/medicine \ name=exp01

参数说明:model 选 yolov8s 而不是 n 或 m,是因为药盒文字和小目标需要一定容量,n 容易欠拟合,m 在单卡上训练太慢;imgsz=640 是精度和速度的平衡点,如果药盒在图中占比很小,可以提到 960 或 1280,但显存要够;lr0=0.01 是 SGD 的初始学习率,用 AdamW 的话降到 0.001;patience=30 表示 30 轮无提升就早停,避免过拟合。训练过程中重点看 metrics/mAP50-95 和 val/box_loss,如果 box_loss 震荡不降,多半是标注框质量有问题。

3.2 药盒小目标检测的三个必调参数

药盒在货架图中往往只占几十个像素,属于典型小目标。除了提高 imgsz,还有三个参数直接影响小目标召回:

参数默认值药盒场景建议作用
imgsz640960 或 1280提升小目标分辨率
box7.59.0 到 10.0加大框回归损失权重
cls0.50.8 到 1.0加大分类损失权重

box 和 cls 在训练命令里通过box=9.0 cls=0.9传入。加大 box 权重能让模型更关注定位精度,加大 cls 权重能缓解同类药盒包装相似导致的误分类。但不要同时拉太高,否则训练不稳定,建议先调 box,再微调 cls。

3.3 导出 ONNX 与 TensorRT 的取舍

训练完的 best.pt 要部署,通常导出 ONNX 或 TensorRT:

# 导出 ONNX,通用性最好 yolo export model=runs/medicine/exp01/weights/best.pt format=onnx opset=12 simplify=True # 导出 TensorRT,NVIDIA 平台推理最快 yolo export model=runs/medicine/exp01/weights/best.pt format=engine half=True device=0

ONNX 适合跨平台,CPU 也能跑,但速度一般;TensorRT 在 NVIDIA 显卡上能快 2 到 3 倍,但绑定硬件和驱动版本。如果部署环境不确定,先导 ONNX 保底,确定是 NVIDIA 边缘设备再导 engine。注意导出 engine 时 half=True 用 FP16,精度损失通常可接受,但如果药盒文字识别对置信度敏感,先用 FP32 验证再切 FP16。

4. 可视化界面与一键部署

4.1 用 Gradio 搭一个药盒识别界面

可视化界面不需要多复杂,能上传图片、显示检测框和类别就够了。Gradio 是最快的方式:

import gradio as gr from ultralytics import YOLO model = YOLO("runs/medicine/exp01/weights/best.pt") def detect(image): results = model.predict(image, imgsz=960, conf=0.35, iou=0.5) # results[0].plot() 返回带框的 numpy 图像 return results[0].plot() demo = gr.Interface( fn=detect, inputs=gr.Image(type="numpy"), outputs=gr.Image(type="numpy"), title="药盒药品识别", description="上传药盒图片,自动识别类别与位置" ) demo.launch(server_name="0.0.0.0", server_port=7860)

conf=0.35 是药盒场景的经验阈值,太低会误检货架纹理,太高会漏检遮挡盒。iou=0.5 控制 NMS 合并,密集货架可以降到 0.4 减少漏检。界面里不要只显示框,最好把类别名和置信度一起画上去,方便人工复核。

4.2 Docker 一键部署与依赖锁定

一键部署的核心是把 Python 环境、模型权重、界面代码全部封进镜像:

FROM python:3.10-slim WORKDIR /app RUN pip install --no-cache-dir ultralytics gradio opencv-python-headless COPY runs/medicine/exp01/weights/best.pt /app/model.pt COPY app.py /app/app.py EXPOSE 7860 CMD ["python", "app.py"]

构建命令docker build -t medicine-detect:v1 .,运行命令docker run --gpus all -p 7860:7860 medicine-detect:v1。注意 opencv-python-headless 而不是完整版,避免容器里缺 GUI 库报错。模型权重直接 COPY 进镜像,不要挂载,否则换机器容易路径错。如果要用 GPU,基础镜像换成 nvidia/cuda 对应版本,并确保宿主机驱动匹配。

5. 避坑与常见问题排查

5.1 训练 loss 正常但验证 mAP 极低

现象:训练集 box_loss 持续下降,但验证集 mAP50 一直在 0.1 以下。原因通常是 data.yaml 里 train 和 val 路径写反,或者验证集图片和标签不对应。解决:手动打开 val 目录,确认每张图都有同名 txt,且 txt 里类别 id 在 names 范围内。另一个常见原因是标注时类别名大小写不一致,转换脚本里被过滤掉了。

5.2 模型在货架图上漏检严重

现象:单盒图检测正常,货架密集图大量漏检。原因是训练集里货架场景占比太低,模型没见过密集分布。解决:补充货架场景样本到总数据的 40% 以上,同时把 imgsz 提到 960,conf 降到 0.3,iou 降到 0.4。如果还不行,开 copy_paste 增强,把单盒实例粘贴到货架背景上合成训练数据。

5.3 导出 ONNX 后推理结果和 PyTorch 不一致

现象:pt 模型检测正常,ONNX 推理框偏移或类别错乱。原因多半是导出时 opset 版本和推理引擎不匹配,或者预处理归一化方式不同。解决:导出时固定 opset=12,推理时确保输入是 RGB、归一化到 0-1、尺寸和训练一致。用 onnxruntime 跑一遍和 pt 对比,逐张图核对输出。

5.4 界面部署后上传图片无响应

现象:Gradio 界面能打开,上传图片后一直转圈。原因是容器内模型加载失败或显存不足。解决:先看容器日志,确认模型路径存在;如果显存不够,把 imgsz 从 960 降到 640,或者换 yolov8n 权重。另外 server_name 必须设 0.0.0.0,否则容器外访问不到。

5.5 同类药盒误分类严重

现象:不同药品但包装相似的盒子被识别成同一类。原因是类别划分太粗,或者 cls 损失权重太低。解决:把相似包装的药品拆成独立类别,增加类间样本差异;训练时把 cls 从 0.5 提到 0.9;推理时如果两类置信度接近,输出 top-2 供人工确认,不要强行取 top-1。

6. 把药盒识别推到可用的最后一公里

模型训练完、界面跑通,不代表系统能用。最后一公里是推理后处理与业务规则结合。我一般会在检测结果上加三层过滤:第一层是尺寸过滤,药盒在图中宽高比通常在 0.3 到 3 之间,超出范围的框直接丢弃,能滤掉货架边缘的误检;第二层是位置过滤,如果检测框中心落在图片最上方 10% 区域,多半是货架顶层标签,不是药盒;第三层是类别一致性过滤,同一张图里如果同一类别出现超过 20 个框,大概率是误检,触发人工复核。

验证方法上,不要只看 mAP。准备一个 50 张图的真实测试集,人工统计三个指标:单盒召回率、货架场景召回率、误检率。单盒召回率低于 95% 说明模型容量不够或 imgsz 太低;货架召回率低于 80% 说明密集场景训练不足;误检率高于 5% 说明 conf 阈值太低或后处理太松。这三个指标比 mAP 更贴近业务。

一个具体技巧:把推理结果按置信度分三档处理。conf 大于 0.7 直接输出;0.4 到 0.7 之间标记为“待确认”,界面用黄色框;低于 0.4 不显示。这样既保证高置信度结果直接可用,又不会因为阈值太高漏掉遮挡盒。这个分档策略在药房盘点场景里比单一阈值实用得多。

我自己踩过最深的坑是早期只追求 mAP,忽略了货架场景的召回率,结果模型在演示时翻车。后来养成习惯:每次训练完先跑真实货架测试集,看召回率再看 mAP。模型指标是给别人看的,召回率才是给自己用的。希望帮到你。

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

RELATED READING

延伸阅读

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