简介:面向电力设备巡检与隔离开关状态识别任务的小型标注数据集,适合目标检测初学者做模型训练、算法对比或数据增广练习。包内包含50张jpg原图,每张均配Pascal VOC格式xml与YOLO格式txt标注,类别为close和open,共50个标注框(close 42框、open 8框),由labelImg按矩形框标注,可直接接入常见检测框架。txt文件为不带分割路径的YOLO标注,xml文件则记录图片尺寸、目标类别与矩形框坐标,两种格式一一对应,便于复核和转换。资源共152个文件:52个txt文件、50个xml文件、50个jpg图片,压缩包13.79MB,体量轻、结构简单,适合快速下载与离线实验。目前已有350人学习下载,作为电力视觉项目起步样本集或教学示例都很合适。需注意,数据集仅保证标注准确合理,不对训练模型精度作任何承诺,使用时应结合实际场景评估效果。
1. 这份 50 张 2 类别的隔离开关状态识别检测数据集,到底能拿来做什么
变电站运维里有个很实在的需求:用摄像头代替人工去确认隔离开关的“合闸 / 分闸”状态。你拿到的这份“电力场景隔离开关状态识别检测数据集VOC+YOLO格式50张2类别.7z”,就是干这件事的最小样本:50 张电力场景照片,两个类别,同时给了 VOC(XML)和 YOLO(txt)两套标注。它不是一个能直接训练出生产模型的弹药库,而是一个把“格式理解、转换、训练、验证”整条链路跑通的样板。反直觉的结论是:直接拿它硬训会翻车,必须在预训练权重基础上关掉一大半数据增强,才有可能榨出能用的精度。适合谁?刚入坑 YOLO 想练流程的人,或者手头有自己现场照片、想照着这套格式做数据集的人。50 张图少归少,但用来学“怎么让 YOLO 吃下电力场景数据”已经足够。
2. 解压、盘点与格式对齐:先把 VOC+YOLO 两套标注统一成一套能训的
2.1 先解压,不要急着开工
拿到 .7z 压缩包,第一步不是打开文件夹看图片,而是把它解压到一个固定工作目录,并且确认解压完整。Windows 上常见的压缩工具是 7-Zip,传到 Linux 服务器上训练时,一般用 p7zip 的命令行工具处理。
mkdir -p switch_dataset 7z x '电力场景隔离开关状态识别检测数据集VOC+YOLO格式50张2类别.7z' -oswitch_dataset find switch_dataset -type f | wc -l7z x表示保留压缩包内目录结构完整解压,-o指定输出目录,注意-o后面紧跟路径、不加空格。最后一行find统计文件总数,用于和压缩包里预期的文件数做核对。50 张图、每张至少 1 个 XML 标注和 1 个 txt 标注,再加上可能的划分文件,总数应该在 150 到 200 之间。如果差很多,说明解压过程出问题,比如磁盘写满、压缩包损坏。实际解压时如果遇到中文文件名乱码,多半是压缩包在 Windows 下用 GBK 编码打包,Linux 端按 UTF-8 解出来就花了。这种情况不要硬掰,直接在 Windows 或 macOS 图形端解压后重新用 zip 或 tar 打包再传到服务器,反而更快。
2.2 盘点目录:VOC 和 YOLO 两套标注先对齐
解压完先做一次盘点。常见布局是VOC/下放着Annotations、JPEGImages、ImageSets/Main,YOLO/下放着images和labels。但实际压缩包可能把两套格式混在一个目录里,所以盘点逻辑要按文件类型去扫,而不是按固定目录名去猜。
from pathlib import Path root = Path('switch_dataset') xml_files = sorted(root.rglob('*.xml')) img_files = sorted( list(root.rglob('*.jpg')) + list(root.rglob('*.jpeg')) + list(root.rglob('*.png')) ) # 只统计 labels 目录下的 txt,过滤掉 ImageSets 里的划分文件 label_dir = root / 'YOLO' / 'labels' if not label_dir.exists(): label_dir = root / 'labels' txt_files = sorted(label_dir.rglob('*.txt')) if label_dir.exists() else [] xml_names = {p.stem for p in xml_files} img_names = {p.stem for p in img_files} txt_names = {p.stem for p in txt_files} print(f'图片数量: {len(img_names)}') print(f'VOC标注数量: {len(xml_names)}') print(f'YOLO标注数量: {len(txt_names)}') print('有图无VOC标注:', img_names - xml_names) print('有图无YOLO标注:', img_names - txt_names) print('两套标注文件名不一致:', (xml_names ^ txt_names) & img_names)这段代码的价值在于把“两套标注是否对齐”变成可量化的检查项。文件名我统一用Path.stem取前缀,不带扩展名,这样.xml、.txt、.jpg只要前缀相同就能对应上。运行结果里,比较理想的是三个数量都等于 50,差集为空。如果出现“有图无 VOC 标注”或者“两套标注文件名不一致”,说明压缩包本身有缺失,后面训练时 YOLO 会自动丢弃没有标注的图片,直接导致有效训练样本减少,而这个过程通常不会报错。
2.3 用一份脚本把 VOC 的 XML 转成 YOLO 的 txt
不管压缩包里给的 YOLO 标注是否完整,我拿到手都会自己跑一遍转换脚本。原因很简单:YOLO 训练只需要 txt,而 txt 里写的是归一化坐标,任何一步换算出错都直接影响模型学到的框;用自己的脚本重转一遍,等于把坐标计算过程重新核对一次。
import cv2 from pathlib import Path import xml.etree.ElementTree as ET # 类名到 ID 的映射,以数据集中实际类名为准 class_map = {'close': 0, 'open': 1} def voc_to_yolo(xml_path, img_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() # 不信任 XML 里的 filename 字段,按前缀逐一尝试图片扩展名 img_path = None for ext in ('.jpg', '.jpeg', '.png', '.bmp'): candidate = img_dir / (xml_path.stem + ext) if candidate.exists(): img_path = candidate break if img_path is None: print(f'找不到图片: {xml_path.stem}') return 0 img = cv2.imread(str(img_path)) if img is None: print(f'图片读取失败: {img_path}') return 0 h, w = img.shape[:2] lines = [] for obj in root.findall('object'): cls = obj.find('name').text if cls not in class_map: print(f'未知类名 {cls},跳过') continue bnd = obj.find('bndbox') x1 = float(bnd.find('xmin').text) y1 = float(bnd.find('ymin').text) x2 = float(bnd.find('xmax').text) y2 = float(bnd.find('ymax').text) # VOC 允许框越界,YOLO 不允许;先裁剪到图像范围内 x1 = max(0, min(w, x1)); y1 = max(0, min(h, y1)) x2 = max(0, min(w, x2)); y2 = max(0, min(h, y2)) if x2 <= x1 or y2 <= y1: print(f'无效框被跳过: {xml_path.stem}') continue # 转归一化中心坐标和宽高 cx = ((x1 + x2) / 2) / w cy = ((y1 + y2) / 2) / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f'{class_map[cls]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}') if lines: out_path = out_dir / (xml_path.stem + '.txt') out_path.write_text('\n'.join(lines)) return len(lines)坐标换算的逻辑是:VOC 的bndbox给的是左上角和右下角的像素坐标(x1,y1,x2,y2),YOLO 要求的是class x_center y_center width height,其中中心坐标和宽高都要除以图像宽高做归一化。脚本里先用cv2.imread拿真实图像尺寸,而不是用 XML 里<size>字段的值,因为 XML 的尺寸字段偶尔会和实际图片不一致。cx = ((x1 + x2) / 2) / w这一步就是把像素中心点换算成 0 到 1 之间的比例;x1 = max(0, min(w, x1))是裁剪越界坐标,标注时手滑把框画到图像外面很常见,不裁剪会得到大于 1 的宽高比,训练时直接报错或产生 NaN 损失。转换完务必检查生成的 txt 行数,如果某个文件被跳过了,打印信息里会直接告诉你原因。
2.4 划分 train/val 并可视化抽查
50 张图做训练,验证集不能太多。我一般按 8:2 划分,也就是 40 张训练、10 张验证,不再单独留测试集。用小数据集时验证集只是用来监控过拟合,没必要再切一块测试集出来浪费样本。
import random import shutil from pathlib import Path img_dir = Path('switch_dataset/YOLO/images') label_dir = Path('switch_dataset/YOLO/labels') imgs = sorted(img_dir.glob('*.jpg')) random.Random(42).shuffle(imgs) val_imgs = imgs[:10] train_imgs = imgs[10:] for split, split_imgs in [('train', train_imgs), ('val', val_imgs)]: (img_dir / split).mkdir(parents=True, exist_ok=True) (label_dir / split).mkdir(parents=True, exist_ok=True) for img in split_imgs: shutil.move(str(img), str(img_dir / split / img.name)) label = label_dir / (img.stem + '.txt') if label.exists(): shutil.move(str(label), str(label_dir / split / label.name))固定random.seed(42)是为了保证每次划分结果一致,方便复现。划分后images/train、images/val、labels/train、labels/val四个目录各就各位,YOLO 训练时直接按目录读数据。这里有个最容易忽略的点:移动图片的同时必须移动同名 txt,只移图片不移标注,训练时那个目录里就会混入“有图无标注”的脏数据。
划分之后一定做一次可视化抽查。50 张图里的每一张都值得看一遍,这是小数据集仅有的好处——人工能检查完。
import cv2 names = {0: 'close', 1: 'open'} def draw_yolo(img_path, label_path, out_path): img = cv2.imread(str(img_path)) h, w = img.shape[:2] for line in label_path.read_text().splitlines(): cid, cx, cy, bw, bh = line.split() cid = int(cid); cx = float(cx); cy = float(cy) bw = float(bw); bh = float(bh) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[cid], (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imwrite(str(out_path), img) for label in sorted((Path('switch_dataset/YOLO/labels/val')).glob('*.txt')): draw_yolo( Path('switch_dataset/YOLO/images/val') / (label.stem + '.jpg'), label, Path('preview') / (label.stem + '.jpg'), )画框用的是 txt 里的归一化坐标,重新乘回宽高得到像素坐标,这一步能验证转换脚本没有把坐标算错位。抽查时重点看两类问题:第一,框是不是紧紧包住隔离开关的触头部分,有没有框到绝缘子串;第二,close和open的标签对不对,类别标反是这种小数据集里最常见的翻车点。
3. 用 YOLOv8 在这 50 张图上训练:最小可复现配置与必调参数
3.1 data.yaml 怎么写:目录结构、类别 ID 与 names
训练前先把数据配置文件写好。YOLOv8 的 data.yaml 就是告诉训练器“数据在哪、类别有几类、名字是什么”的清单。对这份 50 张的数据集,配置文件很简单:
path: /home/yourname/switch_dataset/YOLO train: images/train val: images/val names: 0: close 1: openpath写 YOLO 目录的绝对路径,train和val可以是相对path的目录名,也可以是 txt 文件路径;这里直接用上一步划分好的目录,最省事。names里的 0 和 1 必须和 labels 里 txt 的第一列整数严格对应。这一步最容易踩的坑是:压缩包里给的 YOLO txt 里,close可能是 0 也可能是 1,如果不先打开 txt 看一眼就照抄网上的模板,类别就会整体对调。写 yaml 前,随便打开一个labels/val下的 txt,看第一列是什么,再回来写names。
3.2 选预训练权重与训练命令
50 张图从头训练一个 YOLO 模型没有任何意义,必须加载预训练权重。模型规格我选yolov8n.pt,nano 版参数量最小,对极小数据集来说是最不容易过拟合的选择;显存 8G 以上的机器也可以换yolov8s.pt,但对 50 张图来说 nano 已经足够,省下来的时间都花在调参上。
yolo detect train \ data=/home/yourname/switch_dataset/YOLO/switch.yaml \ model=yolov8n.pt \ epochs=200 \ imgsz=640 \ batch=16 \ mosaic=0.0 \ mixup=0.0 \ close_mosaic=0 \ lr0=0.001 \ optimizer=AdamW \ patience=30 \ seed=42这条命令里真正决定成败的不是epochs,而是mosaic=0.0和mixup=0.0。YOLOv8 默认开启 mosaic 增强,训练时会随机把四张图拼成一张,这对大规模数据集是神器,但对只有 50 张、目标又是隔离开关这种细长结构的数据集,拼接会把触头截断、把绝缘子串拼到另一张图上,模型学到的是碎片而不是完整的开关状态。close_mosaic=0是让训练从第一个 epoch 起就不启用 mosaic,而不是训练后期才关闭。lr0从默认的 0.01 降到 0.001,因为预训练权重的特征已经很好,微调阶段学习率太大容易把之前学到的通用特征冲掉。patience=30表示验证集指标连续 30 个 epoch 不提升就自动停,防止 200 个 epoch 白跑。
3.3 关掉 mosaic 之后的增强方案:哪些保留,哪些必须改
数据增强在 50 张图上是双刃剑。增强太猛,模型在增强后的失真图上学不到真实特征;增强太弱,50 张图又不够泛化。我按 YOLOv8 默认参数逐项说明留还是改。
| 参数 | 默认值 | 建议值 | 原因 |
|---|---|---|---|
mosaic | 1.0 | 0.0 | 拼图会截断细长目标,破坏隔离开关整体结构 |
mixup | 0.0 | 0.0 | 默认关闭,不要打开;小数据集里只会让框混乱 |
fliplr | 0.5 | 0.5 | 水平翻转不改变开关的开合语义,保留 |
flipud | 0.0 | 0.1 | 上下翻转同样不改变语义,但会让绝缘子方向变化,轻微开启增强泛化 |
hsv_h | 0.015 | 0.03 | 模拟不同光照色偏,电力场景早晚色温差异大 |
hsv_s | 0.7 | 0.7 | 保留,能模拟阴天低饱和度和晴天高饱和度 |
degrees | 0.0 | 5.0 | 隔离开关安装有角度偏差,5 度以内的旋转足够覆盖 |
translate | 0.1 | 0.1 | 保留,模拟相机位置偏移 |
表格里这几个参数是硬经验。hsv_h我习惯调高到 0.03,因为变电站的摄像头画面经常偏色,黄昏和夜间的白平衡都不稳定;degrees不要超过 5,隔离开关是刚性设备,超过 5 度的旋转增强出来的样本在真实场景中几乎不存在,反而让模型学到不真实的姿态。这套增强策略的核心思路是:只在“真实可能出现的扰动”范围内做增强,不做破坏性增强。
yolo detect train \ data=/home/yourname/switch_dataset/YOLO/switch.yaml \ model=yolov8n.pt \ epochs=200 imgsz=640 batch=16 \ mosaic=0.0 mixup=0.0 close_mosaic=0 \ flipud=0.1 degrees=5.0 hsv_h=0.03 \ lr0=0.001 optimizer=AdamW \ patience=30 seed=423.4 训练过程中盯什么:loss 曲线和验证指标
训练启动后不要干等。yolo detect train会在runs/detect/train/下实时写入日志和训练曲线,用下面的命令可以随时看训练状态:
tail -f runs/detect/train/train_args.yaml实际要看的是两部分:命令行的 loss 输出和验证集指标。YOLOv8 的 loss 拆成box_loss、cls_loss、dfl_loss三项,box_loss负责框的位置回归,cls_loss负责类别判断。对隔离开关状态识别来说,cls_loss比box_loss更重要——框偏几个像素问题不大,开合状态判错就是事故。正常走势是三个 loss 都稳步下降,验证集 mAP50 缓慢上升并最终稳定;如果cls_loss降不下去,回到第 2 章重新检查类别标签,多数时候是标错了。如果曲线骤降后 loss 变成 NaN,直接进下一章看 BN 崩溃的处理。
4. 避坑:50 张数据集训练与 .7z 处理里的 5 个翻车现场
4.1 7z 解压报错:密码正确却解不出来,或文件名全乱码
现象:在 Linux 上用7z x解压,明明密码是对的,却一直报 CRC 错误或文件名变成一堆乱码。原因:压缩包是在 Windows 上用 7-Zip 打的,中文文件名按 GBK 编码存储,Linux 端默认 UTF-8 解码就会错乱;部分压缩包还带了 Windows 的 Unicode 扩展头,p7zip 版本太旧解析失败。解决:先升 p7zip 到最新版,再解压时加上-p参数显式传密码;如果乱码依旧,最快的办法是把压缩包拷贝到 Windows 或 macOS 上用图形界面解压,再重新打成 zip 传到服务器。不要花半小时研究 Linux 端转码,这不是技术问题,是压缩工具跨平台兼容问题。
4.2 两套标注对不上:有图无标注,训练集悄悄缩水
现象:训练启动日志里显示的图片数量不是预期的 40 张,而是 32 张或者更少,程序不报错。原因:压缩包里的 YOLO labels 目录本身有缺失,几张图的 txt 没打包进去,或者文件名前缀有细微差异(比如IMG_001和img_001)。YOLO 训练时会静默跳过没有对应 txt 的图片,不生成任何警告。解决:训练前一定要跑第 2 章的盘点脚本,把img_names - txt_names的结果打出来。如果找到缺失,就从 VOC 的 XML 重新转一份 txt 补进去,这也是我在第 2 章坚持自己重转一次标注的另一个原因。
4.3 类别 ID 错位:把“闭合”学成了“断开”
现象:训练 loss 正常下降,验证 mAP 也有 0.8 以上,但拿到现场一测,闭合状态的开关被识别成断开,并且置信度很高。原因:data.yaml 里names的顺序和 txt 里的类别 ID 对不上。比如 txt 第一列 0 代表open,而 yaml 里写0: close,模型从头到尾都在用相反的标签训练,最后学到的就是一个镜像分类器。解决:训练前随机打开一个 labels txt,把第一列数字和应用代码里names[0]的结果人工对齐;图像可视化时把类别名打印在框上,人眼扫一遍 val 的预测结果比看任何指标都直接。
4.4 训练到一半 loss 变 NaN:BN 崩溃
现象:前几十个 epoch 一切正常,某个 epoch 开始cls_loss跳成nan,验证 mAP 直接归零,重启训练后同样位置再次翻车。原因:BatchNorm 层的统计量在小 batch 下不稳定。50 张图的验证集只有 10 张,如果 batch 设成 4 或更小,BN 层的均值方差估计在每一步都剧烈震荡,加上 mosaic 开启时梯度方向混乱,最后数值溢出成 NaN。解决:先把 batch 提到 16(显存不够就降 imgsz 到 480),同时确保mosaic=0.0,再把lr0降到 0.0005 重试。这里一个血泪经验是:出现 NaN 不要只想着调学习率,先检查 batch 大小,BN 层在小 batch 上崩溃是这类小数据集的头号杀手。
4.5 训练集 99%,换现场照片全废:背景过拟合
现象:train loss 降到很低,验证集 mAP 也很高,但把手机拍的现场照片喂进去,要么漏检、要么乱框。原因:50 张图大概率来自同一个变电站同一台相机,背景、角度、光照高度一致,模型学到的是“这个背景里有开关”而不是“开关长什么样”。解决:先在数据增强里把hsv_h、degrees、translate调到我前面写的值,用增强去打破背景一致性;然后最实际的做法是补拍数据——这份数据集的价值是帮你把流程跑通,真正要投产,至少再拍 200 张不同角度、不同背景、不同光照的照片,按 VOC 格式标注后并入训练集。只靠这 50 张图就认为自己有了一个能上线的检测模型,是最危险的误判。
5. 验证:用 mAP、混淆矩阵和时序投票确认它能上现场
训练结束不是终点,对 50 张图训练出来的模型,验证必须比训练更严格。第一步先用验证集跑一次标准评估:
yolo detect val \ model=/home/yourname/switch_dataset/runs/detect/train/weights/best.pt \ data=/home/yourname/switch_dataset/YOLO/switch.yaml \ conf=0.25跑完重点看两个输出:mAP50 和混淆矩阵。mAP50 能到 0.9 以上说明模型在这 10 张验证图上没有明显问题;盯着混淆矩阵时注意,YOLO 的混淆矩阵里会多出一个背景类(bg),所有被模型漏检的目标都会归到 bg,所以矩阵各格数字之和大概率不等于图片总数,也不等于标注总数,这是正常的,不要当成 bug。真正要警惕的是“close 预测成 open”这类非对角线数字偏高,一旦出现,回到第 4 章查类别 ID。
验证集只是及格线,我习惯再做一步“按时间序列验证”。隔离开关的状态判断本质上是一个时序问题,单帧画面的置信度再高也可能因为遮挡、运动模糊产生误判。做法是让模型连续处理同一摄像头拍摄的若干帧,在跟踪到同一个开关设备的前提下做投票:
# 每 5 帧对同一隔离开关的状态做一次投票,至少 3 帧判定闭合才算闭合 for i in range(0, len(frame_preds), 5): window = frame_preds[i:i+5] close_votes = sum(1 for pred in window if pred['class_id'] == 0 and pred['confidence'] > 0.5) final_state = 'close' if close_votes >= 3 else 'open'这段逻辑在工程上叫“状态防抖”。单帧输出会闪烁,5 帧窗口取多数能过滤掉偶发的误检,代价是状态切换响应慢了一点点,但对隔离开关这种动作频率极低、状态持续很久的设备来说,响应慢一两秒完全可以接受,换来的是误报率明显下降。跑完这一套,这个 50 张数据集训练的模型才能算“验证过”,而不是“训练过”。
我现在的习惯是:任何小数据集训出来的模型,先当它不可靠,跑完标准评估、混淆矩阵、时序投票这三步,再谈能不能拿到现场。希望帮到你。
本文还有配套的精品资源,点击获取