简介:面向电力运维与计算机视觉开发者,这套项目围绕高压输电线玻璃绝缘子缺陷检测,提供了基于深度学习的完整实现方案。压缩包共47个文件,包含13个Python脚本、14个样本图像、4张效果展示图、模型配置与README说明文档,整体约11.74MB;脚本覆盖数据标注转换、数据增强、训练集划分及YOLO检测等关键环节。项目从数据预处理、模型训练到缺陷定位与分类一路贯通,并附效果展示图片,便于直观验证算法在复杂背景下的识别能力。由于该场景对裂纹、破损、污染等缺陷敏感,这套工程化代码对电力巡检智能化落地有直接参考价值;同时目录结构清晰,便于按模块查阅和移植。已有94人学习浏览,适合需要快速上手绝缘子缺陷检测或在此基础上做二次开发的工程与研究人员。
1. 高压输电线玻璃绝缘子缺陷检测项目:从图像到检测报告的全链路实现
高压输电线路上的玻璃绝缘子长期暴露在野外,自爆、裂纹、闪络和污闪是最常见的缺陷类型,其中玻璃绝缘子自爆后只剩钢帽,在巡检图像里往往只占几十像素,是典型的小目标难题。这个缺陷检测项目把计算机视觉和深度学习结合,提供了一条可复现的完整链路:用xml2label.py把VOC标注转成YOLO格式TXT,用gen_empty_label.py给无缺陷样本生成空标签,配合aug.py增强,经tool/split.py划分数据集后,由yolo.py训练模型,最后用detecte.py做推理和结果可视化。相当于工业缺陷检测项目里“数据准备—训练—推理—部署”的Pipeline全拆开,适合正在做巡检算法落地、需要参考真实工程源码的开发者。下面按这个流程把关键代码、参数和坑位讲透。
2. 数据工程底座:XML转YOLO格式、空标签与数据增强
2.1 为什么绕不开标签格式转换
缺陷检测项目里最耗费时间的往往不是模型结构,而是数据。训练一个YOLO系列模型,标签是归一化的txt文件,每行表示一个目标:类别ID、中心点x、中心点y、宽度w、高度h。而主流标注工具默认保存为Pascal VOC的XML,因此必须做一个中间层转换。项目里的xml2label.py就是干这个的。
这件事看起来简单,但我在实际项目中吃过亏:一是坐标必须归一化,否则不同分辨率图像下模型学到的尺度不一致;二是类别索引从0开始,类别列表一旦定下来就不要乱动,否则重新标注后还要改映射;三是标注框的坐标有时会被标反,转换时要做min/max保护。如果没有这道保护,训练时loss会反复震荡,检测框全是异常宽高比。
xml2label.py的核心逻辑大致如下:
# xml2label.py 核心转换逻辑 import xml.etree.ElementTree as ET def convert(xml_path, out_txt_path, classes, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in classes: continue cls_id = classes.index(name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) xmin, xmax = min(xmin, xmax), max(xmin, xmax) ymin, ymax = min(ymin, ymax), max(ymin, ymax) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) box_w = min(box_w, 1.0) box_h = min(box_h, 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(out_txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines))这段代码做的事情是:解析XML里每个object的类别名和bndbox边界框,按类别列表映射成整数ID,再把边界框归一化到0到1之间。归一化后的坐标不依赖图像尺寸,模型在推理不同分辨率时才能保持一致的输出尺度。min/max防御性写法能挡住最常见的标注坐标顺序错误。如果xml里混入不在classes里的类别,代码会直接跳过,避免脏标签进入训练。
2.2 无缺陷样本:gen_empty_label.py生成空标签的坑
绝缘子缺陷检测里最容易忽略的是正常样本。模型如果只在有缺陷的图像上训练,会把杆塔、导线、鸟巢都当成异常,推理时误检率高到没法看。解决方法是给无缺陷图像生成空标签文件,让YOLO把它们作为纯背景负样本参与训练。
我一般把原始图像分成两类:一类是有缺陷的,走labelImg标注流程;另一类是无缺陷的,只放图像。然后跑一遍gen_empty_label.py:
# gen_empty_label.py 为缺失标签的图像生成空txt import os from pathlib import Path IMG_SUFFIX = {'.jpg', '.jpeg', '.png', '.bmp', '.tif'} ROOT = Path('datasets/insulator') for img_path in ROOT.rglob('*'): if img_path.suffix.lower() not in IMG_SUFFIX: continue label_path = img_path.with_suffix('.txt') if not label_path.exists(): label_path.write_text('', encoding='utf-8') print(f'generated empty label: {label_path}')脚本的核心就是遍历数据集根目录下所有图片,找到同名txt,不存在就创建一个空文件。空文件内容为空,表示这张图里没有任何待检测目标。实际使用中要注意:训练前先清空labels目录重新生成,避免上一个项目残留的旧txt被误认;空标签文件必须存在,否则YOLO训练时会报标签缺失。另外tif格式在巡检数据里很常见,后缀列表里别漏掉。
2.3 aug.py数据增强:翻转到Mosaic的边界
巡检图像通常只有几百张,直接训练YOLO很容易过拟合,所以项目里的aug.py承担了数据增强职责。常见做法是组合水平翻转、旋转、亮度扰动、随机裁剪。针对绝缘子这种细长目标,增强不是越多越好,旋转角度过大容易让目标出界,增强后标签坐标要同步变换。
| 增强方式 | 参数建议 | 适用场景 | 注意事项 |
|---|---|---|---|
| 水平翻转 | p=0.5 | 左右对称的绝缘子串 | 标签x坐标镜像 |
| 旋转 | ±15°以内 | 不同悬挂角度 | 超过30°目标易截断 |
| 亮度扰动 | ±30% | 逆光、阴天巡检图 | 高光会加剧玻璃反光误检 |
| HSV饱和度 | ±25% | 积灰、污闪样本 | 与污染类别强相关 |
| Mosaic | 4张拼1张 | 小目标数量不足 | 验证阶段要关闭 |
表格里看起来都是常规操作,但参数值要结合缺陷类型调整。对于裂纹这种弱纹理目标,亮度扰动超过30%会把裂纹细节洗掉,模型反而学不到关键特征。饱和度扰动对污染检测有帮助,但会让玻璃的高光区域看起来更像裂纹,所以我会把饱和度扰动控制在±25%以内,同时增加逆光样本模拟。
2.4 数据划分与训练文件列表
划分数据集时最怕数据泄漏。同一串绝缘子被切成多张图时,如果随机划分,训练集和验证集会包含同一目标的相似视角,导致验证mAP虚高。tool/split.py如果只按图像名随机分,我会先按绝缘子串ID分组,再按组划分。命令行调用方式一般是:
python tool/split.py \ --image_dir datasets/insulator/train_images \ --label_dir datasets/insulator/labels \ --train_ratio 0.8 \ --seed 42 python gen_train_valid_txt.py \ --data datasets/insulator \ --out_dir datasets/insulatorsplit.py的train_ratio表示训练集比例,剩余归验证集,seed固定随机种子让实验可复现。train_ratio我一般从0.8开始,如果某个缺陷类别样本太少,就改成0.85或0.9,但验证集至少留15%,否则评估结果波动太大。gen_train_valid_txt.py负责生成train.txt和valid.txt,每行一个图像路径,YOLO的数据配置里直接引用这两个文件。注意Windows上路径分隔符是反斜杠,生成时统一转成/,避免训练时路径解析失败。
3. 训练配置与yolo.py:把YOLOv8落到绝缘子缺陷检测上
3.1 yolo.py在项目里的角色
项目里的yolo.py并不是从零复现一个目标检测算法,而是把模型初始化、数据集配置、训练超参数集中管理起来。从文件位置和调用方式看,它更接近一个训练入口,实际网络结构和损失函数用的是YOLOv5/v8的现成实现。就玻璃绝缘子这类小目标缺陷检测而言,我一般直接选择Ultralytics YOLOv8,因为v8的anchor-free设计、C2f结构和Decoupled Head对中小目标更友好,且工程封装完善,切换训练和推理都方便。
这个YOLOv8缺陷检测项目是很典型的深度学习实战项目案例:先验证默认配置能跑通,再针对缺陷小、背景杂的问题调整训练参数。我最开始做这类项目时总想着改模型结构,后来发现几百张巡视图上,把数据整理干净带来的收益远大于魔改网络。
3.2 数据集配置文件
YOLOv8训练前需要一个data yaml,内容如下:
# datasets/insulator/insulator.yaml path: datasets/insulator train: train.txt val: valid.txt names: 0: crack 1: broken 2: pollution这个文件里的names顺序必须和xml2label.py里的classes保持一致。项目摘要提到的裂纹、破损、污染正好对应三类缺陷。如果只检测自爆和裂纹,可以减少names数量,但一定重新跑一遍xml2label.py和gen_empty_label.py,避免旧标签类别索引错位。path字段在Windows下建议写绝对路径,或者用相对路径时保证当前工作目录在datasets的上一级。
3.3 训练超参数怎么定
yolo.py里的参数没有标准答案,但有几个默认值值得参考。我通常在YOLOv8n预训练权重上微调,而不是从头训练,因为ImageNet预训练能让模型在几百张绝缘子图上更快收敛。
| 参数 | 示例值 | 作用 | 调参建议 |
|---|---|---|---|
| model | yolov8n.pt | 预训练权重 | 显存不足换yolov8n,追求精度换yolov8s |
| epochs | 300 | 训练轮数 | 小数据集建议300起步 |
| imgsz | 640 | 输入分辨率 | 长图可试1024,但显存占用翻倍 |
| batch | 16 | 批大小 | 单卡24G可以到32 |
| lr0 | 0.001 | 初始学习率 | 微调用0.001,从头训练才用0.01 |
| lrf | 0.01 | 最终学习率 | 预留0.001到0.0001区间 |
| cache | True | 缓存图像到内存 | 小数据集提速明显 |
| workers | 8 | 数据加载进程 | Windows建议4,避免worker崩溃 |
这张表里最重要的三个参数是imgsz、lr0和batch。imgsz越高,小目标保留的像素越多,但训练和推理都更慢;lr0设成0.01在迁移学习下经常loss爆炸,0.001更稳;batch如果太小,BN层的统计量不稳定,导致验证集mAP波动大。
3.4 训练入口代码
yolo.py的训练核心代码一般长这样:
# yolo.py 训练入口 from ultralytics import YOLO if __name__ == '__main__': model = YOLO('yolov8n.pt') model.train( data='datasets/insulator/insulator.yaml', epochs=300, imgsz=640, batch=16, lr0=0.001, lrf=0.01, device=0, cache=True, workers=8, fliplr=0.5, mosaic=1.0, mixup=0.0, project='runs/insulator', name='glass_defect', seed=42 )这里fliplr=0.5让水平翻转以50%概率生效,mosaic=1.0表示每轮训练都使用四合一拼图,mixup=0.0关掉混合增强。我关掉mixup是因为绝缘子缺陷像素少,mixup会让多个目标重叠后产生模糊标签,损失函数在分类上变得不稳定。project和name指定输出目录,训练好的best.pt和last.pt都在runs/insulator/glass_defect/weights下。
3.5 训练中要盯的三个指标
训练时我会盯着bbox_loss、cls_loss和验证集mAP50。loss下降平滑但不代表模型一定在变好,关键看mAP50是否在40到50轮后开始爬升。如果loss一直降、mAP不动,优先回去检查标签序号和边界框是否归一化正确,而不是调学习率。玻璃绝缘子的小缺陷很容易出现在混淆矩阵里,比如crack被误判成broken,说明两者形态太接近,应补充区分性样本。
训练中断时Ultralytics会自动保存last.pt,恢复时在model.train里加一行resume=True即可。长轮次训练建议加上patience=50,验证mAP连续50轮不更新就自动早停,省下的时间可以用来处理数据错误。
4. detecte.py推理:置信度阈值、NMS与批量输出
4.1 推理脚本的整体设计
detecte.py是项目的检测入口。实际部署时面对的往往是批量巡检图片,而不是单张图,所以脚本要考虑三点:支持CPU/GPU两种运行环境,能遍历整个目录输出检测结果,并且结果能直接被检测报告程序使用。项目源码里保留了detecte.py这个名字,我建议在自己分支里改成detect_infer.py,避免后续混淆。
4.2 批量推理代码
# detecte.py 批量推理核心代码 from pathlib import Path import cv2 from ultralytics import YOLO def main(): src_dir = Path('datasets/insulator/test') out_dir = Path('runs/detect/glass_defect') out_dir.mkdir(parents=True, exist_ok=True) model = YOLO('runs/insulator/glass_defect/weights/best.pt') conf_thres = 0.25 iou_thres = 0.45 imgsz = 640 for img_path in sorted(src_dir.glob('*.jpg')): img = cv2.imread(str(img_path)) results = model.predict( source=img, conf=conf_thres, iou=iou_thres, imgsz=imgsz, verbose=False ) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf_score = float(box.conf[0]) if conf_score < conf_thres: continue x1, y1, x2, y2 = map(int, box.xyxy[0].tolist()) label = f"{model.names[cls_id]} {conf_score:.2f}" cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(str(out_dir / img_path.name), img) print(f"{img_path.name} done") if __name__ == "__main__": main()model.predict传入的是cv2读取的numpy数组,返回结果对象里保存了坐标、类别和置信度。box.xyxy[0]是tensor类型,用map(int, ...)转成整数坐标才能画框。conf=0.25是默认推荐值,但对绝缘子小目标,我会在验证集上分别试0.15、0.20、0.25三档,看漏检和误检的取舍。iou=0.45控制NMS框合并的激进程度,设太低会把同一缺陷的多个检测框合并成一个,设太高则一个缺陷出现多个框。verbose=False避免每张图打印详细日志,批量跑时省下大量I/O。
4.3 从检测框到检测报告
项目摘要里提到“生成相应的检测报告”,工程上我一般把检测框汇总成JSON和CSV两种格式,JSON给前后端项目实战里的Web平台调用,CSV让运维人员在Excel中筛选。输出字段可以这样设计:
| 字段 | 类型 | 说明 |
|---|---|---|
| image_id | str | 原始图像文件名 |
| class_name | str | 缺陷类别 |
| score | float | 置信度 |
| bbox | list | [x1, y1, x2, y2] |
| area | int | 缺陷像素面积 |
生成汇总的代码很简单:
# 生成检测汇总 report.py import csv, json def save_report(results_summary): with open('report.json', 'w', encoding='utf-8') as f: json.dump(results_summary, f, ensure_ascii=False, indent=2) with open('report.csv', 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=results_summary[0].keys()) writer.writeheader() writer.writerows(results_summary)results_summary是一个列表,每个元素对应一个检测框。area字段在代码里通过bbox宽高相乘得到,单位是像素。生成报告之前先按置信度降序排列,优先展示最可疑的缺陷,运维人员不用从几百行CSV里翻。
4.4 效果展示中容易忽视的问题
项目自带的1-4.png效果图展示了训练样本、增强结果和检测可视化。从实际测试看,如果推理图像里的误检框集中在杆塔角钢、导线间隔棒附近,问题通常不在模型,而在第二章说的空标签覆盖不足。这时候回去跑一遍gen_empty_label.py,再补一点背景负样本,比直接调大conf有效得多。调大conf确实能减少误检,但真实缺陷的置信度往往也在0.25附近,阈值一提就把缺陷也滤掉了。
5. 落地调参:绝缘子小目标、玻璃反光与类别不平衡的处理技巧
5.1 大图切片推理
绝缘子串横跨整张巡检图时,直接resize到640会把单个绝缘子压成十几个像素。我采用切片推理:原图按512×512切patch,相邻patch重叠20%,每个patch单独跑模型,再把检测框映射回原图坐标,最后在整图上做一次NMS合并重叠框。切片尺寸不是固定的,图像宽度超过2000像素时,用原图宽度的1/4作为patch边长通常比较稳。
5.2 玻璃反光引起的误检
玻璃绝缘子表面的高光区域在纹理上和裂纹很像,训练阶段增强太激进反而会加重视觉混淆。我一般在aug.py里把HSV饱和度扰动控制在±25%,另加一个亮度模拟分支,增强逆光拍摄的高光效果。推理阶段如果输入图像过暗,先做一次CLAHE对比度增强,但增强参数必须和训练阶段保持一致,否则模型看到的特征分布变了,mAP会明显下降。
5.3 背景负样本的比例控制
空标签文件本身没有类别信息,但YOLO会把对应图像作为背景参与损失计算。经验值是无缺陷图占总图的30%到50%。太少时模型没见过足够背景,误检率升高;太多时模型过度保守,真实缺陷也被当成背景。如果数据集里无缺陷样本很少,最简单的办法是用cutout随机遮挡有缺陷的区域,生成一张负样本,而不是去网上找一堆风格完全不同的图片。
5.4 看单类别AP而不是只看mAP
绝缘子缺陷里自爆和破损在形态上接近,污染又和玻璃老化相似,整体mAP50过了0.85不代表每一类都可用。我会在Ultralytics的results目录里打开per-class AP曲线,如果某个类别比其他类低15个百分点以上,先补充该类别样本,再考虑focal loss。实践证明,复制同一张缺陷图三份做增强,往往比修改损失函数带来的精度提升更直接。
本文还有配套的精品资源,点击获取