HarmonyOS游戏开发:姿态估计与动作捕捉
HarmonyOS游戏开发:姿态估计与动作捕捉
核心要点:掌握人体姿态估计技术原理,使用MindSpore Lite部署骨骼关键点模型,实现实时动作捕捉与体感游戏交互
一、背景与动机
还记得小时候玩Wii Sports的感觉吗?挥动手柄就能打网球、打保龄球,那种"身体就是控制器"的体验让人着迷。但Wii还需要手柄,而今天,我们只需要一个手机摄像头。
想象这样的场景:你在客厅里做健身,手机放在电视柜上,它不仅能识别出你做的是深蹲还是开合跳,还能实时纠正你的姿势——“背部再挺直一点”、“膝盖不要超过脚尖”。或者你在和朋友玩体感格斗游戏,出拳、踢腿、闪避,所有的动作都被实时捕捉并映射到游戏角色上。
这就是**姿态估计(Pose Estimation)**技术的魅力。它通过AI模型从图像中检测人体骨骼关键点,再通过关键点之间的空间关系推断出人体的姿态和动作。HarmonyOS结合MindSpore Lite推理框架,让这一切在端侧就能实现——不需要云端服务器,不需要担心隐私泄露,延迟还极低。
对于游戏开发者来说,姿态估计打开了一个全新的交互维度。从传统的"按键→响应"到"动作→响应",游戏体验的沉浸感有了质的飞跃。
二、核心原理
2.1 人体骨骼关键点模型
姿态估计的核心是检测人体的骨骼关键点(Keypoints)。主流模型通常检测17个关键点:
flowchart TB
A[人体骨骼关键点体系] --> B[头部关键点]
A --> C[上肢关键点]
A --> D[躯干关键点]
A --> E[下肢关键点]
B --> B1[0: 鼻子 Nose]
B --> B2[1: 左眼 Left Eye]
B --> B3[2: 右眼 Right Eye]
B --> B4[3: 左耳 Left Ear]
B --> B5[4: 右耳 Right Ear]
C --> C1[5: 左肩 Left Shoulder]
C --> C2[6: 右肩 Right Shoulder]
C --> C3[7: 左肘 Left Elbow]
C --> C4[8: 右肘 Right Elbow]
C --> C5[9: 左腕 Left Wrist]
C --> C6[10: 右腕 Right Wrist]
D --> D1[11: 左髋 Left Hip]
D --> D2[12: 右髋 Right Hip]
E --> E1[13: 左膝 Left Knee]
E --> E2[14: 右膝 Right Knee]
E --> E3[15: 左踝 Left Ankle]
E --> E4[16: 右踝 Right Ankle]
classDef primary fill:#4F46E5,stroke:#3730A3,color:#fff
classDef warning fill:#F59E0B,stroke:#D97706,color:#fff
classDef error fill:#EF4444,stroke:#DC2626,color:#fff
classDef info fill:#06B6D4,stroke:#0891B2,color:#fff
classDef purple fill:#8B5CF6,stroke:#7C3AED,color:#fff
class A primary
class B,B1,B2,B3,B4,B5 info
class C,C1,C2,C3,C4,C5,C6 warning
class D,D1,D2 purple
class E,E1,E2,E3,E4 error
2.2 姿态估计推理流程
从摄像头采集到骨骼关键点输出,整个推理流程如下:
- 图像采集 → 相机获取视频帧
- 图像预处理 → 缩放、归一化、格式转换
- 模型推理 → MindSpore Lite执行前向计算
- 后处理 → 解码热力图,提取关键点坐标
- 动作识别 → 基于关键点序列判断动作类型
2.3 动作识别算法
有了关键点序列后,如何判断用户在做什么动作?这里有两种主要方法:
基于规则的方法:定义动作的几何约束条件。比如"双手举过头顶"可以定义为:左腕和右腕的Y坐标都小于鼻子Y坐标。这种方法简单直接,适合动作类型有限的场景。
基于序列的方法:将关键点序列输入RNN/LSTM或Transformer,让模型自动学习动作特征。适合动作类型多、时序特征复杂的场景。
对于游戏开发,基于规则的方法通常够用,而且延迟更低、可解释性更强。
2.4 关键点角度计算
动作识别中一个核心操作是关节角度计算。通过三个关键点(如肩-肘-腕)可以计算肘关节的弯曲角度:
角度 = arccos((向量1 · 向量2) / (|向量1| × |向量2|))
这个角度可以用来判断关节的弯曲程度,进而识别动作。
三、代码实战
3.1 骨骼关键点渲染器
这个示例展示了如何渲染人体骨骼关键点和骨骼连线,构建姿态可视化的基础组件。
// SkeletonRenderer.ets - 骨骼关键点渲染器
// 功能:渲染17个人体骨骼关键点及其连线,支持关键点置信度过滤
@Component
export struct SkeletonRenderer {
// 17个关键点数据:[x, y, confidence]
@Prop keypoints: Array<Keypoint> = []
// 渲染区域宽度
private canvasWidth: number = 360
// 渲染区域高度
private canvasHeight: number = 480
// 置信度阈值:低于此值的关键点不渲染
private confidenceThreshold: number = 0.3
// 关键点半径
private pointRadius: number = 6
// 连线宽度
private lineWidth: number = 3
// 骨骼连线定义:[起点索引, 终点索引]
private skeletonConnections: number[][] = [
// 头部
[0, 1], [0, 2], [1, 3], [2, 4],
// 上肢
[5, 6], [5, 7], [7, 9], [6, 8], [8, 10],
// 躯干
[5, 11], [6, 12], [11, 12],
// 下肢
[11, 13], [13, 15], [12, 14], [14, 16]
]
// 连线颜色分组
private connectionColors: Map<string, string> = new Map([
['head', '#4ECDC4'], // 头部连线:青色
['upper', '#FF6B6B'], // 上肢连线:红色
['torso', '#FFEAA7'], // 躯干连线:黄色
['lower', '#45B7D1'] // 下肢连线:蓝色
])
build() {
Stack() {
// 连线层
ForEach(this.skeletonConnections, (connection: number[], index: number) => {
const startKp = this.keypoints[connection[0]]
const endKp = this.keypoints[connection[1]]
// 两个关键点都有效时才画线
if (startKp && endKp &&
startKp.confidence > this.confidenceThreshold &&
endKp.confidence > this.confidenceThreshold) {
Line()
.width(this.canvasWidth)
.height(this.canvasHeight)
.startPoint({ x: startKp.x * this.canvasWidth, y: startKp.y * this.canvasHeight })
.endPoint({ x: endKp.x * this.canvasWidth, y: endKp.y * this.canvasHeight })
.stroke(this.getConnectionColor(index))
.strokeWidth(this.lineWidth)
.strokeLineCap(LineCapStyle.ROUND)
}
}, (connection: number[], index: number) => `line_${index}`)
// 关键点层
ForEach(this.keypoints, (kp: Keypoint, index: number) => {
if (kp && kp.confidence > this.confidenceThreshold) {
Stack() {
// 外圈光晕
Circle()
.width(this.pointRadius * 3)
.height(this.pointRadius * 3)
.fill(this.getKeypointColor(index))
.opacity(0.2)
// 内圈实心
Circle()
.width(this.pointRadius * 2)
.height(this.pointRadius * 2)
.fill(this.getKeypointColor(index))
}
.position({
x: kp.x * this.canvasWidth - this.pointRadius * 1.5,
y: kp.y * this.canvasHeight - this.pointRadius * 1.5
})
}
}, (kp: Keypoint, index: number) => `kp_${index}`)
}
.width(this.canvasWidth)
.height(this.canvasHeight)
}
// 根据连线索引获取颜色
private getConnectionColor(index: number): string {
if (index < 4) return '#4ECDC4' // 头部
if (index < 9) return '#FF6B6B' // 上肢
if (index < 12) return '#FFEAA7' // 躯干
return '#45B7D1' // 下肢
}
// 根据关键点索引获取颜色
private getKeypointColor(index: number): string {
if (index < 5) return '#4ECDC4' // 头部
if (index < 11) return '#FF6B6B' // 上肢
if (index < 13) return '#FFEAA7' // 躯干
return '#45B7D1' // 下肢
}
}
// 关键点数据结构
export interface Keypoint {
x: number // 归一化X坐标 [0, 1]
y: number // 归一化Y坐标 [0, 1]
confidence: number // 置信度 [0, 1]
}
3.2 动作识别引擎:基于规则的体感检测
这个示例实现了基于骨骼关键点的动作识别引擎,支持深蹲、举手、侧弯等常见健身动作的检测。
// ActionRecognizer.ets - 动作识别引擎
// 功能:基于骨骼关键点的几何约束规则,实时识别用户动作
export class ActionRecognizer {
// 动作识别结果
private currentAction: string = 'idle'
// 动作持续时间(帧数)
private actionFrames: number = 0
// 动作确认阈值:连续N帧识别为同一动作才确认
private readonly ACTION_CONFIRM_FRAMES: number = 5
// 角度阈值
private readonly ANGLE_THRESHOLD: number = 160
// 垂直阈值
private readonly VERTICAL_THRESHOLD: number = 0.05
// 识别动作:输入关键点序列,输出当前动作
recognizeAction(keypoints: Array<Keypoint>): ActionResult {
// 安全检查:关键点数量不足
if (!keypoints || keypoints.length < 17) {
return { action: 'idle', confidence: 0, details: '关键点不足' }
}
// 计算各关节角度
const leftElbowAngle = this.calculateAngle(keypoints[5], keypoints[7], keypoints[9]) // 左肘
const rightElbowAngle = this.calculateAngle(keypoints[6], keypoints[8], keypoints[10]) // 右肘
const leftKneeAngle = this.calculateAngle(keypoints[11], keypoints[13], keypoints[15]) // 左膝
const rightKneeAngle = this.calculateAngle(keypoints[12], keypoints[14], keypoints[16])// 右膝
const leftShoulderAngle = this.calculateAngle(keypoints[7], keypoints[5], keypoints[11])// 左肩
const rightShoulderAngle = this.calculateAngle(keypoints[8], keypoints[6], keypoints[12])// 右肩
// 检测各种动作
let detectedAction = 'idle'
// 1. 深蹲检测:膝盖弯曲角度小于阈值
if (leftKneeAngle < 120 && rightKneeAngle < 120) {
detectedAction = 'squat'
}
// 2. 双手举过头顶:手腕Y坐标小于鼻子Y坐标
else if (keypoints[9].y < keypoints[0].y && keypoints[10].y < keypoints[0].y) {
detectedAction = 'hands_up'
}
// 3. T字姿势:双臂水平展开
else if (leftShoulderAngle > 150 && leftShoulderAngle < 195 &&
rightShoulderAngle > 150 && rightShoulderAngle < 195 &&
leftElbowAngle > 160 && rightElbowAngle > 160) {
detectedAction = 't_pose'
}
// 4. 左侧弯:左肩到左髋的距离明显缩短
else if (this.calculateDistance(keypoints[5], keypoints[11]) <
this.calculateDistance(keypoints[6], keypoints[12]) * 0.7) {
detectedAction = 'lean_left'
}
// 5. 右侧弯
else if (this.calculateDistance(keypoints[6], keypoints[12]) <
this.calculateDistance(keypoints[5], keypoints[11]) * 0.7) {
detectedAction = 'lean_right'
}
// 6. 出拳:一只手快速前伸
else if (keypoints[9].confidence > 0.5 && keypoints[9].x > keypoints[7].x + 0.1) {
detectedAction = 'punch_left'
} else if (keypoints[10].confidence > 0.5 && keypoints[10].x < keypoints[8].x - 0.1) {
detectedAction = 'punch_right'
}
// 动作确认:连续多帧识别为同一动作
if (detectedAction === this.currentAction) {
this.actionFrames++
} else {
this.currentAction = detectedAction
this.actionFrames = 1
}
const isConfirmed = this.actionFrames >= this.ACTION_CONFIRM_FRAMES
const confidence = Math.min(this.actionFrames / this.ACTION_CONFIRM_FRAMES, 1.0)
return {
action: isConfirmed ? this.currentAction : 'idle',
confidence: confidence,
details: `左膝:${leftKneeAngle.toFixed(0)}° 右膝:${rightKneeAngle.toFixed(0)}°`
}
}
// 计算三个关键点之间的角度(度数)
// pointB为顶点
private calculateAngle(pointA: Keypoint, pointB: Keypoint, pointC: Keypoint): number {
// 向量BA
const vectorBA = { x: pointA.x - pointB.x, y: pointA.y - pointB.y }
// 向量BC
const vectorBC = { x: pointC.x - pointB.x, y: pointC.y - pointB.y }
// 点积
const dotProduct = vectorBA.x * vectorBC.x + vectorBA.y * vectorBC.y
// 模长
const magnitudeBA = Math.sqrt(vectorBA.x * vectorBA.x + vectorBA.y * vectorBA.y)
const magnitudeBC = Math.sqrt(vectorBC.x * vectorBC.x + vectorBC.y * vectorBC.y)
// 防止除零
if (magnitudeBA < 0.001 || magnitudeBC < 0.001) {
return 180
}
// 计算角度
const cosAngle = Math.max(-1, Math.min(1, dotProduct / (magnitudeBA * magnitudeBC)))
return Math.acos(cosAngle) * 180 / Math.PI
}
// 计算两个关键点之间的距离
private calculateDistance(pointA: Keypoint, pointB: Keypoint): number {
return Math.sqrt(
Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)
)
}
// 重置识别状态
reset() {
this.currentAction = 'idle'
this.actionFrames = 0
}
}
// 动作识别结果
export interface ActionResult {
action: string // 动作名称
confidence: number // 置信度 [0, 1]
details: string // 详细信息
}
// 关键点数据结构(同上)
export interface Keypoint {
x: number
y: number
confidence: number
}
3.3 体感健身游戏:深蹲挑战
这个示例将骨骼渲染器和动作识别引擎组合起来,构建一个完整的体感健身游戏。
// SquatChallenge.ets - 深蹲挑战体感游戏
// 功能:通过摄像头实时检测深蹲动作,计分并反馈
import { SkeletonRenderer, Keypoint } from './SkeletonRenderer'
import { ActionRecognizer, ActionResult } from './ActionRecognizer'
@Entry
@Component
struct SquatChallenge {
// 游戏状态
@State gameState: 'ready' | 'playing' | 'paused' | 'finished' = 'ready'
@State score: number = 0 // 当前得分
@State combo: number = 0 // 连击数
@State maxCombo: number = 0 // 最大连击
@State squatCount: number = 0 // 深蹲次数
@State targetSquats: number = 20 // 目标深蹲数
@State timeLeft: number = 60 // 剩余时间(秒)
@State currentAction: string = 'idle' // 当前动作
@State actionConfidence: number = 0 // 动作置信度
@State feedbackMessage: string = '' // 反馈消息
@State feedbackColor: string = '#808090' // 反馈颜色
@State keypoints: Array<Keypoint> = [] // 骨骼关键点
// 识别引擎
private actionRecognizer: ActionRecognizer = new ActionRecognizer()
// 计时器ID
private timerId: number = -1
// 上一帧动作(用于检测动作转换)
private lastAction: string = 'idle'
// 模拟关键点数据(实际开发中从相机+MindSpore获取)
private simulationInterval: number = -1
aboutToDisappear() {
this.stopGame()
this.stopSimulation()
}
build() {
Column() {
// 顶部状态栏
Row() {
// 得分
Column() {
Text(`${this.score}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#4ECDC4')
Text('得分')
.fontSize(12)
.fontColor('#808090')
}
.alignItems(HorizontalAlign.Center)
Blank()
// 深蹲进度
Column() {
Text(`${this.squatCount}/${this.targetSquats}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFEAA7')
Text('深蹲')
.fontSize(12)
.fontColor('#808090')
}
.alignItems(HorizontalAlign.Center)
Blank()
// 连击
Column() {
Text(`${this.combo}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor(this.combo >= 5 ? '#FF6B6B' : '#8B5CF6')
Text('连击')
.fontSize(12)
.fontColor('#808090')
}
.alignItems(HorizontalAlign.Center)
Blank()
// 倒计时
Column() {
Text(`${this.timeLeft}`)
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor(this.timeLeft <= 10 ? '#EF4444' : '#e0e0ff')
Text('秒')
.fontSize(12)
.fontColor('#808090')
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.height(80)
.padding({ left: 16, right: 16 })
.backgroundColor('#16213e')
// 骨骼渲染区域
Stack() {
// 背景
Column()
.width('100%')
.height('100%')
.backgroundColor('#0f0f23')
// 骨骼渲染器
SkeletonRenderer({
keypoints: this.keypoints,
canvasWidth: 360,
canvasHeight: 480
})
// 动作反馈叠加层
Column() {
// 当前动作指示
Text(this.getActionEmoji(this.currentAction))
.fontSize(48)
.opacity(0.6)
Text(this.feedbackMessage)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.feedbackColor)
.margin({ top: 8 })
// 置信度条
Progress({ value: this.actionConfidence * 100, total: 100, type: ProgressType.Linear })
.width(120)
.color('#4F46E5')
.margin({ top: 8 })
}
.position({ x: '50%', y: 40 })
.translate({ x: '-50%' })
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.layoutWeight(1)
// 深蹲进度条
Progress({ value: this.squatCount, total: this.targetSquats, type: ProgressType.Linear })
.width('90%')
.height(8)
.color('#4F46E5')
.margin({ top: 8 })
// 底部操作栏
Row() {
if (this.gameState === 'ready') {
Button('🎮 开始挑战')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.backgroundColor('#4F46E5')
.width('80%')
.height(48)
.onClick(() => this.startGame())
} else if (this.gameState === 'playing') {
Button('⏸ 暂停')
.fontSize(14)
.backgroundColor('#F59E0B')
.onClick(() => this.pauseGame())
} else if (this.gameState === 'paused') {
Button('▶ 继续')
.fontSize(14)
.backgroundColor('#10B981')
.onClick(() => this.resumeGame())
} else {
Column() {
Text('🎉 挑战完成!')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#4ECDC4')
Text(`总得分: ${this.score} | 最大连击: ${this.maxCombo}`)
.fontSize(14)
.fontColor('#a0a0cc')
.margin({ top: 4 })
Button('再来一次')
.fontSize(14)
.backgroundColor('#4F46E5')
.margin({ top: 8 })
.onClick(() => this.resetGame())
}
.alignItems(HorizontalAlign.Center)
}
}
.width('100%')
.height(80)
.justifyContent(FlexAlign.Center)
.backgroundColor('#16213e')
}
.width('100%')
.height('100%')
.backgroundColor('#0f0f23')
}
// 开始游戏
private startGame() {
this.gameState = 'playing'
this.score = 0
this.combo = 0
this.maxCombo = 0
this.squatCount = 0
this.timeLeft = 60
this.actionRecognizer.reset()
this.startTimer()
this.startSimulation()
}
// 暂停游戏
private pauseGame() {
this.gameState = 'paused'
this.stopTimer()
}
// 继续游戏
private resumeGame() {
this.gameState = 'playing'
this.startTimer()
}
// 停止游戏
private stopGame() {
this.gameState = 'finished'
this.stopTimer()
this.stopSimulation()
}
// 重置游戏
private resetGame() {
this.gameState = 'ready'
this.score = 0
this.combo = 0
this.maxCombo = 0
this.squatCount = 0
this.timeLeft = 60
this.currentAction = 'idle'
this.feedbackMessage = ''
this.keypoints = []
this.actionRecognizer.reset()
}
// 启动计时器
private startTimer() {
this.timerId = setInterval(() => {
this.timeLeft--
if (this.timeLeft <= 0) {
this.stopGame()
}
}, 1000)
}
// 停止计时器
private stopTimer() {
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
}
// 启动模拟数据(实际开发中替换为相机+MindSpore推理)
private startSimulation() {
let simFrame = 0
this.simulationInterval = setInterval(() => {
simFrame++
// 模拟周期性深蹲动作
const phase = (simFrame % 60) / 60 // 0~1循环
const isSquatting = phase > 0.3 && phase < 0.7
// 生成模拟关键点
this.keypoints = this.generateSimulatedKeypoints(isSquatting)
// 执行动作识别
const result: ActionResult = this.actionRecognizer.recognizeAction(this.keypoints)
this.currentAction = result.action
this.actionConfidence = result.confidence
// 检测动作转换:idle→squat 表示完成一次深蹲
if (this.lastAction === 'squat' && result.action === 'idle' && result.confidence > 0.8) {
this.onSquatCompleted()
}
this.lastAction = result.action
}, 33) // ~30fps
}
// 停止模拟
private stopSimulation() {
if (this.simulationInterval !== -1) {
clearInterval(this.simulationInterval)
this.simulationInterval = -1
}
}
// 深蹲完成回调
private onSquatCompleted() {
this.squatCount++
this.combo++
this.maxCombo = Math.max(this.maxCombo, this.combo)
// 计分:基础10分 + 连击加成
const bonus = Math.min(this.combo, 10) * 2
this.score += 10 + bonus
this.feedbackMessage = `深蹲+${10 + bonus}分!连击x${this.combo}`
this.feedbackColor = '#10B981'
// 检查是否完成目标
if (this.squatCount >= this.targetSquats) {
this.score += this.timeLeft * 5 // 时间奖励
this.stopGame()
}
}
// 生成模拟关键点数据
private generateSimulatedKeypoints(isSquatting: boolean): Array<Keypoint> {
const squatOffset = isSquatting ? 0.15 : 0 // 深蹲时身体下移
const kneeBend = isSquatting ? 0.05 : 0 // 深蹲时膝盖外展
// 17个关键点的模拟坐标(归一化)
return [
{ x: 0.5, y: 0.15 + squatOffset * 0.3, confidence: 0.95 }, // 0: 鼻子
{ x: 0.47, y: 0.12 + squatOffset * 0.3, confidence: 0.9 }, // 1: 左眼
{ x: 0.53, y: 0.12 + squatOffset * 0.3, confidence: 0.9 }, // 2: 右眼
{ x: 0.44, y: 0.14 + squatOffset * 0.3, confidence: 0.85 }, // 3: 左耳
{ x: 0.56, y: 0.14 + squatOffset * 0.3, confidence: 0.85 }, // 4: 右耳
{ x: 0.38, y: 0.28 + squatOffset * 0.5, confidence: 0.92 }, // 5: 左肩
{ x: 0.62, y: 0.28 + squatOffset * 0.5, confidence: 0.92 }, // 6: 右肩
{ x: 0.32, y: 0.42 + squatOffset * 0.6, confidence: 0.88 }, // 7: 左肘
{ x: 0.68, y: 0.42 + squatOffset * 0.6, confidence: 0.88 }, // 8: 右肘
{ x: 0.30, y: 0.55 + squatOffset * 0.7, confidence: 0.85 }, // 9: 左腕
{ x: 0.70, y: 0.55 + squatOffset * 0.7, confidence: 0.85 }, // 10: 右腕
{ x: 0.42, y: 0.55 + squatOffset, confidence: 0.9 }, // 11: 左髋
{ x: 0.58, y: 0.55 + squatOffset, confidence: 0.9 }, // 12: 右髋
{ x: 0.40 - kneeBend, y: 0.72 + squatOffset, confidence: 0.87 }, // 13: 左膝
{ x: 0.60 + kneeBend, y: 0.72 + squatOffset, confidence: 0.87 }, // 14: 右膝
{ x: 0.41, y: 0.90, confidence: 0.82 }, // 15: 左踝
{ x: 0.59, y: 0.90, confidence: 0.82 } // 16: 右踝
]
}
// 获取动作对应的Emoji
private getActionEmoji(action: string): string {
const emojiMap: Record<string, string> = {
'idle': '🧍',
'squat': '🏋️',
'hands_up': '🙌',
't_pose': '✈️',
'lean_left': '⬅️',
'lean_right': '➡️',
'punch_left': '🥊',
'punch_right': '🥊'
}
return emojiMap[action] || '❓'
}
}
四、踩坑与注意事项
4.1 MindSpore Lite模型部署
问题:模型文件(.ms格式)放置位置不对,导致加载失败。
解决方案:模型文件必须放在 rawfile 目录下,通过 resourceManager 读取。
import resourceManager from '@ohos.resourceManager'
import { mindspore } from '@ohos.ai.mindspore'
// 正确的模型加载方式
async loadModel(context: Context): Promise<mindspore.Model> {
const msResource = await context.resourceManager.getRawFileContent('pose_estimation.ms')
const model = await mindspore.createModel(msResource)
return model
}
4.2 相机帧率与推理帧率不匹配
问题:相机以30fps采集,但模型推理只能达到15fps,导致帧积压和内存泄漏。
解决方案:使用"最新帧策略"——推理完成时取最新帧,丢弃中间帧。
// 最新帧策略:避免帧积压
@State latestFrame: ImageData | null = null
@State isInferencing: boolean = false
onFrameAvailable(frame: ImageData) {
// 始终更新最新帧
this.latestFrame = frame
// 只在推理空闲时启动新推理
if (!this.isInferencing) {
this.runInference(this.latestFrame)
}
}
async runInference(frame: ImageData) {
this.isInferencing = true
try {
const result = await this.model.predict(frame)
this.processResult(result)
} finally {
this.isInferencing = false
}
}
4.3 关键点坐标映射
问题:模型输出的关键点坐标是归一化的(0~1),但渲染时需要映射到屏幕坐标,容易搞反X/Y。
注意事项:
- 模型输出的坐标通常是
[x, y],其中x对应水平方向,y对应垂直方向 - 相机图像可能需要镜像翻转(前置摄像头)
- 归一化坐标乘以画布宽高即可得到屏幕坐标
// 正确的坐标映射(含镜像)
private mapKeypoint(kp: Keypoint, canvasWidth: number, canvasHeight: number, mirror: boolean): { x: number, y: number } {
return {
x: (mirror ? (1 - kp.x) : kp.x) * canvasWidth,
y: kp.y * canvasHeight
}
}
4.4 多人场景的关键点分组
问题:画面中有多个人时,关键点需要按人分组,否则骨骼连线会交叉混乱。
解决方案:使用模型的实例分割输出或关键点分组ID,按人ID分别渲染。
五、HarmonyOS 6适配
5.1 MindSpore Lite API变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 模型加载 | mindspore.createModel() |
mindspore.loadModelV2()(支持增量更新) |
| 推理接口 | model.predict() |
model.runInference()(新增异步回调) |
| GPU加速 | 手动配置 | 自动选择最优设备(GPU/NPU/CPU) |
| 模型格式 | .ms |
.ms + .msq(量化格式,体积更小) |
5.2 相机API变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 相机管理 | camera.CameraManager |
camera.CameraManagerV2(支持多流并发) |
| 帧回调 | on('frameAvailable') |
on('frameAvailableV2')(支持零拷贝) |
| 人体检测 | 需自行实现 | 内置 BodyDetection API |
5.3 迁移要点
- 模型自动量化:HarmonyOS 6的MindSpore Lite支持训练后量化(PTQ),可将FP32模型自动转换为INT8,推理速度提升2-3倍
- NPU自动调度:推理时无需手动指定设备,框架会根据模型特征自动选择NPU/GPU/CPU
- 内置人体检测:新增
@ohos.ai.bodyDetection模块,可直接获取骨骼关键点,无需自行部署模型 - 零拷贝帧传输:相机到推理引擎的帧数据传输不再需要内存拷贝,延迟降低30%
// HarmonyOS 6 新增:内置人体检测API
import { bodyDetection } from '@ohos.ai.bodyDetection'
async detectPose(image: PixelMap): Promise<bodyDetection.PoseResult> {
const result = await bodyDetection.detectPose(image)
// result.keypoints: 17个关键点坐标和置信度
// result.boundingBox: 人体边界框
return result
}
六、总结
mindmap
root((姿态估计与动作捕捉))
骨骼关键点
17个标准关键点
置信度过滤
骨骼连线渲染
推理流程
图像采集
预处理缩放归一化
MindSpore Lite推理
后处理解码热力图
动作识别
基于规则方法
关节角度计算
动作确认防抖
基于序列方法LSTM
体感游戏
深蹲检测
举手检测
出拳检测
连击计分系统
关键技巧
最新帧策略
坐标映射与镜像
多人关键点分组
模型量化加速
HarmonyOS 6
内置人体检测API
NPU自动调度
零拷贝帧传输
模型增量更新
classDef primary fill:#4F46E5,stroke:#3730A3,color:#fff
classDef warning fill:#F59E0B,stroke:#D97706,color:#fff
classDef error fill:#EF4444,stroke:#DC2626,color:#fff
classDef info fill:#06B6D4,stroke:#0891B2,color:#fff
classDef purple fill:#8B5CF6,stroke:#7C3AED,color:#fff
| 知识点 | 核心内容 |
|---|---|
| 骨骼关键点 | 17个标准关键点,含坐标和置信度,需过滤低置信度点 |
| 推理流程 | 采集→预处理→MindSpore推理→后处理→动作识别 |
| 角度计算 | 三点夹角公式,用于判断关节弯曲程度 |
| 动作确认 | 连续N帧识别为同一动作才确认,避免误触 |
| 最新帧策略 | 推理完成时取最新帧,避免帧积压 |
| 坐标映射 | 归一化坐标→屏幕坐标,注意前置摄像头镜像 |
| 模型部署 | rawfile目录存放.ms模型,resourceManager读取 |
| HarmonyOS 6 | 内置bodyDetection API、NPU自动调度、零拷贝帧传输 |
姿态估计是体感交互的核心技术。从骨骼关键点检测到动作识别,再到游戏逻辑整合,每一步都需要精心设计。掌握角度计算和动作确认机制,你就能构建出流畅、准确的体感游戏体验。下一篇,我们将探索表情识别与情感计算,让设备读懂你的情绪。
- 点赞
- 收藏
- 关注作者
评论(0)