做粮食加工这行的人应该都有感受:小麦从田间收上来,看着干干净净,但没过清理筛、去石机、磁选机之前,里面的杂质种类多到让人头疼。石子、秸秆、麦壳、破碎粒、虫蚀粒、霉变粒、异种粮粒,形态各异、大小不一,有些颗粒甚至比一颗麦粒还小。人工挑选费时费力,不同操作工的判断标准也不一致,漏检率很难压下去。我这两年一直在折腾深度学习目标检测在这个场景下的落地,把YOLOv11的主干网络和BiFPN特征融合结构接到一起,专门对付小麦杂质在线检测,效果比之前用的基础YOLO模型明显上了一个台阶。这篇内容不是什么玄学调参,都是我在实际项目里跑过、踩过坑、最后沉淀下来的方案。包括数据怎么做标注、模型结构怎么改、训练参数怎么定、上线部署会遇到哪些问题,这些都会拆开讲清楚。适合正在做农产品质检、粮食物流、食品加工视觉检测的朋友,尤其是对目标检测有一定基础、又想把小目标检测做好的人。看完之后,你至少可以按照这个思路快速搭起一套自己的小麦杂质检测方案。1. 项目背景与整体设计思路1.1 小麦杂质检测到底难在哪先说清楚我为什么要做这个项目。粮食加工企业在入磨前要对小麦进行杂质含量检测,传统方式是人工抽检。人工抽检的痛点很明显:效率低、主观性强、无法做到全量在线检测。更麻烦的是,杂质是小概率事件,一小堆麦粒里可能只有几颗石子,人工盯着传送带看,十分钟后注意力就下降了,漏检率其实相当高。用机器视觉来替代人工,第一个要解决的难题就是小目标。假设工业相机的视野是一米宽,图像分辨率做到1920x1080,一颗直径3毫米的石子,在图像里大概也就几个像素到几十个像素。这种目标在目标检测领域属于典型的small object,特征非常微弱,很容易被下采样过程直接丢弃。第二个难题是类别多样性。小麦杂质不是一个均匀的类别,而是“一堆杂物的统称”。石子边缘锐利、颜色偏灰;秸秆细长、有纤维纹理;麦壳薄、半透明、颜色与麦粒非常接近;霉变粒表面有暗斑;虫蚀粒形态不规则。这些类别之间差异很大,强行当成一个“杂质”类别来检测,会导致类内方差过高,模型很难收敛。第三个难题是类别不平衡。在一批合格原粮里,杂质占比通常很低,大部分区域是正常麦粒。这意味着训练数据里正样本(麦粒)和负样本(杂质)的比例很可能达到几百比一。如果直接端到端训练,模型很容易收敛到“全部预测为背景”的平庸解。这三个难点,恰好对应目标检测模型的两个关键能力:多尺度特征融合能力,以及困难样本挖掘能力。这就是我选择YOLOv11加BiFPN组合的原因。1.2 为什么基座选YOLOv11YOLO系列在工业视觉检测领域几乎是默认选项,原因很简单:推理速度快、训练生态完善、从数据标注到模型部署的全链路工具链都很成熟。在YOLOv11之前,我做过YOLOv5的长期运维,也试过YOLOv8,但最终把基座定在YOLOv11,是看重了它在目标检测性能上的几个针对性改动。首先是主干网络部分,YOLOv11引入了C3k2模块,这一系列基础模块组合让网络在提取局部纹理特征时更加高效。后来又加入类似C2PSA的注意力结构,让网络能够自适应地把计算资源集中在特征最明显的区域。对于小麦杂质检测来说,这意味着模型更容易注意到那些面积很小、但局部特征异常的目标,比如混在大量麦粒中的一粒石子。其次是正负样本分配策略。YOLOv11保留了anchor-free设计,同时改进了训练过程中的样本匹配方式,对不同尺度目标的正样本分配更均衡。我实测下来的感受是,即使不做特殊调优,YOLOv11在同等输入尺寸下的小目标召回率也要高于之前的几个版本。当然,YOLO系列有n、s、m、l、x多种规格,我在项目初期用n规格把整个pipeline跑通,等数据质量稳定后,再切到s规格提升精度。这个策略我很推荐,不要在算法没定型前就上大模型,不然训练一次就要烧掉大量GPU时间,迭代成本太高。1.3 为什么还要引入BiFPN,默认Neck不够用吗YOLOv11默认的neck结构是类似PANet的设计,自顶向下传递语义信息,自底向上传递空间细节。这个结构本身不差,但它有个问题:不同尺度的特征在融合时,权重是默认等价的。换句话说,来自深层特征图的语义信息和来自浅层特征图的细节信息,在相加或拼接时被一视同仁地对待。在杂质检测场景里,这种“一视同仁”是有问题的。一粒石子很小,它在浅层特征图上的响应很活跃,但到了深层特征图,经过多层卷积和下采样,这个响应会被严重稀释。而判断这粒石子是不是杂质,又需要足够强的语义信息。特征融合时如果不知道该着重听谁的,网络就容易在“看得见”和“认得出”之间失去平衡。BiFPN的解决思路很直接:给每条融合路径加上可学习的权重。它的完整名字叫加权双向特征金字塔网络,最早是在目标检测框架EfficientDet中提出的。核心有三点:自顶向下和自底向上两条路径反复融合,信息在多尺度特征之间双向流动;每条输入特征带一个可学习的标量权重,网络在训练过程中自己学会信任哪个尺度;删掉只有单条输入边的节点,减少冗余计算。落到小麦杂质检测任务上,我最大的体会是:加了BiFPN之后,模型对“小而不明显”的杂质的召回能力提升明显,尤其是麦壳和半透明秸秆这类,浅层纹理和深层语义需要动态配比的特征。用个生活化类比:默认PANet像是一个会议里每个人发言音量一样大,但真正有信息量的人可能只有一两个;BiFPN相当于给每个发言人配了一个麦克风音量旋钮,网络在训练中自己学会把有效信息放大、把干扰信息压低。这个特性对信噪比低的小目标检测场景来说,价值非常大。2. 核心细节解析与实操要点2.1 数据采集与标注规范任何视觉检测项目,数据是地基。小麦杂质检测的数据采集,我的建议是别只在实验室里拍样本,一定要去真实的产线环境取图。传送带震动、光照变化、物料堆积厚度、麦粒粉尘附着,这些在实际场景里都会改变图像分布。在硬件层面,我用的方案是工业面阵相机加恒定光源。分辨率不低于1920x1080,帧率根据产线速度选择,通常在15到30fps之间。相机安装在传送带正上方,视场宽度控制在麦料流宽度附近,避免拍到大量无关背景。打光方式用过环形光源和同轴光源,环形光对麦粒这种半球形表面的物体更友好,阴影更少。标注规范是这类项目里最容易翻车的地方。我的标注分类是这样的:石子、秸秆、麦壳、破碎粒、虫蚀粒、霉变粒、异种粮粒。一开始我把“杂质”统一标成一个类,后来发现模型精度怎么调都上不去,因为石子这类刚性目标的特征和霉变粒这类表面渐变特征差异太大,模型在一个类里要学的东西太多,互相干扰。拆成多个类别后,每个类别的特征更聚焦,整体检测效果反而更好。具体标注规则有几条,直接写在这里供参考:目标在图像中至少要有3x3像素以上,否则不标;目标被遮挡面积超过70%时不标;边界模糊、肉眼都无法确认的类型,放到后台由项目负责人复核,不勉强归入某一类;每个类别的框尽量紧贴目标边缘,不要留白太多;全部样本由至少两个人交叉标注,用标注一致性指标做校验。这里有个心得,很多人容易忽略:要把“完全干净的麦堆”图像也加进训练集,并且不做任何标注。这些图像是负样本,能教会模型“什么情况下应该输出空结果”,对降低误检率的作用非常大。这点我在后面的误检问题里还会再提到。2.2 数据增强策略数据增强在小目标检测中的作用,比很多人想象中大得多。默认的训练配置里一般会带Mosaic增强,就是把四张图随机拼成一张,这样既增加了样本多样性,又提高了单位图像里小目标的密度。Mosaic增强对杂质检测特别有用,因为杂质本身稀疏分布,一张真实原图里往往只有几个目标,模型学不到密集目标的分布特征。但Mosaic也有副作用。训练后期如果一直用Mosaic,模型会慢慢适应“图像由四块不同区域拼接而成”这种人工痕迹,真实场景里的连续背景反而让模型不适应。我在训练时会设置一个close_mosaic的轮数,让最后十来个epoch自动关闭Mosaic,回到真实的完整图像分布上微调,验证集的mAP通常会有小幅回升。针对杂质数据,我强烈建议加一个Copy-Paste增强。具体做法很简单:从训练集中裁剪出已经标注好的杂质目标,把它的掩膜区域粘贴到另一张“干净麦堆”图像上,同时做随机缩放、旋转、光照扰动,生成大量合成困难样本。这个增强对缓解虫蚀粒、霉变粒这类样本不足的问题,非常有效。粘贴时要注意边缘羽化,不然模型学会靠“生硬的矩形边缘”识别杂质,一换真实场景就失效。其他基础增强我一般这样设置:HSV扰动开启,色相变化在0.015左右,明度饱和度在0.4左右;随机翻转开启,随机旋转90度开启;Cutout按0.3概率开启。这些参数不是固定不变的,但基本覆盖了产线上光照波动的主要情况。2.3 模型结构修改细节把BiFPN融合进YOLOv11,我是这样处理的。YOLOv11的检测器由三部分组成:backbone用来提取特征,neck用来融合多尺度特征,head用来输出分类和边框。我需要动的部分主要是neck。整个检测器在结构定义上一般会用一个yml格式的模型配置文件来描述。我把默认neck部分拿掉,改成一个带权重自学习的双向融合结构。大致拓扑是这样的:backbone输出的最后三个尺度的特征图,分别记为P3、P4、P5;P5先做自顶向下融合,上采样后与P4融合,再上采样与P3融合;P3再做自底向上融合,下采样后与P4融合,再下采样与P5融合;每个融合节点处,不是简单的相加操作,而是先对每条输入特征分别做1x1卷积统一通道数,乘上各自的软权重,再求和;权重计算采用快速归一化融合方式,输出等于每个输入乘以对应权重的加权和,再除以所有权重加总,防止权重无界增长。这个过程用公式写出来,每个融合节点的输出是:O = Σ( wi * Ii ) / ( Σ(wi) + ε )其中ε是一个非常小的常数,比如0.0001,用来避免除零。所有wi初始化为1,训练过程中由网络自动学习。这个公式看起来简单,但实际作用非常明显,本质上是对多尺度特征做了一个可学习的软选择。具体实现层面,当时是在一个二开过的ultralytics风格代码库里改的。如果不想自己从头写,也有现成方案,把BiFPN当成一个自定义模块注册进模型定义文件里,替换neck段即可。改完之后,head部分不用动,输出层的尺寸和数量保持一致,训练脚本、评估脚本、导出脚本全部可以沿用,这是我最看重的一点——改动成本尽量低,系统其他部分不用跟着翻车。2.4 损失函数与正负样本分配模型结构改完之后,损失函数也要稍微做一些适配。YOLOv11默认把分类损失和边框回归损失加在一起,分类用BCE,回归用CIoU加DFL。在杂质检测场景下,我保留了这套默认组合,但对正负样本分配做了重点检查。训练时模型会计算每个目标候选框与真实框的匹配度,把匹配上的作为正样本,其余的作为负样本。YOLOv11使用的task-aligned assigner会把分类得分和IoU联合起来分配样本,目标是一个非常小的石子,如果分类响应不够强,很可能被当成负样本丢弃。针对这一点,我会在训练配置里稍微调低正样本匹配的阈值,让更多“边缘候选”参与训练。效果是召回率提升了,但代价是训练时间变长,需要自己平衡。还有一个技巧:如果小目标漏检特别严重,可以考虑在neck输出里增加P2或P4尺度的高分辨率特征层参与检测。P2层的步长是4,相当于640输入下输出160x160的特征图,对小目标的响应会更敏感。代价是计算量增加,显存占用变大,推理变慢。我的建议是后续项目先记录当前模型指标,再决定要不要加。3. 实操过程与核心环节实现3.1 环境准备先交代一下我这边的运行环境,供参考。深度学习框架这块,用的是Python 3.10加PyTorch 2.x版本,GPU用的是一块24G显存的单卡。安装命令比较常规,把这几个包装好即可:pip install torch torchvision pip install ultralytics版本选择上有一点要注意:GPU驱动的CUDA版本要和PyTorch的编译版本匹配,我一开始没检查,装了不匹配的版本,跑训练时直接报CUDA initialization error,排查了半小时才发现是版本问题。显存方面,如果用的是12G的卡,训练s规格模型加640输入加16的batch是勉强可以跑的,n规格更没问题。如果显存不够,优先缩小batch,别先降低输入分辨率,那样对小目标检测的影响会更大。3.2 数据集划分与目录组织数据集的组织方式,我沿用目标检测任务常见的目录结构,可以参考这个版本:wheat_impurity/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── dataset.yamldataset.yaml里面写清楚类别名称和图片路径。图片与标注文件一一对应,文件名保持一致,只有后缀不同。划分比例我用的是70%训练、15%验证、15%测试。但要特别提醒一点:如果这些图片是从同一个视频流里连续截取的,直接随机划分会导致训练集和验证集里出现“同一场景的连续帧”,验证结果会虚高。正确的做法是按时间段或按批次划分,比如前三天采集的数据做训练,后面一天的数据做验证和测试,这样更接近真实上线后遇到的情况。3.3 训练关键参数与调参路线训练参数的设置是我在这类项目里花时间最多的部分,直接给出一份我用下来效果比较稳定的配置:参数取值说明imgsz640训练输入尺寸batch16视显存调整epochs120观察损失与验证指标收敛情况optimizerSGD动量设为0.937,权重衰减5e-4lr00.01按batch做线性缩放warmup_epochs3前3轮预热学习率mosaic1.0开启Mosaic增强close_mosaic10最后10轮关闭Mosaicmixup0.1小概率MixUp增强copy_paste0.3拼接粘贴增强概率hsv_h0.015HSV色相扰动幅度hsv_s0.4饱和度扰动幅度hsv_v0.4明度扰动幅度fliplr0.5水平翻转概率训练启动命令大致长这样:yolo detect train \ data=dataset.yaml \ model=yolov11n.pt \ imgsz=640 \ batch=16 \ epochs=120 \ lr0=0.01 \ mosaic=1.0 \ close_mosaic=10如果是用自定义了BiFPN结构的新模型文件,把model参数指向自己写的yml配置文件即可,其他流程完全一致。关于学习率,我多说一句。SGD在目标检测上的稳定性比AdamW好,尤其是当数据量不够大的时候,AdamW很容易跑出“一开始损失降得快、后期精度上不去”的情况。学习率0.01对应的batch是16,如果改到batch为32,最好学习率也等比放大到0.02,否则有效更新步长变小,训练会变慢。调参路径我的建议是:先拿n规格加默认配置跑一个baseline,记录mAP50、mAP50-95和召回率。然后在这个基础上,一次只改一个变量,比如先只加BiFPN,看涨不涨点;再改输入尺寸到960,看有没有进一步提升;最后再调整数据增强参数。不要同时改结构和超参数,不然出了问题根本定位不到是哪里引入的。3.4 模型评估与问题定位评估指标不能只看mAP一个数。在杂质检测场景里,我重点关注四个指标:precision、recall、mAP50、mAP50-95。precision衡量的是模型“框出来的东西里面有多少是真正杂质”,recall衡量的是“真正的杂质里有多少被框出来了”。在产线质检项目里,recall的权重通常比precision高,因为漏检一个石子进入下一道工序,可能直接造成设备损坏或者成品质量事故。如果多类别模型跑完之后,单独看每个类别的mAP,会发现差异很大。通常石子和秸秆这类外形特征明显的类别精度较高,霉变粒和破碎粒这类特征不稳定的类别精度偏低。这时候不要盲目调整整体模型,应该单独检查数据量最少的类别,看看是不是标注数量太少,或者类别间特征有重叠。模型收敛后,把可视化预测结果画到一批随机验证图上,人眼检查是非常必要的。画框结果能直接暴露两个问题:一是小目标区域密集的石子可能只被召回一部分,二是麦粒之间的空隙容易被误判为杂质。这两个现象单靠指标数值很难看出来,但人眼扫一眼图片就清楚了。3.5 模型导出与部署训练完成后,要把PyTorch模型导出成推理引擎。我的标准导出链路是先用ONNX作为中间格式,再转到TensorRT的engine文件。命令大致如下:yolo export model=best.pt format=engine half=True dynamic=True导出时打开half模式,模型权重转成FP16,显存占用和推理延迟都会明显下降。在24G显存的卡上,s规格模型加640输入,FP16下的推理延迟大概在10毫秒到20毫秒之间,放到产线实时检测是完全够用的。实际部署时还有几个工程细节要处理。工业相机通过网口或者USB口输出图像流到工控机,程序逐帧读图,做归一化预处理,送入推理引擎,得到检测框,再做NMS后处理,最后把结果叠加回原图。这个过程要控制总延迟,在工控机上我会用双线程模型,一个线程专管相机取流和图像预处理,另一个线程做模型推理和后处理,两组操作通过队列解耦,避免取帧等待阻塞推理。4. 常见问题与排查技巧实录4.1 小目标漏检严重怎么办这是我在项目里被问得最多的问题。如果你的模型在验证集上对大块杂质检测正常,但对石子这类小目标频繁漏检,首选检查的不是模型结构,而是输入分辨率。640的输入对小麦这类小而密集的物料来说,目标占比太低。把输入分辨率提到960或者1280,通常recall会有肉眼可见的提升。代价是训练时间和推理时间变长,需要自己权衡。如果提升输入分辨率后仍然漏检,做一个简单实验:把测试集里的小目标区域单独裁剪放大,看模型能不能识别。如果能识别,说明模型知道“放大后的杂质长什么样”,但不知道“缩小后的杂质长什么样”,这时应该重点加强小目标样本在训练数据中的占比,可以用Copy-Paste增强专门生成大量小尺寸杂质样本。还有一类场景是输入分辨率不能无限提高,因为产线需要处理速度。这时候可以走推理端切图方案:把一帧大图切成若干张640的patch,每张patch做推理后,把检测框映射回原图坐标,再做一轮全局NMS。这个方案在工程上稍微复杂一些,但对小目标检测的提升非常直接。切图时要注意相邻patch之间保留10%到20%的重叠,否则正正好在边界上的目标容易被切掉一半。4.2 误检偏多,把麦粒或设备纹理当成了杂质误检多的原因通常不是模型结构问题,而是数据问题。我踩过最大的坑是:训练集里所有图像都是“含杂质的麦堆”,模型从来没有见过“没有杂质的干净麦堆”,导致它把一切看起来略微不统一的纹理都当成潜在的杂质。解决方法也简单:专门采集一批完全干净、无任何杂质的麦堆图像加入训练集,图像里没有任何标注框。让模型学习到一类“空输出”的负样本模式,误检率会显著下降。实际产线中,干净麦堆的图像数量可以占到训练集的30%左右,多了反而会影响杂质样本的学习。如果负样本已经加了还误检,就去看误检具体集中在哪些场景。我在实际排查时发现,有一类误检是传送带表面的划痕被当成了秸秆,另一类是特定光照角度下麦粒缝隙间的阴影被当成了石子。这些情况很难靠加数据完全解决,最后的兜底手段是调高推理时的置信度阈值。在PR曲线上找到precision与recall的平衡点,选一个让F1得分最高的置信度阈值,一般能过滤掉大部分误检。4.3 训练损失震荡、不收敛训练早期出现损失震荡是正常现象,尤其是数据增强比较强的情况下。但如果训练了20个epoch以上,验证集指标还在大幅波动,就要系统排查了。我一般按这个顺序检查:学习率是否过大。把lr0降到原来的1/10,看损失曲线是否变平滑。Mosaic是否过强。Mosaic在训练早期提升多样性,但如果数据本身分布差异大,持续用Mosaic会让模型很难收敛。可以提前到第30个epoch就关闭Mosaic。标签是否有错误。跑一个标签可视化脚本,把标注框画在原图上,人眼检查一圈,经常能发现某些类别标注错位或漏标。batch内样本是否不均衡。如果某个batch碰巧全是同一类别,BN层的统计量会跳动,损失就会震荡。这个在数据量小的时候尤其常见,优先增加数据集多样性。还有一个容易忽略的点:如果卡显存不够,用了小的batch size,比如2或4,BN层会因为统计量不稳定导致训练过程非常颠簸。这时候要么加大batch,要么冻结backbone层只在neck和head上训练,减少BN的统计波动。4.4 类别严重不平衡,稀有杂质样本不够虫蚀粒和霉变粒的典型样本在正常原粮里非常难找,经常出现训练集里某个类别只有几十个样本的情况。这时候硬训练,模型对这个类别的mAP基本等于零。我处理这类问题有三个方法,按有效性排序:第一是Copy-Paste增强,把稀有类别目标反复粘贴到不同干净背景上,人工扩充样本量。第二是过采样,让稀有类别的样本在采样时被更大概率选中。第三是给损失函数中对应类别的分类部分增加权重,让模型更重视稀有类别的错误分类。但这里有个隐性陷阱:合成样本不能无限加。如果Copy-Paste生成的图像里,石子的边缘、光照和背景完全不协调,模型会倾向于靠“违和感”来识别杂质,而不是靠杂质本身的物理特征。合成样本与真实样本的比例,我一般控制在1:1以内,宁缺毋滥。4.5 部署帧率不够快的取舍如果发现推理帧率达不到产线需求,优化顺序是有讲究的。我的建议是:先上TensorRT和FP16,这个改动几乎无损精度但能省一截算力;再考虑降低输入分辨率,比如从960降到640;再考虑换更小的模型规格,从s换到n;最后才是跳帧检测,比如每两帧检测一次、中间帧沿用上一帧结果。跳帧检测听起来简单,但对小目标检测不友好,因为小麦在传送带上高速运动,两帧之间目标可能已经移动了半个视野,沿用旧框会造成很大的位置误差。如果非要跳帧,最好配合运动速度设置重叠采集区域,让相邻帧之间留有足够的空间重合度。我在实际项目中把这些措施组合起来后,整个系统稳定运行在25fps以上,单帧处理时间约40毫秒,产线速度再快也能覆盖住。最后再分享一点个人体会。不要一上来就追求新模型、新结构,先把baseline跑出来,再去替换某个模块,每次只做一处改动并记录指标变化。我在这个项目里,最重的收获不是YOLOv11或者BiFPN本身,而是完整走通了“数据收集、清洗、模型改造、训练、部署”这个链路。换BiFPN确实带来了明显的精度提升,但如果一开始没有把数据基础打牢,换任何结构都白搭。后续这个方案如果继续做,我建议可以向两方向扩展:一是对更细颗粒的缺陷类型做多标签分类,比如区分虫蚀程度和霉变程度;二是把检测结果和产线上的风机、筛选设备联动,做到“检出即剔除”的闭环控制。这条路走通之后,粮库的杂质检测效率还有很大的提升空间。
做粮食加工这行的人应该都有感受:小麦从田间收上来,看着干干净净,但没过清理筛、去石机、磁选机之前,里面的杂质种类多到让人头疼。石子、秸秆、麦壳、破碎粒、虫蚀粒、霉变粒、异种粮粒,形态各异、大小不一,有些颗粒甚至比一颗麦粒还小。人工挑选费时费力,不同操作工的判断标准也不一致,漏检率很难压下去。我这两年一直在折腾深度学习目标检测在这个场景下的落地,把YOLOv11的主干网络和BiFPN特征融合结构接到一起,专门对付小麦杂质在线检测,效果比之前用的基础YOLO模型明显上了一个台阶。这篇内容不是什么玄学调参,都是我在实际项目里跑过、踩过坑、最后沉淀下来的方案。包括数据怎么做标注、模型结构怎么改、训练参数怎么定、上线部署会遇到哪些问题,这些都会拆开讲清楚。适合正在做农产品质检、粮食物流、食品加工视觉检测的朋友,尤其是对目标检测有一定基础、又想把小目标检测做好的人。看完之后,你至少可以按照这个思路快速搭起一套自己的小麦杂质检测方案。1. 项目背景与整体设计思路1.1 小麦杂质检测到底难在哪先说清楚我为什么要做这个项目。粮食加工企业在入磨前要对小麦进行杂质含量检测,传统方式是人工抽检。人工抽检的痛点很明显:效率低、主观性强、无法做到全量在线检测。更麻烦的是,杂质是小概率事件,一小堆麦粒里可能只有几颗石子,人工盯着传送带看,十分钟后注意力就下降了,漏检率其实相当高。用机器视觉来替代人工,第一个要解决的难题就是小目标。假设工业相机的视野是一米宽,图像分辨率做到1920x1080,一颗直径3毫米的石子,在图像里大概也就几个像素到几十个像素。这种目标在目标检测领域属于典型的small object,特征非常微弱,很容易被下采样过程直接丢弃。第二个难题是类别多样性。小麦杂质不是一个均匀的类别,而是“一堆杂物的统称”。石子边缘锐利、颜色偏灰;秸秆细长、有纤维纹理;麦壳薄、半透明、颜色与麦粒非常接近;霉变粒表面有暗斑;虫蚀粒形态不规则。这些类别之间差异很大,强行当成一个“杂质”类别来检测,会导致类内方差过高,模型很难收敛。第三个难题是类别不平衡。在一批合格原粮里,杂质占比通常很低,大部分区域是正常麦粒。这意味着训练数据里正样本(麦粒)和负样本(杂质)的比例很可能达到几百比一。如果直接端到端训练,模型很容易收敛到“全部预测为背景”的平庸解。这三个难点,恰好对应目标检测模型的两个关键能力:多尺度特征融合能力,以及困难样本挖掘能力。这就是我选择YOLOv11加BiFPN组合的原因。1.2 为什么基座选YOLOv11YOLO系列在工业视觉检测领域几乎是默认选项,原因很简单:推理速度快、训练生态完善、从数据标注到模型部署的全链路工具链都很成熟。在YOLOv11之前,我做过YOLOv5的长期运维,也试过YOLOv8,但最终把基座定在YOLOv11,是看重了它在目标检测性能上的几个针对性改动。首先是主干网络部分,YOLOv11引入了C3k2模块,这一系列基础模块组合让网络在提取局部纹理特征时更加高效。后来又加入类似C2PSA的注意力结构,让网络能够自适应地把计算资源集中在特征最明显的区域。对于小麦杂质检测来说,这意味着模型更容易注意到那些面积很小、但局部特征异常的目标,比如混在大量麦粒中的一粒石子。其次是正负样本分配策略。YOLOv11保留了anchor-free设计,同时改进了训练过程中的样本匹配方式,对不同尺度目标的正样本分配更均衡。我实测下来的感受是,即使不做特殊调优,YOLOv11在同等输入尺寸下的小目标召回率也要高于之前的几个版本。当然,YOLO系列有n、s、m、l、x多种规格,我在项目初期用n规格把整个pipeline跑通,等数据质量稳定后,再切到s规格提升精度。这个策略我很推荐,不要在算法没定型前就上大模型,不然训练一次就要烧掉大量GPU时间,迭代成本太高。1.3 为什么还要引入BiFPN,默认Neck不够用吗YOLOv11默认的neck结构是类似PANet的设计,自顶向下传递语义信息,自底向上传递空间细节。这个结构本身不差,但它有个问题:不同尺度的特征在融合时,权重是默认等价的。换句话说,来自深层特征图的语义信息和来自浅层特征图的细节信息,在相加或拼接时被一视同仁地对待。在杂质检测场景里,这种“一视同仁”是有问题的。一粒石子很小,它在浅层特征图上的响应很活跃,但到了深层特征图,经过多层卷积和下采样,这个响应会被严重稀释。而判断这粒石子是不是杂质,又需要足够强的语义信息。特征融合时如果不知道该着重听谁的,网络就容易在“看得见”和“认得出”之间失去平衡。BiFPN的解决思路很直接:给每条融合路径加上可学习的权重。它的完整名字叫加权双向特征金字塔网络,最早是在目标检测框架EfficientDet中提出的。核心有三点:自顶向下和自底向上两条路径反复融合,信息在多尺度特征之间双向流动;每条输入特征带一个可学习的标量权重,网络在训练过程中自己学会信任哪个尺度;删掉只有单条输入边的节点,减少冗余计算。落到小麦杂质检测任务上,我最大的体会是:加了BiFPN之后,模型对“小而不明显”的杂质的召回能力提升明显,尤其是麦壳和半透明秸秆这类,浅层纹理和深层语义需要动态配比的特征。用个生活化类比:默认PANet像是一个会议里每个人发言音量一样大,但真正有信息量的人可能只有一两个;BiFPN相当于给每个发言人配了一个麦克风音量旋钮,网络在训练中自己学会把有效信息放大、把干扰信息压低。这个特性对信噪比低的小目标检测场景来说,价值非常大。2. 核心细节解析与实操要点2.1 数据采集与标注规范任何视觉检测项目,数据是地基。小麦杂质检测的数据采集,我的建议是别只在实验室里拍样本,一定要去真实的产线环境取图。传送带震动、光照变化、物料堆积厚度、麦粒粉尘附着,这些在实际场景里都会改变图像分布。在硬件层面,我用的方案是工业面阵相机加恒定光源。分辨率不低于1920x1080,帧率根据产线速度选择,通常在15到30fps之间。相机安装在传送带正上方,视场宽度控制在麦料流宽度附近,避免拍到大量无关背景。打光方式用过环形光源和同轴光源,环形光对麦粒这种半球形表面的物体更友好,阴影更少。标注规范是这类项目里最容易翻车的地方。我的标注分类是这样的:石子、秸秆、麦壳、破碎粒、虫蚀粒、霉变粒、异种粮粒。一开始我把“杂质”统一标成一个类,后来发现模型精度怎么调都上不去,因为石子这类刚性目标的特征和霉变粒这类表面渐变特征差异太大,模型在一个类里要学的东西太多,互相干扰。拆成多个类别后,每个类别的特征更聚焦,整体检测效果反而更好。具体标注规则有几条,直接写在这里供参考:目标在图像中至少要有3x3像素以上,否则不标;目标被遮挡面积超过70%时不标;边界模糊、肉眼都无法确认的类型,放到后台由项目负责人复核,不勉强归入某一类;每个类别的框尽量紧贴目标边缘,不要留白太多;全部样本由至少两个人交叉标注,用标注一致性指标做校验。这里有个心得,很多人容易忽略:要把“完全干净的麦堆”图像也加进训练集,并且不做任何标注。这些图像是负样本,能教会模型“什么情况下应该输出空结果”,对降低误检率的作用非常大。这点我在后面的误检问题里还会再提到。2.2 数据增强策略数据增强在小目标检测中的作用,比很多人想象中大得多。默认的训练配置里一般会带Mosaic增强,就是把四张图随机拼成一张,这样既增加了样本多样性,又提高了单位图像里小目标的密度。Mosaic增强对杂质检测特别有用,因为杂质本身稀疏分布,一张真实原图里往往只有几个目标,模型学不到密集目标的分布特征。但Mosaic也有副作用。训练后期如果一直用Mosaic,模型会慢慢适应“图像由四块不同区域拼接而成”这种人工痕迹,真实场景里的连续背景反而让模型不适应。我在训练时会设置一个close_mosaic的轮数,让最后十来个epoch自动关闭Mosaic,回到真实的完整图像分布上微调,验证集的mAP通常会有小幅回升。针对杂质数据,我强烈建议加一个Copy-Paste增强。具体做法很简单:从训练集中裁剪出已经标注好的杂质目标,把它的掩膜区域粘贴到另一张“干净麦堆”图像上,同时做随机缩放、旋转、光照扰动,生成大量合成困难样本。这个增强对缓解虫蚀粒、霉变粒这类样本不足的问题,非常有效。粘贴时要注意边缘羽化,不然模型学会靠“生硬的矩形边缘”识别杂质,一换真实场景就失效。其他基础增强我一般这样设置:HSV扰动开启,色相变化在0.015左右,明度饱和度在0.4左右;随机翻转开启,随机旋转90度开启;Cutout按0.3概率开启。这些参数不是固定不变的,但基本覆盖了产线上光照波动的主要情况。2.3 模型结构修改细节把BiFPN融合进YOLOv11,我是这样处理的。YOLOv11的检测器由三部分组成:backbone用来提取特征,neck用来融合多尺度特征,head用来输出分类和边框。我需要动的部分主要是neck。整个检测器在结构定义上一般会用一个yml格式的模型配置文件来描述。我把默认neck部分拿掉,改成一个带权重自学习的双向融合结构。大致拓扑是这样的:backbone输出的最后三个尺度的特征图,分别记为P3、P4、P5;P5先做自顶向下融合,上采样后与P4融合,再上采样与P3融合;P3再做自底向上融合,下采样后与P4融合,再下采样与P5融合;每个融合节点处,不是简单的相加操作,而是先对每条输入特征分别做1x1卷积统一通道数,乘上各自的软权重,再求和;权重计算采用快速归一化融合方式,输出等于每个输入乘以对应权重的加权和,再除以所有权重加总,防止权重无界增长。这个过程用公式写出来,每个融合节点的输出是:O = Σ( wi * Ii ) / ( Σ(wi) + ε )其中ε是一个非常小的常数,比如0.0001,用来避免除零。所有wi初始化为1,训练过程中由网络自动学习。这个公式看起来简单,但实际作用非常明显,本质上是对多尺度特征做了一个可学习的软选择。具体实现层面,当时是在一个二开过的ultralytics风格代码库里改的。如果不想自己从头写,也有现成方案,把BiFPN当成一个自定义模块注册进模型定义文件里,替换neck段即可。改完之后,head部分不用动,输出层的尺寸和数量保持一致,训练脚本、评估脚本、导出脚本全部可以沿用,这是我最看重的一点——改动成本尽量低,系统其他部分不用跟着翻车。2.4 损失函数与正负样本分配模型结构改完之后,损失函数也要稍微做一些适配。YOLOv11默认把分类损失和边框回归损失加在一起,分类用BCE,回归用CIoU加DFL。在杂质检测场景下,我保留了这套默认组合,但对正负样本分配做了重点检查。训练时模型会计算每个目标候选框与真实框的匹配度,把匹配上的作为正样本,其余的作为负样本。YOLOv11使用的task-aligned assigner会把分类得分和IoU联合起来分配样本,目标是一个非常小的石子,如果分类响应不够强,很可能被当成负样本丢弃。针对这一点,我会在训练配置里稍微调低正样本匹配的阈值,让更多“边缘候选”参与训练。效果是召回率提升了,但代价是训练时间变长,需要自己平衡。还有一个技巧:如果小目标漏检特别严重,可以考虑在neck输出里增加P2或P4尺度的高分辨率特征层参与检测。P2层的步长是4,相当于640输入下输出160x160的特征图,对小目标的响应会更敏感。代价是计算量增加,显存占用变大,推理变慢。我的建议是后续项目先记录当前模型指标,再决定要不要加。3. 实操过程与核心环节实现3.1 环境准备先交代一下我这边的运行环境,供参考。深度学习框架这块,用的是Python 3.10加PyTorch 2.x版本,GPU用的是一块24G显存的单卡。安装命令比较常规,把这几个包装好即可:pip install torch torchvision pip install ultralytics版本选择上有一点要注意:GPU驱动的CUDA版本要和PyTorch的编译版本匹配,我一开始没检查,装了不匹配的版本,跑训练时直接报CUDA initialization error,排查了半小时才发现是版本问题。显存方面,如果用的是12G的卡,训练s规格模型加640输入加16的batch是勉强可以跑的,n规格更没问题。如果显存不够,优先缩小batch,别先降低输入分辨率,那样对小目标检测的影响会更大。3.2 数据集划分与目录组织数据集的组织方式,我沿用目标检测任务常见的目录结构,可以参考这个版本:wheat_impurity/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── dataset.yamldataset.yaml里面写清楚类别名称和图片路径。图片与标注文件一一对应,文件名保持一致,只有后缀不同。划分比例我用的是70%训练、15%验证、15%测试。但要特别提醒一点:如果这些图片是从同一个视频流里连续截取的,直接随机划分会导致训练集和验证集里出现“同一场景的连续帧”,验证结果会虚高。正确的做法是按时间段或按批次划分,比如前三天采集的数据做训练,后面一天的数据做验证和测试,这样更接近真实上线后遇到的情况。3.3 训练关键参数与调参路线训练参数的设置是我在这类项目里花时间最多的部分,直接给出一份我用下来效果比较稳定的配置:参数取值说明imgsz640训练输入尺寸batch16视显存调整epochs120观察损失与验证指标收敛情况optimizerSGD动量设为0.937,权重衰减5e-4lr00.01按batch做线性缩放warmup_epochs3前3轮预热学习率mosaic1.0开启Mosaic增强close_mosaic10最后10轮关闭Mosaicmixup0.1小概率MixUp增强copy_paste0.3拼接粘贴增强概率hsv_h0.015HSV色相扰动幅度hsv_s0.4饱和度扰动幅度hsv_v0.4明度扰动幅度fliplr0.5水平翻转概率训练启动命令大致长这样:yolo detect train \ data=dataset.yaml \ model=yolov11n.pt \ imgsz=640 \ batch=16 \ epochs=120 \ lr0=0.01 \ mosaic=1.0 \ close_mosaic=10如果是用自定义了BiFPN结构的新模型文件,把model参数指向自己写的yml配置文件即可,其他流程完全一致。关于学习率,我多说一句。SGD在目标检测上的稳定性比AdamW好,尤其是当数据量不够大的时候,AdamW很容易跑出“一开始损失降得快、后期精度上不去”的情况。学习率0.01对应的batch是16,如果改到batch为32,最好学习率也等比放大到0.02,否则有效更新步长变小,训练会变慢。调参路径我的建议是:先拿n规格加默认配置跑一个baseline,记录mAP50、mAP50-95和召回率。然后在这个基础上,一次只改一个变量,比如先只加BiFPN,看涨不涨点;再改输入尺寸到960,看有没有进一步提升;最后再调整数据增强参数。不要同时改结构和超参数,不然出了问题根本定位不到是哪里引入的。3.4 模型评估与问题定位评估指标不能只看mAP一个数。在杂质检测场景里,我重点关注四个指标:precision、recall、mAP50、mAP50-95。precision衡量的是模型“框出来的东西里面有多少是真正杂质”,recall衡量的是“真正的杂质里有多少被框出来了”。在产线质检项目里,recall的权重通常比precision高,因为漏检一个石子进入下一道工序,可能直接造成设备损坏或者成品质量事故。如果多类别模型跑完之后,单独看每个类别的mAP,会发现差异很大。通常石子和秸秆这类外形特征明显的类别精度较高,霉变粒和破碎粒这类特征不稳定的类别精度偏低。这时候不要盲目调整整体模型,应该单独检查数据量最少的类别,看看是不是标注数量太少,或者类别间特征有重叠。模型收敛后,把可视化预测结果画到一批随机验证图上,人眼检查是非常必要的。画框结果能直接暴露两个问题:一是小目标区域密集的石子可能只被召回一部分,二是麦粒之间的空隙容易被误判为杂质。这两个现象单靠指标数值很难看出来,但人眼扫一眼图片就清楚了。3.5 模型导出与部署训练完成后,要把PyTorch模型导出成推理引擎。我的标准导出链路是先用ONNX作为中间格式,再转到TensorRT的engine文件。命令大致如下:yolo export model=best.pt format=engine half=True dynamic=True导出时打开half模式,模型权重转成FP16,显存占用和推理延迟都会明显下降。在24G显存的卡上,s规格模型加640输入,FP16下的推理延迟大概在10毫秒到20毫秒之间,放到产线实时检测是完全够用的。实际部署时还有几个工程细节要处理。工业相机通过网口或者USB口输出图像流到工控机,程序逐帧读图,做归一化预处理,送入推理引擎,得到检测框,再做NMS后处理,最后把结果叠加回原图。这个过程要控制总延迟,在工控机上我会用双线程模型,一个线程专管相机取流和图像预处理,另一个线程做模型推理和后处理,两组操作通过队列解耦,避免取帧等待阻塞推理。4. 常见问题与排查技巧实录4.1 小目标漏检严重怎么办这是我在项目里被问得最多的问题。如果你的模型在验证集上对大块杂质检测正常,但对石子这类小目标频繁漏检,首选检查的不是模型结构,而是输入分辨率。640的输入对小麦这类小而密集的物料来说,目标占比太低。把输入分辨率提到960或者1280,通常recall会有肉眼可见的提升。代价是训练时间和推理时间变长,需要自己权衡。如果提升输入分辨率后仍然漏检,做一个简单实验:把测试集里的小目标区域单独裁剪放大,看模型能不能识别。如果能识别,说明模型知道“放大后的杂质长什么样”,但不知道“缩小后的杂质长什么样”,这时应该重点加强小目标样本在训练数据中的占比,可以用Copy-Paste增强专门生成大量小尺寸杂质样本。还有一类场景是输入分辨率不能无限提高,因为产线需要处理速度。这时候可以走推理端切图方案:把一帧大图切成若干张640的patch,每张patch做推理后,把检测框映射回原图坐标,再做一轮全局NMS。这个方案在工程上稍微复杂一些,但对小目标检测的提升非常直接。切图时要注意相邻patch之间保留10%到20%的重叠,否则正正好在边界上的目标容易被切掉一半。4.2 误检偏多,把麦粒或设备纹理当成了杂质误检多的原因通常不是模型结构问题,而是数据问题。我踩过最大的坑是:训练集里所有图像都是“含杂质的麦堆”,模型从来没有见过“没有杂质的干净麦堆”,导致它把一切看起来略微不统一的纹理都当成潜在的杂质。解决方法也简单:专门采集一批完全干净、无任何杂质的麦堆图像加入训练集,图像里没有任何标注框。让模型学习到一类“空输出”的负样本模式,误检率会显著下降。实际产线中,干净麦堆的图像数量可以占到训练集的30%左右,多了反而会影响杂质样本的学习。如果负样本已经加了还误检,就去看误检具体集中在哪些场景。我在实际排查时发现,有一类误检是传送带表面的划痕被当成了秸秆,另一类是特定光照角度下麦粒缝隙间的阴影被当成了石子。这些情况很难靠加数据完全解决,最后的兜底手段是调高推理时的置信度阈值。在PR曲线上找到precision与recall的平衡点,选一个让F1得分最高的置信度阈值,一般能过滤掉大部分误检。4.3 训练损失震荡、不收敛训练早期出现损失震荡是正常现象,尤其是数据增强比较强的情况下。但如果训练了20个epoch以上,验证集指标还在大幅波动,就要系统排查了。我一般按这个顺序检查:学习率是否过大。把lr0降到原来的1/10,看损失曲线是否变平滑。Mosaic是否过强。Mosaic在训练早期提升多样性,但如果数据本身分布差异大,持续用Mosaic会让模型很难收敛。可以提前到第30个epoch就关闭Mosaic。标签是否有错误。跑一个标签可视化脚本,把标注框画在原图上,人眼检查一圈,经常能发现某些类别标注错位或漏标。batch内样本是否不均衡。如果某个batch碰巧全是同一类别,BN层的统计量会跳动,损失就会震荡。这个在数据量小的时候尤其常见,优先增加数据集多样性。还有一个容易忽略的点:如果卡显存不够,用了小的batch size,比如2或4,BN层会因为统计量不稳定导致训练过程非常颠簸。这时候要么加大batch,要么冻结backbone层只在neck和head上训练,减少BN的统计波动。4.4 类别严重不平衡,稀有杂质样本不够虫蚀粒和霉变粒的典型样本在正常原粮里非常难找,经常出现训练集里某个类别只有几十个样本的情况。这时候硬训练,模型对这个类别的mAP基本等于零。我处理这类问题有三个方法,按有效性排序:第一是Copy-Paste增强,把稀有类别目标反复粘贴到不同干净背景上,人工扩充样本量。第二是过采样,让稀有类别的样本在采样时被更大概率选中。第三是给损失函数中对应类别的分类部分增加权重,让模型更重视稀有类别的错误分类。但这里有个隐性陷阱:合成样本不能无限加。如果Copy-Paste生成的图像里,石子的边缘、光照和背景完全不协调,模型会倾向于靠“违和感”来识别杂质,而不是靠杂质本身的物理特征。合成样本与真实样本的比例,我一般控制在1:1以内,宁缺毋滥。4.5 部署帧率不够快的取舍如果发现推理帧率达不到产线需求,优化顺序是有讲究的。我的建议是:先上TensorRT和FP16,这个改动几乎无损精度但能省一截算力;再考虑降低输入分辨率,比如从960降到640;再考虑换更小的模型规格,从s换到n;最后才是跳帧检测,比如每两帧检测一次、中间帧沿用上一帧结果。跳帧检测听起来简单,但对小目标检测不友好,因为小麦在传送带上高速运动,两帧之间目标可能已经移动了半个视野,沿用旧框会造成很大的位置误差。如果非要跳帧,最好配合运动速度设置重叠采集区域,让相邻帧之间留有足够的空间重合度。我在实际项目中把这些措施组合起来后,整个系统稳定运行在25fps以上,单帧处理时间约40毫秒,产线速度再快也能覆盖住。最后再分享一点个人体会。不要一上来就追求新模型、新结构,先把baseline跑出来,再去替换某个模块,每次只做一处改动并记录指标变化。我在这个项目里,最重的收获不是YOLOv11或者BiFPN本身,而是完整走通了“数据收集、清洗、模型改造、训练、部署”这个链路。换BiFPN确实带来了明显的精度提升,但如果一开始没有把数据基础打牢,换任何结构都白搭。后续这个方案如果继续做,我建议可以向两方向扩展:一是对更细颗粒的缺陷类型做多标签分类,比如区分虫蚀程度和霉变程度;二是把检测结果和产线上的风机、筛选设备联动,做到“检出即剔除”的闭环控制。这条路走通之后,粮库的杂质检测效率还有很大的提升空间。