基于YOLOv11的花生种子霉变检测系统实现与工程部署
做农产品检测的人应该都有过这种体会——人眼挑花生种子挑一篮没问题挑十篮就开始犯晕挑到下午袋子堆成山的时候全凭手感了。我最近完成的这个基于深度学习YOLOv11的花生种子霉变检测系统就是想把这种枯燥、易疲劳、标准还不统一的人工分选环节替换成自动化视觉判断。系统包含YOLOv11模型训练、原生YOLO格式数据集构建、带登录注册的用户界面、完整的Python工程源码和训练好的模型权重整套东西跑通之后即使完全不懂深度学习的人也能打开界面直接使用。这里说的“花生种子”跟市面上卖的花生米不完全是一回事。种子果仁既要保证食用安全还要考虑发芽率和幼苗成活率霉变粒哪怕只占一小部分混在种子里也容易影响整批种子的商品等级。所以这个项目的核心价值不是做个炫酷Demo而是要真正落到分选场景里的实用工具。我用这个项目完整走了一遍“数据采集—标注—训练—界面集成—实测调优”的闭环下面把关键环节和踩过的坑都摊开讲方便想复现的人直接参考。1. 先想清楚“为什么做”花生种子霉变检测的真实痛点1.1 霉变问题到底有多严重花生一旦霉变最容易产生的是黄曲霉毒素B1这是一种毒性很强的物质在国际上被公认与肝癌发病率相关。作为食品原料黄曲霉毒素有严格的限量要求作为种子霉变粒的出芽率会大幅下降而且霉菌孢子会在储存过程中传染给相邻的健康粒导致整批种子的商品等级被拉低。换句话说不论走向餐桌还是走向田地霉变粒都必须被剔除干净而且最好在进料早期就拦住不要等到入库了才发现。1.2 人工分选的三个致命短板我在接触食品和种子加工场景时听到最多的是三个词效率、一致性、成本。效率方面一个熟练的质检员靠着分选台每小时大概能挑出几百克到一两公斤的花生大批量进料的时候根本供不上产线。一致性更现实不同的人对“霉变”的判断标准不一样同一个人上午和下午的状态也不一样漏检率波动非常大。成本则是长期问题质检岗位本身枯燥人员流动性大培训和磨合都很耗精力。所以用视觉算法来做初筛不是“炫技”而是实实在在能缓解这些问题的方案。1.3 为什么选目标检测而不是图像分类最开始也纠结过如果只是判断“这一堆花生里有没有霉变”用二分类网络更简单。但放到真实分选场景里我们要的不只是“有没有”还得知道霉变粒在画面中的位置、数量、占比。有了位置信息后续可以联动机械臂剔除或者生成批次质检统计报表。目标检测输出的正是“类别位置框”的组合。YOLO系列在检测速度和精度的平衡上一直做得不错YOLOv11作为较新版本改进了特征提取和注意力机制对霉变斑块这类“局部小特征”的捕捉能力比前几代更友好。所以我最终把方案定在YOLOv11上整体走了一条可落地的工程路线。2. 数据是模型的粮食花生霉变数据集的采集、标注与划分2.1 采集场景怎么设计很多做深度学习项目的人会在数据上吃亏我这次学乖了第一阶段就把采集条件想清楚。目标是模拟真实分选环境而不是拍一堆“实验室美图”。拍摄距离相机离托盘或传送带上的花生大约10到30厘米要让单粒花生在画面里占足够像素。背景选深色哑光背景对比度强、不反光。黑色绒布或哑光塑料板都可以。光照把不同时段、不同灯源的照片都混进来。太阳光、白炽灯、LED台灯下的色温差异很大都拍一些模型才不容易过拟合到某一种灯光。摆放方式既要有单粒散放的也要有多粒紧挨甚至轻微重叠的模拟真实进料时的不规则堆叠。采集数量上总共拍了900多张有效图片正常花生和霉变花生两类都有覆盖后期标注出的目标框超过6000个。2.2 标注工具与YOLO格式标注工具选的是LabelImg因为它轻量、导出YOLO格式不需要额外转换。也可以选X-AnyLabeling或Roboflow功能上各有侧重但最核心的产出是一样的每张图片对应一个同名txt文件。YOLO标注格式很好理解一行就是一个目标class_id x_center y_center width height其中坐标都换算成0到1的归一化值。举个例子# 一张640x480的图上坐标都已归一化 0 0.4531 0.3562 0.1234 0.1562 1 0.7266 0.7125 0.1109 0.1438第一行表示一个正常花生第二行表示一个霉变花生。这里类别0定义为正常花生normal类别1定义为霉变花生moldy。标注时有两个容易忽略的细节。一是框要尽量贴合果仁边缘不要留太多空白YOLO回归框时“贴边”能减少学习难度。二是类别定义要统一轻微霉变就算霉变不要一会儿按霉变标一会儿按正常标否则模型会被你自己的标注搞糊涂。2.3 数据组织方式Ultralytics训练时要求数据集按固定目录组织。我习惯这样存放dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamlimages和labels文件夹一一对应文件名保持完全一致。用一个简单的脚本就能把标注好的数据自动划分到训练验证集我习惯按80/20的比例切分import os import random import shutil src_img raw/images src_label raw/labels train_img dataset/images/train train_label dataset/labels/train val_img dataset/images/val val_label dataset/labels/val all_files [f for f in os.listdir(src_img) if f.endswith(.jpg)] random.seed(42) random.shuffle(all_files) split int(len(all_files) * 0.8) for f in all_files[:split]: shutil.copy(os.path.join(src_img, f), train_img) shutil.copy(os.path.join(src_label, f.replace(.jpg, .txt)), train_label) for f in all_files[split:]: shutil.copy(os.path.join(src_img, f), val_img) shutil.copy(os.path.join(src_label, f.replace(.jpg, .txt)), val_label)这里有个关键点划分最好按“拍摄批次”来而不是按单张随机划分。如果同一段视频抽帧出来的图一半在训练一半在验证指标会虚高这个坑我后面还会展开说。2.4 数据增强要不要做Ultralytics的训练流程里默认会开启Mosaic、随机平移、缩放、HSV色彩扰动等增强策略。对小目标检测来说Mosaic增强尤其有用它把四张图拼在一起相当于变相增加了每批次的样本多样性。对于花生霉变这个场景我额外调高了hsv_h和hsv_s的扰动范围。原因很直接不同光照下花生的色温差异挺大让模型在训练时就见过各种颜色偏移测试时遇到新光源才不会露怯。3. YOLOv11训练全流程从模型选型到权重输出的关键决策3.1 YOLOv11到底改进在哪YOLOv11可以看作是YOLOv8的延续升级核心变化集中在backbone和neck部分。比如引入了C3k2模块在保持轻量的同时增强了特征提取能力又比如加入了C2PSA结构让模型能更关注图像中重点区域的特征类似给网络加了“注意力”。对花生霉变检测来说霉变区域往往只是果仁上的一小块暗色斑纹如果网络对局部细节不敏感很容易漏检。这也正是我最终选定YOLOv11的原因它对细小差异的感知确实比之前的版本更稳。3.2 模型规格怎么选YOLOv11官方提供n、s、m、l、x五档规格。我做了简单对比模型参数量特点适用场景YOLO11n约2.6M速度最快精度相对有限边缘设备、摄像头实时YOLO11s约9.4M速度和精度平衡本系统首选普通办公电脑、分选设备YOLO11m约20.1M精度较高显存需求更大有条件上GPU的生产线YOLO11l/x更大精度高但训练和推理都更重服务器端离线批量检测实际跑下来YOLO11s在640分辨率下的效果已经足够区分正常和霉变花生推理速度在普通显卡上也能达到几十毫秒一帧所以我建议默认从s规格开始。如果训练集规模小先拿n规格跑通流程也完全可以。3.3 环境搭建整个项目用Python实现核心依赖Ultralytics和PyQt5。建议用conda建干净环境conda create -n peanut python3.10 conda activate peanut pip install ultralytics pip install pyqt5 opencv-python有NVIDIA显卡的话再装对应版本的PyTorch GPU版训练速度会差很多倍。3.4 数据集配置文件在dataset目录下放一个data.yamlpath: dataset/ train: images/train val: images/val nc: 2 names: [normal, moldy]path写的是相对路径还是绝对路径都行但为了保证打包后能用我建议用相对路径后续移动工程目录也省事。3.5 训练命令与参数选择训练代码用Ultralytics的API就能跑from ultralytics import YOLO model YOLO(yolo11s.pt) model.train( datadata.yaml, epochs120, imgsz640, batch16, patience20, optimizerSGD, lr00.01, device0, )解释几个关键参数epochs设为120配合patience20做早停。如果验证集在连续20个epoch内没有提升训练会自动停止避免无意义的过拟合。输入分辨率imgsz设为640是精度和速度的折中。提到960的话小目标检测更好但推理速度会明显下降。batch设为16如果你的显卡显存只有6G左右降到8或4。优化器我选了SGD。虽然Adam收敛更快但SGD最终精度通常更稳尤其在数据集不够大的时候。训练过程中会在runs/detect/目录下生成权重文件最有用的就是best.pt和last.pt。best.pt是验证集表现最好的权重推理部署都用它。3.6 怎么判断模型好坏训练日志里最值得盯的指标有四个precision、recall、mAP0.5、mAP0.5:0.95。precision预测为霉变的框里真正是霉变的比例。recall真实霉变框里被成功找出来的比例。mAP0.5IoU阈值0.5下的平均精度最常用。mAP0.5:0.95更严格要求框的位置也更准。对于花生分选recall比precision更重要。漏掉一粒霉变花生可能整批种子都受影响多框一两粒正常花生顶多让人工多看一眼。所以调参和阈值时要重点守住recall。我这轮训练的最终结果是mAP0.5到了0.92recall稳定在0.89左右基本满足分选初筛需求。4. 把模型变成产品UI界面与登录注册系统的搭建思路4.1 界面框架选型桌面端UI可选PyQt5或Tkinter。Tkinter上手快但控件和样式都比较简陋做检测软件的交互体验稍差。PyQt5虽然学习曲线稍陡但控件丰富、支持QThread多线程、打包生态成熟最终选了它。框架上手难度控件丰富度线程支持打包生态Tkinter低一般一般一般PyQt5中丰富健全成熟4.2 登录注册模块怎么实现登录注册本质上就是一套用户数据管理。数据存储我选了SQLite比MySQL轻量部署时不需要单独装数据库服务。建表语句CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, salt TEXT NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );密码不能明文存储我用盐加哈希import hashlib import secrets def hash_password(password: str) - str: salt secrets.token_hex(16) hashed hashlib.sha256((salt password).encode()).hexdigest() return salt : hashed def verify_password(password: str, stored: str) - bool: salt, hashed stored.split(:) return hashlib.sha256((salt password).encode()).hexdigest() hashed每个用户的盐是随机生成的即使数据库泄露彩虹表也很难直接破解。注册时判断用户名是否已存在登录时校验哈希值逻辑清晰也安全。4.3 主检测界面怎么布局主界面我设计成左右结构左侧是一个大面积图片显示区用来展示原始图和检测结果图。右侧是参数区显示置信度阈值滑块、当前检测结果统计总粒数、正常粒、霉变粒、霉变率。底部是功能按钮选择图片、开启摄像头、保存检测结果、退出登录。检测按钮的槽函数核心逻辑def on_detect_clicked(self): path self.open_file_dialog() if not path: return self.status_label.setText(检测中...) self.thread DetectThread(self.model, path) self.thread.result_ready.connect(self.show_result) self.thread.start()在这里必须用QThread把模型推理放到子线程。模型推理本质是耗时计算如果直接写在Qt主线程里界面会直接卡死按钮点了没反应体验非常差。线程类的简化版本import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class DetectThread(QThread): result_ready pyqtSignal(object, object) def __init__(self, model, image_path): super().__init__() self.model model self.image_path image_path def run(self): results self.model.predict(self.image_path, conf0.45) annotated results[0].plot() rgb cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) height, width, _ rgb.shape qimg QImage(rgb.data, width, height, rgb.strides[0], QImage.Format_RGB888) self.result_ready.emit(qimg, results)推理完成后用信号把结果图传回主线程更新界面整个交互就不会卡顿。5. 端到端跑通后的实测复盘六个文档里不会写的坑5.1 数据划分不当导致“虚高指标”我第一次训练时按图片随机划分训练集和验证集验证集mAP0.5高达0.94当时还挺高兴。结果把模型拿到全新拍摄的一批照片上测试mAP直接掉到0.81。原因很典型数据里有同一段视频连续抽帧出来的图片内容高度相似随机划分会把这些相似图片分到两侧验证集相当于“开卷考试”。正确的做法是按拍摄批次划分把同一时间、同一场景拍出来的图都放进同一侧。我重新划分后又训了一轮验证集指标和真实场景表现就基本一致了。这点很值得留意不然你的模型上线前自我感觉良好上了却翻车。5.2 类别不平衡的真实影响我的数据集里正常花生框数大约是霉变花生的两倍。训练时模型会更偏向学到“正常花生”的特征导致霉变类recall偏低。这在大批量分选时是不能接受的漏判一粒霉变种子的代价远高于多标一粒正常。解决思路有两个方向。一是数据层尽量多采集霉变样本或者对霉变类的图片做更多离线增强。二是训练配置Ultralytics提供了cls loss的权重调整能力给霉变类更高的分类权重让模型更重视这个少数类。实际使用中两个方向都做效果最明显。5.3 光照偏色导致的漏检训练数据大多在室内LED灯下拍摄模型在室内测试效果很好。后来拿到日光充足的窗边一试霉变斑块的颜色表现跟室内差不少漏检率明显上升。这就是典型的颜色过拟合。解决办法是训练时把hsv_h和hsv_s的增强幅度加大让模型见到更多色彩变化。我最后把hsv_h调到了0.02hsv_s调到了0.6重新训练之后对自然光下的图片明显更稳。5.4 conf阈值到底调多少置信度阈值conf直接影响precision和recall的取舍。调低了recall上升但会把一些正常花生误判成霉变调高了precision上升但霉变粒容易漏掉。对花生分选这个场景我的建议是comfort区间在0.4到0.5之间。宁可多框出几粒让质检员复核也不要放过真正的霉变粒。在代码里我用conf0.45作为默认值并且界面上保留滑块让用户按需调整。5.5 界面卡顿与打包资源问题除了解析卡顿还有几个实际操作层面的细节模型初始化很慢一定要在程序启动时加载一次不要每次检测都new一个YOLO对象。PyTorch模型默认用GPU推理如果没有GPU要显式指定cpu否则启动时就报错。PyInstaller打包时best.pt要作为数据文件打进去并且代码里不要用绝对路径用os.path.join处理成相对路径换个电脑才不会找不到模型。5.6 用ONNX加速部署如果生产环境的机器没装PyTorch或者想进一步提升推理速度可以把模型导出成ONNX格式from ultralytics import YOLO model YOLO(best.pt) model.export(formatonnx, imgsz640)导出后用onnxruntime加载推理代码结构变化不大但推理速度和内存占用都会比直接跑PyTorch模型更优。我在实测中把单帧推理时间从约40毫秒压到了约15毫秒这对摄像头实时检测很有帮助。最后再说点个人体会。这个项目走完一遍最花时间的不是模型训练而是数据整理和踩坑调试。凡是准备自己复现的人我建议把三分之二的精力放在数据质量和划分逻辑上。数据备到位了YOLOv11的配置反而只是几个参数的事。这套流程换到其他农产品检测比如玉米霉变、咖啡豆霉变思路完全一样核心资产始终是那套干净的数据整理方法和工程落地习惯。