
开篇先说一句大实话农田里的杂草识别过去基本靠人弯着腰一垄一垄地看一小块地走下来就是大半天。我在这套“无人机UNET农田图像分割”方案里把整条链路跑通了一版包括数据采集、自建UNET网络、作物与杂草分类训练、大图推理拼接算是我自己沉淀下来最完整的一组工程记录。项目编号我记作10470期如果你正准备做类似方向这篇文章可以直接当成一套复现笔记来用。这套方案解决的事情很明确无人机在农田上方飞一遍拍到高分辨率可见光影像程序自动把地里每个像素分成“作物、杂草、背景”三类输出一张分割图。有了这张图后面无论是变量喷洒、除草机器人导航还是产量估算都只是调用结果的另一步。算法选的是UNET这个选择不是拍脑袋后面我会详细拆解。适合看到这篇文章的人刚入门语义分割但不想只跑MNIST的算法新手正在做农业AI项目的学生以及想给植保无人机加感知能力的工程师。下面直接进入正题。1. 为什么无人机农田分割偏偏选中UNET1.1 传统杂草识别的效率瓶颈传统方式识别杂草主流路径是“颜色指数阈值分割”。比如把图像转到HSV空间用绿色植被指数把植物区域提出来再根据叶片形状特征做形态学区分。这套方法在单一作物、单一杂草、光照均匀的温室里效果尚可一到大田就露馅阳光下叶片过曝、阴影里暗部细节丢失、杂草长得跟作物一个颜色一个形状阈值怎么调都顾此失彼。人工目视排查更不用提一亩玉米地按常规密度算植株大约四五千株每株都要弯下腰看根部的杂草分布一个人一天干不完两亩地。植保无人机装药起飞只需要几分钟真正卡住精准喷洒的从来不是飞行器而是“杂草到底在哪”这张地图没人给得出。深度学习的价值在于让模型从海量标注数据里自己总结“杂草在视觉上到底是什么”而不是人去写那套永远写不完的颜色规则。UNET作为语义分割网络中的基础结构在这类任务里对算力要求不高对数据量要求也不算苛刻是性价比极高的起点。1.2 无人机视角带来的图像特征变化很多人第一次看到无人机航拍农田图会愣一下跟平时拍照完全是两码事。地面拍摄是侧视叶片有遮挡关系、有透视角度无人机是近乎垂直的俯视正射视角看到的是一整片冠层的俯视纹理。这种视角下作物行距、株距呈现出高度规律性的空间分布杂草则是破坏规律的“噪声点”。这意味着模型学到的特征与通用自然图像分割任务差别很大。比如在Cityscapes数据集上表现优异的模型结构直接搬来农田不一定好使——因为没有人行道、汽车、行人这类语义形状取而代之的是大量重复纹理和细小目标。UNET的多尺度特征融合机制在这里表现出了优势高分辨率浅层特征负责保留叶片边缘细节低分辨率深层特征负责判断纹理属于哪一类。1.3 UNET在这个任务里的三个不可替代优势对比FCN、SegNet、DeepLab等常见分割结构我在这个项目里最终选择UNET理由集中在三点。第一点是数据效率。UNET通过跳跃连接把浅层高分辨率特征直接传递给解码器梯度可以“抄近路”回传即使训练样本只有几百张标注图仍然能较快收敛。FCN和DeepLab在数据量不足时很容易出现边界崩坏或者大面积假阳性。第二点是边界质量。杂草分割最头疼的是叶片边缘“糊成一片”UNET的U形结构天然保留多级分辨率信息输出的掩膜边界精细度远高于直接上采样的FCN。第三点是工程友好度。UNET不含空洞卷积不依赖预训练Backbone也能达到可用精度显存占用可控部署时转ONNX、量化都方便。DeepLabV3虽然精度上限更高但对显存和训练时间的需求明显上了一个台阶对于一个需要反复迭代的农业项目来说不是最优解。注意如果你的实际场景是大面积单一作物分类比如整片小麦地块识别高分辨率语义分割的优势体现得没那么明显图像分类模型加滑窗反而更快。UNET真正的舒适区是“同一画面里存在多个细小类别需要逐像素区分”的场景。2. 数据集构建无人机采集规格与标注实操2.1 飞行采集参数我实际用的这套配置数据集的质量直接决定模型上限。很多项目死在算法调参上其实根子是图像采集阶段没做对。我在这轮项目中使用的核心参数如下参数项配置值说明飞行平台四旋翼多轴无人机载重满足单镜头即可相机可见光RGB镜头2000万像素以上多光谱可选但成本翻倍飞行高度30米至50米过低则单张覆盖小过高则杂草目标变小航向重叠率80%保证拼接与单帧选择有冗余旁向重叠率70%同上云台角度垂直向下-90°避免斜视造成的透视变形拍摄时间上午9:00-11:00 或 下午15:00-17:00太阳高度角适中阴影短风速小于4级防止叶片晃动导致模糊采集时还有一个经常被忽略的点不要只挑“长满杂草”的地块拍。模型训练需要负样本也就是苗齐、草少、甚至空地、田埂、水沟的图像否则推理时遇到没见过的土面区域会乱输出。2.2 标注定义与目标编码设计标注环节我用某开源图像标注工具逐像素画多边形。类别定义上我把像素分为三类作物、杂草、背景。背景包含土壤、碎石、枯叶、地膜等一切非绿色植被区域。有人会问为什么不把“土壤”和“作物”分成两类就够了杂草包含在作物里不行吗不行。精准喷洒的核心诉求恰恰是把杂草从作物中摘出来如果模型把杂草当成作物喷洒器就会跳过杂草区域等于白做。标注文件我存成8位单通道PNG掩膜像素值对应类别索引0为背景1为作物2为杂草。这样做的好处是在训练时可以直接用索引模式读标签不需要再做one-hot转换显存占用也低。标注时注意杂草叶片边缘不要“描金边”——贴着叶片外轮廓留1到2个像素的余量否则模型会在边界上产生持续的模糊预测。2.3 数据增强俯视场景的增强逻辑跟自然图像完全不同俯视农业影像有很强的旋转不变性——田野里没有“上下颠倒”的概念无人机航线可以从任意方向飞过同一块地。这意味着水平翻转、垂直翻转、旋转90度、旋转180度都是安全增强不会破坏语义。我在项目中用随机旋转范围0到360度、随机水平与垂直翻转、随机亮度与对比度扰动、轻微高斯噪声四种组合。但有两个增强操作我坚决不用。一个是随机裁剪缩放RandomResizedCrop它会把杂草目标缩到模糊状态反而破坏细节另一个是强色彩抖动农田影像的色彩本身是区分类别的关键线索色相偏移过度会让模型学到假的相关性。实操心得数据增强在农业分割中的核心逻辑是“保持真实采集分布的前提下增加样本多样性”不是把图片弄成抽象画。每换一种增强我都会在验证集上对比一次mIoU没提升就直接删掉。2.4 大图切块与标签同步切割无人机单张影像动辄几千万像素显存根本塞不下。标准做法是把大图切割成固定尺寸的小图块训练和推理都基于图块完成。我的切块尺寸是512x512像素配合50像素的重叠。切块的时候必须同时切割标签掩膜且要保持完全相同的坐标偏移。我写了一个纯Python脚本读大图和PNG掩膜按滑窗步长切出(img, mask)对带重叠地保存成训练样本。切出来的图块我还会用一个滤除规则如果某一图块中背景像素占比超过95%就丢弃该样本否则整张数据集里全是空地模型会飞快过拟合到“全都预测成背景”。3. 自建UNET网络编码器-解码器结构逐层拆解3.1 跳跃连接到底解决什么问题UNET这个名字来自网络结构图长得像字母U。左侧是编码器逐层下采样每次都让特征图分辨率减半、通道数翻倍右侧是解码器逐层上采样逐步恢复分辨率中间腰部是瓶颈层。最关键的设计是跳跃连接——编码器每一层输出都直接拼接到解码器对应层。跳跃连接的核心动机是信息互补。深层特征语义丰富但丢失了空间细节比如知道这里有草但叶片长什么样不清楚浅层特征分辨率高但语义粗糙比如看到一堆绿色纹理但不知道是草还是作物。拼接操作让两侧信息合并在一起解码器同时拥有“是什么”和“在哪”两套信息。没有跳跃连接的纯编码解码结构就是信息单向流动的漏斗分割边界会大幅度劣化这是UNET相对于朴素自编码器结构的最大优势。3.2 从零实现UNET的完整代码我使用PyTorch从零搭建了UNET不加载任何预训练权重。核心组件是DoubleConv模块和上采样模块。import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, kernel_size3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class Up(nn.Module): def __init__(self, in_ch, skip_ch, out_ch): super().__init__() self.up nn.ConvTranspose2d(in_ch, in_ch // 2, kernel_size2, stride2) self.conv DoubleConv(in_ch // 2 skip_ch, out_ch) def forward(self, x, skip): x self.up(x) # H可能差1像素做一次对齐裁剪 if x.size(2) ! skip.size(2) or x.size(3) ! skip.size(3): x nn.functional.interpolate(x, size(skip.size(2), skip.size(3)), modebilinear, align_cornersTrue) x torch.cat([x, skip], dim1) return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, num_classes3, base_ch64): super().__init__() self.enc1 DoubleConv(in_channels, base_ch) self.enc2 DoubleConv(base_ch, base_ch * 2) self.enc3 DoubleConv(base_ch * 2, base_ch * 4) self.enc4 DoubleConv(base_ch * 4, base_ch * 8) self.pool nn.MaxPool2d(2) self.bottleneck DoubleConv(base_ch * 8, base_ch * 16) self.up4 Up(base_ch * 16, base_ch * 8, base_ch * 8) self.up3 Up(base_ch * 8, base_ch * 4, base_ch * 4) self.up2 Up(base_ch * 4, base_ch * 2, base_ch * 2) self.up1 Up(base_ch * 2, base_ch, base_ch) self.out_conv nn.Conv2d(base_ch, num_classes, kernel_size1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(e4)) d4 self.up4(b, e4) d3 self.up3(d4, e3) d2 self.up2(d3, e2) d1 self.up1(d2, e1) return self.out_conv(d1)这段代码中值得注意的细节转角在三处。第一处是Up模块里做了尺寸对齐检查U-Net中下采样导致奇数尺寸时上采样后可能差1像素直接cat会报错加一个插值修正最稳。第二处是base_ch从64起步随着深度加深逐层翻倍这是UNET的经典通道设计因为浅层负责细节通道数太多浪费显存。第三处是输出层用1x1卷积将通道映射到类别数3没有接softmax因为PyTorch的交叉熵损失函数内部会做softmax。3.3 模型选型细节深度、通道数与显存的关系自建UNET有一个好处可以按显存自由裁剪深度。516512输入下完整五层UNET显存占用约6GB到8GB取决于batch size。如果只有一块8GB显卡建议base_ch从32起步或者把输入Patch缩小到384。在我的实际训练中base_ch64、patch512、batch_size8显存占用约7GB刚好卡在8GB的边缘再叠加数据增强的额外计算就容易OOM。所以最终我batch_size压到了4反向传播稳定。深度上四层结构去掉最深的bottleneck在农田任务中也能达到相近效果因为杂草和作物的外观尺度差异不会像医学影像中器官那么大过深反而导致小目标信息被过度压缩。4. 训练全过程损失函数、评估指标与调参记录4.1 BCEDice联合损失为什么不单独用交叉熵语义分割最常规的损失函数是交叉熵。但在农田分割这种场景下作物和杂草的像素占比通常不均衡背景像素占大头如果只用交叉熵模型会倾向于把所有像素都预测成背景来降低损失杂草区域根本学不出来。我用的方案是BCEDice联合损失。Dice损失衡量的是预测掩膜与真实掩膜的集合相似度公式为1减去两倍交集除以总和。它对类别不均衡不敏感即使杂草只占千分之一像素只要有部分预测对了梯度仍然存在。联合损失写成def dice_loss(pred, target, smooth1e-6): pred torch.softmax(pred, dim1) # shape: [B, C, H, W] target_onehot torch.nn.functional.one_hot(target, num_classes3).permute(0, 3, 1, 2).float() intersection (pred * target_onehot).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2.0 * intersection smooth) / (union smooth) return 1.0 - dice.mean()联合损失的计算我分成两条路交叉熵按常规方式对所有像素求平均Dice损失按类别逐类计算再取均值最后按各自权重相加。我用的是交叉熵权重0.6、Dice权重0.4。这样既保留了像素级精度约束又强行把“小目标召回率”拉起来。4.2 mIoU与像素准确率该怎么看训练过程最关键的两个指标是mIoU平均交并比和PA像素准确率。PA容易被背景主导比如一张图97%是土壤模型全预测为背景PA都有97%它会掩盖杂草识别能力的真实水平。所以我更看重mIoU特别是作物类别和杂草类别各自的IoU。IoU计算逻辑是预测为该类且真实为该类的像素数除以“预测为该类真实为该类”的并集像素数。杂草IoU低于0.5基本说明模型没有真正识别出杂草分布作物IoU至少达到0.75分割图才具备实际喷洒参考价值。我在训练中保存了每个epoch的mIoU日志目标精度锚定在整体mIoU大于0.82、杂草IoU大于0.65这个水平线下无人机喷洒作业才敢放心使用输出结果。指标数值是否达标背景IoU0.97达标作物IoU0.83达标杂草IoU0.66达标整体mIoU0.82达标像素准确率PA0.95达标4.3 实际训练日志解读学习率、批大小与收敛曲线我踩过最大的坑是无脑用默认学习率1e-3。UNET这类全卷积网络在随机初始化下梯度噪声极大1e-3很容易让Loss发散。我最终使用的参数组合参数数值备注优化器AdamW权重衰减1e-4初始学习率1e-4稳定优先批次大小4显存8GB训练轮数120早停设置在30轮无提升时学习率调度ReduceLROnPlateau当验证mIoU连续5轮不升学习率乘以0.5输入尺寸512x512固定不缩放实际收敛过程有几次比较典型的波动。前10个epoch Loss快速下降验证mIoU大约在0.5左右波动这是模型在学习“绿色区域和土地怎么分”。20到40轮作物类IoU开始超过0.7此时模型基本掌握了作物的行距规律。60轮后进入胶着期杂草IoU涨得极慢。我把学习率降到0.5倍再训练20轮杂草IoU突然突破了0.6。这说明小目标类别的训练后期需要更小的学习率来精细微调早停或者调度策略如果不管用建议先看学习率是不是太大了。5. 推理与落地大图预测、拼接与工程化部署5.1 滑窗切块推理与拼接策略训练时用512x512切块推理时理论上可以直接复用同样的切块逻辑但有两个关键区别第一推理时重叠率我会提高到100像素避免图块边缘的预测置信度偏低产生拼接缝隙第二预测结果要按原坐标贴回大图重叠区域取平均置信度而不是直接覆盖。拼接方案的流程图逻辑是这样的先把整幅无人机影像切成滑窗块序列逐块送入网络得到概率图每块概率按坐标累加到一个与原始大图同尺寸的累加矩阵同时维护一个计数矩阵记录每个像素被多少块覆盖过最终输出时用累加概率除以计数矩阵得到平均后的分割图。这种方法彻底消除了传统“硬边界拼贴”带来的接缝效应。经验补充滑窗推理速度很慢一亿像素的大图可能推理两三分钟。如果只是给变量喷洒作业用地图不需要追求实时每块地起飞前后等几分钟出图完全没关系。但如果是实时检测就得换方案了后面会讲加速。5.2 模型导出与推理加速训练完成后我习惯把PyTorch模型先转成ONNX格式再用TensorRT做FP16推理。4323层的UNET转换时主要注意两点一是转ONNX时固定输入尺寸为512x512因为转置卷积配合动态尺寸有时会出算子兼容问题二是TensorRT的FP16加速在农田推理中精度几乎没有下降分割掩膜由于阈值判定对FP16的误差不敏感。在嵌入式算力平台上我实测过转换后的推理速度原始PyTorch推理一张512x512图像约需120毫秒单张显卡转为TensorRT FP16后降到30毫秒至40毫秒提速3到4倍。如果是Jetson系列边缘设备建议再配合TensorRT的DLA深度学习加速器核心还能进一步降低延迟。5.3 与无人机植保作业流程的整合模型训练好以后实际作业链路是无人机起飞前规划航线按80%重叠率沿地块边界逐航线飞行采集降落后把存储卡里的影像拷出跑滑窗推理拼接生成整块地的杂草分布栅格图再把这个栅格图导入变量喷洒航线生成器根据杂草密度阈值设定喷洒开关和药量。整套流程里模型在整个链路中充当“眼睛”的角色输出的分割图直接决定后端的执行动作。整合环节有个容易忽略的问题航拍影像带有的地理位置信息必须在推理结束后与分割结果做像素到地理坐标的映射。我一般用影像的经纬度范围和像素尺寸计算每个分割像素对应的实际地面距离再按行号列号重采样到经纬度格网。这样生成的杂草分布图才能被喷洒设备直接使用而不只是一张好看的热力图。6. 实战中的坑与排查记录6.1 苗期杂草和作物长得太像换什么招最棘手的情况是作物刚出苗两片真叶的作物幼苗跟同期的杂草几乎长一个样颜色一致、纹理一致人眼都分不清。遇到这种情况分割模型的性能上限直接取决于训练数据里是否有足够“难分样本”。我从三个方向突破第一增加采样频率在苗期不同生育阶段多次航拍让模型见过作物从小到大的样子第二引入时序信息把同一地块前几天的预测结果当作辅助通道喂给模型利用杂草生长速度通常快于作物的特性辅助判断第三实在分不清的样本我选择在训练时把该区域标记为“未知”让模型不要强行输出错误信息。6.2 验证集指标波动与过拟合训练到80轮后我遇到过一次验证集mIoU突然下降的情况。排查后定位到原因是训练样本中包含了某个特殊天气下采集的回数——那批图整体色调偏暗数据增强的色阶扰动没能覆盖。解决方案是把这批样本单独划分到验证集而不进训练集重新训练后指标恢复稳定。这是一个教科书上不会专门提的教训农业数据采集天然带有时间批次差异按采集时间段划分数据集比完全随机划分更能反映模型的泛化能力。6.3 类别不平衡杂草占比太小训练不出来有一轮实验整片地块杂草覆盖率只有0.5%训练出来的模型杂草IoU仅为0.2几乎等于没识别。处理手段有两种并同时使用第一种是用插图采样Tile采样先统计每个训练图块的杂草像素占比把占比过低的图块下采样率调低保证每个batch里都至少包含几个杂草丰富的图块第二种是加权交叉熵按类别像素占比的倒数给不同类别分配损失权重迫使模型更关注小类别。两种手段叠加后杂草IoU从0.2提升到了0.58效果立竿见影。6.4 光照与阴影干扰大田作业绕不开一个问题上午10点以后的太阳角度会让高大作物在地面投下阴影阴影区域里的杂草细节被遮蔽模型的漏检率显著上升。我验证过几种对策最有效的是在训练数据中加入一半带阴影的样本让模型把阴影当作一种可跨越的干扰而不是类别特征其次是推理前做一个简单的亮度归一化把大图分块统计亮度均值按全局均值做亮度补偿。这些办法不能根治但能把杂草IoU稳定提升5个百分点左右。6.5 常见问题速查表现象可能原因处理方案Loss不下降学习率过大或过小将学习率调至1e-4起手训练集mIoU高、验证集低过拟合增加数据增强强度或按采集批次划分数据集杂草IoU极低样本占比太少插图采样平衡类别叠加类别加权损失分割边界毛糙输入分辨率太低将Patch从256提升到512拼接接缝明显推理重叠率过低重叠率提高到100像素并采用置信度平均显存溢出通道数或batch过大base_ch降到32或batch_size降到2模型全预测为背景数据里有大量空地或损失失衡过滤背景占比超95%的训练图块叠加Dice损失夜间或逆光图像效果差未见过的光照条件采集时加入不同光照样本推理前做亮度归一化最后再分享一个小技巧每次训练结束把验证集上所有预测错误的分割图单独导出成一张拼贴大图逐张翻一遍。你会发现很多指标上的小问题翻图比看曲线更容易定位原因。这个习惯我保留到现在凡是跑新数据集第一件事就是看错误样例图而不是看mIoU数字。我自己在这套项目里最大的体会是农田分割这类任务真正决定成败的往往不是网络结构有多先进而是数据采集是否贴合实际作业场景、标注标准是否前后一致、推理结果能否被下游设备无缝消费。与其反复追新模型不如先把UNET这条链路跑扎实在足够贴近真实应用的数据上打磨细节产出的效果比单纯换大模型实在得多。后续你想把这个能力扩展到大田作业里的其他地块类型或者接入实时机载推理这套方案的扩展点都已经预留好了。