ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

苹果树叶病害数据集格式转换与YOLOv8训练实战

苹果树叶病害数据集格式转换与YOLOv8训练实战 简介这是一套面向智慧农业与目标检测应用的中文苹果叶部病害数据集覆盖花叶病、斑点落叶病、叶枯病三个常见类别共916张真实场景图像。图像背景丰富目标尺寸与拍摄角度多样整体分布均衡所有标注均为纯手工标注边界框精准适合课程设计、算法竞赛以及病害智能识别App等实际项目直接用于模型训练与评估。压缩包共包含3665个文件内有916张jpg原图、916个xml标注、916个json标注和917个txt标注可无缝对接VOC、YOLO等主流目标检测流程整包大小约84.45MB目录结构清晰便于按需取用。数据已按标准格式整理省去格式转换与手工标注的繁重工作可直接进行迁移学习、数据增强或作为预训练验证集帮助开发者快速验证算法效果。该数据集目前已有1208人学习使用数据质量经过下载验证可用于科研和工程实践。1. 一整套苹果树叶病害数据集拿到手先别急着跑YOLO压缩包里躺着一个长名字智慧农业-苹果树叶三种病害(花叶病、斑点落叶病)数据集916张-含vocyolojson三种格式标签。916张图对目标检测来说不算大恰恰适合做迁移学习、毕设demo和标注流程验证。很多人解压后直接开训却在几分钟后被一堆报错打懵labels目录和images对不上、xml里的类名和json里的categories顺序错位、yaml里nc写成4结果训练中断。这篇文章把这类苹果树叶病害数据集从解压到训练要走的路走一遍重点说清VOC、YOLO、JSON三套格式各自的用途、转换脚本怎么写、训练参数怎么调、以及最容易翻车的五个细节。适合刚上手目标检测的新手也适合想快速验证一套标注数据是否干净、能否投入训练的工程师。2. 从VOC到YOLO格式转换解析xml并生成归一化txt的落地脚本2.1 为什么这套数据集要单独做一次格式转换很多公开数据集会用VOC格式打底因为标注工具LabelImg等默认导出就是VOC的xml每个文件里记录object的name和bndbox坐标坐标是像素级的左上右下。但YOLO系列训练框架要的不是这种坐标它要的是归一化中心点坐标即每个目标一行class_id x_center y_center width height并且所有数值除以图像宽高、落在0到1之间。这套苹果树叶病害数据集虽然压缩包命名里写了“含vocyolojson三种格式标签”但实际解压出来之后需要先确认labels目录里的txt与annotations里的xml是否一一对应。我拿到这类包通常不会直接信任文件名而是先做一个校验因为很多时候打包的人在转换时会把类别顺序搞乱或者漏转了一部分图片。自己做一次VOC到YOLO的转换等于把标注数据重新梳理了一遍后面训练时心里有底。2.2 转换前先看数据类目和尺寸怎么核对先不急着写转换主脚本我先看几样东西xml里有哪些name、图片尺寸字段是否齐全、每个类别的样本数量。这一步能发现类别拼写不一致、标签大小写混用、甚至某个类在某个xml里根本没写size的问题。import os import xml.etree.ElementTree as ET voc_dir annotations # 替换成你解压后的xml目录 all_names set() size_missing [] file_count 0 for xml_name in os.listdir(voc_dir): if not xml_name.endswith(.xml): continue path os.path.join(voc_dir, xml_name) tree ET.parse(path) root tree.getroot() file_count 1 # 查size字段是否存在 size root.find(size) if size is None or size.find(width) is None or size.find(height) is None: size_missing.append(xml_name) # 收集所有类名 for obj in root.iter(object): name obj.find(name) if name is not None: all_names.add(name.text.strip()) # 用iter而不是findall有些标注工具会嵌套稳妥些 print(xml文件数:, file_count) print(出现过的类名:, all_names) print(缺少size字段的xml:, len(size_missing), size_missing[:5])这段代码的逻辑是用iter(object)遍历xml里所有目标节点兼容多层嵌套类名取strip()后的文本对size字段做存在性检查。size_missing这个列表很重要后面转换时如果xml里没有宽高就必须从图片本身读取不能继续用xml里的假数据。参数上voc_dir是唯一需要改的地方其他变量都是运行期统计结果不涉及手工调参。如果跑出来all_names超过预期比如两个版本的“花叶病”拼写不同立刻去xml里确认原始标注而不是靠压缩包名猜。类似“斑点落叶病”这种中文名有些工具会直接写英文缩写类名映射表要在这一步确定。2.3 核心转换脚本与每个参数的含义下面就是VOC转YOLO的核心脚本。我把类别映射表放在脚本顶部因为它的顺序就是训练时yaml里names的顺序也是最容易出错的地方。import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射表顺序就是你之后yaml里的names顺序 # 从2.2节的结果里把类名复制过来按你的需求排序 class_names [花叶病, 斑点落叶病, 其他叶片病害] class_to_id {name: idx for idx, name in enumerate(class_names)} voc_dir annotations img_dir images out_dir labels def convert_one(xml_path, img_path, out_path): tree ET.parse(xml_path) root tree.getroot() width None height None size root.find(size) if size is not None: width size.findtext(width) height size.findtext(height) # xml里常常没有size或宽高为0退回读图片 if not width or not height or float(width) 0 or float(height) 0: im Image.open(img_path) width, height im.size img_w float(width) img_h float(height) if img_w 0 or img_h 0: print(图像尺寸异常:, img_path) return False lines [] for obj in root.iter(object): name_node obj.find(name) if name_node is None: continue name name_node.text.strip() cls_id class_to_id.get(name) if cls_id is None: print(f未知类别 {name} 在 {xml_path}) continue box obj.find(bndbox) if box is None: continue xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 坐标越界保护后面章节会专门讲 xmin max(xmin, 0) ymin max(ymin, 0) xmax min(xmax, img_w) ymax min(ymax, img_h) w xmax - xmin h ymax - ymin if w 0 or h 0: print(宽高0跳过:, xml_path, name) continue # 转归一化中心点坐标 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h nw w / img_w nh h / img_h # 限制在(0,1]区间极端情况留个兜底 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) nw min(max(nw, 0.0), 1.0) nh min(max(nh, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {nw:.6f} {nh:.6f}) if lines: with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines) \n) return True这段脚本里有几个参数值得说明。class_to_id决定类别序号如果你在yaml里写的names顺序和这里不一致轻则标签混乱重则训练直接崩。findtext(width)是ElementTree给的手法比find再.text更简洁也不容易报错。越界保护那三行max、min本质是给标注粗心的框“兜底”后面训练时会避免NaN和负数框。为什么要img_w检查大于0因为有的标注软件写了个0占位符导致所有坐标除以0YOLO训练会报“expected box in [0,1]”的错。调用方法很简单遍历所有xml即可os.makedirs(out_dir, exist_okTrue) for xml_name in os.listdir(voc_dir): if not xml_name.endswith(.xml): continue stem xml_name[:-4] xml_path os.path.join(voc_dir, xml_name) img_path os.path.join(img_dir, stem .jpg) out_path os.path.join(out_dir, stem .txt) if not os.path.exists(img_path): print(找不到图片:, img_path) continue convert_one(xml_path, img_path, out_path) print(转换完成)图片后缀建议先确认一下。有些包用的.jpeg或.JPG不带后缀保护就会漏很多。更稳的写法是用glob去匹配把jpg、jpeg、png都列进去但我习惯先跑一次错误打印确认到底用哪个后缀。2.4 转换后验证用txt反向画框回图片这一步很多人跳过但我劝你一定做。随机抽三张图把txt里的坐标换算回像素在图上画框看和目标位置是否吻合。from PIL import Image, ImageDraw img_path images/xxx.jpg label_path labels/xxx.txt im Image.open(img_path) draw ImageDraw.Draw(im) w, h im.size with open(label_path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, x_center, y_center, bw, bh parts x_center float(x_center) * w y_center float(y_center) * h bw float(bw) * w bh float(bh) * h xmin x_center - bw / 2 ymin y_center - bh / 2 xmax x_center bw / 2 ymax y_center bh / 2 draw.rectangle([xmin, ymin, xmax, ymax], outlinered, width3) im.save(check_vis.jpg)这里的核心逻辑是把归一化坐标乘回图像宽高再减半宽半高得到左上角。如果转换脚本把坐标写错这一眼就能看出来框到背景上、框到相邻叶片上、或者框整体偏移半个图。反向验证也是后续训练前百试不爽的手段比任何文档都可靠。3. 用YOLO v8训练苹果病害检测模型环境配置与参数调法3.1 Anaconda环境怎么搭python版本和CUDA的坑YOLO v8现在用ultralytics这个python包来做训练和推理。最常翻车的地方在Anaconda环境配置阶段。我一般会固定用python 3.10而不是最新版本因为ultralytics对python 3.11、3.12的兼容偶尔会出幺蛾子尤其涉及torch的预编译wheel时。conda create -n yolo python3.10 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics参数说明cu118是CUDA 11.8的wheel源如果你的显卡驱动版本较新也可以换成cu121或直接用默认安装。判断依据是nvidia-smi里显示的CUDA版本不要高于这个版本号就行。pip install ultralytics会自动拉opencv、numpy、pandas这些依赖。装完用python -c import torch; print(torch.cuda.is_available())验证输出True再继续这一步输出False后面训练会慢到怀疑人生。这个数据集只有916张训练时推荐直接用yolov8n.pt作为预训练权重因为它体积小、吃显存少。如果你显卡显存足够可以先下载yolov8s.pt放到本地目录训练时model参数直接写文件名。3.2 数据目录结构和yaml文件这样写YOLO v8的数据集目录需要标清train和val路径。我习惯把images和labels放在同一个总目录下train和val分开apple_leaf/ ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ └── ... │ └── val/ │ │ ├── 0002.jpg │ │ └── ... ├── labels/ │ ├── train/ │ │ ├── 0001.txt │ │ └── ... │ └── val/ │ │ ├── 0002.txt │ │ └── ... └── apple_leaf.yaml目录结构这块我的建议是不要直接把整个labels和images混在一个列表里让训练脚本自动切分因为切分种子不同会导致同一张图出现在train和val两边后面的测试指标就不可信了。先手动切一次比例按8:2或者按每个病害类别的数量均衡切。分完后再看一眼每类的train/val数量避免某个类val里只有一张图。yaml文件内容如下path: /abs/path/to/apple_leaf train: images/train val: images/val nc: 3 names: [花叶病, 斑点落叶病, 其他叶片病害]这里的names顺序必须和第2章转化脚本里的class_to_id完全一致一个位置都不能差。nc写3对应yaml里names的元素个数别凭感觉写。path建议写绝对路径相对路径在IDE里跑和命令行跑可能不一致排查起来很烦。这是我踩过的坑之前写过相对路径在vscode里跑没问题换到命令行就报“Dataset not found”。统一用绝对路径立即解决。3.3 训练命令与关键参数从epochs到batchyolo detect train \ dataapple_leaf.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectruns/detect \ nameapple_leaf_exp1各参数的实际含义要在训练前过一遍。epochs100对916张图是够用的再多容易过拟合而且有early stopping兜底。imgsz640是缩放输入尺寸苹果叶片上病斑一般不算极小目标640足够如果叶子在照片里只占中间一小块可以试试imgsz1024但显存占用会涨一倍左右。batch16是单卡一次吃进16张图如果显存不够报OOM先调成8或4不要动imgsz。patience20表示连续20个epoch验证集mAP没提升训练提前结束省时间。训练过程中注意看两个曲线train/loss和metrics/precision。loss要平稳下降如果loss在某个epoch突然跳高然后一路向上基本是学习率崩了或者BN层出问题。BN崩溃是YOLO训练常见的翻车现场现象是loss先降后猛涨precision直接为0。一旦遇到这种情况第一反应是优化器或学习率的问题先关掉自动调整学习率把lr0从默认0.01降到0.001再附带把batch减小试试。如果数据集本身标签不小心有nan也会出现类似表现所以第2章的反向验证不能省。3.4 验证阶段混淆矩阵总合不唯一是正常的训练完别急着拿一张图预测。先跑一遍验证集看指标yolo detect val \ modelruns/detect/apple_leaf_exp1/weights/best.pt \ dataapple_leaf.yaml结果会输出每个类别的precision、recall、mAP50和mAP50-95。你可能会发现混淆矩阵里每一行的数值加起来不是100%这是正常的因为验证脚本用的是normalizeTrue按行归一化而小类样本少四舍五入后误差就显出来了。不要被这个数字吓到具体要看混淆矩阵对角线的值。如果某个病害类别的recall特别低大概率是这类图片在数据集里本来就少。YOLO默认会用类别采样来平衡但数据只有916张病斑分布又很不均匀花叶病可能五百多张另一类只有一两百张模型自然会偏科。这种数据不均衡之下mAP50会被多数类带得虚高你真正要看的其实是少数类的AP值。4. JSON标注格式读懂COCO结构并从中导出你要的数据4.1 COCO的JSON到底长什么样很多初学者拿到带JSON标注的数据集打开文件看见一大片嵌套结构就懵了。其实它是有固定套路的一层层拆开看就行。COCO JSON主要分四个大字段images、annotations、categories、info。images是图片列表每条有id、file_name、width、heightannotations是标注列表每条有id、image_id、category_id、bbox如果涉及实例分割还会有segmentation和areacategories是类别列表每条有id和name。关键点在于categories里的id是COCO内部用的不一定是0、1、2连续而YOLO要的是从0开始的类别索引。所以把JSON转YOLO时不能直接拿category_id当yolo的class id必须先做一个映射读一遍categories列表把name映射到新索引再遍历annotations转坐标。4.2 用JSON统计类别分布和标注质量这套数据集带JSON本质上是因为COCO格式是标准化接口很多开源代码和标注平台认它。我的习惯是先写一个小工具做质量体检比excel打开JSON靠谱得多import json from collections import Counter with open(annotations/annotations.json, r, encodingutf-8-sig) as f: coco json.load(f) # image_id - file_name 映射 id2file {img[id]: img[file_name] for img in coco[images]} # category_id - name 映射 id2name {cat[id]: cat[name] for cat in coco[categories]} # 统计每个类别的目标数 cat_counter Counter() # 统计图片数 image_counter Counter() # 统计异常标注没有对应图片、bbox越界、宽高为0 bad_anns [] for ann in coco[annotations]: cat_name id2name.get(ann[category_id], 未知) cat_counter[cat_name] 1 image_counter[id2file.get(ann[image_id], ann[image_id])] 1 bbox ann.get(bbox) if not bbox or len(bbox) ! 4: bad_anns.append((bbox格式异常, ann[id])) continue x, y, w, h bbox # 宽高不能为0坐标不能为负数 if w 0 or h 0: bad_anns.append((宽高非正, ann[id])) if x 0 or y 0: bad_anns.append((坐标为负, ann[id])) print(每个类别的目标数:, dict(cat_counter)) print(出现过的图片数:, len(image_counter)) print(异常标注数量:, len(bad_anns))这段代码的逻辑就是校验数据完整性。encodingutf-8-sig是一个容易忽略的点因为Windows下的标注工具常常给JSON加BOM头直接open(f, r, encodingutf-8)会在解析第一个字段时报错utf-8-sig会自动剥掉BOM。bbox在COCO标准里是[x, y, width, height]左上角是原点坐标可以到图像宽高边界但不能为负宽高也不能为0。异常标注如果数量很多说明这套数据集打包前没清洗过后面训练时要把这些ann过滤掉。4.3 JSON转YOLO其实和VOC转YOLO一个套路JSON转YOLO比VOC转YOLO还简单坐标已经结构化在bbox里了import json with open(annotations/annotations.json, r, encodingutf-8-sig) as f: coco json.load(f) # 用collections里的OrderedDict按name顺序生成索引会更保险 name_list [花叶病, 斑点落叶病, 其他叶片病害] name_to_id {name: idx for idx, name in enumerate(name_list)} cat_id_to_name {cat[id]: cat[name] for cat in coco[categories]} output_dir labels_from_json import os os.makedirs(output_dir, exist_okTrue) file_name2img {} for img in coco[images]: file_name2img[img[id]] img[file_name] # 收集每个image_id的标注 from collections import defaultdict anns_by_img defaultdict(list) for ann in coco[annotations]: anns_by_img[ann[image_id]].append(ann) for img_id, img in enumerate(coco[images]): img_w img[width] img_h img[height] file_name img.get(file_name, f{img_id}.jpg) stem os.path.splitext(file_name)[0] lines [] for ann in anns_by_img[img[id]]: cat_name cat_id_to_name.get(ann[category_id]) if cat_name is None: continue cls_id name_to_id.get(cat_name) if cls_id is None: continue x, y, w, h ann[bbox] x_center (x w / 2.0) / img_w y_center (y h / 2.0) / img_h nw w / img_w nh h / img_h x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) nw min(max(nw, 0.0), 1.0) nh min(max(nh, 0.0), 1.0) lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {nw:.6f} {nh:.6f}) with open(os.path.join(output_dir, stem .txt), w, encodingutf-8) as f: f.write(\n.join(lines) \n)这段脚本要注意bbox里的w、h是标注目标的实际宽度和高度不是右下角坐标不需要再减xmin。和VOC转YOLO的差别只是坐标来源不同。后面要不要重写images文件夹其实不用YOLO训练时只看images目录里的图只要txt文件名和图片名对上就行。参数上name_list要和训练yaml保持同序这里是我最容易纠结的地方。我的习惯是先把json里所有类别打出来再按文件里categories出现的顺序写进name_list减少人为改动。如果你在VOC转YOLO时已经建立了一套顺序这里应保持完全一致否则同一个数据集两套txt的标签含义就矛盾了。4.4 三种格式为什么都要保留VOC适合人类查看和二次标注YOLO格式适合训练JSONCOCO适合做细粒度任务和程序间交换。这三件事相互之间是经常要来回倒的比如标注平台导出JSON再转成YOLO训练在YOLO上做了新的标注又想导回VOC给别人看。所以不要只留一种格式建议都保留。这套数据集给出三种标签本质就是让你不必困在某一套生态里出不去。但这也带来一个坑如果三份标签不是同一个工具从同一个标注源导出的类别顺序、坐标细节很容易有出入动手训练前先用校验脚本核对一遍不要假设它们是“同一份数据的三份拷贝”。5. 标注重灾区5条关于格式、文件与类别的避坑记录5.1 文件名匹配不上训练时报找不到labels现象训练刚开始几十个epoch后ultralytics提示某个jpg找不到对应txt或者验证集指标全是0打开日志发现大量图片没有标签。原因压缩包里label文件名和图片文件名大小写不一致典型的如1001.JPG对1001.txt这样看起来没问题但列表里的train.txt写的是1001.jpg而实际图片扩展名是.jpeg。另一个常见原因是Windows打包时自动生成的“副本”文件名带空格。解决训练前用脚本统一重命名把所有图片后缀改成统一小写再用stem前缀配labels目录里的文件名做一次差集。import os img_dir images label_dir labels img_stems set(os.path.splitext(name)[0] for name in os.listdir(img_dir)) label_stems set(os.path.splitext(name)[0] for name in os.listdir(label_dir)) print(图片有但标签没有:, len(img_stems - label_stems)) print(标签有但图片没有:, len(label_stems - img_stems))这段代码逻辑很简单但能帮你把问题在训练前暴露出来。如果两个差集数量都是0直接继续如果数量多用os.rename批量修正后缀。5.2 坐标归一化后大于1模型学习时出现NaN现象训练时loss突然变成nan或者报错里有“Expected box [0,1]”。原因VOC的xml里有些xmax、ymax写的值已经超出了图像的宽高标注时手滑把框拖到画布外也有的是图像旋转后宽高值没同步更新原xml的size比新图小。解决在转换脚本里对坐标做clamp截断也就是第2章代码里那四行min(max())。不要觉得“标注有问题训练也能扛过去”YOLO的loss计算里对bbox做log运算坐标一旦越界或为0梯度直接变NaN训练就报废。这一条是最隐蔽的黑匣子问题一旦遇到先对所有txt做一遍数值检查统计最大最小值看到超过1的立即处理。5.3 类别id错位同样一个病三种格式三种序号现象训练后模型把花叶病和斑点落叶病搞混precision和recall整体很低但val loss看着正常。原因JSON里的category_id往往不是从0开始的连续索引可能写着0、2、5YOLO转换时没做映射而VOC转YOLO的代码用的是按类名排序的字典class_id跟着字典序走两套labels的同一个病对应不同数字。解决统一以转换脚本顶部的class_names为唯一版本再写一个校验函数从每个txt里收集出现的class_id确认最大id2、三个类都有且没有负数。类别错位不会报错只会让模型学错知识所以这个校验必须做。5.4 三种病害数量悬殊模型整体mAP可以但minority类全灭现象整体mAP50有0.85但看per-class指标斑点落叶病的AP只有0.2甚至0.1。原因数据集916张三类样本数量不均衡有的类可能只占不到两成。YOLO默认训练会对样本少的类自动调整loss权重但自动权重在极端不均衡下力度不够。解决第一层是数据层面做增强对少数类做复制粘贴增强或额外扭转变换第二层是在yaml配置里把cls的loss权重调大一点或者在yolo detect train命令里加参数比如loss_ota0.4这种偏门参数一般不建议动优先从数据层面改善。另外一个有效手段是用focal loss或调高cls参数的幅度这要看具体版本。如果项目不是为了发论文我更建议的办法是重新组织数据把三类样本按数量均衡后再划分训练集最怕的是拿原封不动的分布直接训练然后只盯着总体mAP蒙混过去。5.5 JSON带BOM或编码错误json.load直接炸现象用json.load(open(path))一跑控制台报UnicodeDecodeError或json.decoder.JSONDecodeError定位到第一行第一个字符。原因Windows上的标注工具默认用UTF-8-BOM保存Python默认读UTF-8时会把BOM当字符解析自然失败。解决把所有json打开方式统一改成encodingutf-8-sig它能自动跳过BOM如果文件里混着GBK和UTF-8两种编码先打开binary模式读取前三个字节判断是不是\xef\xbb\xbf再把字节流decode。with open(path, rb) as f: raw f.read(3) if raw b\xef\xbb\xbf: text open(path, encodingutf-8-sig).read() else: text open(path, encodingutf-8).read()这段代码的核心逻辑是探测BOM前缀再决定用哪种编码模式。打开JSON的环境差异很大尤其你从别人压缩包里拿到的文件不能假设它一定干净。有了这个兜底就不会在数据读取阶段被卡住。6. 让数据集为你所用的最后一步数据划分、增强与导出6.1 训练后的第一件事不是看mAP是拿9张图亲眼看每次训练完我习惯随机挑9张没见过的图片用best.pt做一次预测把预测框和置信度画到图上一张张看过去。这一步可以发现mAP掩盖的问题比如模型有没有把叶片边缘草丛当成病斑、有没有漏掉面积小的初期病斑、框的边界是不是明显比实际病斑大。YOLO预测时会输出多个框NMS会把重叠的框消掉但NMS之后如果两个相近目标保留了一个、漏了一个肉眼马上就能看出来。yolo detect predict \ modelruns/detect/apple_leaf_exp1/weights/best.pt \ sourcetest_images \ conf0.25 \ iou0.5 \ save_txtTrue \ save_confTrue这里conf0.25是置信度门限调低会画出更多框、漏检变少但误检变多iou0.5控制NMS的合并强度数值越大越不容易合并重叠框。对苹果叶片病斑这种密集小目标可以试着把conf调到0.1看看到底模型学到了什么再回到0.25做正式部署。6.2 数据增强不要走向另一个极端916张原始图确实不够大所以增强是必选项。YOLO v8的训练增强参数有两个常用入口一个是命令行里直接调yolo detect train ... hsv_h0.015 hsv_s0.7 hsv_v0.4 degrees15 translate0.1 scale0.5 fliplr0.5参数的直观含义degrees15是随机旋转15度fliplr0.5是50%概率水平翻转hsv_*是颜色扰动。苹果叶片病斑对形态敏感旋转和缩放能增强对病斑方向变化的适应性但如果把degrees开到45度以上图像四角会出现大量空白区域模型会学到“背景黑边叶片”效果不升反降。我一般对叶片病害只开到15度宁可多靠缩放和hsv来凑样本量。增强的另一个误区是盲目复制少数类复制粘贴几次还不如做颜色抖动因为复制粘贴后的图像高度相似模型会记住训练集里的特例而非真正泛化。6.3 把成果导出成可交付的东西到这一步你要的已经不只是“训练完了”这个结果。常见交付方向有两个一是把best.pt导出为ONNX放进一个简单的web服务或边缘设备做实时检测二是把混淆矩阵和预标注结果导回标注工具给农技人员做二次筛选。导出ONNX的命令yolo export modelruns/detect/apple_leaf_exp1/weights/best.pt formatonnx imgsz640注意导出时的imgsz要和你训练时的imgsz一致否则推理端尺寸变化会引入精度损失。我的习惯是导出后先用同样的测试图片做一次onnx推理对比原模型输出在框坐标和置信度上的差异如果差异超过1%立即检查模型是否有动态尺寸问题。做完导出再写一个简单的预测脚本留档下次换机器或换环境不用重新翻训练命令。整套走下来我的最深体会是数据集永远是训练项目里最需要较真的部分而不要一上来就调yolo的损失函数和网络结构。916张图不多但足够让你把一个检测项目从数据清洗、格式转换、训练验证到导出部署完整跑通一遍。先把标签和图像对齐把类别顺序统一把异常坐标过滤掉模型自然会给一个体面的基准线。这套手艺既适合苹果树叶病害也适合任何带VOC/YOLO/JSON三套标签的小型数据集希望帮到你。本文还有配套的精品资源点击获取
返回列表