HarmonyOS开发:运动检测与活动状态识别
HarmonyOS开发:运动检测与活动状态识别
核心要点:本文系统讲解基于HarmonyOS多传感器融合的运动检测与活动状态识别技术,涵盖加速度/陀螺仪/重力传感器数据融合、状态机驱动的活动切换、以及基于滑动窗口特征提取的实时运动状态判定方案。
| 项目 | 说明 |
|---|---|
| 开发语言 | ArkTS |
| 核心API | @ohos.sensor (加速度/陀螺仪/重力)、@ohos.activityRecognition |
一、背景与动机
运动检测(Motion Detection)是智能设备健康生态的基础能力。从简单的"是否在运动"判断,到精确的"正在做什么运动"识别,运动检测技术正在从粗粒度走向细粒度。
HarmonyOS的多传感器融合框架为运动检测提供了丰富的数据源:加速度传感器检测线性运动、陀螺仪检测旋转角速度、重力传感器提供姿态参考。通过融合多种传感器数据,可以实现比单一传感器更精准、更鲁棒的运动状态识别。
典型应用场景
- 智能久坐提醒:检测用户长时间静止,自动提醒起身活动
- 运动模式自动切换:从步行切换到跑步时自动调整监测参数
- 睡眠质量监测:夜间活动检测辅助判断深睡/浅睡阶段
- 老人看护:异常活动检测(如长时间不动、异常剧烈运动)
- 运动处方执行:监测用户是否按计划完成运动量
二、核心原理
2.1 多传感器数据融合
运动检测的核心挑战是从噪声数据中提取可靠的运动特征。单一传感器容易受到干扰:
| 传感器 | 优势 | 劣势 |
|---|---|---|
| 加速度计 | 检测线性运动、步数 | 受重力影响、无法区分倾斜与加速 |
| 陀螺仪 | 检测旋转、姿态变化 | 存在漂移、长时间累积误差 |
| 重力传感器 | 提供稳定的重力参考 | 无法检测动态运动 |
| 线性加速度 | 去除重力后的纯运动信号 | 精度依赖加速度计校准 |
融合策略:以加速度计为主传感器,陀螺仪辅助姿态校正,重力传感器提供参考基线。
2.2 活动状态分类
本文将活动状态分为以下5类:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 静止 │───→│ 微动 │───→│ 步行 │
│ STILL │ │ TINY │ │ WALK │
└─────────┘ └─────────┘ └─────────┘
│
▼
┌─────────┐ ┌─────────┐
│ 跑步 │───→│ 剧烈 │
│ RUN │ │ INTENSE │
└─────────┘ └─────────┘
2.3 特征提取方法
从传感器数据中提取以下时域和频域特征:
时域特征:
- 信号能量(Signal Energy):加速度平方和的均值,反映运动强度
- 过零率(Zero-Crossing Rate):信号穿越基线的频率,反映运动节奏
- 峰峰值(Peak-to-Peak):最大值与最小值之差,反映运动幅度
- 标准差(Standard Deviation):反映信号波动程度
频域特征:
- 主频率(Dominant Frequency):FFT后的最大频率分量,区分步行(1-2Hz)和跑步(2-4Hz)
- 频谱能量分布:低频/高频能量比,辅助判断运动类型
2.4 运动检测算法流程图
flowchart TD
A[多传感器数据采集] --> B[时间对齐与同步]
B --> C[加速度+陀螺仪数据融合]
C --> D[滑动窗口特征提取<br/>窗口2秒,步长0.5秒]
D --> E{特征向量分类}
E -->|能量<0.1 & 标准差<0.05| F[静止 STILL]
E -->|能量0.1-0.5 & 过零率<2Hz| G[微动 TINY]
E -->|主频1-2Hz & 对称性>0.8| H[步行 WALK]
E -->|主频2-4Hz & 能量>1.0| I[跑步 RUN]
E -->|能量>3.0 & 无规律| J[剧烈 INTENSE]
F & G & H & I & J --> K[状态机平滑过滤]
K --> L[活动状态输出]
L --> M[状态变更通知]
classDef inputStyle fill:#1a1a2e,stroke:#e94560,color:#fff,stroke-width:2px
classDef processStyle fill:#16213e,stroke:#0f3460,color:#e0e0e0,stroke-width:2px
classDef decisionStyle fill:#0f3460,stroke:#533483,color:#fff,stroke-width:2px
classDef stateStyle fill:#533483,stroke:#e94560,color:#fff,stroke-width:2px
classDef outputStyle fill:#e94560,stroke:#ff6b6b,color:#fff,stroke-width:2px
class A,B,C inputStyle
class D,K processStyle
class E decisionStyle
class F,G,H,I,J stateStyle
class L,M outputStyle
三、代码实战
3.1 多传感器数据融合管理器
实现加速度计、陀螺仪、重力传感器的同步订阅与数据融合:
// MultiSensorManager.ets — 多传感器数据融合管理器
import sensor from '@ohos.sensor';
import { BusinessError } from '@ohos.base';
// 融合后的传感器数据帧
interface SensorFrame {
timestamp: number; // 时间戳(毫秒)
accelX: number; // 加速度X
accelY: number; // 加速度Y
accelZ: number; // 加速度Z
gyroX: number; // 角速度X
gyroY: number; // 角速度Y
gyroZ: number; // 角速度Z
gravityX: number; // 重力X
gravityY: number; // 重力Y
gravityZ: number; // 重力Z
linearAccelMagnitude: number; // 线性加速度合值
}
// 传感器订阅配置
const MULTI_SENSOR_CONFIG = {
samplingInterval: 20_000_000, // 50Hz采样
fusionTimeout: 50, // 融合超时50ms
};
@ObservedV2
export class MultiSensorManager {
// 最新传感器数据
private latestAccel: sensor.AccelerometerResponse | null = null;
private latestGyro: sensor.GyroscopeResponse | null = null;
private latestGravity: sensor.GravitySensorResponse | null = null;
// 订阅ID列表
private subscribeIds: number[] = [];
// 数据回调
private onFrameCallback?: (frame: SensorFrame) => void;
// 错误回调
private onErrorCallback?: (error: BusinessError) => void;
/**
* 启动多传感器订阅
*/
startSensors(
onFrame: (frame: SensorFrame) => void,
onError?: (error: BusinessError) => void
): void {
this.onFrameCallback = onFrame;
this.onErrorCallback = onError;
// 订阅加速度传感器
try {
const accelId = sensor.on(sensor.SensorId.ACCELEROMETER,
(data: sensor.AccelerometerResponse) => {
this.latestAccel = data;
this.tryEmitFrame();
},
{ interval: MULTI_SENSOR_CONFIG.samplingInterval }
);
this.subscribeIds.push(accelId);
} catch (e) {
this.onErrorCallback?.(e as BusinessError);
}
// 订阅陀螺仪传感器
try {
const gyroId = sensor.on(sensor.SensorId.GYROSCOPE,
(data: sensor.GyroscopeResponse) => {
this.latestGyro = data;
},
{ interval: MULTI_SENSOR_CONFIG.samplingInterval }
);
this.subscribeIds.push(gyroId);
} catch (e) {
this.onErrorCallback?.(e as BusinessError);
}
// 订阅重力传感器
try {
const gravityId = sensor.on(sensor.SensorId.GRAVITY,
(data: sensor.GravitySensorResponse) => {
this.latestGravity = data;
},
{ interval: MULTI_SENSOR_CONFIG.samplingInterval }
);
this.subscribeIds.push(gravityId);
} catch (e) {
this.onErrorCallback?.(e as BusinessError);
}
console.info('[MultiSensor] 多传感器订阅已启动');
}
/**
* 尝试发射融合数据帧
* 以加速度计为主时钟源,确保数据对齐
*/
private tryEmitFrame(): void {
if (!this.latestAccel) return;
const accel = this.latestAccel;
const gyro = this.latestGyro;
const gravity = this.latestGravity;
// 计算线性加速度(去除重力分量)
let linearMag = 0;
if (gravity) {
const lx = accel.x - gravity.x;
const ly = accel.y - gravity.y;
const lz = accel.z - gravity.z;
linearMag = Math.sqrt(lx * lx + ly * ly + lz * lz);
} else {
// 无重力传感器时,使用高通滤波近似
linearMag = Math.sqrt(accel.x * accel.x + accel.y * accel.y + accel.z * accel.z) - 9.81;
}
const frame: SensorFrame = {
timestamp: accel.timestamp / 1_000_000, // 纳秒转毫秒
accelX: accel.x,
accelY: accel.y,
accelZ: accel.z,
gyroX: gyro?.x ?? 0,
gyroY: gyro?.y ?? 0,
gyroZ: gyro?.z ?? 0,
gravityX: gravity?.x ?? 0,
gravityY: gravity?.y ?? 0,
gravityZ: gravity?.z ?? 0,
linearAccelMagnitude: Math.abs(linearMag),
};
this.onFrameCallback?.(frame);
}
/**
* 停止所有传感器订阅
*/
stopSensors(): void {
// 停止加速度计
try { sensor.off(sensor.SensorId.ACCELEROMETER); } catch (e) { /* 忽略 */ }
// 停止陀螺仪
try { sensor.off(sensor.SensorId.GYROSCOPE); } catch (e) { /* 忽略 */ }
// 停止重力传感器
try { sensor.off(sensor.SensorId.GRAVITY); } catch (e) { /* 忽略 */ }
this.subscribeIds = [];
this.latestAccel = null;
this.latestGyro = null;
this.latestGravity = null;
console.info('[MultiSensor] 多传感器订阅已停止');
}
}
3.2 滑动窗口特征提取与活动状态分类
实现基于滑动窗口的特征提取和活动状态分类器:
// ActivityClassifier.ets — 活动状态分类器
// 活动状态枚举
export enum ActivityState {
STILL = 'STILL', // 静止
TINY = 'TINY', // 微动(坐姿小幅度活动)
WALK = 'WALK', // 步行
RUN = 'RUN', // 跑步
INTENSE = 'INTENSE', // 剧烈运动
}
// 活动状态变更事件
export interface ActivityChangeEvent {
previousState: ActivityState;
currentState: ActivityState;
confidence: number; // 置信度 0-1
timestamp: number;
}
// 特征向量
interface FeatureVector {
energy: number; // 信号能量
stdDev: number; // 标准差
zeroCrossingRate: number; // 过零率
peakToPeak: number; // 峰峰值
dominantFreq: number; // 主频率
gyroEnergy: number; // 陀螺仪能量
}
// 分类阈值配置
const CLASSIFY_THRESHOLDS = {
// 静止判定
stillMaxEnergy: 0.08,
stillMaxStdDev: 0.05,
// 微动判定
tinyMaxEnergy: 0.5,
tinyMaxZeroCrossing: 2.0,
// 步行判定
walkMinFreq: 0.8,
walkMaxFreq: 2.5,
walkMinSymmetry: 0.6,
// 跑步判定
runMinFreq: 2.0,
runMaxFreq: 4.5,
runMinEnergy: 1.0,
// 剧烈运动
intenseMinEnergy: 3.0,
};
// 状态机平滑配置
const STATE_MACHINE_CONFIG = {
minStateDuration: 2000, // 状态最短持续时间2秒(防抖)
confirmationCount: 3, // 连续确认次数
};
@ObservedV2
export class ActivityClassifier {
// 当前活动状态
@Trace currentActivity: ActivityState = ActivityState.STILL;
@Trace confidence: number = 0;
// 滑动窗口缓冲区
private windowBuffer: number[] = [];
private readonly windowSize: number = 100; // 2秒@50Hz
private readonly stepSize: number = 25; // 0.5秒步长
// 陀螺仪能量缓冲
private gyroEnergyBuffer: number[] = [];
// 状态机平滑
private pendingState: ActivityState = ActivityState.STILL;
private pendingCount: number = 0;
private lastStateChangeTime: number = 0;
// 状态变更回调
private onStateChangeCallback?: (event: ActivityChangeEvent) => void;
/**
* 设置状态变更回调
*/
setOnStateChangeListener(callback: (event: ActivityChangeEvent) => void): void {
this.onStateChangeCallback = callback;
}
/**
* 处理传感器数据帧
*/
processFrame(frame: { linearAccelMagnitude: number; gyroX: number; gyroY: number; gyroZ: number; timestamp: number }): void {
// 将线性加速度加入滑动窗口
this.windowBuffer.push(frame.linearAccelMagnitude);
if (this.windowBuffer.length > this.windowSize) {
this.windowBuffer.splice(0, this.windowBuffer.length - this.windowSize);
}
// 陀螺仪能量
const gyroEnergy = frame.gyroX * frame.gyroX + frame.gyroY * frame.gyroY + frame.gyroZ * frame.gyroZ;
this.gyroEnergyBuffer.push(gyroEnergy);
if (this.gyroEnergyBuffer.length > this.windowSize) {
this.gyroEnergyBuffer.splice(0, this.gyroEnergyBuffer.length - this.windowSize);
}
// 窗口满时进行分类
if (this.windowBuffer.length >= this.windowSize) {
const features = this.extractFeatures();
if (features) {
const newState = this.classifyActivity(features);
this.smoothStateTransition(newState, frame.timestamp);
}
}
}
/**
* 特征提取
*/
private extractFeatures(): FeatureVector | null {
const data = this.windowBuffer;
if (data.length < this.windowSize) return null;
// 信号能量(均值平方)
const mean = data.reduce((a, b) => a + b, 0) / data.length;
const energy = data.reduce((sum, val) => sum + val * val, 0) / data.length;
// 标准差
const variance = data.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / data.length;
const stdDev = Math.sqrt(variance);
// 过零率
let zeroCrossings = 0;
for (let i = 1; i < data.length; i++) {
if ((data[i - 1] - mean) * (data[i] - mean) < 0) {
zeroCrossings++;
}
}
const zeroCrossingRate = zeroCrossings / (data.length / 50); // 每秒过零次数
// 峰峰值
const maxVal = Math.max(...data);
const minVal = Math.min(...data);
const peakToPeak = maxVal - minVal;
// 主频率(简化FFT:使用自相关法估计)
const dominantFreq = this.estimateDominantFrequency(data);
// 陀螺仪能量
const gyroEnergy = this.gyroEnergyBuffer.length > 0
? this.gyroEnergyBuffer.reduce((a, b) => a + b, 0) / this.gyroEnergyBuffer.length
: 0;
return { energy, stdDev, zeroCrossingRate, peakToPeak, dominantFreq, gyroEnergy };
}
/**
* 自相关法估计主频率
*/
private estimateDominantFrequency(data: number[]): number {
const sampleRate = 50; // 50Hz
const minLag = Math.floor(sampleRate / 4.5); // 对应4.5Hz
const maxLag = Math.floor(sampleRate / 0.5); // 对应0.5Hz
let maxCorr = -Infinity;
let bestLag = minLag;
for (let lag = minLag; lag <= Math.min(maxLag, data.length - 1); lag++) {
let corr = 0;
let count = 0;
for (let i = 0; i < data.length - lag; i++) {
corr += data[i] * data[i + lag];
count++;
}
corr = count > 0 ? corr / count : 0;
if (corr > maxCorr) {
maxCorr = corr;
bestLag = lag;
}
}
return bestLag > 0 ? sampleRate / bestLag : 0;
}
/**
* 活动状态分类
*/
private classifyActivity(features: FeatureVector): ActivityState {
const { energy, stdDev, zeroCrossingRate, dominantFreq, gyroEnergy } = features;
// 决策树分类
if (energy < CLASSIFY_THRESHOLDS.stillMaxEnergy && stdDev < CLASSIFY_THRESHOLDS.stillMaxStdDev) {
return ActivityState.STILL;
}
if (energy < CLASSIFY_THRESHOLDS.tinyMaxEnergy && zeroCrossingRate < CLASSIFY_THRESHOLDS.tinyMaxZeroCrossing) {
return ActivityState.TINY;
}
if (energy > CLASSIFY_THRESHOLDS.intenseMinEnergy) {
return ActivityState.INTENSE;
}
if (dominantFreq >= CLASSIFY_THRESHOLDS.runMinFreq &&
dominantFreq <= CLASSIFY_THRESHOLDS.runMaxFreq &&
energy >= CLASSIFY_THRESHOLDS.runMinEnergy) {
return ActivityState.RUN;
}
if (dominantFreq >= CLASSIFY_THRESHOLDS.walkMinFreq &&
dominantFreq <= CLASSIFY_THRESHOLDS.walkMaxFreq) {
return ActivityState.WALK;
}
// 默认:根据能量判断
if (energy > 1.0) {
return ActivityState.RUN;
} else if (energy > 0.3) {
return ActivityState.WALK;
} else {
return ActivityState.TINY;
}
}
/**
* 状态机平滑过滤(防抖)
*/
private smoothStateTransition(newState: ActivityState, timestamp: number): void {
// 状态未变化
if (newState === this.currentActivity) {
this.pendingState = newState;
this.pendingCount = 0;
this.confidence = 1.0;
return;
}
// 与待确认状态一致,增加计数
if (newState === this.pendingState) {
this.pendingCount++;
} else {
// 新状态,重置计数
this.pendingState = newState;
this.pendingCount = 1;
}
// 计算置信度
this.confidence = Math.min(this.pendingCount / STATE_MACHINE_CONFIG.confirmationCount, 1.0);
// 满足确认条件
if (this.pendingCount >= STATE_MACHINE_CONFIG.confirmationCount) {
// 检查最短持续时间
const durationSinceLastChange = timestamp - this.lastStateChangeTime;
if (durationSinceLastChange >= STATE_MACHINE_CONFIG.minStateDuration ||
this.currentActivity === ActivityState.STILL) {
// 触发状态变更
const event: ActivityChangeEvent = {
previousState: this.currentActivity,
currentState: newState,
confidence: this.confidence,
timestamp: timestamp,
};
this.currentActivity = newState;
this.lastStateChangeTime = timestamp;
this.pendingCount = 0;
this.onStateChangeCallback?.(event);
}
}
}
/**
* 重置分类器
*/
reset(): void {
this.currentActivity = ActivityState.STILL;
this.confidence = 0;
this.pendingState = ActivityState.STILL;
this.pendingCount = 0;
this.windowBuffer = [];
this.gyroEnergyBuffer = [];
}
}
3.3 运动检测完整界面
实现实时运动检测界面,展示活动状态、传感器波形和状态历史:
// MotionDetectPage.ets — 运动检测完整界面
import { MultiSensorManager } from './MultiSensorManager';
import { ActivityClassifier, ActivityState, ActivityChangeEvent } from './ActivityClassifier';
// 活动状态显示配置
interface ActivityDisplayConfig {
label: string;
icon: string;
color: string;
bgColor: string;
description: string;
}
const ACTIVITY_CONFIGS: Record<string, ActivityDisplayConfig> = {
[ActivityState.STILL]: {
label: '静止', icon: '🧘', color: '#78909C', bgColor: 'rgba(120,144,156,0.15)',
description: '检测到您当前处于静止状态'
},
[ActivityState.TINY]: {
label: '微动', icon: '👆', color: '#FFB74D', bgColor: 'rgba(255,183,77,0.15)',
description: '检测到轻微活动,可能是坐姿微调'
},
[ActivityState.WALK]: {
label: '步行', icon: '🚶', color: '#4FC3F7', bgColor: 'rgba(79,195,247,0.15)',
description: '检测到步行运动,保持良好节奏'
},
[ActivityState.RUN]: {
label: '跑步', icon: '🏃', color: '#00E676', bgColor: 'rgba(0,230,118,0.15)',
description: '检测到跑步运动,注意控制心率'
},
[ActivityState.INTENSE]: {
label: '剧烈', icon: '⚡', color: '#FF5252', bgColor: 'rgba(255,82,82,0.15)',
description: '检测到剧烈运动,请注意安全'
},
};
@Entry
@Component
struct MotionDetectPage {
// 管理器
private sensorManager: MultiSensorManager = new MultiSensorManager();
private classifier: ActivityClassifier = new ActivityClassifier();
// UI状态
@State currentActivity: ActivityState = ActivityState.STILL;
@State confidence: number = 0;
@State isDetecting: boolean = false;
@State stillDuration: number = 0; // 静止持续时间(分钟)
@State stateHistory: ActivityChangeEvent[] = [];
@State sensorEnergy: number = 0;
// 久坐提醒计时
private stillStartTime: number = 0;
private stillTimerId: number = -1;
private readonly SEDENTARY_THRESHOLD: number = 60; // 60分钟久坐阈值
build() {
Navigation() {
Scroll() {
Column({ space: 20 }) {
// 当前活动状态展示
this.CurrentActivitySection()
// 传感器能量仪表
this.EnergyGaugeSection()
// 久坐提醒卡片
this.SedentaryAlertSection()
// 状态切换历史
this.StateHistorySection()
// 控制按钮
this.ControlSection()
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 40 })
}
.scrollBar(BarState.Off)
}
.title('运动检测')
.titleMode(NavigationTitleMode.Mini)
}
// ===== 当前活动状态展示 =====
@Builder
CurrentActivitySection() {
Column({ space: 16 }) {
// 状态图标与名称
Row({ space: 16 }) {
// 动态图标
Text(ACTIVITY_CONFIGS[this.currentActivity].icon)
.fontSize(48)
.animation({ duration: 300 })
Column({ space: 4 }) {
Text(ACTIVITY_CONFIGS[this.currentActivity].label)
.fontSize(28)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
// 置信度进度条
Row({ space: 8 }) {
Text('置信度')
.fontSize(11)
.fontColor('#78909C')
Progress({ value: this.confidence * 100, total: 100, type: ProgressType.Linear })
.width(120)
.height(6)
.color(ACTIVITY_CONFIGS[this.currentActivity].color)
.backgroundColor('rgba(255,255,255,0.1)')
Text(`${Math.round(this.confidence * 100)}%`)
.fontSize(11)
.fontColor(ACTIVITY_CONFIGS[this.currentActivity].color)
}
}
Blank()
// 检测状态指示
if (this.isDetecting) {
Circle({ width: 10, height: 10 })
.fill('#00E676')
.animation({ duration: 500, iterations: -1, curve: Curve.EaseInOut })
}
}
.width('100%')
// 状态描述
Text(ACTIVITY_CONFIGS[this.currentActivity].description)
.fontSize(13)
.fontColor('#B0BEC5')
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor(ACTIVITY_CONFIGS[this.currentActivity].bgColor)
}
.width('100%')
.padding(20)
.borderRadius(20)
.backgroundColor('rgba(15, 52, 96, 0.6)')
.backdropBlur(20)
}
// ===== 传感器能量仪表 =====
@Builder
EnergyGaugeSection() {
Column({ space: 12 }) {
Text('⚡ 运动能量')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
// 能量条形图
Row({ space: 4 }) {
ForEach([0.08, 0.5, 1.0, 3.0, 5.0], (threshold: number) => {
Column() {
// 能量柱
Column()
.width('100%')
.height(`${Math.min(this.sensorEnergy / threshold * 30, 60)}%`)
.backgroundColor(this.sensorEnergy >= threshold ? '#e94560' : 'rgba(233,69,96,0.2)')
.borderRadius({ topLeft: 4, topRight: 4 })
}
.layoutWeight(1)
.height(60)
.justifyContent(FlexAlign.End)
})
}
.width('100%')
// 阈值标签
Row({ space: 4 }) {
Text('静止')
.fontSize(9)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('微动')
.fontSize(9)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('步行')
.fontSize(9)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('跑步')
.fontSize(9)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('剧烈')
.fontSize(9)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%')
// 当前能量值
Text(`当前能量: ${this.sensorEnergy.toFixed(3)}`)
.fontSize(12)
.fontColor('#78909C')
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 久坐提醒卡片 =====
@Builder
SedentaryAlertSection() {
Column({ space: 12 }) {
Row({ space: 8 }) {
Text('💺')
.fontSize(20)
Text('久坐提醒')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Blank()
Text(`${this.stillDuration}分钟`)
.fontSize(14)
.fontColor(this.stillDuration >= this.SEDENTARY_THRESHOLD ? '#FF5252' : '#78909C')
}
.width('100%')
// 久坐进度条
Progress({
value: Math.min(this.stillDuration, this.SEDENTARY_THRESHOLD),
total: this.SEDENTARY_THRESHOLD,
type: ProgressType.Linear
})
.width('100%')
.height(8)
.color(this.stillDuration >= this.SEDENTARY_THRESHOLD ? '#FF5252' : '#4FC3F7')
.backgroundColor('rgba(255,255,255,0.1)')
if (this.stillDuration >= this.SEDENTARY_THRESHOLD) {
Text('⚠️ 您已久坐超过60分钟,建议起身活动!')
.fontSize(13)
.fontColor('#FF5252')
.fontWeight(FontWeight.Bold)
} else {
Text(`距离久坐提醒还有 ${this.SEDENTARY_THRESHOLD - this.stillDuration} 分钟`)
.fontSize(12)
.fontColor('#546E7A')
}
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 状态切换历史 =====
@Builder
StateHistorySection() {
Column({ space: 12 }) {
Text('📋 状态切换历史')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
if (this.stateHistory.length === 0) {
Text('开始检测后将记录状态变化')
.fontSize(13)
.fontColor('#546E7A')
.width('100%')
.textAlign(TextAlign.Center)
.padding(20)
} else {
// 最近5条记录
ForEach(this.stateHistory.slice(-5).reverse(), (event: ActivityChangeEvent) => {
Row({ space: 12 }) {
// 时间
Text(new Date(event.timestamp).toLocaleTimeString())
.fontSize(11)
.fontColor('#546E7A')
.width(70)
// 状态变更
Text(ACTIVITY_CONFIGS[event.previousState].label)
.fontSize(13)
.fontColor(ACTIVITY_CONFIGS[event.previousState].color)
Text('→')
.fontSize(13)
.fontColor('#546E7A')
Text(ACTIVITY_CONFIGS[event.currentState].label)
.fontSize(13)
.fontColor(ACTIVITY_CONFIGS[event.currentState].color)
.fontWeight(FontWeight.Bold)
Blank()
Text(`${Math.round(event.confidence * 100)}%`)
.fontSize(11)
.fontColor('#78909C')
}
.width('100%')
.padding({ top: 8, bottom: 8 })
})
}
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 控制按钮 =====
@Builder
ControlSection() {
Button(this.isDetecting ? '停止检测' : '开始检测')
.width('100%')
.height(52)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor(this.isDetecting ? '#FF5252' : '#e94560')
.borderRadius(26)
.onClick(() => this.toggleDetection())
}
// ===== 业务逻辑 =====
private toggleDetection(): void {
if (this.isDetecting) {
this.stopDetection();
} else {
this.startDetection();
}
}
private startDetection(): void {
this.isDetecting = true;
this.stillStartTime = Date.now();
// 设置状态变更回调
this.classifier.setOnStateChangeListener((event: ActivityChangeEvent) => {
this.currentActivity = event.currentState;
this.confidence = event.confidence;
this.stateHistory.push(event);
// 静止状态计时
if (event.currentState === ActivityState.STILL) {
this.stillStartTime = Date.now();
this.startStillTimer();
} else {
this.stopStillTimer();
this.stillDuration = 0;
}
});
// 启动多传感器
this.sensorManager.startSensors(
(frame) => {
this.classifier.processFrame(frame);
// 更新能量显示
this.sensorEnergy = frame.linearAccelMagnitude * frame.linearAccelMagnitude;
},
(error) => {
console.error(`[MotionDetect] 传感器错误: ${error.message}`);
}
);
}
private stopDetection(): void {
this.isDetecting = false;
this.sensorManager.stopSensors();
this.stopStillTimer();
}
private startStillTimer(): void {
this.stopStillTimer();
this.stillTimerId = setInterval(() => {
this.stillDuration = Math.floor((Date.now() - this.stillStartTime) / 60000);
}, 60000) as number;
}
private stopStillTimer(): void {
if (this.stillTimerId !== -1) {
clearInterval(this.stillTimerId);
this.stillTimerId = -1;
}
}
aboutToDisappear(): void {
this.sensorManager.stopSensors();
this.stopStillTimer();
}
}
四、踩坑与注意事项
4.1 陀螺仪权限问题
陀螺仪传感器需要额外权限声明:
{
"requestPermissions": [
{
"name": "ohos.permission.ACCELEROMETER",
"reason": "$string:accelerometer_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.GYROSCOPE",
"reason": "$string:gyroscope_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
}
]
}
⚠️ 注意:部分低端设备可能不支持陀螺仪传感器,订阅前应先检查传感器可用性:
// 检查传感器是否可用
import sensor from '@ohos.sensor';
const isGyroAvailable = sensor.isSensorSupported(sensor.SensorId.GYROSCOPE);
if (!isGyroAvailable) {
console.warn('[MotionDetect] 陀螺仪不可用,将使用加速度计降级方案');
}
4.2 传感器数据时间对齐
多传感器数据的时间戳可能不完全同步,需要处理时间对齐:
// ❌ 错误:直接使用不同传感器的时间戳
const accelTime = accelData.timestamp;
const gyroTime = gyroData.timestamp;
// 两者可能相差数十毫秒
// ✅ 正确:以主传感器时间为基准,使用最近一次的辅助传感器数据
// MultiSensorManager中的tryEmitFrame方法已实现此逻辑
4.3 自相关法频率估计的局限性
本文使用的自相关法是一种简化的频率估计方法,存在以下局限:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 基频倍频 | 自相关在2倍周期处也有峰值 | 搜索最小lag的峰值 |
| 低频分辨率差 | 窗口长度限制 | 增加窗口到4秒 |
| 多频率信号 | 复合运动(如跑步+手臂摆动) | 使用FFT进行频谱分析 |
4.4 状态机防抖参数调优
状态机平滑参数需要根据实际场景调优:
- minStateDuration:过短会导致频繁切换,过长会延迟状态检测
- confirmationCount:过少会误判,过多会延迟响应
- 推荐配置:日常监测2秒/3次,运动场景1秒/2次
4.5 功耗优化
多传感器同时订阅功耗较高,建议:
- 静止状态降低采样率:检测到静止后,将采样率从50Hz降至10Hz
- 动态启停传感器:静止时只保留加速度计,运动时再启动陀螺仪
- 使用系统活动识别API:HarmonyOS提供
@ohos.activityRecognition系统级API,功耗更低
五、HarmonyOS 6适配
5.1 系统级活动识别API
HarmonyOS 6增强了系统级活动识别能力:
// HarmonyOS 6:使用系统活动识别服务
import activityRecognition from '@ohos.activityRecognition';
// 订阅活动状态变更
activityRecognition.on('activityChange', {
minInterval: 5000, // 最小回调间隔5秒
activities: [
activityRecognition.ActivityType.STILL,
activityRecognition.ActivityType.WALKING,
activityRecognition.ActivityType.RUNNING,
activityRecognition.ActivityType.IN_VEHICLE,
activityRecognition.ActivityType.ON_BICYCLE,
]
}, (activities: activityRecognition.ActivityRecognitionResult[]) => {
for (const activity of activities) {
console.info(`活动类型: ${activity.type}, 置信度: ${activity.confidence}`);
}
});
5.2 传感器融合框架增强
HarmonyOS 6提供了更高级的传感器融合API:
// HarmonyOS 6:传感器融合数据直接获取
import sensor from '@ohos.sensor';
// 直接获取线性加速度(系统级融合,无需手动去重力)
sensor.on(sensor.SensorId.LINEAR_ACCELEROMETER, (data) => {
// data.x, data.y, data.z 已去除重力分量
const magnitude = Math.sqrt(data.x * data.x + data.y * data.y + data.z * data.z);
console.info(`线性加速度: ${magnitude}`);
}, { interval: 20_000_000 });
// 直接获取旋转向量(系统级姿态融合)
sensor.on(sensor.SensorId.ROTATION_VECTOR, (data) => {
// 四元数表示的设备姿态
console.info(`旋转: x=${data.x}, y=${data.y}, z=${data.z}, w=${data.w}`);
}, { interval: 20_000_000 });
5.3 端侧AI推理加速
HarmonyOS 6支持NNAPI端侧推理,可用于运动分类模型部署:
// HarmonyOS 6:使用端侧AI进行运动分类
import mindSpore from '@ohos.ai.mindSpore';
// 加载预训练的运动分类模型
const model = await mindSpore.loadModelFromFile('models/activity_classify.ms');
// 使用传感器特征向量进行推理
const inputTensor = mindSpore.createTensor(features, [1, 6]); // 6维特征
const output = model.predict([inputTensor]);
const predictedActivity = output[0].getData()[0]; // 分类结果
六、总结
本文完整实现了基于HarmonyOS多传感器融合的运动检测与活动状态识别系统,核心要点如下:
| 模块 | 关键技术 | 要点 |
|---|---|---|
| 多传感器融合 | 加速度+陀螺仪+重力 | 以加速度为主时钟源,时间对齐 |
| 特征提取 | 滑动窗口+时频域特征 | 能量/过零率/主频率多维特征 |
| 活动分类 | 决策树分类器 | 阈值分层判定,优先低能量状态 |
| 状态平滑 | 状态机防抖 | 确认次数+最短持续时间双重约束 |
| 久坐提醒 | 静止计时 | 60分钟阈值,状态切换自动重置 |
最佳实践建议:
- 优先使用系统级活动识别API(
@ohos.activityRecognition),功耗更低、精度更高 - 自定义分类场景使用多传感器融合,可获得更细粒度的运动状态
- 自相关法适合实时场景,离线分析建议使用FFT获取更精确的频谱
- 状态机防抖参数需根据场景调优,运动场景要求更快响应
- 功耗优化是关键,静止时降低采样率,运动时动态启停传感器
下一篇文章将深入讲解跌倒检测与紧急求助,实现基于加速度突变的跌倒识别和自动报警机制。
- 点赞
- 收藏
- 关注作者
评论(0)