尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Python+YOLOv8实现柚子缺陷检测:数据、训练与部署全攻略

发布时间:2026/9/24 19:38:26

资讯中心
01
ARTICLE

Python+YOLOv8实现柚子缺陷检测:数据、训练与部署全攻略

Python+YOLOv8实现柚子缺陷检测:数据、训练与部署全攻略
简介基于Python实现的柚子缺陷检测项目同时面向毕业设计、课程设计与工业应用预研适合具备基础Python和图像处理知识的开发者参考使用。核心思路利用腐烂果皮与正常表皮在饱和度上的显著差异通过阈值分割提取黑色斑块并根据斑块面积占比辅助判断果实是否需要剔除目前支持单张图像检测预留工业相机SDK接入扩展。压缩包共33个文件涵盖Python源码如main.py、header.py、23张测试样图、标注文件、PyCharm项目配置及README说明文档总大小仅663KB结构轻量清晰。项目源码已经过严格测试现有45人学习下载能够在较小成本下快速复现检测流程。配套文档详细阐述算法思路与后续优化方向读者可将其作为模板迁移到其他水果的表皮缺陷检测场景也可在此基础上进一步实现实时检测或批量处理功能。1. 一个柚子上的黑斑为什么值得你做一个完整的Python项目流水线上柚子按个头和重量分道并不难难的是判断表皮有没有病害、腐烂和机械伤。人工目检一天盯几万个果子漏检率往往比预想高得多。基于python实现的柚子缺陷检测解决的就是这件事用摄像头采集图像由Python完成推理输出缺陷类别和位置再决定这个果子的去向。对做毕业设计、课程设计或者接私活的人来说这个题目的价值不是训练一个多先进的模型而是把数据、模型、源码和文档整理成一套能答辩、能被验收的完整交付物。常见做法是拿YOLO系列检测模型做定位和分类后面我会按数据准备、训练、推理和踩坑的顺序把整条技术路线一次讲透。2. 数据准备是第一道坎缺陷类别怎么定、标注怎么管、划分防串集很多人的第一个误区是把精力全放在模型上结果数据稀烂训练出来的权重一测就露馅。果蔬缺陷检测的数据准备工作量通常占整个项目六成以上这一步做不好后面所有环节都在给错误的结果打补丁。2.1 先定义“缺陷”类别体系决定后面所有工作别急着拍照标注先想清楚到底要分几类。常见柚子的缺陷有溃疡病斑、霉变腐烂、机械伤刮擦印、疤痕愈合组织还有一类很容易被误检的“正常结构”果蒂和花萼它们在图像里也发暗经常被判成缺陷。我的建议是类别体系宁细勿粗。如果把“腐烂”和“疤痕”合并成“坏果”模型是省事了但分拣端没法决定这个果子是降价卖还是直接报废。反过来如果你现场根本没条件区分那就别硬分分错反而让标注数据噪声变大。对课程设计和毕设来说分三类左右最合适比如“rot腐烂”“ulcer溃疡”“scar机械伤”既能说明问题又不至于让标注量失控。采集图像时注意三点光线要接近实际产线不要用单侧强光否则高光和阴影会制造大量假特征距离和角度要固定模型学的是“缺陷长什么样”不是“换个角度就认不出来”同一个果实尽量多拍几张覆盖不同朝向这样小样本也能撑起训练集。2.2 标注工具与YOLO格式一个清洗脚本解决对齐问题标注工具常用的有labelImg和Labelme导出时选YOLO格式会为每张图生成一个同名txt文件。每一行是“类别id 中心点x 中心点y 宽度w 高度h”坐标统一归一到0到1之间。这里最容易出问题的是标注文件与图片对不上或者坐标越界。我一般写完标注先跑一个清洗脚本再进训练。# verify_labels.py from pathlib import Path def verify_dataset(img_dir: str, label_dir: str): img_dir Path(img_dir) label_dir Path(label_dir) broken [] for txt in label_dir.glob(*.txt): stem txt.stem # 找同名图片不限定扩展名兼容 jpg/png/jpeg img_candidates list(img_dir.glob(stem .*)) if not img_candidates: broken.append(f{txt.name}: 找不到对应图片) continue lines txt.read_text(encodingutf-8).strip().splitlines() for line in lines: parts line.split() if len(parts) ! 5: broken.append(f{txt.name}: 每行应含5个字段) break x, y, w, h map(float, parts[1:]) # YOLO格式要求中心和宽高都在[0,1]区间w/h必须大于0 if not (0 x 1 and 0 y 1 and w 0 and h 0): broken.append(f{txt.name}: 坐标越界 x{x} y{y} w{w} h{h}) break print(f共检查 {len(list(label_dir.glob(*.txt)))} 个标注问题 {len(broken)} 个) for item in broken: print(item) return broken if __name__ __main__: verify_dataset(datasets/pomelo/images/train, datasets/pomelo/labels/train)这个脚本做了三件事校验图片和标注文件是否一一对应校验每行是否真的是五个字段校验坐标是否符合YOLO的归一化规范。第三个校验特别有用因为有些工具在导出时会把坐标缩放到图像分辨率而不是归一化比如中心点写成了367而不是0.5模型训练时不但不收敛还会给出离谱的loss。另外提醒一句标注文件别用系统自带记事本打开另存Windows记事本可能会改动编码最好统一用脚本读写编码固定为utf-8。2.3 按果实分组划分数据集防止“串集”导致的虚高指标数据划分看起来简单实际上是最容易埋雷的地方。网上很多教程直接随机打乱图片但对于果蔬检测同一个果实的不同角度照片高度相似如果其中一张进了训练集另一张进了验证集验证指标会虚高到让你误以为模型已经能上线。按果实ID分组再按组划分才是可靠的方案。# split_by_fruit.py import random from pathlib import Path def fruit_id(filename: str) - str: # 文件名约定fruit_003_l2.jpg取前两段作为果实唯一编号 return _.join(filename.split(_)[:2]) def split_by_fruit(img_dir: str, train_ratio: float 0.8, seed: int 42): files [f for f in Path(img_dir).iterdir() if f.suffix.lower() in (.jpg, .jpeg, .png)] groups {} for f in files: groups.setdefault(fruit_id(f.stem), []).append(f) group_ids list(groups.keys()) random.Random(seed).shuffle(group_ids) split_idx int(len(group_ids) * train_ratio) train_ids set(group_ids[:split_idx]) train_files [f for gid, items in groups.items() if gid in train_ids for f in items] val_files [f for gid, items in groups.items() if gid not in train_ids for f in items] print(f果实组数: {len(group_ids)}) print(f训练集图片: {len(train_files)}验证集图片: {len(val_files)}) return train_files, val_files if __name__ __main__: split_by_fruit(datasets/pomelo/images)核心逻辑是把同一果实的全部照片归入同一组然后对组ID做随机划分。train_ratio控制划分比例一般0.8或0.85。seed固定随机种子保证复现结果一致。注意这里只打印了文件列表实际使用时要把分成两组的图片和对应标注分别拷贝到images/train、labels/train、images/val、labels/val四个目录。每次跑完划分脚本至少抽查三五个文件确认没有同名不同内容的错位。3. 训练走通模型选型、参数解读与训练过程监控数据就绪之后进入训练环节。这一步看起来只是执行一条命令实际上模型选型、超参数、训练策略都会影响最终效果。我不建议在这个阶段盲目追求高级结构先把一条成熟的路线跑通再考虑优化。3.1 为什么首选YOLOv8而不是Faster R-CNN或分类网络先说说为什么不选分类网络。分类网络的输出是“这张图属于哪一类”它回答不了“病害在果子的哪个位置、有几个病斑”。柚子缺陷检测的典型场景是同一个果子上既有溃疡又有机械伤必须做到定位加分类这就是目标检测任务。虽然也能用“先切图再分类”的土办法但工程复杂度和推理耗时都更高检测效果还受切图方式影响不划算。剩下的选择主要是YOLO系列和Faster R-CNN。Faster R-CNN在两阶段方法里精度不错但训练和推理都慢依赖的依赖库也更多用来做毕设或者课程设计光是环境就能劝退一堆人。YOLOv8在工程实践里更顺手的原因有三个第一单阶段结构一张图一次推理就能拿到所有框速度优势明显第二生态完整训练、验证、导出ONNX、部署都有现成接口不需要自己拼装流程第三预训练权重和教程多身边同学踩过的坑基本都能搜到对应解决方案。如果你的设备实在跑不动退化到YOLOv5n或者YOLOv8n训练成本会更低。3.2 数据集配置与训练命令参数怎么设才算合理YOLOv8训练的第一步是写一个数据集配置文件告诉框架数据在哪里、总共几类、类别叫什么。# datasets/pomelo.yaml path: ./datasets/pomelo train: images/train val: images/val nc: 3 names: 0: rot 1: ulcer 2: scar训练时用下面的命令行启动。粗看参数很多但真正需要手动调的其实就几个。yolo detect train \ datadatasets/pomelo.yaml \ modelyolov8s.pt \ epochs120 \ imgsz640 \ batch16 \ device0 \ patience20各参数的含义和影响如下参数作用建议data数据集配置路径必须与pomelo.yaml实际位置一致model预训练权重或模型结构小数据用yolov8s.pt起步显存紧用yolov8n.ptepochs训练轮数100到150之间配合早停即可imgsz输入图像尺寸640是性价比选择像素不够再降到480batch每批图片数根据显存调OOM就减半device计算设备0表示第一块显卡没有GPU写cpupatience早停耐心值20表示验证指标连续20轮不提升就停预训练权重的选择有个常见误区总想一上来用最大的model觉得越大的模型越准。实际上你只有几百张数据v8x大概率过拟合训练时间还长。从s或者n起步先把流程跑通再看哪类缺陷漏检多针对性补数据这才是正路。batch和imgsz共同决定显存占用二者是乘数关系。如果你把imgsz从640降到480同样的显存可以把batch翻一倍。CPU训练的另说后面避坑章节详细讲。3.3 训练过程怎么盯loss下降、PR曲线与中断恢复训练启动后不要干等要会看输出。终端里每个epoch结束会打印Box Loss、Cls Loss、Dfl Loss以及Precision、Recall、mAP50等指标。我的习惯是重点关注mAP50和Recall对缺陷检测来说漏检比误检更可怕Recall上不去说明该检出来的缺陷没检出来。训练结束后结果保存在runs/detect/train目录下里面有几个关键文件weights/best.pt和weights/last.pt分别是最优权重和最后一轮权重confusion_matrix.png是混淆矩阵results.png是训练全过程的曲线图。如果看到训练loss持续下降但验证loss在某个epoch后开始回升说明过拟合了这时候应该改回更小的网络结构或者加强数据增强而不是继续加训练轮数。训练中断也是很常见的情况。笔记本合盖、机房断电、显存被别的进程占用都可能让训练停掉。不要从头再跑直接续训yolo detect train resume modelruns/detect/train/weights/last.ptresume会自动读取上次训练的状态、优化器和epoch计数从断点继续。这也是我建议训练时不要随手改项目目录结构的原因runs目录一旦被移动resume就会找不到之前的配置。续训前建议看一眼训练日志里的“Resuming from epoch 87”确认是从正确轮数接上的。4. 源码与推理把权重变成别人也能用的结果模型训练完项目才完成一半。剩下的一半是把训练好的权重封装成可执行程序让不懂深度学习的人也能传入一张图片得到检测结果。评审和答辩时一个清晰的项目结构和能跑通的命令行入口比口头解释十句都有说服力。4.1 项目结构长什么样拿到源码包先看哪里一份可交付的项目目录结构应该让人一眼就找到入口。pomelo_defect/ ├── datasets/ │ ├── images/ # 原始图像 │ └── labels/ # YOLO标注 ├── weights/ │ └── best.pt # 训练好的模型权重 ├── scripts/ │ ├── split_by_fruit.py # 划分数据集 │ ├── train.py # 封装训练命令 │ └── detect.py # 推理入口 ├── docs/ │ ├── 环境部署.md │ ├── 训练记录.md │ └── 接口说明.md └── requirements.txt拿到一份源码项目先不要急着跑主程序第一件事是看两个地方requirements.txt里锁定了哪些依赖README里写的运行顺序是什么。很多人python基础语法没问题卡一下午的往往是环境问题torch和opencv版本不匹配、CUDA版本和PyTorch对不上。建议按项目文档的依赖清单新建虚拟环境先跑一个最小推理用例确认模型能加载、图片能出结果再动其他功能。4.2 推理脚本的核心逻辑加载权重、预测、过滤结果下面是整个项目最核心的推理脚本我把它精简成能直接落地的版本。# detect.py import argparse from ultralytics import YOLO def main(): parser argparse.ArgumentParser(description柚子缺陷检测推理) parser.add_argument(--source, requiredTrue, help图片路径或视频路径) parser.add_argument(--weights, defaultweights/best.pt, help模型权重) parser.add_argument(--conf, typefloat, default0.35, help置信度阈值) parser.add_argument(--iou, typefloat, default0.45, helpNMS的IOU阈值) parser.add_argument(--device, default0, help推理设备cpu或显卡编号) args parser.parse_args() model YOLO(args.weights) results model.predict( sourceargs.source, confargs.conf, iouargs.iou, deviceargs.device, saveTrue, projectruns/infer, nameexp, ) for r in results: boxes r.boxes print(f{r.path}: 检出 {len(boxes)} 个缺陷) for b in boxes: cls int(b.cls[0]) conf float(b.conf[0]) xyxy [round(v, 1) for v in b.xyxy[0].tolist()] print(f 类别{r.names[cls]} 置信度{conf:.3f} 坐标{xyxy}) if __name__ __main__: main()逻辑很直白加载权重对输入图片做预测然后遍历每个预测框把类别、置信度和坐标打印出来。saveTrue会把画好框的结果图存到runs/infer/exp目录方便直接查看检测效果。cli参数需要认真对待它们在实际调试中会频繁调整。conf是置信度阈值默认0.35调低会召回更多框但也会带出更多误检调高则相反。iou是NMS合并重叠框的阈值两个同类别框交并比大于该值就只保留置信度更高的那个默认0.45在多数场景合适。推理设备要区分GPU和CPU有独立显卡写0只有核显的机器写cpu否则会报CUDA不可用的错误。4.3 从单张图到批量验证跑通与查看结果命令行入口封装好之后执行下面这条命令就能看到效果python scripts/detect.py --source test_images/ --weights weights/best.pt --conf 0.35 --device cpu注意--source传的是目录时模型会对目录下所有图片逐一推理。输出目录runs/infer/exp里能看到每张图的标注结果和打印出的缺陷统计。我一般会准备一个“效果验收”文件夹放几张训练时没见过的柚子图包括正常果、轻微伤痕果和重度腐烂果推理后逐个翻看结果图。如果正常果被画了框说明误检偏多把conf往高调如果腐烂果没被画框说明漏检需要回到数据层面补样本而不是继续降阈值。这个阶段也是检验“源码”交付质量的时候。项目文档里建议写清楚三件事环境安装步骤从创建虚拟环境到跑通推理、训练记录每次实验的参数、数据集版本、最终指标、接口说明detect.py的每个参数含义。答辩评委和接手的人主要就看这三块写清楚了项目就立住了。5. 避坑缺陷检测里那些不跑一遍根本发现不了的问题不少缺陷检测类的项目翻车九成问题不在模型结构而在数据、环境和评估口径上。这一章把最常踩的几个坑按“现象、原因、解决”拆开讲每条都是实操里反复出现的教训。5.1 现象训练loss很低实测却漏检严重训练日志里loss掉到零点零几验证集mAP50也有0.9以上可换成产线拍的真实图片缺陷一个都没框出来。这种落差会让第一次做检测项目的人非常崩溃。原因是多方面的最常见的是过拟合和分布偏移。小数据集很容易让模型记住训练图的背景特征比如纸箱纹理、特定光照色偏而不是缺陷本身的纹理。验证集和训练集来自同一天同一批拍摄条件指标自然好看但换到另一个环境就失效。解决思路分两步。第一步回看PR曲线和混淆矩阵确认是哪个类别漏检最严重针对性补那个类的样本而不是盲目加所有数据。第二步训练时打开数据增强YOLO自带的HSV扰动、随机翻转和缩放能让模型少记背景、多学缺陷本身。如果补了数据还是没改善把conf阈值降低到0.2左右看原始输出确定是模型没检出来还是检出来被阈值过滤了。5.2 现象高光、胶带、叶子被当成缺陷推理结果里出现大量误检框框住的是柚子表面的反光点、贴标的位置或者背景里的叶片阴影。这类误检在实际项目里几乎必现。原因在于缺陷样本太少模型学到的是“颜色异常、和周围对比明显”这些浅层特征而高光和深色阴影恰好也满足这些特征。标注环节也可能埋了雷比如把缺陷周围一大圈正常果皮也标进框里模型学出来的特征就包含了正常纹理。解决从两头下手。一是采集数据时尽量接近产线实际光照避免单侧强光造成的镜面反射拍摄角度略微抬升而不是正对光源。二是把误检图收进训练集做“硬负样本挖掘”被误检的图片放到数据目录里配上空标注文件明确告诉模型这些场景没有缺陷。硬负样本对误检的压制往往比调阈值有效得多。5.3 现象验证集指标虚高换到新果子上明显变差一个经典场景mAP50跑到了0.95发朋友圈都觉得稳了结果拿着训练时的某个果实翻个面重新拍照模型居然漏检了。问题出在数据划分方式上。跟正文前面讲的一样同一个果实拍了多张不同角度的图随机划分时这些高度相似的图片被拆进了训练集和验证集。模型在训练时已经“见过”了类似的纹理验证指标自然虚高。这不是模型没学好是评估口径本身有水分。解决就是按果实ID分组划分。把同属一个果实的全部图片归入同一组整个组要么全在训练集要么全在验证集。我习惯在划分脚本里打印分组数量和每组图片数如果发现某些组成百上千张就要检查是否拍摄重复度过高考虑每类只保留代表性视角。5.4 现象CPU训练慢到怀疑人生一个epoch要十几分钟没有NVIDIA显卡的学生机跑YOLOv8simgsz640、batch16一个epoch甚至能跑到二十分钟120个epoch跑一天半。很多人等不起就把训练强行中断结果权重没保存白忙一场。原因很简单设备不支持CUDA代码默认还是往GPU上放或者训练参数按显存卡配置写死CPU上根本吃不消。解决不是放弃训练而是把配置调到让机器跑得动的水平。model换成yolov8n.ptimgsz降到480batch降到8workers设为0避开Windows多进程卡死问题。训练时间能从一天半压到三四个小时。CPU训练时记得把device参数写明确同时打开resume机制每隔一定轮数保存检查点机器再怎么卡也能续上。换句话说配置是为硬件服务的不是为参数看起来好看服务的。5.5 现象Windows下图片读不出来标注内容变成乱码标注工具生成的标注文件放到训练脚本里报错说找不到图片或者标签内容解析出来全是乱码。这类问题最气人因为数据本身没错是系统环境的细节在折腾人。常见原因有两个一是数据集路径或图片文件名里有中文OpenCV和PyTorch在某些版本下对中文路径支持不完整读取时直接失败二是标注文件被记事本修改过编码被转成了GBK而Python脚本按UTF-8读取自然乱码。解决的规范做法是项目根目录、数据集路径、图片文件名全部改成英文字母和下划线标注文件统一用脚本读写不要手动编辑。加一句校验代码读取时显式指定encodingutf-8遇到解码异常直接打印文件名比训练中途诡异报错好排查得多。路径和编码这类问题养成习惯之后不会再犯但第一次碰上往往能卡半天。6. 从mAP到业务指标用混淆矩阵和分类别统计验收模型训练完模型很多人盯着mAP50看几秒就收工了。mAP是综合指标但它回答不了产线上最关心的两个问题哪种缺陷漏检最多哪种正常果实最容易误杀。我建议训练结束后补一个分类别评估脚本统计每一类缺陷的TP、FP、FN。思路是读取模型输出和真实标注按类别汇总算每个类的Precision和Recall。# eval_by_class.py import argparse from pathlib import Path from torchvision.ops import box_iou import torch def evaluate(pred_dir: str, gt_dir: str, nc: int, iou_thr: float 0.5): stats {i: {TP: 0, FP: 0, FN: 0} for i in range(nc)} for pred_file in Path(pred_dir).glob(*.txt): gt_file Path(gt_dir) / pred_file.name if not gt_file.exists(): continue preds [list(map(float, l.split())) for l in pred_file.read_text().strip().splitlines()] gts [list(map(float, l.split())) for l in gt_file.read_text().strip().splitlines() if l.strip()] gt_boxes [g[1:] for g in gts] gt_cls [int(g[0]) for g in gts] matched set() for p in preds: pcls int(p[0]) pbox torch.tensor([p[1:]]) # 用真实框计算IoU这里按预测类别与真实类别一致才计匹配 for gi, gbox in enumerate(gt_boxes): if gi in matched or gt_cls[gi] ! pcls: continue iou box_iou(pbox, torch.tensor([gbox])).item() if iou iou_thr: stats[pcls][TP] 1 matched.add(gi) break else: stats[pcls][FP] 1 for gi, gcls in enumerate(gt_cls): if gi not in matched: stats[gcls][FN] 1 for cid, s in stats.items(): prec s[TP] / (s[TP] s[FP]) if s[TP] s[FP] else 0 rec s[TP] / (s[TP] s[FN]) if s[TP] s[FN] else 0 print(f类别{cid}: Precision{prec:.3f} Recall{rec:.3f} TP{s[TP]} FP{s[FP]} FN{s[FN]}) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--pred_dir, requiredTrue) parser.add_argument(--gt_dir, requiredTrue) parser.add_argument(--nc, typeint, requiredTrue) args parser.parse_args() evaluate(args.pred_dir, args.gt_dir, args.nc)脚本把每个预测框和真实框做IoU匹配IoU大于0.5且类别相同才算命中。跑完你会看到可能溃疡类的Recall只有0.6而机械伤类有0.9这说明该优先补哪类数据一目了然。再往前一步是给项目加一个“分级输出”的交付功能按推理结果给每个果子打等级无缺陷是一级果有少量机械伤是二级果有腐烂直接报废。这个功能不复杂在推理脚本里统计每个果子的缺陷数量和最高严重级别即可但它让整个项目从“画框”变成了“能决策”答辩时是加分项。我还在做第一个缺陷检测项目时只顾着刷mAP结果被现场问了一句“烂果漏检率超过多少就不能上线”当场答不上来。从那以后我做这类项目都先定验收口径再调模型。现在拿到柚子缺陷检测我会把“允许5%以内的漏检、10%以内的误检”写进项目文档第一页再针对这两个数字调conf和补数据。先想清楚怎么验收再动手训练这个顺序能让整个项目少走一半弯路。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。