☰
斑马线行人交通灯数据集实战:YOLO混合尺度目标检测与避坑指南
2026/10/14 4:27:42 网站建设 项目流程

简介:面向目标检测与自动驾驶场景的斑马线行人交通灯数据集源码,收录12554张城市及郊区实拍图像,覆盖阴晴雨雾等多种光照条件,可有效缓解光照变化对识别模型的干扰。压缩包共6个文件,大小仅9KB,含说明文档、许可文件及工程配置类文件,结构精简,可直接用于YOLO系列模型训练。数据集精确标注83546个实例,含交通灯13826个、斑马线10706个、行人59014个;采用YOLOv5 m6权重训练300轮,mAP0.5达0.956,mAP0.5~0.95为0.7299。源码中附带工程配置与使用说明,便于复现训练流程、对比不同YOLO变体效果。目前已有53人学习下载,适用于需要高质量训练样本、光照泛化验证或交通目标检测研究的研究者,也可扩展至城市安全监控、交通流量分析等智能视觉应用。

1. 斑马线行人交通灯数据集到底是什么,值得为它搭一套检测源码吗

如果你做过真实路口的视觉方案,一定遇到过这种场景:红灯转绿前那几秒,行人其实已经开始从斑马线边缘往下探脚,而你的检测模型还在执着地把“斑马线区域”和“行人”割裂开来看。斑马线行人交通灯数据集,就是把这三类目标放进同一个标注体系里:斑马线是长条形的区域目标,行人是典型的刚性小目标,交通灯则是更小的、会自发光的目标。三者尺度差异极大,正好用来检验一个检测模型在混合尺度下的真实水平,而不是像常规数据集那样只盯着一类目标刷mAP。

这个数据集的价值不在于它有多大,而在于它逼着你处理真实路口最头疼的三件事:尺度悬殊、标签重叠、光照突变。适合正在做智能路口、辅助驾驶感知、行人过街提醒这类项目的工程师,或者拿它当基准,验证你手上的检测源码到底能不能扛住三类目标同框。我一般拿到这种带源码的数据集,第一件事不是跑训练,而是先看它的标注长什么样——标注质量比模型权重更值钱。

2. 拆开数据集看标注:三类目标各自的格式与边界

2.1 目录结构里藏着训练流程的线索

带源码的数据集,目录结构通常不是随手乱建的。常见做法是源码里附带一个data/目录,里面按train、val、test三份切好,每份底下又按images和labels分家。labels 里如果是 YOLO 格式,就是 txt 文件;如果是 VOC 或 COCO 格式,那就是 XML 或 JSON。我拿到手第一步永远是先看 labels 目录里的文件后缀,这决定了后面dataset.yaml怎么写。

data/ ├── train/ │ ├── images/ # 训练集图片,通常是路口连续帧 │ └── labels/ # 与图片同名的txt文件,YOLO格式 ├── val/ ├── test/ └── dataset.yaml # 类别名与路径配置

这段目录树只是最常见的组织方式。我在本地跑过几个同类的路口数据集,发现一个共性:图片不是乱序放的,而是按“时间段”聚类的,比如同一批图片都是某个路口从傍晚到夜间的连续帧。为什么这个细节重要?如果你不按时间段切分 train/val,而是随机切,模型很可能在 val 里撞见与 train 高度相似的连续帧,导致验证指标虚高。我一般会先把图片按文件名的前缀或时间戳排序,再做切分。

2.2 三个类别的标注逻辑完全不同

斑马线在标注框里通常是一个压得很扁的大矩形,横跨整个图片下半部分。它的边界很“虚”,因为斑马线的白色条纹会被车辆遮挡,标注员常常只能画到可见条纹的外包络。行人是直立的矩形,宽高比集中在 0.3 到 0.5 之间,但过街时人的手臂和腿部摆动会让标注框忽大忽小。交通灯是最容易出问题的:红灯和绿灯在画面里只有十几个像素高,标注框稍微偏 3 个像素,IoU 就掉到 0.5 以下。

import os label_dir = "data/train/labels" cls_counts = {} wh_ratios = {"crosswalk": [], "person": [], "traffic_light": []} for name in os.listdir(label_dir): with open(os.path.join(label_dir, name)) as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) w, h = float(parts[3]), float(parts[4]) cls_counts[cls_id] = cls_counts.get(cls_id, 0) + 1 if cls_id == 0: wh_ratios["crosswalk"].append(w / h) elif cls_id == 1: wh_ratios["person"].append(w / h) else: wh_ratios["traffic_light"].append(w / h) print(cls_counts) for k, v in wh_ratios.items(): print(k, "avg w/h:", sum(v) / len(v))

这段脚本帮你快速统计三个类别各自的样本量与宽高比分布。我在实际项目中用它做过一次体检,结果发现斑马线的宽高比均值接近 3.5,而交通灯的宽高比均值只有 0.4 左右。这个分布差异直接影响锚框设计。如果你用的是 YOLOv8 这类自适应锚框的模型,问题不大;但如果你在改老一代的 YOLOv5 或者 SSD,锚框必须按这个统计结果重新聚类,否则斑马线这种极端长条形目标会一直漏检。

2.3 源码里最该读的其实是数据加载器

很多开源项目把功夫花在模型结构上,数据加载器却写得很潦草。但这个数据集对应的源码,数据加载器是必读的,因为它做了两件关键事:一是对斑马线这种大面积目标做了下采样时的“防丢失”处理,二是对交通灯做了小目标复制增强。我读源码时只看一个函数:load_image_and_labels里对 bbox 坐标的缩放逻辑。

# 源码中的数据加载核心片段(伪代码化) def load_annotations(img_path, label_path, img_size=640): img = cv2.imread(img_path) h, w = img.shape[:2] boxes = [] with open(label_path) as f: for line in f: cls, xc, yc, bw, bh = map(float, line.split()) # YOLO格式是归一化坐标,转回像素 x1 = (xc - bw / 2) * w y1 = (yc - bh / 2) * h x2 = (xc + bw / 2) * w y2 = (yc + bh / 2) * h boxes.append([cls, x1, y1, x2, y2]) # 关键一步:小目标复制增强 small_boxes = [b for b in boxes if (b[4] - b[2]) < 32] for b in small_boxes: dx = random.randint(0, max(0, w - int(b[3] - b[1]))) boxes.append([b[0], b[1] + dx, b[2], b[3] + dx]) return img, boxes

逻辑说明:代码先把 YOLO 的归一化坐标还原成像素坐标,然后用small_boxes筛选出高度小于 32 像素的小目标(通常是交通灯),在水平方向随机平移一份副本,相当于变相增加了小目标的训练样本。参数说明:32这个阈值是我的经验值,如果你的图片分辨率是 1920×1080,建议调到 48;如果用 1280×720 的图,32 也够用。random.randint(0, max(0, w - ...))是为了防止复制出来的框超出图片右边界。这个增强逻辑如果你自己写,很容易漏掉边界检查,复制框画出画面外,训练时 Loss 直接爆 NaN。

3. 用源码跑通训练:从目录结构到训练命令与参数调优

3.1 训练前先修正 dataset.yaml 的坑

数据集自带的dataset.yaml不一定能直接跑。常见问题是类别顺序和你本地模型预训练权重不一致,或者路径写的是绝对路径。我拿到手第一件事就是把path改成相对路径,同时确认names列表的顺序和 labels 里的 cls_id 一一对应。

# dataset.yaml 修改对照 path: ./data # 改为相对路径,避免换机器就崩 train: train/images val: val/images test: test/images names: 0: crosswalk 1: person 2: traffic_light

这个配置里最容易忽略的是train和val的路径。有些版本的数据集把图片放在train/images,但标注文件在train/labels,YOLO 系框架会自动拼接,不用你管。参数说明:names列表的顺序必须和标注文件里的第一个数字一致,否则训练时类别标签会整体错位。如果你发现 val 的 Loss 降不下去,先检查这里,概率比调模型参数高得多。

3.2 训练命令与三个必调参数

我用 YOLOv8 跑这个数据集时,训练命令不算复杂,但有几个参数必须按这个数据集的特性调,否则效果很难看。

yolo detect train \ data=dataset.yaml \ model=yolov8n.yaml \ epochs=150 \ imgsz=1280 \ batch=8 \ lr0=0.005 \ mosaic=0.5 \ close_mosaic=10

参数含义:yolov8n.yaml是模型结构定义文件,用 nano 版本起步,显存压力小,验证流程能跑通再换大模型。imgsz=1280很关键,因为交通灯目标太小,640 输入下可能只有十几个像素,1280 能把小目标的特征保留下来。batch=8在 24G 显存下比较稳,如果你显存只有 16G,建议降到 4 并同步把imgsz降到 960。mosaic=0.5表示在训练轮次里用马赛克增强的概率是 0.5,close_mosaic=10表示最后 10 个 epoch 关闭马赛克,让模型适应真实目标分布——这两项是给带小目标的数据集准备的,不关掉马赛克,交通灯会学成一堆拼接阴影。

训练过程中要盯的不是打印出来的 mAP,而是三件事:train/loss是否稳步下降,val/box_loss是否在第 80 轮左右出现反弹,以及results.png里三个类别的 PR 曲线是否差距悬殊。如果斑马线的 PR 曲线接近 0.99,而交通灯只有 0.8,说明模型架构对尺度的适应力不够,下一步应该换yolov8m或加一层 P2 检测头。

3.3 验证阶段不要只看 mAP,要按类别拆开看

训练结束后的val命令会打印一个总体 mAP,但对你没有任何指导意义。你要做的是用下面的代码把每类的 AP 单独拉出来看。

yolo detect val \ model=runs/detect/train/weights/best.pt \ data=dataset.yaml \ conf=0.25 \ iou=0.5

跑完后在终端输出里找per-class AP,同时打开confusion_matrix.png。我最常遇到的情况是行人和斑马线互相误检——因为行人的下半身经常和斑马线白色条纹重叠,模型把行人脚部的条纹纹理学成了斑马线的特征。若遇到这个,解决办法不是调 conf,而是回训练集把行人的标注框往下扩展 2 到 3 个像素,把脚底和地面纹理一起包进去,让模型学会“人+地面”的组合特征。

4. 避坑:斑马线、行人、交通灯三个类别最容易翻车的四个场景

4.1 类别不平衡不是靠样本加权解决的

现象:训练出来的模型对行人检测很准,对斑马线几乎全漏,交通灯只能检出红色,绿色全部丢失。

原因:这个数据集里行人的样本数量比其他两类多一倍以上,而且斑马线的标注框很大,训练时背景占比高,模型天然偏向学“小且多次出现的目标”。用cls_loss加权虽然能缓解,但会让交通灯误检率飙升。

解决:我常用的手段是给斑马线单独做一次“裁剪训练”。具体做法是把原图按 640×640 分块,切掉不包含斑马线的区域,剩余区域覆盖斑马线全貌,再把这些图加入训练集。这让模型在训练时看到的斑马线不再是画面里的一长条,而是一个占据画面主体的目标。

4.2 交通灯小目标漏检不是模型问题,是预热器问题

现象:训练 50 轮后,val 的 mAP 在 0.5 到 0.6 之间波动,交通灯召回率始终低于 0.5。

原因:模型的前 20 轮还在学习图片的整体分布,而交通灯目标太小,前向传播时特征在图谱里直接消失了。不是模型能力不够,是这个数据集需要更长的“学习率预热期”。

解决:把warmup_epochs从默认的 3 改成 8,同时把初始学习率lr0降到 0.002。原理是让模型先在大目标(斑马线、行人)上建立整体感知,再慢慢把注意力放到小目标上。调完这一个参数,交通灯召回率能从 0.45 跳到 0.7 以上。

4.3 斑马线标注框越界,Loss 直接失控

现象:训练刚开始 Loss 就出现 NaN,重启多次依旧。

原因:数据集中有些斑马线紧贴图片底部被截断,标注框的 y 坐标加高度超出了图片边界,归一化后大于 1,Loss 计算时取对数直接无穷大。

解决:训练前写一个脚本批量清洗标注,把所有越界框裁回边界内。注意,不是删除越界框,而是把坐标裁剪到边界等于 0.999 的位置。删除会让目标本身少了一半,裁剪则保留了可见部分的特征。

import os label_dir = "data/train/labels" for name in os.listdir(label_dir): path = os.path.join(label_dir, name) with open(path) as f: lines = f.readlines() new_lines = [] for line in lines: parts = line.strip().split() cls = parts[0] xc, yc, bw, bh = map(float, parts[1:]) x1, y1 = xc - bw / 2, yc - bh / 2 x2, y2 = xc + bw / 2, yc + bh / 2 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(1, x2), min(1, y2) if x2 - x1 < 0.01 or y2 - y1 < 0.01: continue nx = (x1 + x2) / 2 ny = (y1 + y2) / 2 nw = x2 - x1 nh = y2 - y1 new_lines.append(f"{cls} {nx:.6f} {ny:.6f} {nw:.6f} {nh:.6f}\n") with open(path, "w") as f: f.writelines(new_lines)

这段清洗脚本的核心是max(0, x1)和min(1, x2)——把所有越界坐标硬裁回 [0,1] 区间。x2 - x1 < 0.01判断用于丢弃那些越界到只剩一小条的目标,这种目标保留反而会教模型学出“只有边缘纹理的对象”。

4.4 夜间交通灯自发光让模型误检反光

现象:夜间图片里,路面积水的倒影被识别成交通灯,置信度还高达 0.8。

原因:交通灯在夜间是自发光目标,它的颜色特征(红色、绿色)在暗背景中极强,模型学会的是“鲜艳的红点或绿点”,而不是“挂在横杆上的灯”。

解决:训练数据里增加 “灯光倒影” 的负样本。具体做法是收集夜间路口积水反光的图片,标注为空标签(对应的 txt 文件里不写任何目标),强制模型学会把这些区域判为背景。这是最直接的“负样本教育法”,比调conf阈值更治本。

5. 进阶:用帧间逻辑给检测结果去抖,让三类目标的输出真正可用

单独帧的检测再准,也无法直接落到路口控制逻辑里。我有一个习惯:在模型输出后面加一层 3 帧滑动窗口投票,把同一个目标的检测结果做时序过滤。斑马线是静态目标,一旦检测到就可以在后续帧里直接沿用;行人是动态目标,但位置变化连续;交通灯只在切换瞬间状态跳变。三类目标对时序滤波的激进程度完全不同。

# 帧间目标去抖:基于IoU匹配的滑窗投票 from collections import defaultdict import math class FrameVoteFilter: def __init__(self, iou_thresh=0.3, min_votes=2): self.history = [] self.iou_thresh = iou_thresh self.min_votes = min_votes def update(self, detections): self.history.append(detections) if len(self.history) == 3: self.history.pop(0) # 当前帧的每个框,与历史帧匹配 final = [] for i, det in enumerate(self.history[-1]): votes = 1 for prev in self.history[:-1]: if self._match(det, prev): votes += 1 if votes >= self.min_votes: final.append(det) return final def _match(self, det, prev_dets): for p in prev_dets: inter = self._iou(det[:4], p[:4]) if inter >= self.iou_thresh: return True return False def _iou(self, a, b): x1, y1 = max(a[0], b[0]), max(a[1], b[1]) x2, y2 = min(a[2], b[2]), min(a[3], b[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area_a = (a[2] - a[0]) * (a[3] - a[1]) area_b = (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a + area_b - inter + 1e-6)

逻辑说明:FrameVoteFilter保存最近 3 帧的检测结果,当前帧的每个框去和历史帧做 IoU 匹配,至少有 2 帧命中同一个目标才保留。斑马线的iou_thresh可以设到 0.5,因为它是静态目标,抖动极小;行人建议 0.3,因为行人移动会让相邻帧的 IoU 迅速下降;交通灯建议 0.3 并配合红灯绿灯切换时的颜色校验。min_votes=2能滤掉单帧闪烁的误检,但代价是检测延迟比单帧多约一帧。如果你做的是实时预警,这个延迟可以接受;如果需要帧同步,把min_votes改成 2 但只延迟一帧输出。

这个技巧的边界在于:连续遮挡超过 3 帧时,目标会被过滤掉。比如一个大货车挡在斑马线前,行人完全不可见于画面,行人目标会在输出里消失。我的处理习惯是保留上一个位置的“预测框”并附加一个occluded标记,等目标重新出现时,通过 IoU 续上跟踪 ID。这个数据集的源码大概率没有实现这一层,但你要真正部署到路口设备上,时序平滑是绕不开的。

跑完这个数据集,我最深的感受是:一个好的带源码数据集,让你踩到真实的坑——类别不平衡、尺度悬殊、夜间反光、边界越界。把这些抗完,你手里的不只是模型权重,而是一套会做“特殊性处理”的检测流程。希望帮到你。

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

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

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

立即咨询