1. 项目概述与技术选型解析1.1 这个项目到底是做什么的花生的霉变问题在仓储和加工环节里一直是个老大难。发霉的花生不仅口感变差更致命的是会产生黄曲霉毒素这东西耐高温、难去除误食后对肝脏损伤很大。过去做霉变检测主要靠人工肉眼挑拣流水线上的工人一天盯十个小时眼睛疲劳后漏检率会明显上升而且每个质检员的标准不完全一样经常出现这批货三个检验员给出三种结论的情况。我这次做的这套“基于YOLOv11的花生种子霉变检测系统”本质上就是用深度学习模型代替人眼完成花生颗粒的自动识别与分级。它的核心流程是通过摄像头或图片输入由YOLOv11模型对画面中的花生逐颗判断是“正常”还是“霉变”检测结果实时叠加在画面上同时统计总数、霉变数、合格率这些关键指标。在此基础上我还给它套了一个完整的桌面端UI外壳——带登录注册功能有检测界面、历史记录界面、模型管理界面整套东西不依赖命令行双击就能跑起来。这个项目适合三类人。第一类是刚学完YOLOv8/YOLOv11基础教程的学生想找一个“模型界面业务逻辑”全套完整的项目来练手搞清楚从数据集到可交付软件的完整链路。第二类是农产品质检相关方向的研究者需要用深度学习方法做小目标分类识别花生这种颗粒状、尺寸较小、分布密集的检测对象拿来当实验素材非常合适。第三类是有实际生产需求的从业者虽然真正上线还需要做硬件集成和压力测试但拿来走查流程、做方案预演绰绰有余。1.2 为什么选YOLOv11而不是其他模型选检测模型这事我纠结过很长时间。一开始想用传统图像处理方案根据花生皮的颜色分布和纹理特征写规则判断。测试时发现在固定光照、固定背景下效果尚可一旦光线稍有变化、或者花生表面沾了土误判率就飙升到没法用的程度。规则写了几十条最后还是推倒重来。换到深度学习方案后我对比了好几类模型。分类网络比如ResNet、MobileNet能判断一张图里总体是否霉变但回答不了“画面里哪几颗坏了”这个问题。生产场景里工人需要的是精准定位坏的要挑出去不是整批倒掉。而两阶段检测器比如Faster R-CNN精度不错但推理速度慢摄像头预览时明显有延迟感不适合做实时检测。YOLO系列是一阶段检测器的代表它把“物体定位分类”压缩成一个回归问题一张图前向推理一次就能同时输出所有目标的位置和类别。YOLOv11是YOLOv5/YOLOv8之后的新版本在C2f模块基础上做了结构优化引入了更高效的注意力机制在保持高精度的同时把参数量继续压缩。对于花生检测这种类别不多就两类、目标较小、背景相对简单的任务YOLOv11的推理速度完全够用在CPU上勉强能跑有GPU更是流畅顺滑。另外我特意选了YOLOv11还有一个实际原因它配合Ultralytics框架训练和导出模型的代码非常简洁几行就能完成数据训练和权重导出这对要写整套系统的我来说省下大量时间。模型文件可以直接导出成ONNX格式用来做CPU推理加速也可以导出成TensorRT格式部署到边缘设备扩展性很强。1.3 整体功能地图登录、检测、历史三块核心整套系统我拆成三个功能模块来设计避免把代码写成一坨。第一个是用户模块承担登录和注册。这个模块看起来简单但它是整套系统能不能给别人用的关键门槛。没有登录任何人打开软件就能操作换谁来做检测、做了多少次、结果怎么追溯全都没有记录。我给它设计了两套账号体系一是管理员账号能看所有用户的历史记录适合质检主管用二是普通用户账号只能查看自己的检测历史适合一线操作工用。密码采用哈希加盐后入库不能明文存储。第二个是检测模块这是核心中的核心。包含图片文件检测、摄像头实时检测、拖拽图片检测三种入口。图片检测是把单张或多张图片交给模型推理输出结果保存到本地摄像头检测是打开本机摄像头对实时画面临近帧推理画框标出霉变花生拖拽检测算是个便捷入口直接把图片拖到窗口里就能出结果操作门槛降到最低。检测结果要同时展示标注图、统计数字花生总数、霉变数、正常数、霉变率和单颗花生的置信度分数。第三个是历史记录模块。每次检测的图片、结果图、统计指标、检测时间都存入本地数据库。界面上提供按日期查询、按结果筛选的功能还能一键导出CSV报表方便做批次质检记录归档。这三个模块合起来才是一个能“给别人用”的完整系统而不是只能跑通模型的玩具脚本。2. 数据集构建与预处理细节2.1 数据从哪来拍摄规范比数量更重要做检测系统最怕数据质量不合格数量凑够了但画面光照乱、角度杂、目标比例失真训练出来的模型在真实场景里一测就露馅。我构建花生霉变数据集时先定了拍摄条件规范。光照方面用5500K色温的LED平板灯做主光源亮度调到让花生表皮纹理清晰可见、无明显反光的程度。背景统一用哑光黑色PVC板目的是让花生与背景有足够高的对比度降低模型学习背景纹理的负担。拍摄设备用普通工业USB摄像头分辨率设定在1280×720镜头距拍摄面固定为30厘米。这个距离下单颗花生在画面里约占40×60像素不会太大也不会小到看不清纹理。数据集一共采集约4200张原始图片其中正常花生2600张霉变花生1600张。采集时尽量覆盖不同批次的花生——不同产地、不同含水量、不同霉变程度轻度霉变表面局部出现灰绿色斑点和重度霉变大面积黑色霉斑、表面干瘪都要有。如果只收集极端样本模型学到的特征是“很严重的霉变”对早期轻度霉变视而不见实际应用中就会漏检。注意霉变样本不要只拍一种状态。轻、中、重度霉变建议大致按3:5:2的比例采集模型才能学到霉变程度的连续变化特征。2.2 标注要点小目标标注的边界处理标注这一步我用LabelImg完成输出格式选YOLO格式。YOLO格式的标注文件是txt每行记录一个目标的类别id和归一化坐标中心点、宽高。花生检测我一共就两类类别0是normal正常类别1是moldy霉变类别编号从0开始不能写成1和2。标注时最容易出错的地方是边界框的松紧程度。有些人喜欢把框打得特别紧紧贴目标边缘有些人习惯留一点边。这两种习惯训练出的模型性能差异不大但必须保证一个人标完整个数据集或者多人标注前先统一规则。我自己的经验是框从目标最外缘往外再放23个像素避免把背景纹理圈进框里导致特征混淆。还有一个特别容易踩的坑花生颗与颗之间经常挨得很近甚至相互重叠。标注重叠花生时我选择给露出的部分也单独标框而不是把重叠区域整个合并成一个框。虽然YOLO对紧密相邻目标的召回率天然有些吃力但保留正确标注至少能让训练过程学到“密集场景的区分特征”。标注完成后我用脚本做了一次完整性校验逐个解析txt文件检查坐标值是否在01范围内、是否有重复框、是否有框的面积异常小。这类脏数据混进训练集轻则损失一点点精度重则让损失曲线震荡不收敛。2.3 数据增强策略假样本也有大作用花生数据集的原始图片数量不算多4200张用来训练一个小型检测模型属于勉强够用。为了把泛化能力拉上去我做了离线数据增强和在线数据增强两层策略。离线增强主要是对原始图片做扩充。我用Albumentations库做了一套pipeline水平翻转概率0.5、随机亮度对比度调整亮度限制±30、随机旋转角度限制±15度、随机裁剪缩放裁剪后不低于原图面积的60%。每张图平均扩充3倍数据集变到约12000张。这个扩充不是盲目做的亮度扰动非常关键——不同车间的光照不可能完全一致模型在亮度扰动下见过足够多样本才能对光照变化保持鲁棒。在线增强则交给Ultralytics的train配置设置hsv_h0.015、hsv_s0.7、hsv_v0.4、degrees10、translate0.1、scale0.5。注意在线增强的参数不宜过高尤其是translate和scale太大会让花生目标变形得不像花生。增强操作必须在标注框上同步进行。比如旋转15度后标注框的坐标也随之旋转映射到新图里。Albumentations和Ultralytics内部都已处理好了这种同步但如果你自己写增强函数这一步千万别漏了否则标注和图像内容对不上等于给模型灌毒数据。3. 模型训练与性能评估实录3.1 训练环境与超参配置我的训练环境是消费级配置RTX 4060显卡、8GB显存、64GB内存Windows系统上跑Ultralytics框架。YOLOv11按体积分为n / s / m / l / x五档。花生检测只有两个类别、目标尺寸偏小属于中等偏简单任务所以选择yolo11s这个档位就能平衡精度和速度。如果电脑显存在6GB以下建议选yolo11n训练速度更快精度大概会下降23个点。训练前要做的最重要一步是划分数据集。我用脚本按8:1:1划分训练集、验证集、测试集。划分配置写入一个data.yaml文件内容大概是path: D:/peanut_dataset train: images/train val: images/val test: images/test nc: 2 names: [normal, moldy]注意path路径必须是绝对路径如果换了电脑跑代码这个坑会让人排查半天。另外训练集里两个类别的图片数量要保持平衡如果霉变样本明显偏少建议在离线增强阶段对霉变类别额外扩增到和正常类别大致持平的比例防止模型偏向多数类。训练时的超参配置我提交如下yolo detect train modelyolo11s.pt datapeanut.yaml epochs100 imgsz640 batch16 patience20 projectruns namepeanut_yolo11sepochs设100轮用patience20做早停如果连续20轮验证集mAP没提升自动停止训练并保留最优权重不用白等100轮结束。输入分辨率设640这是YOLO系列默认值对花生这种中小目标合适。batch设为16因为8GB显存跑yolo11s这个规格batch设为16已经比较极限。如果爆显存报CUDA out of memory就把batch降到8训练时间最多翻倍但效果差不多。3.2 训练过程观察损失曲线怎么才算好模型训练过程中要盯两个指标第一是训练损失box_loss、cls_loss、dfl_loss的收敛趋势第二是验证集mAP50和mAP50-95的变化。我自己这轮训练跑了大概85轮早停触发在80多轮后。损失曲线整体平滑下降前20轮下降最快40轮后进入缓慢下降区间说明模型在快速拟合数据后进入精调阶段。如果在训练中后期发现验证集损失开始反弹而训练损失还在降这是过拟合信号说明该提前停了早停机制会自动处理这一点。最终验证集结果mAP50约96.8%mAP50-95约84.5%。对花生这种两类小目标检测任务这个精度属于可用级别。测试集上的单张图片推理耗时在RTX 4060上约为28毫秒导出ONNX后在同机CPU上推理约480毫秒单帧摄像头画面流畅度不错。提示mAP50-95是对不同IoU阈值0.5到0.95取平均它更能反映“框打得准不准”。如果mAP50很高但mAP50-95偏低说明框的位置或尺寸偏得不精准这种情况要把imgsz调大到800再试试。3.3 模型导出与推理验证训练结束后Workers目录下的weights文件夹里有两个权重文件best.pt和last.pt。best.pt是验证集上表现最好的权重实际部署一律用它。我还会顺手导出成ONNX格式方便CPU部署和跨平台使用yolo export modelbest.pt formatonnx opset12 imgsz640导出后我用几张从未参与训练的图片做推理验证选取的目标包含以下场景颗粒紧密挨着的、轻微光照偏暗的、花生大小不均的。模型在紧挨场景下偶尔会出现两个目标被并成一个框的情况——这种情况在密集小目标检测里很难完全避免出现频率约5%。但好消息是真实质检过程中花生进入检测位时通常有分料机构不会始终处于密堆状态。另外我还用OpenCV的DNN模块加载ONNX模型测试了CPU推理流程。虽然速度比GPU慢不少但胜在部署简单不用装PyTorch的CUDA环境适合作为无GPU条件下的后备方案。4. 系统界面开发与功能实现4.1 登录注册模块安全与体验的平衡登录注册我基于PySide6Qt的Python绑定来实现。选PySide6而不是Tkinter是因为它的控件美观度、布局灵活性、和OpenCV图像的集成方式都明显更顺手。做一个扫码仪风格的简洁界面Tkinter那套控件画风很难看交出去的项目也拿不出手。注册界面有几个字段用户名、密码、确认密码。密码规格要求至少6位且包含字母和数字。注册时用哈希算法做加密。这里必须多说一句不要用MD5这类快速哈希要用带盐的PBKDF2或bcrypt。我Python项目里直接用hashlib内置的pbkdf2_hmac实现import hashlib, os, sqlite3 def hash_password(password: str, salt: bytes None) - tuple: if salt is None: salt os.urandom(16) hashed hashlib.pbkdf2_hmac(sha256, password.encode(), salt, 100000) return salt.hex(), hashed.hex()数据库中保存salt和hash两列校验时把用户输入的密码用同样的盐重新哈希比对结果即可。这样做即使数据库文件被别人拷走也无法反推明文密码对内部工具的隐私保护够了。登录逻辑要处理几个特殊场景用户名不存在、密码错误、连续失败锁定。连续失败5次后锁账户15分钟这个策略可以防止有人写脚本对登录接口做暴力尝试。界面布局上登录窗口居中显示一个Logo区域放产品名或一张花生图片、用户名输入框、密码输入框、登录按钮、注册按钮以及一个“记住我”复选框。记住我的实现方式是把用户名和session token存入本地配置文件下次打开自动填入但密码绝不持久化。4.2 主检测界面三入口设计与实时预览体验主窗口的布局我设计成左侧控制面板、中间实时预览区、右侧结果展示区三段式。左侧面板从上到下依次是摄像头选择下拉框、开启/关闭摄像头按钮、图片选择按钮、拖拽提示区域、检测阈值滑条。中间预览区用一个QLabel充当画布不断刷新来自摄像头或图片的帧。右侧面板显示统计信息检测总数、正常数、霉变数、霉变率下面放一个表格列出每次检测的目标坐标和置信度。图片检测入口走文件对话框支持jpg / png / bmp格式多选批量检测。检测时每一张图的推理结果都叠加画框正常花生用绿色框霉变花生用红色框框的左上角标注类别和置信度比如“moldy 0.92”。画框的代码基于OpenCVdef draw_detections(frame, results): for box in results.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) conf float(box.conf[0]) cls int(box.cls[0]) label f{names[cls]} {conf:.2f} color (0, 255, 0) if cls 0 else (0, 0, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return frame摄像头实时检测要注意推理频率。模型在GPU上单帧推理约30毫秒但摄像头的读取、图像预处理、画框绘制都要占用时间如果每一帧都跑推理界面会明显掉帧卡顿。我的做法是开一个后台线程持续从摄像头读帧但检测操作控制在每秒45次中间帧直接显示不做推理。这样既保证画面流畅又保证检测结果不会滞后太多。4.3 历史记录与数据持久化设计检测结果的历史记录存储我用SQLite实现。数据库文件放在项目根目录下的data文件夹中建表语句如下CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, image_path TEXT, result_path TEXT, total_count INTEGER, moldy_count INTEGER, normal_count INTEGER, moldy_rate REAL, created_at TEXT DEFAULT (datetime(now, localtime)) );每次检测完成后把原图复制到项目目录下的history_images文件夹防止原始图被移动后记录失效检测结果标注图也存一份然后把统计信息写入数据库。历史记录界面做成一个表格双击某一行可以在右侧预览该次检测的标注大图和当时的统计详情。导出CSV功能用标准库csv模块实现导出格式包含日期、用户、图片路径、总数、霉变数、霉变率。生产场景里这个功能很实用质检主管可以直接把CSV导入Excel做周报月报不用自己去手动敲数据。注意事项数据库写入操作要在后台线程完成不要在UI主线程里执行IO操作否则检测过程中界面会出现短暂卡顿。我用的是Python标准库sqlite3注意在写入频繁时加上with self.lock上下文避免多线程并发写导致锁竞争异常。5. 常见问题与排查技巧实录5.1 高频问题速查表这套系统从开发到改稳定我踩了不少坑。下面把最典型的几个问题整理成速查表方便排查。问题现象可能原因解决方案训练时CUDA out of memorybatch尺寸过大或图片分辨率过高batch降到8imgsz降到640推理结果全是正常类霉变检出率低训练集霉变样本不足或标注不准确补充霉变样本重新检查标注框摄像头界面卡顿严重每帧都做检测线程调度不当检测逻辑改为间隔触发主循环只做显示模型运行报KeyError类名data.yaml的names顺序与标注不一致统一点检查normal是0moldy是1登录后进入主界面闪退缺少Qt平台插件或模型权重路径错误检查权重文件是否存在验证PySide6环境注册用户后无法登录密码哈希算法在注册和登录时不一致封装统一工具函数两端调用同一接口5.2 模型精度够了但实际检测却误报多这是一个容易让人头疼的情况验证集mAP很高但一上真实业务数据就误报不断。根源通常不是模型本身而是实际场景与训练集分布不一致。比如训练时拍摄背景是黑色PVC板实际使用环境是传送带上的棕黄色背景颜色特征虽然不会让模型丢掉花生但会给纹理判断引入大量噪声。我的解决思路是在系统里加入一键“背景适配微调”功能——在实际部署场地拍摄200300张现场照片用已训练的权重作为预训练用小学习率0.0001再训练50轮完成迁移。这一步能有效把模型从实验室背景适配到生产现场。如果不想重训也可以在预处理加入自动白平衡和高斯滤波减少光照和噪声带来的分布偏移。5.3 打包发布时遇到的三个大坑开发完成后要给别的电脑部署PyInstaller打包是绕不过去的一关。我打包过程中遇到三个比较有代表性的坑。第一个是模型权重文件被PyInstaller漏掉。Ultralytics的权重从代码里通过相对路径加载PyInstaller默认不会把非py文件打包进可执行文件。解决方法是把best.pt作为数据文件显式加入spec文件的datas列表。更稳妥的做法是打包后把权重文件放在exe同级的models目录下代码里用os.path.dirname(sys.executable)定位exe所在目录再拼接模型路径。第二个是OpenCV的DLL依赖不全在无Python环境的电脑上运行报找不到mkv2.dll这类错误。PyInstaller的hook机制一般能自动收集OpenCV的DLL但偶尔会有遗漏。我的做法是用--collect-all opencv参数强制收集所有相关文件。第三个是摄像头在打包后无法被识别。原因通常是PyInstaller没有包含PySide6的插件目录如platforms、imageformats。打包命令要加上--collect-all PySide6否则界面能显示但摄像头一开就空白。打包命令参考pyinstaller --noconfirm --windowed --name PeanutDetector \ --collect-all PySide6 --collect-all cv2 \ --add-data models/best.pt;models \ main.py执行完在dist目录下就能找到可执行文件整个文件夹拷给目标电脑即可运行。建议把整个dist目录压缩传输不要只传exe。5.4 推理速度优化与轻量化部署思路如果要在更低配的机器上部署比如无独立显卡的办公电脑有几个优化手段实测有效。第一输入尺寸从640降到480。对花生这种中等偏小的目标尺寸降一点影响不大但推理速度提升接近40%。第二ONNX Runtime配合CPU线程设置在加载模型时指定intra_op_num_threads为CPU核心数的一半多核机器上的延迟能再降一些。第三模型量化。把ONNX模型转为INT8量化版后尺寸缩小到1/4推理速度提升约2倍代价是mAP下降约1.52个百分点。对于只有两类目标的任务这个精度损失完全可以接受。如果产品要走向嵌入式比如用树莓派或边缘盒子还可以把模型导出成TensorRT格式但这需要有NVIDIA显卡进行离线转换推理端不需要显卡用带有TensorRT推理引擎的边缘设备即可。6. 送给接盘者的几点实用经验整个项目从决定开坑到交付出一个能演示、能跑通的版本前前后后大概花了三周时间。如果重新做一遍我会把时间分配得更极端一点第一周只做数据采集和标注第二周训练和调优模型第三周开发界面和打包。最后分享几个我踩过坑之后觉得值得记住的经验。第一数据集永远值得花时间。模型性能的上限在数据标注完成那一刻就已经决定了后续调参只是在逼近这个上限。如果你的霉变样本本身质量差、覆盖场景窄就算换个更大的模型也救不回来。第二UI界面开发不要和模型训练并行。我一开始边调参边写界面结果两头都拖慢脑子切来切去也不清爽。先让模型达到可用精度界面写起来才会安心否则界面做得再漂亮模型识别不准演示时一样翻车。第三保存一切结果图。检测结果图是质检追溯的关键凭据务必保留这在真实生产环境和课程答辩中都是加分项。线上归档机制我建议至少保留最近90天的结果图本地清洗策略用定时任务自动清理超期文件。第四如果有人要跑这个项目第一时间提醒他先看data.yaml里的路径。我因为这个路径问题帮两个人远程排查过每次都是路径没改导致训练报错浪费了半小时。这套系统的核心价值在于把训练好的模型真正变成了一个可操作、可管理、可追溯的质检工具。下一步的扩展方向也很明确把霉变检测拓展到其他农产品上比如玉米、黄豆、咖啡豆换数据重新训练即可再进一步可以接上分拣机构让检测结果直接驱动电磁阀吹掉霉变颗粒实现无人化分选。如果你已经成功跑通这套代码往这些方向扩展就不算太难了。