
简介面向计算机视觉与人体姿态识别入门及进阶开发者该项目基于 Python、OpenCV 与 OpenPose 构建实时人体形态识别系统覆盖摄像头视频流读取、关键点检测、结果可视化及自定义数据集训练流程可直接用于安全监控、运动分析、健身指导等场景。压缩包共 40 个文件以 19 个 Python 脚本和 12 个编译后的 pyc 文件为主体包含姿态估计模块、损失函数、关键点滤波、ONNX 转换等工具脚本另有 README 与训练说明文档、演示视频、预览图片及依赖清单整体仅 8.12MB结构清晰。已有超过 7000 人学习下载。内容同时提供基于 MobileNet 的轻量模型实现、COCO 数据集处理脚本和自定义数据集训练指南开发者可在 demo.py 基础上快速跑通演示也能进一步改造用于人体关键点跟踪、动作识别等扩展应用适合通过实战快速上手 OpenPose 与 OpenCV 的开发者。1. 视频和摄像头里识别人体形态为什么绕不开 OpenPose搜过 opencv 图像处理项目、翻过一堆人体姿态识别源码的人大概都有同一种体会这活儿看着是深度学习的事真落到自己电脑的摄像头上八成的功夫都花在环境、模型和坐标系上。标题里三个词各管一段——Python 把工程串起来OpenCV 负责从摄像头或视频文件里取帧、做预处理和结果绘制OpenPose 负责在每一帧里找出人的关键点肩、肘、腕、髋、膝、踝。这套组合能做健身计数、体态提醒、安防行为分析这类应用的前置识别层也是生成式管线里 OpenPose 预处理那套东西的基本逻辑。「人体形态识别」听起来像端到端的动作分类实际工程里常态是两段式先用 OpenPose 把骨架捞出来再写规则去读骨架。这篇文章把从摄像头取流到输出形态语义的链路拆开重点放在可以直接照抄的推理代码、阈值怎么设以及十次调试九次会撞上的坑。若只想做物体识别和计数传统图像处理就够不需要走到这一层。2. 先把环境盘干净OpenCV DNN 加载 OpenPose 模型这条路为什么最省心2.1 三条候选路线只有 OpenCV DNN 适合快速落地OpenPose 接入有几条典型路线。第一条是直接编译官方 C 仓库它依赖 Caffe、CUDA 和 cuDNN配合 CMake 的版本闭环配好了效果上限最高能输出 BODY_25 的 25 点骨架但对大多数人来说光是把依赖彼此驯服就能耗掉一个通宵。第二条是用 tf-pose-estimation 这类 TensorFlow 迁移实现Python 侧看着友好但它依赖的 TF 版本与当前新环境匹配一般装完常常遇到算子兼容问题。第三条也是我实际项目里最常用的一条不碰 OpenPose 源码只把官方训练好的 Caffe 模型交给 OpenCV 的 DNN 模块加载。OpenCV 从 3.4.3 开始内置 DNN 模块cv2.dnn.readNetFromCaffe()可以直接读 prototxt 结构文件加 caffemodel 权重文件因此无需安装 Caffe 和 CUDA一台普通笔记本从零到出画面一小时左右。在线检索「人体形态识别」相关项目时你会发现独立开发者的方案大多也收敛到这条线不是没有理由的。我的建议是先用这条路把业务验证跑通确认形态识别在你的场景可行之后再回过来评估要不要上原版拿到更精细的 PAF 连接。上来就编译官方包等于把验证周期的前三天押在环境上这在项目节奏里是最不划算的。三条路线对比如下路线依赖优点缺点官方 C OpenPoseCaffe、CUDA、cuDNN、CMake25 点最全、PAF 最准编译成本极高劝退率最高tf-pose-estimationTensorFlow 旧版本Python 侧友好环境匹配差、维护滞后OpenCV DNN Caffe 模型opencv-python numpy纯 CPU 能跑十分钟起 demoPAF 连接被简化靠预设连线2.2 最小环境清单Python、OpenCV 和两个模型文件的对应关系如果从零开始我的环境建议是Windows 上直接去 python 官网下载安装包安装时勾选 Add to PATH省掉后面所有「python 不是内部或外部命令」的烦恼Ubuntu 上则用 apt 装好 python3 后全部通过python3 -m pip装包不要混用系统自带的 pip。这套方针适合 python 入门阶段就把它定下来后面换机器也不容易踩到解释器错位。组件版本建议备注Python3.8 ~ 3.103.11 也能配 opencv-python 4.8但沿用旧项目时容易出妖opencv-python4.x3.4.3 起步DNN 模块内置不需要 opencv-contribnumpy随 opencv 自动安装如报错可单独python3 -m pip install numpy模型只需要两个文件pose_deploy_linevec.prototxt是网络结构描述pose_iter_440000.caffemodel是预训练权重大小在 200MB 量级。prototxt 在 OpenCV 官方 extra 仓库的 samples/dnn 目录里有现成副本权重在 OpenPose 官方 release 页面能找到。关键点是两者必须同源同一个 COCO 18 点训练产物的结构和权重才配得上混用 MPI 15 点或 BODY_25 的 prototxt 会导致网络形状直接对不上readNetFromCaffe()一执行就抛异常。2.3 用一条加载命令验证模型到了手先把模型加载和环境连通性验证掉再往上叠逻辑这是最小验证原则。import cv2 PROTO ./pose_deploy_linevec.prototxt WEIGHT ./pose_iter_440000.caffemodel net cv2.dnn.readNetFromCaffe(PROTO, WEIGHT) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) print(model loaded, layers:, len(net.getLayerNames()))这段代码的逻辑只有三步读结构、读权重、指定后端。readNetFromCaffe在读文件时就会做一次拓扑检查所以只要它能正常返回对象就说明结构与权重匹配网络里的全部层都能被当前 OpenCV 版本解释。setPreferableBackend指定用 OpenCV 自己的推理后端setPreferableTarget指定 CPU这样能排除显卡驱动、CUDA 版本这些外部变量。打印层数只是给你一个「模型确实进来了」的确认信号不用背具体数值。常见报错有两类文件路径找不到那是路径写错shape mismatch 或 unknown layer那是 prototxt 与 caffemodel 不配套。换一套同源文件即可其他都不用动。3. 摄像头和视频流里跑通姿态推理从取帧到骨骼可视化的完整链路3.1 读摄像头与读视频文件的标准姿势OpenCV 对摄像头和视频文件的接入是同一套接口VideoCapture的参数传设备编号就是摄像头传文件路径就是视频。真正容易翻车的地方在于不同操作系统的摄像头后端以及懒于检查摄像头是否真的打开。import cv2 src 0 # 0 表示默认摄像头换成 demo.mp4 就是视频文件 cap cv2.VideoCapture(src, cv2.CAP_DSHOW if src 0 else 0) if not cap.isOpened(): raise IOError(摄像头打不开检查设备占用和系统权限) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ok, frame cap.read() if not ok: print(取帧失败可能到了视频结尾或摄像头被拔出) break # 推理与绘制逻辑写在这里 cv2.imshow(pose, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()CAP_DSHOW是 Windows 下指定 DirectShow 后端很多笔记本摄像头在默认后端下会黑屏加了这个参数通常能解决。读视频文件时直接传路径不需要指定后端。cap.set把采集分辨率压到 640x480这一步很关键OpenPose 本身输入是 368x368采集 1080p 再缩放CPU 浪费在中间的缩放和格式转换上一帧要多吃几十毫秒。cap.read()返回两个值ok是布尔标志frame是图像。真实项目里必须处理okFalse的分支视频末尾会走到这里摄像头被其他程序占用时也可能走到这里。没有这个检查程序通常会在后续frame为 None 时以一条看不懂的底层报错退出排错成本反而更高。3.2 blob 预处理与 forward 输出heatmap 不是坐标OpenPose 的 Caffe 模型输入是一个固定尺寸的 blob输出是一堆 heatmap。heatmap 的每个像素表示「人体某个关键点出现在这个位置的概率」它不等于关键点坐标需要额外一步把峰值位置映射回原图。in_height, in_width 368, 368 blob cv2.dnn.blobFromImage( frame, 1.0 / 255.0, (in_width, in_height), (127.5, 127.5, 127.5), swapRBFalse, cropFalse ) net.setInput(blob) out net.forward() # 形状: (1, 57, 46, 46)blobFromImage做了三件事缩放、减均值、按比例放缩像素值。这里的 scale1/255 和 mean127.5 必须跟训练时的预处理一致改动任何一项都会让置信度明显下降甚至完全识别不出来。swapRBFalse也很关键因为 OpenCV 读进来是 BGR 顺序而 Caffe 训练这套模型用的也是 BGR如果你是从 TensorFlow 或 ONNX 生态转过来的习惯性写成 True 反而会让颜色通道错位。out的形状是 (batch, channels, height, width)。57 层通道的构成是18 层关键点 heatmap、19 对肢体的 PAFPart Affinity Fields各占 2 个通道、再加 1 层背景。我们这里只关心前 18 层。heatmap 的宽高是 46x46等于输入 368 的八分之一因为模型的骨干网络做了 8 倍降采样所以取到峰值后要按比例放大回原始分辨率。height, width frame.shape[:2] threshold 0.4 points [] for i in range(18): prob_map out[0, i] _, conf, _, point cv2.minMaxLoc(prob_map) x int(point[0] * width / prob_map.shape[1]) y int(point[1] * height / prob_map.shape[0]) points.append((x, y) if conf threshold else None)cv2.minMaxLoc直接在 46x46 的 heatmap 上找到最大值及其坐标比先转 numpy 再argmax少一次复制。坐标映射的公式是把 heatmap 坐标除以 heatmap 边长再乘原图边长得到的是概率最大点在原图中的位置。置信度低于threshold的点直接存成 None这些点后续不会参与连线。threshold0.4是兼顾漏检和错检的经验起点。光线暗、人物小、肢体被遮挡时膝盖和脚踝的置信度经常掉到 0.2 附近这时调低到 0.2~0.3 能找回不少关键点画面干净、人物占画面比例大时上调到 0.5 效果更干净。记住一个原则宁可缺一个点也不要一个错点。错点会把整条骨骼的形态语义带偏。3.3 画骨骼编号表、连线表与置信度门控拿到 18 个点的像素坐标后需要一套编号语义把它们画成人形。COCO 18 点模型的编号顺序是固定的比如 0 是鼻子、1 是脖子、2 是右肩、3 是右肘、4 是右腕。连线关系也有固定的配对表两个端点任一置信度不足就不画这条线避免画出一半肢体。BODY_PARTS { 0: Nose, 1: Neck, 2: RShoulder, 3: RElbow, 4: RWrist, 5: LShoulder, 6: LElbow, 7: LWrist, 8: MidHip, 9: RHip, 10: RKnee, 11: RAnkle, 12: LHip, 13: LKnee, 14: LAnkle, 15: REye, 16: LEye, 17: REar } POSE_PAIRS [ [1, 2], [1, 5], [2, 3], [3, 4], [5, 6], [6, 7], [1, 8], [8, 9], [9, 10], [10, 11], [8, 12], [12, 13], [13, 14], [0, 1], [0, 14], [14, 16], [0, 15], [15, 17] ] for pair in POSE_PAIRS: p1, p2 points[pair[0]], points[pair[1]] if p1 is None or p2 is None: continue cv2.line(frame, p1, p2, (0, 255, 0), 2) for i, p in enumerate(points): if p is not None: cv2.putText(frame, str(i), p, cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.circle(frame, p, 3, (0, 0, 255), -1)POSE_PAIRS里每一对表示一条肢干例如 [8, 12] 是盆骨中点到左髋[10, 11] 是右膝到右踝。绘制时两个端点必须都存在否则跳过这是防止画出悬空线的关键。给每个关键点标上编号是个好习惯调试阶段在画面上看到编号能立刻发现「右肘怎么跑到左边去了」到底是识别问题还是编号表问题。有一点必须提前说清楚OpenCV DNN 这条链路只取了 18 层 heatmap 的峰值PAF 那 38 个通道被忽略掉了。原版 OpenPose 用 PAF 做全局二分图匹配把散点连接成最合理的骨架而我们这里用预设的POSE_PAIRS直接连线。单人、肢体无遮挡的场景下两者差别不大但多人交叉、人物重叠时OpenCV 方式会出现错连这是已知边界不是代码 bug。4. 从关键点到人体形态角度阈值与蹲起、举手判定怎么做才靠谱4.1 关键点坐标先换算成关节角度网上流传的免费 python 源码里姿态识别 demo 一大把但大多到画骨骼就停了。业务真正需要的是把骨骼转换成形态语义——蹲没蹲下去、手有没有举起来、有没有跌倒。第一件事就是把像素坐标换算成关节角度。import math def angle_between(a, b, c): 计算以 b 为顶点的夹角度a、b、c 为 (x, y) 坐标 ba [a[0] - b[0], a[1] - b[1]] bc [c[0] - b[0], c[1] - b[1]] denominator math.hypot(ba[0], ba[1]) * math.hypot(bc[0], bc[1]) cos_a (ba[0] * bc[0] ba[1] * bc[1]) / (denominator 1e-6) cos_a max(-1.0, min(1.0, cos_a)) return math.degrees(math.acos(cos_a))实现用的是向量点积与反余弦两个向量长度乘积做分母点积做分子得到夹角的余弦值再通过acos还原成角度。代码里两个细节是给实际项目用的分母加1e-6防止两个关键点重合时除零cos_a裁剪到 [-1, 1] 防止浮点误差导致acos返回 NaN。这两个问题在真实数据里一定会出现不加就是莫名其妙的全屏 NaN。以深蹲为例右腿形态看三个点右髋 9、右膝 10、右踝 11。angle_between(points[9], points[10], points[11])得到膝关节角度。为什么不用关键点的 y 坐标直接判断因为人的身高、站位远近、摄像头焦距都会让同一姿态的像素坐标差出几百个值角度对尺度近似不变。同一个 160 度的膝关节角站在 1 米和 3 米外y 坐标差一倍角度几乎一样。这就是形态识别里角度比坐标好用的根本原因。4.2 蹲起判定的阈值表与标定方法有了角度计算函数后接下来的问题是阈值定多少。我一般会用一组经验值起步然后在自己的摄像头画面里重新标定动作状态膝关节角度范围形态描述立姿160 ~ 180 度大腿与小腿基本伸直半蹲100 ~ 160 度微屈膝重心开始下降深蹲小于 90 度大腿接近或低于水平这组值对应的是摄像头摆在胸口高度、人侧身对着镜头的场景。摄像头改成俯拍或仰拍角度会系统性偏移阈值必须重标。标定方法很简单让人分别做标准立姿和标准深蹲打印实时角度取两次读数的中位值作为高阈值和低阈值。这个动作花五分钟省掉后面一星期的阈值试错。再举一个抬手判定的例子。检测右手举起最稳妥的不是看右手腕的 y 坐标是否高于肩膀——因为角度标定更抗干扰。可以用右肩 2、右肘 3、右腕 4 计算肘关节角度手伸直举起时这个角度接近 180 度手自然下垂时约 160 度以下也可以结合「手腕 y 小于肩膀 y 且两个关键点置信度都过阈值」做二次确认。两种条件同时满足才判定举手误报会少很多。4.3 从单帧判定到连续行为描述单帧判定是每一帧独立执行的但业务逻辑很少需要「这一帧是什么」更多需要「这一秒在做什么」。滑动窗口多数投票是最简单有效的中间层把最近 N 帧的分类结果放进队列最终状态取出现次数最多的那个。from collections import deque history deque(maxlen10) def classify_frame(points): # 简化的分类逻辑深蹲返回 squat站立返回 stand knee angle_between(points[9], points[10], points[11]) if knee 90: return squat if knee 160: return stand return transition # 每帧调用 history.append(classify_frame(points)) final_state max(set(history), keyhistory.count)deque(maxlen10)会自动丢弃最旧的记录等价于一个长度 10 的滑动窗口。max(set(history), keyhistory.count)是经典写法原理是取出去重后的每个状态统计其出现次数返回次数最多的那一个。窗口长度对应时间尺度假设推理 10 fps10 帧就是 1 秒适合蹲起、举手这类节奏不快的行为如果识别快速动作窗口缩小到 5 帧。滑动窗口能抹掉偶发的单帧误判但代价是引入延迟。临界状态切换时总是要攒够多数票才会翻转这个延迟在慢动作场景无所谓在需要实时响应的场景就要把窗口调小或者改用下一章的状态机方案。5. OpenPose 落地避坑环境、延迟、模型编号与关键点乱跳的排错记录5.1 ModuleNotFoundError: No module named cv2十次有八次是装错了环境现象命令行里pip install opencv-python明明显示安装成功一执行代码就报ModuleNotFoundError: No module named cv2。原因终端里的pip和python指向的不是同一个解释器。最常见的是 Ubuntu 系统自带 Python 2/3 并存pip装到了 Python 2 的 site-packages而代码用 Python 3 跑另一个常见场景是用户装了两个 Python 3PyCharm 里选的解释器和命令行里用的是两个。这类问题跟 opencv 本身无关纯属环境变量配置没对齐。解决强制使用同一解释器的包管理命令不要用裸pip。我的习惯是这样python3 -m pip install opencv-python numpy python3 -c import cv2; print(cv2.__version__)python3 -m pip install会把包装到当前python3解释器对应的环境里python3 -c import cv2; print(cv2.__version__)验证下一步。看到版本号跟 OpenCV 官网最新版本吻合才算环境打通。在 Windows 上同理用python -m pip而非直接pip条件是安装 Python 时勾选了 Add to PATH。5.2 摄像头打不开或取帧黑屏检查后端、权限和分辨率三个位置现象cap.isOpened()返回 True但cap.read()一直返回 False或者首帧黑屏在 Ubuntu 上还常见权限错误/dev/video0无法访问。原因Windows 上默认后端对部分笔记本摄像头兼容性差明明设备在工作后端却拿不到画面Ubuntu 上则是当前用户不在video组没有访问摄像头设备的权限还有一个隐蔽原因是前面程序没释放摄像头资源被占用。解决Windows 上创建VideoCapture时显式传cv2.CAP_DSHOWUbuntu 上执行sudo usermod -aG video $USER后注销重登同时把采集分辨率调低到 640x480。另外建议在正式循环前用 5 帧cap.grab()做热身摄像头驱动在刚打开的一瞬间经常给不出完整帧。这些都是我在 ubuntu 配置 opencv 的过程里踩过的坑花的时间比模型本身多得多。5.3 画出来的骨骼张冠李戴编号表、连线表和镜像方向三个坑叠在一起现象骨骼能画出来但左胳膊连到右肩膀或者动作明明是右手举起代码里却是左手编号的点在动。原因主要有三个。第一用了错误的编号表——MPI 15 点、COCO 18 点、BODY_25 三种模型的关键点编号顺序完全不同MPI 的 2 号点是右肘COCO 的 2 号点却是右肩混用直接错位。第二连线表POSE_PAIRS引用的编号和当前模型不匹配。第三摄像头拍到的画面是镜像——人的右手显示在画面左侧如果你按「画面左侧就是人的左手」来写业务逻辑就会全面反转。解决先用上一章的调试代码把关键点编号打印在画面上人做一个明确的动作比如挥右手确认编号确实对应身体的正确部位再写形态判定。镜像问题可以这样处理frame cv2.flip(frame, 1)把画面翻成「照镜子」模式此时画面左侧对应人的左手与BODY_PARTS里的 L 系列对齐。三个坑一次排干净后面就不会再怀疑人生。5.4 关键点在画面上乱跳单帧推理的抖动怎么压现象人站在原地不动手腕和脚踝的关键点却在小范围来回跳动形态判定在站立和半蹲之间反复横跳计数一次动作被算了两三次。原因每个关键点是模型对单帧独立预测的相邻帧之间存在随机波动。置信度越低的关键点空间抖动越明显脚踝和手腕这类小关节尤其严重。这是模型输出的固有噪声不是代码 bug想靠提高阈值彻底消除不现实。解决分三层压抖动。第一层是置信度过滤已经在关键点提取时做了。第二层是角度平滑用一阶低通滤波处理角度序列下一章细讲。第三层是给状态判定加滞回区间进入深蹲需要低于低阈值离开深蹲需要高于高阈值中间留缓冲带。这三层叠加之后静态场景下的抖动基本不会再影响业务判定。6. 把骨架语义做成可靠计数器滞回状态机与一阶低通滤波6.1 单阈值为什么会把一次深蹲数成三次假设深蹲判定用单个 150 度阈值角度在 149 和 151 之间抖动时状态就会不断翻转每翻一次计一次数一次运动被数成多次。解决思路是双阈值滞回进入动作的阈值和退出动作的阈值分开中间留缓冲带抖动落在缓冲带内不会触发状态翻转。angle_smooth 0.3 * angle_now 0.7 * angle_smooth STATE_UP, STATE_DOWN up, down def squat_fsm(angle, state, counter, low130, high160): if state STATE_DOWN and angle high: counter 1 state STATE_UP elif state STATE_UP and angle low: state STATE_DOWN return state, counter第一段是一阶低通滤波的实际用法angle_now是当前帧的角度angle_smooth是平滑后的值每帧更新一次。系数 0.3 表示新值占三成权重适合 10~15 fps 的推理节奏帧率翻倍时把系数调到 0.15 左右保持同样的时间常数。第二段是滞回状态机。膝关节角度低于 130 度才进入down状态高于 160 度才算完成一次深蹲并回到up。131 到 159 之间的任何抖动都不会导致状态翻转因为状态切换只看两个边界。与滑动窗口多数投票相比这个方案几乎零延迟跨过边界的瞬间就能反应。6.2 把技巧收进工程这套「平滑 滞回 状态机」的组合不只适用于深蹲举手、跌倒、引体向上都能用同一个框架先定义关节角度再定进入与退出阈值最后放进状态机。我做第一版计数器时直接拿瞬时角度和单阈值卡测试视频里一个深蹲被数出三次。换成滞回加一阶滤波后连续录了三段不同光线和角度下的素材计数才终于贴合实际动作节奏。这个项目做完我最深的体会是OpenPose 负责把关键点找出来但它不会替你把形态语义搞干净后半场全靠约束和状态。希望帮到你。本文还有配套的精品资源点击获取