简介:这是一套基于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:算力、功耗与接口的取舍
很多毕业设计拿到树莓派就开始装环境,结果把时间耗在“摄像头打不开”和“内存不够卡死”上。先看选型,寝室监控这种常开设备对功耗和算力有明确要求,不能只看树莓派长得小巧就下单。
| 对比项 | 树莓派 4B(2G/4G/8G) | 树莓派 Zero 2 W |
|---|---|---|
| 处理器 | 四核 Cortex-A72 | 四核 Cortex-A53 |
| 内存 | 2GB 起步 | 512MB |
| USB 接口 | 2 个 USB 3.0 + 2 个 USB 2.0 | Micro USB OTG |
| 有线网络 | 千兆以太网 | 无,仅 Wi-Fi |
| 典型功耗 | 约 3-5W | 约 1-2W |
| OpenCV 处理 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 就够。需要注意,新版树莓派 OS(Bookworm 及之后)默认用 libcamera 框架,老教程里的picamera库已经不能用,要改用picamera2或者直接让 OpenCV 通过 V4L2 读取设备。
USB 摄像头的优势是即插即用,适合不想折腾排线的同学。缺点是部分廉价摄像头在树莓派 4B 上会报select timeout,原因多半是 USB 供电不足或者驱动不兼容,接在有源 USB HUB 上能缓解。
PIR 人体红外传感器适合作为第二路告警源。HC-SR501 的接法很固定:VCC 接 5V 引脚,GND 接 GND,OUT 接 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) # history=500: 用最近500帧建立背景模型 # varThreshold=36: 像素与背景分布的差异超过36才算前景 mog2 = cv2.createBackgroundSubtractorMOG2( history=500, varThreshold=36, detectShadows=True ) while True: ret, frame = cap.read() if not ret: break mask = mog2.apply(frame) # detectShadows=True 时,阴影点灰度是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()history=500决定了模型更新速度。它表示用多长时间窗口的背景来拟合当前场景。寝室里从开灯到关灯属于剧烈变化,模型需要大约几十秒重建,期间会产生大量误报,这是正常现象。varThreshold=36是判定阈值,调大可以减少误报,但真实移动目标可能漏检。detectShadows=True会识别出物体投下的阴影,但阴影灰度值固定为 127,所以用threshold(mask, 254)把阴影剔除,避免一个路过的人被框成一大片影子。
3.3 轮廓面积过滤与连续帧确认:减少误报的工程手段
算法层面做完背景减除,工程上还要加两道保险:轮廓面积过滤和连续帧确认。单帧检测到前景不代表真的有人经过,可能是飞虫、灯光闪动或者背景模型还没收敛完。
| 参数 | 推荐初值 | 调整方向 | 典型误报场景 |
|---|---|---|---|
| varThreshold | 36 | 调大到 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), default=0) 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_ok=True) 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, fps=10, frameSize=(640, 480))fps=10对应实时检测的间隔。录制时要注意,如果主循环里有耗时操作,实际写入的帧率低于设定的 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错误。常见做法是每次操作都新建独立连接,并设置timeout:
import sqlite3 DB_PATH = "/home/pi/monitor_data/logs/events.db" def insert_event(event_type, snapshot_path='', video_path='', motion_area=0): conn = sqlite3.connect(DB_PATH, timeout=5) # WAL 模式允许多线程并发读写,写入不阻塞读 conn.execute("PRAGMA journal_mode=WAL;") conn.execute( "INSERT INTO events(event_time, event_type, snapshot_path, video_path, motion_area)" " VALUES(?,?,?,?,?)", (datetime.now().isoformat(timespec="seconds"), event_type, snapshot_path, video_path, motion_area) ) conn.commit() conn.close()timeout=5表示拿不到写锁时最多等 5 秒,之后才报错,这能躲开大部分短时竞争。PRAGMA journal_mode=WAL把写操作放到预写日志里,读操作不阻塞写操作。这里不把数据库连接作为全局变量,是因为 Flask 的线程模型下,连接对象跨线程复用反而会引发更多问题。
4.3 存储生命周期管理:清理旧文件与保护 SD 卡
监控数据是持续增长的,如果不清理,SD 卡很快被占满。SD 卡的擦写寿命有限,频繁写入小文件会加速损坏。部署时把监控目录放到外接 U 盘或移动硬盘上,系统装在 SD 卡里,是更稳妥的布局。
| 数据 | 路径 | 建议保留时间 |
|---|---|---|
| 运动截图 | monitor_data/snapshot/YYYYMMDD/ | 7 天 |
| 触发视频 | monitor_data/video/YYYYMMDD/ | 7 天 |
| 事件日志 | monitor_data/logs/events.db | 30 天 |
清理脚本不需要引入第三方库,标准库就能实现:
#!/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(days=KEEP_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 # quality=70 压缩到几 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" b"Content-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(), mimetype="multipart/x-mixed-replace; boundary=frame") if __name__ == "__main__": t = threading.Thread(target=camera_worker, daemon=True) t.start() app.run(host="0.0.0.0", port=8000, threaded=True)这里最关键的工程决策是:相机读取和运动检测在独立线程里运行,latest_frame保存最新一帧的 JPEG 数据。Flask 推流时不直接读摄像头,而是读这个全局变量。frame_lock保证多线程同时访问时不会出现半写状态,但加锁本身也有开销,所以只在拷贝整数个 JPEG 字节时加锁,比直接传递原始frame数组高效得多。
boundary=frame出现在 mimetype 里,而 yield 的每一段也要以--frame\r\n开头,两者必须完全一致,否则视频流无法显示。浏览器访问/video_feed时会把响应当成持续更新的图片流,而不是一次性下载数据。
5.2 部署到系统服务:systemd 单元文件、开机自启与日志定位
监控系统不能依赖手动执行python3 run.py,断电重启后就丢了。树莓派 OS 自带 systemd,用一个 service 文件把 Python 进程托管起来是最基本的交付要求。
[Unit] Description=Dorm Monitor Service After=network.target [Service] User=pi WorkingDirectory=/home/pi/dorm_monitor ExecStart=/usr/bin/python3 /home/pi/dorm_monitor/run.py Restart=always RestartSec=5 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-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-monitorUser=pi是刻意为之,进程不需要 root 权限。摄像头设备节点/dev/video0默认属于video用户组,而pi用户默认在这个组里,所以直接能访问。Restart=always让进程崩溃后自动拉起,RestartSec=5避免高频重启把系统日志刷爆。
排查问题用journalctl -u dorm-monitor -n 50 -f看日志。这里有个易错点:代码里的print()默认是行缓冲,systemd 重定向输出时可能丢失部分日志,设置Environment=PYTHONUNBUFFERED=1强制 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?token=dorm-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 | 最小轮廓面积 | 连续确认帧数 | 效果预期 |
|---|---|---|---|---|
| 窗边风吹窗帘 | 36 | 2000 | 5 | 窗帘飘动不告警 |
| 门口检测人进出 | 25 | 800 | 2 | 进门立即触发 |
| 全屋夜间布防 | 48 | 3000 | 3 | 减少灯光和蚊虫干扰 |
帘飘动时轮廓面积往往不小,单靠面积过滤无效,所以把连续确认帧数调到 5,短促的运动不会触发。门口场景要求低延迟,确认帧数降到 2,检测灵敏度提高。全屋布防适合夜间,varThreshold=48让背景模型对微弱光影变化不敏感,同时用 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/?token=dorm-2025-lan" -o /dev/null -w "%{http_code}\n"这条 curl 命令返回 200 的瞬间,意味着摄像头采集、运动检测、SQLite 存储、Flask 推流和 Token 鉴权这五层链路全部通了。用这个结果作为“可交付”的验收标准,比录一段实时画面更经得起追问。
本文还有配套的精品资源,点击获取