
简介基于YOLOv8的食品包装生产日期识别项目聚焦工业质检场景中的日期喷码检测问题可作为计算机视觉、深度学习方向学生的毕业设计或课程设计。资源包含配套数据集、可视化页面和部署说明覆盖数据准备、模型训练、测试评估与结果可视化等环节适合已有一定Python基础的学习者也可供初学者跟随说明文档完成环境搭建并快速出结果。压缩包共8个文件包括3个Python脚本、3个PyTorch权重文件和2个txt说明文档脚本涉及训练模式、视频检测与可视化页面权重包含预训练模型与最佳模型文档则用于配置说明与部署指引整体大小约15.91MB便于下载与部署。项目代码已由作者在毕业设计中完整运行测试可直接复现能够输出混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果和标签分布图等关键评估图表配套可视化界面可直观展示检测效果方便答辩汇报与二次开发。目前已有77人学习下载适合作为项目初期的功能演示或课程设计、毕业设计的完整参考。1. 基于YOLOv8的食品包装生产日期识别一个能跑通全链路、适合毕设落地的视觉项目食品包装上的生产日期往往是一行喷墨小字打在塑料膜上还有反光字迹断断续续人眼都要凑近才能看清更别说让机器自动识别了。基于YOLOv8的食品包装生产日期识别就是把“目标检测 光学字符识别”组合成一条自动读取流水线先用YOLOv8在包装图上定位日期区域再把裁剪出的区域交给OCR转成文字。它解决的是质检和生产追溯场景里靠人工核对效率低的问题对做毕业设计或课程设计的人来说这也是一个上手难度适中、展示效果好、能写进简历的完整项目。特别适合有一定Python基础、想认真跑通一个视觉系统的人。2. YOLOv8生产日期识别的技术内核从目标检测到OCR的完整链路2.1 为什么生产日期不能直接丢给OCR食品包装的文字版面相当杂乱配料表、营养表、生产许可证号、厂商地址、条形码全都是文本而“生产日期”只是其中很不起眼的字段。直接把整张图丢给OCR引擎极大概率会把能读的文本都读一遍然后你还要在输出里寻找哪一段才是日期更麻烦的是“保质期”后面往往跟着一串数字OCR根本分不清哪个是生产日期。两段式流程能一次性解决这个痛点先让YOLOv8框出日期数字区域切下区域再做OCR。我一般用这样的链路原图 → YOLOv8 检测 date_area → 裁剪区域 → OCR 识别 → 正则后处理 → 输出 2024-11-21这么做有三个直接价值一是杜绝了跨文本串扰问题OCR输入里只有日期数字和少量杂质误读空间小二是反光、褶皱、斜体等干扰被限制在局部后续预处理更好施展三是输出是结构化字段就算要同时识别“保质期”“生产批号”只需要多加一个类别整套方案不推倒重来。2.2 YOLOv8网络结构选型n还是s显存说了算YOLOv8的结构可以拆成三块backbone用C2f模块加SPPF做特征提取neck用PAN-FPN做多尺度融合head是解耦头把分类和边框回归分开预测。它在设计上的一个重要变化是改成anchor-free网格直接回归中心点坐标不再像YOLOv5那样要预先按数据集尺寸聚类锚框。这对自定义数据集很友好省掉了最烦的锚框调参环节。训练时的损失通常看三类分别对应三个输出分支box_loss边框回归损失、cls_loss分类损失、dfl_loss分布焦点损失用于精调边框位置。做生产日期识别时类别往往只有一个或者两个训练目标比COCO简单得多收敛通常很快。模型该选哪个尺寸我的判断标准是看硬件条件模型适合场景显存需求yolov8nCPU推理、界面流畅优先1GB左右即可yolov8s毕设默认、有入门级显卡4GB到6GB够用yolov8m/l场景复杂、精度优先8GB以上这个项目目标通常较小dete_region在整幅图中占比大概10%到20%类别单一n或s足够。如果你有一张GTX1660Ti这种级别的卡直接用s batch 16 imgsz 640跑完全程如果打算在无显卡的机器上做演示训练时就老老实实选n后面部署才不会卡。训练完成后ultralytics会在输出目录保存best.pt和last.pt前者是按验证集指标挑出的最优权重后者是最后一个epoch的权重。交付时只用best.pt不要拿last.pt硬凑。2.3 OCR选型PaddleOCR、Tesseract与EasyOCR怎么选YOLOv8负责定位OCR负责把裁剪区域里的字符变成文本。这一步可选空间不小我按实际项目经验排一张对比表OCR引擎印刷体识别效果依赖体量部署难度PaddleOCR中英文数字表现稳定日期类短文本拟合好中等约几百MB相对简单自带方向分类Tesseract英文数字可用中文要额外字库小简单但参数多容易出乱码EasyOCR中英文支持不错依赖torch全家桶CPU推理偏慢生产日期里常见“2024/11/21”“2024-11-21”“20241121”多种格式甚至有只印“2024/11”的情况paddleocr在印刷字体上综合表现最好。我这里只使用它的识别能力不跑它自带的检测网络避免和YOLOv8的定位重复冲突from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def read_date(crop_img): result ocr.ocr(crop_img, clsTrue) if not result or not result[0]: return # result[0][0][1][0] 是识别文本result[0][0][1][1] 是置信度 return result[0][0][1][0]代码里的use_angle_clsTrue让OCR先做方向分类保证倒置文字也能转正后再识别clsTrue是推理时也启用这个方向分类器。返回结构中第二项是置信度毕设演示时可以把置信度一并显示在界面上显得更专业。OCR结果不是直接能用正则后处理是必须的。日期格式五花八门统一成YYYY-MM-DD再输出import re def normalize_date(text): # 兼容 2024-11-21 / 2024.11.21 / 2024/11/21 等写法 match re.search(r(\d{4})\D(\d{1,2})\D(\d{1,2}), text) if match: return f{match.group(1)}-{int(match.group(2)):02d}-{int(match.group(3)):02d} # 有的包装只印两位年份如 24-11-21 match re.search(r(\d{2})\D(\d{1,2})\D(\d{1,2}), text) if match: return f20{match.group(1)}-{int(match.group(2)):02d}-{int(match.group(3)):02d} return text这段的作用是先把“2O24”这类形近字错误挡回去再把分隔符统一。如果OCR输出的年份位数不对宁可原样返回并标记“识别失败”也不要给一个错日期这在使用场景里是基本原则。第2章这套链路跑通后数据集的大小和质量就成了决定精度的主要因素。3. 构建食品包装生产日期数据集样本采集、Labelme标注与VOC转YOLO格式3.1 样本采集先把拍摄条件做齐再谈数量做毕设100张照片能演示但要调出能见人的精度建议从300张起步。如果包装种类多、拍摄条件乱500张也不算多。我见过不少案例是把同一个产品连续拍几百帧结果模型把拍摄背景学进去了换一个场景就失效。采集时至少覆盖这几种变化包装材质塑料膜、纸盒、铝箔袋至少各来一批拍摄角度正拍、俯拍、斜拍日期区域不总在画面正中光照条件室内灯光、自然光、反光面不要全在均匀光线下拍日期位置有的在封口处有的在背面有的在侧面拼接处框目标的时候还有一个细节只框“生产日期”里的数字区域还是把“生产日期”四个字和数字一起框我一般只让YOLOv8负责数字区域文字前缀由后续逻辑去关联。原因很简单OCR模型对数字的识别本身就比汉字可靠框小了误检少。3.2 Labelme标注矩形框怎么框一个类还是两个类Labelme是常用的标注工具矩形框用起来最直接。pip install labelme labelme打开Labelme后在菜单栏选“Create Rectangle”沿着日期数字周围拉一个矩形框标签填date_area。注意不要把上下两行文字都框进去只框目标区域本身留一点边距即可。标完一张保存一次默认输出同名JSON文件。标注一致性比数量更重要。如果你自己标了一批又让同学标了一批两个人框的范围习惯不一样模型会被来回拉扯。我建议定三条规则然后严格执行框必须紧贴字符外沿带反光的字宁可框大一圈不要框到缺角图片旋转了矩形框跟着旋转不要贴着一个斜字框出正矩形。Labelme的JSON直接给YOLO用不了常规做法是先把JSON转成VOC XML或者直接在转换脚本里读JSON坐标。后面这段脚本用的是VOC XML更适合后面接入训练脚本。3.3 VOC转YOLO格式转换脚本与坐标越界防御YOLO训练需要的标注是每个图一个txt文件每行内容是类别id cx cy w h其中cx、cy、w、h都是归一化到0到1的小数。VOC XML存放的是左上角和右下角的绝对像素坐标必须做一次换算。import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, img_dir, save_dir): os.makedirs(save_dir, exist_okTrue) classes [date_area] # 与 data.yaml 的 names 保持一致 for xml_file in os.listdir(xml_path): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_path, xml_file)) root tree.getroot() img_name root.find(filename).text img_full os.path.join(img_dir, img_name) with Image.open(img_full) as im: img_w, img_h im.size # 用真实宽高防止标注写错 txt_lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue 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) # 防御坐标越界统一夹到图片范围内 xmin max(0.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h cls_id classes.index(name) txt_lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n) base_name os.path.splitext(xml_file)[0] with open(os.path.join(save_dir, base_name .txt), w) as f: f.writelines(txt_lines)这段脚本用PIL读取图片真实宽高而不是信任XML里写的width和height能避免不少标注工具版本差异带来的问题。“坐标越界防御”那四行尤其重要部分标注框可能超出图片边缘不夹住的话训练时会报错或者算出一个负的宽高。脚本运行完成后在输出的txt目录里抽查几个文件确认数字都在0到1之间。这是最廉价的排错手法比训练完发现指标异常再回来找原因高效得多。3.4 划分train/val按批次分组别让数据泄露数据集划分看起来就是复制文件但如果只用random.shuffle随便切同一款产品不同角度、同一批次拍的连续帧很可能同时出现在训练集和验证集里。验证集指标虚高模型实际泛化能力却不行这就是典型的数据泄露。我习惯先把所有图片按“产品品牌拍摄批次”分组再把组整体放进训练集或验证集。代码不复杂import random groups { # 同一品牌同一批次的多张图放在同一组 brand_a_batch1: [a1.jpg, a2.jpg, a3.jpg], brand_a_batch2: [a4.jpg, a5.jpg], brand_b_batch1: [b1.jpg, b2.jpg], } group_names list(groups.keys()) random.Random(42).shuffle(group_names) # 固定随机种子结果可复现 val_groups group_names[: int(len(group_names) * 0.2)] val_images [img for g in val_groups for img in groups[g]]这里把种子固定为42别人的复现结果和你一致毕业设计答辩时能拿这个细节说明你在实验方法上的严谨度。划分比例按7:2:1还是8:2看总样本量300张左右我倾向用8:2测试集单独留50张做最终验收。4. 训练YOLOv8检测模型环境配置、data.yaml与loss曲线判断4.1 YOLOv8环境配置Ubuntu20.04的CPU版本和GPU版本各来一条路环境配置是这套项目第一个劝退点尤其对只用过Windows的同学。Ubuntu20.04上搭建YOLOv8环境分CPU和GPU两条路。CPU版本纯粹为了跑通流程速度慢但兼容性最好conda create -n yolov8 python3.9 -y conda activate yolov8 # CPU 版 PyTorch不需要 CUDA 环境 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics--index-url这个参数指定从PyTorch官方CPU wheel源下载避免pip给你装上默认的GPU版本然后导入时报CUDA not available。GPU版本则要先确认显卡驱动支持再装对应CUDA版本的torchconda activate yolov8 # 先看驱动支持的最高CUDA版本用 nvidia-smi 确认 pip install torch torchvision pip install ultralyticsGTX1660Ti这种显存6GB的卡训练yolov8s完全没压力不需要上云GPU。装完ultralytics后在Python里执行from ultralytics import YOLO不报错就没问题。这里常遇到的一个坑是opencv版本和系统自带库冲突建议用conda环境隔离而不是直接装在系统Python里。4.2 配置data.yaml路径、类别数与标注一一对应YOLOv8训练前要写一个数据集配置文件指定数据和类别。文件放在项目根目录内容非常简单# data.yaml path: /home/你的用户名/date_dataset # 数据集根目录 train: images/train val: images/val test: images/test nc: 1 names: [ date_area ]path写绝对路径最省事省去相对路径在不同机器上失效的问题。train和val是相对path的路径如果你没有测试集把test这行注释掉也行。关键是names列表的顺序必须和第3章转换脚本里的classes顺序一致第0个类别就是date_area顺序错了损失函数直接学错目标。4.3 启动训练yolo train的关键参数表与GTX1660Ti上的取值配置写完后一条命令启动训练yolo train modelyolov8s.pt datadata.yaml epochs100 batch16 imgsz640 device0 patience20modelyolov8s.pt会先下载COCO预训练权重在它的基础上做微调比从零训练快得多效果也通常更好。下面是几个必调参数的含义参数作用我的常用值epochs训练轮数100到150看loss收敛情况batch每批图片数显存不够就调小6GB用16imgsz训练图片缩放尺寸640兼顾速度和精度device0是GPUcpu是纯CPU有卡用0没卡用cpupatience早停容忍轮数20连续20轮指标不升就停lr0初始学习率默认0.01不熟就别动epochs不要无脑贪多。类别少、样本不多的情况下模型通常在40到60轮就收敛后边全是过拟合。开了patience20之后训练会自动提前停止这时runs/detect/下会生成train目录里面的weights/best.pt就是成果。4.4 读训练结果loss曲线、混淆矩阵和results.csv训练结束后很多人只会看最后几行mAP忽略了loss曲线的价值。ultralytics在输出目录直接生成了results.png里面包含box_loss、cls_loss、val指标等。但那张图信息堆太满我习惯自己从results.csv里提取三张loss曲线单独画import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) df.columns [c.strip() for c in df.columns] fig, axes plt.subplots(1, 3, figsize(15, 4)) axes[0].plot(df[epoch], df[train/box_loss], labeltrain/box_loss) axes[0].set_title(box loss) axes[1].plot(df[epoch], df[train/cls_loss], labeltrain/cls_loss) axes[1].set_title(cls loss) axes[2].plot(df[epoch], df[val/dfl_loss], labelval/dfl_loss) axes[2].set_title(dfl loss) for ax in axes: ax.legend() ax.grid(True) plt.savefig(loss_curve.png, dpi200).columns里列名可能带前导空格strip()清理之后才不会报KeyError。三张图里train/box_loss持续下说明检测框在收敛cls_loss如果震荡不降大概率是标注类别有问题回头查txt文件有没有空标签val/dfl_loss是边框分布损失的验证值它上升就意味着过拟合。毕设报告里放这张图比放一张网上的示意图有说服力得多。训练这块跑通后就轮到图形界面了但界面集成阶段坑更多下面这章把典型问题一次说清。5. 可视化界面与推理排查PyQt5集成生产日期识别时必踩的5个坑5.1 PyQt5界面最小框架加载best.pt、选图、推理并显示做可视化界面PyQt5是最常见的选型生态成熟、打包工具多。一个能演示的最小界面只需要三个部件图片显示区、“选择图片”按钮、“识别结果”文本区。import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QFileDialog, QVBoxLayout, QWidget) from PyQt5.QtGui import QPixmap from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) class DateRecogWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(食品包装生产日期识别) self.label QLabel(点击下方按钮选择图片) self.btn QPushButton(选择图片) self.btn.clicked.connect(self.open_image) layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.btn) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def open_image(self): path, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.jpg *.png)) if not path: return result model.predict(path, imgsz640, conf0.3, verboseFalse)[0] # result.boxes.xyxy 是检测框坐标裁剪后交给 OCR 再画到图上 print(result.boxes)这个框架能跑但直接点击按钮后推理界面会卡住因为推理跑在了UI主线程。后面五个坑都是我在实际集成中踩过的从现象到解决一次讲完。5.2 坑1界面卡死——推理丢到子线程现象点击“选择图片”后窗口失去响应转圈几秒到十几秒才恢复图片大时甚至像死机。原因model.predict和OCR都耗CPU/GPU跑在UI线程里把界面事件循环堵死了。解决把推理放进QThread子线程UI只负责接收结果信号。from PyQt5.QtCore import QThread, pyqtSignal class Worker(QThread): finish pyqtSignal(str) def __init__(self, path): super().__init__() self.path path def run(self): result model.predict(self.path, imgsz640, conf0.3, verboseFalse)[0] text f检测到 {len(result.boxes)} 个日期区域 self.finish.emit(text)子线程里跑模型推理主线程只管更新界面窗口不会假死。这是PyQt5程序的基本原则所有耗时操作都不得碰UI线程。5.3 坑2误检一堆杂物——conf阈值不是越低越好现象conf0.1时包装背景上的纹理、文字块被误检成日期区域界面上一堆框。原因阈值设太低模型会把低置信度的预测也吐出来。解决把阈值调到0.3以上并适当提高NMS的iou参数让重叠框合并得更干净。毕设演示时0.3到0.4是合理区间低了演示翻车高了漏检严重。这个值不是玄学用验证集跑一遍选视觉效果最干净的即可。5.4 坑3反光小字OCR读错——先上采样再识别现象裁剪出的日期区域只有几十像素高OCR输出“2O24-11-2l”把“0”认成“O”把“1”认成“l”。原因输入图太小笔画细节丢失形近字区分不开。解决裁剪区域先放大再送OCR。用OpenCV做一次线性插值缩放到宽度至少200像素import cv2 crop cv2.imread(cropped_date.jpg) crop_up cv2.resize(crop, None, fx3, fy3, interpolationcv2.INTER_CUBIC)INTER_CUBIC比默认的线性插值保留边缘更锐利日期数字这类高对比度内容放大三倍足够。如果还有反光先转灰度再cv2.equalizeHist拉一下对比度命中率能再上一个台阶。5.5 坑4训练和推理imgsz不一致框不准现象训练用imgsz640推理代码里忘了写imgsz参数模型默认用训练时保存的尺寸但输入图片是1920宽的大图检测框明显偏移。原因推理时图片被resize到模型输入尺寸大图等比缩放后日期区域变得更小小目标特征丢失。解决训练和推理的imgsz保持同一数值推理时固定写imgsz640不要依赖默认值。如果实际图片特别大先按比例缩放到不超过1600像素宽再喂给模型。5.6 坑5换机器跑不起来——依赖版本锁死现象在自己电脑跑得好好的项目拷到答辩教室的电脑上import ultralytics直接报错或者cv2版本冲突。原因环境依赖没有固化对方机器Python、torch、opencv版本都对不上。解决项目根目录放一个requirements.txt写明核心依赖版本范围并把整个推理逻辑封装成独立入口ultralytics8.0.0 paddleocr2.6.0 PyQt55.15.0 opencv-python4.5.0部署时用pip install -r requirements.txt一键装齐。如果对方机器没有GPU推理部分要提前切到CPU版本这也是我建议把devicecpu参数做成可配置的原因。6. 验证模型与进阶用测试集算准数用ONNX给自己留后路6.1 用脚本统计整个测试集的识别准确率训练完模型后真正能说明问题的是在测试集上的整体指标而不只是挑几张“好看”的图演示。我习惯写一个脚本遍历测试集所有图片边跑边记录两个数字检测框与真实框的IoU是否达标以及OCR输出经过正则后能否解析出合法日期。如果数据集标注里保存了日期文本就做严格比对没保存的话人工抽看50张统计能正确读出的比例。这一比例超过95%在毕设场景里就能说得过去了。单独抽出失败案例看是检测漏了还是OCR读错后面调优才有方向。6.2 导出ONNX早做这一步部署就不慌最后给我的习惯建议训练完成后立刻把best.pt导出成ONNX格式。yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640ONNX的好处是摆脱PyTorch环境依赖到时候无论用onnxruntime还是转成嵌入式平台格式都有基础。即使毕设不要求这一步也会让你对模型边界理解更深导出过程本身会暴露动态尺寸和算子兼容问题早点踩掉答辩时多一个可讲的优化点。我做这类项目有个习惯永远在交付前一夜把环境从零装一遍用README里写的命令跑通全流程。装不上的依赖、写错的路径都在这一步现原形。提前验证过的东西才敢说“简单部署即可运行”希望帮到你。本文还有配套的精品资源点击获取