简介:这是一份围绕电梯监控视角下电动车与自行车识别的人工智能毕业设计项目包,适合计算机视觉方向的本科生、研究生,以及希望用完整案例入门深度学习的开发者。项目链路覆盖数据准备、特征学习、模型训练与评估,核心涉及卷积神经网络、图像分类、数据集增广、过拟合缓解和实时部署等知识点,能够帮助理解算法在真实监控场景中的落地方式与鲁棒性挑战。压缩包共计一百三十四个文件,大小约十七兆,包含三十四个可执行脚本、三十四个配置文件、二十三张图片样本、十八张图示,以及容器部署文件、训练结果统计、事件日志、交互式教程笔记本和说明文档,兼顾复现训练、环境搭建与理论研读。目前已有158人浏览学习,学习者可以从教程笔记入手分步运行,也可直接阅读源码与配置,将训练流程迁移至自己的毕业设计或相关课题。
1. 电梯监控里识别电动车,不是“再训练一次”那么简单
把“用于识别电梯监控视角内的电动车以及自行车”这个项目落地,第一件事是承认:这不是普通目标检测训练课的课后作业。电梯摄像头装在角落,镜头朝下带广角畸变,人推着自行车进去时车把、踏板互相遮挡,电动车和自行车在俯视画面里轮廓高度相似。实测里最容易翻车的不是“检测不到”,而是“把自行车当成电动车报警”,或者是“电动车进了轿厢,系统却等到关门后才出框”。这套方案的价值,是让物业和安防团队在非机动车进入电梯的几秒内收到提醒,同时尽量不误报。适合手里有电梯监控网络、想用视觉识别替代保安盯屏幕的团队,也适合能把模型推到边缘盒子里的算法工程师往下看。
2. 先看懂电梯监控视角:为什么同一个目标换个角度就翻车
2.1 电梯摄像头的画面特征:俯角、短焦、顶光
电梯监控的画质和普通道路监控完全是两个世界。摄像头多数是 2.8mm 或 3.6mm 短焦镜头,安装高度两米多,向下倾斜 30 到 60 度。画面里目标呈现强烈的透视形变,一辆电动车从进门到停稳,车长和车宽在画面里变化非常大。这种视角下,目标检测框的宽高比不稳定,容易从 1:2 变到 2:1,模型如果用常规数据集训练,第一关就会被视角打懵。
另一个麻烦是光照。电梯轿厢顶部是 LED 灯板,直下式照明导致车篮、座椅下方出现高光阴影对比;轿厢不锈钢侧壁还会形成大面积反光。电梯门开的时候,外部走廊光和轿厢内光剧烈交替,摄像头自动增益会把画面拉得忽明忽暗。用白天的普通视频做训练,模型在夜间和高动态场景下会出现肉眼可见的漏检。
应对思路很明确:不要死磕预训练模型的通用能力,而是用电梯视角的样本做迁移。数据增强中要专门加透视变换、亮度抖动、反光模拟,让模型学会在短焦广角下也认得出车。目标对象识别在这种场景里,拼的不是模型结构多新,而是训练数据分布和现场画面有多接近。
2.2 电动车和自行车的关键差异点:轮径、踏板、车篮、反光
两类车在侧视图里很好分,电动车有标志性的电池盒、宽踏板和后轮毂电机;自行车只有细细的车架和脚踏。但在电梯俯视视角里,这些特征全部被压扁,只剩下“一个长条形的车体”。因此标注逻辑要从“认出这辆车”转向“找到几个稳定的判别特征”。
我在项目里惯用的判据有三个。第一是踏板宽度,电动车的脚踏区域明显比自行车宽,而且很多电动车装了防滑脚垫,在俯视画面里表现为一块深色矩形;第二是后轮毂,电动车后轮多数是轮毂电机,侧边是圆盘状,几乎看不到辐条,自行车后轮则辐条清晰可见;第三是车篮和前围,很多电动车在踏板上方有封闭式前围板,自行车通常没有。这些特征受监控分辨率影响相对小,比看车漆、贴纸这些颜色特征可靠得多。
还有一个容易被忽略的细节:反光板。自行车出厂强制带前后反光板,监控的红外补光灯或电梯白光打到反光板上会出现高亮点,而电动车反光板经常损坏或缺失。这个特征可以作为辅助,但不能做唯一依据,否则后期很容易被现场其他发光物体干扰。标注时如果只画一个框,模型只能学到整体轮廓;如果要进一步提高细分准确率,可以考虑在数据里增加关键点标签,例如把“后轮中心”“踏板中心”作为额外监督信息。
2.3 数据采集与标注:目标对象识别的第一步
没有现场数据,后面所有参数都是空谈。常见做法是从电梯监控的 RTSP 流里抽帧,而不是下载公开数据集,因为公开数据集几乎没有“电梯顶部视角”这个分布。抽帧脚本用 OpenCV 就能写,注意处理断流重连。
import cv2 import os import time rtsp_url = "rtsp://your_user:your_password@camera_ip:554/stream1" cap = cv2.VideoCapture(rtsp_url) # 只缓冲最新一帧,避免画面越积越旧 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) cap.set(cv2.CAP_PROP_FPS, 5) save_dir = "capture" os.makedirs(save_dir, exist_ok=True) frame_id = 0 while True: ok, frame = cap.read() if not ok: print("read fail, reconnect after 2s") time.sleep(2) cap.open(rtsp_url) continue # 按帧号抽样:25fps 视频每 10 帧约 0.4 秒一张 if frame_id % 10 == 0: cv2.imwrite(os.path.join(save_dir, f"{frame_id:05d}.jpg"), frame) frame_id += 1 if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release()这段脚本里有三个参数值得解释。CAP_PROP_BUFFERSIZE设置为 1,是为了保证读到的是最新画面,而不是积压了三秒的旧帧;frame_id % 10控制抽帧密度,电梯进出过程通常只有 10 到 30 秒,抽太密会让同一目标大量重复,抽太疏又会漏掉关键姿态;time.sleep(2)是 RTSP 断流后的重连间隔,摄像头 IP 地址、用户名密码要在实际现场填。抽出的帧先人工快速筛一遍,去掉空车、无关人员和画面完全模糊的帧,剩下的图用 LabelMe 标注。
标注类别就是两个:ebike和bicycle。我的建议是标注完整的车体包围盒,哪怕车辆有一部分被轿厢里的人挡住。如果一辆车被严重遮挡到只剩一个车把,这种样本不要硬标,放进“丢弃”列表。每类有效样本尽量不少于 1500 张图、3000 个实例。只标几百张就训练,模型会很快过拟合到少量场景的固定光照下,换一台电梯立刻翻车。
3. 把识别模型跑通:从数据目录到 YOLOv8 训练的最小路线
3.1 选 YOLOv8 还是 YOLOv5:边缘设备上的推理开销
电梯识别项目里,模型最终跑在嵌入式盒子、IPC 主板或者带有 GPU 的小服务器上。YOLOv5 非常稳定,资料多,但 YOLOv8 在多类别细分和训练收敛上更省心,尤其是提供了 nano 和 small 两个轻量版本,适合边缘部署。实际项目里我用得最多的是 YOLOv8n,理论上 GFLOPs 最低,在 RK3588 或 Jetson 上可以跑到实时。如果标注数据很多,追求更高精度,再切到 YOLOv8s。
这里要明确一个原则:电梯识别不需要很小的目标框。电梯轿厢最大也就两三米见方,摄像头视角内的一辆电动车,在 640×640 输入下大约占 150 到 400 像素的框。不需要为了远处小目标放大输入到 1280,那只会白白增加延迟。用imgsz=640是最平衡的选择。
3.2 按 YOLO 格式组织数据集和 YAML 配置
LabelMe 输出的 JSON 不能直接喂给 YOLO,需要转换。YOLO txt 标注的格式是每行一个目标:
class_id center_x center_y width height其中坐标是相对图像宽高的归一化值。转换脚本要处理两个细节:一是多边形标注转矩形框,二是坐标越界裁剪。电梯门打开时,车身常被画面边缘切掉一半,边界框坐标可能出现超出 0~1 的情况,YOLO 训练能容忍稍微越界,但最好还是 clamp 到 0.0001~0.9999。
import json import os import glob def convert_labelme_to_yolo(json_path, out_dir, class_map): with open(json_path, encoding="utf-8") as f: data = json.load(f) image_h = data["imageHeight"] image_w = data["imageWidth"] lines = [] for shape in data["shapes"]: label = shape["label"] if label not in class_map: continue # LabelMe 存的是多边形顶点,这里直接取矩形包围盒 pts = shape["points"] xs = [p[0] for p in pts] ys = [p[1] for p in pts] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) cx = ((x_min + x_max) / 2) / image_w cy = ((y_min + y_max) / 2) / image_h bw = (x_max - x_min) / image_w bh = (y_max - y_min) / image_h # 防止转换后面朝边界越界 cx = min(max(cx, 0.0001), 0.9999) cy = min(max(cy, 0.0001), 0.9999) bw = min(max(bw, 0.0001), 0.9999) bh = min(max(bh, 0.0001), 0.9999) lines.append(f"{class_map[label]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") if lines: base_name = os.path.basename(json_path).replace(".json", ".txt") with open(os.path.join(out_dir, base_name), "w", encoding="utf-8") as f: f.write("\n".join(lines))这个脚本的核心逻辑只有一个:把多边形的矩形外接框转成 YOLO 需要的中心点加宽高格式。实际标注中不建议用很夸张的多边形去描车轮廓,直接画矩形框更快,也足够训练。class_map可以定义为{"ebike": 0, "bicycle": 1},转完以后检查一遍 txt 文件,看看没有漏生成、没有空文件。
数据集目录按 YOLO 约定建成这样:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── electric_bike.yaml验证集建议从不同电梯、不同时段的数据里抽取,而不是随机抽帧。很多项目在这里犯迷糊:随机抽帧会让同一天的画面同时出现在训练集和验证集里,Final mAP 虚高,到现场换一台电梯就不行。每台电梯保留 20% 的帧做验证,跨场景验证集才有说服力。
3.3 训练命令和关键参数:epochs、imgsz、batch
YAML 内容如下:
path: /path/to/dataset train: images/train val: images/val names: 0: ebike 1: bicycle然后执行训练:
yolo detect train \ data=electric_bike.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ device=0 \ patience=20 \ project=./runs \ name=elecbike_v1参数说明:epochs=120是初始值,配合patience=20早停,模型连续 20 个 epoch 在验证集上不涨就自动结束;batch=16在 16GB 显存下比较稳妥,显存小的改成 8,不用太纠结;device=0指定第一张 GPU,没有 GPU 环境把训练规模缩小但会非常慢;project和name指定输出目录,方便多个实验对比。
训练过程中重点看两个指标:一是mAP50,它反映“框的位置大致对了就算对”,适合电梯这类对精确分割要求不高的场景;二是bicycle与ebike这两类之间的混淆程度。如果训练结束后的混淆矩阵显示自行车有 15% 被判成了电动车,这个模型不能直接上线,因为误报会触发物业警报。
3.4 从 results.csv 里读模型的真实水平
YOLOv8 训练完会在运行目录留下results.csv,里面每个 epoch 都有train/box_loss、val/cls_loss等记录。用一个小脚本扫一眼:
python -c "import pandas as pd; df=pd.read_csv('runs/elecbike_v1/results.csv'); print(df.columns.tolist()); print(df.tail(1))"关注最后一列平衡的指标,并输出 PR 曲线。电梯场景里我会额外统计一个“每千帧误报数”,这个数比 mAP 更贴近物业的体验。如果模型在 1000 帧里把过道里的垃圾桶误认成自行车,虽然 mAP50 还是 0.98,现场也无法接受。这个统计可以用测试集视频跑一遍推理脚本,把连续 3 帧以上误报的片段记录下来,单独分析。
4. 从模型到电梯:RTSP 视频流里的实时识别与报警逻辑
4.1 拉取电梯监控流:OpenCV 的 VideoCapture 与延迟控制
训练好的模型如果只是离线跑图片,没有落地价值。现场部署时,算法服务要以 RTSP 流为输入。实际写推理服务时,OpenCV 的VideoCapture虽然简单,但有几个明显的坑:RTSP 流断线后不会自动恢复;cap.read()会阻塞到下一帧到达,导致 CPU 空转;还有缓冲区积累带来的画面延迟。
我常用的拉流推理结构长这样:
import cv2 import torch import time model = torch.hub.load("./yolov8", "custom", path="best.pt", source="local") cap = cv2.VideoCapture("rtsp://your_user:your_password@camera_ip:554/stream1") cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) skip_count = 0 while True: ok, frame = cap.read() if not ok: print("connection lost, wait 3s") time.sleep(3) cap.open("rtsp://your_user:your_password@camera_ip:554/stream1") continue # 电梯场景 5~10fps 足够,不需要每帧都推理 skip_count += 1 if skip_count % 3 != 0: continue results = model.predict(frame, imgsz=640, conf=0.35, device=0, verbose=False) boxes = results[0].boxes ...这里的skip_count % 3是抽帧推理控制。电梯门开关、人的走动不会快到单帧内完成,25fps 的视频每 3 帧推理一次,大约 8fps,已经足够抓取“推车进入”这个行为。如果边缘设备算力不够,可以继续降到% 5。不要用模型处理每一帧,那样只会把延迟拉高,反而丢失报警窗口。
4.2 行为识别:连续帧判定而不是单帧报警
单帧检测到电动车就报警,是最早被现场否掉的方案。电梯门打开时,走廊上停着的电动车可能短暂出现在画面边缘,如果此时报警,等于每天报几百次。正确做法是基于连续帧做行为判定:只有当电动车或自行车“进入轿厢并停留超过一定时间”才触发。
一个可复现的滑动窗口逻辑如下:
from collections import deque window = deque(maxlen=5) alarm_threshold = 3 # 连续5帧中至少3帧有有效目标 def update_detection(current_frame_has_target): window.append(1 if current_frame_has_target else 0) if sum(window) >= alarm_threshold: return True return False这个逻辑的意义很简单:用 5 帧的窗口做去抖,瞬时噪声不会触发报警。结合电梯场景,还应该加一个“停留时间”条件。进门可能只要 1 到 2 秒,但如果车停在轿厢内,窗口会持续命中。我一般会取连续 1.5 秒存在目标作为确认报警,报警后要等到连续 2 秒无目标才解除,避免重复报警。
这里有个容易被忽略的参数:conf阈值。模型训练时默认用 0.25 的置信度导出预测框,但现场频闪、反光会把置信度推高。我建议部署时把conf设在 0.35~0.45 之间,宁肯漏掉一些低置信度目标,也要保证报警不吵。置信度阈值要根据电梯实际画面调,不能照抄训练环境。
4.3 边缘端部署:把 PyTorch 模型导出成 TensorRT Engine
训练机上是 PyTorch,现场设备多数是 NVIDIA Jetson 或者带独立显卡的工控机。直接把.pt文件拷过去推理,速度慢而且依赖环境多。常见做法是先导出 ONNX,再在目标设备上编译成 TensorRT engine。
导出命令:
yolo export model=best.pt format=onnx imgsz=640 opset=11 # 在 Jetson 或 x86 上使用 TensorRT 8.x trtexec --onnx=best.onnx \ --saveEngine=best.engine \ --fp16 \ --maxBatch=1这两个命令背后的参数含义:opset=11是为了兼容旧版推理框架;--fp16半精度在电梯识别这类分类任务上精度损失很小,但推理速度能提升接近一倍;--maxBatch=1表示单路单帧批处理,因为电梯检测通常是一路视频对应一个模型实例,不需要动态 batch。
如果现场设备不是 NVIDIA,例如瑞芯微 RK3588,建议用rknn-toolkit2把 ONNX 转成.rknn格式,注意 YOLOv8 的检测头在 RKNN 转换时要额外处理输出层,最好参考官方提供的 YOLOv8 适配脚本。这块很容易踩坑,我在常见问题里详细说。
5. 避坑与排查:电梯场景里最容易翻车的五个实际问题
5.1 自行车和电动车互相误识别
这个现场最容易翻车。有些自行车配有宽大的车筐和电池组,外形上和轻型电动车非常接近;反过来,一些电动车断电推行时踏板不动,前后轮辐条清晰,模型会误判成自行车。
排查思路:先看混淆矩阵,确认是“检测框问题”还是“分类头问题”。如果ebike的框位置很准,但框内物体被分类成bicycle,说明模型学到的主要特征是挡泥板、车架粗细这类细纹理,而这些纹理在俯视视角下不稳定。
解决方法是补数据,针对性标注“轮廓接近”的样本,同时把两者互为负样本加入数据增强。另一个有效做法是把问题从“二分类”改成“检测+属性分类”:先用一个通用模型检出所有两轮车,再对框内区域用一个小分类网络判断是电动还是自行车。这样即使框不完美,分类网络也能看到更大上下文。
5.2 夜间模式和背光导致的漏检
电梯摄像头在光线暗时自动切到红外模式或补光模式,画面变成黑白,且对比度低。训练集以白平衡彩色画面为主的话,模型在红外画面上的特征模式不匹配,表现断崖式下降。
解决方法是采集同场景的黑白/夜视片段加入训练集。如果现场不方便,可以用数据集增强里的灰度化、亮度抖动和对比度调整来近似。还有个实用习惯:给模型输入时先做自适应直方图均衡化(CLAHE),只在推理前处理中加上:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray) frame = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR)这种预处理可以让夜间画面对模型更友好,但要注意亮度提升也会把电梯金属框的反射放大,需要和降置信度阈值配合使用。
5.3 推车进出时检测框抖动,触发断断续续的报警
电梯内人推着车移动时,车身大部分被人体挡住。模型可能这一帧检出ebike,下一帧因为遮挡强度不够变成低置信度,窗口逻辑反复横跳,报警器直到人走了才触发。
处理方法是给推理结果做一个短跟踪。不用上 DeepSORT 这种重型跟踪器,单纯用上一次检测的 IoU 匹配就够:
def match_previous(prev_box, curr_boxes, iou_threshold=0.3): for box in curr_boxes: if iou(prev_box, box) > iou_threshold: return box return prev_box # 短暂遮挡时沿用上一帧检测结果这个“沿用”逻辑最多延续 3 帧,超过 5 帧仍然无匹配就清除目标。它解决的是电梯内目标被短暂遮挡导致的报警闪断,而不是跨镜头跟踪。操作上要注意不要把其他乘客的腿误当成延续目标,IoU 阈值要调低到 0.2~0.3 之间。
5.4 边缘设备推理慢,一帧要 300 毫秒
很多团队直接把 YOLOv8s 或 m 型号带上 Jetson Nano,发现根本跑不动。原因不是模型训练不好,而是没有做模型剪枝和输入尺寸限制。
解决路线有两条。第一条是把imgsz=640降到imgsz=480,电梯目标占画面比例较大,降低分辨率对 mAP50 影响很小,速度提升明显;第二条是换用yolov8n配合 TensorRT FP16,实测在 Jetson Orin Nano 上能跑到 30ms 左右。如果还不能接受,就检查有没有在推理时意外开启verbose=True或持续把所有结果写入日志,I/O 反而比模型更耗时。
5.5 测试集准确率很高,但现场误报暴增
这是最玄学也最伤人信心的问题。测试集是从这台电梯抽帧出来的,验证集也是同一台,模型相当于作弊式记住了这部电梯的背景。到现场换一台不同品牌电梯,地面反光、门框颜色、顶上装饰灯全变了,模型把反光边缘推断成自行车辐条。
解决方法是训练时把验证集改成“跨电梯集”,保证训练集和验证集不要从同一台设备抽帧。现场上线后还要做增量训练:把误报的截图回传,每隔一周标注一两百张误报样本,用小学习率继续训练。这是模型部署后三个月内必须坚持的一步,电梯视角的差异一定要靠现场数据来填。
6. 现场验证与模型迭代:用 2000 张真实截图检验过不过关
模型准备上线前,我会先跑一个很“土”但很有效的验收脚本。找现场部署那台电梯保存 2000 张不同光线、不同时段、不同人流的截图,按时间顺序回放,模型对这 2000 张图逐张推理,输出每个目标的类别、置信度和时间点。然后人工对着结果表检查:哪些是真正需要报警的车,哪些是误报警。
这个验证脚本最关键的不是平均精度,而是误报率和漏报率。我会设定一个明确标准:电梯识别工况下,每 1000 帧允许最多 2 次误报,漏报允许在真正推车进入后的 3 秒内出现 1 次。超过这个线就不上正式环境。通过这轮验证后,再花一小时调阈值:
# 把误报、漏报最多的阈值区间打印出来 def print_threshold_report(results, ground_truth): for conf_threshold in [0.25, 0.3, 0.35, 0.4, 0.45, 0.5]: tp = len(results[(results.conf > conf_threshold) & (results.is_target == 1)]) fp = len(results[(results.conf > conf_threshold) & (results.is_target == 0)]) fn = len(results[(results.conf <= conf_threshold) & (results.is_target == 1)]) print(f"conf={conf_threshold:.2f} TP={tp} FP={fp} FN={fn}")这里得到的不是百分之百完美的阈值,只是一个起点。上线后第一周,我会每天翻一次报警记录,特别是深夜和早晨逆光时段的误报截图,把可以复现的问题样本攒起来做下一轮增量训练。这个习惯帮我解决过无数个角落里的暗光问题,也让我意识到:电梯监控视角下的两轮车识别,其实没有一劳永逸的模型,只有持续跟现场搏斗的小模型。
现在我把 YOLOv8n 作为默认基线,凡是新接的电梯先在本地跑一遍模拟回放,再决定要不要加现场样本。这套流程走顺之后,从拿到视频流到模型上线大约只需要三天,希望帮到你。
本文还有配套的精品资源,点击获取