拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv7实战:柑橘缺陷检测从数据标注到TensorRT部署全流程

YOLOv7实战:柑橘缺陷检测从数据标注到TensorRT部署全流程

做目标检测项目这些年,我发现自己和周围同事最容易犯的错,就是拿到任务先翻模型榜单,比完 YOLOv5 比 YOLOv8,然后把代码 clone 下来跑通一个 demo,就以为万事大吉。真正到了实际落地,尤其是要从零开始做数据、训练、优化、部署一条龙的时候,问题往往都出在那些看着不起眼的环节上。

这篇文章想分享的是我用 YOLOv7 完成的一个从数据标注到模型优化再到部署落地的完整项目,场景选的是柑橘外观缺陷检测——简单说,就是在分选线上把有磕伤、腐烂、果梗凹陷的果子自动挑出来。我会把整个流程里我认为最关键的节点挨个拆开讲,包括数据怎么采集和标注才不容易翻车、训练参数怎么调才稳、模型怎么优化才能在 Jetson、RK3588 这类边缘设备上跑得动、部署现场有哪些坑等着你。适合正在做目标检测落地项目,或者准备在低算力硬件上部署 YOLO 系列模型的工程师和学生参考,哪怕你是第一次接触整套流程,按着这个脉络走也能少走很多弯路。

1. 项目需求拆解与方案选型

最开始接到这个需求时,对方描述得非常简单:在分选线上把“坏的果子”挑出来。但“坏”这个字背后其实藏着一堆问题,是我整个项目里最花时间的地方,也是后面所有方案决策的依据。

1.1 明确检测目标:难点从来不在网络结构

我们面对的不是简单的“苹果 vs 橘子”分类,而是同一种果实上的细微外观差异。具体来说,检测目标包括三类缺陷:磕伤(碰撞造成的局部变色或凹陷)、腐烂(颜色发暗、表皮皱缩)、果梗凹陷(果蒂周围的不规则区域,本身不一定坏,但影响分级)。麻烦的地方在于,这些类别的特征和目标大小差异非常大。

举个实际例子,整颗果实在图像里可能占几百个像素,但一个早期的磕伤区域可能只有十几个像素,在 640x640 的输入分辨率下非常容易被漏掉。再加上分选线现场环境复杂,自然光照变化大,果实之间还有重叠遮挡,同一个缺陷从不同角度看过去,形态完全不一样。这比许多公开数据集里的场景要难处理得多。所以做完需求分析后我就确定了一件事:这个项目的瓶颈不在选哪个模型,而在数据能否把这些细粒度差异表达清楚。

1.2 为什么选 YOLOv7 而不是 YOLOv5 或 YOLOv8

网上关于“v5 v7 v8 谁更强”的争论一直没停过,我的选择标准很简单:部署成熟度、精度速度平衡、训练成本。当时做了一轮横向评估,结论是这样的:

模型精度表现部署生态训练资源消耗结论
YOLOv5稳定,但细粒度缺陷表现一般非常成熟中等备选
YOLOv7在细粒度小目标上突出,E-ELAN 结构信息流动好TensorRT 支持成熟,官方提供导出脚本比 v5 略高,但可接受选定
YOLOv8整体精度优秀,训练技巧丰富生态也好,但当时量化工具链对 v7 更友好显存占用偏高,训练时间更长备选

YOLOv7 让我最满意的一点是它的重参数化结构。训练时可以用复杂的多分支结构提升精度,推理时又能把结构重参数化成一个简单的 3x3 卷积串联,这不仅让推理速度快,还给后面的 INT8 定点量化留了更大余量。我们在测试里发现,同样的数据下 v7 量化后的精度损失普遍比 v8 小 1 到 2 个点,这对边缘部署来说相当关键。当然如果你完全不用量化,v8 也是个不错的选择,但我们要部署的设备算力有限,这 1 到 2 个点往往就是能不能跑起来的区别。

1.3 整体流程规划和时间分配

项目整体拆成了九个步骤,每个步骤都有明确的输出物和验收标准:

  1. 需求分析与场景调研:输出缺陷定义文档和标注规则草案
  2. 数据采集与清洗:输出建好的图片库,剔除模糊帧和重复帧
  3. 数据标注与质检:输出 YOLO 格式标注文件和类别分布统计
  4. 数据集划分与增强:输出训练集、验证集、测试集,三者互不干扰
  5. 模型训练与调参:输出 baseline 权重和训练曲线
  6. 模型优化与评估:输出剪枝、量化后的权重和精度对比报告
  7. 模型导出与格式转换:输出 ONNX 和 TensorRT engine
  8. 部署环境搭建:输出推理服务程序和接口文档
  9. 现场联调与迭代:输出最终版本模型和问题清单

这里我想重点强调一下时间分配。很多人以为模型训练和调参是大头,实际上我们整个项目里数据相关工作占了大概 60% 的时间,模型训练和优化只占 25%,最后部署调试占 15%。数据标注不是“体力活”,而是一个需要持续修订标注规则、反复质检的迭代过程,前期把它做扎实了,后面训练环节能省非常多事。

2. 数据标注:这一步直接决定了模型的天花板

数据标注很容易被人低估,总觉得“不就是画框吗”。但画框和画框之间的差距,在模型精度上的反映是非常直接的。下面我把采集规范和标注质量控制拆开讲,这些细节是我真正踩过坑之后总结出来的。

2.1 数据采集的规范和细节

整体采集量级是这样的:第一版总共采集了 3200 张图片,包含健康果、磕伤、腐烂、果梗凹陷四个类,共标注了 15800 个目标框。后续根据验证结果又补充采集了 800 张困难样本。有几个采集细节值得特别说明。

第一,场景多样性比总量更重要。同样是“磕伤”,晴天直射光下、阴天散射光下、室内分选线灯光下,颜色和纹理特征差异非常大。我开始第一版数据时只采了分选线固定机位,结果模型在自然光环境下测试掉点严重,后来重新补充了不同时段、不同天气的数据才解决。建议至少覆盖三种光照条件,每种光照下尽量让果实处在不同的摆放角度。

第二,相机参数必须固定。分选线用的多个相机如果白平衡、曝光参数不统一,会让同一个缺陷在不同相机下呈现完全不同的色偏。模型学到的可能不是“磕伤”本身,而是某个相机的色彩特征,这就是典型的领域偏移。我们的处理方式是把所有相机参数统一锁定,并采集了多相机的标定板图像做颜色校正。

第三,图片命名和存储规范从一开始就要定好。我见过不少项目因为图片命名混乱,后期回查数据时根本不知道哪张是哪批。我们统一用“采集日期_采集批次_序号”的方式命名,比如 20240511_A_0234.jpg,同时维护了一个记录表格,把每批数据的场景、相机参数、光照情况都记录下来。这个表在后面积累困难样本时帮了大忙。

2.2 标注工具选型与配置

标注工具选择会直接影响效率和质量。我个人的建议是:单人小规模项目直接用 X-AnyLabeling,它功能足够顺手;如果是多人协作的大项目,用 CVAT 更合适,因为它有完善的任务分配和审核机制,多人标注时的管理会轻松很多。这两款工具都非常成熟,在社区里用的人也很多。

我当时是单人标注,所以选了 X-AnyLabeling。它支持直接导出 YOLO 格式的 txt 文件,省去了 VOC xml 转 YOLO 的中间步骤。界面操作上,有几个设置项强烈建议打开:自动保存、十字线辅助、标签快捷键。尤其是十字线辅助,在标小目标时能明显减少画框偏移,因为小目标区域太小,鼠标稍微一抖框就歪了。

标注格式方面,直接以 class cx cy w h 的归一化 YOLO 格式保存。这里有个经验之谈:不要在标注过程中频繁调整图片尺寸,YOLO 训练时会在加载阶段做 letterbox 缩放,你标注时的基准尺寸影响不大,统一就好。

2.3 标注规则定义和质检

标注规则必须在开工前定清楚,并且形成文字发给所有人(哪怕只有你自己)。我们当时定了几条关键规则:

  • 缺陷面积超过果实面积的 1/10 才标为该类缺陷,小于这个比例的不标
  • 果实重叠遮挡时,能看到超过 1/2 轮廓的果就标,不足 1/2 的不标,避免标注框范围里包含大量无关像素
  • 一个果实上同时有磕伤和腐烂时,按更严重的腐烂类来标,不标两个框
  • 目标框必须紧贴目标边缘,不允许留大空白,也不允许切掉目标超过 10%

规则定了之后要配质检环节。我们采用了两道检查:第一道,按 10% 比例抽检,重点看有没有漏标、错标,特别是腐烂和健康果之间的边界案例,这两个类在人工视觉上就存在一定的连续性,是最容易标出分歧的地方;第二道,统计每张图的标注框数量,如果出现极端值,比如一张图有 30 个框而平均只有 5 个,就拉出来人工复查,大概率是标重了或者把背景纹路当成了目标。

这里稍微说一句,现在行业内已经有不少项目开始往 3D 点云标注延伸,比如做立体分选的时候,需要把 RGB 图像和深度点云对齐后标注。3D 标注的流程更复杂,但核心思想还是那几句话:标准先行、工具顺手、抽检兜底、持续迭代。如果你后续要做双目视觉或者点云分选,可以在 2D 标注流程跑通之后再逐步迁移。

3. 模型训练与优化:从“能跑”到“好用”

数据准备得差不多了,训练环节就顺理成章。但“训练跑通”和“模型好用”之间还有很长的距离,这部分的重点是超参数理解、精度优化和部署前的模型瘦身。

3.1 训练环境与数据集拆分

训练环境我用的是一台 RTX 3090 的服务器,具体组合是 Ubuntu 20.04 + Python 3.8 + PyTorch 1.12.1 + CUDA 11.6。这里提醒一下,YOLOv7 官方仓库在不同 PyTorch 版本下的表现会有细微差异,安装时不要盲目追求最新版 PyTorch,以官方 requirements.txt 推荐的版本组合为准更稳妥。

数据集目录结构建议直接按照 YOLO 训练的标准方式组织:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

data.yaml 里写好类别名称和训练验证路径,直接交给训练脚本读取。

数据集划分这里特别想多说一句,不要随机划分。如果图片是从视频抽帧来的,或者一批数据是连续时间拍摄的,相邻帧之间的相似度很高,如果一部分进了训练集、一部分进了验证集,验证集精度会虚高,也就是典型的数据泄漏。我们当时按拍摄批次划分:同一天的图片全进训练集,另一批单独留作验证集,这样验证结果才接近真实水平。

3.2 训练核心参数理解与设置

我把训练时最重要的几个参数和决策逻辑列成了一张表,这些参数之间相互影响,不能单独看某一个:

参数我们用的值选择理由
img_size640精度与显存占用的平衡点,对小目标检测也比较友好
batch_size323090 显存下稳定运行的偏大值,能减少梯度噪声
epochs300(配合早停)模型在 200 轮左右基本收敛,留足余量
lr00.01YOLOv7 默认值,配合 warmup 很稳
mosaic1.0默认开启,对小目标场景有显著增益
mixup0.2适当的 mixup 增强模型泛化性,太高会让训练难收敛

训练命令是标准的:

python train.py --data data.yaml --weights yolov7.pt --batch-size 32 --img 640 --epochs 300 --device 0 --workers 8

训练日志怎么看,我有一点心得:很多人盯着 mAP 不放,但我建议同时观察 Precision 和 Recall,尤其在缺陷检测场景里,Recall 代表“该捡出来的果子有没有漏掉”,这个指标比 mAP 更能反映现场使用的真实痛点,因为漏检一个坏果的代价往往比错检一个健康果更大。标准做法是看 val 的 P、R、mAP 三者是否同时保持在高位。

3.3 精度优化:数据增强、类别不均衡和轻量注意力

第一版 baseline 训练完成,mAP 到了 0.912,但在验证集上发现问题集中在两个方面:腐烂类别的 Recall 偏低(0.87),果梗凹陷类别的小目标漏检较多。针对这两个问题做了三轮针对性优化。

第一轮是类别不均衡处理。四个类别中健康果占比接近 50%,果梗凹陷只有 12%,模型自然偏向样本量大的类别。我们用了过采样策略,把果梗凹陷的样本在训练时重复采样,使其占比提升到接近 25%,Recall 直接涨了 3 个点。这里不建议用欠采样,因为数据总量本就不算大,丢掉样本太可惜。

第二轮是引入轻量注意力模块。在 YOLOv7 的 backbone 输出和 head 输入之间加了一个 CBAM 模块,它的作用机制可以这样理解:让模型在处理特征图时既能关注通道维度的信息,又能关注空间维度的位置信息,对小目标定位和缺陷细微特征增强都有帮助。代价是参数量增加了一点点,推理速度几乎无感。加了 CBAM 之后整体 mAP 涨到 0.921,尤其是果梗凹陷的精度提升了接近 2 个点。

第三轮是小心地调整训练策略。把 warmup 轮数从默认的 3 轮增加到 5 轮,让模型在前期更平稳地起步,避免一开始就走偏;同时把余弦退火的最小学习率从 lr0 的 0.001 倍改成 0.0001 倍,让模型在后期迭代得更细致。这轮下来 mAP 稳定在 0.925。

3.4 模型瘦身:剪枝与量化

训练阶段告一段落,接下来要考虑的是部署性能。原始 YOLOv7 模型 FP16 精度下在 RTX 3090 上能跑多快不重要,关键是要在 Jetson Orin Nano 这样的设备上达到实时,这就必须做剪枝和量化。

剪枝这块我们用了结构化剪枝,以通道为单位删除对结果贡献小的卷积核。具体操作上是先用训练集做敏感性分析,找出哪些层的通道冗余度最高,然后逐层剪掉 20% 到 30% 的通道,再用极低的学习率做一次短周期的微调恢复精度。剪枝后模型大小从约 71MB 降到了 50MB 左右,精度只掉了 0.4 个点,可接受。

量化是部署前的最后一步。我们用 TensorRT 的 INT8 模式,用验证集中抽出的 500 张图片做校准数据集,注意校准集图片必须覆盖所有类别和亮度场景,否则量化后的精度损失会集中爆发在没覆盖到的场景上。INT8 量化后实测精度比 FP16 再掉大概 1 到 1.5 个点,但推理速度几乎翻倍。下面是我们实测的一组对比数据:

方案mAP推理耗时(Orin Nano)说明
原始 FP160.92545msbaseline
剪枝 + FP160.92135ms剪掉 25% 通道
剪枝 + INT80.90618ms量化后速度优势明显

这个优化顺序很重要,先想办法提精度,最后再做压缩和量化,不要在精度还没达标的时候就急着部署。

4. 模型部署:从 PyTorch 到生产环境

部署环节最核心的事情就两件:把模型转成推理引擎能吃的格式,以及保证服务稳定实时地跑起来。这两件事各自都有不少细节,我按顺序讲。

4.1 模型导出:PyTorch 到 ONNX 的坑

导出 ONNX 的命令很简单,但有几个前提条件必须满足。首先,导出前一定要把模型切到 eval 模式,否则导出的模型里包含训练相关的计算图,推理结果可能没问题,但性能会打折扣,极端情况下会报错。其次,输入尺寸建议直接固定为 640x640,虽然 ONNX 支持动态输入,但固定尺寸能让后面的 TensorRT 优化做得很彻底,速度收益非常明显。

import torch from models.experimental import attempt_load model = attempt_load('best.pt', map_location='cpu') model.eval() dummy_input = torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, 'model.onnx', opset_version=12, input_names=['images'], output_names=['output'], dynamic_axes={'images': {0: 'batch'}, 'output': {0: 'batch'}} ) print("export done")

opset 版本这里有个经验:不是越高越好。用 opset 13 导出的模型在 ONNX Runtime 里有时会触发某些算子的兼容性问题,导致加载失败或推理变慢。我们最终锁在 opset 12,稳定性最好。

导出后必须做一次精度验证:把同一张图分别跑 PyTorch 模型和 ONNX 模型,比较输出张量,差异应该在一个很小的范围内(通常是 1e-5 以下)。如果差异突然变大,排查方向通常是某个自定义算子没被正确映射,比如 YOLOv7 里的一些重参数化结构在导出时需要用官方提供的 deploy 模式代码,这一点在官方仓库里有明确说明,照着做就行。

4.2 推理框架选型与 TensorRT 部署

不同硬件平台适合不同的推理框架,这个选择直接影响最终的运行效率和工程复杂度,我在选型时会首先根据目标平台的芯片类型来决定方向:

硬件平台推荐推理框架优点注意事项
x86 服务器(Intel CPU)ONNX Runtime / OpenVINO部署简单,CPU 上优化好多线程要配置好
NVIDIA GPU / Jetson 系列TensorRT速度极致,支持 INT8版本和 JetPack 要匹配
手机 / ARM 开发板(RK3588、树莓派等)NCNN / RKNN轻量,适配移动端算子兼容性需要逐一验证

我们用 Jetson Orin Nano 做边缘推理设备,配套的是 JetPack 5.1.2,里面带的是 TensorRT 8.5。TensorRT 引擎构建推荐直接用官方 trtexec 工具做离线构建,不要在推理时动态构建:

/usr/src/tensorrt/bin/trtexec \ --onnx=model.onnx \ --saveEngine=model.engine \ --fp16 \ --int8 \ --calib=calibration.cache \ --workspace=4096

engine 文件生成后,可以先用 trtexec 做一遍性能测试,确认吞吐量和延迟符合预期,再进入代码集成。推理脚本的核心流程长这样,一共就这么几件事:

import tensorrt as trt import numpy as np import cv2 # 加载 engine def load_engine(engine_path): runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open(engine_path, 'rb') as f: return runtime.deserialize_cuda_engine(f.read()) # 预处理:letterbox + 归一化 def preprocess(img, input_size=640): h, w = img.shape[:2] scale = min(input_size / h, input_size / w) nw, nh = int(round(w * scale)), int(round(h * scale)) resized = cv2.resize(img, (nw, nh)) canvas = np.full((input_size, input_size, 3), 114, dtype=np.float32) canvas[0:nh, 0:nw] = resized return canvas.transpose(2, 0, 1)[None] / 255.0

这里要专门提醒一下 letterbox 的问题:YOLOv7 训练时图像会先做 letterbox 填充到 640 再进网络,推理时如果不用同样的 letterbox 逻辑,模型看到的数据分布和训练时不一致,精度会明显掉。填充值用 114(RGB 灰值)是 YOLO 系列的默认做法,不要因为嫌麻烦改成 0,实测影响很大。另外记得推理时也要对输出结果做反 letterbox 的坐标换算,否则画框位置会偏移。

4.3 工程化部署的稳定性考量

模型能跑只是第一步,真正的工程化部署还要处理不少实际问题。

第一个问题是预处理耗时经常被忽略。在 Jetson 上,单张图的 resize 和 letterbox 操作在 CPU 上要花 5 到 8ms,如果算上归一化和内存拷贝,这部分耗时常常接近推理耗时的一半。处理方法是把预处理和模型推理用流水线的思路拆开,采集线程和推理线程解耦,不要让 CPU 预处理和 GPU 推理互相等待。

第二个问题是内存和线程管理。我踩过一个非常典型的坑:推理服务每检测一帧就重新分配一次输入输出内存,运行几小时后显存持续增长,最后还是进程崩溃。解决办法是启动时一次性分配好固定大小的输入输出缓冲区,在循环里复用。另外如果做视频流接入,建议直接采用生产者-消费者模式,视频解码一个线程,推理一个线程,再用队列衔接,避免网络抖动时阻塞整个链路。

第三个问题是超时和异常保护。边缘设备上跑服务,要考虑模型推理偶发卡死的情况,最稳妥的方案是给推理调用加超时机制,超时后自动恢复线程;同时把输入图片做格式校验,遇到损坏或空帧直接丢弃,不要进入推理流程。

最终的实测数据是:在 Jetson Orin Nano 上,剪枝 + INT8 模型单帧推理 18ms,算上预处理后处理,整体端到端延迟大约 26ms,可以稳定跑在 30FPS 左右,满足了分选线的实时性要求。

5. 常见问题与排查经验

整个项目做下来,踩过的坑也不少。我把有代表性的问题整理成三张速查表,按数据、训练、部署三个阶段分类,这些都是实际出现过并解决掉的问题,你可以直接对照排查。

5.1 数据阶段的典型问题

问题现象常见原因解决办法
某个类别识别率特别低该类别样本量太少,或者标注标准不统一优先扩充该类别的数据,统一并复查标注边界
训练精度高但实拍效果差训练数据和现场数据分布不一致回看采集规范,补充现场光照和背景的图片
小目标几乎检不出来标注框太小、小目标样本不足专门采集包含小目标的图片,适当做尺度增强
两个相似类别互相混淆标注时类别边界定义不清重新审视标注规则,尤其要统一边界案例的处理方式

5.2 训练阶段的疑难杂症

训练 loss 一直不降是最让人头疼的问题之一。排查方向按优先级排:先确认标注文件和图片是否对得上,很多人花半天时间发现是目录路径写错了;再检查学习率是否过小,YOLOv7 默认 lr0 在 0.01 左右,如果改成 0.001 会让训练极慢;最后看是否存在标签格式错误,比如 class 编号越界,这类错误会让 loss 看起来异常但又不完全崩溃。

还有一种情况是验证集 mAP 很高,一上真实场景就露馅。这多半是数据划分泄漏,或者现场环境与训练集差异太大。我们后来养成了一个习惯,训练完不用验证集图,而是直接用手机在分选线拍一批新图片丢进模型里跑,这叫“现场冒烟测试”,效果比任何指标都真实。

5.3 部署阶段的问题速查表

问题现象常见原因解决办法
engine 加载很慢每次启动都重新构建引擎用 trtexec 离线构建并保存为 .engine 文件
TensorRT 报版本不兼容JetPack 与 CUDA/tensorrt 版本不匹配检查发布说明,使用推荐版本组合
推理结果偶尔为空NMS 阈值设置过高或目标过小调低 conf_thres,检查输入预处理是否正确
内存持续增长推理循环中反复分配内存启动时一次性分配缓冲区,循环中复用
摄像头视频流卡顿解码线程和推理线程互相阻塞改为生产者-消费者模式,用队列解耦

我自己在实际操作中最深的体会是:这个项目的每一步都会默默影响最终结果,数据标注看起来最枯燥,却是最值得花时间的地方;模型优化要沉住气按“先提精度、再压速度”的顺序来,跳过任何一步后面都要加倍补回来。最后再分享一个实用小技巧,模型部署完成后,别急着把所有测试都交给程序跑,先从现场拍它几百张不同光线、不同角度的真实照片,人工看图排查一遍,往往能比测试脚本更早发现问题。把这一步做扎实,剩下的交给模型就好。

返回列表