简介:面向风机叶片智能运维场景的YOLOv8检测源码包,聚焦风力发电机叶片裂纹与异物识别,适合深度学习开发者、风电设备维护人员及计算机视觉学习者使用。资源覆盖数据标注、模型训练、UI集成的完整流程,内含训练与测试脚本、模型权重、数据集配置文件、界面相关代码及说明文档,可直接复现检测流程或进行二次开发。压缩包共17个文件,以Python脚本、样例图片、配置及说明为主,总大小5.76MB,结构紧凑,目前已有128人学习。借助该源码可快速建立基于YOLOv8的叶片检测原型,了解从数据集目录构建到训练脚本编写、再到可视化界面测试的完整工程思路。轻量级模型便于在普通计算设备上运行,结合数据增强与多尺度检测可进一步提升精度,为风电设备智能运维提供实用参考。
1. YOLOv8风机叶片检测:这个源码能替你扛下最费眼的巡检活
风机叶片检测这几年几乎是无人机巡检的标配需求:飞一圈回来几千张照片,靠人眼找裂纹、砂眼、剥落和雷击痕迹,看得快容易漏,看得慢真的会看吐。YOLOv8风机叶片检测要做的事,就是把“叶片在哪”和“叶片有没有缺陷”这两件事交给模型:输入无人机航拍或地面机位的照片,输出叶片目标框与缺陷类别。这个方向适合两类人:一类是刚拿到巡检数据、打算从零搭检测流程的算法工程师;另一类是已经在用旧版目标检测模型、被漏检和误报折腾够了的开发者。先说结论,这个源码工程解决的核心问题不是训练一个模型,而是让模型在现场数据上稳定可用——这两者之间隔着一整条数据、标注、训练和部署的链路。
2. 先把任务定义清楚:叶片检测到底是“找叶片”还是“找缺陷”
2.1 两种任务边界与模型输出差异:目标框、分割掩码与缺陷分类
“风机叶片检测”这个名字在源码工程里至少对应两种完全不同的任务,拿到代码第一件事不是跑训练,而是看它输出什么。第一种是叶片定位任务:输入任意一帧巡检图,输出风机叶片的外接矩形框,类别只有“叶片”一类,解决的是“图里有没有叶片、叶片在哪个位置”,一般作为后续裁剪分析的前置步骤。第二种是缺陷检测任务:在叶片区域内识别裂纹、剥落、腐蚀、雷击烧伤等缺陷,类别有多个,目标是小目标,困难程度比定位高一个量级。很多源码工程会把两者合在一条流水线里:先定位叶片,再从叶片框里找缺陷。如果工程里只有一个模型且只有一个类别,那它大概率只做了定位,缺陷部分需要你自己补。
另一个判断点是输出头。YOLOv8默认的检测头是 anchor-free 的解耦头,输出的是目标框;如果源码里用的是 YOLOv8-seg,那输出的是像素级掩码。叶片是细长目标,水平框天然会包入背景,如果下游要做缺陷面积估算或边缘形态分析,分割掩码比目标框好用得多。但如果只是做“是否有缺陷、缺陷在哪一块”的筛查,目标框足够,分割反而会在标注环节消耗大量时间。我建议按你手上的标注资源来决定:只有几百张图、目标是快速上线,用检测头;有专业标注团队且需要精确面积,再上分割。
2.2 采集与筛选叶片影像:分辨率、角度与负样本是三个硬指标
数据质量决定模型上限,这个方向尤其明显。常见的做法是以无人机航拍数据为主,飞行高度一般在 50 到 120 米,叶片在画面中的形态差异极大:有的是完整三叶从侧面看过去,有的是一根长条斜穿画面,有的则是俯拍下叶片在画面里只占很小一块。数据采集阶段我会按以下表格做质检筛选,不达标直接剔除,宁可数量少也不要脏数据:
| 质检项 | 合格线 | 不合格的典型表现 | 处理方式 |
|---|---|---|---|
| 图像分辨率 | 不低于 1920×1080 | 低于 1080P 时叶片边缘模糊 | 删掉或做超分后再入训练集 |
| 叶片主体宽度 | 不小于 40 像素 | 叶片太细,特征不足 | 滑窗切图放大 |
| 叶片完整度 | 多数样本可见长度超过 60% | 叶片被机身、云雾大面积遮挡 | 按遮挡程度单独建类别 |
| 背景多样性 | 至少含 3 类背景 | 全是蓝天,现场全是海面 | 补采海面、云层、山体、地面纹理 |
| 叶片朝向 | 每个角度区间都有样本 | 只有侧面视角,俯拍几乎没有 | 按相机位姿补采 |
正负样本的比例也值得提前定好。负样本指的是没有叶片但有类似纹理的图,比如海面波纹、云层、雪地、草地纹理,这些是误检的主要来源。我一般会把负样本控制在训练集的 15% 到 30%,太少模型分不清背景与叶片边缘,太多会把召回率压下来。如果现场数据只有风机正常运行的侧视角,务必补一些叶片变桨、停机、背光条件下的影像,否则模型很容易在变桨角度上翻车。
2.3 标注规范:细长叶片的长宽比、遮挡与边缘框的约定
标注质量在叶片这类长条目标上影响极大,而且翻车方式很隐蔽。叶片宽度在画面里可能只有 20 像素,长度却占画面的 70%,水平框的长宽比经常超过 1:10,标注人员如果手一抖,框的边界切在叶片内部,训练时模型就会学到“叶片带毛刺”的错误特征。我在项目中定过几条标注约束,实测能显著减少训练时的奇怪损失波动:
- 框的上下边界贴近叶片外缘,但不要求像素级贴合;左右边界包住叶尖和叶根,叶尖被云遮挡时框到可见部分为止,不要外推到推测位置。
- 遮挡处理要统一:叶尖被另一叶片挡住时,要么全部按完整叶片标,要么全部按可见部分标,混着标会让模型在推理时输出不稳定的框。
- 多个叶片重叠时,每个可见的叶片都要单独标框,不能用一个框包住多个叶片。
- 云、雾、海面反光造成的半透明叶片,只要人眼能分辨轮廓就标,否则不标。
标注完第一版,我会跑一个脚本统计标签分布,重点看长宽比分布和坐标是否越界。YOLO 标签格式是 class x_center y_center width height,所有值都是相对图片尺寸归一化的 0 到 1 小数,一旦出现坐标大于 1 或宽高为 0,训练时 OpenCV 读图加归一化后会直接产出 NaN 损失。统计脚本按下面这个写法来:
import os from glob import glob label_dir = "labels/train" cls_names = {0: "blade", 1: "crack"} # 按实际工程修改 ratio_gt5 = 0 total_boxes = 0 bad_files = [] for txt_path in glob(os.path.join(label_dir, "*.txt")): with open(txt_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: bad_files.append((txt_path, "字段数不足")) continue cls_id, cx, cy, w, h = map(float, parts) if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1): bad_files.append((txt_path, f"坐标越界: {line}")) if w <= 0 or h <= 0: bad_files.append((txt_path, f"零面积框: {line}")) total_boxes += 1 if max(w, h) / min(w, h) > 5: ratio_gt5 += 1 print(f"总框数: {total_boxes}, 长宽比>5占比: {ratio_gt5 / max(total_boxes, 1):.2%}") print(f"异常文件数: {len(bad_files)}") for item in bad_files[:10]: print(item)这个脚本里统计“长宽比大于 5 的框占比”是一个有效自检指标。叶片场景这个比例通常在 60% 以上,如果比这个低很多,说明标注框选得过大或过小。异常文件打印前 10 条就够,改完标签再跑一遍,直到异常清零再开训练。这一步能省掉后面大量“loss 不降”的排查时间。
3. 用源码把第一个模型跑起来:最小训练链路
3.1 环境准备与数据目录组织:YOLO 标签格式与文件摆放
拿到源码工程后不要急着执行训练脚本,先把依赖环境和数据集目录理清楚。YOLOv8 系列工程基本都建立在 ultralytics 之上,安装依赖只需要一条命令,重点在于数据目录的组织要和工程的默认路径一致。常见做法是建一个datasets/blade/目录,下面分images/train、images/val、labels/train、labels/val四个文件夹,图片和同名 txt 标签文件一一对应。这里有个容易被忽略的坑:标签文件的主文件名必须与图片完全一致,哪怕后缀不同,前缀名错了模型也能训练但评估结果会虚高。
# 创建数据集目录结构 mkdir -p datasets/blade/{images/{train,val},labels/{train,val}} # 安装 ultralytics 与基础依赖 pip install ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 按实际 CUDA 版本调整目录创建和依赖安装是两条命令,但背后的逻辑要清楚:pip install ultralytics会同时装好模型定义、训练器、评估器和导出工具,不需要自己写 YOLO 网络结构;torch的安装渠道根据你的显卡驱动决定,NVIDIA 显卡用 CUDA 版本,纯 CPU 机器可以直接pip install torch。我建议装完后跑一句python -c "import torch; print(torch.cuda.is_available())",输出 True 再继续,False 的话训练会慢几十倍,先解决驱动问题再说。
3.2 写 data.yaml 与最小训练命令:从预训练权重开始迁移学习
数据放好后,写一个blade.yaml告诉训练器数据集在哪、有几个类别。这里要特别注意path字段的写法:在 ultralytics 中,path是数据集根目录,train和val是相对路径。路径写错时训练不会立刻报错,而是会一路下载或找不到数据,最后报空数据集错误。
# blade.yaml path: ./datasets/blade # 数据集根目录,相对当前终端路径 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 names: 0: blade 1: crack 2: peel 3: lightning然后执行最小训练命令。模型我一般先用 YOLOv8s 起步,叶片是大目标,s 级足够,m 级留给缺陷检测阶段:
yolo detect train \ model=yolov8s.pt \ data=blade.yaml \ imgsz=1280 \ epochs=100 \ batch=8 \ device=0 \ cache=True这里每个参数都有讲究。model=yolov8s.pt是官方预训练权重,首次运行会自动下载,加载后输出层会根据你的类别数自动重建,这个过程叫迁移学习,能用 COCO 上学会的通用特征帮你的小数据集快速收敛。imgsz=1280是这次训练里最关键的参数:默认 640 对叶片这种细长目标太粗暴,叶片宽度在 640 分辨率下往往只有十几个像素,特征已经被压缩没了;提到 1280 后叶片边缘纹理能保留下来。代价是显存占用增加,batch=8在 12G 显存的卡上比较稳,24G 可以试batch=16。cache=True会把图片预加载进内存,小数据集训练速度提升明显,但内存不足时反而会拖垮机器,数据量超过 1 万张时建议去掉这个参数。
训练开始后关注两个文件:runs/detect/train/weights/best.pt和last.pt。best 是按验证集指标保存的最优权重,last 是最后一个 epoch 的权重,两者不冲突。如果训练中途机器断电或者你觉得某次增强参数导致后期掉点,last.pt 就是后悔药。训练日志里每行会打印 P、R、mAP50、mAP50-95,叶片场景我主要盯 recall 和 mAP50-95,前者决定漏检率,后者对叶片这种长条目标的框质量更敏感。
3.3 第一次评估:用 val 和 predict 看失败样例而不是只看指标
训练完第一版,先跑验证再跑可视化推理,顺序不能反。验证命令如下:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=blade.yaml \ imgsz=1280 \ conf=0.25 \ iou=0.5 \ plots=Trueconf=0.25是置信度阈值,iou=0.5是评估时的 IoU 匹配阈值,plots=True会在输出目录生成混淆矩阵、PR 曲线和一批带预测框的样例图。这些图比 mAP 数字有用得多——你会直观看到哪些叶片被切成两段、哪些误检来自云层、哪些缺陷完全没被检出。跑完后对验证集再做一次批量推理,把预测框叠加到原图上逐张翻:
yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=datasets/blade/images/val \ imgsz=1280 \ conf=0.25 \ save=True第一次评估我建议把 conf 调低到 0.15 跑一遍,专门看在低阈值下模型输出了什么。低阈值下大量出现的假框就是模型的“隐藏困惑”:它可能对背景纹理响应很高,只是被默认阈值挡住了。这些失败样本是下一轮数据补充和参数调整的直接依据,比对着 mAP 数字猜有效得多。
4. 风机叶片优化的三个核心参数与训练策略
4.1 输入分辨率与叶片尺度的匹配:imgsz=1280 与滑窗推理
叶片检测里最立竿见影的优化不是换更大的模型,而是提高输入分辨率。同一个模型在 imgsz=640 和 imgsz=1280 下推理,mAP50 可能差 8 到 15 个点,这不是模型变强了,而是叶片在输入图上占的像素量变多了。叶片这种目标的长边经常占满整幅画面,短边却只有画面宽度的 3% 到 5%,在 640 分辨率下短边只有十几个像素,卷积下采样几层之后特征图上的响应非常弱。传到 1280 后,叶片短边大约有 30 到 40 像素,刚好够提取纹理特征。
但把整图缩放到 1280 是有上限的。无人机拍的原图普遍是 4000×3000 甚至更高,直接缩到 1280 会把叶片的长边信息丢掉大半。更稳的做法是滑窗推理:把原图按 1280×1280 的窗口、960 步长切块,每块独立输入模型,最后把所有窗口的预测框拼回原图坐标,再做一次全局 NMS 合并重叠框。这样既保证了输入分辨率不浪费,又让叶片在窗口内保持足够像素。参考实现:
import cv2 import torch from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") def slide_infer(image_path, win_size=1280, stride=960): img = cv2.imread(image_path) h, w = img.shape[:2] all_boxes = [] for y in range(0, max(h - win_size, 0) + 1, stride): for x in range(0, max(w - win_size, 0) + 1, stride): x2, y2 = min(x + win_size, w), min(y + win_size, h) crop = img[y:y2, x:x2] res = model.predict(crop, imgsz=win_size, conf=0.25, verbose=False)[0] for box in res.boxes: bx1, by1, bx2, by2 = box.xyxy[0].tolist() all_boxes.append([bx1 + x, by1 + y, bx2 + x, by2 + y, float(box.conf), int(box.cls)]) return all_boxes这段代码的关键在于循环边界:range(0, max(h - win_size, 0) + 1, stride)保证最后一个窗口能覆盖到图像右边缘和下边缘,否则叶片横跨窗口边界时就会在拼接处漏检。stride=960小于win_size=1280,相邻窗口有 320 像素重叠,重叠区域的重复框要在后续用 IoU 高于 0.5 的 NMS 合并。实际部署时滑窗数量多,每张 4000×3000 的图会产生约 12 个窗口,推理耗时是单图缩放的 10 倍以上,但对离线巡检分析来说可接受。如果要在无人机机载端侧跑,建议先把模型导出为 TensorRT 再配合滑窗,否则帧率撑不住。
4.2 数据增强在叶片场景的开关:Mosaic、翻转与尺度扰动
数据增强是叶片场景里最容易被照搬默认值坑到的地方。ultralytics 默认开启 Mosaic、随机旋转、随机翻转和颜色扰动,这些在通用目标检测数据集上是好东西,但放到叶片上要逐个开关检查。Mosaic 会把四张图拼在一起训练,问题在于叶片是大目标,拼接后叶片经常被切到边缘,模型学到的是半截叶片的响应,推理时反而对完整叶片不敏感。我建议直接关掉 Mosaic,或者保留但在最后 10 个 epoch 用 close_mosaic 参数强制关闭,让模型在最后阶段专注于完整目标。旋转增强也要慎重:叶片的倾斜角度本身已经丰富,额外旋转 30 度以上会让水平框的背景占比剧烈变化,训练信号变得很脏。
一个针对叶片场景实测有效的增强配置是这样:
yolo detect train \ model=yolov8m.pt \ data=blade.yaml \ imgsz=1280 \ epochs=150 \ batch=8 \ device=0 \ mosaic=0.0 \ close_mosaic=10 \ degrees=0.0 \ translate=0.1 \ scale=0.3 \ fliplr=0.5 \ hsv_h=0.015 \ hsv_s=0.5 \ hsv_v=0.3逐个解释这些参数:mosaic=0.0直接关闭拼接,避免半截叶片污染;degrees=0.0关闭旋转,因为叶片数据里各种角度已经很齐全,再转容易把标志性纹理扭曲;translate=0.1允许图片平移 10%,相当于让目标在画面里位置多样化;scale=0.3做 30% 的尺度扰动,模拟无人机不同飞行高度下的目标大小差异;fliplr=0.5保留水平翻转,左右翻转不会改变叶片的语义类别,但对海天背景的对称纹理有很好的对抗作用;hsv_h/s/v三个颜色扰动参数里,重点在hsv_v=0.3,亮度扰动可以帮助模型适应背光和低照度巡检时段。这里有一个容易忽略的细节:如果你在推理前对图像做了预处理,比如调整对比度或加滤波器,训练时也要保持同样的预处理,否则训练和推理的输入分布不一致,性能下滑得莫名其妙。
4.3 缺陷类别少怎么办:两阶段裁切训练与正负样本配比
当缺陷类别加入训练后,大部分团队会撞到同一个问题:缺陷样本太少。裂纹、剥落这类缺陷在一张 4000 像素的原图里往往只占几十个像素,标注框小、数量少,和叶片框的数量比可能到 1:100。直接用单模型同时检测叶片的缺陷,模型会把绝大多数注意力放在叶片框上,缺陷类别的召回率惨不忍睹。我一般用两阶段方案解决:第一阶段用“叶片”单类模型把叶片区域检测出来并裁切保存;第二阶段在裁切后的叶片图上标注并训练缺陷检测模型。这样缺陷在输入图中占的比例提升了 5 到 10 倍,模型学起来轻松得多。
裁切阶段的做法是先用第一阶段的 best.pt 跑一遍训练集,把所有叶片框裁出来并保存为独立小图,代码示意如下:
import cv2 import os from glob import glob from ultralytics import YOLO blade_model = YOLO("runs/detect/blade_best.pt") src_dir = "datasets/blade/images/train" out_dir = "datasets/blade_crops/train" os.makedirs(out_dir, exist_ok=True) for img_path in glob(os.path.join(src_dir, "*.jpg")): img = cv2.imread(img_path) res = blade_model.predict(img, imgsz=1280, conf=0.5, verbose=False)[0] base = os.path.splitext(os.path.basename(img_path))[0] for i, box in enumerate(res.boxes.xyxy.cpu().numpy()): x1, y1, x2, y2 = [int(v) for v in box] pad = 20 # 向外扩边,避免缺陷在切图边缘被截断 crop = img[max(y1 - pad, 0):y2 + pad, max(x1 - pad, 0):x2 + pad] cv2.imwrite(os.path.join(out_dir, f"{base}_{i}.jpg"), crop)裁切时的pad=20很重要,缺陷可能紧贴叶片框边缘,如果不扩边,裁出来的图会直接截掉缺陷。把裁切后的图送到标注平台重新标注缺陷类别,第二阶段训练时 imgsz 依然用 1280,但此时 1280 对应的是叶片局部,缺陷的像素占比和原图场景完全不是一个量级。验证阶段要严格走同一条流水线:先用叶片模型检测出叶片框,裁切,再用缺陷模型检测,不能直接把测试集裁好送去评估,那会高估真实效果。这套方案的问题在于误差会累积,叶片框如果漏检了,缺陷模型再准也没用,所以第一阶段模型的 recall 必须优先保证。
5. 避坑:风机叶片检测的 5 个高频翻车现场与排查
5.1 完整叶片被拆成两半,mAP 却不低
现象:可视化推理结果时,同一个叶片被两个预测框各框住一半,两个框都过了置信度阈值,叶片看起来像是被拦腰截断。更迷惑的是 mAP50 还保持在 0.8 以上,不深入看可视化根本发现不了。
原因:叶片长宽比极端,模型在预测时对长条目标产生双峰响应——叶尖和叶根的特征都足够强,中间连接区域的特征相对弱,于是输出两个互不重叠的框。NMS 按 IoU 合并框,两个框重叠区域太小,不会被合并。评估指标里一个框如果保住了主区域就计入正样本,所以拆框问题在指标上不敏感。
解决:先把 imgsz 提到 1280 以上,让叶片中部特征不丢失;然后把 conf 阈值从 0.5 降到 0.2 看失败样例,如果拆框现象大量减少就说明是特征响应弱导致的,继续提分辨率;如果拆框依然存在,检查标注框是否都覆盖了完整叶片,标注时框如果只标到 60% 的叶片长度,模型自然会学到“半个叶片”。最保险的方案是给后处理加一条约束:同一类别的多个框如果长边方向近似且端点距离小于框长的 30%,合并为一个框。
5.2 白色叶片与海面背景融为一体,误检漏检一起炸
现象:白色风机叶片在蓝天背景下检测得很好,一旦背景换成海面、雪地或雾天,叶片框明显减少,同时海面波纹被大量误检成叶片。现场单帧误报能到几十个。
原因:白色叶片和海面云层在 RGB 颜色空间上的统计特征高度接近,模型学到的区分信号主要是边缘纹理而不是颜色。海面波浪的纹理在某些尺度下和叶片边缘很像,加上训练数据里海面背景占比不足,模型对这类背景几乎零抗性。
解决:负样本增强是最直接的手段——收集大量海面、云层、雪地、沙漠纹理图片放入训练集,并保证负样本中不包含任何风机。症状严重时可以采用 copy-paste 式合成数据:把标注好的叶片随机粘贴到海面背景图上,同时粘贴对应的标签框,做几百张合成图就能明显压低误检。推理端可以加一个简单预处理:把输入图转成灰度再调高对比度,但这要和训练时的预处理保持严格一致,否则就成了玄学调参。最稳的还是数据层面补充负样本。
5.3 训练 loss 不降反升:先查标签再调学习率
现象:训练到第 20 轮 loss 还在高位,甚至出现 NaN,验证集的 mAP 一直为 0。很多人这时候开始调学习率、换优化器,折腾半天毫无起色。
原因:绝大多数情况是标签数据出问题。标注软件导出格式错误、类别 id 超过了 data.yaml 里定义的类别数、框坐标有 1.2 这样的越界值,都会让模型在训练时产生错误的梯度信号。YOLO 框架对这类错误容忍度很低,但报错信息往往只显示在某一次迭代的 data loader。
解决:回到第 2.3 节的标签统计脚本,先检查每行标签的坐标是否都在 0 到 1 之间、宽高是否大于 0、类别 id 是否落在 names 定义范围内。然后把训练集图片和标签可视化叠加,逐张看标注框是否与图片内容对应。最后一个常见坑是图片和标签文件数量不一致,某张图没有对应的 txt,训练器会跳过这张图,但验证时候也会少一条数据,指标虚高。用find或 Python 统计两边文件数量做差集,全部对齐后再训练。
5.4 无人机俯拍小目标叶片漏检:滑窗与 TTA 哪个先试
现象:训练集里大多是侧视角叶片,模型在侧视图上表现不错,一旦换成无人机高空俯拍,叶片在画面中只占一小块,大量漏检。这本质是训练集和测试机的视角分布不一致。
原因:俯拍时叶片宽度在图像里可能只有 10 来个像素,且叶片与背景的对比度更低。模型在训练时没见过这个尺度下的叶片形态,泛化自然失败。
解决:优先做数据补采,这是根本方案。等不及补数据时,可以先用滑窗推理把俯拍图切块,让叶片在小图里占更大比例;同时打开推理时的 TTA,yolo predict时加一个augment=True,模型会对输入做多尺度推理再融合,通常能提高几个点的召回。但要明白,TTA 只是补救,推理时间是原来的 2 到 3 倍,且过大的尺度变化反而会引入误检。另一个有用的手段是把训练数据里的大图缩小后再加入训练集,让模型显式见过“小叶片”长什么样,比部署时硬调阈值靠谱。
5.5 导出部署掉点:训练指标高,ONNX 里漏一半
现象:训练阶段 mAP50 有 0.9,导出成 ONNX 或 TensorRT 后在现场数据上一测,漏检严重,而且看起来不像参数问题。
原因:训练和推理之间的预处理差异是第一大嫌疑。训练时图像缩放、归一化、颜色空间转换的流程与部署端不一致,比如训练走 BGR、部署端读图后没转 RGB,或者归一化除的常数不同,都会让模型输入分布完全改变。第二个嫌疑是导出时把 NMS 算子一并固化,固化后的 conf 和 IoU 阈值与推理脚本不一致,导致大量框被过滤。
解决:导出后第一件事不是直接上线,而是用同一张图分别跑 PyTorch 模型和导出的 ONNX 模型,对比输出框的坐标和置信度。坐标差异在几个像素内说明布局是对的,置信度差异大要检查预处理;框数量差异明显要检查 NMS 阈值。TensorRT 导出时还会遇到动态输入尺寸问题,如果只固定了 640×640 的输入,部署时传 1280 的图会直接报错或产生错误输出。我建议把导出模型的输入尺寸固定为训练用的 imgsz,并把 NMS 放在导出模型外部,用统一的后处理逻辑做推理,方便随时调整置信度阈值。
6. 把检测结果接进巡检链路:多帧投票与缺陷面积估算
模型训练好、能出框只是第一步,巡检系统真正需要的是一个能减少人工复核负担的决策输出。我常用的做法是加多帧确认机制:无人机巡检视频中同一片叶片会在连续多帧中反复出现,单帧误检是随机噪声,真正的缺陷会在连续帧中反复触发。只需要维护一个简单的轨迹表,记录每个检测框的跨帧命中次数,命中达到 3 帧才上报,就能把大部分偶发误检挡在人工复核之外。
def update_tracks(new_dets, tracks, iou_thres=0.1, max_hits=3): # new_dets: 当前帧检测框列表,每项为 [x1,y1,x2,y2,conf,cls] # tracks: 已有的跨帧轨迹,每项包含 box 和 hits for det in new_dets: best_idx = -1 best_iou = 0.0 for i, trk in enumerate(tracks): iou_val = compute_iou(det[:4], trk["box"]) # 标准IoU,略 if iou_val > best_iou: best_iou = iou_val best_idx = i if best_idx >= 0 and best_iou >= iou_thres: tracks[best_idx]["box"] = det[:4] tracks[best_idx]["hits"] += 1 else: tracks.append({"box": det[:4], "hits": 1}) return [trk for trk in tracks if trk["hits"] >= max_hits]这段代码里 IoU 阈值只用了 0.1,比检测评估用的 0.5 低很多,原因是无人机悬停时画面会有抖动,同一个叶片在相邻帧的框位移可能超过框面积的 50%,阈值太高会导致同一叶片被当成新目标反复建轨迹。hits 达到 3 就上报,能有效过滤海面反光、云层边缘这类单帧偶发误检。还有一个实用细节:叶片检测框的长宽比天然很大,轨迹里可以记录长宽比,上报时排除长宽比小于 2 的“类叶片框”,这能过滤掉很多与叶片位置无关的噪点。
关于缺陷面积估算,我一般把缺陷框面积除以叶片框面积得到一个相对值,供运维参考。经验上占比超过 0.5% 的缺陷建议安排详细检测,0.1% 到 0.5% 之间记录到台账持续观察,低于 0.1% 的暂不处理。这个比例只适合叶片在画面中占比稳定的场景,无人机距离叶片远近不同时,相同比例对应的真实面积差异很大,需要结合飞参里的相机高度和焦距换算才能得到毫米级的真实缺陷尺寸。我自己第一次做叶片检测项目时,就是只盯着单帧检测结果直接上报,结果一条巡检航线下来误报了几十条,运维直接不信任这个系统。后来把检测结果接入多帧投票和轨迹管理,误报降到个位数,才算真正可以用。这个后悔药,你提前吃了就好。希望这些参数和踩坑记录帮到你,少走一趟我走过的弯路。
本文还有配套的精品资源,点击获取