基于OpenCV的智慧养老监控系统:人脸识别、摔倒检测与Web可视化全流程解析
2026/9/16 6:36:21 网站建设 项目流程

简介:面向计算机视觉方向的毕业设计、课程设计,资源是一套基于Python+OpenCV+Web+FFmpeg的智慧养老系统完整实现。系统模拟多组摄像头实时画面,综合FaceNet单样本人脸识别、Mini-Xception表情识别、OpenPose摔倒检测、GF(4)标定交互检测与质心跟踪入侵检测等技术,可识别老人情绪、摔倒、误闯、互助及陌生人出现等情况并自动写入数据库,Web端报表联动Nginx-RTMP推流,帮助管理人员快速响应。资源包共1078个文件,约316.77MB,涵盖Python源码、OpenCV/CMake工程配置、CSS/Vue前端页面、模型文件(caffemodel/h5)以及答辩PPT、课程设计文档等,目录结构清晰,便于按模块查阅与二次开发。已有237人学习,适合希望快速理解人脸识别、行为分析、视频推流整合流程,并完成毕设/课设方案的同学参考。

1. 从摄像头到数据库:智慧养老系统的视觉事件流水线到底怎么拆

老人摔倒后,通常不是没有摄像头拍到,而是没有人在几十路监控流前一直盯着。一套能用的智慧养老系统,价值不在某个模型刷了多少分,而在于“摄像头画面 → 事件检测 → 数据库落盘 → Web 报表可见”这条链路能不能稳定跑通,让管理员打开网页就看见“3 号房摔倒”这条记录。这个基于 python、opencv、web 和 ffmpeg 的毕业设计/课程设计,把 OpenCV 人脸检测、FaceNet 单样本身份识别、Mini-Xception 表情识别、OpenPose+背景减除摔倒检测、质心跟踪入侵检测,以及 Nginx-RTMP 直播推流整合成一条可演示闭环。适合正在做计算机视觉大作业、Web 与视觉结合的课程设计,或者想快速搭一个能答辩展示的养老监控原型的人。

2. 人脸与表情识别:OpenCV 人脸录入、FaceNet 单样本身份识别和 Mini-Xception 情感判定

2.1 为什么检测层用 OpenCV DNN 而不是 Haar Cascade

很多入门项目还在用 cv2.CascadeClassifier 做检测,理由是调用简单、模型文件小。但放到养老场景里,老人常常是侧脸、低头、逆光,Haar 正脸检测器漏检和误检都非常明显,一旦漏检,后面人脸录入、身份比对全部失效。这个系统采用的是 OpenCV DNN 模块加载 Caffe 格式的 SSD 人脸检测器,也就是 res10_300x300_ssd_iter_140000.caffemodel,配合 deploy.prototxt。它把一个 300x300 的 BGR 图像输入网络,输出 shape 为 (1, 1, N, 7) 的检测结果,其中每个检测包含类别置信度和归一化的人脸框坐标。

人脸录入时,先从摄像头取一帧,用下面的代码定位人脸并截取区域,把对齐后的人脸图片保存到 identity 目录。常见做法是对每张注册照片做一次检测,只保留置信度大于 0.7 的框,避免把背景衣服纹理当成脸。

import cv2 import numpy as np face_detector = cv2.dnn.readNetFromCaffe( "deploy.prototxt", "res10_300x300_ssd_iter_140000.caffemodel" ) def get_face_boxes(frame, conf_thresh=0.7): h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage( frame, 1.0, (300, 300), (104.0, 177.0, 123.0) ) face_detector.setInput(blob) detections = face_detector.forward() boxes = [] for i in range(detections.shape[2]): score = detections[0, 0, i, 2] if score < conf_thresh: continue box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype(int) x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) boxes.append((x1, y1, x2, y2)) return boxes

这里blobFromImage有三个参数要注意:第一,(300, 300)必须与模型的训练输入尺寸一致,随便改成 224 会直接影响检测精度;第二,(104.0, 177.0, 123.0)是 Caffe SSD 训练时使用的 BGR 三通道均值,不能换成像 ImageNet 那样的 RGB 归一化,否则肤色较深的老人很容易被漏检;第三,detections[0, 0, i, 2]是第 i 个检测框的置信度,一般经验值是 0.5 到 0.7,走廊这种空旷场景 0.5 够用,答辩演示时可以调到 0.7 减少误报。

OpenCV 的rect相关函数在后续裁剪时也会用到,比如cv2.boundingRectcv2.rectangle。需要注意的是 OpenCV 里的坐标是 (col, row),先 x 后 y,很多从 numpy 转过来的人容易把切片顺序写反。人脸框拿到后,用frame[y1:y2, x1:x2]就能得到人脸区域。录入时建议每隔几帧抽一张,多保存几张备选,因为单样本人脸识别对注册照片质量很敏感,拍糊的照片会直接拉高身份判定的距离阈值。

2.2 单样本识别:用 FaceNet embedding 做身份判定

老人注册只需要拍一张照片,这是典型的单样本人脸识别问题。如果直接用分类网络训练,每个人只有一张样本,模型很容易过拟合到衣服或背景。FaceNet 的做法是把人脸映射到 128 维的 embedding 空间,同类人脸的距离小,异类距离大。运行时先提取注册照片的 embedding 存起来,再对摄像头里的每张人脸提取 embedding,计算欧式距离,距离小于阈值就判定为同一人。

下面这段逻辑是这个项目里最常用的工具代码,既服务签到点名,也服务陌生人检测。FaceNet 的权重可以用 TensorFlow 的 Facenet 仓库导出 pb,再转成 ONNX,或者直接用 Keras 加载 facenet_keras.h5。为了让 OpenCV 统一加载,我建议转成 ONNX 后调用cv2.dnn.readNetFromONNX,这样整个项目只依赖 OpenCV 一个推理后端。

import cv2 import numpy as np facenet = cv2.dnn.readNetFromONNX("facenet.onnx") def get_embedding(face_rgb): face = cv2.resize(face_rgb, (160, 160)) blob = cv2.dnn.blobFromImage( face, 1.0 / 255.0, (160, 160), (0, 0, 0), swapRB=True ) facenet.setInput(blob) return facenet.forward().flatten() def face_distance(emb_a, emb_b): return float(np.linalg.norm(emb_a - emb_b))

这里的swapRB=True表示把 OpenCV 读到的 BGR 转成 RGB,因为 FaceNet 训练时用的是 RGB 图像;1.0 / 255.0是输入归一化。距离阈值会因模型权重不同而波动,我一般会在摄像头前拍同一个人 20 张正面照,统计同类距离,再拍 10 个陌生人看跨类距离,取两者的中间值。按常见 Facenet 权重的经验,阈值通常落在 0.9 到 1.2 之间;阈值太小会把老人误判成陌生人,阈值太大陌生人会混进来。

有了 embedding 之后,还要建一个注册表。项目里的常见做法是用 SQLite 或 JSON 文件保存 person_id 和 embedding 的对应关系,避免每次启动都重新推理注册照片。匹配时遍历注册表,找到最小距离的那个人;如果最小距离仍然大于陌生人阈值,就标记为 visitor。后续的“陌生人出现并追踪”就是基于这个 visitor 标签来驱动的。

2.3 微笑检测与 win32com.client 语音提示

“Happy-Elder-Care-Net”在这个项目里不是一个模型文件,而是一条推理组合:先用 OpenCV 人脸检测器找脸,再用 FaceNet 判断身份,最后将身份已知的人脸输入 Mini-Xception 表情分类器。Mini-Xception 是基于 fer2013 数据集训练的轻量表情模型,输入 48x48 灰度图,输出 7 类情绪概率,结构上比大分类网络轻很多,可以在 CPU 上跑,适合和检测、识别放在同一条视频管线里。

fer2013 的类别顺序通常是 angry、disgust、fear、happy、neutral、sad、surprise。做“微笑检测”时不必看概率最大的类别,可以直接读 happy 这一维的分数,比如大于 0.6 就算一次微笑。对于单人老人房间,这个组合逻辑的伪代码如下。

from win32com.client import Dispatch speaker = Dispatch("SAPI.SpVoice") def analyze_face(frame, face_box): x1, y1, x2, y2 = face_box face = frame[y1:y2, x1:x2] emb = get_embedding(cv2.cvtColor(face, cv2.COLOR_BGR2RGB)) person, dist = match_identity(emb) # 自定义匹配函数 if person is None: insert_event("visitor", camera_id="cam1", frame=frame) speaker.Speak("有陌生人出现") return emotion_probs = emotion_net.predict(prepare_emotion_input(face)) happy_score = emotion_probs[4] # 对应 happy 类 if happy_score > 0.6: insert_event("smile", person_id=person, frame=frame)

win32com.client只能运行在 Windows 环境,项目说明里写了基于 Windows 10,所以用 SAPI 播报很合适。语音提示不是必备模块,但答辩演示时非常有效:现场摄像头检测到人脸,电脑直接播报,比只看日志直观。Dispatch("SAPI.SpVoice")是 Windows 自带 COM 接口,不需要额外安装语音包。要注意 Voice 对象不能跨线程并发调用,多路摄像头同时触发时最好加一个锁,或者只在主线程做语音播报。

模型任务输入尺寸关键输出
OpenCV DNN SSD人脸检测300x300 BGR人脸框、置信度
FaceNet身份 embedding160x160 RGB128 维向量
Mini-Xception表情分类48x48 GRAY7 类概率

prepare_emotion_input通常包含灰度化、缩放和归一化,具体代码和上面的get_embedding类似,只是通道数不同。Mini-Xception 的权重只要是 fer2013 训练的即可,转成 ONNX 或直接用 Keras 加载都可以。这里要注意的是表情模型对低分辨率人脸敏感,如果 OpenCV 检测出来的人脸小于 32x32,最好直接跳过表情识别,否则输出基本是随机概率。

3. 摔倒、入侵与交互检测:OpenPose 姿态特征、背景减除和质心跟踪的取舍

3.1 OpenPose 与背景减除的双通道设计

摔倒检测是这个系统技术含量最高的模块。直觉上,用 OpenPose 拿到 18 个关键点,再判断“脖子到髋部是否接近地面”就够了。但在实际视频里,只靠关键点会出现两类问题:一是姿态估计存在抖动,单帧关键点位置不稳定,误报率高;二是多人场景下关键点匹配到谁、遮住一半身体时怎么办。所以项目在 OpenPose 前面增加了一层背景减除,先用cv2.createBackgroundSubtractorMOG2把移动的人物区域切成前景 mask,再用这个 mask 缩小 OpenPose 的推理区域,同时提供额外的几何指标。

背景减除适合摄像头固定不动的场景,养老院房间和走廊恰好满足这个前提。常见做法如下。

import cv2 sub = cv2.createBackgroundSubtractorMOG2( history=500, varThreshold=16, detectShadows=True ) fgmask = sub.apply(frame) fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))) _, fgmask = cv2.threshold(fgmask, 200, 255, cv2.THRESH_BINARY) cnts, _ = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

history 是背景更新帧数,值越大背景越稳定,但对光照变化适应越慢。varThreshold 是每个像素的方差异常阈值,室内固定摄像头一般取 16 到 25。detectShadows 默认会把人物阴影标成灰色,后续用开运算和阈值处理可以消掉一部分,但完全去阴影往往要配合 HSV 阴影检测,毕业设计做到 Open + Threshold 即可。findContours之后只保留面积最大的几个轮廓,可以显著减少噪声。

拿到前景轮廓后,可以计算外接矩形和质心,同时把轮廓外接框作为 OpenPose 的输入裁剪框。这里有一个比较关键的取舍:OpenPose 在 GPU 上推理一帧需要几十毫秒,而背景减除只需要几毫秒,因此应该降低 OpenPose 调用频率,比如每 3 帧调用一次姿态估计,中间 2 帧用前景 mask 维持跟踪。这个“采样-插值”的做法能稳定提高 FPS,也是答辩时值得展开的细节。

3.2 摔倒判据:高度、宽高比、角度和速度

摔倒检测不能只看某一个指标。项目里采用 OpenPose 的 18 点骨架和背景减除的数据一起计算四个指标:人体高度、人体宽度、躯干主轴与水平面夹角、质心速度。摔倒瞬间,人体高度骤降,宽度增大,躯干角度从近似垂直变成接近水平,质心速度先冲高再骤然降为零。把四个指标做加权投票,比单独用宽高比更稳。

下面的函数演示了如何把这些指标串起来。这里用背景减除得到的轮廓外接矩形粗略替代人体包围框,用 OpenPose 的鼻尖点(0 号)和髋部中心(8 号)之间的连线近似躯干主方向。

def compute_fall_features(fgmask, keypoints, prev_center): h, w = fgmask.shape[:2] ys, xs = np.where(fgmask > 0) if len(xs) < 20 or len(ys) < 20: return None box_h = ys.max() - ys.min() box_w = xs.max() - xs.min() wh_ratio = box_h / (box_w + 1e-5) nose = keypoints[0][:2] mid_hip = keypoints[8][:2] angle = np.abs(np.arctan2(mid_hip[1] - nose[1], mid_hip[0] - nose[0] + 1e-5)) center = np.array([(xs.min() + xs.max()) / 2, (ys.min() + ys.max()) / 2]) speed = float(np.linalg.norm(center - prev_center)) if prev_center is not None else 0.0 return box_h, wh_ratio, angle, speed, center

实际判据可以写成:box_h小于站立时平均高度的 60%,或wh_ratio小于 0.6,且angle小于 30 度。speed 只作为辅助,因为速度要在历史帧上算,不能单帧判断。这些阈值绝不能凭经验写死,建议让系统先采集每个人 10 秒钟正常站立和走动数据,算出该人高度基线,再用“当前高度 < 基线高度 * 0.6”作为核心条件。注意 OpenPose 的keypoints[0][:2]是鼻子的像素坐标,顺序是 (x, y),如果模型输出的 y 轴方向不同需要先翻转,否则角度会算成 90 度的补角。

3.3 入侵检测与质心跟踪

入侵检测在养老院场景里是“老人或陌生人进入禁止区域”的报警。管理员在摄像机画面里画一个多边形禁区,系统对每一帧人物质心做判断,质心落入多边形即触发事件。这里用到的函数是cv2.pointPolygonTest,返回值为正表示点在多边形内部,负表示外部,0 表示在边界上。多个摄像头各自配置不同的禁区多边形,事件表里带上 camera_id 即可。

跟踪任务则需要质心跟踪算法。背景减除得到多个轮廓,每个轮廓计算一个质心,然后把这些质心与已有的轨迹对象做最近邻匹配。经典的 CentroidTracker 只需要几十行代码:

class CentroidTracker: def __init__(self, max_disappeared=50): self.next_id = 0 self.objects = {} self.disappeared = {} def register(self, centroid): self.objects[self.next_id] = centroid self.disappeared[self.next_id] = 0 self.next_id += 1 def update(self, centroids): if not centroids: self.disappeared = {k: self.disappeared[k] + 1 for k in self.objects} return self.objects # 对每个已有对象找最近的质心,做贪心匹配 ...

真正的 update 里,应该先计算所有已有对象和新质心之间的欧氏距离矩阵,按最近距离升序分配,并设置一个最大匹配距离,超过这个距离的质心视为新对象。匹配后,对象连续消失超过 max_disappeared 帧就删除。这样做的好处是即使前后两帧检测不连续,轨迹仍能保持,陌生人从进入画面到走出画面都能被追踪,从而在报表里展示“陌生人从哪个摄像头进入、停留了多长时间”。当质心首次进入禁区且对象 ID 是陌生人时,再插入一次入侵事件,避免每一帧都写数据库。

3.4 交互检测与 GF(4) 地面标定

原文提到的 GF(4) 标定,乍看像抽象代数里的有限域 GF(4),但在工程实现里一般是指使用四个地面参考点做单应性标定,把像素坐标映射到地面物理坐标。交互检测为什么要做这件事?因为“老人和义工在互动”不能只看屏幕上的像素距离,还要考虑摄像头视角带来的透视畸变:画面远处两个人在像素上隔 200 像素,实际可能只有半米;近处隔 200 像素可能有 3 米。用四个点标定地面后,可以将像素坐标投影到米制地面坐标,再判断两人之间的物理距离是否持续小于 1 米。

常见做法是在地面上用 A4 纸贴四个点,位置分别为房间四角,记录它们的像素坐标和实际坐标。代码只需要一个投影函数:

src = np.array([[120, 240], [520, 250], [540, 470], [100, 460]], dtype=np.float32) dst = np.array([[0, 0], [4, 0], [4, 3], [0, 3]], dtype=np.float32) # 单位米 H = cv2.getPerspectiveTransform(src, dst) def pixel_to_ground(x, y): pixel = np.array([[[x, y]]], dtype=np.float32) ground = cv2.perspectiveTransform(pixel, H) return ground[0][0][0], ground[0][0][1]

cv2.getPerspectiveTransform接受两个数组,各包含 4 个点,前一个是图像坐标,后一个是地面坐标。投影矩阵 H 是一个 3x3 的单应性矩阵。只有当四个地面点确实共面时,单应才成立,所以标定要贴着地面选点。交互检测逻辑里,先把所有目标质心用pixel_to_ground转换,再计算两两物理距离,连续若干帧距离都小于阈值,就认为存在互动。这个“地面距离”替代“像素距离”的做法,在答辩时能明显提升方案技术深度,因为它解决了视角问题而不是简单堆模型。

4. 事件落库、Web 报表与 RTMP 推流:从摄像头到管理员的可见延迟

4.1 事件表结构与 SQLite 写入

当摔倒、陌生人、入侵、微笑这些事件被检测出来后,下一步是写入数据库。选型上 SQLite 足够支撑毕业设计和课程设计级别的演示,不需要单独安装数据库服务,代码里用标准库 sqlite3 即可。如果后续要部署到多管理员同时查看、事件量达到每天几十万条,再迁移到 PostgreSQL 也不难,因为 SQL 结构基本一致。事件表建议设计成下面这样。

CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL, confidence REAL, camera_id TEXT NOT NULL DEFAULT 'cam1', person_id TEXT, frame_path TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);

字段里 event_type 用字符串,比如 fall、visitor、intrusion、smile、interaction,而不是数字枚举,这样 Web 端拿回去直接显示中文映射,排错时也容易看懂。confidence 是检测置信度或距离得分,例如摔倒投票得分、人脸匹配距离的负值。frame_path 保存触发瞬间截图在服务器上的相对路径,报表页面可以直接配上缩略图,这对养老院管理人员非常重要。

写入函数要封装成独立方法,并且注意在同一线程里复用连接。检测线程有多个时,每个线程只创建自己的连接,避免 SQLite 连接跨线程。事件量不大时,每次插入后立即 commit 的代价可以忽略。

import sqlite3 def insert_event(event_type, confidence, camera_id, person_id=None, frame_path=None): conn = sqlite3.connect("elder.db") try: conn.execute( "INSERT INTO events(event_type, confidence, camera_id, person_id, frame_path) " "VALUES (?, ?, ?, ?, ?)", (event_type, confidence, camera_id, person_id, frame_path) ) conn.commit() finally: conn.close()

这段代码里使用?占位符而不是字符串拼接,能防止 SQL 注入,也能自动处理 None 值。commit 放在 try 的结尾,close 放在 finally,保证连接一定会释放。事件插入后,报表页面重新查询即可看到最新记录,这就实现了“事件立即插入数据库”的需求。

4.2 Flask 实时报表:轮询还是 WebSocket

Web 界面是管理人员真正会打开的东西。后端用 Flask 最省事,一条路由返回 JSON 给前端轮询。轮询和 WebSocket 的取舍在于:轮询实现简单、部署不容易出问题,2 秒一次足够养老院管理场景;WebSocket 能毫秒级推送,但需要维护长连接和断线重连,课程设计里没必要为它增加复杂度。这里用 Flask 提供一个最新事件接口。

from flask import Flask, jsonify import sqlite3 app = Flask(__name__) @app.get("/api/events/latest") def latest_events(): conn = sqlite3.connect("elder.db") conn.row_factory = sqlite3.Row rows = conn.execute( "SELECT * FROM events ORDER BY created_at DESC LIMIT 20" ).fetchall() conn.close() return jsonify([dict(r) for r in rows])

前端用 setInterval 每 2 秒拉一次接口,渲染成表格或卡片。row_factory = sqlite3.Row让查询结果可以直接转成 dict,省去手动映射。20 条的限制是为了避免首屏加载过慢,实际报表页往往会加日期筛选和分页。如果事件突然增多,2 秒轮询的重叠请求可能堆积,可以在前端判断上一次请求完成后再发起下一次,而不是单纯 setInterval。也可以用 fetch 加 AbortController 做超时取消。

管理员看报表时最关心三块:当前有哪些未处理事件、事件在哪个摄像头发生、截图长什么样。所以接口返回里要带上 camera_id 和 frame_path,前端遇到 frame_path 为 null 时显示占位图。实时性不追求毫秒,反而要保证每次刷新页面不卡,轮询完全够用。

4.3 Nginx-RTMP 直播推流与 ffmpeg 命令

摄像头不仅是用来视分析的,管理员还想实时看到画面。项目采用 Nginx-RTMP 直播推流,把 OpenCV 读到的画面或者本地视频文件推给流媒体服务器,Web 端再拉流播放。这样视频流和事件分析共用同一路输入,不会出现“报表说有人摔倒,但回放找不到”的问题。

FFmpeg 负责把视频源转成 RTMP 流。Windows 下如果用的是 USB 摄像头,命令这样写:

ffmpeg -f dshow -i video="USB Camera" -vcodec libx264 -preset ultrafast -tune zerolatency -f flv rtmp://127.0.0.1:1935/live/cam1

参数说明:-f dshow是 Windows 的 DirectShow 输入格式,-i后跟的是摄像头设备名,可以通过ffmpeg -list_devices true -f dshow -i dummy查看;-preset ultrafast-tune zerolatency是直播推流的核心参数,前者降低编码 CPU 占用,后者减少编码器缓冲,从而降低端到端延迟;-f flv指定输出封装格式为 FLV,因为 RTMP 只接受 FLV 封装。如果只想用本地视频模拟一路摄像头,可以把前半段换成-re -stream_loop -1 -i demo.mp4-re控制按视频原始帧率读取,-stream_loop -1无限循环。

Nginx 侧需要编译或安装带 rtmp 模块的版本。配置文件里加入以下块:

rtmp { server { listen 1935; chunk_size 4096; application live { live on; allow_play all; } } }

配置好后,浏览器播放 RTMP 需要依赖插件,所以更常见的做法是在 Nginx 里把直播流转成 HLS:在 application 块里加hls on; hls_path /tmp/hls;,这样前端用 video 标签直接播http://127.0.0.1:8080/live/cam1.m3u8。实际项目中,OpenCV 的视频分析进程和 ffmpeg 推流进程可以分开,一个进程负责检测、另一个进程负责推流,避免单点积压导致画面延迟越来越大。

到这里,整条链路就齐了:摄像头被 ffmpeg 推流,同时被 OpenCV 读帧分析,分析出的事件写 SQLite,Flask 把事件返回给 Web 页面,页面里还有 m3u8 直播地址。管理员的可见延迟由推流延迟、事件轮询延迟和数据库刷新延迟三部分叠加,一般能控制在 3 秒以内。

5. 跑通后的调参、验证和排错技巧

5.1 用日志记录事件延迟和 FPS

系统能跑和能答辩是两回事。答辩时老师问“检测速度是多少”,答不上来会减分。建议在检测主循环里加一个简单的耗时统计,每 5 秒打印一次平均 FPS 和事件落库延迟。

import time fps_frames = 0 fps_start = time.time() last_event_latency = 0.0 while cap.isOpened(): ret, frame = cap.read() t0 = time.time() # 检测、识别、写入事件 fps_frames += 1 if time.time() - fps_start >= 5.0: fps = fps_frames / (time.time() - fps_start) print(f"FPS={fps:.2f}, event_latency={last_event_latency:.1f}ms") fps_frames, fps_start = 0, time.time()

这段日志是调阈值时唯一的参照。FPS 低于 10 说明检测太重,优先降低 OpenPose 调用频率;FPS 稳定后,再逐项测试摔倒、陌生人、入侵,保证每个事件都能在 1 秒内出现在报表页。答辩时拿出这份日志,比空口讲“很快”更有说服力。

5.2 模型加载和摄像头调试的常见坑

这类项目最常见的报错是ModuleNotFoundError: No module named 'opencv',本质是没装对包,需要用pip install opencv-python而不是手动下载 opencv 安装包。另一个高频坑是readNetFromCaffe读不到模型文件,通常因为工作目录不对,最好用绝对路径或用os.path.join拼接。cv2.VideoCapture(0)打不开摄像头时,先检查设备是否被会议软件占用,Windows 上关掉占用程序再试。如果使用 ffmpeg 推流,还要确认防火墙放行 1935 端口,否则 Web 端一直拉不到流。

5.3 环境就绪后的分级验证清单

把完整链路拆成三级来验证:先只跑 python 人脸模块,确认能在标注框里看到人名;再启动摔倒、入侵检测,故意在地上躺倒或跨入禁区,观察事件表里是否新增记录;最后开 ffmpeg 推流和 Web 页面,用手机流量访问一次,排除局域网 IP 配置问题。这套验证顺序能让你在改完代码后快速定位是模型问题还是链路问题。多路摄像头模拟用两个本地视频文件分别推流到 cam1、cam2,再在事件表和 HLS 中确认 camera_id 字段对应正确即可。

整条基于 OpenCV 的智慧养老事件链路到这里就具备可复现性了:人脸录入、单样本身份识别、摔倒判据、入侵跟踪、事件落库、Web 报表、RTMP 直播,每一层都有可单独调试的接口,这也是答辩时最能打的内容。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询