简介:本资源是一个面向嵌入式AI与Web前端开发者的学习型项目,聚焦于低成本、低延迟的实时人体姿态识别智能交互系统构建。它解决了树莓派端视频采集、WebRTC流传输与浏览器端轻量级AI推理协同难的问题,适用于智能健身指导、远程教育互动、体感游戏开发等场景。压缩包共29个文件(192KB),含14个核心JavaScript逻辑文件(实现PoseNet加载与关键点渲染)、4个HTML页面(含rws.html流接入页与posenet.html识别页)、2个配置文件(media_config.conf与nginx.conf)、2个CSS样式文件及README.md、说明文件.txt、附赠资源.docx等辅助文档,结构清晰、开箱即用。目前已有110人学习下载,读者可直接部署运行,获得从树莓派摄像头采集→rpi-webrtc-streamer推流→TensorFlow.js PoseNet浏览器端实时姿态估计的完整链路代码、配置模板与实操指引,特别适合具备基础JavaScript和Linux命令能力的中级开发者快速复现并二次拓展。

1. 项目缘起:从“玩具”到“智能交互”的蜕变

几年前,当我第一次把树莓派和摄像头模块接在一起,看着屏幕上实时显示的画面时,那种感觉就像小时候拆开一个新玩具。但很快,这种简单的“视频监控”玩法就让我觉得索然无味。我一直在想,能不能让这块小小的板子“看懂”画面里的人在做什么?比如,识别出一个人是在挥手、举手还是跳舞,然后触发一些有趣的交互。这就是我启动这个项目的初衷: 让树莓派摄像头不再只是“眼睛”,更要成为能理解人体动作的“大脑” 。

这个想法听起来很酷,但实现起来却是一连串的挑战。最核心的问题有两个: 延迟 和 算力 。如果我想在浏览器里实时看到姿态识别的效果,视频流从摄像头采集、传输到浏览器,再经过AI模型分析、渲染出骨骼点,整个过程必须在几十毫秒内完成,否则交互体验会非常糟糕。同时,树莓派的CPU性能有限,如果让它在本地运行一个复杂的姿态识别模型,帧率会惨不忍睹。

经过一番折腾和试错,我最终确定了现在的技术栈: 树莓派 + OV5647摄像头负责采集,rpi-webrtc-streamer实现超低延迟的视频流推送,最后在用户的浏览器端,用TensorFlow.js的PoseNet模型完成实时姿态识别 。这个架构的精妙之处在于“各司其职”:树莓派只做它最擅长的视频采集和高效编码传输,将最消耗算力的AI推理任务“卸载”到了拥有强大GPU的客户端浏览器上。这样一来,树莓派的负载极低,而用户端也能获得流畅的实时体验。

整个系统非常适合那些想探索边缘AI、实时交互的开发者,或者任何想给树莓派项目增加一点“智能”的朋友。无论是用来做体感游戏控制器、智能健身教练原型,还是简单的安防人形检测,这个框架都能提供一个坚实、低延迟的起点。

2. 硬件选型与系统搭建:为实时流打好地基

工欲善其事,必先利其器。硬件和系统环境的准备是项目成功的第一步,这里面的门道不少,选错了或者配不好,后面会麻烦不断。

2.1 树莓派与摄像头的选择:为什么是OV5647?

首先说树莓派。 树莓派4B 4GB版本 是这个项目的黄金选择。它的CPU和内存足够流畅运行我们所需的流媒体服务,并且价格适中。树莓派5性能更强,但功耗和发热也更大,对于这个项目来说有些性能过剩。树莓派Zero 2虽然便宜小巧,但其孱弱的算力在处理视频编码时可能会成为瓶颈,不推荐。

摄像头的选择至关重要。标题和相关热词里提到了“树莓派ov5647摄像头模块”,这并非偶然。树莓派官方摄像头模块主要有两种: CSI接口的OV5647(500万像素)和IMX219(800万像素) 。我强烈推荐使用OV5647,原因有三:

  1. 驱动与兼容性 :OV5647是树莓派上历史最久、支持最完善的摄像头模块之一,几乎所有基于Linux V4L2(Video for Linux 2)框架的软件都能即插即用,减少了驱动层面的折腾。
  2. 性能与带宽平衡 :500万像素对于实时视频流绰绰有余。我们通常只需要传输720p(1280x720)甚至480p(640x480)的视频,OV5647在这些分辨率下能轻松跑到30fps以上。更高的像素意味着更大的数据量,会给树莓派的CPU编码和网络传输带来不必要的压力。
  3. 成本 :OV5647模块通常比IMX219更便宜。

注意 :购买时请认准CSI排线接口。市面上有些USB摄像头虽然也能用,但USB总线传输和编码的延迟通常高于树莓派专用的CSI总线,不利于实现最低延迟。

2.2 系统刷机与基础配置

拿到硬件后,第一步是给树莓派安装操作系统。

  1. 系统选择 :使用 Raspberry Pi OS(原Raspbian)Lite版本(64位) 。Lite版本是无桌面环境的纯命令行系统,资源占用极少,能把所有算力都留给我们的服务。通过官方工具“Raspberry Pi Imager”刷写镜像到MicroSD卡是最稳妥的方式。
  2. 基础配置 :首次启动前,可以在刷卡完成后,在boot分区创建一个名为 ssh 的空文件(开启SSH服务)和一个包含 country=CN 等信息的 wpa_supplicant.conf 文件(配置Wi-Fi)。这样你就能通过SSH远程登录树莓派了,无需连接键鼠显示器。
  3. 系统更新与换源 :登录后第一件事是换源,国内用户使用清华或中科大的镜像源能极大提升软件下载速度。执行 sudo apt update && sudo apt upgrade -y 进行全系统更新。
  4. 摄像头接口启用 :这是关键一步。运行 sudo raspi-config ,进入 “Interface Options” -> “Legacy Camera”,选择 “Yes” 以启用旧的摄像头驱动栈。虽然新的“libcamera”是趋势,但 rpi-webrtc-streamer 目前对旧版驱动支持更成熟稳定。

2.3 网络环境考量:有线优于无线

为了追求最低延迟, 请务必使用千兆有线网络(网线)连接你的树莓派和路由器 。Wi-Fi(无论是树莓派连接Wi-Fi,还是客户端在Wi-Fi下访问)会引入不可预测的抖动和延迟,在实时视频流和交互场景中是致命的。确保你的树莓派、开发电脑/客户端都在同一个局域网内,这样可以走内网传输,速度最快。

3. 核心引擎:rpi-webrtc-streamer的部署与调优

视频流传输是整个系统的血管,它的通畅与否直接决定了体验。我们选择 rpi-webrtc-streamer ,是因为它专为树莓派优化,直接调用硬件编码器(H.264),并通过WebRTC协议实现浏览器与树莓派间的点对点低延迟通信。

3.1 编译与安装rpi-webrtc-streamer

这个项目需要从源码编译,过程稍显复杂,但一步步来并不难。

# 1. 安装必要的依赖包
sudo apt install -y git build-essential cmake pkg-config libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libv4l-dev

# 2. 克隆仓库(建议使用国内镜像或提前下载,原始仓库较大)
git clone https://github.com/kclyu/rpi-webrtc-streamer.git
cd rpi-webrtc-streamer

# 3. 创建并进入构建目录
mkdir build && cd build

# 4. 使用CMake配置,关键是指定使用树莓派的MMAL(多媒体抽象层)硬件编码
cmake -DCMAKE_BUILD_TYPE=Release -DUSE_MMAL_ENCODER=ON ..

# 5. 开始编译(树莓派4B上可能需要20-30分钟)
make -j4

# 6. 编译完成后,可执行文件`webrtc-streamer`就在当前目录下

编译成功的关键在于 -DUSE_MMAL_ENCODER=ON 这个选项。它告诉编译器使用树莓派的GPU进行H.264视频编码,这比用CPU(软件编码)效率高几十倍,能极大降低CPU占用并保证高帧率。

3.2 配置与启动流媒体服务

直接运行编译好的程序即可启动服务。但我们需要通过参数进行一些优化配置。

# 在build目录下运行
./webrtc-streamer -C config.json

这里的 -C 参数指定了一个配置文件。我们可以创建一个 config.json 来固化我们的设置:

{
  "webrtc": {
    "port": 8000,
    "ssl": false
  },
  "streams": {
    "raspicam": {
      "input": "v4l2",
      "device": "/dev/video0",
      "width": 1280,
      "height": 720,
      "framerate": 30,
      "codec": "H264",
      "bitrate": 2000000,
      "gop": 60
    }
  }
}

参数解析与调优经验 :

  • port : WebRTC信令服务器端口,浏览器通过这个端口与树莓派建立连接。
  • ssl : 局域网内测试设为 false 即可,避免证书的麻烦。如需公网访问,必须设为 true 并配置证书。
  • device : /dev/video0 是启用Legacy Camera后CSI摄像头的默认设备路径。
  • width/height : 设置为720p。这是延迟和画质的一个很好平衡点。480p(640x480)延迟更低,但画质稍差;1080p对树莓派编码和网络压力都太大,不推荐。
  • framerate : 30fps对于人体姿态识别足够流畅。
  • codec : 必须为 H264 ,这是WebRTC和浏览器硬件解码支持最好的编码。
  • bitrate : 码率,单位bps。2000000(2Mbps)对于720p30帧的H.264是足够的。如果画面复杂、动态多导致模糊,可以尝试提高到3000000。 码率越高,画质越好,但网络带宽消耗和编码延迟也会增加。
  • gop : 关键帧间隔,设为帧率的2倍(60)。这个值影响视频的“seek”能力和网络波动后的恢复速度,对于实时流,不宜设置过大。

启动服务后,你应该能在树莓派上看到类似“Server listening on http://0.0.0.0:8000”的日志。此时,在同一局域网内的电脑浏览器打开 http://<树莓派IP>:8000 ,就能看到一个简单的网页,点击“播放”按钮,应该就能看到来自树莓派摄像头的实时视频了。 延迟通常可以控制在100-200毫秒以内 ,肉眼几乎感觉不到。

3.3 常见问题排查(踩坑记录)

  1. 启动失败,提示“Failed to open V4L2 device /dev/video0” :

    • 检查摄像头是否启用 :再次确认 sudo raspi-config 中已启用Legacy Camera。
    • 检查摄像头连接 :重新插拔CSI排线,确保金色触点面向网口方向。
    • 检查设备权限 :运行 ls -l /dev/video0 ,看是否属于 video 组。可以将当前用户加入 video 组: sudo usermod -a -G video $USER ,然后 重新登录 生效。
  2. 网页能打开,但视频黑屏或无法播放 :

    • 检查防火墙 :确保树莓派的8000端口是开放的: sudo ufw allow 8000 (如果使用了UFW)。
    • 查看服务端日志 :在启动 webrtc-streamer 的终端里查看错误信息。常见的是编码器初始化失败,确保编译时打开了 USE_MMAL_ENCODER 。
    • 浏览器控制台报错 :在浏览器按F12打开开发者工具,查看“Console”和“Network”标签页是否有WebSocket连接失败或SSL相关的错误。
  3. 视频卡顿、延迟高 :

    • 首要怀疑网络 :换用有线网络!这是解决卡顿最有效的方法。
    • 降低分辨率和码率 :在 config.json 中将分辨率改为 640x480 ,码率改为 1000000 试试。
    • 检查树莓派负载 :运行 htop 命令,查看CPU占用率。如果 webrtc-streamer 进程CPU占用长期超过50%,可能是编码参数太高或后台有其他任务。

4. 智能之眼:在浏览器中集成TensorFlow.js PoseNet

视频流畅通了,接下来就是赋予它“理解”能力。我们将利用TensorFlow.js,直接在用户的浏览器里运行PoseNet模型,识别视频中的人体关键点(如鼻子、肩膀、手肘、膝盖等)。这样做的好处是服务器(树莓派)零压力,且能利用客户端(可能是高性能电脑)的GPU加速。

4.1 前端项目结构与基础HTML

我们创建一个简单的前端项目。首先是一个 index.html 文件,用于展示视频和绘制姿态。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>树莓派实时姿态识别</title>
    <style>
        body { margin: 0; padding: 20px; font-family: sans-serif; background: #f5f5f5; }
        .container { display: flex; flex-wrap: wrap; gap: 20px; max-width: 1400px; margin: auto; }
        .video-box, .canvas-box { flex: 1; min-width: 300px; border: 2px solid #333; border-radius: 8px; overflow: hidden; background: #000; }
        h2 { color: #333; text-align: center; margin-top: 0; padding: 10px; background: #eee; }
        video, canvas { display: block; width: 100%; height: auto; }
        #stats { padding: 10px; background: #fff; border-radius: 5px; margin-top: 10px; font-family: monospace; }
        .controls { margin-bottom: 15px; text-align: center; }
        button { padding: 10px 20px; margin: 0 5px; font-size: 16px; cursor: pointer; }
    </style>
</head>
<body>
    <div class="container">
        <div class="video-box">
            <h2>实时视频流</h2>
            <div class="controls">
                <button id="connectBtn">连接视频流</button>
                <button id="disconnectBtn" disabled>断开连接</button>
            </div>
            <video id="videoElement" playsinline autoplay muted></video>
            <div id="stats">状态: 等待连接...</div>
        </div>
        <div class="canvas-box">
            <h2>姿态识别结果</h2>
            <canvas id="outputCanvas"></canvas>
        </div>
    </div>

    <!-- 引入TensorFlow.js和PoseNet -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/posenet@latest"></script>
    <!-- 引入我们自己的逻辑 -->
    <script src="app.js"></script>
</body>
</html>

页面布局很简单:左侧是来自树莓派的实时视频,右侧是一个Canvas画布,用于绘制识别出的骨骼关键点和连线。我们通过CDN引入了TensorFlow.js及其PoseNet模型库。

4.2 连接WebRTC视频流

接下来是核心的 app.js 。第一部分是连接我们刚才搭建的 rpi-webrtc-streamer 服务。

// app.js
const videoElement = document.getElementById('videoElement');
const outputCanvas = document.getElementById('outputCanvas');
const ctx = outputCanvas.getContext('2d');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const statsDiv = document.getElementById('stats');

let peerConnection = null;
let stream = null;

// 替换为你的树莓派IP地址和端口
const RASPBERRY_PI_IP = '192.168.1.100';
const SIGNALING_SERVER_URL = `http://${RASPBERRY_PI_IP}:8000`;

connectBtn.onclick = async () => {
    if (peerConnection) return;
    statsDiv.textContent = '正在连接信令服务器...';
    connectBtn.disabled = true;

    try {
        // 1. 从信令服务器获取SDP Offer配置
        const response = await fetch(`${SIGNALING_SERVER_URL}/get_offer`);
        const offer = await response.json();

        // 2. 创建RTCPeerConnection
        const configuration = {
            iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // STUN服务器用于NAT穿透
        };
        peerConnection = new RTCPeerConnection(configuration);

        // 3. 当有远程流到来时,将其赋值给video元素
        peerConnection.ontrack = (event) => {
            console.log('收到远程流');
            if (videoElement.srcObject !== event.streams[0]) {
                videoElement.srcObject = event.streams[0];
                stream = event.streams[0];
                statsDiv.textContent = `已连接。视频分辨率: ${videoElement.videoWidth}x${videoElement.videoHeight}`;
                // 视频加载后,调整Canvas大小并开始姿态估计
                videoElement.onloadedmetadata = () => {
                    outputCanvas.width = videoElement.videoWidth;
                    outputCanvas.height = videoElement.videoHeight;
                    startPoseEstimation();
                };
            }
        };

        // 处理ICE候选信息(网络连接信息)
        peerConnection.onicecandidate = (event) => {
            if (event.candidate) {
                // 通常需要将candidate发送给信令服务器,但rpi-webrtc-streamer的简单示例中可能不需要
                // 这里我们使用一个简化流程
            }
        };

        // 4. 设置本地描述(SDP)
        await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);

        // 5. 将Answer发送回信令服务器以完成握手(此处为简化,实际可能需要POST)
        // 注意:rpi-webrtc-streamer的默认示例可能使用不同的信令交互。
        // 更通用的方式是使用WebSocket进行完整的信令交换。
        // 这里假设服务器端能自动处理,仅作演示。复杂场景需实现完整信令。

        disconnectBtn.disabled = false;
    } catch (error) {
        console.error('连接失败:', error);
        statsDiv.textContent = `连接失败: ${error.message}`;
        connectBtn.disabled = false;
        peerConnection = null;
    }
};

disconnectBtn.onclick = () => {
    if (peerConnection) {
        peerConnection.close();
        peerConnection = null;
    }
    if (videoElement.srcObject) {
        videoElement.srcObject.getTracks().forEach(track => track.stop());
        videoElement.srcObject = null;
    }
    stream = null;
    ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
    statsDiv.textContent = '连接已断开';
    connectBtn.disabled = false;
    disconnectBtn.disabled = true;
};

这段代码的核心是使用WebRTC的 RTCPeerConnection API与树莓派建立点对点连接。我们从一个简单的HTTP端点获取SDP Offer,然后创建Answer并设置本地描述,从而建立媒体通道。 ontrack 事件是成功的关键,当视频流到达浏览器时,它会被赋值给 <video> 标签。

重要提示 :上述信令过程是极度简化的。 rpi-webrtc-streamer 的示例可能提供了一个更完整的网页( http://<ip>:8000 ),其中包含了处理信令的JavaScript。在实际项目中,你可能需要参考其示例代码,或者自己实现一个基于WebSocket的信令服务器来处理SDP和ICE候选信息的交换。这里为了聚焦核心流程,使用了简化模型。

4.3 加载PoseNet模型与实时估计

连接视频成功后,我们就可以在 videoElement 上运行PoseNet了。

let net = null;
let isEstimating = false;

async function loadPoseNetModel() {
    statsDiv.textContent = '正在加载PoseNet模型...';
    // 加载MobileNetV1架构的模型,速度较快,精度尚可
    // 参数说明:architecture - 模型架构;inputResolution - 输入图像尺寸,越小越快;multiplier - 深度乘数,越小越快
    net = await posenet.load({
        architecture: 'MobileNetV1',
        inputResolution: { width: 640, height: 480 }, // 可以比视频分辨率小,加速推理
        multiplier: 0.75, // 平衡速度和精度
        quantBytes: 2 // 使用2字节量化,模型更小,加载更快
    });
    statsDiv.textContent += ' 模型加载完成!';
    console.log('PoseNet模型加载成功');
}

async function startPoseEstimation() {
    if (!net || !stream || isEstimating) return;
    isEstimating = true;
    statsDiv.textContent = '开始姿态估计...';

    async function estimatePose() {
        if (!isEstimating) return;

        // 1. 从video元素中估计姿态
        // 参数说明:videoElement - 输入源;imageScaleFactor - 图像缩放因子,小于1可提速;flipHorizontal - 是否水平翻转
        const poses = await net.estimatePoses(videoElement, {
            flipHorizontal: false,
            maxDetections: 2, // 最多检测2个人
            scoreThreshold: 0.5, // 关键点置信度阈值
            nmsRadius: 20 // 非极大值抑制半径
        });

        // 2. 清空画布
        ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);

        // 3. 绘制检测到的人体姿态
        poses.forEach(pose => {
            drawKeypoints(pose.keypoints, 0.2, ctx); // 绘制关键点,阈值0.2
            drawSkeleton(pose.keypoints, 0.2, ctx); // 绘制骨架连线
        });

        // 4. 计算并显示帧率(简易版)
        // 在实际项目中,可以使用更精确的`stats.js`库

        // 5. 循环调用,实现实时估计
        requestAnimationFrame(estimatePose);
    }

    // 先加载模型,然后开始循环估计
    if (!net) {
        await loadPoseNetModel();
    }
    estimatePose();
}

// 停止姿态估计
function stopPoseEstimation() {
    isEstimating = false;
}

// 绘制关键点函数
function drawKeypoints(keypoints, minConfidence, ctx, scale = 1) {
    keypoints.forEach(keypoint => {
        if (keypoint.score >= minConfidence) {
            const { y, x } = keypoint.position;
            ctx.beginPath();
            ctx.arc(x * scale, y * scale, 4, 0, 2 * Math.PI);
            ctx.fillStyle = 'aqua';
            ctx.fill();
        }
    });
}

// 绘制骨架连线函数
function drawSkeleton(keypoints, minConfidence, ctx, scale = 1) {
    // PoseNet定义的相邻关键点对
    const adjacentKeyPoints = posenet.getAdjacentKeyPoints(keypoints, minConfidence);
    adjacentKeyPoints.forEach((keypoints) => {
        const [start, end] = keypoints.map(kp => kp.position);
        ctx.beginPath();
        ctx.moveTo(start.x * scale, start.y * scale);
        ctx.lineTo(end.x * scale, end.y * scale);
        ctx.lineWidth = 2;
        ctx.strokeStyle = 'lime';
        ctx.stroke();
    });
}

// 页面加载时预加载模型(可选)
window.onload = () => {
    loadPoseNetModel(); // 提前加载,减少首次检测延迟
};

代码关键点解析与调优经验 :

  1. 模型加载参数 : posenet.load() 中的参数是性能调优的关键。

    • inputResolution :模型内部处理图像的大小。 设为640x480或更低,能显著提升帧率,对精度影响在可接受范围内 。因为我们的视频源是720p,模型会先将其缩放到这个尺寸再处理。
    • multiplier :MobileNet的宽度乘数,值越小(如0.5),网络越窄,速度越快,精度越低。0.75是一个不错的平衡点。
    • quantBytes :量化字节数。 2 表示使用16位浮点数,模型更小,加载和推理更快,是推荐选项。
  2. 姿态估计参数 : net.estimatePoses() 中的参数控制检测行为。

    • scoreThreshold :关键点置信度阈值。 调高(如0.7)可以减少误检,但可能丢失部分关键点;调低(如0.3)则更敏感,但噪声也多 。需要根据场景调整。
    • maxDetections :根据你的场景设置。如果只检测单人,设为1效率最高。
  3. 绘制优化 : drawKeypoints 和 drawSkeleton 函数中的 scale 参数用于适配Canvas和视频源的分辨率。如果Canvas大小与视频分辨率一致(如我们之前设置的), scale 应为1。

  4. 性能瓶颈 :姿态估计是计算密集型任务。在低端电脑或手机浏览器上,帧率可能下降。除了调整上述模型参数,还可以 降低估计频率 ,例如不用 requestAnimationFrame (通常60fps),而改用 setInterval(estimatePose, 50) (20fps)。

现在,打开浏览器访问你的页面,点击“连接视频流”,再稍等片刻模型加载,你应该能在右侧画布上看到实时绘制的人体骨骼点了。挥手、走动,看看系统的反应速度。

5. 从演示到应用:构建智能交互逻辑

有了稳定的视频流和准确的关键点数据,我们就可以在此基础上构建真正的“智能交互”了。姿态数据只是一组坐标,真正的价值在于如何解读它们。

5.1 解析姿态数据与定义动作

PoseNet输出的 pose.keypoints 是一个包含17个关键点(如鼻子、左右眼、左右肩等)的数组。每个关键点对象包含 position:{x, y} 和 score (置信度)。

一个简单的交互例子: 识别“举手”动作 。我们可以通过比较左右手腕与肩膀的Y轴坐标来判断。

// 在estimatePose函数中,检测到姿态后添加逻辑判断
poses.forEach(pose => {
    drawKeypoints(...); // 原有绘制代码
    drawSkeleton(...); // 原有绘制代码

    // 1. 获取关键点索引(根据PoseNet模型定义)
    const KEYPOINTS = pose.keypoints;
    const leftWrist = KEYPOINTS[9]; // 左手腕索引
    const rightWrist = KEYPOINTS[10]; // 右手腕索引
    const leftShoulder = KEYPOINTS[5];
    const rightShoulder = KEYPOINTS[6];

    // 2. 定义动作判断函数
    function isHandRaised(wrist, shoulder, threshold = 30) {
        if (wrist.score < 0.5 || shoulder.score < 0.5) return false; // 置信度太低,忽略
        return wrist.position.y < (shoulder.position.y - threshold); // 手腕Y坐标小于肩膀Y坐标减去阈值
    }

    // 3. 判断并触发事件
    if (isHandRaised(leftWrist, leftShoulder)) {
        console.log("检测到左手举起!");
        // 触发自定义事件,例如:改变页面背景色、发送控制指令等
        document.body.style.backgroundColor = 'lightblue';
        triggerAction('LEFT_HAND_RAISED');
    }
    if (isHandRaised(rightWrist, rightShoulder)) {
        console.log("检测到右手举起!");
        document.body.style.backgroundColor = 'lightcoral';
        triggerAction('RIGHT_HAND_RAISED');
    }
});

function triggerAction(action) {
    // 这里可以实现具体的交互逻辑
    switch(action) {
        case 'LEFT_HAND_RAISED':
            // 例如:控制幻灯片翻页、调节音量、打开灯光...
            break;
        case 'RIGHT_HAND_RAISED':
            // 另一个动作
            break;
        // ... 更多动作
    }
}

这只是最简单的示例。更复杂的交互可以包括:

  • 姿态分类 :计算关键点之间的角度、距离比例,使用简单的逻辑或机器学习分类器(如KNN)来识别“深蹲”、“开合跳”、“太极拳起手式”等。
  • 手势识别 :追踪手部关键点(手腕、食指、拇指)的运动轨迹,识别画圈、滑动、点击等手势。
  • 多人交互 :利用 maxDetections 检测多人,并为每个人分配ID,实现多人游戏或协作应用。

5.2 系统优化与进阶思考

要让这个系统更健壮、更实用,还需要考虑以下几个方面:

  1. 延迟优化链条 :

    • 采集端 :确保树莓派摄像头曝光模式固定,避免自动调整带来的延迟波动。在 rpi-webrtc-streamer 配置中尝试 tuning=fast 等编码参数。
    • 传输端 :如前所述,有线网络是必须。在路由器上为树莓派设置静态IP和可能的QoS(服务质量)优先规则。
    • 处理端 :浏览器中,确保Canvas绘制操作高效。可以尝试使用 OffscreenCanvas 在Web Worker中进行绘制,避免阻塞主线程。
  2. 稳定性提升 :

    • WebRTC重连机制 :网络不稳定时,需要在 peerConnection.onconnectionstatechange 或 oniceconnectionstatechange 事件中监听连接状态,实现自动重连。
    • 模型热更新 :PoseNet模型加载较慢。可以考虑使用 localStorage 或IndexedDB缓存模型文件,第二次加载时从本地读取。
    • 降级策略 :当客户端性能不足时,自动降低 inputResolution 或估计频率,保证基础功能可用。
  3. 从原型到产品 :

    • 封装与部署 :将前端代码(HTML, JS, CSS)部署到一个简单的HTTP服务器(如树莓派上的Nginx),并设置开机自启动 rpi-webrtc-streamer 服务。
    • 安全考虑 :如果需要在公网访问,必须启用SSL(HTTPS),并为 rpi-webrtc-streamer 配置TLS证书。同时考虑添加简单的身份验证。
    • 与后端通信 :识别出的动作可以通过WebSocket或HTTP API发送到后端服务器(可以是树莓派上的另一个服务,也可以是云端服务器),从而控制物联网设备、记录数据或触发更复杂的业务流程。

这个项目就像搭积木,核心的三块积木(视频采集、低延迟传输、浏览器AI)已经稳稳当当地拼好了。剩下的,就是发挥你的想象力,用姿态数据这根“线”,去操控属于你的数字世界。无论是做一个体感互动的数字艺术墙,还是一个纠正姿势的健身小助手,技术的大门已经敞开。我在实际调试中发现,最大的成就感往往来自于看到一个简单的坐标比较,最终演变成屏幕上生动反馈的那一刻。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐