☰
油茶果成熟检测数据集:VOC XML转YOLO格式与YOLOv8训练全流程
2026/10/1 9:26:41 网站建设 项目流程

简介:一套面向油茶儿果实成熟度检测的YOLO专用数据集,适合正在训练目标检测模型或研究农业视觉识别的读者。压缩包共220个文件,包含110张JPG现场图像与110个配套XML标签,整体约276.03MB;其中JPG覆盖不同季节、光照条件以及油茶儿由青涩到成熟的各阶段外观,XML则精准记录每个果实的位置边界框与成熟状态,标注由专业人员手工完成,格式规范,可直接导入YOLO进行训练、验证与调参。与自行采集标注相比,该数据集能显著节省数据准备时间,让使用者把精力集中在模型结构优化和成熟度识别精度提升上,并且因为标签信息完整,也便于做数据增强、难例挖掘等后续处理。已有85人浏览学习,适合作为油茶成熟检测项目起步阶段的基准数据资源,也适合用于农业智能检测相关课程实验。

1. 从“油茶儿成熟检测数据集(图片+xml标签)”到能跑的YOLO模型,差几步转换

看到这个压缩包名,第一反应是有人替你完成了最脏的数据收集。油茶儿,也就是油茶树上摘下来的鲜果,成熟度直接决定出油率和榨油品质,过去靠老师傅用眼看、用手捏。图片+xml标签的数据集,就是把老师傅的判断转成了机器能学的东西:每张图对应一个xml标签文件,框出青果、熟果的位置。目标很明确——接YOLO目标检测,训练一个能在果园现场判断“这果能不能采”的模型。适合两类人:做智慧农业、油茶产业数字化的人,和被标注数据折磨过的YOLO入门者。但要说清楚:xml是VOC标注格式,YOLO不吃xml,需要转换。这篇文章就围绕“审计-转换-训练-避坑-验证”展开。

2. 先审计再动手:VOC xml标签结构、图片对应关系和类别统计

拿到一个写着“图片+xml标签”的压缩包,先别解压完就直接搜“目标检测常用标注工具”然后开始训练。xml标签意味着这份数据用的是PASCAL VOC组织方式,这是目标检测界流传最广的标注格式之一,很多标注工具导出的默认格式就是它。VOC格式对发布方很友好:一张图片配一个同名xml文件,里面是树状结构,不依赖任何私有库,任何语言拿标准库都能解析。对使用方来说,xml反而比直接给YOLO的txt更可靠,因为txt里只剩数字类别id,而xml里保留了完整的类别名字段,你还能改、还能审计。

油茶儿成熟检测这个任务,数据本身不复杂:果实挂在树上,青果是绿色、熟果是黄褐色或红褐色,标注时用矩形框框住目标,再把类别名写成类似green、mature这样的词。但数据集的坑往往不在“检测”而在“标签”。一个几十G的压缩包解压完,很可能出现某些图片没有对应xml、某些xml里混入了拼错的名字、某些标注框画到图片外面去了。这些问题不提前清掉,后面每一步都会跟着翻车。

2.1 一个xml标签文件里到底有什么:object节点、bndbox和name

先看一个最典型的VOC xml长什么样,心里有个底。油茶果成熟检测数据里的标注,通常就是下面这种结构:

<annotation> <filename>IMG_20231015_092831.jpg</filename> <size> <width>1280</width> <height>960</height> </size> <object> <name>mature</name> <bndbox> <xmin>432</xmin> <ymin>287</ymin> <xmax>541</xmax> <ymax>396</ymax> </bndbox> </object> <object> <name>green</name> <bndbox> <xmin>122</xmin> <ymin>544</ymin> <xmax>215</xmax> <ymax>637</ymax> </bndbox> </object> </annotation>

每个object节点代表一个标注目标,name是类别名,bndbox里存的是矩形框的左上角和右下角坐标。注意四个值是绝对的像素坐标,不是归一化的。xmin代表框左边线、ymin代表框上边线,xmax和ymax代表右下角。这是VOC格式和小部分人熟悉的COCO格式最大的区别:COCO存的是左上角坐标加宽高,VOC存的是左上右下两个点。

这个细节很重要,因为转换脚本要做的就是把左上右下的绝对坐标,换算成YOLO需要的中心点x、中心点y、框宽、框高,并且四个值全部除以图片尺寸,归一化到0到1之间。另外注意size节点里已经存了图片宽高,但在实际转换时我一般不信它,而是直接打开图片读实际宽高——有些数据集处理过图片后,xml里的宽高已经和实际图片对不上了。

油茶儿这个场景里,通常xml里只有一个或两个object。一个框代表一颗油茶果,如果两颗果挨得很近,标注者可能会画一个大框把两颗都包进去。这个不用纠结,按数据现状来就行。真正要警惕的是xml文件里出现歪歪扭扭的标签名,比如mature_fruit、Mature、green_fruit混着用。YOLO的类别映射是按字符串精确匹配的,名字不统一,同一颗果就会被当成两个类别。

2.2 用审计脚本盘清数据:图片对得上xml吗,类别名干不干净

转换之前,先花五分钟把整个数据集审计一遍。常见做法是把xml目录和图片目录各自扫一遍,统计图片数量、xml数量、类别名分布和空标注文件。这份数据如果存在图片和xml数量不一致的情况,后面划分训练集时一定会出问题。

import os import xml.etree.ElementTree as ET from collections import Counter xml_dir = "Annotations" # 解压后存放xml的目录,按实际路径改 img_dir = "JPEGImages" # 存放图片的目录 xml_files = [f for f in os.listdir(xml_dir) if f.lower().endswith(".xml")] img_files = [f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))] xml_stems = {os.path.splitext(f)[0] for f in xml_files} img_stems = {os.path.splitext(f)[0] for f in img_files} print("xml文件数:", len(xml_files)) print("图片数:", len(img_files)) print("有xml没图片:", len(xml_stems - img_stems)) print("有图片没xml:", len(img_stems - xml_stems)) class_counter = Counter() empty_files = 0 bad_files = [] for fn in xml_files: path = os.path.join(xml_dir, fn) try: tree = ET.parse(path) except Exception as e: bad_files.append((fn, str(e))) continue objs = tree.getroot().findall("object") if len(objs) == 0: empty_files += 1 for obj in objs: name = obj.find("name").text if name is not None: class_counter[name] += 1 print("类别统计:", dict(class_counter)) print("空xml数量:", empty_files) print("无法解析的xml:", bad_files if bad_files else "无")

这段脚本逻辑简单,但能直接告诉你三件重要的事。第一,“有图片没xml”的数量如果大于0,这些图片必须删掉,否则训练时YOLO会报找不到标签的错,或者在数据加载阶段直接卡住。第二,类别统计能帮你发现标签名是不是统一。正常油茶儿数据应该只有两类,比如green和mature,如果出现green_fruit、mature_fruit或者未命名对象,说明这批数据在标注时就有脏标签。第三,空xml文件不是没标注,而是标注者画了框又删了但没保存干净,这种文件留着会导致txt文件为空,训练时损失函数没法计算。

我一般会在这个阶段把统计结果存成一个txt存档,后面转换和训练都按这份统计来。如果发现类别名不统一,趁现在改xml里的name字段可比转换完了再改txt容易得多,用sed或脚本批量替换一遍,别手改,几百个xml手改必出事。

3. 把VOC格式转成YOLO训练格式:转换脚本与四个边界坑

VOC xml标注和YOLO训练格式之间的换算关系很简单:YOLO训练时每个txt文件对应一张图片,每一行是一个目标,格式是“类别id 中心点x 中心点y 框宽 框高”,五个值用空格分隔,坐标全部归一化到0到1。类别id不是类别名,而是你在训练配置里定义的类别顺序下标。这个顺序是整个转换过程的锚点,从转换脚本到训练配置,必须始终保持一致。

网上能找到一堆VOC转YOLO的脚本,绝大多数版本都能跑,但很多脚本只处理“完美标注”。真实数据集里,标注框越界、类别名拼写错误、xml里没有object、图片文件名后缀大小写不一致,这些情况几乎必然存在。转换脚本如果不去处理这些边界情况,生成的txt里要么是空文件,要么是大于1的坐标值,YOLO训练时会直接给你颜色看。

3.1 完整转换脚本:从xml的bndbox到txt的归一化坐标

下面这份脚本是我处理农业检测数据集时一直在用的版本,处理了越界钳位、空框丢弃、未定义类别跳过和图片缺失四种情况。把它存成voc2yolo.py,在同一目录下建好xml存放目录和图片存放目录就能跑。

import os import xml.etree.ElementTree as ET from PIL import Image # 按审计输出填类别,数字由列表顺序决定,训练配置必须与此保持一致 CLASSES = ["green", "mature"] def clamp_box(x1, y1, x2, y2, img_w, img_h): # 标注框可能超出图片边界,钳位到图片有效范围 x1 = max(0.0, min(x1, img_w - 1)) y1 = max(0.0, min(y1, img_h - 1)) x2 = max(0.0, min(x2, img_w - 1)) y2 = max(0.0, min(y2, img_h - 1)) if x2 <= x1 or y2 <= y1: return None return x1, y1, x2, y2 def convert_one(xml_path, img_path, out_dir): # 用实际图片尺寸做归一化,不要信xml里的size节点 with Image.open(img_path) as im: img_w, img_h = im.size tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASSES: print(f"跳过未定义类别: {name} in {os.path.basename(xml_path)}") continue box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) boxed = clamp_box(x1, y1, x2, y2, img_w, img_h) if boxed is None: print(f"空框丢弃: {xml_path}") continue x1, y1, x2, y2 = boxed # 左上右下绝对坐标 -> 中心点+宽高的归一化坐标 cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h lines.append(f"{CLASSES.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") if not lines: return 0 txt_path = os.path.join( out_dir, os.path.splitext(os.path.basename(xml_path))[0] + ".txt" ) with open(txt_path, "w") as f: f.write("\n".join(lines)) return len(lines) def convert_all(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) total = 0 for fn in sorted(os.listdir(xml_dir)): if not fn.lower().endswith(".xml"): continue stem = os.path.splitext(fn)[0] xml_path = os.path.join(xml_dir, fn) img_path = None for ext in (".jpg", ".jpeg", ".png", ".bmp"): cand = os.path.join(img_dir, stem + ext) if os.path.exists(cand): img_path = cand break if img_path is None: print(f"找不到图片: {fn}") continue n = convert_one(xml_path, img_path, out_dir) total += n print(f"已转换: {fn}, 当前共 {total} 条标注框") if __name__ == "__main__": convert_all("Annotations", "JPEGImages", "labels")

逻辑说明分三层。第一层是类别映射:CLASSES列表的顺序就是训练时类别数字id的顺序,脚本里用CLASSES.index(name)把字符串变成一个整数。这里必须和后续data.yaml里的names定义一模一样,一个写错,整个模型的预测结果全错位。第二层是坐标换算:x1、y1、x2、y2是绝对像素坐标,先相加除2得到中心点像素坐标,再除以图片宽高得到相对值。注意框的宽高是x2减x1再除以图片宽,不是除以图片高,新手写错最常见的就在这一步。第三层是容错处理:图片打不开、xml没法解析、类别对不上,都会打印出具体文件名,跑完脚本扫一眼输出就能定位脏数据。

跑完转换后,随意抽查一下生成的txt。一个正常txt的内容大概是:

0 0.382031 0.325521 0.083594 0.095833 1 0.105859 0.614583 0.072656 0.096875

第一列0代表green,1代表mature,后四列全部是0到1之间的小数。如果你发现哪个txt文件为空,回看审计阶段的“空xml数量”,把对应的图片一并删掉。这套处理数据集用于YOLOv8训练的流程,顺序不能乱,审计、转换、抽查,一步步来。

3.2 四个边界坑:类别名脏、坐标越界、空xml和找不到图

第一个坑是类别名“脏”。常见现象是同一个目标被标成mature和mature_fruit,或者大小写混用。原因大多是标注工具自动补全了历史上某个文件的类别名,标注者没注意就一路标下去了。解决方式是在审计阶段用脚本把同义类别名合并成统一的name,别等到转换完再改txt。改xml用ET解析循环替换name字段就行,比改txt可靠得多。

第二个坑是标注框越界。表现是转换完后出现大于1的坐标,或者训练时loss突然异常。原因有两种:一种是标注工人画框时鼠标拖出了图片边缘;另一种是图片在标注后被裁剪或缩放,xml没同步更新。解决方式就是脚本里的clamp_box钳位逻辑,把坐标限制在0到图片宽度减1之间。钳位后如果框变空了,直接丢弃这条标注,不要硬留。

第三个坑是空xml。一个xml文件里找不到任何object节点,转换脚本会生成一个0字节的txt。YOLO训练时会把这个txt当作“背景图”处理,如果这类文件多了,模型会偏向不检测任何目标,也就是俗称的“什么也框不出来”。解决方式是在审计阶段统计空xml数量,确认超过一定比例后要么重标,要么把对应的图删掉,留着没有好处。

第四个坑是文件名对不上。图片是IMG_001.JPG,xml是IMG_001.xml,理论上文件名后缀大小写不影响stem匹配,但如果图片目录里混入了同一stem不同后缀的图,比如同时有img_001.jpg和img_001.png,脚本只会匹配找到的第一张,容易造成图片和标注错位。解决方式就是在脚本里匹配到多个候选时打印警告,并同时提示用户去重。

4. 用YOLOv8训练油茶儿成熟检测模型:目录整理、data.yaml和四个必调参数

数据转换完,标签是txt了,接下来才进入训练环节。油茶果成熟检测在视觉上属于相对简单的任务,目标大、类别少,YOLOv8的nano或small版本就能跑出不错的效果。很多初学者一上来就去下载大模型,油茶果又不是针尖大小的目标,没必要为这点检测难度背大模型的推理负担。先跑通,再谈优化。

这一章按yolov8训练自己的数据集的常规路线走:整理目录、写data.yaml、跑训练命令。每一步都有参数需要解释,也有坑需要提前说明。

4.1 整理YOLO训练目录:train/val划分脚本

YOLOv8读取数据时,默认要求目录结构是images下放图片、labels下放txt,且train和val分开放。可以手动建目录,但更稳妥的方式是用脚本划分,同时保证图片和标签同步。下面这段脚本把已经转换好的labels和原始图片按比例随机划到train和val两个子集里:

import os import random import shutil def split_images(img_dir, label_dir, out_dir, val_ratio=0.2, seed=42): random.seed(seed) # 固定随机种子,多次划分结果一致,方便复现 imgs = [f for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(imgs) n_val = int(len(imgs) * val_ratio) val_set = set(imgs[:n_val]) for sub in ("images/train", "images/val", "labels/train", "labels/val"): os.makedirs(os.path.join(out_dir, sub), exist_ok=True) for img in imgs: stem = os.path.splitext(img)[0] label_src = os.path.join(label_dir, stem + ".txt") if img in val_set: dest_img = os.path.join(out_dir, "images/val", img) dest_label = os.path.join(out_dir, "labels/val", stem + ".txt") else: dest_img = os.path.join(out_dir, "images/train", img) dest_label = os.path.join(out_dir, "labels/train", stem + ".txt") shutil.copy(os.path.join(img_dir, img), dest_img) if os.path.exists(label_src): shutil.copy(label_src, dest_label) else: print(f"警告: {stem} 没有对应标签,已拷贝图片但不参与有效训练") if __name__ == "__main__": split_images("JPEGImages", "labels", "yolodata")

这段脚本有两个重要参数。第一个是val_ratio,也就是验证集比例。油茶果数据集一般不会太大,如果总量在几百张这个量级,val_ratio取0.2是相对稳妥的,太少验证集不够看,太多训练集饿肚子。第二个是seed,固定成42或任意整数,保证下次重新划分时结果一致。如果不固定种子,每次跑出来的train/val都不一样,同一批实验之间就没法比较了。

shutil.copy用的是复制而不是移动,这样原始数据不会被破坏。如果硬盘吃紧,可以改成shutil.move,但我不建议这么做——原始数据一旦动过,后面发现问题想重来就没有后悔药了。

4.2 写data.yaml和选择预训练权重

目录整理好后,要写一个data.yaml文件,这是YOLO训练时读取数据配置的唯一入口。我一般把data.yaml放在yolodata目录外面一层,避免它和图片混在一起。

# 路径建议写绝对路径,相对路径在换机器后经常找不到 path: /home/user/oiltea/yolodata train: images/train val: images/val # 类别数由审计阶段的统计结果决定 nc: 2 names: 0: green 1: mature

path字段写的是yolodata的父目录,train和val是相对path的路径。如果你把data.yaml放在和yolodata同级的位置,这个写法就能直接跑通。nc必须等于CLASSES列表的长度,names的顺序必须和转换脚本里的CLASSES完全一致。前面转换脚本里写了CLASSES.index(name),YOLO训练时用names这个字典把数字id映射成类别名做可视化,顺序对不上,训练出来的模型框对了但名字会显示错。

预训练权重的选择上,yolo预训练模型下载可以直接用命令自动拉取。YOLOv8的官方权重会随detect train命令自动下载到当前目录。对于油茶儿这种规模的数据集,yolov8n.pt是最稳的起步选择,显存占用小,训练速度快。如果你的显卡是V100或者更好的卡,可以换yolov8s.pt,效果会好一点,但没有必要一上来就上yolov8l,训练时间翻几倍,提升有限。

4.3 训练命令与四个必调参数,以及损失函数怎么判断

data.yaml写好,直接跑:

yolo detect train \ data=oiltea.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=30 \ project=runs/oiltea \ name=mature_v1

这个命令里四个参数比模型选择更影响最终效果。

第一个必调参数是imgsz,定义为640是兼顾速度和精度常用的平衡点。油茶果挂在树上,单颗果实占整图的比例通常不大,imgsz再用416这种小分辨率,小果直接糊成一团,检测器根本看不见。如果显存允许,这个任务建议直接上800或960,小目标召回率会有肉眼可见的提升。

第二个必调参数是epochs。150个epoch对几百张图的数据集来说通常够用,但不要死守这个数。配合下面的patience参数,如果连续30个epoch验证集mAP没有提升,训练会自动停,所以epochs写大一点也不会过度浪费算力。

第三个必调参数是batch。batch大小受显存限制,不是想设多大就多大。油茶果数据集图片一般不会特别小,960分辨率下batch设不了太大。如果batch必须压得很小,验证集loss会震荡得更厉害,这时可以把patience适当调大,给它更多容忍空间。

第四个必调参数是patience,也就是早停耐心值。新手最容易犯的错是设成100甚至200,结果模型早就不学了,还在那里空转。对这类数据集我一般设在20到30之间,够用且省时间。

命令跑起来之后,怎么判断损失函数有没有在正常工作。YOLOv8的损失由分类损失、框回归损失和DFL组成,训练日志里会打印一组loss数值。油茶果检测任务里,你最应该关注的是box_loss列,因为它是判断“框得准不准”的直接指标。如果box_loss稳步下降,说明模型在学习目标的位置;如果几个epoch之后还在0.2以上反复横跳,大概率是标注数据里框的位置本身就不准,回去检查标注,而不是调参。

另外,epochs跑到一半时bn层崩溃也不是没见过,表现是训练集loss正常,验证集mAP突然掉到0。这种情况多见于训练集和验证集图片分布差异过大,或者batch太小导致bn统计量不稳定。优先把batch调大,其次检查数据划分里有没有把同一批果园的照片全部切到验证集。

5. 避坑清单:油茶果成熟检测训练中最常见的5个事故

这部分写的都是我自己在农业检测类项目里真实遇到过、或者帮别人排查过的问题,每一条都是现象、原因、解决三段式。这里的优先级是“先能发现”再“会解决”,每一个事故都对应训练日志或者验证结果里的一个明显信号。

5.1 转了格式但类别名没对齐,训练结果全是同一个类别

现象:训练过程loss正常下降,但验证时预测出来的框全部叫同一个名字,另一个类别一个都召不回。去查confusion matrix,发现矩阵里有一行完全是0。

原因:转换脚本中的CLASSES顺序和data.yaml里的names顺序不一致,或者转换时把两个不同名的标签都映射成了同一个id。比如xml里同时有mature和mature_fruit两个name,转换脚本只把mature_fruit映射到1,mature也映射到1,结果模型把两类当成一类学。

解决:回到审计脚本的“类别统计”,把真实出现的所有name列出来,逐一对齐CLASSES和data.yaml。统一规则是转换脚本唯一信任的源头,data.yaml只是它的镜像,不要在两处分别手写。

5.2 loss变成NaN,查学习率更要查损坏图片

现象:训练进行到几十个batch时,loss列突然变成nan,然后后面的所有epoch全部nan,训练直接废掉。

原因:最常见的是学习率太高,模型权重发散;但农业数据集还有一个高频原因是数据集里有损坏图片或全黑图片。全黑图片输入网络后,目标检测的head输出极端值,梯度爆炸。

解决:先用git或文件管理工具确认训练日志里loss变成nan的时间点,把time列和当前batch对应上,去那个batch附近找是不是有纯黑或损坏的图片。如果没有,再把lr从默认值往下降半个数量级重跑。两个操作都做完,nan基本都能定位。

5.3 mAP很高实际识别却翻车,验证集和训练集混进了同一棵树

现象:验证集mAP跑到0.9以上,让你觉得自己已经做完了。结果拿到果园里一测,换一棵树、换个角度,框全歪。

原因:数据划分时没有对图片做群体控制。同一棵油茶树的照片往往是在同一天、同一个角度连续拍的,模型在训练里见过这棵树,在验证里又遇到它,自然“记得”而不是“学会”。mAP虚高,但模型对没见过的树是瞎的。

解决:划分train/val时按文件名前缀或拍摄批次分组,我一般手动把来自同一棵树的图片单独抽出来做验证集。宁可验证集图片少一点,也不能让它和训练集同源。

5.4 熟果样本太少,调class_weights不如先做过采样

现象:训练完看每类召回率,green能到0.95,mature只有0.4。模型对熟果几乎全漏。

原因:数据集中熟果标注天然少。油茶果成熟期短,能采的那几天去拍照本来就难凑样本,熟果数量少,损失函数里被green类别主导,模型干脆学了个“什么都是青果”的最优解。

解决:与其调class_weights这种玄学参数,不如先把mature类别的图片过采样几轮,让训练时两类样本比例接近。常见做法是把mature样本复制2到3份混进训练集,或者对熟果图片做随机旋转、亮度抖动后再复制。数据增强在这里的效果比调权重更直观,模型至少能见到足够多的熟果正样本。

5.5 混淆矩阵总合不唯一,这是YOLO的正常现象

现象:验证完打印混淆矩阵,发现行和列的总数对不上,每一类单独看都有缺损,有人开始怀疑数据有问题。

原因:目标检测不像分类任务,一个目标只能有一个预测结果。YOLO在预测时会把置信度低于conf_thres的框直接丢弃,这些丢弃的框不会出现在混淆矩阵的对角线或者任意类别里。另外,同一目标被多个预测框覆盖时,NMS会去掉重复框,去重的框也不会计入矩阵。所以混淆矩阵总和小于标注框总数是完全正常的。

解决:不用管“总合唯一”这件事。应该关注每一类对角线上的数值和它的分母,也就是这一类的召回率。比如mature类别对角线如果偏小,说明漏检多,回去看5.4的样本问题。

6. 验证与进阶:用混淆矩阵和连续帧把成熟度模型推到可用

6.1 验证的三件套:验证集推理、混淆矩阵和每类召回率

训练结束后,第一个动作是跑验证集而不是直接上视频。YOLOv8自带的val命令会输出mAP50、mAP50-95和每类AP,但比这些数字更重要的是混淆矩阵图,它直接告诉你每个类别之间互相误检的情况。

yolo detect val \ model=runs/oiltea/mature_v1/weights/best.pt \ data=oiltea.yaml \ project=runs/oiltea \ name=mature_v1_val

跑完在输出目录里找confusion_matrix.png,看两类之间有没有明显的交叉误检。油茶果检测里最常见的误检是熟果和枯叶混淆,因为两者都是黄褐色。如果混淆矩阵里mature那一列有大量来自背景的false positive,说明模型学到的是颜色而不是形状,需要去检查训练图片里是否包含大量枯叶背景。如果验证通过,再用一段果园实拍视频跑predict,观察连续帧的稳定性,成熟度检测在果园里要做的是决策,一帧框错问题不大,连续十几帧都框错就是模型在某个光照条件下失效了。

6.2 进阶方向:成熟度分级、SAHI切图和边缘设备部署

模型跑通之后有三个值得做的进阶方向。一个是把二分类扩展成熟度分级,在青果和熟果之间加一个“转色期”类别,标注成本增加不多,但对采摘决策的价值更大。另一个是对付小目标,油茶果在远处树冠上确实只有几十个像素,此时可以用SAHI这类切图推理工具,把大图切成小块分别检测再合并结果,召回率提升明显,代价是推理时间变长。第三个方向是把模型导出成TensorRT的engine格式,部署到边缘盒子或无人机上,这一点对果园场景几乎是刚需。

我最早做油茶果检测项目时,最大的教训就是没先审计就直接转了格式,转完跑训练才发现类别名统一不了,全部重来。把“审计、转换、验证集隔离”这三件事刻进流程里,后面每一步都会顺畅得多。希望帮到你。

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

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

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

立即咨询