☰
TTPLA数据集实战:输电通道检测+分割的COCO标注处理与训练避坑指南
2026/10/11 22:47:20 网站建设 项目流程

简介:输电杆塔与输电线路图像检测与分割数据集,面向电力巡检、计算机视觉与深度学习研究者,覆盖真实输电场景中杆塔、线路、绝缘子等关键设施的识别与分割任务。压缩包共1244个文件,包含1242张JPG图像和2个JSON标注文件,整体约275.8MB,采用COCO格式,并划分了训练集、验证集与测试集,方便直接开展模型训练、调参与评估。目前已有531人学习下载,图像来自多种环境和天气光照条件,可用于输电线路日常巡检、部件状态监测、故障预警等场景。借助该数据集可同时训练目标检测与语义分割模型:检测任务可获取杆塔、线路等目标的边界框定位,分割任务可输出绝缘子等部件的像素级掩膜,实现对电力设施的精细识别。这些标注信息为搭建高效、可靠的智能巡检算法提供了标准化数据基础,尤其适合需要工程落地和学术研究的读者。

1. TTPLA:既有检测框又有分割掩膜的输电通道数据集,值得先跑通再谈创新

输电杆塔和输电线路(TTPLA)图像检测+分割数据集,是我在电力巡检视觉方案里用得比较顺手的一套数据基础。它同时带目标检测的边界框标注和语义分割/实例分割的多边形标注,并且整理成了coco标注格式,意味着检测模型和分割模型可以直接吃同一份数据,不用维护两套标注。对做无人机巡检、通道隐患识别、或者在做输电线路三维重建前的二维感知的人来说,这是一个能省掉大量标注成本的起点。

这篇文章不是数据集说明书,而是按我实际处理这类数据的顺序来写:解压后先看什么、标注字段怎么读、训练前要做什么校验和转换、训练参数怎么设,以及最容易翻车的几个细节。新手照着做能跑通检测+分割的最小闭环,熟手可以只看第五章的避坑和第六章的迭代思路。先说明一点,我对这个数据集的判断基于通行的coco组织方式和输电场景图像特点,具体到你手上的压缩包,解压后以实际文件结构为准。

2. 数据集结构与coco标注格式:解压后先做三件事

拿到这个压缩包,我一般不会急着写训练脚本。先做三件事:看目录结构、确认标注文件里的字段是否完整、用一个统计脚本把标注的底细摸清楚。这三步做好了,后面训练阶段能少踩一半的坑。

2.1 目录与文件构成:一份能直接喂给训练框架的coco标注

常见做法是压缩包解压后出现一个根目录,里面分成 images 和 annotations 两个子目录。images 下放着原始巡检图像,可能是无人机可见光拍摄的杆塔和线路照片,分辨率通常比较高;annotations 下则是一个或几个 JSON 文件,coco标注格式的核心就是这些 JSON。

coco格式里最关键的三个顶层字段是 images、annotations 和 categories。images 数组里每个元素记录一张图的 file_name、height、width 和 id;annotations 数组里每个元素是一条标注记录,通过 image_id 关联到具体图像,检测用的是 bbox 字段,分割用的是 segmentation 字段;categories 则定义类别 id 到类别名的映射。这个数据集的类别按标题看至少包含输电杆塔和输电线路两类,实际训练时以 JSON 里的 categories 内容为准。

拿到文件后第一步是用命令行确认 JSON 能正常解析,别让训练跑到一半才发现标注文件是坏的。

python -c "import json; d=json.load(open('annotations/instances_train.json')); print(len(d['images']), len(d['annotations']), d['categories'])"

这段命令的作用是快速加载标注文件并输出图像数量、标注数量、类别列表。正常情况下三行数据都能打印出来;如果某个值明显离谱,比如图像数量为 0 或者 categories 为空,说明文件结构不对或者解压不完整。我习惯在训练前先跑这一步,因为在电力巡检项目里,我遇到过好几次标注 JSON 被别人二次编辑后少了逗号,解析报错直到深夜调训练才发现。

2.2 检测与分割共用一套coco:bbox和segmentation字段怎么协同

coco标注格式对于检测和分割来说,区别主要体现在 annotation 对象内部的字段上。检测任务只需要 bbox 字段,格式是 [x, y, width, height],其中 (x, y) 是边界框左上角坐标,width 和 height 是框的宽高;分割任务则需要 segmentation 字段,在该数据集中通常是多边形格式,即一个二维数组,每个内层数组是一组坐标点 [x1, y1, x2, y2, ...]。

一条完整的标注记录通常会同时携带 bbox、segmentation、area、iscrowd 这些字段。area 是多边形围成的面积,用于计算分割指标的权重;iscrowd 表示该目标是否是一群密集对象,为 1 时一般训练时会被跳过。这个数据集里存在一种情况值得注意:某些标注对象是多边形和边界框共存的,但两者并非总是严格对应,因为多边形是人工勾的,而边界框可能是从多边形取外接矩形生成的,细微偏差属于正常现象。

我处理这类数据时的做法是不去强行修正 bbox 和 segmentation 的微小不一致,而是让检测和分割各自用自己的标注做训练和评估。强行统一反而会把原本标注准确的 segmentation 坐标改歪。如果发现某个对象的 bbox 明显没有包住多边形,那才需要标记出来并做人工修正。

2.3 用一个统计脚本摸清标注底细

解压后先写个一次性脚本统计标注质量,这能帮你判断这个数据集需不需要清洗。我一般会统计每张图的标注数量、标注框的宽高分布、segmentation 多边形的点数量,以及是否存在空标注图像。

import json import numpy as np with open('annotations/instances_train.json') as f: data = json.load(f) img_id_to_info = {img['id']: img for img in data['images']} anno_per_img = {} bbox_sizes = [] poly_points = [] empty_images = [] for ann in data['annotations']: img_id = ann['image_id'] anno_per_img[img_id] = anno_per_img.get(img_id, 0) + 1 x, y, w, h = ann['bbox'] bbox_sizes.append((w, h)) seg = ann['segmentation'] if seg: poly_points.append(len(seg[0]) // 2) for img_id, img in img_id_to_info.items(): if anno_per_img.get(img_id, 0) == 0: empty_images.append(img['file_name']) bbox_sizes = np.array(bbox_sizes) print('图像总数:', len(img_id_to_info)) print('标注总数:', len(data['annotations'])) print('bbox宽度中位数:', np.median(bbox_sizes[:, 0])) print('bbox高度中位数:', np.median(bbox_sizes[:, 1])) print('多边形点数中位数:', np.median(poly_points) if poly_points else 0) print('空标注图像数:', len(empty_images))

这段脚本统计了几个关键指标。bbox 宽高分布能直接反映目标尺度:如果中位宽高都只有几十像素,说明图像里目标整体偏小,训练时要考虑高分辨率输入。多边形点数中位数如果非常高,比如几百甚至上千,说明标注勾得很细致,转换 mask 时耗时更长。空标注图像如果数量可观,要么是背景图,要么是漏标,需要决定是保留用于负样本还是筛掉。

参数上需要注意,脚本里 seg[0] 取了第一个多边形,如果一个对象被拆成多个多边形,其他部分没统计进来。对绝大多数巡检图像这个简化够用,但要意识到它低估了点总数。统计完这些数值,你对这个数据的脾气基本就有数了。

3. 把coco标注变成可训练样本:从校验到mask生成

coco标注格式虽然在检测和分割框架里都能直接读,但真正拿来训练前还有两步必须做:一是图像和标注对账,二是生成语义分割需要的 mask 图。很多框架内部做了这些事,但你要知道它做了什么、在什么情况下会静默失败。

3.1 图像-标注对账:缺图、坏图、重复标注一次查清

图像和标注对账看起来简单,实际踩坑最多。常见情况是压缩包经过多次拷贝后,images 目录里少了部分文件,但标注 JSON 仍然引用它们;另一种情况是图像改名后没有同步改 JSON 里的 file_name,导致训练时报错找不着图。我一般会写个遍历脚本做一次硬核对。

import json import os from PIL import Image with open('annotations/instances_train.json') as f: data = json.load(f) img_dir = 'images/train' missing = [] corrupted = [] for img_info in data['images']: path = os.path.join(img_dir, img_info['file_name']) if not os.path.exists(path): missing.append(img_info['file_name']) continue try: with Image.open(path) as im: im.load() except Exception: corrupted.append(img_info['file_name']) print('缺失图像:', len(missing)) print('损坏图像:', len(corrupted))

这段脚本做了两件事:文件存在性检查和图像可解码性检查。Image.load() 会真实读取像素数据,能抓出那些扩展名是 jpg 但内容已经损坏的文件。在电力巡检数据里有一种常见情况是图像从相机导出时中断,文件大小非零但无法解码,训练框架在读图时表现为随机报错,很难定位。

对账脚本输出缺失和损坏列表后,我的处理方式是直接生成过滤后的新标注 JSON,而不是去补图。因为这个数据集的标注本身是完整的,删除坏样本比补样本成本低得多。过滤时注意同步更新 images 数组和 annotations 数组中关联到这些图的标注,否则会出现新的悬空引用。

3.2 语义分割必须的mask:从polygon到PNG的转换实现

语义分割训练需要的是像素级标签图,每张图对应一张单通道 PNG,像素值等于类别 id。coco 里的 segmentation 是坐标点列,需要先把多边形填充成 mask。这里有个关键选择:用 pycocotools 还是直接用 OpenCV 填充。pycocotools 的 mask 转换对规则多边形没问题,但在这个数据集上我遇到过坐标点边界轻微超出图像宽高的情况,pycocotools 会直接报错,而 OpenCV 的 fillPoly 会自动裁剪边界,更抗造。

import json import numpy as np import cv2 import os with open('annotations/instances_train.json') as f: data = json.load(f) cat_ids = {cat['id'] for cat in data['categories']} out_dir = 'masks/train' os.makedirs(out_dir, exist_ok=True) anns_by_img = {} for ann in data['annotations']: anns_by_img.setdefault(ann['image_id'], []).append(ann) for img_info in data['images']: img_id = img_info['id'] h, w = img_info['height'], img_info['width'] mask = np.zeros((h, w), dtype=np.uint8) for ann in anns_by_img.get(img_id, []): cat_id = ann['category_id'] for poly in ann['segmentation']: pts = np.array(poly, dtype=np.int32).reshape(-1, 2) cv2.fillPoly(mask, [pts], cat_id) cv2.imwrite(os.path.join(out_dir, img_info['file_name'].replace('.jpg', '.png')), mask)

这段代码的核心逻辑是逐图建立空白 mask,然后把该图的所有多边形标注用 fillPoly 填充成对应的类别 id。参数上需要注意三点:dtype 必须是无符号整型,类别 id 如果超过 255 需要改用 uint16,但该数据集类别数量少,uint8 够用;reshape(-1, 2) 要求 polygon 坐标是合法的偶数长度数组,如果某个 polygon 只有一个坐标点,这里会直接报错,需要在前面加上长度校验;输出文件名我保留了和原图相同的命名前缀,只是扩展名换了,这样后续数据集类可以通过字符串替换找到对应 mask。

这里有个容易翻车的细节:同一张图内如果有两个同类目标,按上述代码 fillPoly 会直接覆盖,像素值不变,看起来没问题。但如果两个目标类别不同且多边形有重叠,后画的会覆盖先画的,最终 mask 只保留后者的类别。输电线场景中杆塔和线路天然存在遮挡关系,我的建议是标注越靠后的对象层级越高,填充顺序按 annotation 在 JSON 中的顺序即可,不用刻意排序,因为这类数据集的多边形重叠区域通常很小。

3.3 检测与分割的加载配置:给训练框架的data path与transform

生成 mask 之后,检测和分割的数据加载可以分为两条路线:检测模型继续吃 coco JSON,分割模型吃图像加 mask。我在实际项目中常用现成框架,但配置加载器时有个共通点:检测和分割对数据增强的要求不完全一样。

检测任务中,随机翻转和尺度抖动是标配,因为 bbox 会随图像一起变换,coco 格式的加载器会自动处理;分割任务中,翻转同样安全,但像随机裁剪就需要格外小心,裁剪区域必须保证 mask 和图像对齐,否则切出来的训练样本会出现标签错位。

# 检测分支加载配置(伪配置,适配常见检测框架) dataset_det = dict( type='CocoDataset', data_root='data/ttp/', ann_file='annotations/instances_train.json', img_prefix='images/train', pipeline=[ dict(type='LoadImageFromFile'), dict(type='LoadAnnotations', with_bbox=True), dict(type='Resize', scale=(1333, 800), keep_ratio=True), dict(type='RandomFlip', prob=0.5), dict(type='PackDetInputs') ] ) # 分割分支加载配置 dataset_seg = dict( type='CustomSegDataset', img_dir='images/train', mask_dir='masks/train', pipeline=[ dict(type='LoadImageFromFile'), dict(type='LoadMask'), dict(type='Resize', scale=(512, 512)), dict(type='RandomFlip', prob=0.5), dict(type='PackSegInputs') ] )

配置里值得关注的是 Resize 的不同策略。检测分支我保持 1333x800 的长边缩放,因为检测框对分辨率相对钝感,但电力杆塔这类大目标和小线路这类小目标尺度差异大,输入太小会导致小目标直接丢失;分割分支用 512x512 的方形输入,这是为了控制显存,因为分割的 loss 是逐像素计算的,分辨率翻倍显存占用接近四倍。实际训练时这两个配置可以独立调,不必强求一致。

还需要注意 transform 之间的顺序。RandomFlip 必须放在 Resize 之后,因为翻转操作依赖图像和标注的坐标系统一致,先 resize 后 flip 不会产生坐标扭曲。这个顺序在框架里通常是固定的,但如果你自己搭 pipeline,很容易写成先翻转后缩放,导致 bbox 坐标和 mask 位置错位。这类问题训练时不报错,只在验证时 mAP 偏低,定位起来非常耗时间。

4. 检测+分割联合训练:参数怎么设才不翻车

数据集准备妥当后,进入训练阶段。同一份 TTPLA 标注同时输出检测和分割结果,有两种做法:一种是把检测和分割分别训练两个模型,部署时串行推理;另一种是训练一个带检测头和分割头的模型。从工程效率看,我更推荐后者,但需要处理好损失权重和类别不平衡问题。

4.1 最小训练管线:检测头与分割头共享backbone

把检测和分割放在同一个模型里,常见做法是基于一个分割模型加检测分支,或者基于检测模型加 mask 分支。backbone 共享意味着图像只需要过一遍特征提取网络,检测头在特征图上预测 bbox,分割头预测像素类别,推理时多任务的额外开销主要来自分割头的上采样。

# 多任务模型配置骨架(伪配置) model = dict( type='MultiTaskModel', backbone=dict(type='ResNet', depth=50, out_indices=(1, 2, 3)), neck=dict(type='FPN', in_channels=[256, 512, 1024], out_channels=256), det_head=dict(type='RetinaHead', num_classes=2), seg_head=dict(type='FPNHead', num_classes=2, loss_decode=dict(type='CrossEntropyLoss', use_mask=True)), train_cfg=dict(), test_cfg=dict() )

这里检测头的 num_classes 和分割头的 num_classes 都设成 2,对应杆塔和线路两类。如果你的标注里还有背景类需要单独处理,检测头一般用 num_classes 加 1 的方式表示背景,而分割头直接用 0 表示背景,这两个类别的计数规则容易混淆,是配置里最常见的错误点。

实际训练时损失是检测损失加分割损失。我一般把分割损失权重设为 1.0,检测损失权重设在 0.5 到 1.0 之间。原因很实际:分割损失收敛慢但更稳定,检测损失收敛快但容易出现波动,权重太大会让训练早期梯度被检测任务主导,导致分割头学不到位。

这条管线跑通后,建议先用少量图像做一次单步验证,确认正反向传播没有问题,再上全量数据。我在某次项目里跳过这步直接全量训练,跑了 6 个小时发现分割头的 loss 一直是 0,原因是 mask 目录路径配错,模型一直在学全零标签,白白浪费了算力。

4.2 类别不平衡与权重设置,以及评估指标的正确打开方式

输电场景的类别天然不平衡。杆塔数量少、面积大,线路数量多但很多是细长条。如果直接拿原始标注训练,模型会偏向学习线路而忽略杆塔。常用的处理手段是给损失函数加类别权重,让数量少的类别有更大的梯度贡献。

# 类别权重示例:假设类别0为杆塔,类别1为线路 class_weight = [2.0, 1.0] criterion = torch.nn.CrossEntropyLoss(weight=torch.tensor(class_weight))

这里的关键是确定权重值,而我一般不会拍脑袋定。先用统计脚本算出两个类别的像素占比,比如杆塔占 15%、线路占 85%,那么权重可以设为占比的反比,即杆塔权重高、线路权重低。但我实际用下来会发现反比会导致杆塔过度拟合,靠近但不等于反比的取值更靠谱,比如 1.5 到 2.0 之间。这个区间需要小规模实验验证,没有通用最优值。

评估指标上,检测任务看 bbox mAP,分割任务看 mIoU 或 mask AP。这两个指标的分布很能说明问题:如果检测 mAP 高而分割 mIoU 低,说明模型找准了位置但对边界不敏感;反之则说明边界分割好但定位漂移。我会在训练日志里同时记录这两个指标,观察它们的差距是否收敛,而不是只看单个指标。另一个值得注意的点是,coco 格式的评估默认使用 0.5 到 0.95 的多个 IoU 阈值取平均,如果你发现 mAP 数值低得离谱,先确认是不是评估配置默认值太高,而不是模型完全没学会。

5. 用TTPLA做检测和分割的几个避坑点:现象、原因、对策

这类数据集在使用过程中有几个高频问题,我按「现象→原因→对策」写出来,每条都是实际踩过的教训。

5.1 多边形自相交导致mask面积异常

现象:从 coco 标注转换出的 mask 里,某些杆塔区域像素值出现明显错误的填充,有的甚至把背景也填成了目标类别。

原因:标注人员在勾画复杂钢结构时,多边形顶点顺序出现交叉,形成自相交多边形。OpenCV 的 fillPoly 在遇到自相交时会按非零环绕规则填充,结果可能跟标注意图相反。

解决:转换 mask 前对每个 polygon 做面积校验,比较 polygon 的面积和其最小外接矩形面积。如果两者比例异常高,比如超过 0.9,就认为多边形接近矩形,可能是简化过度;如果 proportion 低于 0.05,说明多边形极细长,多半是画歪了。这类样本我直接丢弃而不修复,因为输电通道中杆塔标注数量足够多,丢几个不影响整体训练。

5.2 线路被标注成断断续续的短线

现象:分割训练后 mIoU 不低,但目视检查 mask 时发现线路标签不连续,明明一整条线被标成了三四段。

原因:coco 格式里一个目标只允许一个 annotation id,但有些标注人员为了省事,把一条完整的线路拆成多段分别标注,导致同一根导线有多个多边形,每个多边形的首尾都停留在中间某处。

解决:检测任务不受影响,因为 bbox 会把整条线路包住;分割任务就需要做标注合并。我的做法是在转换 mask 时按 category 做形态学连接:先填充所有线段多边形,再用闭运算把距离较近的线连接起来。注意闭运算的核大小要跟图像分辨率挂钩,比如 1333x800 的图像核取 5x5 到 7x7 即可,过大的核会把相邻但不同的线路也连成一根。这个方法虽然粗暴,在巡检数据里效果比逐段人工合并靠谱。

5.3 杆塔尺度大、线路尺度小,同一个模型两难

现象:训练完的模型对杆塔的检测 mAP 超过 80,对线路只有不到 50,分割结果也是线路的边界明显粗糙。

原因:杆塔占据图像大部分面积,线路往往只有几个像素宽。模型的下采样倍数决定了线路特征很容易在深层特征图中消失,检测头根本看不到小目标。

解决:我做了两件事。第一,把训练输入分辨率从 800 提到 1200 到 1333 之间,线路的像素宽度随之增加;第二,给 FPN 增加一个更高分辨率的输出层级,让检测头能拿到 stride 为 4 的特征。这相当于让模型在特征金字塔底层多做一次预测,对小目标更友好。显存不够时优先保分辨率,其次再保 batch size,因为线路目标的存在感对分辨率更敏感。

5.4 输出是coco,但某些框架要segmentation是RLE格式

现象:调用某个框架的 mask 解码接口时报错,说 segmentation 既不是 list 也不是 dict,无法解析。

原因:coco 标注的 segmentation 有两种合法格式:多边形列表(适用于非密集目标)和 RLE 编码(适用于 iscrowd=1 的目标)。该数据集的标注以多边形为主,但某些第三方转换工具读入后会尝试把多边形转成 RLE 存储,导致下游代码误判。

解决:在编写数据加载脚本时对 segmentation 字段做类型判断,如果是 list 就按多边形处理,如果是 dict 就用 pycocotools 的 maskUtils.decode 解码成 mask。判断逻辑一句话就能写完,但要写在最前面,否则容易在个别样本上突然崩溃。

seg = ann['segmentation'] if isinstance(seg, dict): mask = maskUtils.decode(seg) else: mask = np.zeros((h, w), dtype=np.uint8) for poly in seg: cv2.fillPoly(mask, [np.array(poly).reshape(-1, 2)], 1)

5.5 图像分辨率过高,训练时显存不够

现象:单张图像 4000x3000,直接送进模型后显存直接爆炸,batch size 只能设 1 甚至报错。

原因:输电巡检相机的原图分辨率普遍很高,而分割头的逐像素 loss 在 4000x3000 输入下显存占用是检测任务的几十倍。

解决:我的标准做法是离线切图,把原图切成 1024x1024 的 patch,相邻 patch 之间保留 100 像素的重叠。切图时同步把标注 JSON 里的 bbox 和 segmentation 坐标做平移,超出 patch 边界的部分裁剪掉或丢弃。注意如果一个目标横跨多个 patch,它会在多个 patch 中重复出现,评估时需要根据原图坐标做去重后计算指标,直接用 patch 的 mAP 会虚高。

6. 进阶做法:用小样本验证、切图推理与难例回灌迭代数据集

当你把上述流程跑通后,这个数据集带来的价值才开始真正释放。我建议的进阶路线分三步:小样本端到端验证、切图推理部署、难例回灌持续迭代。

小样本验证是指从全量数据里随机抽出 5% 到 10% 的图像,加上对应标注,跑一个最小训练和评估闭环。目的不是追求指标,而是确认加载、转换、训练、推理、评估整条链路没有隐藏 bug。我通常会用这个子集测试不同的 backbone 和输入分辨率,得到一个粗粒度结论后再上全量训练,能省下好几轮全量实验的时间和算力。

切图推理是部署环节的必然选择。因为训练时用了离线切图,推理时也要把整张大图切成 patch 分别预测,然后把结果拼回原图坐标。这里有个经验参数:patch 的重叠区域取 200 像素而不是训练时的 100 像素。原因是推理时没有训练阶段的随机增强来补偿边界信息损失,更大的重叠能让边缘处的目标被至少一个 patch 完整包含,减少跨 patch 漏检。

难例回灌是我目前坚持在做的一步。训练出的模型在验证集上挑出预测置信度低或分割边界差的样本,人工复核后把修正过的标注补充进训练集。这类数据集的原始标注质量总体不错,但输电场景中逆光、雾天、杆塔被植被遮挡的情况很难在一版数据里覆盖全。每轮回灌只补 200 到 300 张难例,模型的泛化能力提升比单纯加大训练轮次明显得多。

说句实在话,这类数据集的价值不仅在于它能直接训练模型,更在于它提供了一个稳定的基准:你的数据增强方案、模型选型、训练策略都可以先在这里验证,再迁移到自己的业务数据上。我在某次项目里就是先用它验证了分割头比检测头更适合细长目标识别,这直接影响了后续业务模型的结构选型。每一条踩过的坑,最后都变成了迭代流程里的一道检查工序。

希望这篇笔记能帮你少走弯路,把 TTPLA 数据集的检测和分割能力尽早落到你自己的巡检方案里。

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

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

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

立即咨询