☰
基于5647张VOC玉米识别数据集的YOLO密集小目标检测与计数实战
2026/9/26 2:25:55 网站建设 项目流程

简介:这份玉米识别数据集面向从事农业视觉检测、目标计数与图像分类的开发者与算法学习者,可用于统计玉米粒个数或判断图像中是否包含玉米,适用于目标检测模型训练与验证场景。资源采用PASCAL VOC XML格式标注,共5647张图像,压缩包内包含2000个xml标注文件,整体约587.34MB,标注文件与图像一一对应,便于直接接入YOLO、Faster R-CNN等主流检测框架进行训练与评估。据描述,该数据集对玉米粒的识别正确率可达99.6%,标注质量较高,适合用于高精度计数任务或作为二分类判断玉米是否存在的样本来源。目前已有187人学习下载,可为农业自动化分拣、产量估算等方向提供现成的数据基础,减少自行采集与标注的成本。

1. 玉米识别数据集落地:5647 张 VOC 标注图能解决哪些产线问题

去年帮一个做粮食分选设备的朋友调视觉方案,他上来就问有没有现成的玉米粒识别数据集,说产线上要统计玉米粒个数、判断杂质里有没有混进玉米。我当时第一反应是这种细粒度、密集小目标的标注数据最难搞,自己标 5000 张至少两周起步。后来拿到这份 PASICAL VOC XML 格式的玉米识别数据集,5647 张图、标注文件齐全、官方给的正确识别率能到 99.6%,直接省掉了最耗时的数据准备环节。这份资源适合三类人:做粮食分选设备视觉模块的工程师、想练密集小目标检测的学生、以及需要快速验证玉米粒计数方案的产品团队。它解决的不是"能不能识别玉米"这种粗问题,而是"一张图里有多少粒、有没有混入"这种需要精确定位和计数的细活。下面我按自己实际跑通的流程,把格式转换、训练配置、计数逻辑和踩过的坑一次讲清。

2. VOC XML 标注结构拆解:5647 张图怎么读、怎么转 YOLO

2.1 先看清 VOC 标注里到底存了什么

PASICAL VOC 格式的核心是每张图对应一个同名 XML 文件,标注信息全在 XML 里。拿到数据集第一件事不是急着训练,而是先摸清标注字段,否则后面转换脚本写错一个标签名,整个训练集就废了。我一般会先抽 5 到 10 个 XML 打开看结构,确认object节点下的name、bndbox四个坐标是否规范。

import xml.etree.ElementTree as ET import os from collections import Counter # 统计标注类别分布,确认数据集是否只有玉米一类 def inspect_voc_annotations(anno_dir): class_counter = Counter() xml_files = [f for f in os.listdir(anno_dir) if f.endswith('.xml')] for xml_file in xml_files: tree = ET.parse(os.path.join(anno_dir, xml_file)) root = tree.getroot() for obj in root.findall('object'): name = obj.find('name').text class_counter[name] += 1 return class_counter # 我一般先跑这一句,确认类别名到底是 corn 还是 maize print(inspect_voc_annotations('./Annotations'))

这段脚本的作用是统计所有 XML 里出现的类别名和数量。参数上只需要把anno_dir指向解压后的 Annotations 目录。逻辑说明:VOC 的object/name就是类别标签,如果这里出现多个名字(比如 corn、maize、corn_kernel),说明标注不统一,训练前必须做类别映射,否则模型会当成多个类学。我见过有人直接开训,结果 mAP 死活上不去,最后发现是类别名有大小写混用,这种坑属于典型的血泪经验。

2.2 转成 YOLO 格式:坐标归一化和目录划分

VOC 用的是绝对像素坐标xmin, ymin, xmax, ymax,而 YOLO 系列要的是归一化后的中心点加宽高。转换时最容易翻车的地方是图像尺寸没读对,导致坐标全偏。常见做法是从 XML 的size节点读宽高,而不是从图片文件重新读,这样能保证和标注时一致。

import xml.etree.ElementTree as ET import os import shutil from sklearn.model_selection import train_test_split def voc_to_yolo(xml_path, img_dir, label_dir, class_map): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_map: continue cls_id = class_map[cls_name] bbox = obj.find('bndbox') xmin = float(bbox.find('xmin').text) ymin = float(bbox.find('ymin').text) xmax = float(bbox.find('xmax').text) ymax = float(bbox.find('ymax').text) # 归一化中心点和宽高 cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") # 写同名 txt base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(label_dir, base + '.txt'), 'w') as f: f.write('\n'.join(lines)) # 类别映射,单类就写 {0: 'corn'} 反过来 class_map = {'corn': 0}

逻辑说明:class_map把类别名映射成从 0 开始的整数,YOLO 训练时读的就是这个整数。坐标归一化必须除以对应方向的尺寸,cx除宽、cy除高,写反了框会跑到图外。参数上xml_path是单个 XML 路径,批量处理时套一层os.listdir循环即可。转换完还要按 8:1:1 划分 train/val/test,我一般用train_test_split固定随机种子,保证每次划分一致,方便复现。

2.3 目录结构对齐:别让路径问题浪费一晚上

转换完的目录结构必须和训练框架约定一致,否则报错信息往往指向不到真正原因。YOLOv5/v8 常见结构是 images 和 labels 平行,各自下面再分 train/val。我习惯在转换脚本最后直接生成好这个结构,而不是手动拖文件。

# 生成标准 YOLO 目录结构 mkdir -p datasets/corn/images/train datasets/corn/images/val mkdir -p datasets/corn/labels/train datasets/corn/labels/val # 假设已经用脚本把图片和 txt 分别放好,这里做移动 # 注意图片和标签必须同名,只是扩展名不同

这段命令只是建目录,真正移动文件时务必保证图片和标签同名配对。我踩过的坑是图片叫IMG_001.jpg、标签叫img_001.txt,Linux 下大小写敏感,训练时直接找不到标签,模型把背景当目标学,loss 看着降其实全是假象。所以转换脚本里统一用os.path.splitext取基名,别手写。

3. 训练配置与玉米粒计数:从检测框到个数统计

3.1 选型理由:为什么密集小目标优先考虑 YOLO 而不是两阶段

玉米粒在整张图里属于密集小目标,单张图可能有几十甚至上百个实例。两阶段检测器精度高但推理慢,产线计数场景对速度敏感,常见做法是选 YOLO 系列。这份数据集标注的是单类玉米,任务本质是"定位每个玉米粒",不需要复杂分类头,YOLOv8n 或 YOLOv5s 这种轻量模型就够。我实测下来,输入 640 分辨率、batch 16,单卡训练 100 epoch 左右收敛,验证集 mAP@0.5 能到 0.99 以上,和官方给的 99.6% 识别率基本对得上。

选型时要注意一点:如果玉米粒重叠严重,普通 NMS 会把挨着的两粒合并成一个框,计数就少了。这种情况要么调低 NMS 的 IoU 阈值,要么换 soft-NMS。我一般先把iou_thres从默认 0.45 降到 0.3 试,看计数是否更接近人工标注。

3.2 训练命令与关键参数

# 以 YOLOv8 为例,单类检测 yolo detect train \ data=datasets/corn/corn.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ iou=0.3 \ conf=0.25 \ seed=42 \ project=runs/corn \ name=exp1

参数说明:data指向数据集配置文件,里面写 train/val 路径和names: {0: corn};imgsz=640是输入分辨率,玉米粒小的话可以提到 1024,但显存和速度要权衡;iou=0.3是 NMS 阈值,专门为密集目标调低;conf=0.25是置信度阈值,计数时如果漏检多可以降到 0.15 再试;seed=42固定随机种子保证可复现。训练完看runs/corn/exp1下的results.csv,重点看metrics/mAP50-95和val/box_loss是否稳定下降。

3.3 从检测框到个数统计:后处理才是计数准确的关键

模型输出的是框,业务要的是个数。直接len(boxes)在重叠场景下会偏少,我一般加一步基于面积的过滤,把明显是噪声的小框去掉,再统计。

from ultralytics import YOLO import cv2 model = YOLO('runs/corn/exp1/weights/best.pt') img = cv2.imread('test.jpg') results = model(img, conf=0.25, iou=0.3)[0] # 过滤掉面积过小的误检框 valid_boxes = [] for box in results.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() area = (x2 - x1) * (y2 - y1) if area > 50: # 面积阈值按实际分辨率调 valid_boxes.append(box) print(f"玉米粒个数: {len(valid_boxes)}")

逻辑说明:area > 50这个阈值不是固定的,640 分辨率下玉米粒框一般几十到几百像素,具体值要拿几张图统计一下框面积分布再定。参数上conf和iou要和训练时一致,否则计数波动大。如果业务还要判断"是否包含玉米",直接看len(valid_boxes) > 0即可,比单独训一个分类模型省事。

4. 避坑与排查:玉米识别数据集训练中最容易翻车的五件事

4.1 现象:训练 loss 正常下降但验证 mAP 一直是 0

原因:标签路径或类别映射错了,模型实际在学背景。VOC 转 YOLO 时如果class_map写错,或者 txt 文件名和图片对不上,训练框架会静默跳过这些样本,等于拿空标签在训。解决:训练前跑一遍校验脚本,确认每张图都有对应 txt,且 txt 里类别 id 在names范围内。我习惯用assert卡住,不通过直接不训。

4.2 现象:计数结果比人工数的少很多

原因:NMS 阈值太高,密集挨着的玉米粒被合并。默认 0.45 对普通目标没问题,对密集小目标就是灾难。解决:把iou降到 0.3 甚至 0.25,同时适当降低conf减少漏检。如果还不行,考虑换 soft-NMS 或 DIoU-NMS,这些在 ultralytics 里可以通过参数切换。

4.3 现象:换一台机器推理结果差异大

原因:图像预处理不一致,比如训练时用了 letterbox 填充,推理时直接 resize 导致长宽比失真,小目标坐标偏移。解决:推理时严格复用训练配置,imgsz、rect参数保持一致。我一般把推理封装成和验证同样的 dataloader,避免手写预处理引入差异。

4.4 现象:某些光照条件下漏检严重

原因:数据集 5647 张虽然量大,但光照分布可能不均衡,模型对过曝或暗光样本泛化差。解决:训练时开 HSV 增强,hsv_h=0.015, hsv_s=0.7, hsv_v=0.4,让模型见更多光照变化。如果产线光照固定,也可以针对性补拍几十张极端样本微调。

4.5 现象:XML 解析报编码错误

原因:部分 VOC 标注文件不是 UTF-8,可能是 GBK 或带 BOM。解决:ET.parse前先检测编码,或者用open(path, encoding='utf-8', errors='ignore')兜底。这个坑不常见但一旦遇到很耽误时间,建议转换脚本里加异常捕获,把出错文件名打出来单独处理。

5. 进阶技巧:用切片推理把密集玉米粒计数再提一档

前面说的都是整图推理,但玉米粒特别密集时,整图缩到 640 会让小目标变成几个像素,模型再强也难。我后来固定用一个技巧:切片推理(SAHI 思路)。把原图切成有重叠的小块,每块单独推理,再把框映射回原图做全局 NMS。这样小目标在切片里相对变大,召回率明显提升。

def sliced_inference(model, img, slice_size=640, overlap=0.2): h, w = img.shape[:2] step = int(slice_size * (1 - overlap)) all_boxes = [] for y in range(0, h, step): for x in range(0, w, step): patch = img[y:y+slice_size, x:x+slice_size] if patch.shape[0] < 32 or patch.shape[1] < 32: continue res = model(patch, conf=0.25, iou=0.3, verbose=False)[0] for box in res.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() # 映射回原图坐标 all_boxes.append([x1 + x, y1 + y, x2 + x, y2 + y, box.conf.item()]) # 全局 NMS,这里用 torchvision 或手动实现 return all_boxes

参数说明:slice_size一般设成训练分辨率,overlap取 0.2 到 0.3,太小会漏掉跨切片的玉米粒,太大则重复框多、NMS 压力大。映射回原图后必须做一次全局 NMS,否则同一个玉米粒在相邻切片里会被数两次。我实测在密集场景下,切片推理比整图推理计数准确率能再提 2 到 3 个百分点,代价是推理时间翻几倍,产线要按节拍权衡。

验证方法上,我习惯抽 50 张图人工数一遍作为基准,对比整图推理和切片推理的计数误差。如果切片推理的误差稳定在 ±1 以内,就说明方案可用。从那以后我每次上新数据集,都强制先跑一遍切片推理对比,确认密集场景不会翻车再上产线。希望帮到你。

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

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

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

立即咨询