
鸿蒙跨端姿势提醒系统开发指南 原创
鸿蒙跨端姿势提醒系统开发指南
一、项目概述
本文基于HarmonyOS的姿态识别能力和分布式技术,开发一款智能姿势提醒系统。该系统能够通过设备摄像头或传感器检测用户坐姿,在发现不良姿势时向多设备发送提醒,借鉴了《鸿蒙跨端U同步》中多设备数据同步的技术原理。
二、系统架构
±--------------------+ ±--------------------+ ±--------------------+
主设备 <-----> 分布式数据总线 <-----> 从设备
(手机/平板) (Distributed Bus) (智能手表/其他设备)
±---------±---------+ ±---------±---------+ ±---------±---------+
±---------v----------+ ±---------v----------+ ±---------v----------+
姿态检测模块 提醒管理模块 数据同步模块
(Posture Detect) (Alert Manager) (Data Sync)
±--------------------+ ±--------------------+ ±--------------------+
三、核心代码实现
姿势检测服务
// src/main/ets/service/PostureService.ts
import { distributedData } from ‘@ohos.data.distributedData’;
import { BusinessError } from ‘@ohos.base’;
import { camera } from ‘@ohos.multimedia.camera’;
import { image } from ‘@ohos.multimedia.image’;
import { poseDetection } from ‘@ohos.ai.poseDetection’;
import { sensor } from ‘@ohos.sensor’;
interface PostureData {
postureType: ‘good’ ‘bad’
‘unknown’;
confidence: number;
timestamp: number;
joints?: {
[key: string]: {
x: number;
y: number;
score: number;
};
interface PostureAlert {
type: ‘slouching’ ‘leaning’ ‘head_down’
‘cross_legs’;
timestamp: number;
duration: number;
severity: ‘low’ ‘medium’
‘high’;
export class PostureService {
private static instance: PostureService;
private kvStore: distributedData.KVStore | null = null;
private readonly STORE_ID = ‘posture_data_store’;
private cameraInput: camera.CameraInput | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private poseDetector: poseDetection.PoseDetector | null = null;
private motionSensor: sensor.Sensor | null = null;
private currentPosture: PostureData = {
postureType: ‘unknown’,
confidence: 0,
timestamp: Date.now()
};
private postureAlerts: PostureAlert[] = [];
private constructor() {
this.initKVStore();
this.initCamera();
this.initPoseDetector();
this.initMotionSensor();
public static getInstance(): PostureService {
if (!PostureService.instance) {
PostureService.instance = new PostureService();
return PostureService.instance;
private async initKVStore(): Promise<void> {
try {
const options: distributedData.KVManagerConfig = {
bundleName: 'com.example.posture',
userInfo: {
userId: '0',
userType: distributedData.UserType.SAME_USER_ID
};
const kvManager = distributedData.createKVManager(options);
this.kvStore = await kvManager.getKVStore({
storeId: this.STORE_ID,
options: {
createIfMissing: true,
encrypt: false,
backup: false,
autoSync: true,
kvStoreType: distributedData.KVStoreType.SINGLE_VERSION
});
this.kvStore.on('dataChange', distributedData.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (data) => {
data.insertEntries.forEach((entry: distributedData.Entry) => {
if (entry.key === 'posture_data') {
this.notifyPostureChange(entry.value.value as PostureData);
else if (entry.key === ‘posture_alerts’) {
this.notifyAlertsChange(entry.value.value as PostureAlert[]);
});
});
catch (e) {
console.error(Failed to initialize KVStore. Code: {e.code}, message: {e.message});
}
private async initCamera(): Promise<void> {
try {
const cameraManager = camera.getCameraManager();
const cameras = cameraManager.getSupportedCameras();
if (cameras.length === 0) {
console.error(‘No camera available’);
return;
// 使用前置摄像头
this.cameraInput = cameraManager.createCameraInput(cameras[0]);
await this.cameraInput.open();
// 创建预览输出
const surfaceId = 'previewSurface';
this.previewOutput = cameraManager.createPreviewOutput(surfaceId);
// 创建会话并开始预览
const captureSession = cameraManager.createCaptureSession();
await captureSession.beginConfig();
await captureSession.addInput(this.cameraInput);
await captureSession.addOutput(this.previewOutput);
await captureSession.commitConfig();
await captureSession.start();
catch (e) {
console.error(Failed to initialize camera. Code: {e.code}, message: {e.message});
}
private async initPoseDetector(): Promise<void> {
try {
const config: poseDetection.PoseDetectorConfig = {
modelType: poseDetection.ModelType.LITE,
scoreThreshold: 0.5,
nmsRadius: 20
};
this.poseDetector = await poseDetection.createPoseDetector(config);
catch (e) {
console.error(Failed to initialize pose detector. Code: {e.code}, message: {e.message});
}
private async initMotionSensor(): Promise<void> {
try {
this.motionSensor = await sensor.getDefaultSensor(sensor.SensorType.SENSOR_TYPE_ACCELEROMETER);
this.motionSensor.on('change', (data: sensor.AccelerometerResponse) => {
this.detectPostureFromMotion(data);
});
await this.motionSensor.setInterval(1000); // 1秒采样一次
catch (e) {
console.error(Failed to initialize motion sensor. Code: {e.code}, message: {e.message});
}
private async detectPostureFromImage(imageObj: image.Image): Promise<void> {
if (!this.poseDetector) return;
try {
const result = await this.poseDetector.detect(imageObj);
this.analyzePose(result);
imageObj.release();
catch (e) {
console.error(Failed to detect pose. Code: {e.code}, message: {e.message});
}
private detectPostureFromMotion(data: sensor.AccelerometerResponse): void {
// 简化的姿势检测逻辑 (基于加速度计数据)
const x = data.x;
const y = data.y;
const z = data.z;
// 计算设备倾斜角度
const angleX = Math.atan2(y, z) * (180 / Math.PI);
const angleY = Math.atan2(x, z) * (180 / Math.PI);
let postureType: 'good' 'bad'
‘unknown’ = ‘unknown’;
let confidence = 0;
// 简单的角度判断
if (Math.abs(angleX) > 30 || Math.abs(angleY) > 30) {
postureType = 'bad';
confidence = 0.7;
else {
postureType = 'good';
confidence = 0.8;
this.currentPosture = {
postureType,
confidence,
timestamp: Date.now()
};
this.checkForBadPosture();
this.scheduleSync();
private analyzePose(pose: poseDetection.Pose): void {
if (!pose.keypoints || pose.keypoints.length === 0) return;
// 获取关键点
const nose = pose.keypoints.find(k => k.name === 'nose');
const leftShoulder = pose.keypoints.find(k => k.name === 'left_shoulder');
const rightShoulder = pose.keypoints.find(k => k.name === 'right_shoulder');
if (!nose |!leftShoulder
| !rightShoulder) return;
// 计算肩膀中点
const shoulderMidX = (leftShoulder.x + rightShoulder.x) / 2;
const shoulderMidY = (leftShoulder.y + rightShoulder.y) / 2;
// 计算鼻子与肩膀中点的垂直距离
const verticalDistance = nose.y - shoulderMidY;
const horizontalDistance = Math.abs(nose.x - shoulderMidX);
let postureType: 'good' 'bad'
‘unknown’ = ‘unknown’;
let confidence = 0;
// 简化的姿势判断逻辑
if (verticalDistance < 0 || horizontalDistance > verticalDistance * 0.5) {
postureType = 'bad';
confidence = pose.score * 0.8;
else {
postureType = 'good';
confidence = pose.score * 0.9;
this.currentPosture = {
postureType,
confidence,
timestamp: Date.now(),
joints: pose.keypoints.reduce((acc, k) => {
acc[k.name] = { x: k.x, y: k.y, score: k.score };
return acc;
}, {})
};
this.checkForBadPosture();
this.scheduleSync();
private checkForBadPosture(): void {
if (this.currentPosture.postureType !== 'bad') return;
const now = Date.now();
const lastAlert = this.postureAlerts[this.postureAlerts.length - 1];
// 判断不良姿势类型
let alertType: PostureAlert['type'] = 'slouching';
let severity: PostureAlert['severity'] = 'low';
// 简化的严重程度判断
if (this.currentPosture.confidence > 0.7) {
severity = 'high';
else if (this.currentPosture.confidence > 0.5) {
severity = 'medium';
// 合并连续的相同类型提醒
if (lastAlert && lastAlert.type === alertType && now - lastAlert.timestamp < 10000) {
lastAlert.duration = now - lastAlert.timestamp;
lastAlert.severity = severity;
else {
this.postureAlerts.push({
type: alertType,
timestamp: now,
duration: 0,
severity
});
// 触发提醒
this.triggerAlert(alertType, severity);
private triggerAlert(type: PostureAlert[‘type’], severity: PostureAlert[‘severity’]): void {
// 实际应用中应该使用通知服务
console.log(Posture alert: {type} ({severity}));
// 同步提醒状态
if (this.kvStore) {
this.kvStore.put('posture_alert', {
value: {
type,
severity,
timestamp: Date.now()
});
}
private scheduleSync(): void {
if (this.syncTimer) {
clearTimeout(this.syncTimer);
this.syncTimer = setTimeout(() => {
this.syncData();
this.syncTimer = null;
}, 2000); // 2秒内多次更新只同步一次
private syncTimer: number | null = null;
private async syncData(): Promise<void> {
if (!this.kvStore) return;
try {
await this.kvStore.put('posture_data', { value: this.currentPosture });
await this.kvStore.put('posture_alerts', { value: this.postureAlerts });
catch (e) {
console.error(Failed to sync data. Code: {e.code}, message: {e.message});
}
private notifyPostureChange(newPosture: PostureData): void {
// 使用时间戳解决冲突 - 保留最新的姿势数据
if (newPosture.timestamp > this.currentPosture.timestamp) {
this.currentPosture = newPosture;
}
private notifyAlertsChange(newAlerts: PostureAlert[]): void {
// 合并新旧提醒
const mergedAlerts = […this.postureAlerts];
newAlerts.forEach(newAlert => {
const existingIndex = mergedAlerts.findIndex(a =>
a.type === newAlert.type &&
Math.abs(a.timestamp - newAlert.timestamp) < 5000
);
if (existingIndex >= 0) {
if (newAlert.timestamp > mergedAlerts[existingIndex].timestamp) {
mergedAlerts[existingIndex] = newAlert;
} else {
mergedAlerts.push(newAlert);
});
this.postureAlerts = mergedAlerts;
public async getCurrentPosture(): Promise<PostureData> {
if (!this.kvStore) return this.currentPosture;
try {
const entry = await this.kvStore.get('posture_data');
return entry?.value || this.currentPosture;
catch (e) {
console.error(Failed to get posture data. Code: {e.code}, message: {e.message});
return this.currentPosture;
}
public async getRecentAlerts(): Promise<PostureAlert[]> {
if (!this.kvStore) return this.postureAlerts;
try {
const entry = await this.kvStore.get('posture_alerts');
return entry?.value || this.postureAlerts;
catch (e) {
console.error(Failed to get posture alerts. Code: {e.code}, message: {e.message});
return this.postureAlerts;
}
public async destroy(): Promise<void> {
if (this.kvStore) {
this.kvStore.off(‘dataChange’);
if (this.cameraInput) {
await this.cameraInput.close();
if (this.poseDetector) {
this.poseDetector.release();
if (this.motionSensor) {
this.motionSensor.off('change');
}
姿势监测组件
// src/main/ets/components/PostureMonitor.ets
@Component
export struct PostureMonitor {
private postureService = PostureService.getInstance();
@State currentPosture: PostureData = {
postureType: ‘unknown’,
confidence: 0,
timestamp: 0
};
@State alerts: PostureAlert[] = [];
@State previewSurfaceId: string = ‘previewSurface’;
aboutToAppear(): void {
this.loadPostureData();
private async loadPostureData(): Promise<void> {
this.currentPosture = await this.postureService.getCurrentPosture();
this.alerts = await this.postureService.getRecentAlerts();
build() {
Stack() {
// 摄像头预览
CameraPreview({ surfaceId: this.previewSurfaceId })
.width('100%')
.height('100%');
// 姿势状态覆盖层
Column() {
// 姿势状态指示器
Row() {
Circle()
.width(20)
.height(20)
.fillColor(this.getPostureColor(this.currentPosture.postureType))
.margin({ right: 10 });
Text(this.getPostureText(this.currentPosture.postureType))
.fontSize(18)
.fontColor('#FFFFFF');
.padding(10)
.backgroundColor('#88000000')
.borderRadius(20)
.margin({ bottom: 20 });
// 姿势关键点可视化
if (this.currentPosture.joints) {
this.buildPoseSkeleton();
// 提醒通知
if (this.alerts.length > 0) {
this.buildAlertsDisplay();
}
.width('100%')
.height('100%')
.padding(20)
.width(‘100%’)
.height('100%')
.onAppear(() => {
this.postureService.getCurrentPosture().then((posture) => {
this.currentPosture = posture;
});
this.postureService.getRecentAlerts().then((alerts) => {
this.alerts = alerts;
});
});
@Builder
private buildPoseSkeleton() {
Canvas({ context: new CanvasRenderingContext2D() })
.width(‘100%’)
.height(‘100%’)
.onReady((ctx: CanvasRenderingContext2D) => {
ctx.strokeStyle = ‘#FF4081’;
ctx.lineWidth = 3;
// 绘制关键点之间的连线
const joints = this.currentPosture.joints;
if (!joints) return;
// 头部连线
if (joints['nose'] && joints['left_eye'] && joints['right_eye']) {
ctx.beginPath();
ctx.moveTo(joints['left_eye'].x, joints['left_eye'].y);
ctx.lineTo(joints['nose'].x, joints['nose'].y);
ctx.lineTo(joints['right_eye'].x, joints['right_eye'].y);
ctx.stroke();
// 肩膀连线
if (joints['left_shoulder'] && joints['right_shoulder']) {
ctx.beginPath();
ctx.moveTo(joints['left_shoulder'].x, joints['left_shoulder'].y);
ctx.lineTo(joints['right_shoulder'].x, joints['right_shoulder'].y);
ctx.stroke();
// 左臂连线
if (joints['left_shoulder'] && joints['left_elbow'] && joints['left_wrist']) {
ctx.beginPath();
ctx.moveTo(joints['left_shoulder'].x, joints['left_shoulder'].y);
ctx.lineTo(joints['left_elbow'].x, joints['left_elbow'].y);
ctx.lineTo(joints['left_wrist'].x, joints['left_wrist'].y);
ctx.stroke();
// 右臂连线
if (joints['right_shoulder'] && joints['right_elbow'] && joints['right_wrist']) {
ctx.beginPath();
ctx.moveTo(joints['right_shoulder'].x, joints['right_shoulder'].y);
ctx.lineTo(joints['right_elbow'].x, joints['right_elbow'].y);
ctx.lineTo(joints['right_wrist'].x, joints['right_wrist'].y);
ctx.stroke();
// 绘制关键点
Object.values(joints).forEach(joint => {
if (joint.score > 0.3) {
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.arc(joint.x, joint.y, 5, 0, 2 * Math.PI);
ctx.fill();
});
});
@Builder
private buildAlertsDisplay() {
Column() {
Text(‘姿势提醒’)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(‘#FFFFFF’)
.margin({ bottom: 10 });
ForEach(this.alerts.slice(0, 3), (alert) => {
Row() {
Image($r('app.media.ic_warning'))
.width(20)
.height(20)
.margin({ right: 10 });
Text(this.getAlertText(alert.type))
.fontSize(16)
.fontColor('#FFFFFF')
.layoutWeight(1);
Text(${Math.floor(alert.duration / 1000)}秒)
.fontSize(14)
.fontColor('#FFC107');
.padding(10)
.backgroundColor('#88000000')
.borderRadius(10)
.margin({ bottom: 10 });
})
.width(‘80%’)
.padding(15)
.backgroundColor('#88000000')
.borderRadius(15)
.position({ x: '10%', y: '70%' });
private getPostureColor(type: ‘good’ ‘bad’
‘unknown’): string {
switch (type) {
case 'good': return '#4CAF50';
case 'bad': return '#F44336';
default: return '#FFC107';
}
private getPostureText(type: ‘good’ ‘bad’
‘unknown’): string {
switch (type) {
case ‘good’: return ‘姿势良好’;
case ‘bad’: return ‘不良姿势’;
default: return ‘检测中…’;
}
private getAlertText(type: PostureAlert[‘type’]): string {
switch (type) {
case ‘slouching’: return ‘驼背警告’;
case ‘leaning’: return ‘身体倾斜’;
case ‘head_down’: return ‘低头警告’;
case ‘cross_legs’: return ‘翘二郎腿’;
default: return ‘姿势问题’;
}
主界面实现
// src/main/ets/pages/PosturePage.ets
import { PostureService } from ‘…/service/PostureService’;
import { PostureMonitor } from ‘…/components/PostureMonitor’;
@Entry
@Component
struct PosturePage {
@State activeTab: number = 0;
@State deviceList: string[] = [];
private postureService = PostureService.getInstance();
build() {
Column() {
// 标题
Text(‘智能姿势提醒’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 标签页
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
// 实时监测标签页
PostureMonitor()
.tabBar(‘实时监测’);
TabContent() {
// 历史记录标签页
this.buildHistoryTab()
.tabBar(‘历史记录’);
TabContent() {
// 设备管理标签页
this.buildDevicesTab()
.tabBar(‘设备管理’);
.barWidth(‘100%’)
.barHeight(50)
.width('100%')
.height('80%')
.width(‘100%’)
.height('100%')
.padding(20)
.onAppear(() => {
// 模拟获取设备列表
setTimeout(() => {
this.deviceList = ['我的手机', '智能手表', '平板电脑'];
}, 1000);
});
@Builder
private buildHistoryTab() {
Column() {
Text(‘姿势历史记录’)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
// 模拟历史数据
List({ space: 15 }) {
ListItem() {
this.buildHistoryItem('驼背警告', '高', '10分钟前', '持续2分钟');
ListItem() {
this.buildHistoryItem('低头警告', '中', '30分钟前', '持续1分钟');
ListItem() {
this.buildHistoryItem('身体倾斜', '低', '1小时前', '持续30秒');
}
.width('100%')
.layoutWeight(1);
.width(‘100%’)
.height('100%')
.padding(10);
@Builder
private buildHistoryItem(
type: string,
severity: string,
time: string,
duration: string
) {
Row() {
Column() {
Text(type)
.fontSize(16)
.fontWeight(FontWeight.Bold);
Text(严重程度: ${severity})
.fontSize(14)
.fontColor('#666666')
.margin({ top: 5 });
.layoutWeight(1);
Column() {
Text(time)
.fontSize(14)
.fontColor('#666666');
Text(duration)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 5 });
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(10);
@Builder
private buildDevicesTab() {
Column() {
Text(‘已连接设备’)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 });
if (this.deviceList.length > 0) {
List({ space: 15 }) {
ForEach(this.deviceList, (device) => {
ListItem() {
Row() {
Image($r('app.media.ic_device'))
.width(40)
.height(40)
.margin({ right: 15 });
Text(device)
.fontSize(16)
.layoutWeight(1);
if (device === '我的手机') {
Text('主设备')
.fontSize(14)
.fontColor('#4CAF50');
}
.width('100%')
.padding(15)
.backgroundColor('#FFFFFF')
.borderRadius(10)
})
.width(‘100%’)
.layoutWeight(1);
else {
Text('没有连接的设备')
.fontSize(16)
.fontColor('#666666')
.margin({ top: 50 });
Button(‘添加设备’)
.type(ButtonType.Capsule)
.width('80%')
.margin({ top: 30 })
.backgroundColor('#2196F3')
.fontColor('#FFFFFF');
.width(‘100%’)
.height('100%')
.padding(10);
}
四、与游戏同步技术的结合点
分布式状态同步:借鉴游戏中多玩家状态同步机制,实现姿势数据的跨设备实时同步
实时数据处理:类似游戏中的实时数据流,处理传感器和摄像头数据
设备角色分配:类似游戏中的主机/客户端角色,确定主监测设备和从属设备
时间同步机制:确保多设备间的时间戳一致,类似游戏中的时间同步
数据压缩传输:优化姿势数据的传输效率,类似游戏中的网络优化
五、关键特性实现
姿势检测:
private analyzePose(pose: poseDetection.Pose): void {
const nose = pose.keypoints.find(k => k.name === 'nose');
const shoulders = [
pose.keypoints.find(k => k.name === 'left_shoulder'),
pose.keypoints.find(k => k.name === 'right_shoulder')
];
if (nose && shoulders.every(s => s)) {
const shoulderMidY = (shoulders[0].y + shoulders[1].y) / 2;
this.currentPosture = {
postureType: nose.y > shoulderMidY ? 'bad' : 'good',
confidence: pose.score,
timestamp: Date.now()
};
}
提醒触发:
private checkForBadPosture(): void {
if (this.currentPosture.postureType === 'bad') {
const now = Date.now();
const lastAlert = this.alerts[this.alerts.length - 1];
if (lastAlert && now - lastAlert.timestamp < 10000) {
lastAlert.duration = now - lastAlert.timestamp;
else {
this.alerts.push({
type: this.detectAlertType(),
timestamp: now,
duration: 0
});
this.triggerAlert();
}
数据同步:
private async syncData(): Promise<void> {
if (this.kvStore) {
await this.kvStore.put('posture_data', { value: this.currentPosture });
await this.kvStore.put('posture_alerts', { value: this.alerts });
}
冲突解决:
private notifyPostureChange(newPosture: PostureData): void {
if (newPosture.timestamp > this.currentPosture.timestamp) {
this.currentPosture = newPosture;
}
六、性能优化策略
传感器采样间隔:
await this.motionSensor.setInterval(1000); // 1秒采样一次
图像分析节流:
private lastAnalysisTime = 0;
private readonly ANALYSIS_INTERVAL = 2000; // 2秒分析一次
private async analyzeImage(imageObj: image.Image): Promise<void> {
const now = Date.now();
if (now - this.lastAnalysisTime < this.ANALYSIS_INTERVAL) {
imageObj.release();
return;
this.lastAnalysisTime = now;
// 分析逻辑...
批量数据同步:
private scheduleSync(): void {
if (this.syncTimer) clearTimeout(this.syncTimer);
this.syncTimer = setTimeout(() => {
this.syncData();
this.syncTimer = null;
}, 2000); // 2秒内多次更新只同步一次
本地缓存优先:
public async getCurrentPosture(): Promise<PostureData> {
// 先返回本地缓存
const cachedPosture = this.currentPosture;
// 异步从分布式存储获取最新状态
if (this.kvStore) {
this.kvStore.get('posture_data').then((entry) => {
if (entry?.value && entry.value.timestamp > cachedPosture.timestamp) {
this.currentPosture = entry.value;
});
return cachedPosture;
七、项目扩展方向
个性化提醒设置:允许用户自定义提醒频率和方式
姿势矫正指导:提供改善姿势的具体方法和指导
长期趋势分析:统计姿势改善情况
工作休息提醒:结合久坐提醒功能
多场景适配:根据不同场景(办公、学习等)调整检测标准
八、总结
本姿势提醒系统实现了以下核心功能:
基于HarmonyOS传感器和摄像头能力的姿势检测
不良姿势实时识别与提醒
多设备间的状态同步
历史记录查看与分析
通过借鉴游戏中的多设备同步技术,我们构建了一个实用的健康监测工具。该项目展示了HarmonyOS在传感器数据处理和分布式技术方面的强大能力,为开发者提供了健康类应用开发的参考方案。
