做基础设施巡检类的视觉项目这几年越来越多了,最常见的需求就是:无人机飞一圈,把风力涡轮机、天线、烟囱、电力线这些高耸物体自动识别出来。这类目标不像车辆、行人那样常见,商用模型里没有现成可用的检测器,必须自己训练。我手头这套项目就是用 YOLOv8 从零构建了一套针对这四类目标的多目标检测识别系统,训练、推理、后处理代码都是完整可跑的。这篇文章把整体思路、数据集处理、训练调参、推理部署的完整链路记录下来,也把我踩过的坑一并交代清楚,给正在做类似基础设施检测需求的朋友一个可以直接参考的路线。
选择 YOLOv8 来做这件事,不需要太多纠结。这类细长结构目标在航拍视角下密集出现,尺度变化非常大,YOLOv8 的 anchor-free 设计对尺度适应性比老版本 YOLOv5 更好,而且训练配置、部署生态都成熟,拿到手改改就能用。四类目标各有各的麻烦:风力涡轮机叶片细长、天线和烟囱是典型的高瘦框、电力线干脆就是一条线状结构。做这种多尺度、多形态目标检测,靠的就是数据集质量、标注规范和后处理策略三板斧。整套项目跑下来,单张 1080P 航拍图在消费级 GPU 上能做到约 15ms 的推理耗时,mAP@0.5 稳定在 0.82 以上,基本满足巡检场景的实时性要求。
1. 项目背景与整体设计思路
1.1 为什么专门做这四类目标
风力涡轮机、天线、烟囱、电力线这四类物体有一个共同的身份标签:城市和工业环境里的高架基础设施。对无人机巡检、城市规划、电力公司资产管理、低空避障系统来说,准确识别这些物体是刚需。比如风电场的定期巡检需要确认叶片表面损伤,第一步就是先把风机主体从画面里框出来;城市管理部门需要摸清天线和烟囱的分布情况,人工看图根本看不过来;电力线的检测更是输电线路通道防外破的基础能力。
这类检测不像通用物体检测那样有大量的预训练权重可以直接迁移。COCO 数据集里有 person、car、dog,但没有 wind turbine,也没有 powerline。所以要解决这个需求,唯一的路就是自建数据集、自己标注、自己训练。这套项目把四类目标放到同一个模型里一起识别,对比分别训练四个单类模型,优势很明显:训练和维护成本低,推理时只需要一次前向传播,在边缘设备上的部署负担小。
1.2 四类目标的检测难点拆解
风力涡轮机这三字看着简单,真正标注过就知道有多别扭。叶片是细长条,塔筒是细长条,整个风机在 2D 图片上往往是一个不规则的对角线分布。如果严格框出叶片端点,框会占掉图片很大的面积,导致叶片本身只占框内非常小的像素比例,模型学到的大部分是背景。
天线和烟囱的形态相对规整,都是杆状物,但尺度跨越极大。近处低空飞行的无人机拍到的天线塔可能有整张图那么高,远处看到的只是一个像素点。这种极端的尺度变化,对特征金字塔网络的要求很高,训练时输入分辨率的选择会直接影响小目标召回率。
电力线是这里面最难搞的。一条导线在航拍 1080P 图像里经常只有 1~3 个像素宽,检测框几乎就是一条线段。模型很难从纹理特征上区分电线和道路标线、田埂、阴影边界这些干扰项,只能靠上下文信息来判断。这也是整个项目里误检率最高的一类。
1.3 项目技术选型的权衡
选 YOLOv8 之前我也对比过 Faster R-CNN 和 RetinaNet。Faster R-CNN 的 two-stage 结构理论上对小目标检测精度更高,但推理速度比 YOLO 系列慢一个量级,在无人机机载设备或边缘盒子上跑不动;RetinaNet 的 focal loss 是处理类别不平衡的利器,但工程生态远不如 YOLOv8 完善。
最终选择 YOLOv8,核心原因是它在精度和速度的平衡点上做到了最好。具体来说:
- 模型家族完整,从 YOLOv8n 到 YOLOv8x 覆盖了从边缘端到服务器的全场景;
- anchor-free 的检测头简化了锚框设计,对细长目标更友好;
- 官方仓库的训练、验证、导出脚本成熟,转 ONNX 和 TensorRT 都有现成接口;
- C2f 模块的梯度流设计让模型的表征能力比同参数量的 YOLOv5 更好。
注意:如果目标只有风力涡轮机这一类,也可以考虑直接用 YOLOv8 的官方预训练权重做微调,但四类混合场景必须从零训练,因为官方 COCO 权重中的底层特征分布和目标形态差异太大,迁移效果不好。
2. 数据集构建与标注策略
2.1 数据来源与采集路线
数据集是这个项目成败的七成因素。我的数据来源主要有三路。第一路是无人机航拍素材,这是主力,覆盖不同高度、不同角度、不同光照条件;第二路是公开的遥感图像切片,比如一些城市地理信息数据集里的建筑和塔架切片;第三路是网络图片中筛选出的正样本,通过关键词搜索人工筛掉低质量和重复内容。
采集时有一个原则必须坚持:一个场景要尽量拍到多个角度的目标。比如一座风力涡轮机,要收集正面、侧面、斜上方、逆光、顺光等不同视角的图像。只用一个视角训练出来的模型,换个角度立刻失分。
数据总量方面,我这套项目最终用了约 8500 张图像,其中风力涡轮机约 12000 个标注框,天线约 18000 个,烟囱约 10000 个,电力线约 16000 个。总量的绝对值不算大,但每一类的形态多样性做得比较足。
2.2 标注规范与细长目标框选技巧
标注工具我用的是 LabelImg 和 X-AnyLabeling 搭配,前者处理常规目标,后者处理电力线这类线状目标时可以借助半自动分割辅助。标注格式统一为 YOLO txt 格式,类别编号依次是 0:wind_turbine、1:antenna、2:chimney、3:powerline。
细长目标框选的规范是项目里最值得说的地方。我踩过一轮坑之后定下的规则是:对于风力涡轮机,框住整个叶片的旋转外接矩形投影,宁可框大一点也不要切到叶片;对于天线和烟囱,如果目标在地面有明确的基座,从基座顶部开始框,底部不需要包括基座本身;对于电力线,框的宽度取线宽的 2~3 倍像素,高度就是线束的实际宽度,框的长度以两座塔杆之间的完整线段为准,中间有断点就分成多个框。
这里有个参数化的细节可以分享:标注电力线时,很多人习惯把框画得特别窄,试图精确贴合线宽,结果模型学到的特征是线本身的颜色和粗细,遇到粗一点的电缆或细一点的钢丝就废了。我后来统一把电力线框的短边控制在 6~8 个像素左右,反而让模型对线状结构的语义理解更好。
2.3 数据增强方案与类别均衡处理
四类目标的数量天然不均衡,天线和电力线样本远多于风力涡轮机。如果直接训练,模型会偏向样本多的类别,风力涡轮机的召回率会明显偏低。我的处理思路分了两步。
第一步是离线增强,对风力涡轮机样本做 90 度整数倍旋转、水平翻转、小角度随机旋转,同时复制生成多尺度版本。第二步是在线增强,YOLOv8 自带的 mosaic 增强默认开启,值是 1.0,我用下来效果不错。此外给电力线类别加了轻微的 HSV 亮度抖动,因为电线的颜色在清晨和傍晚差异很大。
还有一个很关键的增强是随机裁剪缩放。航拍图像的分辨率很高,动辄 4000x3000,直接塞进模型输入尺寸会导致目标严重缩小。我在训练前做了分块切图,把大图切成 1024x1024 的窗口,窗口之间保持 10% 的重叠,避免目标正好被切在边界上。这个操作比任何增强策略都更能提升小目标检测效果。
3. 模型训练核心参数配置与调优实践
3.1 模型规模选择与输入分辨率确定
YOLOv8 的模型规格从 n、s、m、l 到 x,我的建议是起步用 m 作为基线。n 和 s 虽然速度快,但对电力线这种极细目标的表现很不稳定;l 和 x 精度高,可训练时间和部署成本都不是巡检类项目愿意承担的。实测下来 YOLOv8m 在精度和速度的平衡上是四类目标检测的最优选择。
输入分辨率我选的是 1280x1280。初始跑过 640x640,小目标召回率惨不忍睹,尤其是电力线几乎只靠运气捡到。换成 1280 之后,情况明显改善。别小看这个参数,它直接决定了模型能看到多小的目标。代价是训练显存占用和推理耗时同时上升,但巡检类应用更多离线处理或边缘盒子,这个代价值得。
训练超参数的配置我是这样设的:epochs 设 200 但开了早停,patience 设 30;batch size 在单张 24G 显存上设 8;初始学习率 0.01,最后降为 0.001,采用 cosine 退火策略;weight decay 用默认的 0.0005。优化器选择 SGD,实测 AdamW 在前期收敛更快但后期精度略逊于 SGD,所以回归到 SGD 了。
3.2 训练过程与损失曲线解读
训练日志里我最关注的指标是 box_loss 和 cls_loss 的曲线形态。正常的收敛过程应该是 box_loss 在前 30 个 epoch 快速下降,之后进入平台期;cls_loss 下降速度慢一些,但如果出现反复震荡抬升,多半是类别不均衡或标注噪声导致。
我这套项目训练过程中遇到过一个典型现象:电力线的 cls_loss 一直降不下去,稳定在较高水平。排查后发现是标注规范不一致导致的,早期标注的电力线框和后期标注的电力线框在宽高比上差距太大,模型在同一个类别里学到了两种不同的形态特征。重新统一标注标准之后,这个损失才开始正常下降。
训练时还需要定期验证集的 PR 曲线。YOLOv8 训练完后自动生成的 results.png 里有混淆矩阵,四个类别中天线和烟囱容易互相混淆,它们的形态太像了。遇到这种情况就往训练集里补充一些天线和烟囱同时出现在同一画面中的样本,让模型学到它们的上下文差异。
3.3 提升小目标召回率的关键操作:切片推理
训练结束后的第一版模型,mAP@0.5 有 0.78,但把电力线的单独 AP 拎出来看只有 0.52,明显拉胯。我做的第一个优化不是换模型,而是引入切片推理策略。
所谓切片推理,就是推理时不再把整张大图直接送进模型,而是把大图切成若干有重叠的小块,逐块送入模型检测,最后再把结果合并回原图坐标。这个策略对航拍大图特别有效。切块尺寸我定为 960,重叠率 20%,切完的块在原图上的坐标通过简单的几何换算映射回去,NMS 在合并后的所有框上执行,干掉了重复检测。
实际效果令人满意:电力线的 AP 从 0.52 提升到 0.71,风力涡轮机也有 4 个点的涨幅。代价是推理时间由 15ms 涨到了 80ms 左右,毕竟切 4 块就是 4 次前向。如果是实时性要求高的场景,可以用 TensorRT 优化或者减少切块数量来权衡。
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") def slice_inference(image_path, slice_size=960, overlap_ratio=0.2): orig_img = cv2.imread(image_path) H, W = orig_img.shape[:2] stride = int(slice_size * (1 - overlap_ratio)) all_boxes = [] all_scores = [] all_class_ids = [] for y in range(0, H, stride): for x in range(0, W, stride): x_end = min(x + slice_size, W) y_end = min(y + slice_size, H) crop = orig_img[y:y_end, x:x_end] results = model.predict(crop, imgsz=slice_size, conf=0.25, verbose=False)[0] for box in results.boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() score = box.conf[0].cpu().numpy() cls_id = int(box.cls[0].cpu().numpy()) # 还原原图坐标 x1 += x y1 += y x2 += x y2 += y all_boxes.append([x1, y1, x2, y2]) all_scores.append(score) all_class_ids.append(cls_id) # 合并后的 NMS final_indices = cv2.dnn.NMSBoxes( all_boxes, all_scores, score_threshold=0.25, nms_threshold=0.45 ) return all_boxes, all_scores, all_class_ids, final_indices3.4 后处理阶段的参数微调
检测输出的后处理参数对最终效果的影响往往被人忽略。默认的 confidence 阈值 0.25 和 NMS IoU 阈值 0.45 在通用目标上够用,但在这四类目标上需要微调。
电力线因为框特别扁长,相邻框之间的 IoU 计算很容易因为面积的极值特性产生偏差,我最后把电力线类别的 NMS 阈值单独调到 0.55,减少相邻线段框被误删的情况。风力涡轮机叶片框之间如果有重叠,通常来自同一个风机,阈值调到 0.4 反而能压制重复框。
置信度阈值方面,巡检系统中的漏检比误检更致命,所以整体阈值我取的是 0.2,宁可多出几个误检框由下游人工再筛一轮,也不能漏掉目标。这个取舍只适用于巡检场景,如果做的是自动避障部署,建议把阈值拉回 0.4 以上,安全第一。
4. 推理部署与效果实测
4.1 推理代码与视频流支持
训练好的模型在 ultralytics 框架下推理非常方便。如果是批量处理航拍照片,用 predict 方法传入目录即可;如果是实时视频流,用 detect 模式接摄像头或视频文件。下面是我在项目里实际跑的一段多目标检测推理代码,兼容图片目录和视频流两种输入。
from ultralytics import YOLO # 加载训练好的权重 model = YOLO("runs/detect/train/weights/best.pt") # 图片目录推理 results = model.predict( source="test_images/", imgsz=1280, conf=0.2, iou=0.45, save=True, save_txt=True, project="inference_output", name="test_run", classes=[0, 1, 2, 3], # wind_turbine, antenna, chimney, powerline ) # 视频流推理 model.predict( source="drone_stream.mp4", imgsz=1280, conf=0.2, iou=0.45, save=True, stream=True, classes=[0, 1, 2, 3], )推理输出除了保存可视化图片之外,我还会额外落一份 JSON 格式的检测结果,包含每个目标的类别、置信度、归一化坐标。坐标在后续做地理配准、GIS 叠加分析时直接用得上,这一步在巡检类项目里非常关键,省掉了重复解析标注文件的麻烦。
4.2 模型压缩与边缘端部署方案
模型训练完成后,我把它导成了 ONNX 格式,再用 ONNX Runtime 跑了一下 CPU 上的推理速度。在普通 i7 处理器上,单张 640x640 输入大概是 25ms,已经可以接受;如果进一步用 TensorRT 做 FP16 量化,在 RTX 3060 上可以做到 6ms 单帧。
导出 ONNX 的命令很简单,训练完权重直接在 ultralytics 环境中执行:
yolo export model=runs/detect/train/weights/best.pt format=onnx dynamic=True imgsz=1280这里 dynamic=True 是因为航拍图像的尺寸不固定,动态输入维度在部署时更灵活。需要注意的是,导出的 ONNX 模型如果不指定 opset 版本,某些边缘推理框架可能不兼容。我通常显式指定 opset=12,兼容性最好。
TensorRT 部署我没在这篇文章里展开,但提醒一句:如果模型里用了语义分割分支或自定义层,导出之后需要仔细检查每个 TensorRT layer 是否被正确映射,否则推理结果可能会出现轻微偏差。对于纯检测权重,一般不会有这个问题。
4.3 实测效果与性能数据
为了验证模型的实际泛化能力,我在训练时从整体数据集中按场景分出了约 8% 的独立测试集,训练时完全不看这些图。最终测试数据如下:
| 目标类别 | 训练集框数 | AP@0.5 | AP@0.75 | 误检率 |
|---|---|---|---|---|
| 风力涡轮机 | 12000 | 0.91 | 0.74 | 3.2% |
| 天线 | 18000 | 0.87 | 0.66 | 5.1% |
| 烟囱 | 10000 | 0.84 | 0.61 | 6.4% |
| 电力线 | 16000 | 0.71 | 0.45 | 9.8% |
| 整体 | 56000 | 0.83 | 0.62 | 6.1% |
整体 mAP@0.5 是 0.83,在四类目标混合检测任务里算是可用的水平。风力涡轮机的表现最好,形态特征独特,背景干扰少;电力线依然是最大短板,误检率接近 10%,主要集中在复杂地形背景下的伪线状目标。这也是这类线状目标检测的行业通病,短期内靠单模型很难彻底解决。
5. 常见问题与排查技巧实录
5.1 模型训练不收敛的常见原因
训练时最容易踩的第一个坑是学习率设置不当。YOLOv8 默认的 lr0=0.01 是从官方检测任务上迁移来的经验值,在自定义数据集上并不一定适用。如果你的数据集只有几千张且类别分布不均,初始学习率 0.01 很容易导致损失函数发散。我遇到过 loss 曲线一度下降但突然飙升到 Nan 的情况,排查到最后发现是学习率过大加上 batch size 太小引起的梯度爆炸。
解决方法是把 lr0 降到 0.005,同时配合 warmup 让模型在前 3 个 epoch 内缓慢升高学习率。如果显存允许,把 batch size 提到 16 以上,梯度的稳定性会明显改善。
另一个不收敛的原因是标注文件格式错误。YOLO 格式的标注要求类别编号从 0 开始、坐标需要归一化到 0~1 之间。很多工具导出的坐标是像素值,直接训练会报错或者出现损失值异常。训练前一定要做一个数据检查脚本,把所有标注文件读一遍,过滤掉坐标越界、宽高为 0 的无效框。我在这上面吃过亏,整整一个晚上的训练全白费了。
5.2 漏检和误检的针对性排查
漏检多发在两类目标上:极远处的小尺寸大目标,和极近处的大尺寸大目标。前者的原因是输入分辨率不够,解决方法是提升 imgsz、用切片推理;后者的原因是目标框超出图像边界过大,模型没见过框被大面积截断的样本。对策是在训练数据里加入大量目标贴着图像边界或超出边界的样本。
误检多发在电力线上。我遇到的典型情况是把高压塔架的钢架结构边缘误检成电力线,或者把长条形的白色屋顶边缘当成电力线。排查这类误检不能只看模型,回去翻训练集,补充电力线的负样本前景区域剪裁非常关键。我专门截取了大量包含塔架但不包含导线的图像区域,标注为 ignore 区域,配合 YOLOv8 的 drop 参数或者直接删掉这些框不参与训练,误检率才逐步压下来。
还有一个容易忽略的坑是图片的 EXIF 方向信息。无人机或者手机拍摄的图片有时会带有旋转方向标记,OpenCV 的 imread 不会解析这个信息,导致图片被横着或倒着读入,标注坐标意义完全错位。处理方式是在数据预处理阶段用 imutils 或 PIL 主动检查 EXIF 方向并重排像素,再做标注匹配。
5.3 四类场景下的性能调优速查表
上面的经验整理成一张速查表,方便快速定位问题:
| 现象 | 优先排查方向 | 推荐调整策略 |
|---|---|---|
| 损失曲线震荡不下降 | 学习率过高、标注有噪声 | 降低 lr0、检查标注文件、清洗误标数据 |
| 小目标(电力线等)漏检 | 输入分辨率不足、特征层融合不够 | 提升 imgsz 到 1280、启用切片推理 |
| 某两类目标互相混淆 | 类间特征相似、训练数据不均衡 | 补充两者共现场景样本、调整类别权重 |
| 误检框太多 | 置信度阈值过低、负样本缺乏 | 调高 conf、添加难负样本训练 |
| 部署速度不达标 | 模型规格过大、未做加速优化 | 换更小规格模型、TensorRT FP16 量化 |
| 预测结果有系统性偏移 | 标注框中心点系统性偏差 | 核查标注工具导出坐标、检查数据集划分 |
6. 项目扩展方向与持续改进思路
模型跑通了不代表项目结束了。在实际落地过程中,我持续做了一些扩展性的工作,收获不小。最实用的是把检测结果和地理坐标做了一个简单的关联映射。无人机航拍图本身带 GPS 信息,通过检测框的中心点结合相机姿态参数,可以把每个目标的地理坐标粗估出来,叠加到 GIS 地图上做资产管理。这个方向如果继续做深,就是遥感智慧化巡检的雏形了,价值比单纯出检测框大成百上千倍。
另一个值得尝试的方向是把电力线检测从 2D 检测升级到线状目标提取。YOLOv8 分割系列模型可以输出每个目标的掩膜,对电力线来说,掩膜比检测框的信息含量高得多,后续可以做弧垂分析,为输电线路安全评估提供量化依据。我把检测分支和分割分支的模型各跑过一轮对比,分割模型在电力线上的 AP 表现稍低,但输出信息的实用性远高于检测框。
还有一个亲测有效的思路是模型迭代的主动学习策略。用当前模型对新采集的航拍数据做推理,把置信度落在 0.3~0.6 区间的模糊样本筛选出来,让人工去标注这些样本,再混合进训练集做增量训练。每轮迭代这个操作,电力线的误检率都能肉眼可见地往下走。这套方法不需要改任何代码,只需要写一个筛选置信度区间的脚本,就能让标注人力花在最有价值的地方。
我个人在整套项目中体会最深的一点是:目标检测看似调几个参数就能出一版不错的结果,但真正拉开差距的,永远是数据集的组织方式和对问题的理解深度。YOLOv8 只是一个催化的工具,对风力涡轮机的结构了解越深入,标注和调参的每一步就更有针对性。这篇内容分享了整个项目从数据构建到部署实测的全部细节,希望能给做类似基础设施检测项目的人一些参考价值。后续如果大家有更多关于线状目标检测的问题,欢迎在评论区交流讨论。