树莓派摄像头+WebRTC+PoseNet:浏览器端实时姿态识别落地实践
简介:这是一套面向物联网与人工智能方向开发者、创客及高校学生的实战项目源码,围绕树莓派摄像头构建实时人体姿态识别智能交互系统,解决浏览器端低延迟视频传输与姿态识别的落地问题。资源包共29个文件,约192KB,以14个js脚本、4个html页面、2个css样式及2个conf配置为主,辅以说明文档、图标与图片素材,覆盖前端交互、服务配置与页面展示等模块。系统通过rpi-webrtc-streamer实现WebRTC低延迟视频流传输,并借助TensorFlow.js的PoseNet模型在浏览器端完成实时姿态识别,可应用于智能健身、体感游戏、远程教育等场景。目前已有112人学习下载。读者可获得完整可运行的项目结构、前后端页面与配置示例,以及说明文件与附赠资源,便于快速理解系统原理、复现实验并在此基础上二次开发。
1. 树莓派摄像头 + WebRTC + PoseNet:浏览器端实时姿态识别到底怎么落地
很多人第一次听到「树莓派摄像头实时人体姿态识别」这个组合,第一反应是:树莓派那点算力,跑得动姿态识别吗?我一开始也这么想,直到把推理这件事从树莓派挪到浏览器里,才发现整条链路一下子顺了。树莓派只负责一件事——把摄像头画面以足够低的延迟推出去;真正吃算力的 PoseNet 推理,交给打开网页的那台电脑或手机。这就是 rpi-webrtc-streamer 加 TensorFlow.js PoseNet 这套方案的核心思路:边缘采集、浏览器推理、WebRTC 传输。
它解决的是「我想用树莓派做一个能实时看懂人体动作的交互系统,但不想在树莓派上装一堆深度学习环境、也不想忍受 RTSP 转码的高延迟」这个具体问题。适合做体感交互、健身动作计数、手势控制、展厅互动装置的开发者。整条链路里,WebRTC 负责把延迟压到几百毫秒以内,PoseNet 负责在浏览器里把画面变成 17 个关键点坐标,你拿到的就是可以驱动业务逻辑的结构化数据。下面按「先跑通、再调优、最后避坑」的顺序拆开讲。
2. 为什么把推理放到浏览器:链路选型与延迟账
2.1 三种常见架构的延迟与算力对比
在动手之前,先把架构选对,否则后面全是返工。围绕「树莓派 + 摄像头 + 姿态识别」,业内常见三种做法,我按实测体感列个对比。
| 架构 | 推理位置 | 端到端延迟 | 树莓派负载 | 适用场景 |
|---|---|---|---|---|
| 树莓派本地推理 | 树莓派 CPU/GPU | 300ms~1s+ | 极高,常掉帧 | 离线、无网络 |
| 服务端推理 | 服务器 GPU | 依赖网络,200ms~1s | 低 | 多路集中处理 |
| 浏览器端推理 | 客户端浏览器 | 150ms~400ms | 低(仅推流) | 单路交互、体感 |
树莓派 4B 用 CPU 跑轻量姿态模型,单帧推理经常要几百毫秒,帧率掉到个位数,交互体验直接崩。而把推理放到浏览器,树莓派只做 H.264 编码和 WebRTC 推流,负载一下就下来了。代价是每个客户端要自己算,但交互系统通常就一两个人用,这个代价可以接受。
2.2 rpi-webrtc-streamer 在链路里干了什么
rpi-webrtc-streamer 是一个跑在树莓派上的 WebRTC 推流服务,它把摄像头采集、H.264 硬件编码、WebRTC 信令和传输打包在一起。你启动它之后,它会暴露一个网页,浏览器打开就能看到实时画面,底层走的是 WebRTC 的 SRTP 传输,而不是 RTSP 那种需要插件或转码的老路子。
它相比「RTSP + ffmpeg 转 WebRTC」的方案,省掉了中间转码环节,延迟能低一大截。常见做法是:树莓派上跑 streamer,浏览器通过它的信令页面建立 PeerConnection,拿到 MediaStream 之后,把 video 元素喂给 TensorFlow.js 的 PoseNet。整条链路里,视频流是「活」的,PoseNet 是「贴」在 video 上做逐帧推理的。
2.3 从摄像头到关键点:数据是怎么流动的
把链路拆细一点,方便你后面排查问题:
- 树莓派摄像头(CSI 接口的 ov5647 或 USB 摄像头)通过 V4L2 采集 YUV 帧;
- rpi-webrtc-streamer 调用树莓派硬件编码器把帧编成 H.264;
- WebRTC 通过 SRTP 把码流推到浏览器;
- 浏览器解码后渲染到
<video>; - TensorFlow.js PoseNet 从 video 抓帧,输出 17 个关键点的 (x, y, score);
- 你的业务代码消费这些坐标,做动作判断或交互。
这条链路里,任何一环卡住,表现都是「画面卡」或「关键点飘」。所以后面调优和排错,都要按这个顺序一段段看。
3. 把树莓派推流跑起来:streamer 编译与启动
3.1 系统准备与依赖安装
先确认系统。树莓派 4B 上跑 Raspberry Pi OS(32 位或 64 位都行),或者 Ubuntu 22.04。我一般用 Raspberry Pi OS,因为摄像头和硬件编码的驱动最省心。先把源和基础依赖弄好。
# 更新系统与安装编译依赖
sudo apt update && sudo apt upgrade -y
sudo apt install -y git cmake build-essential libssl-dev \
libavcodec-dev libavformat-dev libavutil-dev \
libv4l-dev libasound2-dev
这里装的是编译 rpi-webrtc-streamer 需要的基础库: libssl-dev 用于 WebRTC 的 DTLS, libav* 是 ffmpeg 的编解码库, libv4l-dev 是摄像头采集接口。少一个都会在 cmake 阶段报错,所以别偷懒。
3.2 编译 rpi-webrtc-streamer
从仓库拉代码后,按它的构建脚本走。不同版本构建方式略有差异,常见做法是:
git clone https://github.com/your-mirror/rpi-webrtc-streamer.git
cd rpi-webrtc-streamer
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j4
-DCMAKE_BUILD_TYPE=Release 一定要加,Debug 版本在树莓派上跑会明显掉帧。 -j4 用满四核编译,树莓派 4B 编译这套东西大概要十几到几十分钟,取决于网络和 SD 卡速度。编译产物里会有可执行文件和它自带的网页资源。
提示:如果 cmake 报找不到某个库,先确认
pkg-config --list-all | grep 库名能不能找到,找不到就回去补装对应的-dev包。
3.3 启动参数与摄像头选择
启动时最关键的是指定摄像头设备和分辨率。树莓派 CSI 摄像头在系统里通常是 /dev/video0 ,USB 摄像头可能是 /dev/video1 或更高。
# 启动 streamer,指定摄像头、分辨率与端口
./rpi-webrtc-streamer \
--video-device /dev/video0 \
--width 640 --height 480 \
--fps 30 \
--port 8000
参数说明: --width/--height 建议先用 640x480,PoseNet 对分辨率不敏感,但推流分辨率越高,树莓派编码压力越大; --fps 30 是目标帧率,实际能不能到要看编码能力; --port 8000 是网页和信令端口。启动后浏览器访问 http://树莓派IP:8000 ,能看到画面就说明推流这半条链路通了。
如果画面出不来,先看 v4l2-ctl --list-devices 确认设备名,再用 v4l2-ctl --device=/dev/video0 --list-formats 看支持的像素格式。ov5647 这类 CSI 摄像头和某些 USB 摄像头对格式支持不一样,格式不对会直接黑屏。
4. 浏览器端接 PoseNet:从 video 到 17 个关键点
4.1 引入 TensorFlow.js 与 PoseNet
浏览器端不需要构建工具也能跑,直接引 CDN 最省事。新建一个 HTML,把 video 和 canvas 放好。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PoseNet 实时姿态</title>
<!-- TensorFlow.js 与 PoseNet 模型 -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.x"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/posenet@2.x"></script>
</head>
<body>
<video id="cam" autoplay playsinline width="640" height="480"></video>
<canvas id="overlay" width="640" height="480"></canvas>
<script src="app.js"></script>
</body>
</html>
autoplay 和 playsinline 两个属性都要加,否则在移动端浏览器上视频不会自动播放。canvas 叠在 video 上,用来画骨架。
4.2 建立 WebRTC 连接并绑定 video
streamer 自带信令页面,但如果你要自己控制,可以用它的信令接口建立连接。最省事的做法是直接复用它的播放页面,把 video 元素拿到手。下面这段是常见的「拿到 MediaStream 后绑定到 video」逻辑:
// 假设已通过信令拿到 remoteStream
const video = document.getElementById('cam');
async function attachStream(remoteStream) {
video.srcObject = remoteStream;
await video.play();
// 等元数据加载完再开始推理,避免拿到空帧
video.onloadedmetadata = () => {
console.log('视频尺寸:', video.videoWidth, video.videoHeight);
startPoseNet();
};
}
这里的关键是等 loadedmetadata 再启动推理。如果 video 还没准备好就抓帧,PoseNet 会拿到空画面,表现为关键点全在 (0,0) 或者直接报错。这是新手最容易踩的坑之一。
4.3 PoseNet 推理循环与关键点绘制
PoseNet 加载后,用 requestAnimationFrame 做循环推理。注意 PoseNet 单帧推理在普通笔记本上大概 30~60ms,所以不要盲目每帧都推,可以按需降频。
let net;
let rafId;
async function startPoseNet() {
// 加载模型,ResNet50 精度高但慢,MobileNetV1 快
net = await posenet.load({
architecture: 'MobileNetV1',
outputStride: 16,
inputResolution: { width: 640, height: 480 },
multiplier: 0.75
});
detectLoop();
}
async function detectLoop() {
const video = document.getElementById('cam');
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
// 单姿态检测,速度快;多人用 estimateMultiplePoses
const pose = await net.estimateSinglePose(video, {
flipHorizontal: false
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawKeypoints(pose.keypoints, 0.5, ctx);
drawSkeleton(pose.keypoints, 0.5, ctx);
rafId = requestAnimationFrame(detectLoop);
}
参数说明: architecture 选 MobileNetV1 是为了速度, ResNet50 精度更高但在树莓派推流场景下没必要; outputStride 16 是速度和精度的平衡点,8 更精细但更慢; multiplier 0.75 是模型宽度缩放,越小越快越糙; estimateSinglePose 的第二个参数里 flipHorizontal 在自拍镜像场景要设 true。 drawKeypoints 和 drawSkeleton 是官方示例里的工具函数,直接抄过来即可。
关键点阈值 0.5 是置信度门槛,低于它的点不画。实际调试时如果发现某些点老是闪,可以适当降到 0.3,但会引入更多噪声。
5. 避坑与排查:延迟、掉帧、关键点飘的五个血泪经验
5.1 现象:画面延迟越跑越大,最后卡死
原因:WebRTC 的拥塞控制在高丢包 Wi-Fi 下会不断降码率,同时浏览器端推理占满主线程,导致 video 解码和渲染被饿死,缓冲区越积越多。
解决:把推理放到 Web Worker 里,别占主线程;同时把推流分辨率降到 640x480、码率上限设低一点。树莓派和浏览器尽量走 5GHz Wi-Fi 或有线,2.4GHz 在多人环境基本没法用。
5.2 现象:PoseNet 关键点整体偏移或镜像
原因:video 的显示尺寸和 canvas 尺寸不一致,或者摄像头画面本身是镜像的。
解决:canvas 的 width/height 必须和 video 的 videoWidth/videoHeight 一致,绘制时按比例缩放坐标。镜像问题在 estimateSinglePose 里用 flipHorizontal 修正,别在 canvas 上做 CSS 翻转,那样坐标对不上。
5.3 现象:树莓派 CPU 跑满,推流帧率掉到 10 以下
原因:H.264 编码没走硬件,或者分辨率/帧率设太高。
解决:确认 streamer 用的是树莓派的硬件编码( v4l2m2m 或 omx ),别用软件 x264。分辨率从 1080p 降到 720p 甚至 480p,帧率 30 降到 24,体感交互完全够用。用 top 看是哪个进程吃 CPU,如果是 streamer 本身,就是编码问题。
5.4 现象:关键点抖动严重,动作判断误触发
原因:PoseNet 逐帧独立推理,没有时序平滑,单帧噪声直接进业务逻辑。
解决:加一层滑动平均或一欧元滤波(One Euro Filter),对每个关键点的 x/y 做平滑。简单做法是维护最近 5 帧的坐标队列取中位数,成本低效果好。业务判断再加一个「连续 N 帧满足条件才触发」的防抖。
5.5 现象:换一台电脑打开网页,模型加载失败或极慢
原因:TensorFlow.js 和模型文件走 CDN,网络环境差时加载超时。
解决:把 tfjs 和 posenet 的 js 文件、模型权重下载到本地,用本地静态服务托管。模型文件不大,本地化之后加载稳定,也方便离线部署。这一步在展厅、比赛现场这种网络不可控的环境里几乎是必须的。
6. 进阶:把关键点变成交互信号,以及怎么验证它真的可用
跑通链路只是开始,真正决定这套系统好不好用的是「关键点怎么变成交互信号」。我一般会先定义一个动作判定层,把 17 个关键点映射成几个业务指标,比如手臂抬起角度、身体重心偏移、下蹲深度。以「举手」为例,判断逻辑是手腕关键点的 y 坐标持续高于肩膀关键点,并且置信度都过阈值。
// 判断是否举手:手腕在肩膀上方且置信度足够
function isHandRaised(pose, minScore = 0.5) {
const kp = pose.keypoints;
const lWrist = kp.find(p => p.part === 'leftWrist');
const lShoulder = kp.find(p => p.part === 'leftShoulder');
const rWrist = kp.find(p => p.part === 'rightWrist');
const rShoulder = kp.find(p => p.part === 'rightShoulder');
const leftUp = lWrist.score > minScore && lShoulder.score > minScore &&
lWrist.position.y < lShoulder.position.y;
const rightUp = rWrist.score > minScore && rShoulder.score > minScore &&
rWrist.position.y < rShoulder.position.y;
return leftUp || rightUp;
}
注意 PoseNet 的坐标系原点在左上角,y 越小越靠上,所以「手腕 y 小于肩膀 y」才是举起。这个方向搞反是高频错误。判定层之上再加状态机,处理「举起→保持→放下」的时序,避免单帧抖动导致重复触发。
验证这套系统是否真的可用,我习惯看三个指标:端到端延迟、关键点稳定度、动作识别准确率。延迟用「挥手到画面响应」的体感加浏览器 Performance 面板测;稳定度看同一姿势下关键点坐标的方差;准确率就录一段标准动作,统计误触发和漏触发次数。这三个指标里,延迟超过 500ms 体感交互就会明显「粘」,稳定度方差大就说明要加滤波,准确率低先查阈值和坐标系。
我踩过最深的一个坑,是早期把推理和渲染都塞在主线程,画面一卡就以为是网络问题,查了半天 WebRTC 参数,最后发现是主线程被 PoseNet 占满。后来养成习惯:只要画面卡,先开浏览器 Performance 面板看主线程占用,再看网络。这个顺序帮我省了无数时间。希望帮到你。
更多推荐
所有评论(0)