做农业遥感方向的人应该都有同感:无人机拍回来的影像,和我们在田里拿手机拍的,完全不是同一个世界。去年我参与了一个马铃薯病害早期筛查的项目,最初计划很省事,想着直接拿现成的叶片病斑照片去训练检测模型,毕竟这类公开数据不算少。结果把模型放到无人机正射影像上一跑,误检框多到没法看,噪声来源五花八门:垄沟阴影、土壤反光、植株重叠、地膜裂缝……那时候我才真正意识到,航拍任务必须用航拍视角的数据来喂模型,这也是我决定自己动手做一个无人机视角马铃薯苗病害检测数据集的直接原因。这个数据集一共1539张、2个类别,同时提供VOC和YOLO两种标注格式,能直接在常见检测框架开箱使用。这篇文章就围绕数据采集、标注、格式转换和训练应用四个层面,把完整经验和踩到的坑一次说清楚。
1. 为什么这个任务非要无人机视角:从一次惨痛的迁移学习失败说起
1.1 同一个苗,天上和地上看到的是两种东西
马铃薯这种作物很有意思,同一个物体在不同地方叫法差别很大,有的地方叫土豆、有的地方叫洋芋、很多省份干脆叫马铃薯,标题里把“土豆马铃薯洋芋苗”都写全,就是为了搜索和检索时不会被叫法卡住。但不管叫法怎么变,病害检测的核心逻辑是一致的:晚疫病、早疫病这类真菌性病害会在叶片和茎秆上留下颜色和形态痕迹,而无人机航拍能把这些痕迹以“俯视冠层”的形式完整记录下来。
关键在于,手机拍的病叶照片和无人机影像在视觉分布上差异极大。手机照片通常离叶片很近,拍到的是单片叶子的局部纹理,背景干净、光照可控、病斑细节清晰;无人机拍到的却是几十上百株马铃薯苗的冠层俯视图,单片叶子的纹理会迅速丢失,剩下的信息主要是“颜色异常区域”和“冠层形态异常”。换句话说,手机数据训练出来的模型学习的是叶脉、病斑边缘、背景虚化这类特征,一旦到了俯视场景里,这些特征基本失效。
我最初就是在这上面吃了大亏。用一批拍摄质量很好的手机病叶数据集训练出的YOLOv5模型,在手机测试集上mAP50能到0.93,放到无人机拼图上却只有0.31,检测框乱飞到完全不可用。后来反复排查才明白,模型不是坏了,而是它学到的特征域和无人机图像根本不在一个空间里。这件事给我的经验很简单:做检测任务先别急着堆模型,先检查训练数据和你实际应用场景的视角是否一致,不一致的话,再好的模型也救不回来。
1.2 从“有什么病”到“病在哪里”:任务定义的变化
地面巡检时,人的任务是判断“这株得了什么病”,所以病害分类数据集以病种标签为主,比如晚疫病、早疫病、病毒病等等。但无人机大范围巡查首先要解决的是另一个问题:“病苗在哪、病区面积有多大、扩展方向是什么”。在几十米高度往下看,很多病种之间的细微差异会被分辨率吞掉,反而是异常区域的位置和规模信息更加可靠。
所以在设计这个数据集时,我把类别定为两个:健康苗和病害苗。健康苗指冠层颜色正常、状态完整的植株;病害苗则是在冠层中出现了黄化、枯斑、萎蔫、倒伏等明显症状的植株。这样定义既符合无人机可见光影像能提供的信息边界,也方便后续扩展到病种识别——先让模型学会“哪里有异常”,再进一步训练“异常属于哪一种”,分步推进的稳定性会好很多。
2. 1539张图是怎么来的:从飞行采集到切片清洗的完整流程
2.1 飞行参数选择:高度、重叠率和光照窗口
数据集要可用,采集环节就要把条件定死。我这次在三个不同地块进行了采集,覆盖了马铃薯苗期到块茎形成期的关键生育阶段,共飞了大约10个架次,单架次续航内尽量完成一个完整地块的覆盖,避免跨架次光影变化太剧烈。
飞行高度我主要用了30米和60米两档。30米地面分辨率大约1.5厘米,适合看清楚单株级症状;60米分辨率约3厘米,单张图覆盖面积更大,适合掌握分布态势。实际使用时,我建议优先用30米采集,因为病害苗的单株面积在俯视图里往往只有几十到上百像素,高度再高,细小病斑会直接融合进背景纹理里,模型根本学不到有效特征。
重叠率方面,航向重叠80%、旁向重叠70%,这个配置主要是为了后期正射拼接时能有足够的匹配点,减少拼接错位。相机快门我固定在1/1000秒以上,ISO压倒最低,曝光参数全程锁死,不允许自动曝光随意浮动。云台如果带RTK定位,拼出来的影像坐标更整齐,但这个数据集本身不依赖地理坐标,所以不用RTK也能做,只是接边处检查要更仔细。
还有一个很多人忽略的因素:光照窗口。无人机采集尽量不要在正午强光下硬拍,晴天10点到15点之间,太阳高度角大,叶片镜面反射严重,健康叶片会拍出成片的白斑,这种高光区域和病害苗褪绿白化症状在视觉上很容易混淆。如果条件允许,选择薄云天气拍摄,云层相当于一个巨大的柔光箱,影像颜色均匀度会明显提升。
2.2 正射拼接与切片的取舍:为什么2600张候选只剩1539张
原始航片拼好后,得到整块地块的正射影像,宽高动辄几万像素,不可能直接整图送入检测网络,必须切成训练用的小图片。我的切片方案是1024×1024像素,步长设为950像素,让相邻切片保留一定重叠,这样跨切片边界的病株目标至少有完整形态落入某一个切片。写成脚本就是很常规的滑窗裁剪,但有几个细节值得说。
首先是切片内容占比问题。田块正射影像里有相当一部分是垄沟泥土、地膜、道路、地块边界杂物。直接全部切成小图,纯背景切片会把训练样本稀释得很厉害。我用一个简单的脚本先做了初筛:计算每张切片的RGB饱和度和亮度方差,纯土和空地切片饱和度极低、亮度方差很小,直接淘汰;剩下的2600张左右候选切片再经过人工快速浏览,去掉模糊、严重过曝、病害症状面积过小和明显拼接错位的,最终保留了1539张。
别觉得淘汰一半很浪费,这是正常的。真正有价值的数据从来不是原始采集数量,而是清洗后能稳定提供标签信息的样本数量。1539张切片里,绝大部分含有健康苗目标,超过一半含有病害苗目标,同时还保留了少量完全没有病害的负样本切片。这些负样本看起来“没用”,实际对抑制误检非常关键,后面训练结果会体现出来。
3. 两个类别的边界最磨人:标注规范和被放弃的样本
3.1 为什么要做2类而不是病害多分类
做数据集的人可能都遇到过类似纠结:农田病害那么多,只标2类是不是太粗糙了?我在项目初期其实尝试过细分到晚疫病、早疫病和健康三类,但很快放弃了,原因有两条。
第一,标注一致性很难保证。无人机影像里,同一块病斑在不同高度、不同光照下拍出来,到底是晚疫病还是早疫病,连植保专家都要凑到屏幕前反复确认,普通标注员更是各自凭感觉,标签噪声会远超收益。第二,样本量撑不起多分类。1539张图,如果按三种病害细分,平分下来每个类别的正样本可能只有千级,训练出的病种识别模型很容易过拟合。二分类只需要回答“健康还是染病”,异常区域标注起来直观且稳定,模型的定位能力也不会因为类别太粗而受影响。
实际做的时候,健康苗和病害苗的数量平衡问题也很突出。无人机俯视画面里健康苗的数量远大于病害苗,我统计过,平均每张图健康目标框大约50个,病害目标框只有6个左右,总体病害目标数千级。这个类不平衡符合真实田间情况,也合理,因为田间大面积健康、小面积发病本来就是常态,检测模型要学的是“在大量正常目标里找到少数异常目标”。
3.2 边界样本怎么处理:以“症状面积占比”为分界线
标注规范是整个数据集质量的地基。我定义了几条硬性规则,后期仲裁时都按这个执行。
一,健康苗框标注整个可见冠层区域,框要贴合冠层边界,不要包含垄沟泥土;二,病害苗框标注出现症状的冠层区域,只要一个冠丛内黄化叶片占比超过30%,或者枯萎点、枯死斑面积占比超过15%,整体框成病害苗;三,低于比例阈值的轻微症状不单独标注,宁可漏标也不引入噪声;四,多个植株连成一个冠丛时,如果其中病株数量超过一半,整个冠丛标为病害,否则不标;五,目标在切片边缘被截断超过一半的不标。
这些规则里最关键的是比例阈值。阈值定太低,标注员会把正常老叶颜色变深也算病害,噪声爆炸;阈值定太高,早期症状全被漏掉,模型对初期病斑不敏感。30%黄化占比是我试了两版标注后确定的平衡点,无人机影像在1024像素切片上,低于这个比例的症状,人工标注时自己也很难形成稳定判断。
还有一个容易踩坑的点:无人机影像上的阴影。垄沟阴影、邻近植株投下的阴影都会让冠层局部变暗,暗绿色或暗褐色区域在视觉上和萎蔫病斑很近。我的要求是标注时把纯阴影区域排除在病害框之外,只框有真实颜色异常的叶片区域。这条规则看着简单,不提前说清楚,三个人能标出三种结果。
3.3 标注一致性的日常检查
标注用的工具是常见的LabelImg,导出格式默认就是VOC XML。但工具不值钱,值钱的是流程管理。我让两位标注员独立标注同一批样本,抽了5%的数据做交叉复核,计算了框级重合度和类别一致性。两个标注员框级IoU平均在0.78左右,类别一致性接近九成,这个结果说明规则定得够清晰;不一致的部分都进入人工仲裁,由同一套规则重新判定。
日常检查还有一个笨但管用的办法:每标完一批,抽几张图把所有框画出来,肉眼看框的位置是否符合冠层边界。这一步能快速发现标注员偷懒“拉大框包住多个植株”的行为。框如果包进了大量背景,模型学到的是背景特征而不是冠层特征,后期误检率会显著上升。
4. VOC转YOLO:格式转换里的那些小坑
4.1 两种格式的结构差异和转换原理
题目里写了数据集同时提供VOC和YOLO格式,很多第一次接触的人以为只是换个后缀那么简单,其实两者的组织逻辑完全不同。VOC格式是一个XML文件对应一张图片,目标信息以绝对像素坐标存,比如一个框的xmin、ymin、xmax、ymax;YOLO格式是一行文本对应一个目标,整个数据集目录结构通常是images和labels两个文件夹,每张图片同名txt文件,每一行存类别id、中心点x、中心点y、框宽、框高,且这五个值全部归一化到0到1之间。
转换的核心就是一次性坐标变换:
center_x = (xmin + xmax) / 2 / image_width center_y = (ymin + ymax) / 2 / image_height box_width = (xmax - xmin) / image_width box_height = (ymax - ymin) / image_height
看起来只是四则运算,但实际转换时最容易出问题的环节根本不是公式本身,而是文件对应关系。VOC标注里图片和XML文件名不完全一致、XML里图片路径写错、同一张图被重复标注了两次,这些问题都会在转换后悄悄变成错位标签。因此我建议转换脚本写好后,不要把全部数据一次性跑完就结束,一定要保留校验环节。
4.2 能直接跑的VOC转YOLO脚本
这里提供一个我实际用的转换脚本,处理的是VOC标准目录结构:JPEGImages放图片、Annotations放XML。运行后会生成labels文件夹,每张图片对应一个txt文件。
import xml.etree.ElementTree as ET import os CLASS_NAMES = ['healthy', 'diseased'] # 顺序固定,训练和推理要保持一致 def voc_to_yolo(xml_path, txt_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') if size is None: print(f'[warn] {xml_path} no size') return img_w = float(size.find('width').text) img_h = float(size.find('height').text) base = os.path.splitext(os.path.basename(xml_path))[0] lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in CLASS_NAMES: print(f'[warn] {base}: unknown class {name}') continue cid = CLASS_NAMES.index(name) 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) # 裁剪到图片范围内,避免越界 x1 = min(max(x1, 0.0), img_w) y1 = min(max(y1, 0.0), img_h) x2 = min(max(x2, 0.0), img_w) y2 = min(max(y2, 0.0), img_h) if x2 <= x1 or y2 <= y1: continue cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h lines.append(f'{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}') os.makedirs(txt_dir, exist_ok=True) with open(os.path.join(txt_dir, base + '.txt'), 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) if lines: f.write('\n') if __name__ == '__main__': xml_list = [f for f in os.listdir('Annotations') if f.endswith('.xml')] for xml_name in xml_list: voc_to_yolo(os.path.join('Annotations', xml_name), 'labels')这个脚本关键有三点:从XML内部读取图片宽高而不是自己猜测;对边界框做越界裁剪;类别名称映射表是全局唯一的。只要这三个点不出错,转换过程基本不会有意外。
脚本跑完后,别忘了把VOC的JPEGImages和生成的labels按训练验证测试划分:
import os import random ids = [f[:-4] for f in os.listdir('labels') if f.endswith('.txt')] random.seed(42) random.shuffle(ids) n = len(ids) train_ids = ids[:int(0.8*n)] val_ids = ids[int(0.8*n):int(0.9*n)] test_ids = ids[int(0.9*n):] for subset, subset_ids in [('train', train_ids), ('val', val_ids), ('test', test_ids)]: os.makedirs(f'images/{subset}', exist_ok=True) os.makedirs(f'labels/{subset}', exist_ok=True) for idx in subset_ids: os.replace(f'JPEGImages/{idx}.jpg', f'images/{subset}/{idx}.jpg') os.replace(f'labels/{idx}.txt', f'labels/{subset}/{idx}.txt')1539张图我按8:1:1划分,训练集1231张、验证集154张、测试集154张。划分时注意设置随机种子,保证多次实验对比时样本分布不发生变化。
4.3 转换后必做的反向校验
转换完不要急着开训练,先用上一小节提到的反向读取脚本检查每个txt文件。我这里给出最精简的校验逻辑:
for txt_file in os.listdir('labels'): with open(os.path.join('labels', txt_file)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: raise ValueError(f'{txt_file} bad line: {line}') cid, cx, cy, w, h = map(float, parts) assert cid in (0, 1), f'{txt_file} bad cid' assert 0 <= cx <= 1, f'{txt_file} cx out of range' assert 0 <= cy <= 1, f'{txt_file} cy out of range' assert 0 < w <= 1, f'{txt_file} w out of range' assert 0 < h <= 1, f'{txt_file} h out of range'更可靠的做法是随机抽几十张图,把转换后的归一化框重新画回原图,肉眼核对框是否仍然贴合目标。这一步能看到很多脚本发现不了的问题,比如某些XML的bndbox坐标写反了、框粘在图片边缘被裁掉一半等。数据集质量好坏,往往就体现在这种别人不愿意做的校验环节上。
顺便说明一下VOC侧的ImageSets/Main目录结构。如果框架要求VOC格式,需要在Main下生成train.txt、val.txt、test.txt,每行一个不带扩展名的图片名。这个目录在旧版检测框架里是必须的,新版大多直接用目录划分,但保留一份总没错,兼容老工具链时能省不少事。
5. 用这份数据训YOLO系列:实测指标和值得照抄的调参策略
5.1 训练配置和基线结果
数据准备好之后,我先后在YOLOv5和YOLOv8上做了多组实验。这里以YOLOv8s作为基准配置,因为它在同样输入尺寸下训练速度和精度均衡性更好。数据集配置文件potato.yaml长这样:
path: /data/potato_dataset train: images/train val: images/val test: images/test nc: 2 names: 0: healthy 1: diseased训练命令直接用YOLO官方CLI风格:
yolo detect train \ data=potato.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=640 \ batch=16 \ patience=30 \ device=0第一次训练拿到的基础结果大概是mAP50在0.86附近,mAP50-95在0.58附近。这个数字在我们项目里已经能用了,但对于检测应用来说还远不够稳,尤其是在病害苗这一类上,recall明显低于健康苗。让我先把“能用到什么程度”和“怎么进一步涨点”分开说,后者才是这次调参的重点。
5.2 无人机航拍目标的特殊性:小目标、密集目标与NMS
无人机影像里的目标尺寸和常规物体检测数据集有本质区别。常规COCO数据集里目标平均尺寸大、独立性强;而马铃薯苗在1024切片里,单株冠层可能只有三五十个像素宽,一图里挤着几十上百株,是典型的小目标、密集目标场景。
针对这个特点,第一件事是把输入分辨率提上去。从640提到960,等于把目标有效尺寸放大了1.5倍,病害苗的召回率提升非常明显,代价只是训练显存和推理耗时增加。如果显卡显存够,我建议直接按960来训,这个涨点方式比换大模型性价比高得多。
第二件要做的事是调整NMS策略。密集目标场景下,默认NMS阈值很容易把相邻的多个目标合并成一个,或者把同一株的重叠框错误保留。我用IoU阈值0.65替换默认的0.5,置信度阈值设为0.25后,病害苗的漏检少了约6个百分点。这个参数不要抄我的,要根据你自己的验证集去扫一遍,但不扫一遍会白白丢掉一部分精度。
第三件值得提的是增强策略。YOLO系列默认自带的Mosaic增强对无人机数据很有用,因为它把四张图拼成一张,相当于隐式增加了小目标出现的频率。但颜色增强幅度要收紧,我试过把HSV色相扰动开到0.05,病害苗的验证指标下滑明显,因为病斑检测高度依赖叶片颜色,过度扰动等于给模型灌入错误的颜色分布。最后我基本只用轻微亮度和饱和度扰动,色相扰动幅度压得很低。
5.3 结果对比:不同输入尺寸和模型大小的取舍
这组数据是我在同一批数据划分上跑出来的,供做类似项目的朋友参考:
| 配置 | imgsz | mAP50 | mAP50-95 | 单图推理耗时(近似) |
|---|---|---|---|---|
| YOLOv8n | 640 | 0.80 | 0.52 | 更低 |
| YOLOv8s | 640 | 0.86 | 0.58 | 基准 |
| YOLOv8s | 960 | 0.89 | 0.64 | 约1.8倍基准 |
| YOLOv8m | 960 | 0.91 | 0.69 | 约2.6倍基准 |
| YOLOv8m + 切片后处理 | 960 | 0.93 | 0.72 | 约3倍基准 |
这里的单图推理耗时指的是对一张1024×1024切片在单张GPU上的耗时,只做量级参考,不同硬件差异很大。从表格能明显看到,从n到s再到m,精度持续上升;但m模型配上960输入后推理耗时已经比较高,如果最后要部署到边缘设备上做实时处理,需要再想办法压缩。
如果追求轻量部署,我建议试试YOLOv8s 960加切片后处理:精度接近m模型640的效果,推理代价更可控。这组实验也说明了一个道理,对小目标密集场景,输入分辨率带来的收益往往比模型参数量更直接,先提分辨率,再换大模型,顺序不要反。
6. 无人机影像上的误检重灾区:阴影、反光和跨地块
6.1 误检样本统计分析:到底是谁在说谎
模型训练完成后,我对基线模型的误检样本做了统计,把检测错的框分成几类,结果排前的是下面这些:
| 误检类型 | 大致占比 | 视觉特征 | 为什么像病害 |
|---|---|---|---|
| 垄沟阴影边缘 | 40% | 暗褐色、长条形 | 与萎蔫枯死苗颜色接近 |
| 土壤或地膜裸露 | 25% | 高光或纹理杂乱 | 与褐色病斑颜色接近 |
| 健康叶反光 | 20% | 亮白色高光区域 | 与枯死白斑类似 |
| 其它边界模糊茎秆 | 15% | 形状细长 | 与单株病苗形状接近 |
看到这个分布,我对模型的判断放心了不少。误检不是模型瞎学,而是它确实在图像里找到了和病斑颜色相似的低级视觉特征。换言之,只要想办法处理掉这些相似背景,误检就能大幅下降。
6.2 针对性优化的三个经验
针对阴影误检,我用了一个简单但有效的后处理手段:在推理结果上,对每个候选框内部的HSV色相和亮度做统计。健康叶片的V值通常在0.3以上,而垄沟阴影区域的V值普遍低于0.2,同时饱和度也偏低。凡是满足“平均亮度低于0.2、饱和度低于0.25、框面积又特别大”的候选框,直接降置信度或丢弃。这个规则是靠肉眼观察误检样本总结出来的,逻辑简单,但对阴影类误检的过滤效果非常显著。
针对叶片反光,最根本的办法是改采集时段。把飞行窗口从中午11点到14点挪到9点到10点半、15点前收工,反光区域数量立刻减少一个量级。同时在实际数据集里保留少量带反光的健康苗样本作为难例,模型见过一次真实反光和枯白病的区别后,后续碰到同类场景不再乱报。
土壤和地膜的解决思路不太一样。地膜表面的折痕和高光在俯视图里看起来像白色病斑,但我发现这类误检框几乎都落在绿色冠层区域之外。因此我做了一个预处理:用简单的颜色阈值分割出绿色冠层掩膜,推理时把掩膜之外的检测框全部过滤掉。这个操作有点土,但不需要额外模型,执行效率高,真正上田作业时很管用。
6.3 换个地块就崩:泛化问题的数据层面解法
航拍数据集的另一个大坑是换地块泛化。我在原始地块验证时mAP50能到0.91,把同一模型拿到另一个品种、不同行距、不同土壤背景的地块时,mAP50直线掉到0.62上下,健康苗误检和病害苗漏检都变得很严重。
深挖原因,主要是三个维度变了:品种叶片颜色深浅不同、垄宽和行向导致的空间分布不同、土壤颜色和湿度不一样。模型对训练地块的“整体视觉风格”记忆得太死,而对真正的病害形态特征泛化不足。
解决办法只能从数据层面入手。我把采集范围从1个地块扩展到3个地块,纳入2个不同品种,训练集里每个品种的图像尽量平衡。数据范围扩大之后,同一模型在新地块验证集的mAP50回到了0.82左右。我又加了一步预处理:把所有训练和推理的切片做一次直方图匹配,让光照和色偏尽量对齐,跨地块精度进一步涨到0.86。这个涨幅说明,跨地块泛化问题靠调模型参数很难解决,扎实地扩充数据多样性比什么都管用。
7. 这个数据集后续还能怎么长:检测之外的方向
7.1 从目标框到病斑面积:实例分割和分级估计
二分类检测框能告诉用户“这片区域有病”,但没法精确回答“病斑面积占冠层面积的比例”。农业植保上,病情严重程度直接影响变量喷洒处方,而框级别的面积估计偏差很大,尤其当病害苗和健康苗挨在一起时,框里经常混入大量健康背景。
后续要做更精细的评估,我建议把标注从矩形框升级到多边形分割。无人机图像里病斑边缘本身并不是特别清晰,纯手工像素级标注成本很高,可以先用常见的通用分割大模型做预标注,再由人工修正病斑边界。拿到实例分割结果后,严重程度分级就顺理成章了:比如按病斑像素占据整个冠层的比例,把diseased细分成轻度、中度、重度三级,模型从检测回归问题变成检测加分级问题。
7.2 从RGB到多光谱、从单时相到时序
可见光影像能捕捉到的病害特征主要是颜色变化,但颜色变化往往滞后于植物生理异常。如果有条件搭载多光谱相机,NDVI和NDRE这类指数对叶绿素含量和水分胁迫更敏感,能在叶片明显变色之前就捕捉到异常信号。在我接触到的不少农业遥感方案里,RGB模型负责初筛、多光谱指数负责复核已经是最常规的组合,性价比很高。
另一个方向是时间序列分析。马铃薯晚疫病扩散速度极快,单次航拍只能得到瞬时患病状态,如果每隔3到5天飞一次同一地块,把多次检测结果按位置对齐,就能看到病区的扩展方向和扩散速率。这种带时间上下文的检测结果,比单张静态图的普查价值高很多,尤其适合病害预警和防控效果评估。
7.3 和植保作业系统衔接的落地思考
检测结果最终要落到“喷药还是不喷、喷多少”这类决策上。一个比较现实的落地方案是:无人机航拍后生成病害分布图,经过统计聚合生成处方图,再把处方图交给植保无人机做变量喷洒。检测模型的输出密度和边界决定了处方图的最小作业单元,检测精度不足时,宁可把作业单元划大一点,也不要让变量喷洒在几米尺度上乱跳。
还有一个部署层面的提醒。田间作业经常需要在没有网络的环境下运行推理,模型最终要跑到边缘计算设备上。这时候轻量化模型和输入分辨率压缩不可避免,但经过前面的实验,我们知道输入分辨率对精度影响很大,所以部署前一定要在真实硬件上测一轮时序帧,不要只看单张静态图片的指标。能跑通一次完整的“航拍—拼接—切片—推理—处方图”流程,比模型指标多几个点重要得多。
最后说点个人体会。做这个数据集最消耗我的不是标注本身,而是定义“什么才算病害”。如果一开始没把比例阈值和边界规则说清楚,几个人能标出好几种结果,返工成本远高于晚开工一周。另外,无人机数据集的采集最好在同一个生育期的连续几天内完成,天气一变,叶片反射和土壤背景都会变,模型泛化立刻受影响。我把脚本、标注规则和划分逻辑都固化成了一套流程,每次下地回来就能自动走完一遍。这套流程不一定适合所有项目,但“把视角匹配这件事提前到数据集阶段”的原则,在任何航拍检测任务里都值得优先考虑。