简介:面向电梯监控场景的计算机视觉目标检测数据集,适合训练电梯开关状态与人员进出检测模型的研究者、算法工程师及竞赛爱好者。数据集对应2220张真实场景图片,标注兼容Pascal VOC与YOLO两种格式,覆盖电梯关闭、电梯内有人、电梯空置、电梯开门四个类别,累计标注3330个目标框,状态覆盖较全面。压缩包共2000个文件,以VOC格式的xml标注文件为主,并附使用说明txt,整体约77.26MB,便于快速下载并接入主流目标检测框架。目前已有548人学习下载,既能用于模型训练、算法精度对比,也可作为数据增强或迁移学习的参考样本。作者使用labelImg工具完成逐框标注,保证标注准确合理,不承诺训练精度;使用时可根据场景筛选或补充样本。
1. 电梯开关状态人员进出检测数据集:2220张、4类目标,VOC与YOLO双格式落地
电梯场景的视觉检测,最怕的不是模型不够大,而是数据没对准任务。门扇半开时到底算开还是算关?人站在轿厢门口犹豫时算进还是算出?这类问题在通用目标检测数据集里根本没有答案。这份电梯开关状态人员进出检测数据集,用2220张电梯实拍图把问题收敛成4类目标:门开、门关、人员进、人员出,同时给出VOC(XML)和YOLO(txt)两种标注格式,解压出来就能直接喂给YOLOv8或YOLOv5训练,不需要自己写转换脚本。适合做电梯监控、智慧楼宇、门禁闸机联动这类项目的工程师,也适合刚接触YOLO想拿一个有实际业务语义的数据集练手的人。下面先讲清楚这套数据集的内部结构,再给出我实际跑过的训练流程和几个典型踩坑点。
2. 数据集结构与标签设计:目录骨架、类别定义与两种标注格式对照
2.1 目录骨架:JPEGImages、Annotations、labels 与 ImageSets
拿到压缩包解压后,第一件事不是急着开训练,而是先看目录结构是否完整。这个数据集沿用了VOC风格的目录布局,同时为YOLO训练加了labels目录:
电梯开关状态人员进出检测数据集VOC+YOLO格式2220张4类别.7z ├── JPEGImages/ # 2220张原图,jpg格式 ├── Annotations/ # 2220个VOC标注xml ├── labels/ # 2220个YOLO标注txt ├── ImageSets/ │ └── Main/ │ ├── train.txt │ ├── val.txt │ └── test.txt └── data.yaml # 部分版本自带,没有就自己写JPEGImages、Annotations、labels三个目录一一对应:每张jpg在Annotations里有一个同名xml,在labels里有一个同名txt。xml和txt描述的是同一份标注,只是序列化方式不同。VOC标准目录通常长成VOCdevkit/VOC2007那样,这个包把labels平铺在根目录下,属于YOLO训练常见的目录改法,不影响使用。
我拿到数据集的习惯是先跑一条命令核对三个目录的文件数是否相等。用ls JPEGImages | wc -l数图片,再用同样的命令数Annotations和labels,三个数字不一致就直接找发布者要完整包,不要自己在缺标注的情况下硬补。
2.2 四类标签的边界定义:门状态与人员进出的判定基准
标题写了4类别,具体标签名以Annotations里<name>字段为准。常见命名是door_open、door_closed、person_in、person_out四种,类别ID从0开始编号,但不同发布者可能用open、close、enter、leave这类简写,先确认再改yaml。
这四类的边界在业务上是容易扯皮的,我的判定基准如下:
| 类别ID | 标签名 | 判定基准 |
|---|---|---|
| 0 | door_open | 两扇门扇之间有可见缝隙,能看到轿厢内部或井道 |
| 1 | door_closed | 门扇闭合,门缝不可见或小于2像素 |
| 2 | person_in | 人员主体在轿厢外,朝向门方向移动,标注框住人的全身或上半身 |
| 3 | person_out | 人员主体在轿厢内,朝门外方向移动,标注框住人的全身或上半身 |
人员进出的判定在单帧静图上经常有歧义,因为人站在门口犹豫时不好判断朝向。更稳妥的口径是看双脚位置和身体朝向,进则身体朝内、脚在门槛外侧,出则身体朝外。这个数据集的标注者已经做了统一口径,但如果你要自己扩采数据,必须先定死这个标准再让标注员动手,否则后患无穷。
2.3 VOC XML与YOLO txt:两种格式的字段对照
打开任意一个xml,内容大致如下:
<annotation> <folder>JPEGImages</folder> <filename>20240511_103242_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>door_open</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>612</xmin> <ymin>240</ymin> <xmax>986</xmax> <ymax>872</ymax> </bndbox> </object> </annotation>对应的YOLO txt一行一个目标,格式是class cx cy w h,四个坐标全部归一化到0到1之间:
0 0.4161 0.5148 0.1948 0.5852归一化换算公式是:cx = (xmin + xmax) / 2 / width,w = (xmax - xmin) / width,y方向同理。VOC的bndbox存像素值,YOLO存归一化值,转换过程最坑的就是除以图片宽高时的缩放问题。
这里有个容易被忽略的细节:txt里的class id顺序未必按字母序。发布者可能按自己的标注工具导出顺序编号,所以不要凭xml里的<name>猜测txt第一列数字对应哪一类,正确做法是遍历labels目录统计第一列取值,再反推classes顺序。
提示:我一般把xml转成txt之后,随机抽几张图把标注框画回去人工看一遍。这一步能同时验证坐标换算和类别对应,比训练到一半才发现类别错乱省事得多。
3. 用YOLOv8训练这个数据集:数据集yaml、超参数选择与训练日志判读
3.1 数据集yaml配置与路径组织
数据集自带的data.yaml如果存在,直接用;不存在就自己建一份。yaml内容如下:
path: /path/to/电梯数据集 train: ImageSets/Main/train.txt val: ImageSets/Main/val.txt nc: 4 names: 0: door_open 1: door_closed 2: person_in 3: person_outpath是数据集根目录路径,训练机和验证机如果目录不一样,记得改这里。train和val可以指向txt列表文件,也可以直接指向图片目录。YOLOv8读取train.txt时会逐行把文件名拼到path后面,如果txt里存的是JPEGImages/xxx.jpg这样的相对路径,注意检查拼接结果是否正确。
nc=4必须和names的数量严格一致,names的索引顺序必须和labels里txt文件的第一列数字对齐。这一条是类别错乱的头号来源,错位之后你训出来的模型会把door_open当成person_in,从loss上根本看不出来,因为loss照样在降,只有看混淆矩阵才能发现。
注意:改yaml时不要动names顺序。如果数据集发布者的类别顺序和你的业务顺序不同,宁可改labels里的标注数字,也不要改yaml里的names顺序,因为labels有2000多个文件,批量改数字比手调yaml更容易出错。
3.2 训练命令与关键超参数选择
环境装好ultralytics之后,一条命令就能起训:
yolo detect train data=./data.yaml \ model=yolov8n.pt \ epochs=120 imgsz=640 batch=16 \ lr0=0.01 patience=30 workers=4 device=0model=yolov8n.pt表示加载COCO预训练权重,这是迁移学习的关键,不要省略。2220张图属于中小规模数据集,从n或s这种小模型起步更合适,直接上yolov8l在小数据集上容易过拟合,而且训练时间翻好几倍。imgsz=640是通用选择,电梯门缝和人体都是相对较大的目标,640够用;如果原始图是1080p且想抓细小的门缝特征,可以提到800,但显存占用会明显上升。
几个关键参数的取舍参考:
| 参数 | 建议值 | 说明 |
|---|---|---|
| lr0 | 0.01 | 迁移学习常见起点,loss震荡降不下去就改0.005 |
| batch | 8~16 | 6G以下显存用8,配合accumulate=2凑足有效batch |
| accumulate | 2~4 | 显存不足时用它替代增大batch,实际batch=batch×accumulate |
| epochs | 100~150 | 看验证集mAP是否平台期,patience会提前终止 |
| patience | 20~30 | 连续N个epoch验证集无改善就停,省时间 |
| imgsz | 640/800 | 越大对小目标越友好,但显存和速度代价高 |
训练过程中如果显存报OOM,优先降batch而不是降imgsz。降imgsz会把门缝这种细长目标缩得更小,检测难度变大;降到batch=8再开accumulate=4是比较均衡的解法。
3.3 训练日志怎么读:loss曲线与指标
训练结束后,结果目录下的results.csv记录了每个epoch的完整指标,直接用pandas读:
import pandas as pd df = pd.read_csv('runs/detect/train/results.csv', comment='#') last = df.iloc[-1] print('box_loss:', last['train/box_loss'], '->', last['val/box_loss']) print('mAP50:', last['metrics/mAP50(B)']) print('mAP50-95:', last['metrics/mAP50-95(B)'])判读训练是否健康,核心看三个信号:train/box_loss和val/box_loss是否同步下降;mAP50是否在验证集上稳步上升;混淆矩阵里door_open和door_closed是否存在大面积互串。如果train/box_loss一路降但val/box_loss在某个epoch后掉头上升,就是过拟合,此时模型对训练集的细节记得太死,而不是真正学会了门状态特征。
我在这个数据集上一般以mAP50达到0.85以上作为第一步合格线,mAP50-95达到0.6左右说明模型有一定的定位精度。如果mAP50连0.7都上不去,大概率不是训练参数问题,而是标注一致性出了问题,回到第4章的清洗流程去处理。
4. 训练避坑与排查:标注一致性、BN崩溃与mAP为0的处置记录
4.1 图片与标注数量对不上、XML与txt标签不一致
现象:启动训练后日志提示大量图片找不到标注文件,或者验证集上GT框数量明显偏少。
原因:压缩包在搬运过程中丢文件,或者labels和Annotations不是同一版本导出的。常见情况是有人先导出xml,后来又重标了一版txt,导致两张目录对不上。
解决:先跑一遍核对脚本,把差集列出来再决定是删图还是补标:
from pathlib import Path jpg = set(p.stem for p in Path('JPEGImages').glob('*.jpg')) xml = set(p.stem for p in Path('Annotations').glob('*.xml')) txt = set(p.stem for p in Path('labels').glob('*.txt')) print('缺xml:', jpg - xml - txt) print('缺txt:', jpg - txt) print('多出的标注:', (xml & txt) - jpg)脚本逻辑很简单:对三个目录分别取文件名集合,用集合减法找差集。缺标注的图要么删除,要么回补;多出来的标注文件如果对应的图片不存在,要检查是不是文件名编码问题,比如jpg是1.jpg而标注是01.jpg。我一般先让脚本输出差集,再用ls -la核对文件大小,确认不是空文件。
4.2 门半开状态被归到哪一类:标注边界模糊导致loss不降
现象:训练后期val/box_loss在某个值附近来回震荡,mAP50卡在0.7上不去,混淆矩阵里door_open和door_closed互串明显。
原因:门扇半开时缝隙只有几个像素宽,标注员A把它算成open,标注员B算成closed,标签噪声直接干扰模型收敛。电梯门是金属镜面,反光也会让边界更模糊。
解决:定死判据——门扇之间有可见缝隙就一律算door_open,完全闭合才算door_closed。然后重新过一遍标注,或者至少把训练集里loss贡献大的难例筛出来复核。我自己的血泪经验是,这种业务语义边界问题,加loss权重和换模型结构都不如先把标签洗干净,标注噪声对mAP的影响远大于模型本身的选择。
4.3 BN崩溃与loss为nan
现象:训练到第20到40轮时,train/box_loss突然跳到nan,之后所有终端输出全是nan,保存的权重推理时输出全为0。也有的情况是BN层的权重出现nan,模型直接废掉。
原因:最常见是lr0设置偏大,其次是batch太小导致BN的均值和方差统计量抖动。数据里混入纯黑、纯白或损坏的图片也会触发。在电梯场景中,夜间断电时段抽出来的帧可能整张是黑的,这类图会让前向传播产生异常值。
解决:先把学习率砍半重试,同时用accumulate把有效batch提上去:
yolo detect train data=./data.yaml model=yolov8n.pt \ epochs=100 imgsz=640 batch=8 accumulate=4 lr0=0.005如果还在nan,那就检查图片质量,用OpenCV遍历JPEGImages,把灰度方差接近0的图剔除:
import cv2 from pathlib import Path for p in Path('JPEGImages').glob('*.jpg'): img = cv2.imread(str(p), cv2.IMREAD_GRAYSCALE) if img is None or img.var() < 1: print('坏图:', p)注意img is None处理的是解码失败,var() < 1处理的是全黑或几乎纯色的帧。这部分问题通常在数据侧,不要反复调参。
4.4 mAP为0或极低,但loss在降
现象:train/box_loss正常下降,但每个epoch的mAP50都是0,或者验证集结果远低于预期。
原因:最典型的两个——names顺序和标签ID错位,以及val路径指向空目录。另外如果GT框落在图像边缘,某些预处理会把超出边界的部分裁掉,导致目标消失。
解决:先写脚本把归一化坐标还原成像素坐标,检查是否都在图像范围内:
w, h = 1920, 1080 for line in open('labels/样本.txt'): c, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) print(c, x1, y1, x2, y2)如果发现大量框的宽或高小于3像素,说明在imgsz=640的缩放中被下采样吞掉了。电梯门缝在1080p原图里可能只有5像素宽,缩到640后只剩不到3像素,模型根本看不出来。这种情况把imgsz提到800或1024,并使用s以上的模型才有意义。
4.5 验证集混淆矩阵行和总是对不上GT数量
现象:验证集混淆矩阵里每行的数字加起来,不等于该类别真实的GT数量。有人拿着这个结果质疑数据集标注不对,以为漏标了。
原因:YOLO的混淆矩阵默认在置信度阈值下统计,置信度低于阈值的预测框直接被丢弃;NMS之后同一个GT目标只保留一个匹配框,其他重叠预测试图归入background列。所以行和与GT总数对不上是正常的统计口径问题,不是标注错误,代码里也没有bug。
解决:看混淆矩阵时先确认文档里conf参数的取值口径,判断类别质量要看每类recall是否都处于合理水平,以及door_open和door_closed是否互相串扰,而不是纠结行和。这个坑我换数据集时认真踩过一次,当时花了一下午查标注文件和脚本,最后才发现是统计口径,白费力气。
5. 把数据集改造成自己的任务:VOC/YOLO互转、半开状态拆分与数据量边界
5.1 VOC转YOLO的转换脚本与边界处理
这个数据集本身带了txt,但如果你拿到的是纯VOC版本,或者想从xml重新导出一份自定义类别的txt,转换脚本是刚需。下面这个脚本是我常用的模板:
import xml.etree.ElementTree as ET from pathlib import Path CLASS_MAP = {'door_open': 0, 'door_closed': 1, 'person_in': 2, 'person_out': 3} def voc_to_yolo(xml_path, out_dir): root = ET.parse(xml_path).getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) if w == 0 or h == 0: print(f'skip {xml_path}: zero size') return lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() cls = CLASS_MAP.get(name) if cls is None: raise ValueError(f'{xml_path}: unknown class {name}') bbox = obj.find('bndbox') x1 = float(bbox.find('xmin').text) y1 = float(bbox.find('ymin').text) x2 = float(bbox.find('xmax').text) y2 = float(bbox.find('ymax').text) x1 = max(0, min(x1, w)); x2 = max(0, min(x2, w)) y1 = max(0, min(y1, h)); y2 = max(0, min(y2, h)) if x2 <= x1 or y2 <= y1: continue cx = (x1 + x2) / 2 / w cy = (y1 + y2) / 2 / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f'{cls} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}') if lines: out_file = Path(out_dir) / (Path(xml_path).stem + '.txt') out_file.write_text('\n'.join(lines) + '\n')逻辑说明:先解析size拿到原图宽高,再遍历每个object,把类别名映射成数字,像素坐标转成归一化中心点加宽高。这里有三处必须处理的边界:图片宽高为零时直接跳过;坐标越界时clip到图像范围内;clip之后宽高为负或为零的框丢弃,这类框通常是标注失误,留下来训练只会产生垃圾loss。
参数说明:CLASS_MAP的顺序必须和后续yaml的names对齐,你要把person_in放第一类就改成{'person_in': 0, ...}。抛异常而不是静默跳过未知类别,是为了第一时间发现xml里的脏标签,静默跳过会让标注错误偷偷溜过去。
5.2 给门状态再加一个“半开”类别怎么办
有些电梯项目不满足于开/关二分类,还想把门扇半开这种危险状态单独检出来。这个数据集本身是4类,但你可以基于它二次加工出half_open。
思路是先用训好的模型把door_open这类框全抽出来,计算每个框的宽高比。电梯门是竖长条,全开状态下bbox的宽高比接近固定值,半开时宽度变窄,比例明显变化。按这个特征做初筛:
import xml.etree.ElementTree as ET threshold_w_ratio = 0.6 # 宽度与全开参考值之比 for xml_file in Annotations.glob('*.xml'): root = ET.parse(xml_file).getroot() for obj in root.iter('object'): if obj.find('name').text != 'door_open': continue bbox = obj.find('bndbox') w = float(bbox.find('xmax').text) - float(bbox.find('xmin').text) h = float(bbox.find('ymax').text) - float(bbox.find('ymin').text) if w / h < threshold_w_ratio: obj.find('name').text = 'half_open'注意这个方法只能做初筛,阈值需要按你的实际机位角度调节,而且必须人工复核一遍。我一般会把初筛结果和原始全开样本的宽高比分布画成直方图,用分布上的断点确定划分阈值,比拍脑袋定值靠谱。
5.3 数据量边界:2220张图能训到什么程度
2220张图、4个类别,每个类别摊下来几百个标注框,属于小型专用数据集。这个规模下有两件事需要提前心理建设。
第一,必须依赖预训练权重做迁移学习。用yolov8n.pt从头微调可以达到可用的检测效果;但如果用随机初始化的权重从零训练,收敛速度会慢得多,mAP也会低不少。这是数据量决定的,不是模型参数调得不好。
第二,这类数据集的场景局限性很强。如果机位固定、电梯型号固定,训练出的模型在同一个场景下表现良好;换一个摄像头角度,或换一款内饰不同的电梯,mAP会有明显回落。正确做法是把它当作一个场景的baseline,后续通过抽帧补充新场景数据再微调。
数据划分上,我习惯按80%训练、10%验证、10%测试切分。如果原始数据是从视频里抽帧的,相邻帧高度相似,切分时务必按时间段分桶,不要让同一段视频的相邻帧同时出现在训练集和验证集中,否则验证集上的mAP会虚高,部署到真实环境立刻现原形。
6. 调优技巧:用混淆矩阵和置信度阈值扫描把误报压下来
6.1 先读混淆矩阵,找到误报的主要来源
训练完成后,第一步不是急着调参,而是看runs/detect/train/confusion_matrix.png。重点关注两个点:door_open和door_closed是否互相串,person_in和person_out之间的混淆方向。电梯门是镜面金属,摄像头角度一变,门扇反光很容易让模型把关闭状态错判成open,这类误报在业务上非常致命,会触发联动开门或告警。
6.2 置信度阈值扫描流程
默认conf=0.25不一定适合这个业务场景,我用一段脚本遍历阈值找F1平衡点:
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') for conf in [0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4]: res = model.val(data='./data.yaml', conf=conf, iou=0.5, verbose=False) print(f'conf={conf:.2f} mAP50={res.box.map50:.3f} ' f'precision={res.box.p:.3f} recall={res.box.r:.3f}')运行后观察数值:conf越高,precision越高但recall下降,告警变少同时漏检变多;conf越低,recall上去了但误报刷屏。电梯场景我通常选F1最高的那个阈值,如果误报比漏检更不可接受,就在F1最高点的右侧再偏高一档。
那次我在一个电梯项目里图省事,直接用默认conf=0.25上线,结果轿厢门上的金属反光把door_open误报刷了整整一屏告警,物业半夜打电话过来问怎么回事。从那以后,每次换数据集、换摄像头角度,我都会强制把阈值扫描跑一遍再谈部署,这一步花不了十分钟,但能省掉一整周的返工。希望帮到你。
本文还有配套的精品资源,点击获取