☰
无人机电力杆塔巡检实时目标检测:从数据治理到边缘部署实战
2026/9/30 4:15:22 网站建设 项目流程

简介:面向电力运维、无人机巡检及计算机视觉研究者的技术文档,系统阐述了基于深度学习算法的无人机电力线路杆塔巡检实时目标检测模型建构方案,可帮助读者解决人工巡检效率低、受损杆塔定位难等问题。资源包内为1个docx文档,约56KB,正文涵盖模型总体设计、Darknet特征提取网络、多尺度特征交互层,以及针对样本不平衡的数据增广、基于K-means的目标框聚类改进等关键环节;文末还给出召回率、交并比、平均均值精度94.09%、检测速度20帧/s及简化版模型30帧/s等实测结果,便于对照复现。目前已有137人学习,适合希望将深度学习目标检测方法落地到电力巡检场景的研究人员、算法工程师和电力行业技术人员参考。

1. 电力杆塔巡检里的实时目标检测,为什么最后像深度学习低头

把无人机飞到220kV杆塔前,机载电脑要在几十毫秒内框出绝缘子、防震锤和缺失的销钉,这就是基于深度学习算法的无人机电力线路杆塔巡检实时目标检测模型建构要解决的事。我在这类项目里摔过最大的跟头不是模型不够准,而是标注框的松紧差异和机载工控机降频——这两个因素比调anchor更影响落地效果。这篇笔记适合四类人:要搭建杆塔巡检方案的电力工程师、负责模型训练的算法同学、给无人机配机载算力的嵌入式工程师,以及准备拿这个题目做毕设或竞赛的研究生。你会看到从数据清洗、模型选型到训练和边缘部署的完整路径,也会看到几个真实存在的坑,以及对应解法。

2. 模型选型:YOLO、Faster R-CNN与SSD在机载场景下的取舍

2.1 先定任务边界:检测什么、多快算实时

杆塔巡检的目标检测不是通用目标检测的简单平移。无人机悬停或慢速绕塔飞行时,镜头里的目标往往只占整幅图像很小一部分:一串绝缘子可能在4K原图上只有120×400像素,单个销钉甚至不足30×30像素。任务类别还得按电力业务拆开,常见的有绝缘子(破损、污秽、爆裂)、防震锤(缺失、滑移)、销钉(缺失)、均压环、鸟巢、导线异物。其中销钉缺失是很多电网公司最看重的检测项,因为它直接关系到掉线和跳闸风险,但它的尺寸也最不友好。

“实时”这个词在无人机场景里尤其容易被误解。不是模型能跑到30FPS就叫实时,而是要在当前飞行速度和相机视场角下做到不漏检。我的经验是先用公式算下限:飞行速度v(m/s)、相机垂直视场角对应的地面或塔身覆盖宽度d(m)、单目标在画面中最小像素宽度s,那么系统帧率至少需要F ≥ v / (d / s)。实际例子:巡检速度6m/s,覆盖宽度10m,目标最小宽度40像素,整图宽度1920像素,算出来F ≥ 6 / (10 × 40 / 1920) ≈ 28.8FPS。如果飞慢一点到3m/s,覆盖宽度不变,帧率下限直接降到14.4FPS。所以部署时先问巡航速度和云台角度,再定帧率目标,不要盲目追求60FPS。常见配置是15到30FPS,超过30的上限往往在散热和存储上得不偿失。

除了帧率,还得区分训练端和推理端。训练端用服务器大显存跑高分辨率输入,推理端在Jetson或者低功耗工控机上跑TensorRT,两者共享同一套模型权重但优化路径不同。下面整个选型讨论都基于推理端算力小于20W这个前提。

2.2 三种模型架构的实时性对比与选型结论

主流的候选大致分三类。Faster R-CNN是两阶段检测器的代表,先由RPN生成候选区域,再过RoI Head逐一分类和回归。它对小目标很友好,mAP在公开集上不差,但一张1280×1280的图像在Jetson Orin Nano上用INT8推理也要60到80ms,算下来12到16FPS,刚够及格线,而且框架部署链路长,TensorRT需要手动搭网络。SSD的优势是结构简单,VGG/ResNet做主干再加多层feature map预测,但深层feature map对小目标召回率偏低,杆塔巡检里最需要的恰恰是那种几十像素的小销钉,所以现在很少人拿SSD做电力目标检测。

剩下就是YOLO系列。从YOLOv5到YOLOv8,one-stage架构加上FPN/PANet对不同尺度目标都有专门的特征层,配合mosaic、copy-paste等增强方式,小目标的表现已经接近甚至超过两阶段模型。工程生态也最完整:Ultralytics官方仓库一条命令导出ONNX、TensorRT,在Jetson上部署文档齐,遇到问题社区里基本都能搜到。选型结论很简单:第一选择YOLOv8s或YOLOv5s。如果目标极小且后处理可以接受更高的切片开销,就在模型前面加一个SAHI式切片推理;如果项目明确要求可解释性,再回头评估Faster R-CNN加可视化分支。

下面这段代码是选型阶段我必跑的“算力摸底”,在目标机或同等GPU上测一次端到端耗时,防止项目做到一半发现推理速度不够:

from ultralytics import YOLO import time, torch model = YOLO("yolov8s.pt") model.export(format="engine", device=0, half=True, workspace=4) # 用TensorRT引擎跑一次预热 model = YOLO("yolov8s.engine") test_input = torch.randn(1, 3, 1280, 1280).cuda() # 预热10次,排除CUDA初始化抖动 for _ in range(10): model.predict(test_input, imgsz=1280, device=0, verbose=False) times = [] for _ in range(100): t0 = time.perf_counter() model.predict(test_input, imgsz=1280, device=0, verbose=False) times.append((time.perf_counter() - t0) * 1000) times.sort() print("P50 ms:", round(times[50], 2), "P95 ms:", round(times[95], 2))

这段脚本把P50和P95的端到端耗时打印出来。只看平均帧率会掩盖抖动:Jetson设备在有电源波动时单次推理可能从30ms飙到60ms,P95比P50更能反映真实巡检场景。预留的20%余量我一般这么用:如果P50是30ms,那就意味着系统只有约33FPS上限,扣掉图像传输和预处理开销,实际可用帧率要再打八折,也就是26FPS左右的稳定输出。算力摸底应该在选型第一天就做,而不是训完模型才后悔。

2.3 实测帧率目标与算力预算分配

机载算力预算要提前拆:传感采集、畸变校正、检测网络、跟踪器、图传编码都在抢同一块芯片。我习惯按时间占比画一个流水线:如果检测推理占30ms,其他环节占20ms,那么落实到录像和告警的视频段帧率就得按50ms的周期算,而不是只按模型推理的30ms算。常见做法是只让检测网络跑在GPU上,图像缩放、色彩转换放在CPU端,用OpenCV的异步队列解耦。这样模型推理延迟稍微抬高,但整体吞吐不降,而且CPU占用高时GPU不会一起卡死。

更细的算力分配是一条经验线:一张1280×1280的图片在Jetson Orin NX上做FP16的YOLOv8s推理,不优化的情况下约40ms;同样条件改成INT8是22ms;再用通道重排和少裁剪,能压到15ms附近。把检测帧率目标定在20FPS时,INT8几乎是刚到及格线,所以我倾向于在项目初期就把目标机锁定为Orin NX或同等算力的板卡,预算不够宁可降class数量也不要降硬件标准,否则后面做切片推理时会反复打回重做。

3. 数据与标注:把无人机巡线影像变成能训练的数据集

3.1 数据采集和筛选:别把原始航片直接喂网络

从无人机拿到的原始素材一般是4K视频或高像素照片,直接抽帧丢给标注工具会让标注同学想辞职,也会让模型的学习信号被背景淹没。标准做法是先做一次粗筛和抽帧。抽帧不是按时间等间隔,而是按位姿变化来抽:无人机绕塔飞行时悬停和缓慢转动阶段会产生大量高度相似的帧,全抽进去会让训练集冗余,类别分布也被自相关性污染。我一般按云台角度每变化2度抽一帧,或按机头朝向每5度取一帧;如果是直线航线,则按重叠率不超过30%抽。

粗筛还要过滤模糊和过曝。下面这段脚本用OpenCV计算图像的Laplacian方差,值低于阈值就丢弃,同时用直方图判断过曝比例:

pip install opencv-python numpy
import cv2 import numpy as np from pathlib import Path def is_blurry(img, thresh=60): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) lap = cv2.Laplacian(gray, cv2.CV_64F).var() return lap < thresh def is_overexposed(img, ratio_thresh=0.4): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) over = np.sum(gray > 245) / gray.size return over > ratio_thresh for video_path in Path("raw_videos").glob("*.mp4"): cap = cv2.VideoCapture(str(video_path)) kept = 0 while True: ret, frame = cap.read() if not ret: break # 30帧视频,只需要每5帧取1次做粗筛,降低IO压力 if int(cap.get(cv2.CAP_PROP_POS_FRAMES)) % 5 != 0: continue if is_blurry(frame, thresh=50) or is_overexposed(frame): continue # 按杆塔名称和帧号命名,方便后续按杆塔分组划分数据集 out_path = Path("selected_frames") / f"{video_path.stem}_{int(cap.get(cv2.CAP_PROP_POS_FRAMES)):06d}.jpg" cv2.imwrite(str(out_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 95]) kept += 1 cap.release() print(f"{video_path.name}: kept {kept}")

这段粗筛有两个要点:Laplacian方差阈值不是固定的,要在第一次运行时打印一批样本的分布,再定50到80之间的值;过曝比例阈值针对的是玻璃绝缘子和金属反光——它们在高光下纹理全丢,标注了也学不出有效特征,宁可直接筛掉,靠后续增强补齐曝光扰动。命名规则里保留杆塔ID非常关键,后面的测试集泄露问题就是从这里救回来的,用杆塔ID做分组划分比纯随机划分科学得多。

3.2 标注格式转换:从PASCAL VOC到YOLO的脚本

标注工具我一般让团队用LabelImg或CVAT。CVAT适合团队协作,有在线任务分配到人;LabelImg适合单人小规模认知。无论用哪个,最后导出的首选格式是PASCAL VOC,因为它的XML里同时保留了对象名、坐标和图片尺寸,后续转COCO或YOLO都方便。YOLO训练需要的是每个图像的txt文件,每行是“class_id cx cy w h”,其中cx、cy是中心点归一化到0~1,w、h是宽高归一化,四舍五入保留6位小数即可。

下面这个脚本完成VOC到YOLO的转换,同时做了三项防御:跳过空标注、过滤非法坐标、自动生成classes.txt:

import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, class_names): tree = ET.parse(xml_path) root = tree.getroot() width = float(root.find("size/width").text) height = float(root.find("size/height").text) if width <= 0 or height <= 0: return None lines = [] for obj in root.findall("object"): cname = obj.find("name").text if cname not in class_names: continue box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) # 坐标越界修正:贴近边缘的目标常被标成负数或超出宽高 x1, x2 = max(x1, 0), min(x2, width) y1, y2 = max(y1, 0), min(y2, height) if x2 - x1 < 1 or y2 - y1 < 1: continue cx = (x1 + x2) / 2 / width cy = (y1 + y2) / 2 / height w = (x2 - x1) / width h = (y2 - y1) / height lines.append(f"{class_names.index(cname)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") return "\n".join(lines) class_names = ["insulator", "damper", "pin", "bird_nest", "foreign_object"] in_dir, out_dir = Path("voc_annotations"), Path("yolo_labels") out_dir.mkdir(exist_ok=True) for xml_file in in_dir.glob("*.xml"): content = voc_to_yolo(xml_file, class_names) if content: txt_name = xml_file.stem + ".txt" (out_dir / txt_name).write_text(content + "\n") else: print(f"skip {xml_file.name}: empty or invalid")

转换脚本有三个坑。第一是class_names的顺序必须和最终训练用的data.yaml保持完全一致,一旦中间插新类,所有已转换的txt都要重新跑一遍,否则训练阶段类别错位不可见但结果全错。第二是坐标越界修正,标注人员习惯把遮挡边界画到画面外,不强行截断会让归一化后的框超出0~1范围,YOLO在计算loss时不会直接报错,但这个框会被忽略或导致数值不稳定。第三是重复标注检查,同一个绝缘子被不同标注员画了两个重叠框时,脚本不会报错,这时需要在转出的yolo_labels里对每个txt做去重,按IoU高于0.85的保留面积更小的框——越小的框对销钉这类目标越友好。

3.3 类别定义与标注一致性检查

类别定义直接影响模型能不能收敛。我把类分成两类:能明确抠轮廓的实体类(绝缘子、防震锤、销钉)和纹理/区域类(鸟巢、异物)。实体类用普通矩形框没问题,纹理类尤其是鸟巢,边界极其不规则,强行用最小外接矩形会把大量背景包进来,导致训练时背景和前景难分。我的做法是对鸟巢采用旋转框或按组件拆开标注,或者直接单独训练一个二分类检测器区分“有无鸟巢”,不打精细框,整体效果更好。

标注一致性是比类别设计更隐蔽的变量。两个标注员对“绝缘子”的框紧松标准不同,一个画得贴合边界,一个习惯四周多留10像素,模型学到的anchor尺度分布会被拉宽,验证集mAP看似稳定,但换到另一段航拍视频时误检立刻变高。我在这里做了个多方校验:统计每类标注框面积的log分布。框的大小比例如果跨度超过一个数量级,就要回头查是不是把“整串绝缘子”和“单个绝缘子片”混在同一个类里了。正规的做法是同一类只标注同一层级的对象:这一帧要么标整串,要么标单片,不要在类别里混层级。为了让标注规范不吃亏,我会在开工前做一份带边界包的标注指引:绝缘子框取伞裙外边缘、防震锤框取锤体、销钉框取裸露钉帽,一张图多截几个示例发给所有标注员。

4. 训练与调参:用YOLO在本地跑通杆塔目标检测的最小配置

4.1 训练环境与配置文件的10个关键项

训练环境我习惯固定成一套版本,避免Ultralytics升级带来的意外行为变化。能固定则固定版本,能锁conda环境则锁conda环境。常用的是Python 3.10、PyTorch 2.0.1、CUDA 11.8和Ultralytics 8.x。安装命令:

conda create -n tower python=3.10 -y conda activate tower pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics==8.1.34 opencv-python tqdm tensorboard

数据集配置文件用data.yaml,这是训练入口。下面是我在杆塔巡检里常用的配置:

# tower_data.yaml path: ./datasets/tower train: images/train val: images/val test: images/test nc: 5 names: 0: insulator 1: damper 2: pin 3: bird_nest 4: foreign_object

训练时最容易被忽略的10个配置项是:path要写绝对路径或相对位置正确,避免后续导出和验证时找不到数据;train/val/test的图片集要按杆塔分组切好,不是随机切;nc和names必须和标注转换脚本里的class_names一致;no_cache参数在内存不足时开启;rect参数影响batch内尺度统一,对大小差异极大的数据有奇效;cos_lr可以让训练后期更稳;mixup和mosaic的开关要看数据集的难例分布;workers数值超过物理核心数会拖慢;device设置多卡时要平衡数据加载;amp混合精度在小目标任务上可能让loss不稳定,需要单独对比。

4.2 训练命令与超参数:imgsz、batch、anchor怎么设

数据准备好了,先跑一版最短训练验证链路通不通。不要第一次就上几百个epoch,先跑20个epoch看loss有没有下降、验证集是否有基本检出:

cd /path/to/ultralytics python -m torch.distributed.run --nproc_per_node=1 train.py \ --data tower_data.yaml \ --weights yolov8s.pt \ --imgsz 1280 \ --batch 8 \ --epochs 20 \ --project experiments \ --name tower_sanity

Ultralytics 8.x更常见的写法是直接用命令行接口:

yolo detect train data=tower_data.yaml model=yolov8s.pt \ imgsz=1280 batch=8 epochs=300 \ project=experiments name=tower_v1 \ patience=30 cos_lr=True mixup=0.5 hsv_h=0.015 hsv_s=0.7 hsv_v=0.4

每个参数这里都有明确用意。imgsz是全局输入尺寸,杆塔巡检里推荐1280起步,原因很简单:目标平均尺寸小,640输入下绝缘子串只有几十像素,特征被下采样四次后基本消失。显存不够时优先降低batch而不是imgsz,batch降到4还能训,imgsz降到640就回到小目标漏检的老路。batch在单卡A10上8到12比较稳,batch过大容易提前过拟合还影响BN统计量。epochs配合patience做早停,设置patience=30表示30个epoch验证指标不涨就自动停,这比硬跑300省时间。

anchor这件事在YOLOv8里已经自动化了,训练前模型会基于数据集自动聚类生成anchor,不再需要手填。但要注意:如果标注框尺寸差距极大(从30×30的销钉到800×400的鸟巢),自动聚类结果可能偏向面积大的类,此时建议关闭自动anchor,显式传入锚点或把图像按尺寸分组后训练。另一个容易被坑的地方是mosaic。mosaic增强能提升泛化,但当目标本身小而密集时,四张图拼一起容易把目标裁成一半,导致早期学习信号混乱。我逐步跑到第50个epoch关闭mosaic,具体做法是降低mixup和mosaic概率,或者直接关闭:

yolo detect train data=tower_data.yaml model=yolov8s.pt \ imgsz=1280 batch=8 epochs=300 \ mosaic=0.0 mixup=0.0 close_mosaic=50

mosaic在close_mosaic=50后再禁用,正好让模型在最后阶段专注于真实尺寸的分布。

训练过程中我最关注三条曲线:train/box_loss持续下降且val/box_loss没有在早期反弹;val精度每5个epoch往回看一次,前期降得太快说明过拟合;类别别只看mAP50,还要看每类的AP,尤其销钉单独一行的AP是否低于其他类,低了就说明只能靠数据层面解决,调参救不回来。

4.3 从PyTorch到TensorRT:把模型压到机载设备能跑的实时水平

训练完成后拿best.pt做部署。不要直接把PyTorch权重拷到Jetson上跑,效率太低。我的标准流程是先导出ONNX,再用TensorRT生成engine,全部用官方工具链:

yolo export model=experiments/tower_v1/weights/best.pt format=onnx imgsz=1280 opset=12 simplify=True

得到best.onnx后,在目标机上用trtexec转INT8或FP16:

/usr/src/tensorrt/bin/trtexec \ --onnx=best.onnx \ --saveEngine=best_fp16.engine \ --fp16 \ --minShapes=images:1x3x1280x1280 \ --optShapes=images:1x3x1280x1280 \ --maxShapes=images:4x3x1280x1280

参数说明里,--fp16是半精度,INT8需要额外的校准数据并写一个python脚本导出engine,更麻烦但推理更快;minShapes和maxShapes是给engine预留动态batch范围。若目标机内存小,把maxShapes的宽度压到2,不然TensorRT会按4张图占用显存。转换完成后跑一次精度对比,量化后的mAP下降超过2个点就要回退到FP16。

在Jetson上部署时还要做两件小事:一是把图像预处理从RGB的GIL和resize挪到CUDA上,OpenCV的cv2.cuda.resize配合GPU直传能省掉在CPU上缩放的时间;二是后处理NMS在TensorRT里可以留给CUDA kernel做,Ultralytics最新版已经做了这个优化,在export的时候加上nms=True参数:

yolo export model=best.pt format=engine imgsz=1280 half=True nms=True device=0

这样推理输出直接就是过滤后的框,省掉了Python层的非极大值抑制,单帧整体延迟大约再降8到10ms。部署端运维至少预留三种回退方案:license过期回退ONNX、精度不过关回退FP16、显存不足回退Jetson内置的TensorRT缓存文件。这些在巡检项目里都真实遇到过,提前准备能少熬几个夜。

5. 避坑:无人机杆塔巡检模型落地中的5个典型故障

5.1 小目标漏检:绝缘子串在整图里只占几十像素

现象:验证集mAP过了0.85,到了无人机实拍图上,较远的绝缘子串完全没有框,但把原图局部放大后人为能看清。反复调confidence阈值从0.35降到0.1也没有明显改善。

原因:模型输入被缩放到640×640,绝缘子串缩到几十像素,经过多次下采样后特征图上的响应极弱。验证集的尺度分布和现场照片不一致,让精度虚高。

解决:提升imgsz到1280是第一步。第二步是切片推理,把原图切成若干重叠块,每块单独送进网络再合并结果。我现在通常配合SAHI的思路,切块尺寸取训练尺寸,重叠率取0.2,已经能把相对距离15米处的绝缘子检出率从22%拉到74%。代价是推理次数变成原来的三到五倍,帧率下降,所以切片推理只在松悬停或低速变焦的精细阶段开启,正常巡航依旧走整图检测。

5.2 标注不一致:同一类目标框大小差距大,mAP虚高

现象:某类绝缘子训练时loss一直下不去,换了一版新标注的同类数据后mAP从0.83掉到0.57,看起来像是模型退步。

原因:标注标准执行不一致,有人标整串绝缘子,有人标单片绝缘子,还有人把防震锤和绝缘子之间的连接金具框进来。检测模型学习到的是混合尺寸目标,回归头和分类头都很矛盾。

解决:把每类目标的框面积log分布打印出来,建立一致性记录。面积跨度超过8倍就重新检查这个类的定义,统一层级。对已经标注完的数据,我用一个后处理脚本把面积超过该类P95的框自动剔除,保留更合理的框。在实际流程里,我自己也吃过一次大亏:标了两万张图后查框才发现有两千个框把接线夹也套进来了,回退成本极其高昂。所以现在第一周只让速度慢的标注员标300张,分布核对后再放量。

5.3 机载工控机降频:推理速度越跑越慢

现象:无人机起飞头两分钟,系统能稳定跑到28FPS,飞到第六分钟以后,帧率掉到12FPS,但垂直的电力杆塔和水平线又没有变化,算法进程CPU占用率一直是80%。

原因:机载电脑在温度墙附近掉频。Jetson在温度高于80度后会把GPU频率从1.2GHz降到800MHz以下,而推理基准测试都是在冷机状态下跑出来的,热态性能被高估。

解决:给机箱增加主动散热只是第一步。硬件层面我还会把nvpmodel设置为最高性能模式,并用jetson_clocks直接锁定GPU/CPU频率上限,不让芯片自己跳水:

sudo nvpmodel -m 0 sudo jetson_clocks --fan

软件层面把推理线程优先级调高,并让图像采集队列背压到检测线程,不要无限囤帧。最关键的验证方法是在满载推理10分钟后重新测P95速度,如果掉得超过15%,就需要从散热或功耗上调整。这个坑在真机上最容易翻车,别问我怎么知道的。

5.4 逆光与反光:玻璃绝缘子在高光下被误判成背景

现象:晴天上午逆光拍摄的杆塔照片中,玻璃绝缘子变成一片白亮的圆形高光,模型会把它们漏检或误检成背景;而顺光照片表现挺好。

原因:训练数据里高光样本太少。之前的数据增强虽然hsv_v=0.4做明度扰动,但在逆光下全局过曝和局部镜面反射是两码事,简单亮度扰动覆盖不了。

解决:在数据集里主动加逆光样本。我一般让飞手在每天上午8点到10点和下午4点到6点各拍一组逆光巡航,虽然视觉上不好看,但训练价值高。增强端也要加局部高光掩膜:用高斯核在随机位置生成一层白斑叠加到目标附近,逼模型不要只依赖纹理特征。这类问题靠调参数几乎改善不了,只能在数据层面补课。

5.5 测试集泄露:同一基塔出现在训练和测试里

现象:本地验证精确到99%,拿到完全新塔的视频后大量漏检,误检还多。排查时发现验证集里有几个塔和训练集中部分视频来自同一个连续飞行段。

原因:数据划分用随机分割而不是按杆塔分组。无人机绕塔飞行时,相邻帧高度相关,随机打散后同一塔的帧既进训练又进验证,模型相当于泄露了上下文信息,给指标灌水。

解决:按杆塔ID分组划分数据集,一个塔的所有帧只能进一个集合。使用group_split逻辑,把全部图像路径按杆塔分组,再组级随机分配:

import random from pathlib import Path from collections import defaultdict imgs = list(Path("selected_frames").glob("*.jpg")) groups = defaultdict(list) for img in imgs: tower_id = img.stem.split("_")[0] # 上面3.1里把塔ID放在命名最前面 groups[tower_id].append(img) gids = list(groups.keys()) random.seed(42) random.shuffle(gids) tr, va, te = gids[:int(len(gids)*0.7)], gids[int(len(gids)*0.7):int(len(gids)*0.85)], gids[int(len(gids)*0.85):] for split, ids in [("train", tr), ("val", va), ("test", te)]: out = Path("datasets/tower/images", split) out.mkdir(parents=True, exist_ok=True) for gid in ids: for p in groups[gid]: p.rename(out / p.name)

这段代码的核心是用塔ID而不是文件名哈希做分组,避免同一塔的不同相机位被割裂开。分组后的测试指标才有意义,尤其是漏检率,否则就是自欺欺人。把这个分组逻辑写进数据准备脚本的固件里,之后每次新增数据都会自动按这个原则执行。

6. 上线前的鲁棒性验证:一张误检率报告换一次放心返航

模型训练完不直接上飞机,先做三个层级的验证。第一级是分级测试集,把数据按塔型(耐张塔、耐张塔转角、直线塔)、天气(晴、阴、雾)、拍摄时段(顺光、逆光)分成若干个子集,逐个跑验证,别只抛一个平均数。我会单独保存一份“故意找茬”测试集,里面全部是模型最容易翻车的高光、遮挡、远距离样本,跑完看每类AP的变化,这比看总mAP靠谱得多。

第二级是Grad-CAM可视化,验证模型在看什么。如果图像里绝缘子串清晰,但网络的高激活区域全部落在背后的树林纹理上,说明模型在偷懒,这种模型换一段背景不同的视频就会崩。看到这种情况,我的习惯是回头查数据和增强,而不是加大模型容量。

第三级是生成一张面向工程人员的误检率报告,用FPPI(每张图平均误检框数)和召回率做表,横轴为目标尺寸,纵轴为部位,输出一张“哪类目标在什么距离下可靠检测”的结论表。现场飞手需要知道的就是“30米外销钉不可靠,飞近到15米再检测”,这种结果比PR曲线直白得多。我每次上线前还会做一次疯狂测试:在岸上将十张最难的照片故意美化成快照丢进模型,违规但有效,它会把那些测试集里没有的极端情况暴露出来。

写到最后想给你留句老实话:模型建构里最花时间的不是训练,而是数据分布和标注口径的治理,以及部署时的温度、功耗、延迟组合。我现在的习惯是第一天就锁死数据划分方式和标注规范,后面才不会反复推倒重来。希望这些绕过坑的经验能帮你把无人机电力线路杆塔巡检的实时检测模型从论文落到真正能每天飞出去的机载系统上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询