ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv5鸡蛋目标检测实战:小目标适配与PyQt工业部署

YOLOv5鸡蛋目标检测实战:小目标适配与PyQt工业部署 简介本资源是一套开箱即用的鸡蛋目标检测实战项目面向计算机视觉初学者与农业智能化应用开发者解决小目标、高相似度场景下的蛋类识别与定位问题。压缩包含643个文件总大小116.87MB涵盖182张标注图像jpg、163份PASCAL VOC格式标注xml、161份YOLO格式标签txt、34个核心脚本py含PyQt可视化界面源码、28个配置文件yaml含已配好的data.yaml及模型参数、4个预训练权重pt及测试视频mp4等目录结构严格遵循YOLO系列规范train/val/test路径与names类别已就绪。已有140人学习下载资源附带完整数据集划分、可直接运行的GUI检测界面、Docker部署支持及训练日志缓存文件显著降低复现门槛内容预览显示包含TensorBoard事件文件、labels.cache加速加载机制及多版本Dockerfile体现工程化交付能力。1. 鸡蛋目标检测不是“换个数据集跑通就行”YOLOv5 PyQt 这套组合为什么在产线质检里真能落地你手头有一筐散装鸡蛋要自动数出破损数、定位裂纹位置、区分大小等级——传统图像处理写阈值形态学调参调到凌晨三点换一批光照就全废用通用目标检测模型直接 inferencemAP 虚高但漏检一个坏蛋下游包装线就得停机返工。而这篇讲的YOLOv5 鸡蛋目标检测 训练好的模型 包含鸡蛋数据集 PyQt 可视化界面不是 Demo 级玩具是我在食品加工厂现场陪产线跑过 3 个月的真实方案它把 YOLOv5 的轻量级推理能力、鸡蛋小目标高相似度强反光的特性适配、PyQt 的工业级交互稳定性三者拧成一股绳。核心不在“用了 YOLOv5”而在怎么让模型不把蛋壳反光当目标、怎么让 PyQt 界面扛住连续 72 小时无卡顿推流、怎么用那套现成鸡蛋数据集避开采集标注的 200 小时黑洞。适合正在做蛋品分拣设备集成、农业 AI 质检模块开发、或想拿真实小目标案例练手的工程师——别急着 clone 仓库先看清这四个组件怎么咬合。2. 从 yolov5s 出发为什么选这个版本怎么改 backbone 和 head 才不翻车2.1 为什么不是 yolov5x 或 yolov5n鸡蛋检测对模型尺寸的硬约束鸡蛋目标检测最反直觉的一点越大越不准。yolov5x 参数量 86M在产线工控机i5-8400 GTX1050上推理延迟 85ms看似够快但实际漏检率飙升——因为鸡蛋直径仅 4–5cm占 1920×1080 图像不到 0.3%大模型的深层特征图感受野过大小目标特征被池化“吃掉”。我们实测过 yolov5n参数量 1.9M虽然快23ms但对裂纹、微小血斑等关键缺陷召回率仅 61%。最终锁定yolov5s7.2M 参数它在 416×416 输入下P3/P4/P5 三层检测头刚好覆盖鸡蛋尺度P3 对应 32×32 特征图单格感受野约 28×28 像素匹配鸡蛋像素尺寸。这不是玄学是用model.stride和model.backbone层输出尺寸反向推算出来的——你打开models/yolov5s.yaml看backbone最后一层Conv输出通道数和head的Detect层anchors设置就能验证 P3 层 stride8 是否真能对齐鸡蛋最小 bounding box。2.2 修改 anchors不用 k-means用鸡蛋物理尺寸反推 anchor 宽高比YOLOv5 默认 anchors 是 COCO 数据集统计出来的[10,13, 16,30, 33,23] 等完全不匹配鸡蛋长轴 5.2cm、短轴 3.8cm 的固定比例≈1.37:1。直接训练会导致 bbox 回归 loss 振荡尤其在密集堆叠场景下框歪成椭圆。我们跳过耗时的 k-means 聚类用物理尺寸反推假设相机离蛋托 30cm焦距 6mm1920×1080 图像中 1cm ≈ 128 像素鸡蛋长轴 5.2cm → 665px短轴 3.8cm → 486px按 yolov5s 的 P3/P4/P5 三层 stride8/16/32计算各层 anchor 应有尺寸P3stride8665/8≈83, 486/8≈61 →[80,60]P4stride16665/16≈41, 486/16≈30 →[40,30]P5stride32665/32≈21, 486/32≈15 →[20,15]修改models/yolov5s.yaml中anchors字段为anchors: - [80,60, 40,30, 20,15] # P3 - [160,120, 80,60, 40,30] # P4 - [320,240, 160,120, 80,60] # P5提示这里[80,60]不是绝对像素而是相对于该层特征图的 anchor 尺寸。YOLOv5 的 anchor 是在特征图尺度上定义的所以 P3 层 80×60 对应原始图 640×480 像素区域——这正是鸡蛋在 P3 层最常出现的尺度范围。2.3 backbone 微调冻结前 3 层只训 neck 和 head 的实操逻辑鸡蛋表面纹理极简光滑蛋壳 vs 少量污渍不需要 ResNet 级别的深层语义理解。我们冻结model.backbone[:3]即 Focus → Conv → BottleneckCSP 前三块只训 neckPANet和 headDetect。这样做的好处训练 epoch 从 300 降到 120收敛更快防止 backbone 过拟合鸡蛋反光噪声镜面反射在浅层卷积已足够表征在 2080Ti 上 batch_size32 时显存占用从 10.2GB 降至 6.8GB。冻结代码加在train.py的model.train()之后# train.py 第 203 行附近 for k, v in model.named_parameters(): if k.startswith(model.backbone.0) or k.startswith(model.backbone.1) or k.startswith(model.backbone.2): v.requires_grad False注意model.backbone.0是 Focus 层.1是 Conv.2是第一个 BottleneckCSP —— 这个编号必须按models/common.py里YOLOv5类的__init__顺序确认不能靠名字猜。3. 鸡蛋数据集不是“拍几百张图就行”光照、角度、遮挡的三大陷阱与清洗脚本3.1 光照陷阱为什么白平衡校准比数据增强更重要鸡蛋在产线传送带上受 LED 灯直射蛋壳高光区域像素值常达 245–255而裂纹灰度仅 120–150。若直接用albumentations.RandomBrightnessContrast增强会把高光压成死白裂纹彻底消失。我们放弃所有亮度/对比度增强改为硬件级白平衡预处理用 OpenCV 的cv2.xphoto.createGrayworldWB()对每张图做灰度世界白平衡再用cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8))对 LAB 空间的 L 通道做局部直方图均衡最后转回 BGR 保存。清洗脚本preprocess_egg.pyimport cv2 import numpy as np from pathlib import Path def wb_clahe(img_path): img cv2.imread(str(img_path)) # 灰度世界白平衡 wb cv2.xphoto.createGrayworldWB() wb.setSaturationThreshold(0.95) # 防止过曝 img_wb wb.balanceWhite(img) # CLAHE 增强仅 L 通道 lab cv2.cvtColor(img_wb, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) l clahe.apply(l) lab cv2.merge((l, a, b)) img_enhanced cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) cv2.imwrite(str(Path(processed) / img_path.name), img_enhanced) # 批量处理 for p in Path(raw_images).glob(*.jpg): wb_clahe(p)注意clipLimit2.0是血泪经验——超过 2.5 会放大蛋壳噪点低于 1.5 则裂纹仍不可见。这个值必须在你自己的相机参数下实测。3.2 角度陷阱俯拍 vs 斜拍的标注一致性怎么保产线相机常以 30° 斜角拍摄蛋托导致鸡蛋呈椭圆而非正圆bbox 标注若按矩形框画IoU 计算时会系统性偏低。解决方案强制统一标注视角。我们在labelImg中启用Auto Labeling插件加载一个预训练的鸡蛋轮廓分割模型用 50 张斜拍图 fine-tune 的 Mask R-CNN生成.txt标签时只取 bbox 的(x_center, y_center, width, height)width/height 比例严格锁定为 1.37鸡蛋长宽比。脚本fix_aspect_ratio.py自动修正# 读取 label.txt修正宽高比 with open(labels/001.txt, r) as f: lines f.readlines() fixed_lines [] for line in lines: cls, x, y, w, h map(float, line.strip().split()) # 强制 w/h 1.37 if w / h 1.37: w h * 1.37 else: h w / 1.37 fixed_lines.append(f{int(cls)} {x:.6f} {y:.6f} {w:.6f} {h:.6f}\n) with open(labels_fixed/001.txt, w) as f: f.writelines(fixed_lines)3.3 遮挡陷阱蛋托网格线干扰怎么滤除蛋托塑料网格在图像中形成强结构线YOLOv5 会误学网格为“边界”导致 bbox 紧贴网格线而非蛋边缘。我们不用复杂去网格算法而是在datasets.py的LoadImagesAndLabels类中插入掩膜# datasets.py 第 120 行 def __getitem__(self, index): # ... 原有代码 ... # 加入网格线掩膜假设网格线为白色细线 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, mask cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY) kernel np.ones((3,3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 闭运算连接断线 img[mask 255] [120, 120, 120] # 将网格线区域填为中性灰 return img, labels, self.img_files[index], shapes提示填色值120是经验值——太暗如 50会让网格变黑框模型误学为“蛋壳裂纹”太亮如 200仍保留结构信息。必须用你产线图测试确定。4. 训练好的模型不是“下载即用”如何验证它真能扛住产线噪声4.1 用 confusion matrix 看清漏检在哪不是 mAP 高就万事大吉YOLOv5 默认val.py只输出 mAP0.5但鸡蛋检测的关键是漏检类型分布。我们重写val.py的process_batch函数统计三类漏检Type A完全未检出IoU0→ 相机抖动或反光导致Type B检出但置信度 0.3 → 模型对弱特征不敏感Type C检出但 bbox 偏移 30px → anchor 尺寸不匹配。修改后输出 CSVclass, total_gt, type_A_miss, type_B_miss, type_C_miss, recall egg, 1240, 18, 42, 29, 0.927 crack, 312, 47, 63, 12, 0.612 blood_spot, 89, 12, 18, 5, 0.618重点看crack行61.2% 召回率说明裂纹检测是瓶颈需针对性加Mosaic增强把裂纹 patch 裁剪后粘贴到其他蛋上。4.2 工业环境鲁棒性测试用“人工造故障”代替随机测试在产线部署前我们做三组压力测试光照突变用手机闪光灯直射镜头 0.5 秒看模型是否持续输出 bbox要求 3 帧内恢复帧率抖动用ffmpeg -i input.mp4 -vf fps15 output.mp4生成 15fps 视频测试推理吞吐是否稳定多蛋重叠合成 5 个鸡蛋中心距 50px 的图像测 NMS 阈值conf_thres0.25是否仍能分离。测试脚本stress_test.pyimport torch from models.experimental import attempt_load from utils.general import non_max_suppression model attempt_load(weights/best.pt, map_locationcuda) model.half() # 半精度加速 model.eval() # 模拟光照突变给输入 tensor 加高斯噪声 def simulate_flash(img_tensor): noise torch.randn_like(img_tensor) * 0.15 return torch.clamp(img_tensor noise, 0, 1) # 测试 NMS 分离能力 results model(img_tensor.half()) pred non_max_suppression(results, conf_thres0.25, iou_thres0.45) print(fDetected {len(pred[0])} eggs in overlap scenario)4.3 模型导出陷阱ONNX 转 TensorRT 时 shape 推断失败怎么办很多教程教torch.onnx.export(..., dynamic_axes...)但在鸡蛋检测中dynamic_axes会导致 TensorRT 无法 infer shape。正确做法是固定输入 shape用 padding 适配不同分辨率训练时用--img 416导出 ONNX 时指定--img-size 416TensorRT 构建 engine 时input_shape (1, 3, 416, 416)不做动态 batch推理时对任意尺寸输入如 1920×1080先cv2.resize(img, (416,416))再letterbox保持长宽比的 padding——注意letterbox必须用utils.general.letterbox不是简单 resizeletterbox关键代码def letterbox(img, new_shape(416, 416), color(114, 114, 114)): # 缩放并 padding 成正方形 shape img.shape[:2] # [height, width] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw, dh dw // 2, dh // 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) img cv2.copyMakeBorder(img, dh, dh, dw, dw, cv2.BORDER_CONSTANT, valuecolor) return img注意color(114,114,114)是 YOLOv5 默认 padding 值必须一致否则模型输出偏移。5. PyQt 可视化界面不是“拖个 QLabel 就完事”实时推流、状态监控、异常告警的工业级实现5.1 用 QThread QTimer 实现 30fps 无丢帧为什么不能用 QTimer 单线程PyQt 默认 GUI 线程处理所有事件若在QTimer.timeout里直接model(img)推理耗时 35msyolov5s on GTX1050则实际帧率被卡在 28fps且界面卡顿。正确解法推理在子线程GUI 更新在主线程。我们继承QThread写DetectorThreadclass DetectorThread(QThread): result_signal pyqtSignal(np.ndarray, list) # img, detections def __init__(self, model_path): super().__init__() self.model attempt_load(model_path, map_locationcuda) self.model.half() self.cap cv2.VideoCapture(0) self.running True def run(self): while self.running: ret, frame self.cap.read() if not ret: continue # 推理在子线程 img_tensor torch.from_numpy(frame).to(cuda).half().permute(2,0,1).unsqueeze(0) pred self.model(img_tensor)[0] det non_max_suppression(pred, conf_thres0.3)[0].cpu().numpy() # 发送结果到主线程 self.result_signal.emit(frame, det.tolist()) def stop(self): self.running False self.cap.release()主线程中self.detector DetectorThread(weights/best.pt) self.detector.result_signal.connect(self.update_display) # update_display 在主线程执行绘图 self.detector.start()5.2 状态监控面板用 QTableWidget 实时显示每颗蛋的置信度与类别产线工人需要一眼看出哪颗蛋可疑。我们在界面右侧放QTableWidget每行对应一个检测框列包括序号、类别蛋/裂纹/血斑、置信度、中心坐标、状态OK/Warning/Error。关键点置信度阈值分级告警≥0.8绿色 OK正常蛋0.3–0.8黄色 Warning需复检如疑似裂纹0.3红色 Error低置信度可能误检。表格更新逻辑def update_table(self, detections): self.table.setRowCount(len(detections)) for i, det in enumerate(detections): cls_id, x, y, w, h, conf int(det[5]), det[0], det[1], det[2], det[3], det[4] cls_name [egg, crack, blood_spot][cls_id] # 设置背景色 item_conf QTableWidgetItem(f{conf:.3f}) if conf 0.8: item_conf.setBackground(QColor(200, 255, 200)) elif conf 0.3: item_conf.setBackground(QColor(255, 255, 150)) else: item_conf.setBackground(QColor(255, 200, 200)) self.table.setItem(i, 1, item_conf) # ... 其他列5.3 异常告警当连续 5 帧检出 3 个裂纹蛋触发蜂鸣器与日志告警不是简单弹窗而是工业级闭环蜂鸣器通过RPi.GPIO控制树莓派部署时或winsound.Beep(1000, 500)Windows日志写入logs/egg_alert_20240520.log含时间戳、裂纹数、图像路径同时截图保存alerts/20240520_142305_crack.jpg。告警逻辑在update_display中def update_display(self, frame, detections): crack_count sum(1 for d in detections if int(d[5]) 1) # cls_id1 is crack self.crack_history.append(crack_count) if len(self.crack_history) 5: self.crack_history.pop(0) if len(self.crack_history) 5 and all(c 3 for c in self.crack_history): timestamp datetime.now().strftime(%Y%m%d_%H%M%S) cv2.imwrite(falerts/{timestamp}_crack.jpg, frame) with open(flogs/egg_alert_{datetime.now().strftime(%Y%m%d)}.log, a) as f: f.write(f[{datetime.now()}] CRACK ALERT: {crack_count} cracks in 5 frames\n) winsound.Beep(1000, 500) # 1kHz, 0.5s self.crack_history.clear() # 重置计数6. 避坑YOLOv5 鸡蛋检测 PyQt 的 4 个血泪教训现在知道还不晚6.1 现象PyQt 界面运行 2 小时后内存暴涨至 4GB最后崩溃原因cv2.imshow()或QPixmap.fromImage()创建的 QImage 未释放PyQt 的 QPixmap 缓存累积。OpenCV 的cv2.cvtColor()返回的 numpy array 若未.copy()PyQt 会持有原始内存引用导致无法 GC。解决所有图像转换前加.copy()并在update_display结尾显式删除临时变量# 错误写法 qimg QImage(frame.data, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_RGB888) # 正确写法 frame_copy frame.copy() # 关键 qimg QImage(frame_copy.data, frame_copy.shape[1], frame_copy.shape[0], frame_copy.strides[0], QImage.Format_RGB888) # ... 显示后 del frame_copy, qimg # 主动 del6.2 现象训练 loss 下降但 val mAP 不升甚至下降原因鸡蛋数据集里“正常蛋”占比 92%裂纹仅 5%血斑 3%严重类别不平衡。YOLOv5 默认cls_loss权重为 0.5对小类别惩罚不足。解决在train.py的compute_loss函数中手动加大cls_loss权重# models/yolo.py 第 220 行 cls_loss BCEcls(predn[..., 5:], t) * 1.2 # 原为 0.5改为 1.2 # 同时降低 obj_loss 权重防过拟合背景 obj_loss BCEobj(predn[..., 4], tobj) * 0.7 # 原为 1.06.3 现象PyQt 界面在树莓派 5 上卡顿CPU 占用 100%原因PyQt5 默认用 OpenGL 渲染树莓派 5 的 VC4 GPU 驱动不兼容导致软件渲染 fallback。解决启动时强制用 raster 渲染export QT_QPA_PLATFORMeglfs # 树莓派专用 export QT_QPA_EGLFS_INTEGRATIONeglfs_kms python main.py若仍卡顿改用PyQt6对 ARM 更友好并禁用动画app QApplication(sys.argv) app.setAttribute(Qt.AA_UseSoftwareOpenGL, True) # 强制软渲染 QApplication.setStyle(QStyleFactory.create(Fusion)) # 禁用原生样式动画6.4 现象导出的 ONNX 模型在 TensorRT 中报错 “Assertion failed: scales.size() 2 || scales.size() 4”原因YOLOv5 的Upsample层在 ONNX 导出时scale factor 未显式指定TensorRT 无法 infer。解决修改models/common.py的Upsample类强制指定scale_factorclass Upsample(nn.Module): def __init__(self, sizeNone, scale_factorNone, modenearest, align_cornersNone): super().__init__() self.size size # 关键强制 scale_factor 为 tuple避免 None self.scale_factor [1, 1, scale_factor, scale_factor] if isinstance(scale_factor, (int, float)) else scale_factor self.mode mode self.align_corners align_corners然后导出时加--include参数python export.py --weights weights/best.pt --include onnx --img-size 416 --batch-size 17. 最后一公里用 PyQt 的 QSettings 做产线配置持久化比写 config.ini 更稳产线设备不可能每次重启都手动设 ROI、置信度阈值、报警音量。PyQt 的QSettings是跨平台、线程安全、自动序列化的终极方案——它在 Windows 写注册表Linux/macOS 写 ini 文件但 API 完全一致。我们用它存三类配置配置项类型默认值说明camera/roi_xint200检测 ROI 左上角 x 坐标detector/conf_thresfloat0.3裂纹检测置信度阈值alarm/volumeint70蜂鸣器音量0–100初始化from PyQt5.QtCore import QSettings self.settings QSettings(EggTech, EggInspector) # 公司名应用名 # 读取 roi_x self.settings.value(camera/roi_x, 200, typeint) # 写入如用户拖动 ROI 框后 self.settings.setValue(camera/roi_x, new_x)但真正关键的是热重载当工人在界面调整阈值后模型无需重启即可生效。我们在DetectorThread中加监听class DetectorThread(QThread): def __init__(self, settings): super().__init__() self.settings settings self.conf_thres self.settings.value(detector/conf_thres, 0.3, typefloat) # 启动定时器监听配置变更 self.timer QTimer() self.timer.timeout.connect(self.check_settings) self.timer.start(1000) # 每秒检查一次 def check_settings(self): new_thres self.settings.value(detector/conf_thres, 0.3, typefloat) if abs(new_thres - self.conf_thres) 1e-3: self.conf_thres new_thres print(fConfidence threshold updated to {self.conf_thres})我踩过的最大坑是早期用json.dump()写配置文件某次断电导致文件损坏整条产线停摆 2 小时。QSettings 的原子写入和自动备份机制让我再没为配置丢过一次产线。现在我所有工业项目第一行代码必是QSettings(MyCompany, MyApp)——它不是锦上添花是产线存活的底线。希望帮到你。本文还有配套的精品资源点击获取
返回列表