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 整体流程规划和时间分配项目整体拆成了九个步骤每个步骤都有明确的输出物和验收标准需求分析与场景调研输出缺陷定义文档和标注规则草案数据采集与清洗输出建好的图片库剔除模糊帧和重复帧数据标注与质检输出 YOLO 格式标注文件和类别分布统计数据集划分与增强输出训练集、验证集、测试集三者互不干扰模型训练与调参输出 baseline 权重和训练曲线模型优化与评估输出剪枝、量化后的权重和精度对比报告模型导出与格式转换输出 ONNX 和 TensorRT engine部署环境搭建输出推理服务程序和接口文档现场联调与迭代输出最终版本模型和问题清单这里我想重点强调一下时间分配。很多人以为模型训练和调参是大头实际上我们整个项目里数据相关工作占了大概 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.yamldata.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_locationcpu) model.eval() dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, model.onnx, opset_version12, 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 CPUONNX 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 \ --onnxmodel.onnx \ --saveEnginemodel.engine \ --fp16 \ --int8 \ --calibcalibration.cache \ --workspace4096engine 文件生成后可以先用 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_size640): 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, dtypenp.float32) canvas[0:nh, 0:nw] resized return canvas.transpose(2, 0, 1)[None] / 255.0这里要专门提醒一下 letterbox 的问题YOLOv7 训练时图像会先做 letterbox 填充到 640 再进网络推理时如果不用同样的 letterbox 逻辑模型看到的数据分布和训练时不一致精度会明显掉。填充值用 114RGB 灰值是 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检查输入预处理是否正确内存持续增长推理循环中反复分配内存启动时一次性分配缓冲区循环中复用摄像头视频流卡顿解码线程和推理线程互相阻塞改为生产者-消费者模式用队列解耦我自己在实际操作中最深的体会是这个项目的每一步都会默默影响最终结果数据标注看起来最枯燥却是最值得花时间的地方模型优化要沉住气按“先提精度、再压速度”的顺序来跳过任何一步后面都要加倍补回来。最后再分享一个实用小技巧模型部署完成后别急着把所有测试都交给程序跑先从现场拍它几百张不同光线、不同角度的真实照片人工看图排查一遍往往能比测试脚本更早发现问题。把这一步做扎实剩下的交给模型就好。
返回列表