
简介基于PythonOpenCVWebFFmpeg的智慧养老系统是一份适配毕业设计、课程设计场景的完整项目包面向计算机视觉方向的学生与开发者。系统通过多路模拟摄像头画面实时分析老人表情、摔倒、闯入禁区、义工互动及陌生人追踪等事件并将异常记录写入数据库供管理端报表查看。资源共1078个文件压缩包约316.77MB主要包含68个Python算法脚本、30个Vue前端页面、25个exe运行程序、PPT答辩稿与项目文档等覆盖OpenCV人脸录入、FaceNet单样本身份识别、Mini-Xception表情识别、OpenPose摔倒检测、质心跟踪入侵预警及Nginx-RTMP推流等模块。额外附带caffemodel模型、OpenPose构建文件与编译信息便于复现和二次开发。已有237人学习下载适合需要快速搭建智慧养老原型并理解多算法协同流程的读者。1. 智慧养老系统到底解决什么问题用计算机视觉替管理员盯几十路摄像头做过养老院/社区日间照料中心信息化的人都有体会真正的痛点不是摄像头不够多而是没人能一直盯着屏幕。这个毕设/课设项目正是冲着这个场景去的——用PythonOpenCVWebFFmpeg搭一套能自动“看”的智慧养老系统摄像头画面交给本地视觉分析识别老人微笑/情感、摔倒、闯入禁入区、与义工互动、陌生人出现并追踪事件命中后立即写数据库Web端报表实时刷新。管理人员不用盯视频只看事件列表就能做出反应服务响应速度能从“事后看录像”变成“事发当时就知道”。这套东西适合两类人一是需要交毕业设计/课程设计、想要完整闭环可复现系统的学生二是已经在养老机构做信息化的开发者想拿现成方案改造成生产系统。2. 系统总架构与开发环境PythonOpenCVWebFFmpeg这条链路怎么分工2.1 双端架构视觉分析端做事件识别Web端做事件展示整套系统拆成两个大块耦合点只有一个数据库。视觉分析端跑在多摄像头的工控机或服务器上负责拉取视频流、逐帧跑检测模型一旦命中事件就把记录写进数据库Web端跑在管理员的电脑上读数据库里的事件记录渲染实时报表和告警列表。两个端之间不直接通信检测端挂了Web端不会崩Web端重启也不会影响检测端写库。这种设计的好处是部署灵活我一般会把检测端单独放到一台机器上摄像头网段和办公网段隔离Nginx-RTMP只在内网直播。事件表是核心的数据结构每条事件至少要有摄像头ID、事件类型、置信度、发生时间、截图路径这五个字段报表页的刷新就是周期性查这张表。模块职责关键技术视觉分析端拉流、逐帧检测、事件判定、上报OpenCV、OpenPose、FaceNet、Mini-Xception、背景减除推流端把摄像头画面转成RTMP直播流FFmpeg、Nginx-RTMPWeb端事件报表、告警展示、人脸录入Flask、MySQL/SQLite、ECharts、win32com.client数据库解耦两端存储事件记录MySQL事件表老人档案表2.2 环境清单与安装顺序先装Python还是先装OpenPose顺序错了会很痛先给一套我自己在Windows 10上验证过的组合你按这个顺序装能少踩一半的坑。第一步装Python 3.8不要用3.10后面OpenPose的编译产物和某些opencv-python轮子在新版本Python下兼容性差第二步装OpenCVpip install opencv-python4.5.5.64就行注意把opencv-contrib-python也一起装上后面用背景减除和DNN人脸检测都用得到第三步装TensorFlow 2.5 CPU版FaceNet和Mini-Xception都是Keras模型CPU跑推理足够应付课设演示第四步装FFmpeg并配到环境变量里第五步编译或下载OpenPose的Windows版。人脸检测的模型文件项目里已经给到了res10_300x300_ssd_iter_140000.caffemodel和MobileNetSSD_deploy.caffemodel这是两个常用的SSD人脸检测器。前者是OpenCV官方DNN模块最常配的模型检测精度高一些但速度稍慢后者更轻量适合低配机器。加载方式用OpenCV的dnn模块统一读import cv2 # 加载SSD人脸检测器 face_net cv2.dnn.readNetFromCaffe( deploy.prototxt, res10_300x300_ssd_iter_140000.caffemodel ) cap cv2.VideoCapture(rtmp://localhost/live/camera01) while True: ret, frame cap.read() if not ret: break h, w frame.shape[:2] blob cv2.dnn.blobFromImage( cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) face_net.setInput(blob) detections face_net.forward() for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: box detections[0, 0, i, 3:7] * [w, h, w, h] x1, y1, x2, y2 box.astype(int) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)这段代码的逻辑是先把帧resize到300x300并做均值归一化forward一次得到所有人脸候选框confidenct大于0.5的才画框。这儿的0.5是经验阈值室内摄像头距离3米以内可以适当降到0.3因为距离远的人脸本来就糊。在智慧养老场景里摄像头一般都装在走廊和活动室天花板人脸目标偏小我建议固定用0.4。2.3 FFmpeg推流与Nginx-RTMP摄像头画面怎么变成Web能看的直播视觉分析端从RTMP流里读帧Web端要预览实时画面中间需要一台Nginx-RTMP服务器。Nginx的配置核心是加一个rtmp块指定直播应用名和鉴权key。rtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; allow play 127.0.0.1; allow play 192.168.1.0/24; deny play all; } } }配置里chunk_size设4096比较稳设太小延迟低但容易丢包花屏设太大延迟会累积到几秒。推流端用FFmpeg命令把本地摄像头或视频文件推到RTMP地址ffmpeg -re -i camera01.mp4 -c:v libx264 -preset ultrafast -tune zerolatency -g 30 -c:a aac -f flv rtmp://192.168.1.100/live/camera01这条命令的重点是-preset ultrafast和-tune zerolatency一个降低编码耗时一个关闭B帧缓冲对实时监控场景是必须的不然延迟会一路涨到5秒以上。实际项目中摄像头是直接推流的本地视频这种模式只在联调测试和答辩演示时用——把预设好的测试视频循环推流就能模拟多路摄像头这也是项目描述里“模拟多组摄像头”的常见做法。3. 老人识别与表情链路OpenCV录入 FaceNet单样本识别 Mini-Xception情感判定3.1 人脸注册流程OpenCV检测人脸拍照入库还有语音提示系统要识别老人第一步是建立老人档案。管理员在Web端上传或现场拍摄一张老人正面照片系统先用OpenCV的人脸检测器确认画面里确实有正脸然后裁剪人脸区域保存为注册图。因为用的是FaceNet的单样本人脸识别方案每个老人只需要一张注册照片不需要像传统人脸识别那样收集几十张。注册完成时的语音提示用的是win32com.client调用Windows自带的SAPI语音引擎这在Windows部署时不额外装任何东西就能响。代码如下import cv2 import win32com.client speaker win32com.client.Dispatch(SAPI.SpVoice) def register_face(frame, elder_id): h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104.0, 177.0, 123.0)) face_net.setInput(blob) detections face_net.forward() for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.7: box detections[0, 0, i, 3:7] * [w, h, w, h] x1, y1, x2, y2 box.astype(int) face frame[y1:y2, x1:x2] cv2.imwrite(ffaces/elder_{elder_id}.jpg, face) speaker.Speak(f老人{elder_id}注册成功) return True return False注册时的置信度阈值我建议比日常检测高0.7起步因为注册图的质量决定后面所有识别的上限。注册成功后一定要把face保存成统一尺寸FaceNet输入是160x160保存时最好就resize好省得推理时反复改尺寸。3.2 FaceNet单样本人脸识别128维embedding和阈值怎么定FaceNet做的事是把人脸图片映射成一个128维的向量同一个人的不同照片在向量空间里距离近不同人的距离远。系统里每个老人只有一个embedding识别时把摄像头抓到的每张脸也算出embedding然后计算它与所有注册老人embedding的欧氏距离取最小距离并判断是否小于阈值。阈值是这套方案最容易翻车的地方。距离阈值设0.9陌生人容易被误认成某个老人设0.6老人换个角度就被当成陌生人。实际项目里我一般先跑一段采集视频统计同一人脸在不同帧间的距离分布再取区分度最大的位置。核心代码如下import numpy as np from scipy.spatial.distance import euclidean def match_elder(face_embedding, elder_embeddings, threshold0.8): min_dist float(inf) matched_id None for elder_id, ref_embedding in elder_embeddings.items(): dist euclidean(face_embedding, ref_embedding) if dist min_dist: min_dist dist matched_id elder_id if min_dist threshold: return matched_id, min_dist return stranger, min_dist这里返回字符串“stranger”很关键陌生人检测就是靠这个分支触发的。一旦判定为stranger系统就把它当作陌生人事件写入数据库并开启质心跟踪。注意阈值和检测器数量要联动画面里同时出现多个老人时逐个人脸做比对每个都要单独判定不能只取一个全局最小值。3.3 Mini-Xception表情识别fer2013的7类情感和综合判定模型表情识别用的是Mini-Xception训练集是fer20137类angry、disgust、fear、happy、sad、surprise、neutral。Mini-Xception结构比完整Xception轻量很多卷积核和通道数都砍了适合实时推理。推理时把对齐后的人脸灰度图resize到48x48网络输出7个类别的概率取最大概率作为当前表情。单项表情识别在养老场景里不够用因为老人面无表情不等于不开心所以项目把Face Detector、FaceNet、Mini-Xception串成了综合模型命名叫Happy-Elder-Care-Net。逻辑是先检测人脸再识别身份最后判断表情。只有同时满足“是注册老人”和“表情是happy”才触发微笑事件如果是陌生人不管什么表情都走陌生人事件分支。见代码def elder_care_predict(face_rgb): # face_rgb: 已经裁剪并resize到160x160的人脸BGR图片 gray cv2.cvtColor(face_rgb, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, (48, 48)) # 表情推理 emotion_prob emotion_model.predict(gray[np.newaxis, ..., np.newaxis] / 255.0) emotion_label fer2013_classes[np.argmax(emotion_prob)] # 身份推理 emb facenet_model.predict(cv2.resize(face_rgb, (160, 160))[np.newaxis, ...]) identity, dist match_elder(emb[0], elder_embeddings, threshold0.75) if identity ! stranger and emotion_label happy: event_type elder_smile elif identity stranger: event_type stranger_detected else: event_type None return event_type, identity, emotion_label, float(np.max(emotion_prob))这个综合判定的好处是避免两套模型各报各的。单独跑表情模型陌生人笑一下也会报“老人微笑”糅合身份判定后误报会明显下降。实际测试里fer2013的happy类别识别率很高但disgust和fear经常混淆如果业务上不需要这两个类别建议在预测时直接把这两类的概率忽略掉只保留sad、neutral、happy、surprise四个有效状态。4. 摔倒、入侵与交互检测OpenPose关键点 背景减除 质心跟踪的指标设计4.1 摔倒检测OpenPose算角度和速度背景减除兜底摔倒检测最怕两件事弯腰捡东西被误判成摔倒真摔倒又没报出来。项目里的做法是用OpenPose提取人体关键点结合背景减除结果计算身高、宽度、角度、速度四类指标而不是单一靠宽高比。OpenPose在Windows上的推理输出是COCO 18个关键点我主要用脖子(关键点1)、左右髋(8和11)、左右脚踝(14和15)。摔倒判定的核心几何指标有两个躯干与地面的夹角以及人体外接框的高宽比。正常站立时躯干接近垂直夹角大于60度高宽比大于1.2倒地后夹角接近0度高宽比小于0.6。再加上关键点位移速度摔倒瞬间速度会出现一个尖峰这是区分“弯腰”和“摔倒”的关键——弯腰时速度平缓摔倒时速度突变然后静止。import math def compute_fall_metrics(keypoints, prev_keypoints, prev_time): # COCO关键点索引: 1-脖子 8-左髋 11-右髋 14-左踝 15-右踝 neck keypoints[1] hip ((keypoints[8][0] keypoints[11][0]) / 2, (keypoints[8][1] keypoints[11][1]) / 2) ankle ((keypoints[14][0] keypoints[15][0]) / 2, (keypoints[14][1] keypoints[15][1]) / 2) # 躯干角度脖子到髋部连线与水平面的夹角 angle math.degrees( math.atan2(abs(hip[1] - neck[1]), abs(hip[0] - neck[0])) ) # 速度髋部中心的位移/时间差 prev_hip ((prev_keypoints[8][0] prev_keypoints[11][0]) / 2, (prev_keypoints[8][1] prev_keypoints[11][1]) / 2) delta math.hypot(hip[0] - prev_hip[0], hip[1] - prev_hip[1]) speed delta / max(prev_time, 1e-6) # 高度/宽度比取人体框 xs [kp[0] for kp in keypoints if kp[0] 0] ys [kp[1] for kp in keypoints if kp[1] 0] hw_ratio (max(ys) - min(ys)) / max(max(xs) - min(xs), 1e-6) return angle, speed, hw_ratioangle小于25度且hw_ratio小于0.7同时speed在短时间内超过阈值的组合才会触发摔倒事件。我一般还会加一个持续帧判定连续10帧满足条件才上报因为刚倒下去时人还有一个缓冲动作单帧判定误报太多。背景减除在这里的作用是过滤掉椅子、拐杖这类静止物体造成的错误人体框用MOG2算前景掩码前景面积占比低于一定值就不送OpenPose。4.2 禁入区入侵检测与陌生人追踪质心跟踪怎么保持ID稳定禁入区检测用的是“质心是否在多边形内”的思路。管理员先在Web端画好禁入区比如配电房、护理站、楼梯口存成多边形坐标。系统每帧检测到人体框后取框底边的中点作为质心用射线法判断质心是否落在禁入区多边形内。用底边中点而不是框中心是因为站立时框中心偏高人站在禁入区外沿时中心可能还在外面但脚已经越界了。def point_in_polygon(x, y, polygon): n len(polygon) inside False for i in range(n): x1, y1 polygon[i] x2, y2 polygon[(i 1) % n] if (y1 y) ! (y2 y): t (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1) if t 0: inside not inside return inside陌生人追踪用的是质心跟踪Centroid Tracking算法。核心思想是对每一帧检测到的目标计算其质心位置然后与上一帧所有目标的质心做欧氏距离匹配距离最小的配对视为同一个目标。这里有几个关键参数最大匹配距离默认50像素超过就算新目标目标连续40帧无匹配就终止跟踪新目标连续出现3帧才正式确认避免单人单帧抖动产生一堆假ID。质心跟踪在俯视摄像头下效果最好但养老系统里摄像头大多是斜视角度前后帧同一目标质心跳变大我建议配一个简单的卡尔曼滤波做平滑或者把匹配距离放宽到80像素代价是两个人靠近时ID容易互换。如果场景里老人密度高就得升级到DeepSORT那种加外观特征的方案。4.3 基于GF(4)标定的交互检测判断老人和义工是不是真的在互动交互检测这块项目里用的方法是基于GF(4)的标定。GF(4)是有限域这里拿它做空间关系的几何约束标定通过标定把摄像头画面里的二维坐标映射到地面平面的坐标再判断老人与义工之间的距离、朝向、接触时间是否满足互动条件。严格来说这套标定方法比普通的单应性矩阵多了一层针对画面坐标的有限域编码不需要额外标定板用画面中的已知尺寸物体就能估算地面平面。交互检测的触发逻辑不复杂先识别出老人和义工的骨架计算两人髋部中心的地面投影距离小于1.5米算“接近”再跟踪这个接近状态保持超过10秒并且双方没有进入摔倒状态才判定为有效互动。这里最容易被忽略的是“义工”身份本身要先注册否则交互检测会把陌生人也算进来。我当时把义工和老人放在两张表里FaceNet比对时先查老人表查不到再查义工表交互逻辑只认义工身份。交互判定伪代码流程 1. 人脸识别区分老人/义工/陌生人 2. 对老人和义工分别提取OpenPose骨架 3. 计算地面投影距离连续10帧小于1.5米 4. 排除交互期间发生摔倒事件 5. 满足则写入interaction事件附带两人ID和起始帧这套流程在答辩演示时效果很好因为交互事件肉眼可见评委能直接看出系统判断对不对。要注意的是交互检测的帧率不能太低OpenPose推理一帧就要几百毫秒串行跑会把交互的10秒窗口拉得很长我一般把摔倒检测和交互检测放在两个进程中并行执行主进程只负责任务分发和结果汇总。5. 直连这几类常见坑OpenPose编译、RTMP延迟、拉流中断、FaceNet误报5.1 OpenPose在Windows上编译翻车CMake报错、CUDA版本不匹配现象按官方文档用CMake配置OpenPose源码到generate阶段报错或编译到一半卡在openpose_generated_bodyPartConnectorBase.cu.obj半天不动最后报CUDA编译器错误。原因OpenPose对CUDA、cuDNN、VS版本组合非常敏感。VS2019配CUDA 10.2可以换VS2022配CUDA 11.4就报一堆编译错误。项目文件里能看到这些编译残留的.cu.obj文件说明原作者也是在Windows下硬编译过或看过编译产物。解决优先下载官方预编译的Windows版OpenPose 1.7.0直接调用别从源码编译。必须自己编译时锁定VS2019 CUDA 10.2 cuDNN 7.6.5三件套CMake里关闭GPU_CUDA选项可以先跑CPU版本验证逻辑但CPU版跑一帧要1秒以上只适合功能测试。更省事的方案是把OpenPose换成OpenCV的DNN人体姿态估计——用MobileNet的prototxt精度低一点但完全避开编译地狱。5.2 FaceNet单样本人脸识别的误报换角度就识别成陌生人现象注册照片是正脸老人侧着坐在沙发上看电视时系统频繁报陌生人事件甚至把同一个老人一会儿判定为老人甲一会儿判定为陌生人。原因单样本embedding对姿态和光照极度敏感。一张正面照提取的embedding和侧脸照提取的embedding之间距离超过阈值很正常。FaceNet本身不是为单样本设计的单样本是硬凑出来的用法。解决注册时不要只拍一张绕老人多拍3-5个角度的照片算出embedding后取平均值存为参考向量阈值从0.9降到0.7给姿态变化留余量。还有一个血泪教训识别前一定要做人脸对齐不然同一个人的embedding波动比不同人之间的差异还大。5.3 Nginx-RTMP直播延迟越来越大的原因和调整现象直播刚连上时延迟1秒运行半小时后延迟涨到5-8秒画面明显比实时慢。原因FFmpeg推流用了默认编码参数I帧间隔太长Nginx-RTMP缓冲积累。RTMP是TCP协议网络抖动时Nginx会为了流畅性缓存大量数据延迟就滚雪球。解决FFmpeg推流参数按第2章那条命令来加-g 30和-tune zerolatencyNginx的chunk_size设4096rtmp块里加drop_idle_publisher 10s让断流的推流端尽快释放。如果还嫌延迟高播放端用VLC的低延迟模式或ffplay加-fflags nobuffer -flags low_delay内网环境下能压到1秒内。5.4 OpenCV读取RTMP流中断后不自动重连现象摄像头推流端重启或者Nginx短暂重启后视觉分析端的cv2.VideoCapture.read()一直拿到False但进程没退出看起来卡死了。原因VideoCapture在底层RTMP断流后状态不会自动恢复read()会一直阻塞或返回False。大部分人的第一版代码都没有重连逻辑进程就僵在那直到管理员手动重启。解决把拉流放到独立线程里循环判断cap.isOpened()读取失败超过10次就用新地址重新构造VideoCapture并设置CAP_PROP_BUFFERSIZE减小积压帧数。基本模板如下def safe_capture(rtmp_url): cap cv2.VideoCapture(rtmp_url) cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) fail_count 0 while True: if not cap.isOpened(): time.sleep(3) cap cv2.VideoCapture(rtmp_url) continue ret, frame cap.read() if not ret: fail_count 1 if fail_count 10: cap.release() cap cv2.VideoCapture(rtmp_url) fail_count 0 continue fail_count 0 yield frame5.5 摔倒检测把弯腰捡东西报成摔倒现象老人系鞋带、弯腰捡遥控器系统触发摔倒告警真摔倒了反而没报或延迟很久。原因只用了高宽比这一个指标弯腰时高宽比同样小于0.7单指标必然撞车另外摔倒瞬间速度尖峰没有被捕捉连续帧判定逻辑缺失。解决必须合并角度、速度、持续帧三个维度。angle小于25度且hw_ratio小于0.7且speed超过历史均值3倍三个条件同时满足计数连续10帧才上报。加一个后置排除触发后0.5秒内人体关键点重新站立就撤销事件或者标记为误报。这组参数要实际录几段弯腰和摔倒视频来标定别拍脑袋设。6. 进阶用回放视频做阈值回归验证把误检率压到可接受范围系统开发到后期真正耗时间的不是写代码而是调阈值。检测器有八九个阈值人脸置信度、FaceNet距离、摔倒角度/速度/持续帧、禁入区越界匹配距离、陌生人确认帧数……调任何一个都可能影响其他事件。以前我改一个阈值就靠肉眼盯着屏幕看效果运气好试几次能稳定运气不好把摔倒阈值调宽松了陌生人区域又多出一堆误报。后来我的习惯是录一段30分钟的真实场景视频包含标注好的事件时间点做成一份简单的CSV标注文件格式是“起始帧、事件类型、人员ID”。每次调整阈值后用脚本对这段视频重新跑一遍全部检测器把输出的事件与标注做比对统计每个事件类型的检全率和误报数。回归验证跑完再改下一个参数。这么做的好处是参数改动效果可量化答辩时评审问“你怎么保证系统稳定性”直接掏出比对结果比口头解释有力得多。阈值调整本身也有先后顺序先定人脸置信度因为它影响所有人脸相关下游任务再定FaceNet距离阈值最后才调摔倒这类低频率事件的阈值。表情模型的fer2013各类别权重我一般不调连权重选项都砍掉。陌生人追踪的匹配距离和最大消失帧数注意别同时设太激进匹配距离小消失帧数小会让同一个陌生人被重复上报数据库里全是同一个人的重复事件。项目文档里如果有原始标注数据建议直接复用没有就自己录熬过一次标定后面就顺了。从那以后我每次改完阈值都强制走一遍视频回放标注比对流程宁可多跑三十分钟脚本也不让一个调参bug带到答辩现场。希望帮到你。本文还有配套的精品资源点击获取