☰
甘蔗病害目标检测数据与YOLO标注格式全解析:从3300张图到模型落地
2026/9/27 23:09:43 网站建设 项目流程

简介:面向甘蔗病害识别场景的 YOLO 格式目标检测数据集,覆盖健康、黄叶病、锈病等共 4 个类别,已划分训练集、验证集与测试集,可直接用于 YOLOv5/YOLOv8 等模型的训练与验证。整个资源包共包含 2000 个文件,以 1679 个 txt 标注文件与 320 张 jpg 图像为主体,另附 1 个数据可视化 Python 脚本,压缩后大小约 68.58MB。txt 标注文件负责保存类别编号与边界框坐标,jpg 图像为原始叶片样本,配合 show 脚本可快速查看检测标签与图像的匹配情况,便于评估标注质量并调整训练策略。目前该数据集已有 148 人浏览学习,适合计算机视觉初学者、农业信息化研究人员以及关注目标检测模型改进的开发者参考使用。通过它可完成从数据读取、类别映射、模型训练到结果评估的完整流程,为甘蔗病害智能化监测提供基础数据支撑。

1. 甘蔗病害目标检测数据:3,300 张标注图到底能帮你解决什么问题

做农业视觉落地的同行应该都有体会:甘蔗病害识别难的不是模型,是数据。病斑形状不规则,早期和后期外观差异大,叶鞘、枯叶、阴影又特别容易干扰判断。这份甘蔗病害图像目标检测数据约 3,300 张,每张图配 YOLO 标注格式的标签文件,直接把"从零拍照 + 手动标注"最耗时的环节砍掉了。拿到手你要做的事很清楚:检查标注质量、按 YOLO 目录规范整理、训练一个能区分病害种类的目标检测模型。适合想快速验证甘蔗植保检测方案的团队,也适合拿来做目标检测项目练手的个人学习者。数据是好数据,但能不能训练出能落地的模型,取决于你怎么对待它,下面从标注格式开始拆。

2. 读懂 YOLO 标注格式:txt 标签里那五列数字的来龙去脉

拿到一份标注数据,第一步不是开训练,而是确认手里的标签到底长什么样。YOLO 标注格式这几年已经成了目标检测领域的事实标准,YOLOv5、YOLOv8、YOLOv11 以及大量端侧推理框架都直接读这套格式。它的形式极简:每张图片对应一个同名 .txt 文件,文件里每一行代表一个标注框。搞清楚这五列数字的含义和边界,后面所有操作才不会翻车。

2.1 为什么用归一化坐标:YOLO 格式的数学逻辑

YOLO 的 txt 每一行固定五列,顺序是类别 id、中心点 x、中心点 y、框宽 w、框高 h。这里有两个反直觉的点。第一,类别 id 是整数,从 0 开始计数,和病害名称没有直接关系,映射关系完全由训练配置里的 names 列表决定,这份数据里具体标的是哪几类、每类叫什么,要以你手上的类别说明为准。第二,四个坐标值全部是归一化之后的相对值,正常范围在 0 到 1 之间。所谓归一化,就是把像素坐标除以图片宽高:cx = x_pixel / img_width,bw = box_width / img_width,y 方向同理。

这套设计的核心动机是尺度无关性。训练时输入图片会被统一缩放,比如原图 3000×2000 压到 640×640 输入,如果用像素绝对值标注,缩放后框的位置和大小全变了;用归一化值,缩放只是等比例变换,模型读到的始终是相对位置。这也是 YOLO 系列多年保持这套格式的原因。另外,目标检测模型里中心点回归和宽高回归是分开算损失的,归一化之后两个方向的误差量纲一致,训练过程更平稳。

理解了这个逻辑,你就明白为什么"坐标越界"是 YOLO 标注里最致命的问题之一。任何一列大于 1 或小于 0,都说明标注或转换过程出了问题,这种脏数据混进训练集,模型会学到根本不存在的框。后面避坑章节会给具体的排查方法。

2.2 把标签读回来画框:一个检查标注质量的脚本

拿到数据的第一时间,先用脚本把标注框画回原图上,肉眼抽查一批。这是验证标签真实性的最直接方式,比任何统计数据都可靠。

import os import cv2 IMG_DIR = "images" # 存放图片的目录 LABEL_DIR = "labels" # 存放同名的 txt 标签目录 def draw_boxes(img_path, txt_path): img = cv2.imread(img_path) if img is None: print(f"图片读取失败: {img_path}") return h, w = img.shape[:2] with open(txt_path, "r", encoding="utf-8") as f: lines = f.read().strip().splitlines() for line in lines: parts = line.split() if len(parts) != 5: print(f"格式异常 {txt_path}: {line}") continue cls_id, cx, cy, bw, bh = parts cls_id = int(cls_id) cx, cy, bw, bh = map(float, (cx, cy, bw, bh)) # 归一化坐标必须在 [0,1],越界说明标注或转换有问题 if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= bw <= 1 and 0 <= bh <= 1): print(f"坐标越界 {txt_path}: {line}") # 归一化坐标乘回图片宽高,得到像素级框 x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls_id), (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow("check " + os.path.basename(img_path), img) cv2.waitKey(0) cv2.destroyAllWindows() # 抽样前 30 张,跑完再换一批 for name in os.listdir(IMG_DIR)[:30]: if not name.lower().endswith((".jpg", ".jpeg", ".png")): continue txt_path = os.path.join(LABEL_DIR, name.rsplit(".", 1)[0] + ".txt") if os.path.exists(txt_path): draw_boxes(os.path.join(IMG_DIR, name), txt_path) else: print(f"缺少标签: {name}")

脚本逻辑不复杂:读图、读同名 txt、把归一化坐标乘回宽高、画框、顺手检查越界。实际使用时我会把 waitKey 逻辑改掉,改成保存标记图到临时目录,批量处理完再统一翻看,不用一张张按键盘。关键是你得真看,不是跑完脚本就算检查过了。

抽样技巧是分三批:前 20 张、中间 20 张、最后 20 张。很多数据集按拍摄批次排序,分布往往不均匀,只看开头会漏掉后面的脏数据。对甘蔗这种叶片密集的场景,重点看叶鞘附近和叶片相互遮挡的区域,那里最容易出现框不准、框住背景的情况。顺手还可以统计一下各类别的框数量分布,如果某类只有几十个框,说明类别严重不平衡,后面训练时要单独处理。

2.3 从其他格式转 YOLO 的四个转换要点

如果你的这份数据将来要和其他来源合并,或者手上还有一批 VOC、COCO 格式的历史数据,转换时注意四个高频坑。第一,类别顺序必须统一,VOC 的 classes.txt 或 COCO 的 categories 顺序决定了转换后的 id,两批数据合并时,同一种病害在两个源里 id 不同是灾难。

第二,归一化的分母必须是每张图自己的宽高,用整个数据集平均宽高来归一化,等于给所有框加了一个随机扰动。第三,坐标系不要搞混。COCO 的 bbox 是左上角坐标加宽高,转成 center 格式要算 (x + w/2) / img_w;OpenCV 读图出来的 shape 顺序是 (h, w),PIL 是 (w, h),这两套混用会让框整体偏移。第四,转完必须做一次回画检查,和 2.2 的脚本一样,别只对比数字。

如果你手上的标注是 LabelImg、LabelStudio 这类常见标注工具导出的,它们默认输出 VOC 格式居多,转过来之后同样要走一遍上面的检查流程。

3. 训练前先调整目录:把 3,300 张图组织成 YOLOv8 的标准结构

标注格式读懂了,下一步是把数据组织成框架能直接吃的结构。这一步最枯燥,但省掉的返工时间最多。YOLO 系框架对目录结构要求很严格,一旦路径或文件名对不上,训练时报的错五花八门,新手最容易在这里卡住。

3.1 标准目录结构:images 与 labels 必须一一对应

YOLOv8 的做法是:images 目录下放图片,labels 目录下放同名 txt,训练时框架只扫描 images,再按同名规则去 labels 里找标签。目录结构是固定套路:

dataset/ ├── images/ │ ├── train/ │ │ ├── canefield_001.jpg │ │ ├── canefield_002.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ │ ├── canefield_001.txt │ │ ├── canefield_002.txt │ │ └── ... │ ├── val/ │ └── test/ └── data.yaml

这套规范里最容易被忽略的两条。其一,images 和 labels 下同一分区的文件名必须完全一致,只能扩展名不同,框架不会帮你做模糊匹配。其二,train 和 val 两个分区里,图片和标签必须一起划分,不能图片分了、标签没分。还有,纯背景图(没有目标的图片)建议单独放一个目录,训练时要么排除要么删掉。背景图混进 train 会让模型学到错误的"无目标惩罚",混进 val 又会影响 mAP 统计口径,后面避坑章节再展开。

3.2 数据划分:按拍摄批次拆,别只靠随机 shuffle

3,300 张图怎么切成 train 和 val?最常见的做法是随机划分,80% 训练、20% 验证。但甘蔗田间数据有个典型问题:同一块地的照片角度、光线、品种高度相似,如果同一拍摄批次的照片同时进了 train 和 val,验证集就等于开卷考试,mAP 虚高。实战中我会在随机基础上多做一件事:按文件名前缀或拍摄批次去重后再切。

import os import random import shutil random.seed(2024) # 固定种子,保证结果可复现 IMG_SRC = "images_all" # 原始图片目录 LBL_SRC = "labels_all" # 原始标签目录 # 收集图片和标签成对的数据 pairs = [] for name in os.listdir(IMG_SRC): if not name.lower().endswith((".jpg", ".jpeg", ".png")): continue txt_name = name.rsplit(".", 1)[0] + ".txt" if os.path.exists(os.path.join(LBL_SRC, txt_name)): pairs.append((name, txt_name)) else: print(f"缺少标签: {name}") random.shuffle(pairs) val_ratio = 0.2 n_val = int(len(pairs) * val_ratio) n_train = len(pairs) - n_val for split_name, files in [("train", pairs[:n_train]), ("val", pairs[n_train:])]: os.makedirs(f"images/{split_name}", exist_ok=True) os.makedirs(f"labels/{split_name}", exist_ok=True) for img_name, txt_name in files: shutil.copy(os.path.join(IMG_SRC, img_name), f"images/{split_name}/{img_name}") shutil.copy(os.path.join(LBL_SRC, txt_name), f"labels/{split_name}/{txt_name}") print(f"train: {n_train}, val: {n_val}")

这里用 copy 而不是 move,是为了保住原始数据这份"后悔药"。训练集和验证集的划分方式直接决定后面所有评估指标的可信度。按批次切的具体做法是:先看文件名里有没有日期或地块编号这类批次信息,再按这个维度分组,把整组划入 train 或 val。随机切只能作为没有批次信息时的兜底方案,切完务必保留种子文件,记录划分逻辑,方便别人复现你的结果。

3.3 data.yaml 怎么写:类别顺序错了全线白练

data.yaml 是训练时的数据描述文件,names 的顺序定义必须和标签里的 id 对齐。假设这份数据标了 3 类病害,示例配置如下:

# data.yaml path: ../dataset # 数据集根目录 train: images/train # 相对 path 的训练图片目录 val: images/val # 相对 path 的验证图片目录 nc: 3 # 类别数 names: # id 从 0 开始 0: cane_red_rot # 具体类别名以你手里的类别定义为准 1: cane_smut 2: cane_leaf_spot

写这个文件最容易踩的坑是把 names 顺序写错。如果标签里 id 0 是某个病害,配置里 id 0 写成了别的,训练不会报错,但模型学到的语义全错位了。每次写完 data.yaml,我都会先确认一遍标签里的 id 分布,再让框架打印类别统计。YOLOv8 里最简单的办法是直接启动一次训练,看第一屏的类别统计;不放心的话,可以先用一小段代码统计 labels 目录里所有 txt 的类别 id 出现次数。

另外注意 nc 的值。它必须等于标签里最大类别 id 加 1。比如标签只出现 0、1、2,nc 就写 3;写 4 也能跑,但多出一个空类会影响类别损失的取值,没必要。标签里如果出现 id = 5 而 nc 只写了 3,训练会直接报错,这也是排查方向之一。

4. 用 3,300 张数据跑通 YOLOv8 训练:模型选型、超参数与命令

数据整理完,进入大多数人最关心的环节:训练。这里给出一套针对 3,300 张中小规模数据集的保守方案,先跑通,再优化。

4.1 选 YOLOv8s 还是 YOLOv8n:三秒定方案

模型选型的逻辑很简单:先小后大。第一次训练的目的是验证数据和标签没问题,不是刷指标。YOLOv8n 是最轻量的模型,训练快,但甘蔗病斑这种小目标场景下检出率偏低;YOLOv8s 是平衡点,精度比 n 高一截,速度慢不了太多;YOLOv8m 及以上适合数据量大、且已经把小模型调明白之后再上。3,300 张图跑 YOLOv8s 是稳妥的起点。

对比一下实际影响:在单张 1080 级别显卡上,YOLOv8s 配 640 分辨率、16 batch,一个 epoch 大约一两分钟,150 个 epoch 三小时上下;同配置下 YOLOv8m 时间接近翻倍。如果你的最终场景是无人机巡田,模型要部署到边缘设备,YOLOv8n 经过量化裁剪也许更合适,但那是后话,先拿 s 把指标跑出来再说。YOLO 系列不同版本的差异主要在训练策略和模块结构上,但数据格式、训练命令大同小异,按你熟悉的版本来即可。

4.2 训练命令与三个必调参数

训练前先装框架:

pip install ultralytics

然后启动训练,以数据配置在 sugarcane.yaml 为例:

yolo detect train \ data=sugarcane.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=30 \ project=runs/sugarcane \ name=exp1

这条命令里最有调整价值的三个参数是 imgsz、batch、patience。imgsz 是输入尺寸,甘蔗病斑很多是小目标,imgsz 设 640 是底线,病斑特别小或者原图分辨率很高的话可以试 960,但显存占用和训练时间明显上升。batch 受显卡显存限制,16 在多数 8GB 显存的卡上能跑 640 分辨率,显存不足就降到 8 或 4,注意也别太低,否则 BN 层统计不稳定。patience 是早停轮数,连续 30 个 epoch 验证指标没提升就自动停,既省电也避免过拟合。

两个容易忽略的细节。其一,model=yolov8s.pt 这个写法会从官方源下载 COCO 预训练权重,数据量小的时候迁移学习收益很大,模型已经会提取通用特征,你相当于在它基础上做领域适配。离线环境就先把权重文件下好放到同目录。其二,load 参数不要乱给,如果你手里有两个训练好的权重,想融合或续跑,用 model=best.pt 再加上 resume=True 是更稳的做法,而不是把模型文件当预训练权重硬接。第一次训练保持默认优化器(SGD)和学习率即可,跑通后再调。

4.3 训练过程中看什么:loss 不降比 mAP 低更值得警惕

训练启动后,终端会实时打印 box_loss、cls_loss、dfl_loss 和验证集 mAP。很多新手只盯着 mAP 看,实际上 loss 曲线更能反映问题。前 20 个 epoch,三个 loss 应该稳步下降;如果 loss 一直横盘不动,先怀疑学习率或数据格式,而不是继续加 epoch。cls_loss 不降说明类别区分有问题,大概率是类别不平衡或标注噪声大;box_loss 不降说明框不收敛,优先检查标注框质量。

训练结束后,runs/sugarcane/exp1/ 目录下会生成 weights/best.pt 和 weights/last.pt,以及一组曲线图。best.pt 是按验证集指标选出的最优权重,后面部署用它;last.pt 是最后一个 epoch 的权重,一般不用。results.png 里能看到 loss 和 mAP 两条曲线,配合着看能判断模型是健康收敛还是过拟合——训练 loss 持续下降但验证 loss 掉头上升,就是过拟合信号。我见过太多人训练完只拷贝一个 best.pt 就走,完全没看曲线,浪费了大量可用的排查信息。

5. 标注数据避坑清单:五个最容易让模型翻车的细节

无论数据是买来的、下载的还是自己标的,标注环节的脏数据问题都会在训练后集中爆发。这一章把高频问题按现象、原因、解决的套路写成清单,遇到类似情况可以对着排查。

5.1 标签和图片数量对不上:训练时莫名报找不到标签

现象:训练刚开始就出现大量 "No labels found in ..." 提示,或者验证时发现参与计算的图片数量只有预期的一半。

原因:数据在拷贝和解压过程中丢了部分 txt 文件;或者原始标注里部分图片没有目标,空的 txt 没有被生成,导致图片和标签不完全一一对应。

解决:写一个文件对比脚本,遍历 images 目录下每张图,检查同名 txt 是否存在,把缺的列出来。确定是空标签的还是丢文件的,空标签的图片要么补一个空 txt,要么直接从数据里移走。我的习惯是宁可图片数少一点,也不要让数据管道在训练中途出问题。

import os img_dir = "images/train" label_dir = "labels/train" missing = [] for name in sorted(os.listdir(img_dir)): if not name.lower().endswith((".jpg", ".jpeg", ".png")): continue txt = os.path.join(label_dir, name.rsplit(".", 1)[0] + ".txt") if not os.path.exists(txt): missing.append(name) print(f"共 {len(missing)} 张图缺标签") for name in missing[:50]: print("缺失:", name)

5.2 归一化坐标越界:框画到了图片外面

现象:用检查脚本跑数据时,发现部分 txt 里的坐标值大于 1 或小于 0;训练出的模型经常预测出跑到图像边缘的框。

原因:坐标转换时没有除以当前图片的宽高;或者把左上角坐标当成了中心点坐标;或者原图经过裁剪缩放后标签没有同步更新。

解决:先全局扫一遍越界文件,确认范围。比例很低就直接丢弃这些框,比例高就要修根因——如果转换脚本写错了,改正后重新转换整批数据,别在脏数据上手工改。越界框里有一种情况值得单独看:center 在图像里但框宽高超出图像范围,这种一般是目标在图像边缘被截断,标注员把可见部分框进来了,模型也能学,但验证时边界框质量会偏低,建议把这些框按图像边界裁剪。

5.3 小病斑漏标:mAP 不低,田间就是漏检

现象:验证集 mAP 看着有 0.8 上下,但拿田间真实照片测试,早期病斑几乎全漏。

原因:早期病斑在图上只有十几个到几十个像素,标注时容易漏;而且 640 分辨率输入下,小目标经过多次下采样,特征几乎丢失。3,300 张里如果小目标占比不高,模型根本没有足够样本去学。

解决:先统计验证集里目标框的尺寸分布,看看宽高小于 32 像素的目标占比。占比高就考虑 imgsz=960 或 1280 训练,或者对图像做切片处理后再训练。另一个容易忽略的问题是,标注时把一个包含多个小病斑的区域直接框成一个大框,模型学到的就是"一个有四个病斑的大框",这在检测任务里是典型的漏标。正确做法是把每个病斑单独框出来,哪怕框只有 8×8 像素。

5.4 类别边界模糊:相似病害互相误检

现象:混淆矩阵里两个类别的互相误检率明显偏高,这两个类的单类 AP 都低。

原因:标注标准不统一。甘蔗不同病害在早期外观高度相似,不同标注员对"什么程度算这个病、什么程度算那个病"的理解不一致,同类样本被分到两个类里,模型被迫在模糊边界上反复横跳。

解决:回到标注层面统一标准。写一份一页纸的标注规范,用典型图把每个类别的判定特征写清楚,比如病斑颜色、边界形状、有没有发黑坏死点。如果误检集中在某两个类,单独抽这两类的验证集样本逐张看,你会发现不少标签本身就标错了。这类问题调模型参数解决不了,只能修数据。尤其中间类和极端类要分开:早期轻微症状单独标注,不要和典型症状混在一个类里。

5.5 把背景当目标:阴影、枯叶、土块都被框了

现象:模型的检出框大量落在叶鞘边缘、阴影交界、枯黄叶片上,检出的"病害"在视觉上根本看不出病灶。

原因:采集环境复杂,标注员在光线不好或图片模糊时,把反光、阴影、机械损伤当成病斑框了。这类噪声在训练集里占比不高,但会让模型学到错误的纹理特征。

解决:宁可没有框,也不要错框。用检查脚本逐张过可疑样本,把明显不是病灶的标签删掉。如果这类噪声集中在某几批图里,直接检查是不是拍摄设备或时间导致的系统性误差。实际处理时我会把删除的标签单独备份,后面发现删错了还有后悔药。这句话也适用于整个数据清洗流程:所有修改都留备份,别在原目录上直接覆盖。

6. 验证与落地:用混淆矩阵和置信度阈值把 mAP 变成可用指标

6.1 看混淆矩阵,别只看 mAP

训练完的 runs/sugarcane/exp1/ 目录里,confusion_matrix.png 比 mAP 数字更有价值。它展示每个真实类别被预测成什么,对角线越亮越健康。如果某两类长期互串,问题几乎出在标注语义上,回到 5.4 的排查流程。另一个必看的是每个类别的单类 AP,YOLOv8 的结果里有 per-class PR 曲线,单类 AP 明显偏低的类别,就是下一步要重点补数据的类别。农业病害检测里最难提的就是小目标和相似类,这类问题靠调阈值只是缓解,根源还是在数据。

6.2 置信度阈值跟着 PR 曲线定,不拍脑袋

部署阶段的 conf 参数默认 0.25,但实际落地时这个值要按 PR 曲线的拐点定。做法是拿一批没参与训练的田间照片跑一遍,把 conf 从 0.5 往下调,观察漏检和误检的平衡点在哪:

from ultralytics import YOLO model = YOLO("runs/sugarcane/exp1/weights/best.pt") results = model("field_photo.jpg", conf=0.35, iou=0.5) for r in results: print(r.boxes.cls, r.boxes.conf, r.boxes.xyxy)

我现在的习惯是,模型训练完先看混淆矩阵和 per-class PR 曲线,再拿 100 张未参与训练的田拍照片做一次批次推理,把三个数打出来对比:漏检率、误检率、单张推理耗时。三个数都满足项目要求之后,再决定阈值和部署方案。这套流程走完,才算真正把这份数据用成了能交付的检测方案,而不是停在"训练出一个模型"这一步。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询