ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于树莓派与Python的寝室监控系统:从运动检测到Flask视频流部署

基于树莓派与Python的寝室监控系统:从运动检测到Flask视频流部署 简介这是一套基于Python与树莓派打造的寝室小监控系统毕业设计项目面向软件工程、计算机科学、自动化、电子信息等专业的在校生适用于毕业设计、课程设计、项目演示或初期立项参考。整套资源聚焦监控场景下的图像采集、状态识别与异常通知核心代码覆盖树莓派端监控主逻辑、百度接口调用、邮件或页面通知等环节配合部署文档可帮助读者理解从硬件环境配置到Python服务运行的全流程。项目代码已通过运行测试并作为高分毕业设计获导师认可便于在此基础上扩展运动检测、远程查看等功能。包体共65个文件以33个Python脚本为主辅以28个pyc编译文件、HTML通知页面、Markdown说明文档以及打包的数据资料压缩包整体仅89KB结构紧凑目录划分明确适合快速阅读和二次开发。目前已有177人学习下载对希望完成同类课题或快速上手树莓派Python开发的学生有直接参考价值。1. 寝室监控为什么是 Python 加树莓派的经典毕设问题边界与选题价值寝室小监控系统的核心需求是在低成本硬件上完成实时画面采集、运动检测、告警记录和远程查看。这类毕设最常见的失败形态是“能跑不能交付”脚本靠手动执行摄像头参数写死在代码里断电重启后服务消失答辩时只能现场演示一屏实时画面。真正值得下功夫的做法是把算法、存储、Web 展示和系统服务串成一条完整链路。Python 负责检测逻辑与业务胶水树莓派负责 GPIO 交互、低功耗常开和摄像头接入整个项目踩中的是嵌入式开发、图像处理和 Web 部署三个硬技能点。选题本身不新但工程完整度高适合需要一份面试时能讲清楚来龙去脉的作品。2. 硬件选型与基础环境树莓派型号、摄像头和 Python 环境准备2.1 树莓派 4B 还是 Zero 2 W算力、功耗与接口的取舍很多毕业设计拿到树莓派就开始装环境结果把时间耗在“摄像头打不开”和“内存不够卡死”上。先看选型寝室监控这种常开设备对功耗和算力有明确要求不能只看树莓派长得小巧就下单。对比项树莓派 4B2G/4G/8G树莓派 Zero 2 W处理器四核 Cortex-A72四核 Cortex-A53内存2GB 起步512MBUSB 接口2 个 USB 3.0 2 个 USB 2.0Micro USB OTG有线网络千兆以太网无仅 Wi-Fi典型功耗约 3-5W约 1-2WOpenCV 处理 640x480 帧15fps 无压力建议降到 320x240适合角色主控节点跑全套服务长时间挂机只做轻量监测如果论文和答辩需要展示实时画面我一般建议用树莓派 4B。理由不是 Zero 2 W 跑不动而是摄像头编码和 Flask 推流同时运行时512MB 内存会先被 Python 解释器和 OpenCV 缓存吃满出现周期性卡顿排查起来比多花一百块买 4B 更浪费时间。2.2 摄像头CSI OV5647 模块与 USB 摄像头和 PIR 传感器的接线方式摄像头是监控系统的眼睛。常见方案有两种CSI 接口的 OV5647 摄像头模块以及免驱 USB 摄像头。CSI 摄像头通过排线直接插在树莓派 4B 的 15 针 CSI 接口上带宽稳定不占 USB 控制器资源是“优秀项目”资料里最常见的配置。OV5647 模块支持 500 万像素实际监控用 640x480 到 1280x720 就够。需要注意新版树莓派 OSBookworm 及之后默认用 libcamera 框架老教程里的picamera库已经不能用要改用picamera2或者直接让 OpenCV 通过 V4L2 读取设备。USB 摄像头的优势是即插即用适合不想折腾排线的同学。缺点是部分廉价摄像头在树莓派 4B 上会报select timeout原因多半是 USB 供电不足或者驱动不兼容接在有源 USB HUB 上能缓解。PIR 人体红外传感器适合作为第二路告警源。HC-SR501 的接法很固定VCC 接 5V 引脚GND 接 GNDOUT 接 GPIO。使用 BCM 编号时把 OUT 接到 GPIO18对应树莓派 4B 的引脚功能图第 12 脚。先看系统是否识别摄像头# 检查摄像头设备节点video0 通常表示第一个摄像头 ls -l /dev/video* # 新系统用 rpicam-hello 列出自带的摄像头模组 rpicam-hello --list-camerasls /dev/video0能确认摄像头驱动是否加载。如果设备节点不存在先检查排线是否插到底再确认系统有没有安装摄像头应用。这里不需要一开始就写图像处理代码先保证图像能进系统后续所有算法才有输入。Python 侧对 PIR 的初始化很简单import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) # 使用 BCM 编号与引脚功能图一致 PIR_PIN 18 GPIO.setup(PIR_PIN, GPIO.IN) # 读取 PIR 输出电平 if GPIO.input(PIR_PIN): print(红外触发)GPIO.setmode(GPIO.BCM)这行很关键。BCM 编号和物理引脚号不同接错会导致读不到电平。PIR 输出高电平表示检测到人体活动可以在之后的运动检测逻辑里作为辅助条件减少摄像头算法误报。2.3 Python 换源与 OpenCV 安装监控系统的基本环境树莓派上装 OpenCV 是个经典坑。直接pip install opencv-python在系统环境里装容易和系统自带包冲突在 Bookworm 版本上还可能出现依赖库缺失。我一般会分三步先换软件源再装系统级 OpenCV最后用 pip 补 Python 依赖。# 备份源文件后换成清华镜像注意按系统版本选择对应源 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak sudo sed -i s|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g /etc/apt/sources.list sudo apt update # 安装系统级 OpenCV、Picamera2 和 Web 框架 sudo apt install -y python3-opencv python3-picamera2 python3-flask sqlite3 # 验证导入是否成功 python3 -c import cv2; print(cv2.__version__)换源解决的是下载速度问题同时也避免了某些国内网络环境下 apt 超时后依赖安装不完整。用 apt 装python3-opencv而不是用 pip是因为 apt 会把底层依赖如libhdf5、libatlas一起装好省去手动补库的步骤。rpicam-hello摄像头预览命令确认正常后再进入算法开发整个环境搭建控制在半小时内。3. 用 OpenCV 实现运动检测与告警判断帧差法、背景减除到轮廓过滤3.1 帧差法先跑通最小流程读取摄像头、灰度转换与差分阈值运动检测的第一版不需要复杂模型帧差法是最快建立代码骨架的方法。核心思路是把当前帧和上一帧做差像素值变化超过阈值的区域就是“运动区域”。import cv2 cap cv2.VideoCapture(0) _, prev cap.read() prev cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY) prev cv2.GaussianBlur(prev, (21, 21), 0) while True: _, frame cap.read() gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (21, 21), 0) # 前后帧差分取绝对值后做二值化 diff cv2.absdiff(prev, gray) thresh cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1] # 统计非零像素数超过阈值认为是运动 motion_area cv2.countNonZero(thresh) if motion_area 2000: cv2.putText(frame, Motion, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imshow(monitor, frame) prev gray if cv2.waitKey(30) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里GaussianBlur的核大小(21, 21)用来抑制传感器噪点噪点会让像素值抖动不加模糊的话任何一帧都会出现大量运动点。threshold的 25 表示灰度差超过 25 才算变化数值越低越灵敏。motion_area 2000过滤掉零散的噪声点。帧差法的问题在寝室场景里很明显窗帘飘动、室友翻书、窗外光影移动都会让“运动面积”突增。只要连续两帧之间光照变化大一点全画面都会变成白色误报率很高。所以帧差法只适合验证摄像头通路不适合直接交付。3.2 MOG2 背景减除的自适应优势与关键参数更实际的选择是 MOG2 背景减除器。它的思路不是比较前后帧而是为每个像素维护一个混合高斯分布模型不断更新“背景长什么样”再把偏离背景的像素标记为前景。import cv2 cap cv2.VideoCapture(0) # history500: 用最近500帧建立背景模型 # varThreshold36: 像素与背景分布的差异超过36才算前景 mog2 cv2.createBackgroundSubtractorMOG2( history500, varThreshold36, detectShadowsTrue ) while True: ret, frame cap.read() if not ret: break mask mog2.apply(frame) # detectShadowsTrue 时阴影点灰度是127正常前景是255这里只保留255 mask cv2.threshold(mask, 254, 255, cv2.THRESH_BINARY)[1] # 开运算去掉孤立噪点先腐蚀再膨胀 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: if cv2.contourArea(cnt) 800: x, y, w, h cv2.boundingRect(cnt) cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(MOG2, frame) if cv2.waitKey(30) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()history500决定了模型更新速度。它表示用多长时间窗口的背景来拟合当前场景。寝室里从开灯到关灯属于剧烈变化模型需要大约几十秒重建期间会产生大量误报这是正常现象。varThreshold36是判定阈值调大可以减少误报但真实移动目标可能漏检。detectShadowsTrue会识别出物体投下的阴影但阴影灰度值固定为 127所以用threshold(mask, 254)把阴影剔除避免一个路过的人被框成一大片影子。3.3 轮廓面积过滤与连续帧确认减少误报的工程手段算法层面做完背景减除工程上还要加两道保险轮廓面积过滤和连续帧确认。单帧检测到前景不代表真的有人经过可能是飞虫、灯光闪动或者背景模型还没收敛完。参数推荐初值调整方向典型误报场景varThreshold36调大到 40-50 更迟钝调小到 20-25 更灵敏墙面光影晃动最小轮廓面积800 像素640x480 画面下 800 约等于一个巴掌大小飞虫、充电指示灯连续确认帧数3 帧值越大越难触发延迟也越高窗帘飘动、灯光开关连续帧确认的实现不复杂核心是维护一个计数器MIN_AREA 800 FRAMES_REQUIRED 3 hit_count 0 while True: ret, frame cap.read() if not ret: break motion_mask get_motion_mask(frame) # 复用 3.2 中的 MOG2 处理逻辑 contours, _ cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) frame_max_area max((cv2.contourArea(c) for c in contours), default0) if frame_max_area MIN_AREA: hit_count 1 hit_count min(hit_count, FRAMES_REQUIRED) else: hit_count 0 if hit_count FRAMES_REQUIRED: trigger_record(frame, frame_max_area) hit_count 0 # 触发后清零避免同一事件反复触发这里有两个关键点。第一面积取轮廓最大值而不是所有轮廓面积之和防止两个人同时入镜时计算值被叠加导致阈值判断失准。第二hit_count触发后立即清零否则连续三帧满足条件会连续触发三次录制导致同一段人流被保存成多段视频。如果只用 PIR 传感器判断触发不需要这个计数器但摄像头画面作为主检测源时必须加。4. 事件存储与告警记录SQLite 日志和截图、视频的保存策略4.1 文件目录设计按日期存放截图与视频片段检测到运动之后数据要落到磁盘上。目录按日期分层是管理监控数据最直接的方式避免所有图片堆在一个文件夹里。import os from datetime import datetime BASE_DIR /home/pi/monitor_data def snapshot_dir(): # /home/pi/monitor_data/snapshot/20250523 path os.path.join(BASE_DIR, snapshot, datetime.now().strftime(%Y%m%d)) os.makedirs(path, exist_okTrue) return path def build_filename(prefix): # 例motion_153045_236.jpg return f{prefix}_{datetime.now().strftime(%H%M%S_%f)[:-3]}.jpg时间戳精度取到毫秒的前三位即%f后截掉 3 位变成微秒转毫秒。这个粒度在监控里足够区分同一秒内连续触发的多张截图。图片保存用 JPEG 质量 90画质和体积比较均衡path os.path.join(snapshot_dir(), build_filename(motion)) cv2.imwrite(path, frame, [cv2.IMWRITE_JPEG_QUALITY, 90])视频片段保存使用VideoWriter。编解码器选mp4v兼容性最好几乎任何播放器都能打开。录制时长控制在触发后 8-10 秒太大对 SD 卡不友好。fourcc cv2.VideoWriter_fourcc(*mp4v) out cv2.VideoWriter(/home/pi/monitor_data/video/motion.mp4, fourcc, fps10, frameSize(640, 480))fps10对应实时检测的间隔。录制时要注意如果主循环里有耗时操作实际写入的帧率低于设定的 fps播放时会偏快属于正常现象。4.2 SQLite 事件表设计与多线程写入的注意事项文件记录之外需要事件日志用于答辩时展示“某时某刻发生了什么”。SQLite 比 MySQL、PostgreSQL 更适合树莓派它不需要服务进程一个文件就是整个数据库断电恢复也简单。建表语句如下CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_time TEXT NOT NULL, event_type TEXT NOT NULL, -- motion / pir / boot snapshot_path TEXT DEFAULT , video_path TEXT DEFAULT , motion_area INTEGER DEFAULT 0 ); CREATE INDEX idx_events_time ON events(event_time);写入逻辑要注意线程安全。检测线程往数据库插入数据Flask 线程查询数据如果两个线程同时打开同一个 SQLite 连接会抛database is locked错误。常见做法是每次操作都新建独立连接并设置timeoutimport sqlite3 DB_PATH /home/pi/monitor_data/logs/events.db def insert_event(event_type, snapshot_path, video_path, motion_area0): conn sqlite3.connect(DB_PATH, timeout5) # WAL 模式允许多线程并发读写写入不阻塞读 conn.execute(PRAGMA journal_modeWAL;) conn.execute( INSERT INTO events(event_time, event_type, snapshot_path, video_path, motion_area) VALUES(?,?,?,?,?), (datetime.now().isoformat(timespecseconds), event_type, snapshot_path, video_path, motion_area) ) conn.commit() conn.close()timeout5表示拿不到写锁时最多等 5 秒之后才报错这能躲开大部分短时竞争。PRAGMA journal_modeWAL把写操作放到预写日志里读操作不阻塞写操作。这里不把数据库连接作为全局变量是因为 Flask 的线程模型下连接对象跨线程复用反而会引发更多问题。4.3 存储生命周期管理清理旧文件与保护 SD 卡监控数据是持续增长的如果不清理SD 卡很快被占满。SD 卡的擦写寿命有限频繁写入小文件会加速损坏。部署时把监控目录放到外接 U 盘或移动硬盘上系统装在 SD 卡里是更稳妥的布局。数据路径建议保留时间运动截图monitor_data/snapshot/YYYYMMDD/7 天触发视频monitor_data/video/YYYYMMDD/7 天事件日志monitor_data/logs/events.db30 天清理脚本不需要引入第三方库标准库就能实现#!/usr/bin/env python3 import os from datetime import datetime, timedelta DATA_ROOT /home/pi/monitor_data KEEP_DAYS 7 def clean_old_files(): cutoff datetime.now() - timedelta(daysKEEP_DAYS) for root, _, files in os.walk(DATA_ROOT): for name in files: path os.path.join(root, name) try: mtime datetime.fromtimestamp(os.path.getmtime(path)) if mtime cutoff: os.remove(path) except FileNotFoundError: pass这个脚本适合放到系统 crontab 里每天凌晨执行或者更简单地在监控主进程里每 6 小时调用一次。注意os.walk默认不修改目录结构删完文件后空目录会留下来不影响后续写入。5. 用 Flask 把监控画面推给浏览器MJPEG 视频流与 systemd 部署5.1 生成器实现 MJPEG 视频流线程安全地共享最新帧寝室监控最终要能在手机或电脑上打开浏览器看实时画面。Flask 不直接支持推流但可以用multipart/x-mixed-replace方式实现 MJPEG 视频流把一帧帧 JPEG 图片拼成一个无限响应体浏览器会自动刷新显示。import cv2 import threading import time from flask import Flask, Response app Flask(__name__) latest_frame None frame_lock threading.Lock() def camera_worker(): global latest_frame cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: time.sleep(0.5) continue # quality70 压缩到几 KB 到几十 KB降低网络传输压力 ret, jpg cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 70]) if not ret: continue with frame_lock: latest_frame jpg.tobytes() time.sleep(0.1) # 限制 10fps避免树莓派 CPU 打满 def mjpeg_stream(): while True: with frame_lock: frame latest_frame if frame is not None: yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n frame b\r\n) time.sleep(0.05) app.route(/video_feed) def video_feed(): return Response(mjpeg_stream(), mimetypemultipart/x-mixed-replace; boundaryframe) if __name__ __main__: t threading.Thread(targetcamera_worker, daemonTrue) t.start() app.run(host0.0.0.0, port8000, threadedTrue)这里最关键的工程决策是相机读取和运动检测在独立线程里运行latest_frame保存最新一帧的 JPEG 数据。Flask 推流时不直接读摄像头而是读这个全局变量。frame_lock保证多线程同时访问时不会出现半写状态但加锁本身也有开销所以只在拷贝整数个 JPEG 字节时加锁比直接传递原始frame数组高效得多。boundaryframe出现在 mimetype 里而 yield 的每一段也要以--frame\r\n开头两者必须完全一致否则视频流无法显示。浏览器访问/video_feed时会把响应当成持续更新的图片流而不是一次性下载数据。5.2 部署到系统服务systemd 单元文件、开机自启与日志定位监控系统不能依赖手动执行python3 run.py断电重启后就丢了。树莓派 OS 自带 systemd用一个 service 文件把 Python 进程托管起来是最基本的交付要求。[Unit] DescriptionDorm Monitor Service Afternetwork.target [Service] Userpi WorkingDirectory/home/pi/dorm_monitor ExecStart/usr/bin/python3 /home/pi/dorm_monitor/run.py Restartalways RestartSec5 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target把上面的内容保存为dorm-monitor.service然后执行sudo cp dorm-monitor.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now dorm-monitor sudo systemctl status dorm-monitorUserpi是刻意为之进程不需要 root 权限。摄像头设备节点/dev/video0默认属于video用户组而pi用户默认在这个组里所以直接能访问。Restartalways让进程崩溃后自动拉起RestartSec5避免高频重启把系统日志刷爆。排查问题用journalctl -u dorm-monitor -n 50 -f看日志。这里有个易错点代码里的print()默认是行缓冲systemd 重定向输出时可能丢失部分日志设置EnvironmentPYTHONUNBUFFERED1强制 Python 不缓冲日志就能实时落盘。5.3 局域网访问的安全边界简易鉴权与不做公网暴露监控画面涉及室友隐私部署时不要图方便把端口映射到公网。最稳妥的方式是只在实验室或寝室局域网内访问地址类似http://192.168.1.100:8000/video_feed。Flask 的app.route默认任何人都能访问至少加一个 Token 参数校验防止同网段其他人直接偷看画面。systemd 命令作用systemctl enable dorm-monitor设置开机自启systemctl restart dorm-monitor改完代码后手动重启journalctl -u dorm-monitor -n 50查看最近 50 行日志Token 校验写在路由修饰器里最省事from functools import wraps from flask import request, abort ACCESS_TOKEN dorm-2025-lan def require_token(view): wraps(view) def wrapped(*args, **kwargs): if request.args.get(token) ! ACCESS_TOKEN: abort(404) return view(*args, **kwargs) return wrapped app.route(/) require_token def index(): return img src/video_feed?tokendorm-2025-lan这只是应用层访问控制流量在局域网里是明文传输Token 也会出现在 URL 里所以只能防君子不能防抓包。远程访问属于另一个层面的安全问题毕设答辩中明确说明“本系统仅在可信局域网运行”即可不要对照网上教程随便做端口映射。6. 验收与调参技巧让监控系统在答辩前稳定跑三天6.1 验证摄像头通路与硬件状态的三个命令答辩前的第一件事不是调算法而是确认硬件链路的稳定性。三个命令基本能覆盖常见故障# 摄像头设备节点是否存在 ls -l /dev/video0 # CPU 温度超过 80 度就要检查散热 vcgencmd measure_temp # 查看内核日志中摄像头相关的错误 sudo dmesg | grep -i camera/dev/video0存在不代表摄像头调好了还需要用 Python 实际读一帧import cv2 cap cv2.VideoCapture(0) ok, frame cap.read() assert ok and frame is not None, camera not ready print(frame size:, frame.shape[:2])如果返回的frame是None常见原因是摄像头被占用比如rpicam-hello预览进程没退出或者另一个 Python 进程还开着/dev/video0。dmesg里如果出现camera sensor not detected基本可以断定 CSI 排线接触不良重新插拔排线就能解决。6.2 检测灵敏度的参数速查表与寝室场景推荐值调参目标是减少误报的同时不漏掉真实事件。没有一组参数能适配所有环境这里给一组按场景划分的起点参数。场景varThreshold最小轮廓面积连续确认帧数效果预期窗边风吹窗帘3620005窗帘飘动不告警门口检测人进出258002进门立即触发全屋夜间布防4830003减少灯光和蚊虫干扰帘飘动时轮廓面积往往不小单靠面积过滤无效所以把连续确认帧数调到 5短促的运动不会触发。门口场景要求低延迟确认帧数降到 2检测灵敏度提高。全屋布防适合夜间varThreshold48让背景模型对微弱光影变化不敏感同时用 3000 像素的面积下限过滤掉飞虫。6.3 交付物清单与快速换机部署的检查脚本最后一步是把项目整理成可交付的“优秀项目”形态。源码之外部署文档里最好包含依赖文件和环境检查脚本答辩时老师换一台树莓派也能复现这个项目的完整度才算达标。python3 -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt内容不用贪多能覆盖 OpenCV、Flask、SQLite 和摄像头库即可opencv-python-headless flask picamera2opencv-python-headless是无 GUI 版本树莓派服务器上跑监控不需要imshow用 headless 版能省掉显示依赖冲突。环境检查脚本只做一件事逐项确认摄像头、数据库和 Web 端口可用让部署不依赖人肉记忆。curl -s http://localhost:8000/?tokendorm-2025-lan -o /dev/null -w %{http_code}\n这条 curl 命令返回 200 的瞬间意味着摄像头采集、运动检测、SQLite 存储、Flask 推流和 Token 鉴权这五层链路全部通了。用这个结果作为“可交付”的验收标准比录一段实时画面更经得起追问。本文还有配套的精品资源点击获取
返回列表