ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于YOLOv11的药物识别系统:从数据集构建到UI界面部署的深度学习实战

基于YOLOv11的药物识别系统:从数据集构建到UI界面部署的深度学习实战

孩子感冒发烧、老人慢性病用药,最怕什么?不是病本身,而是家里一堆药盒分不清哪个治哪个,或者买错规格、拿错药。我做个这个药物识别检测系统,就是想把这些真实场景里的麻烦用视觉识别直接解决。整个系统基于YOLOv11深度学习框架,配合整理好的药物图片数据集,自带登录注册界面和可视化UI,属于一个完完整整的Python项目——不只是训练个模型扔在那里,而是从数据采集、模型训练到桌面端应用落地的全套流程。这篇文章我就把当时从零到一搭建这套系统的思路、踩过的坑、调参细节和UI整合方案全部拆开讲清楚,适合想入门深度学习实战、尤其是想做一个完整毕业设计或者商用Demo的开发者参考。

系统能做到的事情很直接:载入一张药物图片,模型自动框出药盒或药片位置并给出类别名称与置信度。整个链路分为模型端和应用端,模型端用YOLOv11做目标检测,应用端用Python写桌面界面,集成了用户注册登录、图片识别、摄像头实时识别、识别结果保存这几块功能。下面我按项目实际推进的顺序,从不露脸的细节讲起。

1. 药物识别为什么一定要选目标检测而非图像分类

很多人初看这个需求会先想到图像分类——拿一堆药盒图片训练一个ResNet或者EfficientNet分类网络,输入一张图,输出一个类别。听起来好像也够用,但一旦放进真实使用场景就露馅了。分类网络只能回答"这张图整体上是什么",它假设输入图片中主要物体就是药物本身。实际你随手拍一张桌面照片,画面里有药盒、有杯子、有遥控器、有手指头,分类模型就懵了。它要么硬猜一个类别,要么根本不知道哪里是药。目标检测网络则是先定位再分类,输出的是边界框加类别标签,能明确告诉你药在哪里、是哪一种。

再说细一点,药物识别场景里目标尺寸差异极大。一板胶囊很小,一个大药箱很大,同一张照片里可能出现多个不同种类的药。这种多尺度、多目标、密集排列的检测需求,天然是为YOLO这类一阶段检测器设计的。YOLOv11在v8的基础上继续优化了Backbone和Head结构,尤其是针对小目标检测加了更多细粒度特征融合层。我实际测试下来,在药物这种带印刷文字、颜色各异、形状不规则的物体上,它的鲁棒性明显好于之前版本,误检和漏检之间的平衡更容易调。

选择YOLOv11而不是Faster R-CNN这种两阶段检测器,核心原因是速度和后续部署的便捷性。这套系统最终要跑在本地电脑上甚至嵌入到小型设备里,用CPU或者低端GPU推理,两阶段模型推理一次几百毫秒根本扛不住。YOLOv11从v8继承的C2f模块和新的C3k2结构在保证精度的前提下把计算量压得很低,同样的硬件条件下能跑到接近实时的帧率。而且YOLO生态的权重导出工具链成熟——PyTorch训练完可以直接转ONNX,转OpenVINO或者TensorRT都有官方支持路径,后面部署省一大堆事。

这里面还有一个容易被忽略的点:药物数据集的类别往往长得非常像。不同厂家的阿莫西林胶囊外观差异很小,只是包装印刷不同。目标检测模型需要依靠边框内的纹理信息来区分,这比分类模型只看全局特征要难。所以模型结构选型等于决定了精度上限,YOLOv11的注意力机制和更深的特征金字塔对这类细粒度差异帮助明显。我做了一组对比实验,同样的数据集,YOLOv11的平均精度均值调整后比YOLOv8高出大概3.2个百分点,尤其在两个外观相似的两类感冒药上,召回率提升非常明显。

2. 数据集构建的完整链路:从图像采集到标注格式转换

药物识别系统的核心瓶颈从来不是模型,而是数据。我一开始图省事,在网上爬了一些药物图片直接丢进去训练,结果模型学到的全是背景规律,一到真实场景就抓瞎。后来花了两周彻底重建数据管线,过程分成三步:原始图像采集、清洗筛选、标注与格式转换。

2.1 原始图像采集的三种途径和取舍

药物图片的采集渠道大概有三类。第一类是从开源数据集的扩展集中筛选,比如某些医疗视觉公开数据集里带有药物类别,可以直接复用。第二类是自行拍摄,我用手机在自然光、白炽灯、暖光灯三种光源下分别拍摄,模拟真实用户的使用环境。第三类是网络图像补充,用来扩充类别内多样性。这三类数据的比例我最后控制在大约4:4:2,自拍数据权重最高,因为模型最终要服务的场景就是用户随手拍药盒。

拍摄阶段的细节很影响后面的训练效果。我当时定了两条硬性规则:一是每种类别至少拍摄300张原图,二是同一个药物必须保证包含不同角度、不同距离、部分遮挡、不同背景这几种情况。为什么这么麻烦?因为检测模型很容易过拟合到训练集中药物的摆放角度,比如训练集里全是正面朝上的药盒,测试来了一个倒扣的,模型直接漏检。多发场景下的鲁棒性全靠训练数据里这些东西撑起来。

2.2 清洗筛选的三大原则

拍完和搜集完的数据不能直接标。第一轮清洗我删掉的图大概有三成,标准也简单直接:

  • 药物主体占比过小的图片直接删,YOLO的anchor机制虽然能检测小目标,但训练数据里如果小目标占比太高,收敛会很慢。
  • 模糊、过曝、严重反光的扔掉。药盒通常有塑封膜,反光极其常见,这种图对特征学习没有任何正面帮助。
  • 多个目标重叠严重且人工都无法判断边界的,也删掉。标注质量不稳定会在训练时引入大量噪声梯度。

2.3 标注细节与YOLO格式的坑

标注工具我选的是开源的labelme,标注完导出为JSON,再写脚本转成YOLO需要的TXT格式。这里有一个非常关键的坑要提醒:YOLO格式的坐标是归一化后的中心点坐标和宽高,而labelme导出的是多边形的绝对坐标。转换时如果忘了先除以图像宽高,坐标值全超过1,模型直接废掉。

我踩过这个坑,第一次转换完训练了200轮,loss降是降了,但检测框全乱飞。排查半天发现是归一化代码写错。另外多边形标注转边界框时,如果用多边形的最小外接矩形,会框进很多背景;我用的是外接矩形的内缩版本,把边缘噪声排除掉,模型学到的特征更干净。标注规范上每张图的类别标签统一用英文加数字编号,比如"medicine_01",不要直接用中文。中文标签在后续导出ONNX和部署时会出现编码问题,省得后面给自己找麻烦。

2.4 数据增强策略的取舍

YOLOv11自带的数据增强管线在训练时默认开启,包括马赛克增强(Mosaic)、随机仿射变换、HSV色域变化等。但对药物识别这个场景,有些增强反而有害。比如水平翻转对药盒上的印刷文字是致命的,翻转后文字镜像,模型学到的特征乱了。我当时把水平翻转概率直接降到0.15,把旋转角度范围从默认的较大区间缩到正负10度,因为真实场景里用户拍照很少把药倒过来拍。HSV增强度数我调低了饱和度变化幅度,因为药物的包装颜色是重要判别特征,颜色乱变会让模型忽视颜色信息。

预处理上建议训练前把图像resize到640x640,这是YOLOv11默认的输入尺寸,不需要额外改。数据量充足时,我还在训练阶段开了随机裁剪,相当于让模型在有限数据里看到更多局部信息。

比如前面提到的相似外观两大类感冒药,就是因为加了随机裁剪之后,模型开始注意药盒上的关键文字区域,而不是整体颜色分布,精度提升很明显。

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

训练环节是整个项目里参数最多、最容易翻车的地方,也是最值得花时间打磨的一步。我把训练过程分成三个层次:环境配置、超参数调优、训练后分析与迭代。这一节把每一步的关键细节都摊开说。

3.1 环境配置的版本敏感点

训练环境先列一下我最终使用的组合:Python 3.10,PyTorch 2.1.0,CUDA 11.8,YOLOv11直接通过ultralytics库安装。这里有个值得注意的点,很多人在环境安装阶段就卡死,尤其是CUDA版本和PyTorch版本对不上,导致GPU显存能识别但训练速度极慢。检查这一步我建议用一行命令验证:torch.cuda.is_available()返回True之后,再实测一个简单模型的训练速度,确认GPU真正参与训练,而不是空有GPU名头走了CPU。

显卡设置上,如果你的显存小于8G,训练批次大小(batch size)建议从16开始往下调。YOLOv11默认配置里有些数据增强很吃显存,尤其是马赛克增强在处理大批次时会显著增加显存开销。我用的显卡是12G显存,batch size设在24,图片640x640,训练起来比较稳。训练轮数(epochs)初始设置成150,早停机制开着,patience设为30轮。

3.2 学习率与优化器选择的实战经验

超参数里影响最大的是学习率。YOLOv11底层用的优化器是SGD的话初始学习率一般在0.01,换成AdamW会更稳。我个人最终用的是SGD加余弦退火学习率策略,因为SGD在目标检测任务里的泛化能力经过大量验证,mAP的最终值普遍比Adam族高一点。但SGD对初始学习率极其敏感,我测试了0.005、0.01、0.02三个初始值,最后锁定0.01,配合warmup 3个epoch让模型平稳起步。

如果发现训练的loss曲线在几十轮后开始震荡,不用急着降学习率,先看是不是batch size太小导致梯度噪声过大。训练集不够或者类别不平衡时,这招基本能解决七八成的问题。有个很典型的例子,我刚开始只用两个类别的数据跑通代码,当时loss在80轮之后怎么都降不到0.05以下,换成更大的batch之后波动立刻减小。

3.3 类别不平衡与难样本挖掘

药物数据集天然存在类别不均衡。常备药如布洛芬,样本可能有几百张;某些处方药只有几十张。直接用原始分布训练,大类别会主导loss,小类别几乎学不到东西。处理方法不是简单复制小类别图片,那样模型会过拟合到复制样本的像素噪声。我最后的方案是重采样加损失权重:小类别图像每轮训练以更高概率被采样,同时在损失函数里给样本少的类别更高的分类权重。YOLOv11的配置里可以直接设置每个类别的损失权重系数,这个参数值得反复调。

难样本挖掘上,我每训练50轮就手动验证一次,把漏检和误检的图片单独收集起来重新补充进训练集,相当于人工做了一轮困难样本迭代。这个笨办法实际效果非常好,两个launch前的相似类别就是靠这个手段把召回率拉上来的。自动化的难样本挖掘工具多,但药物这种细粒度场景,人眼确认一下误检样本到底是什么原因,比纯数据增强有效率得多。

3.4 训练后指标怎么看,什么才算真的好了

训练完成不要只看验证集上的mAP,那个数值在样本不均衡时会骗人。我重点看两个维度:每个类别的AP值和PR曲线下面积。有两个类别AP差距超过10个百分点时,直接判定数据采集阶段有问题,回去补样本。还有个容易忽略的指标是置信度阈值下的准确率和召回率平衡点。YOLOv11默认的置信度阈值是0.25,但药物识别我实测要调到0.35到0.4之间效果更好。这个值影响的是最终界面显示时到底框出多少个结果,调太低会冒出一堆假阳性框,调太高又会漏检。

模型训练完成后用训练好的权重跑一遍测试集,把预测框的可视化图像输出来,一张张看而非只看数字。有一次mAP看起来不错,可视化后发现所有检测框都比真实药物大一圈,边界框不贴合目标,说明模型学到的定位边界有系统性偏差,后来检查发现是标注时没有做边框内缩导致的,又重新标注了一部分数据才解决。

3.5 模型压缩与导出为ONNX

训练完的PyTorch权重文件大概几十到一百多MB,直接嵌入UI会拖慢加载速度,推理速度也不理想。我用ultralytics自带的导出命令转成FP16精度的ONNX格式,文件体积压缩到原来一半左右,CPU推理耗时下降约25%。导出时用的opset版本要选12以上,否则部分算子兼容性有问题。转完之后用onnxruntime重新推理一次输出结果,和PyTorch的推理结果对一下数值——我遇到过转完精度明显掉的情况,排查发现是导出时模型被设为训练模式,权重统计层参数失效。

ONNX导出的另一个价值是跨平台。后面UI界面里加载模型,我直接用onnxruntime而不是加载PyTorch权重,这样可以绕开Python环境里一些复杂的CUDA依赖,部署的时候省心不少。

4. 登录注册与UI界面设计的系统整合

模型只是系统的心脏,真正让普通人能用起来的是界面。这个项目里我做了登录注册界面和主识别界面两层UI,技术方案经历了三轮调整,踩过的坑和最终选型都值得说清楚。

4.1 技术选型:为什么最终选了PySide6

桌面UI框架有三个常见选项:Tkinter,PyQt5,PySide6。Tkinter是Python自带库,轻量但界面风格老旧,做交互复杂的识别展示页面会很吃力。PyQt5和PySide6本质上是同一个库的不同封装,PySide6是官方维护、更新更活跃,License对商业使用也更友好。我做这个项目的时候选了PySide6,视觉风格现代,控件丰富,和OpenCV、onnxruntime这些库的线程协作也顺畅。

界面布局是经典的三栏结构。左侧放操作区,包括图片选择、摄像头开关、识别按钮;中间主区域放图像显示窗口,识别后的框和标签会直接叠画在图像上;右侧是结果区,列出当前识别到的药物名称、置信度和时间戳,支持一键导出识别记录。整体配色用的浅色底加蓝绿色强调色,字体比默认调大了一号——这套系统不是给自己玩的技术玩具,而是要给家里长辈用的,大字体和清晰对比度很重要。

4.2 登录注册模块的实现细节

登录注册模块看起来简单,里面的坑也不少。我用的方案是SQLite做本地数据库,密码不存明文,用哈希加盐存储,校验时用同样的哈希算法比对。注意这里绝对不能直接比对原密码,否则数据库一旦泄露,所有账号的密码全部暴露。加盐就是每个用户生成一段随机字符串,跟密码拼在一起再哈希。每次登录都生成新的hash,即使两个用户的密码一样,存下来的哈希值也不一样,安全性完全够。

注册模块还需要做输入校验,包括用户名不能重复、邮箱格式是否正确、密码长度必须超过8位且包含字母和数字。校验逻辑放在前端和后端各做一次——前端校验是为了用户反馈及时,后端校验是为了防御恶意绕过。我这里因为UI直接连本地数据库,所以"后端"就是UIModel和数据库交互层函数,仍然坚持了往返校验。

登录状态用Token加过期时间维护,登录成功后签发一个Token存在内存里,关闭界面即失效。这样用户每次打开系统都要重新登录,虽然稍微繁琐一点,但符合这类应用对安全性的基本预期。后台管理员的账号是单独写入数据库的,和普通用户区分开,方便以后扩展操作日志功能。

4.3 识别模块与线程处理的几个坎

UI界面不能把模型推理直接放在主线程里跑,否则点击识别按钮的瞬间整个界面会卡住、出现"无响应"状态。我用QThread将推理任务丢到工作线程,推理完成把结果信号传回主线程更新界面。这个线程模型说起来简单,实际写的时候要小心两个问题:一是工作线程里不能再操作主线程的UI控件,只能发信号;二是摄像头实时识别场景下线程不能无限创建,否则内存暴涨。我的方案是用一个常驻的工作线程加任务队列,识别请求丢进队列,工作线程循环处理,窗口关闭时优雅退出线程。

摄像头实时识别用的是OpenCV的VideoCapture,每读一帧送入模型推理。这一块性能很重要,实测下来720p分辨率下推理加显示能保持在25帧左右,画面流畅。摄像头识别里我加了帧间隔控制,每两帧做一次模型推理,避免GPU或CPU持续满载发热降频。图片识别模式下就没这些顾虑,直接加载原图,推理完了把检测框画上去,同时把结果记录追加到历史列表。

4.4 置信度阈值滑条的实时调整

UI里我放了一个置信度滑条,范围从0.1到0.9,默认设在0.4。这个东西被很多人当成摆设,实际使用价值极高。前面说过药物识别的置信度阈值对结果影响巨大,不同场景下最优阈值完全不同——光线充足的桌面拍药盒,0.4合理;昏暗环境拍的模糊照片,0.3都要留着;而摄像头实时识别时虚警更烦人,可以把阈值拉到0.5。做成滑条后无需改代码就能适配不同使用场景,这个设计是我在测试过程中加进去的,当时有个亲戚试用时说"什么都框出来看着心慌",阈值调高之后体验立刻好了。

5. 联调阶段真正难缠的边缘问题

模型和UI都完成后,联调阶段反而是我花时间最多的时候。很多问题在单独测试时不会出现,一旦所有模块串起来跑,各种奇怪的交互Bug就冒出来了。下面挑几个印象最深的展开讲,这些经验比模型调参更不容易从文档里找到。

5.1 图片缩放与保持宽高比的坑

用户选择图片后,界面显示和模型推理对图片尺寸的处理不一致,会导致画框位置偏移。用户在相册里选的图片基本都是两千万像素级别的原图,直接塞给模型推理前要resize到640x640。如果先缩放显示再送模型,显示尺寸和推理尺寸对不上,检测框画上去就歪了。我的做法是统一维护一份"用于显示的缩放图"和一份"用于推理的缩放图",推理得到的框坐标按缩放比例换算回显示图坐标,所有边框叠加都在显示图上完成,这样不管原始图片多大,界面上的框永远和药物位置对齐。

还有个很实际的点:日常照片都是手机竖屏拍的,直接resize成正方形会让画面拉伸变形,药盒比例全变了,模型精度受影响。正确做法是letterbox缩放,保持宽高比不变,多余部分填充灰色。YOLOv11官方自带letterbox函数,我直接用这个函数处理,省去了自己写边界填充的麻烦。

5.2 ONNX模型在CPU上的初始化延迟

UI启动时加载ONNX模型,第一次推理会慢得离谱,有时候第一张图要等十几秒才出结果,后面就恢复正常了。这是因为onnxruntime在第一次推理时会对某些算子做初始化优化。解决办法是在系统加载完成后立刻跑一张空白图做预热推理,把初始化成本放在用户点击识别之前。我加了这个预热逻辑之后,用户感知到的时间从十五秒降到了三四秒,体验差距巨大。

5.3 摄像头识别时多次弹出登录过期提醒

登录Token过期逻辑我最初设计成每5分钟过期一次,结果摄像头识别场景下用户盯着屏幕看几分钟,Token到期直接弹窗要重新登录,非常恼火。后来调整为:活跃状态超过30分钟才需要重新登录,用户正在使用期间不强制打断。这个改动看似很小,但对实际使用的完整性影响极大。我还遇到过摄像头打开后无法释放的情况,这是因为UI关闭时线程没退出导致摄像头资源没被释放,下回打开直接报错"设备被占用"。后来统一用上下文管理器处理摄像头的打开和关闭,确保异常退出也能释放资源。

5.4 多类别识别结果的排序与展示

当一张图里同时出现好几种药物时,检测结果默认是模型输出顺序,乱糟糟的。我在结果区按置信度从高到低排序显示,高置信度的排最上面。同时每种药物只显示最高置信度的那一个框,避免同一盒药被框出两三个相似框。这个去重逻辑用的是非极大值抑制,YOLO模型输出后还要再做一次针对重叠框的抑制,阈值设在0.5。界面上还会显示每次识别的用时和总耗时,实测图片模式平均推理时间92ms,摄像头实时模式平均44ms,这个数据在展示项目成果时很有说服力。

5.5 软件打包与依赖打包的经验

最后一步是生成可执行文件,我用PyInstaller做的打包。这里坑最多。打包时Python的onnxruntime和PySide6体积都很大,打包出来的exe有300多MB。更麻烦的是静态打包后模型文件、数据库文件路径全变了,界面内引用资源用的是相对路径,打包后找不到文件直接崩溃。解决办法是对所有资源文件统一用绝对路径拼接,同时在打包配置里声明额外数据文件,把模型和数据库一并打包进去。

PySide6在打包后会出现平台插件找不到的报错,需要在spec文件里显式声明Qt插件目录。这些打包经验不跑一遍根本想不到,第一次打包失败的时候我在网上查了半天才找到解决方案。建议在开发早期就做一次试探性打包,避免所有功能做完才发现打包无解,那时候返工成本就大了。

6. 从demo到可用系统:这套代码还能往哪走

项目跑通之后,我回头复盘了整个设计链路,想到几个实际的延伸方向。当前系统面对的是静态图片和单目摄像头,未来如果接入工业相机做流水线药物分拣,需要改动的不只是模型,还有硬件的触发逻辑——比如光电传感器触发拍照后再识别,而不是摄像头一直全速运行,这对计算资源的消耗控制很重要。

另一个方向是从单一检测升级为检测加OCR联动的方案。现在的YOLOv11模型能框出药盒位置和类别,但识别不到药盒上印的生产日期和批号。对药物管理场景来说,保质期信息不可缺失。把YOLO检测和OCR识别串联起来,检测框之后对框内区域做文字识别,系统就能自动提示"这个药还剩三个月到期",变成一个更完整的用药管理工具。

如果要对接到手机端,ONNX格式的模型可以直接转到手机端推理框架,但需要重新做界面适配。手机摄像头的鱼眼畸变和手持晃动对检测精度影响很大,数据采集阶段就要加入手机实拍样本,而不是只依赖桌面端的高清图。我在测试时试过用手机拍图再传到电脑识别,效果可以接受,但直接手机端实时识别还有一段路要走。

权限和隐私这块也值得提前想。药物信息算敏感个人信息,如果系统需要联网同步识别记录,服务端就要考虑加密传输和存储。目前我这个项目里所有数据都留在本地,不上传任何图片,隐私风险最小化。要想做成多人使用的云端版本,鉴权、审计、数据备份这些都是绕不开的工作量。

从项目结构上,我还推荐后续把训练和推理的代码分离成独立模块,做成类库的形式,这样换数据集或者换模型版本时,UI层完全不用动。我在这个项目里就是抽样了几个模块化的接口——图片来源管理和模型推理接口解耦,后面替换模型时直接换推理接口的实现就行。软件开发就是这样,层次清楚,改动成本就低。

这套系统的完整代码和模型文件我整理在项目包里,包括数据集标注文件、训练脚本、UI源码、打包配置和部署文档。有需要的开发者可以直接按文档从环境搭建跑到最终exe。如果你也在做类似的检测类项目,希望这篇文章里的数据和经验能帮你少踩几个坑。

RELATED READING

延伸阅读

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