YOLO目标检测实战:从水稻稻穗数据集看数据标注与训练流程
2026/9/14 23:54:33 网站建设 项目流程

简介:面向YOLO目标检测实战的稻穗检测数据集,按YOLOv5标准目录保存,标签采用classes、x_centre、y_centre、w、h的yolo相对坐标格式。训练集含6108张图片与对应txt标签,验证集含530张图片与标签,类别仅稻穗1类,可直接投入模型训练与验证。压缩包共2000个文件,以txt标注文件为主,另附一个show.py可视化脚本,随机传入一张图片即可绘制边界框并保存至当前目录,无需改参即可运行,方便快速检查标注质量。资源整体约91.54MB,已有258人学习。随包提供标签类别txt文件,配合可视化脚本能快速核对标注效果,适合研究生、开发者及农业AI入门者快速复现YOLOv5实验、调参对比,省去数据整理与格式转换时间,可直接用于智慧农业稻穗计数、长势分析等方向。

1. 这个“1类”稻穗数据集,先看清它解决的是什么问题

一个只包含一个类别“rice_spike”的水稻稻穗检测数据集,为什么值得单独说?因为目标检测里,多类问题拼的是类别区分度,1类问题拼的是对目标的定位精度和密集场景的鲁棒性。稻穗细长、互相遮挡、背景纹理复杂,且一株稻子上有多个稻穗,这种目标的标注质量直接决定YOLO训练能不能收敛到可用水平。这个数据集把train/val/test已经划分好,附带class文件和数据可视化脚本,意味着拿到手只需要做一件事:跑通YOLO训练流程,不需要再花时间整理标签和拆分数据。适合两类人:一类是想快速上手YOLO训练流程、验证环境配置的初学者;另一类是正在做作物计数、产量预估或田间表型分析,需要一份干净的单类农业数据做迁移学习或算法验证的工程师。

2. YOLO数据集的目录结构和class文件:划分、格式与常见坑

2.1 目录划分:images/labels 与 train/val/test 的对应关系

拿到数据集后,第一件事不是看图片,而是看目录结构。YOLO系列(无论是YOLOv5、YOLOv8还是当前更新的版本)对数据集的通用约定是:图片和标签分目录存放,训练集、验证集、测试集各自独立成文件夹。这个水稻稻穗数据集的典型结构如下:

rice_spike_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── class.txt # 类别文件,1行1类 └── visualize.py # 数据可视化脚本
# 用 tree 命令快速确认目录层级和文件数量 cd rice_spike_dataset tree -L 2 # 统计各划分下的图片数量,和 labels 目录做对照 echo "train images: $(ls images/train | wc -l)" echo "train labels: $(ls labels/train | wc -l)" echo "val images: $(ls images/val | wc -l)" echo "val labels: $(ls labels/val | wc -l)"

目录结构确认之后要核对一个关键点:每张图片都必须有对应的同名txt标签文件。YOLO要求图片和标签的主文件名一致,仅扩展名不同,例如IMG_0001.jpg对应IMG_0001.txt。如果某一划分下图片数量和标签数量对不上,说明数据有缺漏,训练时会在加载阶段报 “Image not found in labels” 之类的警告,这个需要在训练前解决。

这里尤其提醒一点:不要直接用 val 当 test 用。水稻稻穗场景的 test 划分是刻意留出来的,它的作用是评估模型在未见数据上的泛化能力。如果训练时把 test 也合并进 train,最后的评估指标就失真了。第一次跑通流程时,保持原有的划分比例不变。

2.2 class.txt:1类问题也要写对,类别索引从0开始

class.txt 里是类别名,每行一个。对于这个数据集,内容只有一行:

rice_spike

这就是整个数据集唯一的检测目标。理解 class.txt 的作用要从 YOLO 标签文件的结构说起。每个 txt 文件里存放的是目标框的归一化信息,每一行代表一个稻穗,格式为:

class_id x_center y_center width height

class_id 对应 class.txt 中类别所在的行号,行号从 0 开始。比如上面 class.txt 中只有 rice_spike,那么所有稻穗的 class_id 都是 0。如果数据集有 2 个类别,class.txt 是rice_spikeweed,则 rice_spike 是 0,weed 是 1。

打开任意一个 labels/train 下的 txt 文件,看到的多半是这样的内容:

0 0.492187 0.367200 0.082031 0.145600 0 0.743750 0.711400 0.090625 0.122000 0 0.219531 0.840600 0.070312 0.168400

注意看这四个坐标值,全部是 0 到 1 之间的浮点数。x_center、y_center 是目标框中心点相对于图像宽高的比例,width、height 是目标框宽高相对于图像宽高的比例,不是像素绝对值。YOLO 训练时会按这个归一化坐标去解码真实框位置。如果你从其它格式转换而来,例如 COCO 的像素坐标格式,这一条换算关系容易忽略,一旦直接填像素值,训练时损失函数会计算出非常离谱的初始损失。

此外还要注意 class.txt 的命名,不同 YOLO 版本对类别文件的默认文件名不同。YOLOv5 常见的是dataset.yaml文件里写names,而不是外置 class.txt;YOLOv8 和更新的版本同样支持 yaml 中定义 classes。这个数据集给的class.txt是通用格式,需要手动将它对应到训练脚本的配置里。

# 这份是 YOLOv8 训练时对应的 dataset.yaml 写法,class.txt 内容需要迁移到这里 path: rice_spike_dataset/ train: images/train val: images/val test: images/test names: 0: rice_spike

注意这里的names键必须是字典形式,或一个列表['rice_spike']。如果你忘了写names,训练器会报AssertionError: class names are not provided之类的内容。这个错误在刚接触自己的数据集时非常常见。

2.3 划分比例和数据来源:不必迷信 7:2:1

一个刚拿到手就划分好的数据集,省去的是最容易出错的一步:随机划分时图片和标签被拆分到不同目录、同场景的连续帧被同时分到训练和验证集。这个数据集已有的划分,大概率是按地块或采集时间切分的,这比纯随机划分更合理,因为相邻时间的图像高度相似,如果被拆到 train 和 val 中,验证集会虚高,无法反映模型在非同一时段数据上的表现。

常见做法中,划分比例有一个可参考的基线:训练集和验证集按 8:2 或 9:1 分配,测试集单独预留。目标检测中的划分不必死守 7:2:1,因为验证集和测试集的职责不同:验证集是每轮训练后评估模型用的,测试集是整个训练完成后一次性评估用的。对于稻穗检测这种单类目标检测任务,训练集占比可以到 85% 以上,验证集承担调参功能即可,宁小勿大。

用命令统计一下划分情况:

# 统计每个集合中的图像数量分布 find images -type f -name "*.jpg" | sed 's|/.*||' | sort | uniq -c # 统计标签数量 find labels -type f -name "*.txt" | sed 's|/.*||' | sort | uniq -c

输出大致是:

val 58 train 498 test 120

这个比例是合理的。如果 train 占比过小(比如低于 70%),模型容易欠拟合,尤其稻穗这种小目标密集的场景,数据量更敏感。需不需要做数据增强,等第一轮训练后看验证集 mAP 再决定,不要在一开始就强行做 Mosaic 之外的增强。

3. 数据可视化脚本:把YOLO标注画回图像,验证标签和图像是否对齐

3.1 可视化脚本需要做哪三件事

数据可视化脚本是这个数据集里最有价值的配套文件。“可视化”不只是把图显示出来,它承担三件事:第一,快速判断标注框是否和目标位置吻合;第二,检查是否存在漏标、重复框、异常宽高比;第三,让没看过原始图像的人也能直观理解数据质量。

常见的可视化思路是:读入一张图像,读入对应 txt 的每一行,通过类别名和归一化坐标,将框画在图像上,并标出类别文本。YOLO 格式的处理非常简单,用 OpenCV 就能实现,不需要引入额外的深度学习框架。最小实现如下:

import cv2 import os def draw_yolo_boxes(image_path, label_path, class_names, output_path=None): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path, "r") as f: for line in f.readlines(): line = line.strip() if not line: continue parts = line.split() class_id = int(parts[0]) x_center = float(parts[1]) y_center = float(parts[2]) box_w = float(parts[3]) box_h = float(parts[4]) # 归一化坐标 -> 像素坐标 x1 = int((x_center - box_w / 2) * w) y1 = int((y_center - box_h / 2) * h) x2 = int((x_center + box_w / 2) * w) y2 = int((y_center + box_h / 2) * h) # 画框和类别文本 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label_text = class_names[class_id] if class_id < len(class_names) else str(class_id) cv2.putText(img, label_text, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) if output_path: cv2.imwrite(output_path, img) print(f"saved to {output_path}") else: cv2.imshow("visualize", img) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == "__main__": class_txt = "class.txt" with open(class_txt, "r") as f: class_names = [line.strip() for line in f.readlines() if line.strip()] draw_yolo_boxes( image_path="images/train/IMG_0001.jpg", label_path="labels/train/IMG_0001.txt", class_names=class_names, output_path="output_preview.jpg" )

这段脚本做了四件关键的事:把归一化坐标还原成像素坐标;把 class_id 映射成可读的类别名;把框叠加到原图上;可保存为文件而不是直接窗口显示。参数上要注意box_w / 2的计算顺序,避免整型运算导致除零偏差。

运行后打开output_preview.jpg,重点关注稻穗的框是否完整包裹住穗体、框的上下边缘是否切掉了一部分穗尖、是否出现同一个稻穗被两个框重叠标注的情况。这些细节决定了训练出来的模型对穗边界的敏感度。如果框普遍偏离,说明数据集本身标注偏差大,后续训练后的 mAP 上限会受影响。

3.2 批量快速浏览:用“抽帧”方式检查全部数据

单张图逐张看效率太低。更快的方式是每次从 images/train 中随机抽取 10 张图,批量调用可视化函数,把结果拼成一张大图输出。这样可以快速覆盖不同光照、不同密度的图像,不需要人工打开几十张原始图片。

import os import random import cv2 import math # 将多张可视化结果拼成一张网格图 def visualize_grid(image_dir, label_dir, class_names, sample_count=9, grid_cols=3): images = [f for f in os.listdir(image_dir) if f.endswith((".jpg", ".jpeg", ".png"))] sampled = random.sample(images, min(sample_count, len(images))) cell_h, cell_w = 480, 640 rows = math.ceil(len(sampled) / grid_cols) grid_img = np.zeros((cell_h * rows, cell_w * grid_cols, 3), dtype=np.uint8) for idx, img_name in enumerate(sampled): label_path = os.path.join(label_dir, os.path.splitext(img_name)[0] + ".txt") img = cv2.imread(os.path.join(image_dir, img_name)) img = cv2.resize(img, (cell_w, cell_h)) # 标注逻辑与上一节相同,这里省略重复画框代码 for line in open(label_path): parts = line.strip().split() x_center, y_center, box_w, box_h = map(float, parts[1:]) x1 = int((x_center - box_w / 2) * cell_w) y1 = int((y_center - box_h / 2) * cell_h) x2 = int((x_center + box_w / 2) * cell_w) y2 = int((y_center + box_h / 2) * cell_h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) r, c = divmod(idx, grid_cols) grid_img[r * cell_h:(r + 1) * cell_h, c * cell_w:(c + 1) * cell_w] = img cv2.imwrite("grid_preview.jpg", grid_img)

注意这里有一个细节:我在画框时使用了cell_wcell_h做坐标换算,而不是原始图像的宽高。因为图像被 resize 成 640x480 后,归一化坐标对应的像素位置已经变化。如果你的脚本里画框位置整体偏移,十有八九是这里出了问题——用原图宽高画框,却输出到 resize 后的图。

可视化检查的核心标准有三条:小目标是否清晰、框是否紧密贴合目标边界、是否存在漏标。批量抽帧看 3 到 5 轮,覆盖超过 50% 的 train 图像,数据质量基本能掌握。

3.3 从可视化结果反推训练效果

可视化不仅能看标注质量,还能预判训练结果。如果发现大部分稻穗框面积占整张图的比例小于 1%,说明数据集以小目标为主,训练时就需要注意三个参数的适配。首先,img_size不能设得太小。其次,损失函数中针对小目标的部分可能需要调整权重。最后,如果使用 YOLOv8 的默认 anchor-free 结构,小目标的召回率通常还可以,但当目标密集交叠时,NMS 的 IoU 阈值需要往下调。

观察可视化图中框的密度也能推测训练时的正负样本平衡情况。稻穗密集区域如果多个框重叠严重,模型在计算损失时会把这些框当作邻近的正样本。这时可以采用 YOLO 自带的 mosaic 增强来缓解,但 batch size 不够大时会引入一些拼接痕迹。这些判断都来自对数据可视化的直观观察,比直接看训练曲线更前置。

# 观察到的框宽高比范围,用于后续数据分析 # 如果 width/height 普遍小于 0.5,说明稻穗以竖直长条形为主 # 训练时 anchor 的选择要偏向高瘦比例

4. 标签、坐标和负样本:数据进入YOLO前的三类错误自查

4.1 坐标归一化与边界检查:labels 里的数字可能不合法

可视化脚本能发现明显画错的框,但有一类错误是可视化也难暴露的,即坐标值本身不合法。YOLO 对归一化坐标没有做强制校验,有些标注工具会导出x_center为负数或width为 0 的标签,这些非法值在训练中会生成 NaN 损失或异常梯度。常见原因是人工标注时框超出了图像边界,或自动标注脚本对空目标生成了占位记录。

写一个简单的检查脚本扫描全部标签:

import os def check_labels(labels_dir): problem_files = [] for root, _, files in os.walk(labels_dir): for fname in files: if not fname.endswith(".txt"): continue path = os.path.join(root, fname) with open(path, "r") as f: lines = [line.strip() for line in f if line.strip()] if len(lines) == 0: print(f"[EMPTY] {path}") problem_files.append(path) continue for line in lines: parts = line.split() if len(parts) != 5: print(f"[FORMAT] {path}: {line}") problem_files.append(path) continue cls_id = int(parts[0]) values = list(map(float, parts[1:])) if cls_id < 0: print(f"[CLASS] {path}: class_id {cls_id}") for v in values: if not (0.0 <= v <= 1.0): print(f"[RANGE] {path}: {line}") problem_files.append(path) break if values[2] == 0.0 or values[3] == 0.0: print(f"[ZERO_SIZE] {path}: {line}") problem_files.append(path) print(f"scanned finished. problem count: {len(problem_files)}") return problem_files check_labels("labels/train")

这个脚本检查了五类问题:空标签、字段数量不等于 5、类别 ID 越界、坐标超出 0 到 1 的范围、宽或高为 0。显示屏上没有任何输出时,说明这批数据格式干净。出现[RANGE]时,常见的处理方式是裁掉越界的框,而不是删除整张图,因为稻穗长在图像边缘的情况很常见。

这些检查在 YOLO 训练流程中非常重要,某些框架在加载数据时会用向量的方式做缩放,碰到负坐标时会产生一个无法预估的错位框。灌进训练循环后,这些坏数据会导致前几个 epoch 的 loss 异常大,且不易定位。

4.2 空标签与负样本的问题

稻穗数据集中会存在部分图像没有稻穗的情况。这种图在农业场景中很常见,例如插秧初期、收割后的稻田。没有目标的图像对应的标签文件是空的(0 字节),它们对训练并不是毫无价值。在目标检测中这叫负样本,作用是降低误检率,让模型学会在无目标区域不产生框。

如果数据集中空标签的比例较低(小于 5%),可以保留。如果比例过高,假设 20% 的图片都没有稻穗,模型会偏向于“不检测”,对中等密度的稻穗容易出现漏检。这种情况下优先做的不是删除负样本,而是统计负样本比例,然后考虑分阶段训练或在 loss 中调整正样本权重。统计空标签的脚本:

# 一行命令统计空标签数量及其占比 find labels/train -name "*.txt" -size 0 | wc -l

建议不要直接删负样本,保留它们对抑制误检有帮助,尤其对于稻田背景中的叶子边缘、水滴反光这些容易误判的区域。判断是否需要处理空标签,关键看训练后的 precision 和 recall 曲线,如果 recall 比 precision 低得多,而训练集中稻穗密度本身高,那就不是负样本的问题,而是小目标漏检问题。

4.3 目标尺寸分布对损失函数的影响

单类目标检测不需要担心类别间不平衡,但要关注目标尺寸分布。稻穗在田间拍摄的图像中表现为细长形,根据可视化的观察可以估算,许多框的宽度小于图像宽度的 5%,高度小于图像高度 10%。这类小目标在 YOLO 的多尺度特征图中,主要落在浅层特征图对应的大分辨率网格上。

在 YOLOv8 训练时,可以通过--imgsz参数调整输入尺寸。假设原始图像是 1280x720,将imgsz从 640 提升到 960,小目标的像素占比会上升,特征图上的响应更明显。但代价是训练时间增加、显存占用上升。对于单类稻穗数据,建议先用默认 640 跑一轮,观察验证集的 mAP@0.5;如果小目标漏检严重,再提 img_size 或者其他增强策略。

还有一个常见做法是增大mosaic概率和copy_paste增强,把多个稻穗背景拼接起来来增加训练样本的上下文多样性。不过这一步在数据量充足时收益不大,数据量在 500 张左右时收益才明显。

5. 训练前最后检查的一步:把可视化流程接到YOLO验证里

可视化脚本不只是训练前的一次性工具,训练完成后它同样有第二层用途:把验证集图片推给训练好的模型推理,再将结果和数据集原标签画在同一张图上,直观对比预测框和地面真值框的差异。这比单纯看 mAP 数值更能定位问题。

最直接的做法是用val集图片跑推理,并调用训练好的模型输出可视化结果:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") # 对验证集的某一张图做推理 results = model.predict( source="images/val/IMG_0042.jpg", conf=0.25, iou=0.5, save=True, save_txt=True, show_labels=True, show_conf=False, ) # 输出结果保存在 runs/detect/predict 下,可以直接与数据集的 ground truth 对比

然后对照同图的可视化原图检查三类现象:预测框是否比标注框更大或更小、是否有多个预测框挤在同一稻穗上、密集区域是否有漏检。

这里有一个推荐的技巧:把第 3 节中的可视化脚本和 validate 结果的输出路径对接,写一个compare.py,让它生成两张并排图——左边是标注框,右边是预测框。因为两张图用的是同一个随机种子,稻穗的空间位置一致,人眼容易直接观察差异。注意两个脚本要用完全相同的resize逻辑,否则坐标错位会造成误判。

稻穗检测的收尾可以卡一个具体指标:查看验证集上模型的mAP@0.5是否已经稳定在 0.9 以上。达到这个数值后,把 test 集跑一遍推理并记录mAP@0.5:0.95的值,两者差距如果小于 2%,说明模型没有明显过拟合。这个数据集里的 test 划分,就是为这个步骤预留的,不要提前把它混进训练。至此,从数据洞察、格式校验、可视化确认到训练评估,整个 YOLO 稻穗检测闭环已经完整走通,后续的工作就是按地块、按光照条件逐步扩充数据。

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

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

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

立即咨询