拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

YOLOv8实战:食品包装生产日期检测与部署全流程解析

YOLOv8实战:食品包装生产日期检测与部署全流程解析

简介:基于YOLOv8的食品包装生产日期识别项目,面向计算机、人工智能、自动化等专业的毕业生、课程设计学生以及刚接触目标检测的初学者。资源包为zip格式,共8个文件,包含3个Python脚本(分别负责模型训练、视频检测和可视化页面设计)、3个模型权重文件(涵盖YOLOv8与YOLO11系列)以及2个说明文档,压缩包仅15.91MB,轻量易用。代码已通过运行验证,可自动生成核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图,覆盖训练评估全流程。配套的可视化界面简化了操作,部署说明则帮助快速搭建环境,适合从零复现或在现有基础上扩展功能。目前已有77人学习使用,无论是毕业设计还是课程设计,都能作为扎实的项目起点。

1. 从食品包装生产日期到 YOLOv8:这个小目标比你想象的更吃细节

做毕业设计、课程设计或产线视觉预研的人,看到《基于YOLOv8的食品包装生产日期识别》这类资源时,第一反应通常是两个问题:它到底能不能跑,以及跑出来能不能应付答辩演示。我的答案是能,但前提是别把它当成一个“丢张图进去就输出一行日期字符串”的黑匣子。这份资源真正解决的是“生产日期喷码区域定位”这个目标检测问题:用 YOLOv8 在包装袋、瓶盖、纸盒上找到日期码所在位置,再配合可视化界面完成检测结果回显。适合刚入门深度学习和计算机视觉的人,也适合需要快速完成毕业设计或课程设计的学生。我拆完这套资源后最直观的感受是:它把数据集、源码、界面、部署教程凑齐了,省掉的是最耗时的“从零攒数据”阶段,但真正决定分数高低的,还是会调参数、能说清踩坑点的人。

2. 资源结构与数据准备:train/val 目录、data.yaml、标注格式

2.1 拿到资源先看这几个文件,别急着点 train.py

我拿到这类资源时,习惯先不碰训练脚本,花十分钟把目录结构摸清楚。这种做法不是谨慎过度,而是很多毕设资源里往往包含两三套代码,有的能直接跑,有的只是参考,README 里写的主入口未必是真正能跑的那个。先看目录结构,能让后面每一步都在可控范围内。

这份资源里常见的目录组织是这样的:

D:/date_project ├─ datasets │ ├─ images/train │ ├─ images/val │ ├─ labels/train │ ├─ labels/val │ └─ data.yaml ├─ weights ├─ gui │ └─ main_window.py ├─ train.py ├─ detect.py └─ requirements.txt

先解释一下我的判断:datasets里如果已经有images和labels两个平级目录,说明标注格式大概率是 YOLO 的 txt 格式,这是 YOLOv8 最省事的输入方式;如果labels目录里放的是 Pascal VOC 的 xml 文件,则说明还需要一步转换,不能直接开训。gui/main_window.py是 PyQt5 或 PySide6 的可视化界面入口,train.py和detect.py分别是训练和推理脚本。我把这些文件看了一遍后,会先用cat datasets/data.yaml确认类别定义,再随机翻开一个标注文件看内容。

cat datasets/data.yaml type datasets/labels/val/00001.txt

第一条命令查看训练配置,第二条命令在 Windows 环境查看标注内容。YOLO 标注文件每一行五个数,格式是class_id cx cy w h,其中 cx、cy 是目标框中心点的归一化坐标,w、h 是框宽高归一化到图像尺寸后的值。如果看到的是0 0.513 0.721 0.044 0.028,说明类别 id 为 0,框中心在图像大约 51% 水平位置、72% 垂直位置。这个细节很重要,很多人后面转换标注时把中心点写成了左上角,结果模型怎么训都发疯。

2.2 验证标注格式和类别,先跑通再改数据

在正式训练之前,我强烈建议先做一遍标注合法性检查,而不是直接开始训练。训练跑了一晚上最后发现 loss 不下降,往往不是模型问题,而是数据格式问题。最简单的方式是写一个校验脚本,扫描所有 txt 文件,统计每一行是否恰好五个数字,以及归一化坐标是否都落在 0 到 1 之间。

import glob bad_lines = [] for txt in glob.glob('datasets/labels/**/*.txt', recursive=True): with open(txt, 'r', encoding='utf-8') as f: for line_no, line in enumerate(f.read().splitlines(), 1): parts = line.split() if len(parts) != 5: bad_lines.append((txt, line_no, '字段数不为5')) continue try: class_id = int(parts[0]) cx = float(parts[1]) cy = float(parts[2]) w = float(parts[3]) h = float(parts[4]) except ValueError: bad_lines.append((txt, line_no, '数字格式错误')) continue if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w <= 1 and 0 < h <= 1): bad_lines.append((txt, line_no, '归一化坐标超范围')) print(f'共发现 {len(bad_lines)} 处问题') for item in bad_lines[:20]: print(item)

这段代码的作用是把所有标注文件过一遍,只要有一行格式不对就记录下来。参数说明:glob递归匹配labels下的 txt 文件,line.split()按空格拆分,class_id 必须能转成整数,坐标必须能转成浮点数。我把这段脚本放在项目根目录下取名为check_labels.py,跑完如果输出为空,再往下走;如果报错,先修数据,不要用脏数据训练。

2.3 自建数据:从抽帧、清洗到 labelImg 转 YOLO 格式

如果你打算给这份资源补充自己的生产日期数据,最省力的流程是先抽帧,再清洗,再标注,最后转格式。常见做法是用 ffmpeg 从产线视频里抽帧,因为食品包装在传送带上会移动,直接截图可能全是模糊帧。

ffmpeg -i packing_line.mp4 -vf "fps=2" frame_%04d.jpg

-vf "fps=2"表示每秒抽两张,30 秒视频能得到大约 60 张候选图。抽帧后我会把模糊、反光、重复度太高的帧删掉,保留 200 到 500 张有效图就够了。标注工具用 labelImg 或 labelme 都可以,注意 labelImg 默认保存 Pascal VOC 的 xml,需要再转成 YOLO txt。转换脚本我一般写成这样:

import glob import os import xml.etree.ElementTree as ET classes = ['date'] # 与 data.yaml 的 names 顺序保持一致 for xml_path in glob.glob('annotations/*.xml'): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in classes: continue class_id = classes.index(cls_name) box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f'{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') txt_path = os.path.join('labels', os.path.basename(xml_path).replace('.xml', '.txt')) with open(txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines))

这段脚本把 xml 里的 bndbox 左上角和右下角坐标,转换成 YOLO 需要的中心点坐标和宽高,最后全部归一化到 0 到 1 之间。参数说明:classes的顺序就是最终的 class id,如果 data.yaml 里0: date,这里必须把date放在列表第一位。还有一个容易踩的坑:如果可以把标注框画得贴近字符边缘,不要为了省事把一整块“生产日期:2025-03-14”连商标一起框进去,框太松会让模型学到的特征包括大量背景,泛化能力反而下降。

3. 环境搭建与模型训练:从 requirements.txt 到 results.png

3.1 环境安装:先按 requirements.txt 装,再验证 torch 是否可用

环境问题是这套资源里最劝退新手的部分。实际上并不复杂,只是需要按顺序来。我推荐用 conda 建一个独立环境,避免把系统里的其他深度学习环境搞乱。

conda create -n date_yolo python=3.10 -y conda activate date_yolo pip install -r requirements.txt

requirements.txt里通常会包含 ultralytics、torch、torchvision、PyQt5、opencv-python、numpy、pandas。需要注意,torch 版本不能太新也不能太老,最好按资源里锁定的版本装。装完后立即验证导入是否正常:

python -c "from ultralytics import YOLO; print('ultralytics ok')"

如果这里报错,先看是不是 torch 和 torchvision 版本不匹配。没有 GPU 的机器也能训练,CPU 环境下把batch调小、epochs稍微缩短,训练几百张生产日期图是可行的,只是更慢。若你的机器只有 CPU,还可以单独装 CPU 版 torch:

pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu

这种装法会明显减少安装体积,但注意之后不要再装带 CUDA 的 torch,否则环境会被覆盖掉。我一般会确认一句“当前这台机器有没有 GPU”,再决定是否装 CUDA 版,避免装完发现推理时用的还是 CPU。

3.2 训练配置:data.yaml、预训练权重、输出目录

训练前一定要检查data.yaml的路径和类别,这是翻车率最高的地方。这份资源自带的datasets/data.yaml大致的结构是:

path: /home/user/date_project/datasets train: images/train val: images/val names: 0: date

path最好写成项目所在的绝对路径,因为在命令行里运行yolo detect train时,工作目录可能不是项目根目录,相对路径很容易找不到图片。train和val是相对path的目录名,names下的类别编号必须和标注文件里的 class id 一致。如果你自己扩了数据,类别名改成date_code、expire都可以,但不要出现两个类别却共用同一份标注。

确认无误后,直接跑训练命令:

yolo detect train model=yolov8n.pt data=datasets/data.yaml epochs=100 imgsz=640 batch=16 patience=20 project=runs/train name=date_exp

参数说明:model=yolov8n.pt是预训练权重,首次运行会自动下载;data指向 data.yaml;epochs=100表示最多训练 100 轮;batch=16是批大小,显卡显存不够就降到 8 或 4;imgsz=640是输入分辨率;patience=20表示连续 20 轮验证指标没有提升就提前停止;project和name决定输出目录,最终权重在runs/train/date_exp/weights/下。训练结束后不要只看 terminal 里的日志,要看runs/train/date_exp/results.png,这张图里有训练集和验证集的 box_loss、cls_loss、mAP50 等曲线。

3.3 训练参数怎么调:imgsz、batch、epochs、patience 的选择逻辑

对于生产日期这种小目标识别,我一般会把imgsz从 640 调到 960,因为喷码字符在整张包装图里占比很小,640 输入下字符特征可能只剩十几个像素。调大输入能让细节更丰富,但训练时间和显存占用也会跟着涨。

batch的选择优先考虑显存。我的习惯是先设 16,如果报 CUDA out of memory,就减半;如果显存有余量且训练集较大,可以提到 32。epochs不要一看默认 100 就安心,关键要看验证集停止提升的位置。patience我保留 20 到 30,避免权重停在过拟合阶段。训练完随手测一下:

yolo predict model=runs/train/date_exp/weights/best.pt source=samples/test.jpg conf=0.25 iou=0.5 save=True

这里的conf是置信度阈值,iou是 NMS 的交并比阈值。我通常先用 0.25 看召回,如果框多但误检也多,再往 0.35 抬高;如果漏检严重,先不要动阈值,回训练侧调数据或调imgsz。阈值只是最后一层过滤,真正决定上限的是训练数据的质量和标注框边界。

4. 可视化界面与部署:从图片/摄像头到结果回显

4.1 可视化界面怎么组织:入口文件、模型加载、图片选择

这份资源里的可视化界面是毕业设计演示最加分的地方。界面代码一般集中在gui/main_window.py,核心逻辑并不复杂:加载 YOLO 模型,用户选择图片或开启摄像头,推理后把检测框画在图像上,再通过 QLabel 显示。一个最小可用的 PyQt5 界面可以写成这样:

import cv2 from PyQt5.QtWidgets import QFileDialog, QLabel, QMainWindow, QPushButton from PyQt5.QtGui import QImage, QPixmap from ultralytics import YOLO class DateWindow(QMainWindow): def __init__(self): super().__init__() self.model = YOLO('weights/best.pt') self.image_label = QLabel(self) self.open_btn = QPushButton('选择图片', self) self.open_btn.clicked.connect(self.open_image) self.current_rgb = None def open_image(self): file_path, _ = QFileDialog.getOpenFileName( self, '选择图片', '', 'Image Files (*.jpg *.jpeg *.png)') if not file_path: return results = self.model.predict( source=file_path, conf=0.25, iou=0.5, verbose=False)[0] img = cv2.imread(file_path) for box in results.boxes: x1, y1, x2, y2 = [int(v) for v in box.xyxy.tolist()[0]] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 200, 0), 2) cv2.putText(img, f'date {float(box.conf[0]):.2f}', (x1, max(0, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 200, 0), 2) rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) self.current_rgb = rgb h, w, _ = rgb.shape qt_img = QImage(rgb.data, w, h, 3 * w, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qt_img).scaled(900, 600))

这段代码的逻辑是:点击按钮后弹出文件选择框,拿到图片路径后直接调用model.predict,然后把检测框坐标从结果里取出来,用 OpenCV 画矩形框和置信度,最后转成 PyQt 能显示的 QImage 上屏。关键参数在于verbose=False能避免推理时终端刷屏,QImage.Format_RGB888必须配合三通道 RGB 数据使用,如果直接喂 BGR 图,颜色会乱。还有一点要注意:QImage底层引用的是 numpy 数组内存,所以我把rgb存到self.current_rgb,防止局部变量被回收后界面出现花屏或黑图。

4.2 界面参数:conf、iou 怎么暴露给使用者

单纯写完界面不等于好用。我在做演示时遇到过现场环境光线变化,固定置信度阈值根本不现实。更好的做法是把conf和iou做成滑块或输入框,让使用者自己调。

from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QSlider self.conf_slider = QSlider(Qt.Horizontal, self) self.conf_slider.setRange(10, 90) self.conf_slider.setValue(25)

使用时把滑块值除以 100 当作conf,例如滑块在 25 就表示conf=0.25。原因很简单:生产日期喷码在不同材质的包装上对比度差异很大,有些铝箔包装反光严重,0.25 会同时框出很多反光区域;调到 0.4 以后误检减少,但模糊喷码可能漏掉。把阈值交给使用者在界面上实时调节,比每次改代码重新跑要高效得多。

4.3 部署:best.pt 转 ONNX 和最小推理脚本

毕业设计只要求本地演示的话,直接用 PyTorch 权重就够了。如果希望部署到没有深度学习框架的机器上,或者做简单的服务封装,我会把best.pt导出成 ONNX:

yolo export model=runs/train/date_exp/weights/best.pt format=onnx imgsz=640 opset=12 simplify=True dynamic=True

参数说明:format=onnx指定导出格式;imgsz=640要和训练时保持一致,否则框位置会偏;opset=12兼容性较好,老一点的 ONNX Runtime 也能加载;simplify=True会让计算图更干净;dynamic=True允许输入尺寸动态变化,代价是推理速度略有下降。导出完成后用 ONNX 权重做最小推理:

from ultralytics import YOLO model = YOLO('runs/train/date_exp/weights/best.onnx') results = model.predict('samples/test.jpg', conf=0.25, iou=0.5) for r in results: for box in r.boxes: print(box.xyxy.tolist(), int(box.cls[0]), float(box.conf[0]))

这段脚本输出每个检测框的坐标、类别 id 和置信度。注意box.xyxy.tolist()返回的是像素坐标,box.conf是浮点数。如果你脱离 ultralytics 直接用 ONNX Runtime,需要自己实现 letterbox,也就是先把图片等比缩放再补边,转成 RGB 并归一化到 0 到 1,输入 tensor 的顺序是NCHW,这些细节缺一个都会导致检测结果和 PyTorch 版本不一致。

5. 避坑与排查:训练到部署的五个常见翻车点

这条避坑清单来自我拆这类资源时的实际排查记录。每一条都按“现象、原因、解决”写清楚,照着对号入座能省下半天时间。

现象:训练一开始就报labels not found,或者 loss 完全不动。

原因:data.yaml 里的path写成了相对路径,命令行运行目录又不在项目根目录下,导致图片路径全部失效;另一种可能是标注 txt 里 class id 超出 data.yaml 的 names 数量,模型读到非法类别直接放弃学习。

解决:把 data.yaml 的path改成绝对路径,并先用yolo detect train里同样的data参数跑一遍输出检查。或者先执行python check_labels.py,确认所有标注行都是五列且 class id 合法。注意 YOLO 格式的第一列是数字 id,不是类别名,0和date不能混用。

现象:小目标检测效果差,640 输入下日期区域频繁漏检,mAP50 很高但实际看不清框在哪。

原因:生产日期喷码在整张包装图里往往只占几个百分点面积,512 或 640 的输入分辨率让字符特征在多次下采样后丢失。训练集和验证集如果都来自同一批包装、同一光线,模型会学到“这一版面的固定位置有字”,而不是学习字符本身,真实场景一换包装就翻车。

解决:把imgsz提到 960,训练时开启适当的数据增强,比如随机亮度、对比度、轻微旋转和模糊。更实用的做法是对喷码区域做裁剪放大后再训练一个专门检测器,或者把检测框区域直接作为后续 OCR 的输入,检测模型只负责精确定位。

现象:PyQt 界面能打开,但一点图片按钮直接卡死或者闪退。

原因:model.predict被放在 UI 主线程里执行,模型加载和大图推理会阻塞 Qt 事件循环,表现是窗口无响应;另外如果代码里同时调用了cv2.imshow,OpenCV 自带的窗口系统会和 PyQt 的事件循环抢资源,闪退就来了。

解决:推理代码放到 QThread 中,主线程通过信号接收结果并刷新 QLabel;所有结果展示统一用 QImage 转 QPixmap,不要再用cv2.imshow。如果只是课设演示,可以先把imgsz调低到 640,图片缩放后再推理,界面流畅度会明显改善。

现象:用导出的 ONNX 推理时,检测框和 PyTorch 结果不一致,尤其小目标丢框。

原因:导出时输入尺寸和推理时不一致,或者输入预处理时没有做 letterbox。Ultralytics 在 PyTorch 推理时默认会做等比例缩放补边,但 ONNX Runtime 裸调用时只按原始图尺寸喂进去,模型输入尺寸不匹配,框自然就飘了。

解决:导出一个固定imgsz=640的 ONNX,推理前先把图片 resize 到 640×640,做同样的 letterbox 操作。我先用同一张图分别跑best.pt和best.onnx,把两种结果画在同一张图上做 diff,确认差别可以接受后再封装给界面用。

现象:训练曲线很好看,验证集 mAP50-95 却在一半 epoch 后持续下跌。

原因:这是典型的过拟合,模型开始记住日期在固定版面上的位置,而不是字符形状。很多课程设计数据集只有一两百张图,且全部来自同一产品,随机拆分训练集和验证集会加剧这种假象。

解决:按视频段或拍摄批次划分 train/val,不要让同一条传送带连续帧同时出现在两个集合里。训练时保留patience=30,最终权重用best.pt,不是last.pt。如果模型在“没有日期的包装”上硬画框,可以补几十张无目标背景图放进训练集,让负样本逼模型学会拒绝。

6. 进阶采集与分析:把识别框按时间戳写入追溯表

可视化界面适合现场演示,但要说清楚“检测结果有什么用”,我建议把识别结果落成 CSV 追溯表。这样不仅答辩时有据可查,做产线验证时也能统计白天和夜班的漏检率。我用一个脚本遍历某批图片,把每个检测框坐标和置信度按图片路径、检测时间写入 CSV:

import csv from datetime import datetime from pathlib import Path from ultralytics import YOLO model = YOLO('weights/best.pt') save_dir = Path('trace_logs') save_dir.mkdir(exist_ok=True) log_path = save_dir / 'detection_log.csv' need_header = not log_path.exists() or log_path.stat().st_size == 0 for image_path in Path('samples').glob('*.jpg'): result = model.predict(str(image_path), conf=0.25, iou=0.5, verbose=False)[0] boxes = [] for box in result.boxes: x1, y1, x2, y2 = [round(float(v), 1) for v in box.xyxy.tolist()[0]] conf = round(float(box.conf[0]), 3) boxes.append((x1, y1, x2, y2, conf)) row = { 'image_path': str(image_path), 'detected_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'box_count': len(boxes), 'boxes': boxes, } with open(log_path, 'a', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=list(row.keys())) if need_header: writer.writeheader() writer.writerow(row)

这段脚本把每次推理的图片路径、检测时间、框数量和所有框坐标追加到同一个 CSV 里。need_header用文件大小判断文件是否为空,避免每次运行都重复写表头。boxes字段里存的是像素坐标元组,后续要接 OCR 时可以直接按(x1, y1, x2, y2)裁剪区域,把裁剪图片交给 PaddleOCR 或 Tesseract 做字符识别。对毕设来说,这个“检测加追溯”的最小闭环,比单独一张检测效果图更有说服力。

我自己的习惯是:每次拿到新的检测数据集,都会强制先抽 200 张没参与训练的现场图跑一遍推理,把漏检和错框按类目记进 CSV,再回去调数据或参数。确认误检率压到可接受范围后,才把权重交给别人演示。希望帮到你。

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

返回列表