Project AIRI 中的 MediaPipe 单人体感动捕管线:从摄像头帧到 VRM 驱动的架构与实践
2026/9/12 17:40:53 网站建设 项目流程

Project AIRI 中的 MediaPipe 单人体感动捕管线:从摄像头帧到 VRM 驱动的架构与实践

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

本篇技术指南聚焦 Project AIRI 仓库中的实验性单人体感动捕(mocap)管线包@proj-airi/model-driver-mediapipe。它以camera frame → @mediapipe/tasks-vision → PerceptionState → overlay / VRM的闭环为骨架,讲清引擎调度、丢帧策略、任务级限速与媒体后端边界,并给出从摄像头采集到骨骼姿态应用至 VRM 模型的完整可运行方案。读完你将掌握一套可直接复用的浏览器端单人动捕架构,以及将 2D/3D 关键点转换为 VRM 骨骼驱动方向的工程化实现。

包定位与设计意图

packages/model-driver-mediapipe/AGENTS.md 将该包定义为"实验性单人体感动捕管线(experimental single-person mocap pipeline)",服务对象是 stage-web 开发者工具(devtools)。其核心数据流是一条极简闭环:

camera frame → @mediapipe/tasks-vision → PerceptionState → overlay

在 packages/model-driver-mediapipe/README.md 中,这一闭环进一步明确为 stage-web 可直接消费的最小回路,并给出了实际体验入口:

  • 开发工具页:apps/stage-web/src/pages/devtools/model-driver-mediapipe.vue
  • 菜单入口:Settings → System → Developer → "MediaPipe Workshop"(路由注册见 apps/stage-web/src/pages/settings/system/developer.vue)

包本身位于 packages/model-driver-mediapipe/package.json,依赖@mediapipe/tasks-vision@pixiv/three-vrmthreees-toolkit@moeru/std,通过postinstall脚本自动准备推理任务资产。

设计约定:函数式、窄契约与干净的后端边界

AGENTS.md 明确了几条对整个实现有约束力的编码约定,读懂它们有助于理解后续每一层代码的结构:

  • 偏好函数式编程(FP)与纯函数:有状态模块用工厂函数 + 闭包实现,例如createMocapEngine();除非是扩展浏览器 API 或被外部库要求,避免使用 class。这与 src/engine.ts 中createStatscreateSchedulercreateMocapEngine三个工厂函数一一对应。
  • 保持后端边界干净:引擎/调度器不得 import@mediapipe/tasks-vision,MediaPipe 相关细节全部收拢在 src/backends/ 下。事实上,src/backends/mediapipe.ts 是唯一的后端实现。
  • 类型稳定且窄:stage 消费方以 src/types.ts 为契约,新增字段必须可选且向后兼容。

中层契约:PerceptionState 与配置类型

src/types.ts 是整个管线的"中间层契约",也是 stage 消费方唯一依赖的类型面。它把底层 MediaPipe 结果归一化为三种感知状态与一套质量指标:

  • PoseStatelandmarks2d(归一化 2D 关键点)与worldLandmarks(3D 世界关键点),分别对应NormalizedLandmarkLandmark
  • HandStatehandedness'Left' | 'Right')、21 个 2D 关键点及置信度score
  • FaceStatehasFace与 468 个 2D 关键点;
  • PerceptionQualityfpslatencyMsdroppedFrames,并标记backend: 'mediapipe'mode: 'split-tasks'
  • PerceptionPartial:上述三个子状态均为可选,用于支持"部分合并"(partial merge)。

运行配置MocapConfig的关键字段为:

字段类型含义
enabledRecord<MocapJob, boolean>每个任务(pose/hands/face)的开关
hzRecord<MocapJob, number>每个任务的调度速率(Hz),用于限流
maxPeople1固定为单人

同时类型层定义了后端与引擎的接口:MocapBackendinit/isBusy/run)与MocapEngineinit/start/stop/updateConfig/resetState),这正是"引擎不依赖 MediaPipe 具体实现"得以成立的基础。完整导出见 src/index.ts。

引擎调度:限速、丢帧策略与状态合并

src/engine.ts 实现了调度 + 丢帧策略 + 部分合并三件事,全程采用函数式写法。

帧率统计(createStats:以requestAnimationFrame的时间戳为基准计算瞬时 FPS,并用指数滑动平均(smoothedFps * 0.9 + fps * 0.1)平滑抖动。

任务调度器(createScheduler:每个任务(pose/hands/face)独立记录上次执行时刻lastRunplan(nowMs)1000 / hz判断本轮是否轮到某个任务,从而把每一帧的推理开销控制在用户配置的速率之下——这是对"同步detectForVideo()阻塞主线程"的直接对冲手段。

引擎主循环(createMocapEngine

const tick = async () => { const frame = source.getFrame() const now = performance.now() // Skip this frame if the backend is still busy. if (backend.isBusy()) { droppedFrames++ rafId = requestAnimationFrame(tick) return } const jobs = scheduler.plan(now) const t0 = performance.now() const partial = jobs.length > 0 ? await backend.run(frame, jobs, now) : {} const latencyMs = performance.now() - t0 lastPartial = { ...lastPartial, ...partial } onState({ t: now, ...lastPartial, quality: { fps, latencyMs, droppedFrames, backend: 'mediapipe', mode: 'split-tasks' } }) rafId = requestAnimationFrame(tick) }

值得注意的实现细节:

  1. 丢帧策略:当后端isBusy()返回 true(上一帧推理尚未完成),当前帧直接跳过并累加droppedFrames,绝不排队积压,避免 UI 线程被无限阻塞;
  2. 部分合并(partial merge)lastPartial = { ...lastPartial, ...partial },只更新本轮实际推理过的任务子状态,其余子状态沿用上一帧,使各任务可以以不同速率独立刷新;
  3. 错误隔离:异常会停止主循环并回调options.onError,避免错误风暴,消费方可决定是否重启(apps/stage-web/src/pages/devtools/model-driver-mediapipe.vue 中的onError正是这一设计的使用方)。

MediaPipe 后端:模型加载与同步推理

src/backends/mediapipe.ts 是@mediapipe/tasks-vision的适配层,要点如下:

  • 惰性加载init()动态import('@mediapipe/tasks-vision')并通过FilesetResolver.forVisionTasks(visionTaskWasmRoot)解析 WASM 文件集,仅首次调用时执行;
  • 按需创建 landmarkerensurePoseLandmarker/ensureHandLandmarker/ensureFaceLandmarker三个工厂各自缓存实例,配置分别为:
// 姿态 PoseLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: visionTaskAssets.pose }, runningMode: 'VIDEO', numPoses: 1, }) // 手部 HandLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: visionTaskAssets.hands }, runningMode: 'VIDEO', numHands: 2, }) // 面部 FaceLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: visionTaskAssets.face }, runningMode: 'VIDEO', numFaces: 1, })
  • 同步推理run()内以es-toolkitSemaphore(1)保证并发安全,置位busy标志供引擎查询,再按任务列表依次调用detectForVideo(frame, nowMs)。正如 references/tasks-vision-api.md 所记录的,detectForVideo()是同步调用、可能阻塞主线程,这正是引擎限速与丢帧策略存在的根本原因;
  • 结果归一化:姿态取res.landmarks[0]res.worldLandmarks[0](单人场景);手部取res.handedness[i][0].categoryName判定左右手并附带置信度score;面部只保留hasFace与关键点,作为"仅存在性"处理(468 个点开销较大,参考任务注释)。

任务资产准备:tasks/prepare-tasks.ts 是postinstall触发的资产准备脚本:从 MediaPipe 官方模型存储下载pose_landmarker_litehand_landmarkerface_landmarker三个.task文件,并把node_modules/@mediapipe/tasks-vision/wasm复制到包内assets/wasm。资产路径在 tasks/tasks.ts 中以new URL('./assets/...', import.meta.url)的方式声明,保证浏览器端可解析;脚本内置withRetry重试与已存在文件的跳过逻辑。

调试叠加层:overlay 渲染器

src/utils/overlay.ts 提供 canvas 叠加渲染。它使用 MediaPipe 的DrawingUtils,分别绘制:

  • 姿态:PoseLandmarker.POSE_CONNECTIONS连接线 + 关键点;
  • 手部:HandLandmarker.HAND_CONNECTIONS连接线 + 关键点,左右手使用不同调色板区分(左手指向 palette 索引 1,右手索引 2);
  • 面部:468 个点用小半径(facePointRadius: 2)绘制以减少视觉杂乱。

调色板与线宽/点半径常量以"面向 devtools 可读性"为原则做了手工调优,drawOverlay支持按enabled参数单独开关三类绘制,绘制前先clearRect清空画布,逐帧重绘。

动捕到 VRM:姿态骨骼驱动

管线并不止步于叠加可视化,而是进一步把 MediaPipe 的姿态输出映射为 VRM 骨骼驱动方向,这是它区别于纯演示的核心价值所在。相关实现位于 src/three/:

pose → VRM 目标(pose-to-vrm.ts)

poseToVrmTargets()以世界关键点为主、归一化关键点为兜底,输出VrmPoseTargets——一组"方向 + pole 向量"目标,覆盖hipsspinechest、左右肩/大臂/小臂/大腿/小腿等 13 个骨骼。工程细节包括:

  • 轴重映射axis选项(x/y/z各取1 | -1)把 MediaPipe 世界坐标系映射到 three/VRM 空间,默认{1,1,1},页面端可通过 flip 开关翻转(见 model-driver-mediapipe.vue 的vrmMapping);
  • 置信度门控confidence.minVisibility(默认0.5)/minPresence基于关键点的visibility/presence字段过滤;当visibility缺失时直接不输出该关键点依赖的目标,避免噪声驱动;
  • 躯干朝向推导:由hipCenter → shoulderCenter求 up、leftShoulder → rightShoulder求 right,再以right × up得 forward,并做符号校正;
  • 稳定化stabilize.previousTargets/previousForward用于避免因极点歧义导致的 180° 翻转,具体手段是当上一帧 pole 与当前 pole 点积为负时取反;
  • 下肢保守策略:腿只在大腿/膝盖/脚踝三者齐备时才输出目标,减少下半身离屏时的幻觉翻转。

应用至 VRM(apply-pose-to-vrm.ts)

createVrmPoseApplier()返回applyPoseDirectionsToVrm/applyPoseTargetsToVrm两个函数,把上述目标应用到@pixiv/three-vrm的人形骨骼。关键机制:

  • 骨骼链映射CHAINS表把 13 个姿态键映射到 VRM 骨骼名及其候选子骨骼(如spine的子骨骼候选为chest / upperChest / neck),骨骼不存在时优雅降级;
  • 静止方向/极点缓存:首次应用时按骨骼世界坐标与子骨骼差向量推算 rest 方向与 rest pole,并在局部空间缓存;
  • 方向应用:无 pole 时用Quaternion.setFromUnitVectors(currentDir, targetDir)求增量旋转;有 pole 时构建 rest/target 两组 basis 矩阵(makeBasis(dir, y, pole))求相对旋转矩阵,把目标方向与极点同时映射到骨骼;
  • 翻转拒绝minDotBeforeReject(默认-0.2,约拒绝 >101°)基于上一帧目标方向而非当前骨骼姿态判断瞬时 180° 翻转;minPoleDotBeforeReject对 pole 做同样的保护;
  • 平滑插值alpha(默认0.35,[0..1])对最终局部四元数做slerp,alpha 越高越跟手、越低越平滑。

端到端接线:MediaPipe Workshop 页面

apps/stage-web/src/pages/devtools/model-driver-mediapipe.vue 把以上所有模块串成可运行闭环,可作为读者整合各层的参考范式:

  1. startCamera()通过navigator.mediaDevices.getUserMedia({ video: true })获取摄像头流,绑定<video>元素并play()
  2. startPipeline()依次createMediaPipeBackend()createMocapEngine(backend, config)engine.init()engine.start({ getFrame: () => video }, onState, { onError }),其中FrameSource.getFrame直接返回视频元素;
  3. onState回调中做三件事:更新页面状态摘要(enabled/hz/fps/latency/dropped);调用poseToVrmTargets(state.pose, { axis, confidence, stabilize })生成 VRM 目标并通过vrmPoseApplier.applyPoseTargetsToVrm(vrm, targets)ThreeScene的帧钩子中驱动模型;最后drawOverlay(ctx, state, enabled)把关键点画到 canvas 叠加层;
  4. 页面watch(config, ...)深监听配置变化并调用engine.updateConfig(),实现运行中动态调整任务开关与 Hz 限速;
  5. stop()设计上考虑了"MediaPipe 可能仍在处理进行中帧"的情况,通过ignoreErrorsUntil窗口吞掉停止期间的瞬时错误,再释放摄像头轨道与清空画布。

页面默认开启 pose/hands/face 三个任务且各 30Hz、maxPeople: 1、姿态过滤minVisibility: 0.5,并在onMounted自动启动,方便直接体验。

上游 API 备忘与适用前提

references/tasks-vision-api.md 是包内维护的"最小 API 备忘",记录了三项对本实现最关键的结论,可视为理解整条管线的速查卡:

  1. 初始化FilesetResolver.forVisionTasks(wasmRoot)后以createFromOptions(vision, { baseOptions, runningMode: 'VIDEO', numPoses: 1 })创建 landmarker;
  2. 同步推理detectForVideo(videoEl, nowMs)同步执行、可能阻塞主线程,因此引擎必须限速与丢帧;
  3. 结果形状(单人场景):姿态res.landmarks/res.worldLandmarks[0]x/y归一化到[0..1];手部res.landmarks每只手 21 点、res.handedness[i][0]提供左右手与置信度;面部res.faceLandmarks为 468 点、开销大,默认仅作存在性使用。

需要说明的适用前提与限制:本包是实验性单人管线,maxPeople固定为 1;依赖浏览器端 WASM 推理,性能受设备与所选模型(姿态使用 lite 版)影响;手部支持最多 2 只手;仓库未提供对多人、离线编译模型或非浏览器运行环境的支持。这些边界在 README.md 的 "Backend assumptions" 一节中亦有明确声明。

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询