
无人机拍回来的农田影像堆满了硬盘真正让人熬夜的不是飞行而是怎么把每一棵杂草从作物中间抠出来。最近我完整复盘了一个基于深度学习UNET算法的农田图像分割项目用自建的UNET网络实现作物与杂草的像素级分类顺带把杂草分割检测一并解决了。这个项目的数据集和代码都是自己整理的整条链路从影像采集、数据标注、网络搭建到训练调参、大图推理拼接走了一遍整个过程花了大概三周踩了不少坑也沉淀出一些能直接用起来的方法。如果你正准备入门语义分割想找一个比街景、医疗影像更有落地感的练手方向或者你在做农业AI、植保无人机相关产品需要一个能跑通的基线方案——这篇复盘应该都能帮上忙。1. 项目整体设计目标、路线和模型选型1.1 先想清楚要什么分类、检测还是分割在动手写代码之前我花了一整晚想一个问题这个项目到底要输出什么很多朋友听到“杂草识别”第一反应就是上检测模型在图像上画框标出每株杂草的位置。但农田场景对检测框非常不友好——杂草不是独立物体经常是一丛一丛贴着作物长框会同时框进作物叶片和裸露土壤后续根本无法指导机械除草或变量喷洒。如果你只需要知道“这块田有没有草”那图像分类就够了但分类对植保作业没有操作价值它说不清楚草在哪、密度多大、面积占比多少。真正有用的是像素级图像分割。每个像素都被赋予一个类别——背景、作物或杂草。这样杂草的位置、面积、密度就能精确定量下游无论做精准喷雾还是机械除草都有据可依。回头看我当初的需求本质上不是“识别出杂草”而是“告诉机器除草剂该喷哪里”所以任务形态从一开始就应该选分割而不是检测或分类。这个决定直接影响后面所有数据标注方式和网络结构选型提前想清楚能省掉至少一周的返工时间。1.2 UNET为什么是我最终的选择语义分割的模型池子不小FCN是开山祖师DeepLab系列在街景分割上表现强势SegNet也常用于自动驾驶场景。但在无人机农田影像这个特定场景里UNET是性价比最高的选择原因有三条。第一小数据集友好。农业图像标注非常贵一个熟练标注员一天能标几十张512×512的图已经很不错。UNET的编码器-解码器结构加上跳跃连接天然适合从有限样本里学习。它的空域先验比纯Transformer类模型强得多不需要海量预训练数据就能达到可用精度。第二跳跃连接对细小目标和边界非常关键。杂草叶片细长边缘复杂经过五次下采样之后在深层特征图里基本糊成一团。UNET把编码器每一层的空间信息直接接到解码器对应层浅层的高分辨率特征能帮上大忙最后分割出来的草丛轮廓明显更干净。这一点在做对比实验时看得特别清楚——去掉跳跃连接后杂草IoU直接掉了将近10个点。第三实现简单适合自建。UNET没有诡异的模块就是一个编码器加解码器加跳跃连接的骨架自己手写一遍forward不到60行代码。调试也方便想改通道数、换空洞卷积、加注意力模块都很顺手。对比SegNet它虽然也有编码器-解码器结构但下采样的位置信息传递不如UNET直接对细长物体不占优势FCN的上采样方式又太粗糙训练出来的mask边缘锯齿感很强不适合直接做精细化除草。1.3 自建网络和套网库的权衡网上有现成的分割工具库两行命令就能加载预训练UNET但这次我坚持把网络自己搭了一遍。原因不是“手写更高级”而是三个很实际的问题。一是可控性。项目代码要给同事复现如果依赖某个第三方封装库版本一更新接口就变到时候光是debug环境就要耗掉半天。自己写UNET所有依赖就是PyTorch和TorchVision部署到哪都不会因为库版本问题出岔子。二是可改性。农田场景里杂草尺度变化很大有的刚出苗只有几个像素宽有的长到十几公分高还带分枝。我需要在网络底层换更大的感受野自建网络直接把底层几个普通卷积替换成空洞卷积堆叠前后对比很直观。如果用现成网库改这些结构会非常费劲甚至得绕开高层API去改底层的forward逻辑。三是理解深度。这句话听起来有点虚但做工程的人都知道用别人封装好的模型出了bug只能看文档猜。自己写的网络梯度爆了、激活死掉、拼接尺寸对不上都能直接定位到代码行。这个项目里我正是靠这个能力快速找到了几个影响精度的关键问题后面会详细展开。当然如果你只是想做快速验证、完全不在意底层实现用网库没有问题但如果你想把这个项目作为模块持续迭代自建是更值得走的路。2. 数据没你想的那么简单无人机农田影像的采集、标注与扩增2.1 飞行参数和影像质量决定模型上限这个项目的影像来自某植保团队用多旋翼无人机拍的试验田拍摄高度大概在5到8米地面分辨率约1到2厘米/像素。这个分辨率下作物叶片的脉络纹理、杂草的形状细节都能清楚看到。但分辨率高也带来一个实际问题——原图尺寸非常大单张动辄数千万甚至上亿像素直接扔进网络训练是不可能的必须要做切块预处理。采集环节有几个关键参数直接决定模型能用多久。第一拍摄高度必须和最终使用场景一致。训练用的是5米高度实际作业时飞到30米图像里看到的纹理完全不同模型泛化会断崖式下跌。第二尽量选择光照均匀的时间段拍摄。正午强光底下阴影会把杂草压得很暗标注员都分不清边界模型更学不会。我一般推荐上午9点到11点、下午3点到5点两个时段。第三同一个地块要覆盖不同的作物生长阶段。草刚出苗和长到十几公分高外观差异非常大如果只标注了一个生长时期模型换一个时期基本就是瞎的。数据采集阶段犯的懒后面训练阶段都会加倍还回来。2.2 统一标注规范三重边界怎么定图像分割数据集最核心的其实不是数量而是标注规范和标注一致性。我这次用的是开源的LabelMe工具做多边形标注把每一个像素划分为三类背景土壤、地膜、杂物、作物目标农作物、杂草所有非作物的草本植物。选LabelMe的理由是JSON格式方便后处理界面简单几个标注员上手都很快。团队里也有用其他标注工具的看个人习惯关键是要能导出统一的格式。标注规范层面我踩过一个大坑杂草类目标的定义太模糊。有些杂草的幼苗和作物小苗在视觉上非常接近标注员之间经常产生分歧甚至需要拿着成株植物图鉴去对比。最后我定了一套硬性规则每条都写进标注文档里根系明显长在非种植行上的一律标为杂草作物种植行内混生的细长禾本科草同样标为杂草枯叶、残株如果遮挡严重归入背景边界上存在争议的像素优先归入作物——误伤作物的代价比漏掉一点草边更大。这套规则后来直接决定了模型的行为宁可漏掉边界上一小圈草也绝对不把作物当杂草切掉。在最终的验收测试里这条规则帮我们躲过了好几个会被客户直接否掉的错误案例。没有统一标注规范的话训练数据里出现互相矛盾的标签模型会学得非常挣扎。2.3 滑窗切块与数据增强涨点最明显的一步原图太大训练前必须要切块。我用滑窗裁剪的方式固定窗口512×512步长256也就是50%重叠的滑动采样策略。为什么用重叠而不是不重叠因为农田目标在图像上分布不均如果步长正好等于窗口大小会漏掉很多跨边界的结构重叠采样相当于做了一次位置级别的增强。切完块之后还要做过滤——杂草和作物像素占比低于总像素1%的图块直接丢掉避免大量纯背景样本拖慢训练节奏。数据增强是提升泛化能力性价比最高的一步我按重要性排一个顺序随机旋转90度倍数和水平/垂直翻转这是最基础的几何增广。注意不能做任意角度的旋转因为农田垄行结构有方向性随便旋转反而会引入错误先验颜色抖动亮度、对比度、饱和度各自±20%用来模拟不同光照条件HSV色相小幅偏移幅度不能太大否则会把绿叶变得不像植物局部裁剪加缩放模拟无人机飞行高度微调时的尺度变化。整套流程下来原始4330张512×512的切块在在线增强后相当于每轮训练都能看到2万多张新图。效果立竿见影——验证集mIoU从0.69直接升到0.85附近。这个提升幅度超过我在模型结构上做的任何改动也再次印证了一个老结论对中小规模数据集先把数据管好再谈调模型。3. 自建UNET实现与训练调参全记录3.1 网络结构设计编码器、解码器与跳跃连接自建UNET以原始论文结构为主干我做了一些面向无人机影像的调整下面把结构逐层说清楚方便直接照着搭。编码器采用4个stage每个stage先做两个3×3卷积加ReLU再接2×2最大池化下采样。通道数从浅到深是64→128→256→512。池化会让特征图尺寸减半所以最后得到的瓶颈层特征只有32×32分辨率。在瓶颈位置我加了一层空洞卷积空洞率设为2和4用来扩大感受野又不额外丢失分辨率——这对杂草这种小目标很关键因为杂草叶片在32×32特征图上可能只有一两个像素宽。解码器侧每个stage先做2倍上采样。这里我用双线性插值加3×3卷积做refine而不是直接转置卷积因为转置卷积容易引入棋盘状噪声。上采样之后把对应编码器的特征拿过来沿着通道维度拼接拼接后连续两个3×3卷积融合特征。最后用1×1卷积把通道数降到3接Softmax得到背景、作物、杂草三类概率。整个网络参数量约2800万输入输出都是512×512×3到512×512×3单张12G显存显卡跑batch16完全没问题。如果显存紧张可以把编码器第3、4层的通道数从256/512砍半精度损失很小但显存能省下约30%。后续要部署到机载嵌入式平台的话我建议直接用更小的32-64-128-256四层版本。跳跃连接有两点需要注意拼接前的编码器特征最好过一层BatchNorm对齐分布否则两边的数据量纲不一致会影响训练稳定性另外如果显存不够除了砍通道也可以把跳跃连接改成1×1卷积先降维再拼接能省不少显存。3.2 损失函数与评价指标不是所有loss都适合杂草训练用了组合损失CrossEntropyLoss加DiceLoss按比例6:4混合。为什么这么配分类交叉熵逐像素独立计算梯度信息丰富收敛比较稳定Dice损失直接优化类别区域的交并比对类别不平衡天然不敏感。农田数据里杂草面积可能只占整张图的5%到15%单独用交叉熵的话模型把杂草全预测成背景loss也并不会涨多少加上Dice之后每一块杂草区域的误差都会被明显放大模型不得不去追那些小块的真实区域。类别权重也不能省。我在交叉熵里给杂草类别手动加了2倍权重背景设0.6作物设1.0进一步压制模型偷懒的倾向。我也试过纯Focal Loss效果并没有比CEDice好调参还麻烦不建议新手一上来就用Focal。评价指标重点看mIoU也就是所有类别IoU的平均值。IoU的计算公式是TP除以TP加FP加FN简单理解就是预测掩膜和真实掩膜的并集里交集占多大比例。Pixel Accuracy这个指标在农田场景里没有意义——背景通常占七成以上全猜成背景也能有70%的准确率什么都说明不了。除了mIoU我还会单独盯杂草类别的IoU和Recall这才是整个项目的核心。杂草漏检意味着这株草不会被喷洒到后面会长起来和作物抢养分而误检多喷一点除草剂损失相对可控。所以落地时我对杂草Recall的要求非常严格哪怕杂草IoU不是三类里最高的Recall也必须拉到90%以上。3.3 超参数配置与训练过程实录直接放一套可以复现的训练配置这是我项目里最终跑通的方案优化器AdamWlearning rate 1e-4weight decay 1e-4学习率调度CosineAnnealingLRT_max 50Batch size16单张12G显存GPUEpoch60前5轮线性warmup从1e-6升到1e-4训练/验证划分8:2按地块随机分验证集的地块不参与训练用来测泛化能力混合精度开启AMP训练显存占用下降约40%速度提升明显但要注意grad scaler是否正常工作训练过程我记录了关键节点大致是这样变化的。第1轮结束训练loss约0.91验证mIoU只有0.22基本等于乱猜。第10轮训练loss降到0.66验证mIoU升到0.61模型已经能把大块的背景和作物区分开但杂草区域还是和背景混在一起。第25轮训练loss约0.51验证mIoU到0.78杂草IoU开始冒头到0.51模型已经勉强勾出杂草区域的轮廓了。第45轮验证mIoU到达0.86杂草IoU到0.72验证loss基本平稳。第55轮以后指标不再明显变化我在第58轮做了早停。最终在测试集上的结果指标数值mIoU0.862作物IoU0.913杂草IoU0.754像素准确率0.927杂草Recall0.906这个结果对一套完全自建网络加自制数据来说已经能拿去和客户谈需求了。调参过程中的心得有三条初始学习率试过3e-4模型涨到0.85之后开始震荡降到1e-4才稳住warmup真的有用数据量不大的时候能避免早期训练把特征搞炸验证集loss如果开始反弹不要急着加数据先把学习率减半观察两个epoch很多时候只是学习率过大导致的震荡。4. 从训练到落地推理拼接、常见问题与进阶技巧4.1 大图推理拼接窗口步长与重叠区域怎么选模型训练好之后另一个现实问题来了实际无人机拍的原图是6000×4000这种级别没法直接像512×512那样喂给网络。我的做法是推理阶段也走滑窗窗口仍然用512×512但步长设置成384也就是相邻窗口有128像素的重叠。重叠区域的预测概率取两次预测的平均值这样能削平窗口边缘产生的接缝噪声整个mask看起来更自然。推理阶段要注意两个容易忽略的细节。第一必须设置model.eval()并关闭梯度用torch.no_grad()包裹。BatchNorm和Dropout在train和eval两种模式下行为完全不同我遇到过不止一次因为忘记切换eval导致指标忽高忽低排查半天才发现是模式没切。第二如果批量处理几十张大图建议用多进程把图像切成瓦片、分片推理再拼接比一张张串行跑能节省非常多时间。用一张12G显存的显卡单张6000×4000原图在滑窗推理下大约需要30到50秒批量场景下完全够用。如果你还要进一步提升速度下一步可以做ONNX导出再用TensorRT做半精度推理配合小通道版本UNET速度能提升到接近实时。大图推理拼接这块优化的空间很大值得提前规划。4.2 实战高频问题排查速查表整个项目推进过程中踩过不少坑也帮身边朋友排查过类似问题我整理成了下面的速查表遇到症状直接对着找原因现象可能原因排查手段解决方案训练loss不下降学习率过大或标签错位可视化一张训练输入和对应mask降低lr到1e-5检查DataLoader的索引是否错位验证mIoU远低于训练集过拟合或验证集分布差异对比两边图像的亮度、地块差异加强颜色抖动、添加Dropout、确保验证集独立杂草几乎全漏检类别权重太低或样本太少单独统计杂草类别的Recall把交叉熵的杂草权重从1提高到2到3对杂草区域做CutMix增强预测mask边缘颗粒感重上采样方式太粗糙直接观察网络输出把转置卷积替换为双线性插值或加CRF后处理训练时显存OOM输入太大或batch太大查看GPU显存占用曲线开启AMP混合精度、减小batch、编码器通道减半换一个地块效果骤降拍摄条件差异大检查两张图的色调直方图做风格增强或多个地块数据按比例混合训练推理速度太慢模型参数量大、计算图冗余单独测单次推理耗时导出ONNX、半精度推理、用小版本模型分割结果忽好忽坏推理窗口重叠策略不对或没切eval看预测置信度图重叠区域取平均置信度、确认eval模式这表里最常被忽视的是最后一行分割结果忽好忽坏尤其在批量推理时非常迷惑人。后来定位到的原因大部分都是窗口拼接策略不一致或者模型还在train模式开关一开一关指标就差了好几个点。4.3 提升杂草分割精度的三个进阶技巧先讲伪标签迭代。农业数据的标注成本极高但我手头有大量完全没标注的航拍图。做法很简单用已经训练好的模型去预测这些无标注图取预测置信度高的区域——比如Softmax概率超过0.95的像素——直接当作伪标签加入训练集。这一步能再把杂草类别的表现往上推一推mIoU能到0.88左右。但一定要设置置信度阈值否则模型会把自身错误当成标准答案反馈回来越练越偏。然后是测试时增强TTA。推理时把图像水平翻转、垂直翻转各预测一次再把三次结果取平均。简单、有效单图耗时增加两倍但mIoU通常能上涨1到2个百分点。我一般只在客户演示和提交报告时开TTA批量部署时关掉灵活切换就好。最后分享一个常用的两阶段训练策略先用背景和植物二分类把问题简化让网络学习“绿色像素与土地像素的边界”这个通用能力然后冻结编码器只微调解码器让它细分作物和杂草。这个思路尤其适合杂草小目标因为网络先学会了分割植物的能力再学叶片纹理差异比直接端到端训练更稳。最后配合形态学后处理——先做开运算去掉零散噪点再做闭运算填充内部空洞视觉效果会干净很多。我个人实际操作中的最大体感是这个项目的成败模型结构只占很小一部分。数据规范性、标注口径、增广策略和训练细节叠加起来的作用远比换一个更大更花哨的网络明显。如果你手上也正好有无人机影像想用UNET做农田分割最该花时间的地方是先把自己那批图像的标注口径整理干净。数据整理扎实了网络这边按上面的配置基本能一次跑通。后续如果还想深挖可以继续沿着伪标签迭代和轻量化部署两条线走每一步的收益都是看得见摸得着的。