小白菜叶子病害检测这件事,听起来不大,做起来其实挺折腾。真正上手之后你才会发现,模型训练能不能出效果,一大半取决于数据集做得干不干净。最近我正好在整理一批小白菜叶子病害健康状态检测数据集,总共1863张图、2个类别,同时导出了VOC和YOLO两种标注格式,也就是XML和txt双版本。这套数据既可以用来训练目标检测模型,也可以拿来验证标注质量、做算法对比实验,对做农业视觉、植物病害识别的朋友来说,是相当顺手的一套底子。
我自己在整理和清洗这类数据时踩了不少坑,从拍摄角度、光线控制到标注边界,再到格式转换时的坐标归一化,每一步都有一些常规文档里见不到的细节。这篇就把我的实操过程、背后逻辑和踩坑经验完整写出来,给正准备做植物检测数据集的同学一个参照。
1. 小白菜叶子病害健康状态检测数据集到底是什么
1.1 从标题拆出三个关键信息
先把这个标题拆开看。1863张,是图片总量;2类别,对应的是健康叶子和病害叶子;VOC+YOLO,则是两种并行的标注格式。看起来简单,但每一样都有讲究。
图片数量这块,1863张对目标检测来说属于中等偏小规模的数据量。做二分类目标检测,这个量级配合合理的数据增强,完全够把模型训到能用的程度。如果只有几百张,模型很容易过拟合,换一组光照条件就崩;如果上万张,对采集和标注的压力又会大很多。1863张算是一个性价比很高的起步线。
类别数量上,2类别意味着你的检测头只需要输出两个概率值,不用处理长尾分布,也不用担心类别间特征太接近导致混淆。健康叶子和病害叶子在颜色、纹理、形态上都有肉眼可见的差异,这对模型提取特征是友好的。但要注意的是,类少不代表简单,背景里的杂草、泥土、水珠、遮阴造成的色差,照样会骗过模型。
VOC格式和YOLO格式的区别,简单说就是一套用一个XML文件描述每张图里的标注信息,另一套用一个TXT文件按行记录归一化坐标。XML适合给人看、做检查、做格式中转;TXT适合给YOLO系列训练框架直接吃。两套都存在的好处是,你既能用标注工具回看检查,又能不写转换脚本直接丢进训练管道。我在实际项目里长期保留双格式,就是为了随时回来改标签、做筛查,不用临时找工具转来转去。
1.2 二类别问题的适用边界
二分类目标检测看起来简单,实际用的时候要明确边界。你的模型只能回答这张图里叶子是健康还是发病,回答不了“这是什么病”。如果数据里包含霜霉病、软腐病、黄叶病等多种症状,但都统一标成病害这一类,那么模型学到的就是“看起来不正常”的共性特征,而不是具体病害的个性特征,换个症状不同的病叶过来,有可能漏检。
所以这个数据集适用的场景是:做早期预警、做健康状态筛查、做农情监测里的“正常/异常”判断。比如在大棚里部署巡检设备,发现病叶就报警,再由人来复诊,这套数据就非常好用。反过来,如果你想细分病害种类,需要重新设计类别体系,把每一种病单独作为一类,并保证每个类别的样本量均衡。这也是我当时没有急着扩类别的原因,类别越少,标注一致性问题越少,模型越不会学歪。
另外,二类别数据还有一个隐性价值,就是能用来做迁移学习。哪怕你最后的目标是十几种病害多分类,也可以先用这套二类别数据把特征提取层训扎实,再用带细分标签的小批量数据做微调,收敛速度和稳定性都会好很多。
2. 数据采集与标注实操:1863张样本是怎么凑起来的
2.1 拍摄场景设计与样本多样性
很多人在这一步偷懒,拿着手机对着几棵白菜一顿拍,结果模型一上测试集就翻车。表面上是模型不行,本质上是你拍回去的图太“干净”了。自然环境下的小白菜,叶子有遮挡、有阴影、有泥土、有水珠、有虫咬痕迹,还有拍摄时手抖导致的轻微模糊,这些全都要覆盖进数据集里。
我采集时的思路是,分几个子场景去拍。首先是晴天上午和阴天两种自然光条件,因为大棚里的光线变化很大;其次是室内补光灯均匀照射的条件,作为低干扰样本;最后还专门挑了一部分带土壤背景、带杂草背景、带滴灌水珠的照片,用来让模型学会忽略干扰。拍摄距离也分了两档,一是离叶子约30厘米的近距离特写,二是约60厘米带多片叶子的中距视角,这样模型对不同尺度下的目标都有感知。
数量分配上,我没有做三三平均,而是让田间自然场景占六成,室内可控光场景占三成,困难样本占一成。原因是模型最终大概率用在大田或大棚环境,训练分布尽量靠近推理分布,效果才稳。拍完之后还有一步很多人会漏掉,就是把图片按场景分组过一遍,删掉完全重复的、严重过曝的和对焦完全失败的照片,避免无效样本干扰训练。
2.2 标注策略:边界框该画到哪里
标注是决定数据集质量的核心环节。对叶子目标检测来说,我采用的策略是“完整叶片,宁大勿小”。意思是只要叶片主体完整出现在画面里,就标一个矩形框把整片叶子包住;如果一片叶子的健康部分和病斑连在一起,只要这片叶子被判断为病叶,就用一个框框住整片叶子,而不是单独框病斑。
这里有个容易钻牛角尖的地方。有人会把叶片上的每个病斑单独标注,想增加“信息量”,但实际上这会让模型学到的不是“病叶长得什么样”,而是“病斑长得什么样”。病斑有大有小、有深有浅,单独做目标会导致模型对微小病斑不敏感,漏检率明显上升。以整片叶子作为检测单位,反而是更稳的做法,尤其适合这种做健康状态筛查的项目。
遮挡情况的处理也得提前定好规则。我采用的规则是:遮挡面积不超过叶片面积百分之三十的,按完整叶片标注;超过的,弃标。这么做是为了保证绝大多数样本是完整形态,减少标注边界模糊的可能性。标注时框的四边尽量贴合叶缘,但不必追求像素级精准,YOLO训练对于两三像素的框偏移不敏感,硬抠边缘反而浪费时间。
2.3 一致性校准与算量统计
一致性校准是最容易被忽略的环节。如果三个人标同一批图,一个人习惯框松一点,一个人习惯框紧一点,模型看到的正样本边界就是乱的。我做数据时,先由一个人标完前100张,定好“框完整叶缘、不留大空隙、也不压到叶子内部”的统一标准,然后其他人对照这100张的标准去标,最后再抽取一部分交叉检查。
交叉检查的方式也不复杂,把同一批图片分别给两个人标,比对两个框的交并比,一般要求IoU大于0.7就算一致,低于0.6的单独拉回来重标。这个过程不是走形式,很多框的偏移问题就是从这里暴露出来的。标注完成后,我用脚本统计每个类别框的数量、每张图的平均目标数,以及所有框的宽度高度分布。病叶框总数量和健康叶框总数量不要偏差太大,最好控制在1:1到1:1.5以内,如果某一类太多了,训练时会出现类别倾向,后面调整会麻烦。
3. VOC转YOLO格式:从XML到txt的完整流程
3.1 两种格式的底层逻辑
VOC格式,简单说就是每张图片对应一个同名XML文件,文件里用object节点记录目标的名称和边界框坐标。坐标是绝对值,单位是像素,记录了左上角xmin、ymin和右下角xmax、ymax。这种格式的好处是任何标注工具都支持打开编辑,坏处是训练框架一般不能直接吃,还要做一次转换。
YOLO格式则是每张图片对应一个同名TXT文件,每一行是一个目标,格式是“类别ID x_center y_center width height”。这里所有数值都是归一化的,需要除以图片宽度和高度。类别ID是从0开始编号的整数,不是类别名称。比如0代表健康,1代表病害,那么在TXT里健康叶子的行首就是0,病害叶子的行首就是1。
这两个格式之间最核心的转换点有两个:一是坐标从绝对像素转成相对比例,二是类别从名称映射成ID。这两个点任何一个搞错,轻则训练报错,重则模型学废了还不知道原因。尤其要注意的是,有的工具导出坐标时用的是目标中心点和宽高,有的用的是左上角右下角,转换前必须确认清楚。
3.2 格式转换脚本与坐标归一化
我自己写的转换脚本并不复杂,Python的xml.etree.ElementTree就能搞定,不需要装额外依赖。核心逻辑是,遍历图片目录下所有XML文件,提取object节点的name和bndbox子节点,把xmin、ymin、xmax、ymax取出来,除以图片的宽高得到归一化坐标,再计算中心点x_center、y_center和宽高width、height,最后按“类别ID x_center y_center width height”的顺序写入TXT。
坐标系换算的公式我自己反复验过,实际用起来就一句话:x_center = ((xmin + xmax) / 2) / image_width,y_center = ((ymin + ymax) / 2) / image_height,width = (xmax - xmin) / image_width,height = (ymax - ymin) / image_height。其中image_width和image_height要取图片的真实像素尺寸,不是标注时的缩放尺寸,如果两者不一致,坐标就全错位了。
写脚本时我特意加了一个越界检查,凡是计算出来的x_center或者width不在0到1之间的,记录到日志里单独处理。因为有些标注框会超出图片边界,直接转进TXT,YOLO训练时会出现严重的锚框匹配异常。越界样本也不一定要丢掉,把框的边界截断到0到1之间就可以,但要注意截断后的框不能太小,太小的框对模型训练几乎没有贡献。
3.3 训练集验证集划分的注意事项
格式转换完成后,下一步就是划分训练集和验证集。很多人会直接随机打乱然后按比例切,这在物体检测数据里其实有隐患。同一片叶子可能在多张照片里反复出现,如果这些相似图像被同时分进训练集和验证集,验证集就失效了,模型在验证集上的分数会虚高,上了新图就露馅。
我自己的做法是把图片按拍摄场景分组,先按场景为单位做划分,保证训练集和验证集里的场景不重叠。比如晴天场景的图进训练集,那么晴天场景的所有图就不进验证集;验证集专门用室内光场景或部分田间场景。这样验证集才是真正“没见过的环境”,指标才可信。
比例上,我习惯用8:2,也就是1490张训练、373张验证。没有单独再分测试集,因为1863张的规模再拆测试集会稀释训练数据。真正上线前会额外找一批新拍的图片做一次盲评,那批图完全不出现在训练和验证流程里,用盲评的mAP来估算真实表现。
4. 训练前必须做的质量审计与数据增强
4.1 自动化体检脚本的思路
很多同学拿到别人分享的数据集,第一件事就是直接开训。我建议先写一段自动化检查脚本,把数据质量做一次审计,至少排查这几类问题:图片与TXT文件名完全对应、每个TXT里的类别ID不超过总类别数、所有归一化坐标在0到1之间、每个TXT文件非空、以及图片本身能正常解码。
这几个检查看起来简单,真跑起来能发现不少毛病。比如有些TXT文件是0字节的,原因可能是标注时漏存,也可能是转换脚本没匹配到XML;再比如某些图片的宽高是奇数,缩放尺寸后坐标比例会有一点点偏差,虽然影响不大,但最好统一处理成偶数尺寸。我踩过最狠的一次坑,是某批图片被人为裁剪过,但XML里的坐标还是裁剪前的原图尺寸,结果模型训练时大量锚框落在图外,损失一直在高位震荡,查了半天才定位到是坐标错位。
审计脚本跑完之后,另一步是可视化抽样。把训练集中的图片和YOLO标注画出来,用随机抽样的方式看100张,肉眼检查框和叶片边缘是否匹配。这一步不是形式主义,特别是检查有没有把背景里无关目标误标成叶子,或者把病斑单独框成了目标。标注错误分布不均匀,模型是能学到的,只不过学到的全是错误规律。
4.2 数据增强的“能做”与“不能做”
数据增强是1863张数据集能不能跑出更好效果的关键,但植物检测场景要格外谨慎。能做的增强包括:随机水平翻转、小幅旋转、亮度扰动、对比度扰动、高斯噪声和随机缩放。这些操作不会改变病斑和健康叶子的根本特征,还能提升模型对光照、拍摄角度变化的鲁棒性。
不能做或者必须谨慎做的增强,第一是大幅度的色相变化。白菜叶子的绿色、病斑的黄色褐色,是模型分类的重要依据,你把色相随机转了几十度,叶子可能变成蓝色或紫色,训练出的模型对颜色就失去了敏感性,真实场景下容易把黄化叶漏掉。第二是强烈透视变换,叶子形状会发生非真实扭曲,导致模型学到的叶片轮廓特征失真。第三是完全随机的裁剪,你可能把一片叶子裁掉一半,标准好的框也变得不完整,干扰训练。
我个人常用的增强组合是mosaic加mixup配合轻度颜色抖动,但颜色抖动的幅度控制在HSV三个通道各自的扰动不超过20。增强逻辑是在线增强,训练时实时生成增强后的样本,不额外把增强后的图片写回硬盘,这样不占存储空间,也等于变相增加了训练样本量。
4.3 类别失衡问题与对策
2类别看似简单,类别失衡还是可能出现。比如健康叶子很好找,拍几组就有几千张;病害叶子需要碰运气,可能拍了很多只有几百张。一旦病叶框数量明显少于健康叶框,模型会倾向于把一切叶片判断为健康,因为这样在损失函数里犯错更少。
我处理失衡的思路不是粗暴地复制病叶样本,而是优先调整训练时的采样权重。YOLO训练的数据加载器支持按类别权重采样,把病叶样本的上采样概率调高,相当于让模型在每个batch里都能见到足够多的病叶。其次是增加病叶场景的增强力度,对病叶样本做更多的翻转和亮度变化,让有限的数据产生更多的有效变体。
如果失衡特别严重,比如病叶只有健康叶的十分之一,那就建议回到采集阶段补充数据了。单靠算法补救,能到及格线但很难到优秀水平。我当时把病叶框和健康叶框的数量比对出来,发现大约是1:1.2,属于可接受范围,就没有额外做采样加重处理,直接训练效果已经不错。
5. YOLO训练配置与结果排查
5.1 模型选型与数据配置文件
数据集准备到这个阶段,训练本身反而轻松了。模型选型上,如果是边缘设备部署,我建议直接用YOLO系列里的轻量版本,兼顾速度和精度;如果显卡资源充足,可以用稍大的模型换更高的准确率。对于这种叶片目标大、类别差异明显的任务,轻量模型的性价比很高,没必要杀鸡用牛刀。
训练前要写好数据配置文件。里面要指定训练图片路径、验证图片路径、类别数量和类别名称列表。我习惯把整份数据放在同一个目录下,目录里建images和labels两个子文件夹,images下再建train和val两个子目录,labels对应也建同样的结构,YOLO系列框架默认就是按这个结构找数据的,适配起来零成本。
配置文件的写法大概长这样:
path: ./data/cabbage_leaf_dataset train: images/train val: images/val nc: 2 names: 0: healthy 1: disease要注意的是,类别名称的顺序必须和TXT文件里的ID对应。TXT里写0,模型就认为这是names列表里第0个类别,顺序错位会导致模型学习的语义和最终输出对不上,推理时症状标签彻底错乱。我一开始也没太注意,后来发现推理结果里健康标签和病害标签完全反了,查了半天才发现是转换脚本里类别ID映射写反了。
5.2 训练参数与结果指标解读
训练参数里最需要花心思的是输入尺寸、批量大小和训练轮数。输入尺寸我建议从640起步,叶子检测目标不算特别小,640的输入能在细节和速度之间取得平衡。批量大小要根据显存来定,显存不太够就把批量调小一点,但不要小于16,过小的批量会让BN层的统计不稳定,影响收敛。
训练轮数的话,这个量级的数据集我一般从100轮起跑,配合早停机制,当验证集mAP连续20轮不提升就提前终止。这样能避免在后期过拟合浪费时间。损失曲线要盯着box_loss和cls_loss两条线来看,如果训练损失持续下降但验证损失开始回升,说明模型开始记训练样本的细节了,要提前停或者加大增强。
结果指标上,植物检测场景重点看mAP0.5和mAP0.5:0.95两个值。mAP0.5是IoU阈值0.5时的平均精度,反映的是“大致框得准不准”,对实际落地参考意义最大。mAP0.5:0.95是多个IoU阈值下的平均结果,更严格,一般会比mAP0.5低20个百分点左右,不用因为数值低而焦虑。PR曲线也要看,曲线越靠近右上角越好,如果曲线中间出现凹陷,说明某些类别的置信度分布有问题,经常是误检或者标注噪声导致的。
5.3 漏检误检的排查思路
训练完之后,最常遇到的情况就是漏检和误检。漏检通常是模型没把某些病害叶子“识别出来”,误检则是模型把背景或者健康叶子当成了病害。排查的时候我建议先做两类可视化:一类是画置信度低于阈值但真实标注存在的图,看看是病斑颜色不典型还是叶片被遮挡严重;另一类是画置信度高但预测框明显偏掉的图,看看目标是不是长得像病叶的枯草或泥土。
如果发现漏检的样本主要集中在某一种环境条件下,比如全部是逆光拍摄的,说明训练集中这类场景样本不够,需要回去补拍或者增强亮度扰动。如果误检集中在某些特定背景,比如土壤团块被误判为病叶,说明模型学到了背景纹理而不是叶子特征,这时候要检查是不是标注时把背景里的东西圈进去了,或者增强时对比度加得太狠,导致纹理失真。
还有一个容易被忽略的原因是框选边界。如果你标注时把病叶框得特别紧,叶缘部分被切掉,模型就没法学到叶片的完整形状。当目标处于图像边缘或与其他叶子重叠时,模型会拿不准窗口该给多大,表现出来就是预测框只有真实大小的一半。针对这个问题,我后来标注时统一多留几个像素的外边距,效果立竿见影,漏检率降了大概两成。