简介:一套面向电梯监控场景的电动车与自行车识别项目,基于YOLO预训练模型在电梯内视角数据集上微调而成。项目同时提供检测与跟踪两条技术路线:检测方法逐帧输出目标标注结果,跟踪方法在检测基础上增加去重逻辑,更贴合连续监控需求。整体适合毕业设计、课程设计、实训或竞赛,也可作为目标检测与多目标跟踪的练手项目。资源包为zip格式,共134个文件,大小约16.96MB。其中包含Python脚本、YAML配置、JPG与PNG图像样本,以及Dockerfile、Shell脚本、Jupyter教程、CSV结果记录等辅助内容。源码结构清晰,配有说明文档与可运行的推理脚本,拿到后可按README步骤快速复现。目前已有64人学习浏览。压缩包涵盖完整工程源码、运行说明、模型训练记录与推理示例,既可直接部署验证,也便于在此基础上调整数据集或模型结构,扩展其他车辆识别功能。项目经测试运行稳定,使用中遇到问题作者会提供解答支持。
1. 电梯监控电动车识别:这个毕设项目到底包含哪些东西
电梯监控里的电动车识别,本质上是一个「小目标 + 强遮挡 + 光照突变」的垂直场景检测问题,跟普通街景识别完全不是一回事。普通场景下训练好的 YOLO 模型直接搬到电梯轿厢里往往会翻车——车把和座椅被乘梯人挡住大半,金属轿厢反光导致目标颜色失真,俯拍视角下人车重叠严重,远处推进来的车在画面里只有几十个像素。这个项目做的就是针对电梯内视角数据集对 YOLO 预训练模型进行微调,并同时提供「每帧画框」和「按 track_id 去重后只输出一次结果」两套推理方案。适合拿来做毕设、课设、实训和大作业,也适合想完整拆解垂直场景目标检测全流程的人。
2. 电梯视角数据集与 YOLO 微调:为什么不能直接拿来就用
2.1 预训练模型在电梯场景下的两个致命缺陷
很多同学拿到项目第一反应是:YOLO 预训练权重不是在 COCO 上跑得挺好吗,为什么还要单独做微调?这里有个容易被忽略的事实:COCO 的 80 个类别里根本没有「电动车」「自行车」这种细分类别,只有 bicycle 一个接近项,而且 COCO 的训练图像绝大多数是平视或略带俯视的自然场景。电梯监控是典型的顶置俯拍,视角接近 60 到 90 度,目标的外形特征比如车轮辐条、车筐、脚踏板在俯视下全部变形。
更麻烦的是遮挡问题。电梯轿厢内人车共梯时,车把经常被身体挡住,车尾被轿厢门挡住,有时候一帧画面里只能看到半个车轮和一段车座。预训练模型在 COCO 上学到的多是「完整目标」的语义特征,对这类强遮挡目标响应很弱。如果不做微调,直接拿 yolov5s.pt 去跑电梯视频,结果通常是漏检率极高,或者把行李箱、婴儿车误检成自行车。
2.2 数据集目录结构与标注格式:先搞懂 YOLO 的数据契约
项目里既然带有完整数据集,第一步是先把目录结构理清楚。YOLO 系模型的训练数据契约非常固定:每张图片对应一个同名 txt 标注文件,放在 labels 目录下,每行是「类别 id + 归一化中心坐标 x + 归一化中心坐标 y + 归一化宽 w + 归一化高 h」。这里要特别提醒:所有坐标必须除以图片宽高,存成 0 到 1 之间的小数。
dataset/ ├── images/ │ ├── train/ │ │ ├── elev_0001.jpg │ │ └── elev_0002.jpg │ └── val/ │ ├── elev_0101.jpg │ └── elev_0102.jpg ├── labels/ │ ├── train/ │ │ ├── elev_0001.txt │ │ └── elev_0002.txt │ └── val/ │ └── elev_0101.txt └── dataset.yaml标注文件内容就是纯数字,比如下面这行表示类别 0 的一个目标,中心点在图片 (0.45, 0.32) 处,宽高分别占图片的 0.32 和 0.21:
0 0.451233 0.325611 0.322114 0.213568dataset.yaml 是训练入口的数据配置文件,核心是把上面的路径和类别数量告诉训练脚本。写错这个文件是新手最常见的翻车点,尤其是类别数量 nc,项目里只识别两类,那就写 2,classes 列表里 0 对应电动车,1 对应自行车。我一般会先打印一条验证语句确认 label 文件数量和图片数量对得上,再开始训练。
# dataset.yaml path: ./dataset train: images/train val: images/val nc: 2 names: 0: electric_bike 1: bicycle2.3 标注规范:俯拍场景下类别定义必须单独约定
拆这个项目时我发现一个特别容易踩的坑:电动自行车和自行车的区分标准。电梯场景里人推着车进来时,车身一半在轿厢外是常态,画面里能看到的就是一个车把和一个前轮。这时候如果按「有没有电池」来标,标注员根本没法判断。我拆项目时建议的标注规范是:只看轮径和车架粗壮程度,粗胎、大车架标电动车,细胎、标准车架标自行车,拿不准的一律标电动车——因为从物业管理的角度,把自行车误报成电动车只是虚惊,把电动车漏报才是事故。
还要注意高度重叠的处理。两个人同时推车进电梯,两辆车在画面上有 40% 以上的 IoU,这种情况下不能只标露出来的那辆。YOLO 的标注允许目标重叠,训练时 NMS 会处理,标注时只需要保证每个可见的、能辨认出轮廓的目标都画框。如果有目标被完全遮挡到只剩不到 20% 的面积,我一般会放弃标注这一帧,而不是硬画,硬画的框会对模型产生严重的噪声信号。
3. 训练配置与指标解读:把微调跑通并读懂 results.csv
3.1 从 tutorial.ipynb 到命令行:两条路都能跑通
项目里带了一个 tutorial.ipynb,这是给初学者准备的交互式入口,按顺序执行单元格就能完成从加载权重到训练的全流程。但我个人更推荐在熟悉流程之后改用命令行跑,因为 notebook 的单元格状态容易丢失,尤其是训练到一半内核重启,之前的进度就全没了。命令行方式同样由项目源码支持,只需要进入项目根目录:
python train.py \ --data dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --project runs/train \ --name elevator_ev \ --patience 15这段命令的意思是从yolov5s.pt预训练权重出发,在dataset.yaml指定的电梯数据集上微调 100 轮,输入图片分辨率 640×640,每批 16 张。patience 15是早停参数,连续 15 轮验证集 mAP 没有提升就自动停止,这个参数在实验阶段非常有用,可以帮你省掉大量无效训练时间。训练结束后,结果保存在runs/train/elevator_ev/目录下,里面有 weights 子目录存放最佳权重best.pt和最后一轮权重last.pt。
训练过程中建议时不时看一眼终端输出,重点观察每个 epoch 末尾的验证指标。如果看到 mAP_0.5 在稳步上升但训练集 loss 已经降到很低,说明模型还在正常学习;如果 val loss 开始反弹而上 mAP 还在涨,这就是过拟合的早期信号。
3.2 超参数怎么调:从默认值出发,别一上来就乱改
项目微调的起点是官方默认超参数,保存在data/hyps/hyp.scratch-low.yaml里。这个文件里最关键的是学习率设置:lr0(初始学习率)默认 0.01,lrf(最终学习率系数)默认 0.01,表示学习率会从 0.01 按余弦退火降到 0.0001。做迁移学习微调时,我一般会把lr0降到 0.005 左右,因为预训练权重已经有很好的底层特征,学习率太大会在微调初期破坏这些特征,表现就是前几个 epoch loss 剧烈震荡。
批量大小和显存是强绑定的。8GB 显存跑img 640时 batch 设 16 比较稳,如果爆显存就把 batch 降到 8,同时把--workers设为 4 以下。这里有个血泪经验:workers 设太大在某些 Windows 机器上会导致 DataLoader 卡死,而且报错信息非常不直观,表现为训练进度条长时间不动。遇到这种情况直接降 workers 或加一句os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'。
还有一个容易被忽略的参数是--cache,加上这个参数后数据集会预加载到内存里,训练速度能提升 30% 以上。代价是占用内存,如果你的机器只有 16GB 内存而数据集又特别大,建议不用,磁盘 IO 慢一点总比内存溢出强。
3.3 results.csv 读法:六个字段代表什么
训练过程中每完成一个 epoch,项目就会往results.csv里写一行数据,这个文件在整个项目工程中承担着训练日志的角色。表头是固定的,依次为 epoch、train/box_loss、train/obj_loss、train/cls_loss、metrics/precision、metrics/recall、metrics/mAP_0.5、metrics/mAP_0.5:0.95、val/box_loss、val/obj_loss、val/cls_loss、lr/pg0 等字段。
这里最需要关注的是第 7 列metrics/mAP_0.5,它代表 IoU 阈值设为 0.5 时的平均精度,是判断模型好不好的第一指标。一般在电梯场景里,mAP_0.5 能到 0.9 以上说明模型已经能稳定检出绝大多数目标;metrics/mAP_0.5:0.95则更严格,它计算的是 0.5 到 0.95 多个 IoU 阈值的平均精度,反映框位置的精确程度,这个值在 0.6 以上算合格。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/train/elevator_ev/results.csv") df.columns = [c.strip() for c in df.columns] plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(df["metrics/mAP_0.5"], label="mAP@0.5") plt.xlabel("epoch") plt.ylabel("mAP") plt.legend() plt.subplot(1, 2, 2) plt.plot(df["val/box_loss"], label="val box loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.tight_layout() plt.savefig("training_curve.png")这段代码读完 results.csv 后画出 mAP 和验证集框损失两条曲线。判断训练是否健康的经验法则是:mAP 曲线上升后进入平台期,val loss 先下降后走平,没有明显上扬,就是正常的;如果 val loss 已经掉头向上而 mAP 还在涨,说明模型开始死记训练集了,调高--hyp里的hsv_h等增强参数或增加验证集数据量是首选解法。
4. 检测与跟踪两套推理方法:每帧画框还是按 id 去重
4.1 基于检测的方法:直白但数据冗余
项目提供了两套推理方法。第一套是基于检测的方法,逻辑很简单:对视频流逐帧做目标检测,只要某一帧出现了电动车或自行车,就把这一帧带着标注框的图像保存下来。这套方法的好处是没有任何漏报,只要有目标就一定留痕,适合事后回放查证;坏处是数据爆炸——一辆电动车在电梯里停留 30 秒,按 25 帧每秒算就是 750 张几乎一模一样的图片,给后续人工审核带来很大负担。
import cv2 import torch model = torch.hub.load("ultralytics/yolov5", "custom", path="best.pt", force_reload=True) cap = cv2.VideoCapture("elevator.mp4") frame_id = 0 while True: ret, frame = cap.read() if not ret: break results = model(frame, size=640) det = results.pandas().xyxy[0] if len(det) > 0: # 筛选置信度阈值,默认0.25,漏检多就降,误检多就升 det = det[det["confidence"] > 0.3] annotated = results.render()[0] cv2.imwrite(f"output/frame_{frame_id:06d}.jpg", annotated) frame_id += 1 cap.release()这段代码每帧推理一次,置信度阈值设的是 0.3。阈值这个参数非常敏感:设高了会把远处小目标和严重遮挡目标全部过滤掉,设低了会把不锈钢垃圾桶、轮椅误检成车。我一般先拿一段 5 分钟的真实电梯视频跑一遍,数一下误检数量再决定阈值。
4.2 基于跟踪的方法:用 track_id 做去重的核心逻辑
第二套方法在检测基础上加了跟踪,目的是去重。核心思想是:给画面里每个目标分配一个唯一的 track_id,只有目标第一次出现或者消失一段时间后重新出现时,才输出一条记录,中间的连续帧全部跳过。这样一辆电动车停留 30 秒,最终只输出 1 到 2 条有效记录,而不是 750 条。
import cv2 import numpy as np from collections import defaultdict # 跟踪状态:track_id -> 最后一次出现的帧号 last_seen = {} # 每个track_id对应的目标是否已经上报过 reported = defaultdict(bool) MAX_MISS = 30 # 目标消失超过30帧,视为离开后重新进入 def match_tracks(det_boxes, tracks): # 简单版:用IoU做框的关联,实际工程中会用卡尔曼滤波+匈牙利匹配 # ByteTrack等现代跟踪器会同时考虑高、低置信度检测框,效果更好 matched = [] for track_id, track_box in tracks.items(): best_iou = 0 for det_box in det_boxes: iou = compute_iou(track_box, det_box) if iou > best_iou: best_iou = iou if best_iou > 0.3: matched.append(track_id) return matched frame_id = 0 while True: ret, frame = cap.read() if not ret: break results = model(frame, size=640) det = results.pandas().xyxy[0] det = det[det["confidence"] > 0.3] # 用跟踪器更新轨迹(此处简化为框关联示意) tracks = update_tracks(det) # 实际使用ByteTrack/StrongSORT时调用其API for tid, box in tracks.items(): if not reported[tid]: reported[tid] = True cv2.imwrite(f"events/event_{tid}_first.jpg", frame) print(f"frame {frame_id}: target {tid} first detected") last_seen[tid] = frame_id # 清理消失的目标,超过MAX_MISS帧就重置reported标记 for tid in list(reported.keys()): if frame_id - last_seen[tid] > MAX_MISS: reported[tid] = False frame_id += 1 cap.release()这段代码演示了去重的核心思想,真实项目里会直接调用 ByteTrack 或 StrongSORT 拿到稳定的 track_id,而不是自己写 IoU 匹配。逻辑的关键在reported字典:每个 track_id 只放行一次,目标重新出现时重置标记。MAX_MISS 参数控制「多久算新目标」,电梯场景设 30 帧比较合理,因为人推车进出轿厢通常不超过 3 秒。
4.3 两套方法怎么选:看你的交付目标
选哪套方法取决于项目需求。如果是毕设或课设要展示「识别效果」,用基于检测的方法就够了,把标注帧做成视频对比图,直观且容易讲;如果是实训或竞赛要做「电动车进电梯报警系统」,必须用跟踪去重的方法,否则物业监控中心会被大量重复告警刷屏。
从项目本身的定位来看,它的完整代码同时支持两种模式,切换方式通常是一个命令行参数或配置文件开关。我拆项目时的习惯是先用检测模式跑通全流程,确认模型精度没问题,再切换到跟踪模式,缩小系统只输出关键事件。如果跟踪模式下发现同一辆车被拆成两个 id 导致重复上报,优先调整跟踪器里的max_age参数,把它从默认的 30 帧适当调大。
5. 避坑记录:五个实践中的翻车现场
5.1 小目标漏检严重
现象:电梯监控画面里,3 米外的电动车在图像中只占 60×80 像素,模型完全检测不到,但 1 米内的目标检测正常。
原因:YOLOv5 默认在 640×640 分辨率下训练,小目标经过多次下采样后特征图上的响应极弱。项目数据集中如果包含大量远距离目标,而原始标注框面积占比小,模型很难学到小目标的判别特征。
解决:把--img从 640 提高到 960 或 1280,代价是显存占用翻倍。或者预处理阶段把视频按区域切分,对轿厢门口区域单独放大后送入模型检测。实测中第二种方法对电梯场景更有效,因为轿厢结构固定,门口区域是电动车进入的必经之路。
5.2 车辆长时间不动导致跟踪丢失
现象:电动车推进电梯后停在轿厢角落,30 秒后跟踪算法把同一个目标当成新目标重新上报。
原因:目标长时间静止时,检测框位置几乎不变,但跟踪器对连续帧的相似度打分会有微小波动,一旦低于阈值就把旧轨迹删掉,重新初始化新轨迹。电梯场景恰恰是静止时间长的场景,触发频率不高但每次都是误报。
解决:把 tracking 模块的max_age和min_hits参数调大,让跟踪器容忍更多帧的短暂丢检。或者加一个「静止目标合并」的规则:如果新轨迹的初始框与历史轨迹的最后一帧框 IoU 大于 0.8,就认为还是同一目标,沿用旧 id。
5.3 results.csv 显示 mAP 很高,但实拍视频效果差
现象:训练集验证 mAP_0.5 达到 0.95,一跑到真实电梯视频里,漏检和误检明显增多。
原因:这是典型的过拟合到训练集分布。训练数据可能是同一部电梯、同一个时段、同一部手机拍的,画面背景、光照、角度都高度一致,模型学到的是「这个电梯里的电动车」,而不是「电梯里的电动车」。数据多样性不足是最隐蔽的坑,mAP 指标完全反映不出来。
解决:采集数据时至少覆盖 3 部不同品牌、不同装修风格的电梯,包含白天自然光、夜间灯光、轿厢门开合瞬间光照突变三种情况。如果数据重采成本高,至少用数据增强里的mosaic=1.0、hsv_h=0.02增大色彩扰动,让模型对光线变化更鲁棒。
5.4 Docker 跑 GPU 版本识别不到显卡
现象:用项目里的 Dockerfile.gpu 构建镜像后启动容器,程序能运行但在 CPU 上跑,帧率只有 2 FPS,nvidia-smi报错或torch.cuda.is_available()返回 False。
原因:宿主机没装 NVIDIA Container Toolkit,或者 docker run 时没加--gpus all参数。很多人只装了 NVIDIA 驱动,没装 toolkit,宿主机上能跑 CUDA 程序,容器里就不能。Dockerfile 分 cpu、gpu、arm64 三个版本,选错构建文件也是常见原因。
解决:宿主机先安装 nvidia-container-toolkit 并重启 docker 服务,然后构建时用docker build -f Dockerfile.gpu -t elevator-yolo:gpu .,运行时加--gpus all。注意--ipc=host也要加上,PyTorch DataLoader 的多进程共享内存在容器里默认配置下会报错,这个参数不加的话训练过程经常中途挂掉。
5.5 训练到一半 loss 变 NaN
现象:训练十几个 epoch 后,终端输出的 box_loss 突然变成 nan,然后整个训练崩掉。
原因:最常见的是学习率过大导致梯度爆炸,微调阶段尤其容易发生。另一个原因是模型在某个 batch 中遇上了损坏的图片,解码失败产生了异常像素值。
解决:先把lr0从 0.01 降到 0.003,重启训练。如果还崩,用--workers 0关掉多进程数据加载,定位是不是 DataLoader 的问题。要排除损坏图片的影响,可以遍历训练集,用 cv2.imread 读取每张图并检查返回值,把读取失败的图片直接移出数据集。
6. 交付前验证:用 TensorBoard 与 ONNX 导出来收尾
训练完成后,很多人只盯着 best.pt 的 mAP 就写报告了,这样还不够。项目目录里的events.out.tfevents.*文件是 TensorBoard 的事件日志,它和 results.csv 是同一份训练数据,但可视化更直观。启动方式很简单:在项目根目录执行tensorboard --logdir runs/train/elevator_ev,浏览器打开 6006 端口就能看到每个 loss 分量和指标随 epoch 变化的曲线。我习惯把 PR 曲线(Precision-Recall 曲线)单独调出来看:如果曲线在召回率 0.8 附近急剧下坠,说明模型靠提高阈值才拿到高精度,实际部署时误检风险较大;如果曲线呈现「直角矩形」形态,说明精度和召回率都很好。
还有个值得做的步骤是导出 ONNX 格式,项目源码支持python export.py --weights best.pt --include onnx。导出后可以脱离 PyTorch 环境,用 onnxruntime 在 CPU 上跑,或者部署到带 NPU 的边缘设备上。电梯门禁系统的实际情况是:大部分楼宇机房的 GPU 不可用,能跑 CPU 的 ONNX 是这个项目真正能用起来的关键一步。导出后记得验证一下输出维度对不对:YOLOv5 的 ONNX 输出形状是 (1, 25200, 7) 或类似结构,前 4 列是框坐标,第 5 列是目标置信度,后面是类别得分,用一段 10 秒的测试视频跑一遍确认结果一致再交付。
我最早拆这类电梯检测项目时,跳过 TensorBoard 验证,只看 results.csv 里 mAP 到 0.93 就提交了,结果现场演示时模型对深色电动车频繁漏检,被问得下不来台。从那以后我每次训练完都强制走一遍「TensorBoard 看 PR 曲线 + ONNX 导出 + 真实视频回放」三件套,确认没问题才敢交付。视觉检测项目的终点从来不是训练完一个模型,而是把它放进真实环境里还能稳定跑——希望这个项目的拆解过程能帮你少走这些弯路,祝顺利。
本文还有配套的精品资源,点击获取