简介:面向电梯监控场景的电动车与自行车识别项目,基于电梯内视角数据集对YOLO预训练模型进行微调,提供基于检测与基于跟踪两条技术路线:前者逐帧标注目标,后者在检测基础上跨帧去重,可有效减少重复告警。项目定位于计算机视觉方向的毕业设计、课程设计、工程实训或竞赛项目,适合具备一定深度学习基础、希望快速上手目标检测实战的读者。
压缩包共134个文件,主要包含Python脚本(模型训练与推理)、YAML配置(模型结构)、Jupyter Notebook教程(交互式演示)、Dockerfile(环境部署)及Markdown说明文档,同时附带图像样本、结果可视化图表、CSV训练日志等辅助资料,整体大小16.96MB,目录结构清晰,便于按模块查阅与二次开发。目前已有65人学习,具备参考价值。
资源内含完整源码、工程文件与运行说明,经测试可直接复现运行,另附可借鉴的设计报告思路。读者可快速跑通检测与跟踪完整流程,也可在此基础上扩展其他目标识别功能,适用于项目开发、结课作业或自学练手。
1. 电梯监控视角识别电动车与自行车:这个项目到底解决什么问题
电梯轿厢监控里的电动车/自行车识别,是物业管理和消防改造里一个很具体的真实诉求——电动车进电梯容易引发电池起火,物业靠人盯监控完全不现实。这个项目用电梯内视角数据集对 YOLO 预训练模型做微调,让模型在俯拍、暗光、人员遮挡的监控画面里也能稳定识别两轮车。项目同时提供了检测和跟踪两条推理链路:检测方法针对每一帧有目标的图像输出标注结果,跟踪方法在此基础上跨帧去重,避免同一辆车连续几十帧被重复上报。对做毕业设计、课程设计、工程实训或者竞赛的人来说,这套工程的价值在于它不是半成品:数据、训练日志、指标 CSV、多平台 Dockerfile、教程 notebook 都齐,照着跑就能复现,改也改得动。
2. 基于电梯轿厢数据微调 YOLO:为什么预训练权重不能直接拿来用
2.1 电梯视角的视觉难点
监控摄像头装在轿厢顶部角落,视角是斜俯拍,和自然场景数据集里的正视角、平视角差别很大。电梯里的两轮车有四个明显的视觉干扰:
第一,俯角导致目标形变。车身从正上方斜着看过去,轮子、车把、踏板的比例完全变形,通用目标检测器在 COCO 这类数据上学到的"自行车"特征,在俯拍画面里经常对不上。第二,暗光与反光。很多电梯轿厢灯光偏暖偏暗,金属门和不锈钢墙面会产生大面积反光,目标边缘在这种条件下会丢失,预训练权重没有见过这种光照分布,直接推理时置信度会被拉低。第三,遮挡密集。电梯空间小,人推着车进来时人和车重叠严重,车身被乘客身体挡住一大半,检测器只有看到完整的车把或车座特征才能给出高分。第四,小目标占比高。电梯监控分辨率一般不超过 1080P,车出现在画面远端时只有几十个像素,常规 YOLO 的浅层特征图对这种小目标响应弱。
所以这个项目的核心思路是:用电梯内视角数据对 YOLO 预训练权重做微调,而不是拿 COCO 权重直接硬上。微调的本质是让模型在保留通用特征的基础上,重新学习电梯场景下的边缘纹理、光照分布和尺度分布。这一步决定了后续检测和跟踪的效果上限,训练数据越贴近真实轿厢监控,微调收益越大。
2.2 数据集准备与标注格式
微调的第一步是数据组织。电梯视角数据集一般按 YOLO 格式整理:每张图像对应一个同名 txt 文件,每行写class x_center y_center width height,坐标是归一化后的相对值,类别从 0 开始编号。项目里把自行车和电动车作为两个独立类别处理,这个设计比"统一叫两轮车"更合理——因为电梯管理方往往需要区分是电动自行车还是普通自行车,前者涉及充电电池消防安全,后者影响相对小。
# 单张图像对应的标注文件 20230411_093215.txt # 类别0为自行车,类别1为电动车,后四个值为归一化坐标 0 0.4825 0.6133 0.2140 0.1821 1 0.7150 0.4026 0.1872 0.1454每个数字的含义要对照图像确认:x_center 和 y_center 是目标中心点相对图像宽高的比例,width 和 height 是框的宽高占比。标注完之后我一般会抽查 10% 的样本,用可视化脚本把框画回原图,重点看两个地方:边界框是否紧贴车身而非只框住车轮,类别编号是否和训练配置里的 names 列表一一对应。
数据划分上,我会按 8:1:1 切训练集、验证集、测试集。因为是电梯监控连续帧,要特别注意:同一个时间段抽出的帧不能同时落在训练集和验证集里,否则视频相邻帧的高度相似性会导致验证指标虚高。常见做法是按视频片段切分,而不是按单帧随机切,否则你看到的 mAP 只是"记忆"出来的,不是泛化出来的。
2.3 微调策略与训练配置
微调 YOLO 有两种常见策略:冻结骨干只训练检测头,或者全量微调。电梯场景的数据量通常不大(几百到几千张),如果全部解冻训练,骨干特征容易被小数据集带偏,产生过拟合。比较稳的做法是:第一轮冻结骨干网络训练头部,等损失不再下降后,解冻骨干用低学习率做整体微调。
训练时的几个关键参数:
| 参数 | 常见取值 | 说明 |
|---|---|---|
| image size | 640x640 | 保持 YOLO 默认输入尺寸,监控帧尺寸不同时先 resize |
| batch size | 16 或 32 | 看显存,8GB 显存建议 16 |
| epoch | 100~300 | 以小数据集看验证集 mAP 不再上升为准 |
| learning rate | 0.01 起步 | 解冻微调阶段降到 0.001 |
| patience | 20~30 | 验证集指标连续不增则提前停止 |
训练日志通过 events.out.tfevents 落到 TensorBoard,同时 results.csv 记录每个 epoch 的 box_loss、cls_loss、mAP50 等指标。项目里这两个文件都在根目录,跑完训练直接可以复盘指标曲线。这里有个容易被忽略的点:如果训练集里自行车数量远多于电动车,模型会偏向自行车。我在类似项目里的做法是检查类别频率分布,对少样本类做重复采样或复制增强,保证两类样本在每轮迭代里出现的比例接近,否则"电动车识别不出来"这个毕设答辩最常见的质疑就会落在你头上。
3. 检测与跟踪两套推理链路:逐帧标注与跨帧去重各管什么
3.1 检测链路:每一帧独立推理
检测方法的链路最直接:对视频流逐帧取图,送到模型推理,如果某一帧检测到目标实例,就把带有边界框和类别标签的标注图像输出。对应的核心代码大致是:
import cv2 from ultralytics import YOLO model = YOLO("best.pt") # 加载微调后的权重 cap = cv2.VideoCapture("elevator_cam.mp4") frame_id = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=0.35, imgsz=640) # 逐帧检查是否检测到目标 if len(results[0].boxes) > 0: annotated = results[0].plot() # 绘制边界框与类别 cv2.imwrite(f"outputs/frame_{frame_id:05d}.jpg", annotated) frame_id += 1 cap.release()这段代码的逻辑分三步:读帧、推理、判断有没有目标。conf=0.35 是置信度阈值,低于这个值的框会被丢掉;电梯场景里我一般把阈值放在 0.3 到 0.45 之间——太高漏检,太低会出现把轮椅、婴儿车误报成电动车的干扰。imgsz=640 要和训练时的输入尺寸保持一致,否则模型会先做内部 resize,推理速度受影响。
检测方法最大的问题是重复上报。电梯里一辆电动车从进门到出门,可能停留 30 秒,按 25fps 算就是 750 帧,每一帧都会被输出一张标注图。如果后端接的是告警系统,物业会收到几百条针对同一辆车的告警,这就是检测方法的使用边界——它适合人工复核场景,不适合直接接告警业务。
3.2 跟踪链路:按 ID 去重
跟踪方法在检测结果之上加了一层去重逻辑。思路是:先用检测器得到每帧的目标框,再把这些框交给跟踪器(项目里常见的是基于 IoU 的简单跟踪或 ByteTrack 这类主流方案),为每个目标分配一个稳定的 ID。系统只在某个 ID 第一次出现时上报,或者在目标离开画面后再统计一次,这样一辆车在同一段视频里只算一次。
from collections import defaultdict track_history = defaultdict(int) report_frames = {} for frame_id, detections in enumerate(frame_batches): for box in detections: track_id = tracker.update(box) # 给目标分配或复用 ID if track_id not in report_frames: report_frames[track_id] = frame_id # 新 ID 首次出现,记录上报 save_report(track_id, box, frame_id)去重逻辑要看清楚一个边界:这里的"去重"不是把所有连续帧的检测都删掉只剩一张图,而是保证"同一物理目标"跨帧只上报一次。判断是否是同一物理目标,靠的是跟踪器的 ID 分配——同一辆车因为遮挡短暂丢失再出现时,跟踪器可能给它一个新 ID,导致重复计数。这种情况的调参方向不是降低检测阈值,而是调跟踪器允许目标丢失的最大帧数(max_age)。常见取值是 5 到 30 帧,值越大,抗遮挡能力越强,但也越容易把前后两辆不同的车串成同一个 ID。
电梯场景里目标运动速度慢、轨迹相对简单,IoU 匹配这类轻量跟踪器在 CPU 上也能跑得动,比重新训练一个 ReID 模型划算得多。如果你用的环境支持 GPU,ByteTrack 也够用,它的优势在于不依赖外观特征,纯靠运动关联,在这种低帧率监控场景下稳定性反而更好。
3.3 两条链路怎么选
检测链路适合做可视化展示和人工复核,输出的是逐帧证据,答辩时可以直接放标注视频;跟踪链路适合接真实的告警业务,输出的是一条条去重后的"电动车进电梯"事件记录,附带首现帧和时间戳,后面接短信或声光报警不会刷屏。
实际工程里两条链路共享同一个微调权重,差别只在后处理层。项目的代码组织方式也是这么设计的:检测部分是一个完整可独立运行的推理脚本,跟踪部分在检测结果之上追加跟踪器逻辑,所以你可以先跑检测链路确认模型效果,再切跟踪链路验证去重效果,调试起来不用动模型。这个分层设计我认为是整套工程最值得借鉴的地方。
4. 工程文件落地解析:Dockerfile 与训练产物怎么配合
4.1 工程结构先说清楚
拿到项目包后,根目录下几个文件的定位要分清,不要看见文件就急着跑:
| 文件 | 作用 |
|---|---|
| events.out.tfevents.* | TensorBoard 训练日志,复盘损失曲线和图像预测用 |
| results.csv | 每轮 epoch 的训练/验证指标表,直接看收敛情况 |
| Dockerfile / Dockerfile-cpu / Dockerfile.gpu / Dockerfile-arm64 | 四种构建入口,对应不同运行环境 |
| tutorial.ipynb | 端到端教学 notebook,按顺序跑即可 |
| dockerfile(小写) | 注意,项目里同时有大小写两个 dockerfile,内容可能不同 |
这里有一个坑:项目里同时存在Dockerfile和dockerfile两个文件。在 Linux 下两者是完全不同的文件,但在 Windows 和 macOS 的默认文件系统里大小写不敏感,解压时可能互相覆盖。我拿到压缩包的第一件事是先用ls -la看真实文件名,确认哪个是有效入口,再决定构建用哪个。
4.2 Docker 镜像矩阵怎么选
项目提供了 CPU、GPU、ARM64 三种镜像构建文件,对应三种运行场景:
- Dockerfile-cpu:纯 CPU 推理,适合没有独显的课设演示,跑一帧大约几百毫秒,够用。
- Dockerfile.gpu:CUDA 加速,训练和实时推理推荐用这个,依赖宿主机装好 nvidia-container-toolkit。
- Dockerfile-arm64:目标用于 Jetson 或 ARM 开发板,可以部署到电梯场景的边缘设备上。
构建命令按需选择:
# GPU 版本 docker build -f Dockerfile.gpu -t elevator-yolo:gpu . # CPU 版本 docker build -f Dockerfile-cpu -t elevator-yolo:cpu . # ARM64 版本(在 ARM 机器上构建) docker build -f Dockerfile-arm64 -t elevator-yolo:arm64 .-f参数指定 Dockerfile 路径,-t是镜像名和标签。如果直接跑docker build .不带-f,默认只会找名为Dockerfile的文件,这也是前面说要注意大小写文件的原因——构建工具对文件名敏感,一旦找错文件,装进去的依赖就全对不上。
构建完之后启动容器做推理:
docker run --gpus all -v $(pwd)/data:/app/data elevator-yolo:gpu python predict.py --source data/test.mp4--gpus all只在 GPU 版本下生效,CPU 容器加了这个参数会直接报错。-v把宿主机的数据目录挂载进容器,避免把视频文件拷进镜像里,这是训练类容器最常见的用法。如果你在 Windows 上跑,$(pwd)要改成${PWD}或直接写绝对路径,否则挂载路径解析不了。
4.3 results.csv 与 events.out 怎么对照着看
训练完成后,results.csv 每行是一个 epoch 的记录,列包含 train/box_loss、train/cls_loss、metrics/precision、metrics/recall、metrics/mAP50 等。我复盘训练时习惯先看三列:box_loss、mAP50、val/box_loss。
box_loss 持续下降但 val/box_loss 中途反弹,是过拟合的典型信号。mAP50 在 0.85 以上,对这个场景属于可用的水平;如果稳定在 0.9 以上,拿去跑电梯视频基本不会有大的漏检。同时留意 precision 和 recall 的差值——差值过大说明模型在"框得很准但漏得多"或者"框得全但误报多"之间偏科,偏科方向决定你要调 conf 阈值往哪边挪。
events.out 文件是 TensorBoard 的原始事件日志,它和 results.csv 记录的是同一轮训练的数据,只是格式不同。用 TensorBoard 可以看训练过程的损失曲线实时变化:
tensorboard --logdir . --port 6006启动后在浏览器打开 6006 端口,能看到 images 标签里的预测样本图,这一步在答辩演示里很有说服力,因为你可以直接展示模型对具体电梯帧的边界框可视化,比光念指标生动得多。需要提醒的是,events.out 文件名里的1535430.0是进程 PID,同样的训练配置再跑一次文件名会变,这是正常现象,不代表数据损坏。
5. 避坑排查:训练失效、误检漏检与 Docker 环境的五个坑
5.1 现象:验证集 mAP50 很高,但实际电梯监控视频漏检严重
原因:数据划分时按单帧随机切分,验证集里混入了与训练集来自同一段视频的相邻帧。视频连续帧高度相似,验证指标虚高,模型根本没学会泛化,只是把训练帧"记住"了。
解决:改成按视频片段划分数据。同一段监控视频的帧必须全部落在训练集或验证集,不能让同一辆车在训练和验证里同时出现。切分代码:
# 按片段划分:每个片段是一个视频的连续帧集合 video_segments = load_video_segments("dataset") train_segs, val_segs = train_test_split(video_segments, test_size=0.1, random_state=42)5.2 现象:跟踪方法把一辆车重复计数了好几遍
原因:跟踪器的 max_age 设得太小,车辆被乘客遮挡后短暂丢失,跟踪器判定目标离开,重新出现时分配了新 ID,于是同一辆车被当成两辆上报。
解决:把 max_age 调到覆盖一次遮挡时长的水平。电梯场景里乘客在车前面走动造成的遮挡通常持续 10~30 帧,把 max_age 设为 25 到 30 比较稳。改完后再用同一段视频回归验证,确认重复计数次数下降。
5.3 现象:GPU 容器启动报 CUDA 无法初始化
原因:宿主机没有安装 nvidia-container-toolkit,或者 Docker 的 GPU 运行时没启用,容器里根本访问不到显卡。很多人在这一步翻车,是因为只看宿主机的nvidia-smi能输出就以为容器也能用,其实容器访问 GPU 需要额外配置运行时,这是两个层面的事。
解决:先确认宿主驱动正常,再安装 nvidia-container-toolkit 并重启 Docker。跑docker info看输出里有没有Runtimes: nvidia字段,有才算配置成功。
5.4 现象:TensorBoard 打开后曲线是空的
原因:events.out 文件与日志目录层级不对,或者训练中断导致事件文件没写完整。另一个常见情况是根目录同时存在多个 events 文件,TensorBoard 把它们叠加显示,曲线被拉平看不出趋势。
解决:用ls -la确认 events.out 文件和你启动 tensorboard 的目录在同一层,--logdir就指向那一层,不要指到外层。多个文件混在一起时,用--logdir_spec按子目录指定单独看某一轮训练,把历史日志物理隔开。
5.5 现象:婴儿车、轮椅被误识别成自行车
原因:两类目标在外形上确实有重合区间——都有轮子、有框架,且电梯俯拍视角下形变后特征更接近。置信度阈值设得太低时,低分候选框被放进来,误检就出现了。
解决:把 conf 阈值从 0.25 提到 0.4 左右,并且在标注阶段补拍一些电梯里出现婴儿车、轮椅的负样本帧,标记为背景。负样本能在训练阶段就让模型学到"这类外形不是自行车",这个手段比单纯调阈值更治本。
6. 用 tutorial.ipynb 走一遍端到端验证:从 TensorBoard 到 results.csv
tutorial.ipynb 是项目自带的端到端教学入口,顺序跑下来就能把权重、训练日志、指标 CSV 串起来。我拿到项目包后第一步不是看代码细节,而是按 notebook 的 cell 顺序从头跑一遍,确认环境依赖完整,再动手改自己的数据。
验证环节我习惯按三条线做。第一条线看训练指标:打开 results.csv,直接看最后一行的 mAP50 和 mAP50-95,mAP50 达到 0.85 以上说明模型在这个数据分布上收敛得不错;同时对比 box_loss 和 val/box_loss 的走势,两条曲线都平缓下降才是健康状态。第二条线看可视化:用 TensorBoard 打开 events.out,挑几张验证集的预测图看边界框是否贴合车身,重点看俯拍角度下车轮和车把的位置——这两个部位是电梯视角下最容易漏检的点。第三条线跑行为链路:抽一段 30 秒的电梯监控视频片段,依次用检测模式和跟踪模式各跑一遍,统计输出数量差异。检测模式输出的标注帧数会比跟踪模式大一个数量级,这个数字差异本身就是去重效果的证明。
如果 notebook 里有计时输出,顺便记录一下单帧推理耗时。CPU 环境下单帧超过 300 毫秒,说明模型可以用于离线分析,但要接实时告警就得换 GPU 或 ARM64 设备。这个性能指标在毕设答辩里也很实用,评审老师关心的不只是"能不能识别",更关心"能不能落地到真实监控环境"。
我自己做过类似项目,最深刻的教训是:不要跳过ls -la检查文件这一步就直接解压运行。那次我图省事,在一个 Windows 环境里解压了同时含大小写 dockerfile 的压缩包,结果两个文件互相覆盖,构建镜像时依赖装得乱七八糟,排查了一整晚才发现是文件名大小写被系统折叠导致的。从那以后,凡是拿到这种压缩包,我都强制先列目录、核对文件名,再决定从哪条链路开始跑。这个习惯帮我省了无数个深夜。希望帮到你。
本文还有配套的精品资源,点击获取