电梯监控电动车识别:基于YOLO微调的目标检测实战解析 简介一套面向电梯监控场景的电动车与自行车识别项目基于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.210 0.451233 0.325611 0.322114 0.213568dataset.yaml 是训练入口的数据配置文件核心是把上面的路径和类别数量告诉训练脚本。写错这个文件是新手最常见的翻车点尤其是类别数量 nc项目里只识别两类那就写 2classes 列表里 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.csv3.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.01lrf最终学习率系数默认 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], labelmAP0.5) plt.xlabel(epoch) plt.ylabel(mAP) plt.legend() plt.subplot(1, 2, 2) plt.plot(df[val/box_loss], labelval 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, pathbest.pt, force_reloadTrue) cap cv2.VideoCapture(elevator.mp4) frame_id 0 while True: ret, frame cap.read() if not ret: break results model(frame, size640) det results.pandas().xyxy[0] if len(det) 0: # 筛选置信度阈值默认0.25漏检多就降误检多就升 det det[det[confidence] 0.3] annotated results.render()[0] cv2.imwrite(foutput/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, size640) 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(fevents/event_{tid}_first.jpg, frame) print(fframe {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 部不同品牌、不同装修风格的电梯包含白天自然光、夜间灯光、轿厢门开合瞬间光照突变三种情况。如果数据重采成本高至少用数据增强里的mosaic1.0、hsv_h0.02增大色彩扰动让模型对光线变化更鲁棒。5.4 Docker 跑 GPU 版本识别不到显卡现象用项目里的 Dockerfile.gpu 构建镜像后启动容器程序能运行但在 CPU 上跑帧率只有 2 FPSnvidia-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。注意--ipchost也要加上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 导出 真实视频回放」三件套确认没问题才敢交付。视觉检测项目的终点从来不是训练完一个模型而是把它放进真实环境里还能稳定跑——希望这个项目的拆解过程能帮你少走这些弯路祝顺利。本文还有配套的精品资源点击获取