简介基于YOLOv8的果树成熟度检测系统是一个可直接运行的毕业设计或课程设计工程包包含源码、完整数据集、可视化界面和部署教程。项目代码经过实际测试内置训练、验证和检测闭环流程启动可视化页面即可操作并自动生成混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果与标签分布图等专业评估图表为毕设答辩提供有力支撑。包内共97个文件以70个Python源码为主另有12个pyc编译文件、4个pt模型权重、5个XML配置、2个TXT说明、1个示例mp4视频及工程配置文件等压缩包整体仅24.21MB部署运行十分轻量。目前已有37人学习适合计算机、人工智能等专业学生快速完成课设或毕设也适合深度学习初学者参照练习下载后按README.txt指引即可运行代码结构清晰便于修改或扩展至其他检测任务。1. YOLOv8 遇上果园成熟度检测为什么不能只靠“颜色判断”果农最累的活之一是每天在果园里走来走去抬头看树上的果子熟了没有。颜色判断看起来简单——红了就是熟绿了就是没熟——但光照一变、角度一换人眼都会打架更别说写死几条颜色阈值的程序了。把 YOLOv8 用在果树成熟度检测上本质是把“熟了没有”这个问题从颜色阈值判断换成了“目标检测成熟度分类”框出每个果子同时给出生理成熟和采摘适期两个维度。这套系统适合两类人一类是拿它做毕设或课设的学生另有一类是果园管理者。前者需要快速跑通一整套流程后者更关心误检率和单张推理耗时。这篇文章就按我落地的习惯把环境搭建、数据标注、模型训练、界面展示和踩坑记录完整拆开讲。2. 把它拆成能运行的方案从 YOLOv8 选型到 CPU 也能走的部署环境2.1 成熟度检测的任务边界检测框为什么比分类器更合适拿到“检测系统”这个需求第一件事是分清你到底要检测还是要分类。很多人第一时间想的是做个分类器给一张图打上“未熟/已熟/过熟”的标签。这在单果照片上可行但果园实景里一棵树上有十几二十个果子成熟度参差不齐切片交给分类器会让程序崩溃。正确的做法是用带检测头的网络同时输出框和类别让“绿色的果子”和“红色的果子”各自被框出来。YOLOv8 的 head 部分解耦了分类和回归分支框的定位质量和类别判断互不干扰对果实粘连、前后遮挡这种场景比老一代 anchor-based 模型更稳。成熟度等级划分也要设计而不是简单写“green、red”。对于柑橘类我把等级设为 unmature青果、mature转色期底色开始泛黄、ripe完熟三档对苹果来说成熟度还分背景色和面色这时候要把“底色”写进标注规范否则标图的人会在“这个到底算不算熟”上反复扯皮。先定好这些后面训练出来的模型才知道你要什么。2.2 无 GPU 的 ubuntu20.04 笔记本怎么搭 YOLOv8 环境很多人的课设机是没有 N 卡的 Windows 笔记本但部署教程往往默认你有一块 CUDA 显卡。没有 GPU 不等于不能跑 YOLOv8只是推理速度有上限。我一般建议用 ubuntu20.04 或 WSL2 里的 ubuntu20.04 建虚拟环境CPU 版本的 PyTorch 就能跑通整个流程。先确认 Python 版本3.10 以下比较稳妥。# 建议用 conda 建独立环境避免把系统 Python 弄乱 conda create -n yolo-fruit python3.9 -y conda activate yolo-fruit # CPU 版 torch注意要装 cpu 版本不能图省事直接 pip install torch pip install torch2.1.2cpu torchvision0.16.2cpu -f https://download.pytorch.org/whl/torch_stable.html # 安装 ultralytics会自动带上 opencv-python、numpy 等依赖 pip install ultralytics8.1.0 pip install labelme5.4.1 # 标注工具后面用这里有个细节必须说明torch和torchvision的版本要匹配否则 import 时直接报AssertionError。-f参数指定的是 PyTorch 官方 CPU wheel 索引页Windows 下的 pip 也能识别。装完后用一条命令验证。python -c import torch, torchvision, ultralytics; print(torch.__version__, ultralytics.__version__)能打印出版本号就说明环境通了。这一步失败通常两个原因一是 conda 环境没激活二是 pip 缓存了旧的 CPU 包。我把pip缓存清掉重来一次基本都能过。2.3 引入权重和推理验证跑通最小闭环的命令要留底环境刚建好时不要急着训练先用官方预训练权重跑一次推理验证“安装没装坏”。这一步也叫冒烟测试能让后续所有问题都分隔开环境问题、代码问题、数据问题。我习惯拿一张含果树的实拍图测试。# 下载预训练权重并直接推理第一次会自动下载 yolov8n.pt yolo detect predict modelyolov8n.pt source./test_fruit.jpg saveTrue # 查看输出目录里的标注图 ls runs/detect/predict/yolov8n是 nano 版本参数量最小CPU 上单张 640x640 推理约 2-4 秒适合做功能验证。等你的数据训练完再换上自己训出的权重。这个最小闭环留一个命令在笔记里之后每次换机器重新部署都先跑它能省下大量排查时间。3. 准备完整数据集从 LabelMe 标注到 YOLO 格式的转换路径3.1 成熟度等级怎么定义才不吵架经过前两步环境通了接下来面对的最大工程量是数据。所谓“完整数据集”不单单指图片数量而是标注文件、类别文件、划分脚本、yaml 配置四件套齐全。我见过太多中途翻车的案例都是因为 label 文件格式不对训练时报No labels found。先定等级我这里以最常见的苹果为例分为三个成熟阶段unripe背景色为深绿至浅绿果面未出现红色面积ripening底色转为黄绿果面红色面积小于 50%这是采摘适期的前哨ripe果面红色面积超过 50%底色偏黄达到商品采收标准。定好规范后在项目目录里建好数据集骨架后面几千张图就不会乱。datasets/fruit/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── fruit.yaml3.2 LabelMe 标注完的 json 怎么转成 YOLO 的 txtLabelMe 是毕设圈最常用的标注工具导出的是 json 文件。但 YOLOv8 训练需要的是每个图片同名、每行一个目标的 txt 文件格式为class_id x_center y_center width height坐标全部归一化到 0 到 1 之间。转换脚本我每次都自己写不放心里面那些“一键转换工具”因为边界情况只有自己心里清楚。import json import os def convert_labelme_json_to_yolo(json_path, out_dir, classes_map): with open(json_path, r, encodingutf-8) as f: data json.load(f) image_w data[imageWidth] image_h data[imageHeight] txt_name os.path.splitext(os.path.basename(json_path))[0] .txt out_path os.path.join(out_dir, txt_name) lines [] for shape in data[shapes]: label shape[label] if label not in classes_map: print(f警告: 未知类别 {label}跳过标注文件 {json_path}) continue points shape[points] # polygon 顶点列表 xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 归一化到 0~1 x_center (x_min x_max) / 2.0 / image_w y_center (y_min y_max) / 2.0 / image_h box_w (x_max - x_min) / image_w box_h (y_max - y_min) / image_h # 过滤过小的框可能是误标或边界噪声 if box_w 0.005 or box_h 0.005: continue lines.append(f{classes_map[label]} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines))上面的循环逻辑里两个点最容易踩坑。第一LabelMe 的框坐标是绝对像素整数必须除以宽高归一化第二min/max求边界时遇到正方形框没问题但遇到多边形手工圈选点的顺序不一定是左上到右下所以不能只取第一个和第三个点。加一个最小尺寸过滤是为了防止误点产生面积过小的假正样本干扰训练。3.3 数据增强与类别不均衡的处理数据准备阶段的第二个大坑是类别不均衡。果园真实场景里“未熟”果数量往往远多于“完熟”果完熟果可能只占 5%。YOLOv8 自带hsv色域增强、随机翻转和缩放但这些不够解决样本不均衡。我一般会在标注完成后统计各类别的目标数量# 统计 train 标签里每类 bbox 出现的次数 awk {print $1} labels/train/*.txt | sort | uniq -c如果发现某一类的框数量不足另一类的三成优先补标数据而不是依赖增强。对成熟度检测而言补标的重点不是加多张照片而是增加“不同光照方向下同一颗果子”的照片因为背光面的红色表现完全不一样。补标不够时再考虑使用augment.py做离线增强对完熟果图片复制三份分别做亮度下调、对比度增强和随机遮挡让模型见过更多“难例”。还要留出验证集不要跟训练集混用。我习惯按 8:1:1 划分 train/val/test划分时按“果树个体”而不是按“单张图片”划分避免同一棵树上的照片同时出现在训练集和验证集里导致验证指标虚高这一点越早做越省钱。3.4 训练脚本参数lr、batch 和 imgsz 怎么定训练阶段第一个要改的文件是fruit.yaml它的作用是告诉训练器去哪里读数据、有几个类别和类别名。我把路径写成绝对路径省得相对路径在不同 shell 下解析出错。# fruit.yaml path: /home/user/datasets/fruit train: images/train val: images/val test: images/test names: 0: unripe 1: ripening 2: ripe然后启动训练。CPU 上训练会非常慢但毕设跑个千张图的小数据集也能在十小时内完成如果有 GPU把device0打开。yolo detect train \ modelyolov8n.pt \ datafruit.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ devicecpu \ project./runs/fruit \ nameexp01几个参数我的经验值batch在 CPU 上设 16 极容易内存溢出降到 8 比较稳imgsz不要上来就设 1280检测精度提升有限训练时间翻三倍先用 640 验证流程lr0保持默认 0.01 一般没错迁移学习场景下调到 0.005 会更稳。训练完成后输出目录下的weights/best.pt就是你要的最终权重。4. 训练成型与可视化界面从 loss 曲线到推理面板4.1 训练启动与 loss 曲线怎么看训练开始后终端会滚动打印每一轮的box_loss、cls_loss、dfl_loss和mAP50。新手最容易慌的是看到 loss 在前几轮上涨然后怀疑自己数据标注错了。实际上预训练权重迁移过来前三轮是 warmup 阶段loss 波动是正常的持续到第 10 轮左右才会呈下降趋势。如果 30 轮后cls_loss还是高于 1.0说明类别学不进去去查标注 json 转 txt 时类别映射是不是错了而不是改学习率。训练完去runs/fruit/exp01/下查看results.png这张图直接反映了模型有没有正常收敛——曲线尾部应该平缓如果还在明显下降说明 100 轮不够可以续训。4.2 可视化界面三块核心区域的设计“能跑通训练”和“交付一个可视化界面”是两回事。毕设答辩时评委不关心你训练脚本写得多优雅他们要看到鼠标点一点就能出结果。我的界面做法是用 PySide6 写一个轻量桌面程序核心区域分成三块左侧是图片/视频选择区中间是推理预览区右侧是结果统计区。# ui_main.py 的结构骨架只展示核心布局思路 from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog) from PySide6.QtGui import QImage, QPixmap import cv2 from ultralytics import YOLO class FruitDetectorUI(QWidget): def __init__(self): super().__init__() self.model YOLO(runs/fruit/exp01/weights/best.pt) self.image_label QLabel(等待加载图片) self.result_label QLabel(检测结果: 未运行) self.btn_open QPushButton(打开图片) self.btn_run QPushButton(开始检测) self._build_layout() def _build_layout(self): layout QVBoxLayout(self) btn_row QHBoxLayout() btn_row.addWidget(self.btn_open) btn_row.addWidget(self.btn_run) layout.addLayout(btn_row) layout.addWidget(self.image_label) layout.addWidget(self.result_label)这个骨架里有个界面程序的通用设计模型加载放构造函数只加载一次每次点“开始检测”只调用推理函数不重新 import 模型。很多课设界面卡死就是因为每次推理都重新加载权重白白等十几秒。加载完图片后调用下面的检测逻辑。def run_inference(self): file_path, _ QFileDialog.getOpenFileName( self, 选择图片, , 图片文件 (*.jpg *.png *.jpeg)) if not file_path: return results self.model.predict( sourcefile_path, conf0.25, imgsz640, devicecpu, verboseFalse ) boxed_img results[0].plot() # 返回带框的 BGR 图 # cv2 的 BGR 转 Qt 的 RGB rgb cv2.cvtColor(boxed_img, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qimg)) # 统计各类别数量 names self.model.names counts {} for box in results[0].boxes: cls_id int(box.cls) counts[names[cls_id]] counts.get(names[cls_id], 0) 1 self.result_label.setText(f检测结果: {counts})这里的plot()方法会自动绘制检测框和标签省去手动画框的麻烦。conf0.25是置信度阈值调低到 0.1 会显示出更多框但误检变多界面上我建议暴露一个滑块让用户自己调而不是写死。4.3 界面推理卡死的三个原因与解决界面跑推理时窗口“无响应”是 PySide6 新手最常见的问题原因出在推理阻塞了 UI 线程。predict()在 CPU 上单张图可能需要几秒这期间 Qt 的事件循环被堵住。解决方法是把推理放到QThread里。这个细节值得写进代码注释里答辩时老师会问。# 用 QThread 后台推理避免界面假死 from PySide6.QtCore import QThread, Signal class InferenceThread(QThread): finished Signal(object) def __init__(self, model, file_path): super().__init__() self.model model self.file_path file_path def run(self): results self.model.predict( sourceself.file_path, conf0.25, devicecpu, verboseFalse ) self.finished.emit(results)然后把run_inference里的直接推理代码替换成启动线程收到finished信号后再更新界面。这类改动不算复杂但能让你的界面在 CPU 机器上依然顺滑。5. 避坑从 dataset.yaml 写到导出成品的几处翻车记录5.1 训练的 loss 异常下降但 mAP 为 0类别映射错位现象训练过程cls_loss下降很漂亮mAP 却始终为 0验证集输出全部是背景。原因我检查fruit.yaml时发现 names 顺序和标注 txt 里的 class_id 不一致。LabelMe 转 txt 时按字母表生成映射ripe分到了第 2 类但 yaml 里第 2 类写的是unripe。模型学到的类别 A 和数据显示的类别 B 对不上评价自然全错。解决写一个校验脚本随机挑一张训练图片把 txt 里的坐标画回去叠加到原图上人工检查框和类别对不对得上。这一步花不了十分钟但能省下后面两小时的排错时间。转换脚本的类别映射字典从此固定下来不再手工改。5.2 CPU 训练显存不足的假警报现象训练刚开始报OutOfMemoryError但我用的是核显共享内存任务管理器里显示可用内存还有很大余量。原因YOLOv8 默认开了cacheTrue会把整个数据集缓存进内存一千张高清图在 CPU 机器上直接吃满 16GB 内存。解决训练命令里加cacheFalse改小batch8和workers2。如果是 GPU 的 OOM一般是 batch 太大或imgsz太大先降 batch再考虑关掉cache。不要一上来就换模型结构那样容易引入新问题。5.3 label 文件与图片文件不配对现象训练时提示“found 0 images in val”或 loss 一直不下降。原因我用脚本随机划分数据集时只拷贝了图片文件没有同步拷贝同名 txt 标签文件。YOLOv8 默认把没有标签的图片当作背景图训练出来模型什么都检测不出来。解决改用程序同时复制两个目录并做一次完整性检查打印不存在标签的图片列表人工决定是补标还是删除。规则是“图片必须带标签不强迫每个标签都有对应图片”。5.4 界面显示乱码cv2 的 BGR 与 Qt 的 RGB 混用现象界面里图片颜色明显发蓝红色的果子在界面里变成蓝紫色。原因OpenCV 读取图片输出 BGR 通道顺序Qt 的 QImage 期望 RGB直接塞进 QPixmap 后红蓝通道互换。解决推理后加一行cv2.cvtColor(boxed_img, cv2.COLOR_BGR2RGB)再转QImage。这条坑不仅出现在 PySide6也出现在matplotlib显示检测图时后者同样默认 RGB如果直接把 cv2 结果imshow颜色必然偏。5.5 完熟果搭配红背景模型把背景当成果子现象验证集里 ripe 精度高但拿去果园实拍红色防草布、红色衣服都被框成 ripe 果。原因训练图为了省事大量使用了成熟果密度极高的“果子挨着果子”的照片模型把“红色区域圆形纹理”当成了唯一特征没学到果柄、果型这些结构特征。解决补标含背景干扰的负样本或在标注规范中强制要求果子边界不清、被叶子遮挡超过 30% 的要么精细圈边界要么不标。数据集中加入一批“有红布无果实”的空背景图类别为空理由是让模型学会“没有果子时输出为背景”。这是完全值得做的方向数据质量决定了模型上限。6. 进阶验证模型精度与迁移到边缘设备的最后一步6.1 独立测试集和 mAP 验证脚本训练完不能用 val 指标直接交差要留着 test 集做最终的精度验证。跑一次正式评估。yolo detect val \ modelruns/fruit/exp01/weights/best.pt \ datafruit.yaml \ splittest \ imgsz640 \ batch8输出会给出mAP50和mAP50-95两项关键指标。对成熟度检测这种任务目标至少是mAP50达到 0.85 以上低于 0.7 的建议先检查标签再调参。mAP50-95偏低但mAP50很高通常说明框的定位不够精细这时候该调的是回归分支的 loss 权重而不是加大训练轮数。6.2 导出 ONNX 并想想能不能上 RK3588毕设做到收尾很多人会问“能不能部署到开发板上”。这是一个恰当的收尾方向因为果园场景不可能一直背着笔记本。YOLOv8 导出 ONNX 格式非常顺滑。yolo export modelruns/fruit/exp01/weights/best.pt formatonnx opset12 imgsz640导出后在 ONNX Runtime 上跑一次比较导出前后推理结果是否有差异以确认算子兼容。如果差值超过 0.05 的置信度优先怀疑opset版本问题换opset11再试。上 RK3588 板子这类带 NPU 的边缘设备时要做 RKNN 格式转换同时把精度降到 FP16成熟度检测对精度损失不如自动驾驶敏感FP16 完全够用。板子上的推理代码和桌面端没有本质区别只是加载引擎不同这个方向可作为答辩的加分项。6.3 把误检图收集成负样本库我自己的一个工作习惯每次跑完验证集把预测错误的图片全部复制到一个error_analysis目录按错误类型分文件夹——漏检、误检、类别混淆。积累三十张左右进行一次增量训练把误检图作为难例混入训练集。两轮迭代后模型在实地拍摄上的表现会再上一个台阶。这是我做过几轮之后最直接的收尾建议交完项目不是终点把误检图留好是继续优化的后悔药。如果时间充裕再到果园里拍一段 15 秒的视频用训练好的权重跑一次视频推理把结果导出剪辑成演示素材。现场答辩的时候一个能实时框出成熟果的演示视频会比十页原理说明更有说服力。把“能用”的模型优化到“好用”功夫全在这些不显眼的迭代里希望这些步骤能帮你少走一些弯路把这件事一次性做扎实。本文还有配套的精品资源点击获取