news 2026/10/11 12:19:59

YOLOv5果蔬识别实战:从数据清洗到产线PLC控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv5果蔬识别实战:从数据清洗到产线PLC控制

简介:本资源是一套完整的YOLOv5果蔬识别系统实战项目,面向计算机专业本科生毕业设计、课程设计及深度学习初学者,解决目标检测领域中水果蔬菜类别识别与定位的典型任务。压缩包共56个文件,含14个Python训练与推理脚本(如train_cnn.py、window_realtime.py)、27张标注图像(jpg/jpeg/png)、6个XML标注文件、6个txt说明与日志、2个H5模型权重及README文档等,覆盖数据预处理、模型训练、实时检测、结果可视化全流程,94.07MB体量适配本地调试与教学部署。已有54人学习下载,资源经导师审核通过并完成全链路验证,附带详细教程、测试记录、热力图与训练过程日志,还包含jpeg2jpg转换、异常图像剔除、摄像头调用等实用工具脚本,目录结构分层清晰,便于按模块理解YOLOv5工程化落地细节。

1. 为什么用 YOLOv5 做果蔬识别,不是“跑个 demo 就完事”——它真能扛住菜市场、冷链仓、分拣线的实拍干扰

你手头有一筐刚从田里摘的番茄,表皮带露水、有擦伤、叠在一起;或者一段冷库传送带视频,光照不均、雾气反光、果品堆叠遮挡严重;又或者要部署到边缘盒子上,要求 30FPS 且 CPU 占用低于 45%。这时候拿 ImageNet 预训练模型微调、用 OpenCV 简单阈值分割、甚至套个 Faster R-CNN,大概率在第三帧就漏检青椒、把烂桃子判成苹果、卡在 ROI Align 上掉帧。YOLOv5 果蔬识别系统不是“又一个目标检测 demo”,它是把YOLOv5s/v5m 的轻量结构、Mosaic+MixUp 数据增强对小目标和遮挡的鲁棒性、以及针对果蔬类高频缺陷(裂纹、斑点、萎蔫)定制的标签体系,打包成可落地的数据集 + 可复现源码 + 可调试指南的完整链路。适合农业 AI 初创团队做 MVP 验证、智慧农批市场做分拣计数、高校课程设计需避开“猫狗分类”内卷、以及嵌入式工程师想验证 RK3399/NanoPi R5S 上的实时推理性能。它不承诺“一键商用”,但保证:你按指南走完三步(数据清洗→配置改写→训练监控),能在 2 小时内跑通本地 demo,并看到 mAP@0.5 提升 8.2% 的真实收益——这背后是 37 类常见果蔬(含易混淆项如红心火龙果 vs 白心火龙果、青柚 vs 柚子)、12678 张实拍图(非网络爬虫图)、以及所有标注框经人工复核的硬成本。


2. 从零构建果蔬数据集:不是“下载+解压”,而是用 Python 脚本筛掉 43% 的无效样本

YOLOv5 对数据质量极度敏感:一张模糊图可能让整个 batch 的梯度爆炸;一个错标框(比如把香蕉柄标成独立类别)会让模型学出错误先验。本项目提供的数据集虽已清洗,但你后续要增补自家果园或合作商的图片,必须掌握这套筛选逻辑。核心不是靠肉眼,而是用OpenCV + PIL + labelImg 标注协议一致性校验脚本自动过滤。

2.1 用cv2.imread+np.std筛选低对比度图像

果蔬常因反光、阴影导致局部过曝或欠曝,单纯看直方图不准,需计算整图灰度标准差。低于阈值的图直接剔除——这类图在 YOLOv5 的 Focus 层会丢失纹理信息。

import cv2 import numpy as np import os def filter_low_contrast(img_path, std_threshold=25): img = cv2.imread(img_path) if img is None: return False gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) std_val = np.std(gray) return std_val >= std_threshold # 批量处理 dataset_dir = "datasets/fruit_veg/images" valid_images = [] for img_file in os.listdir(dataset_dir): if img_file.lower().endswith(('.jpg', '.jpeg', '.png')): full_path = os.path.join(dataset_dir, img_file) if filter_low_contrast(full_path): valid_images.append(img_file) print(f"原始 {len(os.listdir(dataset_dir))} 张,保留 {len(valid_images)} 张")

参数说明:std_threshold=25是经 12 类果蔬实测得出的临界值。番茄表皮反光强,标准差常达 45+;而冬瓜在阴天拍摄可能仅 18。若你的场景多为阴棚拍摄,建议下调至 20;若为强光大棚,可上调至 30。该阈值不依赖绝对亮度,只反映像素分布离散程度——这正是 YOLOv5 中 CSP 结构提取特征的基础。

2.2 用PIL.Image检测 JPEG 伪影与裁剪残留

网络下载图常含网页水印、边框、文字遮挡,这些区域在 Mosaic 增强后会污染 anchor 匹配。我们不靠 OCR,而是检测高频噪声能量:

from PIL import Image, ImageStat, ImageFilter import numpy as np def has_jpeg_artifact(img_path, artifact_threshold=0.18): try: img = Image.open(img_path).convert('L') # 计算高频分量能量(用拉普拉斯滤波器近似) laplacian = img.filter(ImageFilter.FIND_EDGES) stat = ImageStat.Stat(laplacian) high_freq_energy = np.mean(stat.mean) # 同时检查是否被裁剪(边缘像素均值异常高) edge_pixels = np.array(img)[0:5, :].flatten() # 顶部5行 edge_mean = np.mean(edge_pixels) return (high_freq_energy > artifact_threshold) or (edge_mean > 220) except: return True # 读取失败视为脏数据 # 运行示例 bad_imgs = [f for f in valid_images if has_jpeg_artifact(os.path.join(dataset_dir, f))] print(f"检测出 {len(bad_imgs)} 张含 JPEG 伪影或裁剪残留图")

为什么不用传统去噪?因为 YOLOv5 的 Neck 层(SPPF)本身具备一定抗噪能力,过度去噪反而抹平果蔬表皮纹理(如草莓籽、芒果纤维)。此脚本只做“判别”,不“修复”——保留原始数据保真度,把问题拦截在训练前。

2.3 标签一致性校验:防止.txt文件与图像尺寸错位

YOLO 格式要求.txt中归一化坐标x_center, y_center, width, height必须在[0,1]内,且width/height ≤ 0.95(避免超大框)。但人工标注常出错,尤其当图像被缩放后未重导出标签:

def validate_labels(img_path, label_path): img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path, 'r') as f: lines = f.readlines() invalid_lines = [] for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: invalid_lines.append(f"第{i+1}行字段数错误:{len(parts)} ≠ 5") continue try: x, y, bw, bh = map(float, parts[1:]) if not (0 <= x <= 1 and 0 <= y <= 1 and 0 < bw <= 0.95 and 0 < bh <= 0.95): invalid_lines.append(f"第{i+1}行坐标越界:x={x:.3f}, y={y:.3f}, w={bw:.3f}, h={bh:.3f}") except ValueError: invalid_lines.append(f"第{i+1}行数值解析失败:{parts[1:]}") return invalid_lines # 批量校验 label_dir = "datasets/fruit_veg/labels" for img_file in valid_images: label_file = os.path.join(label_dir, img_file.replace('.jpg', '.txt').replace('.png', '.txt')) if os.path.exists(label_file): errors = validate_labels(os.path.join(dataset_dir, img_file), label_file) if errors: print(f"{img_file} 标签错误:{errors}")

关键细节:bw ≤ 0.95是硬约束。YOLOv5 的 anchor 设计基于 COCO 统计,果蔬中“整筐苹果”等大目标若占图比超 0.95,会导致compute_loss中iou_loss计算溢出,训练 loss 突然飙升。项目原始数据集中有 7 张此类图,已按此规则剔除并重采样。


3. YOLOv5 源码级改造:不只是改yaml,而是动train.py和val.py的三个关键钩子

官方 YOLOv5 代码开箱即用,但果蔬识别有三大特异性需求:小目标密集(葡萄粒)、类间相似度高(彩椒 vs 红椒)、部署端内存受限。直接python train.py --data data/fruit.yaml --weights yolov5s.pt会掉进坑里。必须修改源码中三个位置,否则 mAP 卡在 0.62 上不去。

3.1 修改models/yolo.py中的Detect类:增加小目标专用 head

YOLOv5 默认的 Detect head 在 P3 层(stride=8)输出最小检测尺度,但葡萄、蓝莓直径常小于 20px,在 640×640 输入下对应 P3 特征图仅 2.5px,无法有效定位。需在 P2 层(stride=4)加一个 head:

# models/yolo.py 第 127 行附近,修改 Detect.__init__ class Detect(nn.Module): def __init__(self, nc=80, anchors=(), ch=()): # detection layer super().__init__() self.nc = nc # number of classes self.no = nc + 5 # number of outputs per anchor self.nl = len(anchors) # number of detection layers self.na = len(anchors[0]) // 2 # number of anchors self.grid = [torch.zeros(1)] * self.nl # init grid self.anchor_grid = [torch.zeros(1)] * self.nl # init anchor grid self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape: nl x na x 2 # 新增:P2 层 head(stride=4),用于小目标 if len(ch) > 3: # 确保输入通道数足够(P2, P3, P4, P5) self.m_p2 = nn.Conv2d(ch[0], self.no * self.na, 1) # P2: ch[0] usually 128 self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch[1:]) # P3-P5

为什么只加 P2 不加 P1?P1(stride=2)特征图太大(320×320),显存暴涨且无必要——葡萄粒在 640 输入下最小约 15px,P2 特征图分辨率为 160×160,已足够定位。实测加 P2 后,葡萄检测 recall 提升 12.7%,但 GPU 显存仅增 180MB(RTX 3090)。

3.2 修改train.py中的train函数:注入果蔬专属损失权重

默认 BCELoss 对所有类别一视同仁,但果蔬中“腐烂”、“虫蛀”等缺陷类样本极少(仅占 3.2%),模型倾向忽略。需在compute_loss前动态加权:

# train.py 第 420 行附近,在 loss += ... 之前插入 if opt.data.endswith('fruit.yaml'): # 仅对果蔬数据集启用 # 根据类别频率反向加权(频率越低,权重越高) class_weights = torch.tensor([1.0, 1.0, 1.0, # 正常苹果、香蕉、番茄 3.2, 2.8, 4.1, # 腐烂苹果、虫蛀香蕉、霉变番茄 1.5, 1.5, 1.5]).to(device) # 其他正常类保持 1.0 loss *= class_weights[tcls] # tcls 是 target class index

权重怎么来的?项目数据集中统计了每类出现频次,用1 / (freq / total)归一化后取整。例如腐烂苹果共 127 张,总样本 12678,则权重 = 12678/127 ≈ 99.8 → 缩放到 3.2(避免梯度爆炸)。这个值比torch.nn.CrossEntropyLoss(weight=...)更灵活,可随 epoch 动态调整。

3.3 修改val.py中的process_batch:自定义 AP 计算逻辑

官方 mAP 计算用 COCO 标准(IoU=0.5:0.95),但果蔬分拣只需判定“是否在框内”,IoU=0.5 足够。且需单独统计“易混淆类对”的 precision:

# val.py 第 180 行,替换原 process_batch 函数 def process_batch(detections, labels, iouv): """ detections: tensor[N, 6] (x1,y1,x2,y2,conf,cls) labels: tensor[M, 5] (cls, x, y, w, h) in normalized xywh """ correct = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device) detected = [] # 记录已匹配的 label idx # 重点:对彩椒/红椒/黄椒,放宽 IoU 阈值到 0.6(因颜色渐变边界模糊) iou_thres = iouv.clone() iou_thres[0] = 0.6 # 第一个阈值设为 0.6 for si, pred in enumerate(detections): pred_cls = pred[-1].long() pred_box = pred[:4] # ... 原有匹配逻辑 ... # 新增:返回混淆矩阵片段 if len(labels) > 0: cls_matrix = torch.zeros(37, 37) # 37类果蔬 for i, l in enumerate(labels): for j, d in enumerate(detections): if correct[j, 0] and d[-1].long() == l[0].long(): cls_matrix[l[0].long(), d[-1].long()] += 1 return correct, cls_matrix return correct, None

为什么改这里?因为val.py的输出直接影响results.txt中的Class-wise AP。原始代码只输出总 AP,而果蔬项目需知道“把青椒标成彩椒”的错误率——这决定是否要加 HSV 颜色空间增强。该修改让val.py输出confusion_matrix_37x37.npy,供后续分析。


4. 避坑:YOLOv5 果蔬识别训练中 5 个血泪经验换来的翻车点

YOLOv5 官方文档没写、GitHub Issues 里散落、但每个都足以让你浪费 12 小时以上。以下是项目实测中踩出的 5 个典型坑,按“现象→原因→解决”结构整理,拒绝玄学。

4.1 现象:训练 loss 从 2.1 突然跳到 15.7,GPU 显存瞬间占满

原因:hyp.scratch-low.yaml中warmup_epochs: 3与batch_size: 64冲突。Warmup 阶段学习率线性上升,但 batch_size 过大导致梯度累积爆炸,尤其在 P2 head 加入后。
解决:将batch_size从 64 降至 32,并同步修改warmup_epochs: 5(确保 warmup 总 step 数 ≥ 1000)。或改用cosine学习率调度器(--lr_scheduler cosine),它对 batch_size 敏感度更低。

4.2 现象:验证时precision高达 0.92,但recall仅 0.41,大量小目标漏检

原因:conf_thres默认 0.001 过低,导致 NMS 前产生海量低置信度框,NMS 后只剩高分框——小目标置信度天然偏低,全被滤掉。
解决:在val.py中将conf_thres从 0.001 改为 0.25,并在test.py推理时用--conf 0.25 --iou 0.45。实测 recall 提升至 0.73,precision 仅降 0.03。

4.3 现象:同一张图,CPU 推理结果与 GPU 推理结果 bbox 坐标偏差 ±3px

原因:PyTorch 1.10+ 中torch.nn.functional.interpolate在 CPU/GPU 上插值算法不同(CPU 用 bilinear,GPU 用 nearest),导致 PAF(Path Aggregation Feature)层输出错位。
解决:强制统一插值模式。在models/common.py的Upsample类中,将mode='nearest'改为mode='bilinear',并添加align_corners=True。注意:此举会使 GPU 推理速度降 8%,但保证跨平台一致性。

4.4 现象:训练 200 epoch 后 mAP@0.5 不再提升,loss 平稳在 0.85

原因:augmentations.py中Mosaic默认概率 0.5,但果蔬图像中“整筐堆放”场景占比 31%,Mosaic 会破坏这种空间关系,让模型误学“单个水果必孤立”。
解决:在train.py中动态调整 mosaic_prob:

mosaic_prob = 0.5 * (1 - epoch / epochs) # 从 0.5 线性衰减到 0 if random.random() < mosaic_prob: # 执行 mosaic

实测最终 mAP@0.5 提升 2.3%,且收敛更快。

4.5 现象:导出 ONNX 模型后,OpenCV DNN 模块加载报错Unsupported op 'Resize'

原因:YOLOv5 导出 ONNX 时默认用opset=12,但 OpenCV 4.5.5 仅支持opset=11且不兼容 Resize 算子。
解决:导出时指定--opset 11,并在export.py中注释掉dynamic_axes相关行(ONNX 11 不支持动态 batch)。若必须动态 batch,改用 TensorRT 推理而非 OpenCV。


5. 指南落地:用detect.py做产线级推理,不是截图测试,而是写入 PLC 控制信号

项目提供的detect.py是起点,但真正接入分拣线需把它变成工业级模块:能接收相机流、输出结构化 JSON、触发 IO 信号。这不是加几行cv2.imshow能解决的,得动底层通信协议。

5.1 改造detect.py为 RTSP 流处理器:用cv2.VideoCapture替代--source

产线相机多为海康/大华 IPC,提供 RTSP 地址。原detect.py的--source参数只支持文件路径,需重写数据加载逻辑:

# detect.py 第 80 行,替换 source 加载部分 if source.startswith('rtsp://'): cap = cv2.VideoCapture(source) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 关闭缓冲,降低延迟 assert cap.isOpened(), f'Failed to open RTSP stream: {source}' while True: ret, im0 = cap.read() if not ret: print("RTSP stream disconnected, retrying...") cap.release() time.sleep(1) cap = cv2.VideoCapture(source) continue # 后续推理逻辑不变... else: # 原有文件/目录处理逻辑

关键参数:CAP_PROP_BUFFERSIZE=1强制单帧缓冲,避免 IPC 自动缓存 3~5 帧导致控制延迟。实测端到端延迟从 420ms 降至 180ms(RTX 3060 + 海康 DS-2CD3T47G2-L)。

5.2 输出 JSON 结构:包含时间戳、置信度、物理尺寸换算

PLC 不认识 bbox 像素坐标,需要毫米级定位。假设相机已标定,焦距 f=12mm,传感器尺寸 1/2.8"(宽 5.37mm),工作距离 800mm:

# detect.py 推理循环内,bbox 处理后添加 def pixel_to_mm(x1, y1, x2, y2, f=12, sensor_w=5.37, distance=800): # 计算像素尺寸(mm/pixel) pixel_size_mm = sensor_w / 1920 # 假设 1080p 宽 1920px # 换算物理尺寸(实际物体长宽) w_mm = (x2 - x1) * pixel_size_mm * distance / f h_mm = (y2 - y1) * pixel_size_mm * distance / f return w_mm, h_mm # 输出 JSON result = { "timestamp": int(time.time() * 1000), "objects": [ { "class": names[int(cls)], "confidence": float(conf), "bbox_px": [int(x1), int(y1), int(x2), int(y2)], "bbox_mm": list(pixel_to_mm(x1, y1, x2, y2)), "center_mm": [int((x1+x2)/2 * pixel_size_mm * distance / f), int((y1+y2)/2 * pixel_size_mm * distance / f)] } for *xyxy, conf, cls in det ] } print(json.dumps(result))

为什么用print(json)而非文件写入?因为 PLC 通常通过串口或 TCP socket 实时读取 stdout。Python 进程启动后,PLC 每 200ms 发送GET_RESULT命令,Python 直接printJSON 即可响应,无需文件 I/O 开销。

5.3 与 PLC 通信:用 Modbus TCP 触发分拣气缸

最简方案是用pymodbus库写 Holding Register,PLC 侧监听地址 40001:

from pymodbus.client import ModbusTcpClient client = ModbusTcpClient('192.168.1.100', port=502) # PLC IP client.connect() # 检测到烂苹果(class_id=2)且置信度>0.85,触发气缸 if names[int(cls)] == 'rotten_apple' and conf > 0.85: client.write_register(40001, 1) # 地址40001写入1 time.sleep(0.1) # 保持100ms client.write_register(40001, 0) # 复位

安全机制:实际部署必须加互锁。我们在detect.py开头加入心跳检测:

# 每5秒向 PLC 发送心跳(地址40002) heartbeat = 0 while True: heartbeat = (heartbeat + 1) % 256 client.write_register(40002, heartbeat) time.sleep(5)

PLC 侧若 10 秒未收到心跳,自动停机——这是产线安全底线,不是可选项。

我带过的三个农业 AI 项目,全部栽在“以为 detect.py 跑通就算交付”上。真正上线那天,不是看 mAP 多高,而是看第一筐草莓过线时,气缸是否在 180ms 内弹出、JSON 时间戳是否与 PLC 时钟误差 <5ms、连续 72 小时无丢帧。这些细节不在 GitHub README 里,但在每次凌晨三点重启 IPC 的日志里。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 12:18:36

银行客户产品认购预测:行为序列+二部图嵌入+ROI排序

简介&#xff1a;本资源是一套完整的银行客户金融产品认购预测实战项目&#xff0c;面向Python数据科学初学者与机器学习实践者&#xff0c;聚焦银行业务场景中的客户行为建模与营销响应预测问题。项目涵盖数据预处理、特征工程、多模型训练&#xff08;含树模型与集成方法&…

作者头像 李华
网站建设 2026/10/11 12:16:27

云迁移回归测试标准化:从假绿到可信的测试体系搭建指南

说出来有点丢人&#xff0c;我负责的第一个云迁移项目&#xff0c;上线前回归测试“全绿”&#xff0c;业务负责人专门在周会上表扬了测试团队。结果上线第二天&#xff0c;订单模块超时率直接飙到15%&#xff0c;数据库连接池被打满&#xff0c;最后靠回滚才稳住局面。复盘的时…

作者头像 李华
网站建设 2026/10/11 12:14:25

AI Agent主导自动化测试:从脚本生成到智能维护实战

1. 从脚本时代到Agent时代&#xff1a;自动化测试为什么突然聊起“主导权”1.1 自动化测试十八年&#xff1a;从录制回放到AI辅助先聊点背景。我最早接触自动化测试时&#xff0c;用的还是录制回放那一套。页面操作录一遍&#xff0c;脚本保存下来&#xff0c;回归时跑一遍&…

作者头像 李华
网站建设 2026/10/11 12:14:10

综科智控IO模块Modbus TCP连接故障排查与适配指南

1. 项目概述&#xff1a;为什么一个IO模块的TCP连接会卡住工程师一整天&#xff1f;“综科智控以太网IO模块Modbus TCP协议适配与连接要点”——这个标题看起来平平无奇&#xff0c;像是一份产品说明书里的小节标题。但如果你真在产线调试现场盯过三小时LED灯不亮、PLC读不到寄…

作者头像 李华
网站建设 2026/10/11 12:13:22

PS5工具链整合实战:从环境搭建到自动化验证全流程解析

做 PS5 工具链整合这一块&#xff0c;我踩过的坑不算少。今天想借着“AnyPS5”这个项目代号&#xff0c;把这段时间沉淀下来的经验完整梳理一遍。它不是某个商店里能下载到的一键软件&#xff0c;而是我基于官方开发接入框架&#xff0c;自己搭建的一整套跨平台验证与联调环境。…

作者头像 李华