HarmonyOS APP开发:表情识别与情感计算

举报
Jack20 发表于 2026/06/21 14:19:58 2026/06/21
【摘要】 HarmonyOS APP开发:表情识别与情感计算核心要点:掌握面部表情识别技术原理,实现情感状态检测与追踪,构建情感驱动的自适应交互系统 一、背景与动机你有没有遇到过这样的情况——正在用一款学习APP背单词,明明已经烦躁得不行了,它还在不停地给你推新词?或者用冥想APP的时候,你已经困得不行了,它还在播放那段催眠引导?这些场景暴露了一个共同的问题:应用完全不知道用户的情绪状态。它只管输出...

HarmonyOS APP开发:表情识别与情感计算

核心要点:掌握面部表情识别技术原理,实现情感状态检测与追踪,构建情感驱动的自适应交互系统


一、背景与动机

你有没有遇到过这样的情况——正在用一款学习APP背单词,明明已经烦躁得不行了,它还在不停地给你推新词?或者用冥想APP的时候,你已经困得不行了,它还在播放那段催眠引导?

这些场景暴露了一个共同的问题:应用完全不知道用户的情绪状态。它只管输出内容,不管你能不能接受。就像一个不会察言观色的朋友,聊天总是不在点上。

表情识别和情感计算,就是让设备学会"察言观色"的技术。通过摄像头捕捉面部表情,分析面部肌肉运动模式,推断出用户当前的情绪状态——开心、悲伤、愤怒、惊讶、恐惧、厌恶,或者就是平静。然后,应用可以根据情绪状态动态调整交互策略。

这不是科幻。在HarmonyOS上,你可以通过人脸检测API获取面部关键点,再结合表情分类模型实现实时情感识别。从学习APP的难度自适应,到音乐APP的心情推荐,再到健康APP的情绪日记——情感计算的应用场景远比你想象的广阔。

当然,隐私是第一位的。所有推理都在端侧完成,面部数据不会离开设备,这是HarmonyOS AI能力的基本原则。


二、核心原理

2.1 面部表情分类体系

情感计算的基础是面部表情分类。最经典的是Ekman的六种基本情绪模型:

flowchart TB
    A[面部表情识别体系] --> B[面部检测]
    A --> C[关键点提取]
    A --> D[表情分类]
    A --> E[情感状态追踪]
    
    B --> B1[人脸定位 BoundingBox]
    B --> B2[人脸对齐 Alignment]
    B --> B3[活体检测 Liveness]
    
    C --> C1[68点面部关键点]
    C --> C2[眉毛/眼睛/鼻子/嘴巴]
    C --> C3[关键点运动向量]
    
    D --> D1[😊 开心 Happy]
    D --> D2[😢 悲伤 Sad]
    D --> D3[😠 愤怒 Angry]
    D --> D4[😲 惊讶 Surprise]
    D --> D5[😨 恐惧 Fear]
    D --> D6[🤢 厌恶 Disgust]
    D --> D7[😐 中性 Neutral]
    
    E --> E1[情绪时间序列]
    E --> E2[情绪变化趋势]
    E --> E3[情绪触发事件]
    E --> E4[情绪恢复周期]
    
    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 info
    class C,C1,C2,C3 warning
    class D,D1,D2,D3,D4,D5,D6,D7 error
    class E,E1,E2,E3,E4 purple

2.2 面部关键点与表情特征

面部表情识别的核心特征来自面部肌肉运动。FACS(面部动作编码系统)定义了44种Action Unit(AU),每种AU对应一组特定的面部肌肉运动:

AU编号 肌肉运动 对应表情
AU1 内眉上扬 惊讶、恐惧
AU4 眉毛下压 愤怒
AU6 颧骨抬起(笑眼) 真笑(Duchenne微笑)
AU12 嘴角上扬 开心
AU15 嘴角下压 悲伤
AU23 嘴唇紧绷 愤怒
AU25 嘴唇分开 惊讶

通过68个面部关键点,我们可以计算这些AU的激活程度。比如AU12(嘴角上扬)可以通过左右嘴角与鼻尖的相对位置变化来量化。

2.3 情感计算流程

从原始图像到情感状态输出,完整流程如下:

  1. 人脸检测 → 定位人脸区域
  2. 关键点提取 → 获取68个面部关键点坐标
  3. 特征计算 → 基于关键点计算AU特征向量
  4. 表情分类 → 将特征向量输入分类模型
  5. 情感追踪 → 对分类结果进行时序平滑和趋势分析

2.4 情感状态平滑

单帧表情分类结果往往不稳定——用户可能只是眨了一下眼就被判定为"惊讶"。因此需要对情感状态进行时序平滑

  • 滑动窗口平均:取最近N帧的分类概率平均值
  • 指数移动平均(EMA):给近期帧更高权重
  • 状态机约束:限制情绪转换的频率,避免频繁跳变

三、代码实战

3.1 面部关键点特征提取器

这个示例展示了如何从68个面部关键点中提取表情特征,计算关键AU的激活程度。

// FacialFeatureExtractor.ets - 面部关键点特征提取器
// 功能:从68个面部关键点计算Action Unit特征向量

export class FacialFeatureExtractor {
  // 上一帧关键点(用于计算运动向量)
  private prevKeypoints: Array<Point> | null = null

  // 提取面部表情特征向量
  // 输入:68个面部关键点坐标
  // 输出:表情特征向量 + 各AU激活度
  extractFeatures(keypoints: Array<Point>): FacialFeatures {
    if (!keypoints || keypoints.length < 68) {
      return this.getDefaultFeatures()
    }

    // 计算面部基准距离(用于归一化,消除距离影响)
    const faceHeight = this.calculateDistance(keypoints[8], keypoints[27])  // 下巴→鼻梁
    const faceWidth = this.calculateDistance(keypoints[0], keypoints[16])   // 左轮廓→右轮廓
    const faceScale = Math.max(faceHeight, faceWidth, 1)  // 防止除零

    // 1. AU12: 嘴角上扬(开心指标)
    // 左嘴角(48)相对鼻尖(30)的Y偏移,右嘴角(54)同理
    const leftMouthCornerUp = (keypoints[30].y - keypoints[48].y) / faceScale
    const rightMouthCornerUp = (keypoints[30].y - keypoints[54].y) / faceScale
    const au12 = (leftMouthCornerUp + rightMouthCornerUp) / 2

    // 2. AU6: 颧骨抬起(真笑指标)
    // 眼角外扩程度:左眼角(36→45距离变化)
    const leftEyeWidth = this.calculateDistance(keypoints[36], keypoints[39])
    const rightEyeWidth = this.calculateDistance(keypoints[42], keypoints[45])
    const avgEyeWidth = (leftEyeWidth + rightEyeWidth) / 2
    const au6 = avgEyeWidth / faceScale

    // 3. AU4: 眉毛下压(愤怒指标)
    // 左眉内端(22)与左眼上缘(38)的距离
    const leftBrowEyeDist = (keypoints[38].y - keypoints[22].y) / faceScale
    const rightBrowEyeDist = (keypoints[43].y - keypoints[27]) / faceScale
    const au4 = -(leftBrowEyeDist + rightBrowEyeDist) / 2  // 负值表示下压

    // 4. AU1: 内眉上扬(惊讶指标)
    // 左眉内端(22)相对鼻梁(27)的Y偏移
    const leftBrowUp = (keypoints[27].y - keypoints[22].y) / faceScale
    const rightBrowUp = (keypoints[27].y - keypoints[27]) / faceScale  // 简化
    const au1 = leftBrowUp

    // 5. AU15: 嘴角下压(悲伤指标)
    const au15 = -au12  // 与AU12相反

    // 6. AU25: 嘴唇分开(惊讶指标)
    // 上唇(51)与下唇(57)的距离
    const mouthOpenness = this.calculateDistance(keypoints[51], keypoints[57]) / faceScale
    const au25 = mouthOpenness

    // 7. 嘴巴宽度变化
    const mouthWidth = this.calculateDistance(keypoints[48], keypoints[54]) / faceScale

    // 8. 运动向量(与上一帧的差异)
    let motionVector: Array<number> = []
    if (this.prevKeypoints) {
      for (let i = 0; i < 68; i++) {
        const dx = keypoints[i].x - this.prevKeypoints[i].x
        const dy = keypoints[i].y - this.prevKeypoints[i].y
        motionVector.push(Math.sqrt(dx * dx + dy * dy) / faceScale)
      }
    }

    // 保存当前帧为下一帧的参考
    this.prevKeypoints = keypoints.map(p => ({ x: p.x, y: p.y }))

    return {
      au1: this.normalizeAU(au1, 0, 0.15),       // 内眉上扬
      au4: this.normalizeAU(au4, -0.1, 0.1),     // 眉毛下压
      au6: this.normalizeAU(au6, 0.2, 0.4),      // 颧骨抬起
      au12: this.normalizeAU(au12, 0, 0.2),      // 嘴角上扬
      au15: this.normalizeAU(au15, -0.2, 0),     // 嘴角下压
      au25: this.normalizeAU(au25, 0, 0.3),      // 嘴唇分开
      mouthWidth: mouthWidth,
      motionIntensity: this.calculateMotionIntensity(motionVector),
      featureVector: [au1, au4, au6, au12, au15, au25, mouthWidth]
    }
  }

  // 归一化AU值到[0, 1]范围
  private normalizeAU(value: number, minVal: number, maxVal: number): number {
    return Math.max(0, Math.min(1, (value - minVal) / (maxVal - minVal)))
  }

  // 计算两点间距离
  private calculateDistance(p1: Point, p2: Point): number {
    return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2))
  }

  // 计算运动强度
  private calculateMotionIntensity(motionVector: Array<number>): number {
    if (motionVector.length === 0) return 0
    const sum = motionVector.reduce((acc, val) => acc + val, 0)
    return sum / motionVector.length
  }

  // 获取默认特征
  private getDefaultFeatures(): FacialFeatures {
    return {
      au1: 0, au4: 0, au6: 0, au12: 0, au15: 0, au25: 0,
      mouthWidth: 0, motionIntensity: 0,
      featureVector: [0, 0, 0, 0, 0, 0, 0]
    }
  }
}

// 面部特征数据结构
export interface FacialFeatures {
  au1: number          // 内眉上扬激活度
  au4: number          // 眉毛下压激活度
  au6: number          // 颧骨抬起激活度
  au12: number         // 嘴角上扬激活度
  au15: number         // 嘴角下压激活度
  au25: number         // 嘴唇分开激活度
  mouthWidth: number   // 嘴巴宽度
  motionIntensity: number  // 运动强度
  featureVector: Array<number>  // 完整特征向量
}

// 点数据结构
export interface Point {
  x: number
  y: number
}

3.2 情感识别引擎:带时序平滑

这个示例实现了基于规则的表情分类器,结合指数移动平均(EMA)实现情感状态的时序平滑。

// EmotionRecognizer.ets - 情感识别引擎
// 功能:基于面部特征的实时情感识别,支持时序平滑和状态追踪

import { FacialFeatures } from './FacialFeatureExtractor'

export class EmotionRecognizer {
  // 七种基本情绪的概率分布
  private emotionProbabilities: EmotionProbabilities = this.getInitialProbabilities()
  // EMA平滑系数(0~1,越大越跟踪当前帧)
  private readonly SMOOTH_FACTOR: number = 0.3
  // 情绪历史记录
  private emotionHistory: Array<EmotionRecord> = []
  // 最大历史长度
  private readonly MAX_HISTORY: number = 300  // 约10秒@30fps
  // 当前主导情绪
  private dominantEmotion: string = 'neutral'
  // 情绪持续时间(帧数)
  private emotionDuration: number = 0

  // 识别情感:输入面部特征,输出情感状态
  recognize(features: FacialFeatures): EmotionState {
    // 基于规则计算各情绪的原始概率
    const rawProbs = this.calculateRawProbabilities(features)

    // EMA时序平滑
    this.applyEMASmoothing(rawProbs)

    // 确定主导情绪
    const newDominant = this.getDominantEmotion()

    // 更新持续时间
    if (newDominant === this.dominantEmotion) {
      this.emotionDuration++
    } else {
      this.dominantEmotion = newDominant
      this.emotionDuration = 1
    }

    // 记录历史
    const record: EmotionRecord = {
      timestamp: Date.now(),
      emotion: this.dominantEmotion,
      probabilities: { ...this.emotionProbabilities },
      duration: this.emotionDuration
    }
    this.emotionHistory.push(record)
    if (this.emotionHistory.length > this.MAX_HISTORY) {
      this.emotionHistory.shift()
    }

    return {
      dominantEmotion: this.dominantEmotion,
      probabilities: { ...this.emotionProbabilities },
      duration: this.emotionDuration,
      confidence: this.emotionProbabilities[this.dominantEmotion] || 0,
      valence: this.calculateValence(),
      arousal: this.calculateArousal()
    }
  }

  // 基于规则计算各情绪的原始概率
  private calculateRawProbabilities(features: FacialFeatures): EmotionProbabilities {
    const probs: EmotionProbabilities = {
      happy: 0, sad: 0, angry: 0, surprise: 0, fear: 0, disgust: 0, neutral: 0
    }

    // 开心:嘴角上扬 + 颧骨抬起 + 嘴巴变宽
    probs.happy = features.au12 * 0.5 + features.au6 * 0.3 + features.mouthWidth * 0.2

    // 悲伤:嘴角下压 + 内眉上扬
    probs.sad = features.au15 * 0.6 + features.au1 * 0.4

    // 愤怒:眉毛下压 + 嘴唇紧绷
    probs.angry = features.au4 * 0.7 + (1 - features.au25) * 0.3

    // 惊讶:内眉上扬 + 嘴唇分开
    probs.surprise = features.au1 * 0.4 + features.au25 * 0.6

    // 恐惧:内眉上扬 + 嘴唇分开 + 运动强度高
    probs.fear = features.au1 * 0.3 + features.au25 * 0.3 + features.motionIntensity * 0.4

    // 厌恶:眉毛下压 + 鼻皱(简化为嘴角不对称)
    probs.disgust = features.au4 * 0.5 + features.au15 * 0.3 + (1 - features.au6) * 0.2

    // 中性:所有AU激活度都很低
    const maxAU = Math.max(features.au1, features.au4, features.au6, features.au12, features.au15, features.au25)
    probs.neutral = Math.max(0, 1 - maxAU * 3)

    // 归一化
    return this.normalizeProbabilities(probs)
  }

  // EMA时序平滑
  private applyEMASmoothing(rawProbs: EmotionProbabilities) {
    const alpha = this.SMOOTH_FACTOR
    for (const key of Object.keys(rawProbs) as Array<keyof EmotionProbabilities>) {
      this.emotionProbabilities[key] =
        alpha * rawProbs[key] + (1 - alpha) * this.emotionProbabilities[key]
    }
    // 重新归一化
    this.emotionProbabilities = this.normalizeProbabilities(this.emotionProbabilities)
  }

  // 获取主导情绪
  private getDominantEmotion(): string {
    let maxProb = 0
    let dominant = 'neutral'
    for (const [emotion, prob] of Object.entries(this.emotionProbabilities)) {
      if (prob > maxProb) {
        maxProb = prob
        dominant = emotion
      }
    }
    return dominant
  }

  // 计算效价(Valence):正面情绪 vs 负面情绪
  private calculateValence(): number {
    const positive = this.emotionProbabilities.happy
    const negative = this.emotionProbabilities.sad + this.emotionProbabilities.angry +
                     this.emotionProbabilities.fear + this.emotionProbabilities.disgust
    return positive - negative  // [-1, 1]
  }

  // 计算唤醒度(Arousal):平静 vs 激动
  private calculateArousal(): number {
    const high = this.emotionProbabilities.angry + this.emotionProbabilities.surprise +
                 this.emotionProbabilities.fear
    const low = this.emotionProbabilities.neutral + this.emotionProbabilities.sad
    return high - low  // [-1, 1]
  }

  // 归一化概率分布
  private normalizeProbabilities(probs: EmotionProbabilities): EmotionProbabilities {
    const sum = Object.values(probs).reduce((a, b) => a + b, 0)
    if (sum < 0.001) {
      return this.getInitialProbabilities()
    }
    const result: EmotionProbabilities = { ...probs }
    for (const key of Object.keys(result) as Array<keyof EmotionProbabilities>) {
      result[key] = result[key] / sum
    }
    return result
  }

  // 获取初始概率分布
  private getInitialProbabilities(): EmotionProbabilities {
    return { happy: 0, sad: 0, angry: 0, surprise: 0, fear: 0, disgust: 0, neutral: 1.0 }
  }

  // 获取情绪历史
  getEmotionHistory(): Array<EmotionRecord> {
    return [...this.emotionHistory]
  }

  // 获取情绪变化趋势
  getEmotionTrend(): EmotionTrend {
    if (this.emotionHistory.length < 10) {
      return { direction: 'stable', rateOfChange: 0 }
    }

    // 比较最近10帧和之前10帧的效价
    const recent = this.emotionHistory.slice(-10)
    const previous = this.emotionHistory.slice(-20, -10)

    const recentValence = recent.reduce((sum, r) => {
      const probs = r.probabilities
      return sum + (probs.happy - probs.sad - probs.angry - probs.fear - probs.disgust)
    }, 0) / 10

    const previousValence = previous.reduce((sum, r) => {
      const probs = r.probabilities
      return sum + (probs.happy - probs.sad - probs.angry - probs.fear - probs.disgust)
    }, 0) / 10

    const rateOfChange = recentValence - previousValence

    return {
      direction: rateOfChange > 0.1 ? 'improving' : rateOfChange < -0.1 ? 'declining' : 'stable',
      rateOfChange: rateOfChange
    }
  }

  // 重置识别状态
  reset() {
    this.emotionProbabilities = this.getInitialProbabilities()
    this.emotionHistory = []
    this.dominantEmotion = 'neutral'
    this.emotionDuration = 0
  }
}

// 情绪概率分布
export interface EmotionProbabilities {
  happy: number
  sad: number
  angry: number
  surprise: number
  fear: number
  disgust: number
  neutral: number
}

// 情感状态
export interface EmotionState {
  dominantEmotion: string      // 主导情绪
  probabilities: EmotionProbabilities  // 各情绪概率
  duration: number             // 持续帧数
  confidence: number           // 置信度
  valence: number              // 效价 [-1, 1]
  arousal: number              // 唤醒度 [-1, 1]
}

// 情绪记录
export interface EmotionRecord {
  timestamp: number
  emotion: string
  probabilities: EmotionProbabilities
  duration: number
}

// 情绪趋势
export interface EmotionTrend {
  direction: 'improving' | 'declining' | 'stable'
  rateOfChange: number
}

3.3 情感自适应阅读器

这个示例将情感识别引擎应用到实际场景中——一个能根据用户情绪状态动态调整内容呈现方式的阅读器应用。

// EmotionAdaptiveReader.ets - 情感自适应阅读器
// 功能:根据用户情绪状态动态调整阅读体验

import { FacialFeatureExtractor, FacialFeatures } from './FacialFeatureExtractor'
import { EmotionRecognizer, EmotionState } from './EmotionRecognizer'

@Entry
@Component
struct EmotionAdaptiveReader {
  // 阅读内容
  @State currentArticle: string = '人工智能正在改变我们的生活方式...'
  @State fontSize: number = 16              // 字体大小
  @State lineHeight: number = 1.8           // 行高
  @State readingSpeed: number = 1.0         // 阅读速度
  @State bgColor: string = '#1a1a2e'        // 背景色
  @State fontColor: string = '#e0e0ff'      // 字体色

  // 情感状态
  @State currentEmotion: string = 'neutral'  // 当前情绪
  @State emotionConfidence: number = 0       // 情绪置信度
  @State valence: number = 0                 // 效价
  @State arousal: number = 0                 // 唤醒度
  @State emotionEmoji: string = '😐'         // 情绪Emoji
  @State emotionTrend: string = '稳定'       // 情绪趋势
  @State adaptationMessage: string = ''      // 适配消息

  // 情绪日记
  @State emotionDiary: Array<DiaryEntry> = []

  // 引擎实例
  private featureExtractor: FacialFeatureExtractor = new FacialFeatureExtractor()
  private emotionRecognizer: EmotionRecognizer = new EmotionRecognizer()
  // 模拟定时器
  private simulationTimer: number = -1
  // 阅读时间(秒)
  private readingTime: number = 0
  private readingTimer: number = -1

  aboutToDisappear() {
    if (this.simulationTimer !== -1) clearInterval(this.simulationTimer)
    if (this.readingTimer !== -1) clearInterval(this.readingTimer)
  }

  build() {
    Column() {
      // 顶部情感状态栏
      Row() {
        // 情绪指示器
        Column() {
          Text(this.emotionEmoji)
            .fontSize(32)
          Text(this.getEmotionLabel(this.currentEmotion))
            .fontSize(12)
            .fontColor('#808090')
        }
        .alignItems(HorizontalAlign.Center)

        // 效价-唤醒度指示
        Column() {
          Row() {
            Text('效价:')
              .fontSize(11)
              .fontColor('#808090')
            Text(this.valence > 0 ? '😊正面' : this.valence < -0.2 ? '😟负面' : '😐中性')
              .fontSize(11)
              .fontColor(this.valence > 0 ? '#10B981' : this.valence < -0.2 ? '#EF4444' : '#F59E0B')
          }
          Row() {
            Text('唤醒:')
              .fontSize(11)
              .fontColor('#808090')
            Text(this.arousal > 0.2 ? '🔥激动' : this.arousal < -0.2 ? '😴平静' : '😐中等')
              .fontSize(11)
              .fontColor(this.arousal > 0.2 ? '#FF6B6B' : this.arousal < -0.2 ? '#06B6D4' : '#F59E0B')
          }
        }
        .margin({ left: 16 })

        Blank()

        // 趋势与适配消息
        Column() {
          Text(`趋势: ${this.emotionTrend}`)
            .fontSize(11)
            .fontColor('#a0a0cc')
          Text(this.adaptationMessage)
            .fontSize(10)
            .fontColor('#4F46E5')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .height(70)
      .padding({ left: 16, right: 16 })
      .backgroundColor('#16213e')

      // 阅读区域
      Scroll() {
        Column() {
          Text(this.currentArticle)
            .fontSize(this.fontSize)
            .fontColor(this.fontColor)
            .lineHeight(this.fontSize * this.lineHeight)
            .letterSpacing(0.5)
            .padding(16)
        }
      }
      .width('100%')
      .layoutWeight(1)
      .scrollBar(BarState.Off)
      .backgroundColor(this.bgColor)

      // 情绪概率分布条
      Row() {
        this.EmotionBar('😊', this.getEmotionProb('happy'), '#10B981')
        this.EmotionBar('😢', this.getEmotionProb('sad'), '#3B82F6')
        this.EmotionBar('😠', this.getEmotionProb('angry'), '#EF4444')
        this.EmotionBar('😲', this.getEmotionProb('surprise'), '#F59E0B')
        this.EmotionBar('😐', this.getEmotionProb('neutral'), '#808090')
      }
      .width('100%')
      .height(40)
      .padding({ left: 8, right: 8 })
      .justifyContent(FlexAlign.SpaceAround)
      .backgroundColor('#16213e')

      // 底部操作栏
      Row() {
        Button('📖 开始阅读')
          .fontSize(14)
          .backgroundColor('#4F46E5')
          .onClick(() => this.startReading())
        Button('📝 情绪日记')
          .fontSize(14)
          .backgroundColor('#2d2d4e')
          .fontColor('#a0a0cc')
          .onClick(() => this.showDiary())
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .justifyContent(FlexAlign.SpaceEvenly)
      .backgroundColor('#16213e')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0f0f23')
  }

  // 情绪概率条组件
  @Builder
  EmotionBar(emoji: string, prob: number, color: string) {
    Column() {
      Text(emoji)
        .fontSize(14)
      Progress({ value: prob * 100, total: 100, type: ProgressType.Linear })
        .width(40)
        .height(4)
        .color(color)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
  }

  // 开始阅读(启动模拟)
  private startReading() {
    this.readingTime = 0
    this.emotionRecognizer.reset()

    // 阅读计时
    this.readingTimer = setInterval(() => {
      this.readingTime++
    }, 1000)

    // 模拟情感识别(实际开发中从相机+MindSpore获取)
    let simFrame = 0
    this.simulationTimer = setInterval(() => {
      simFrame++
      // 模拟情绪变化:先中性→逐渐疲劳→偶尔开心
      const phase = simFrame / 100
      let simulatedFeatures: FacialFeatures

      if (phase < 1) {
        // 初始阶段:中性
        simulatedFeatures = this.getNeutralFeatures()
      } else if (phase < 3) {
        // 中期:逐渐疲劳(嘴角下压,唤醒度降低)
        simulatedFeatures = {
          au1: 0.1, au4: 0.2, au6: 0.1, au12: 0.05,
          au15: 0.3 + (phase - 1) * 0.2, au25: 0.1,
          mouthWidth: 0.25, motionIntensity: 0.05,
          featureVector: [0.1, 0.2, 0.1, 0.05, 0.5, 0.1, 0.25]
        }
      } else {
        // 后期:偶尔开心
        const isHappy = Math.sin(simFrame * 0.1) > 0.5
        simulatedFeatures = isHappy ? {
          au1: 0.1, au4: 0.05, au6: 0.6, au12: 0.7,
          au15: 0.05, au25: 0.3, mouthWidth: 0.4,
          motionIntensity: 0.1,
          featureVector: [0.1, 0.05, 0.6, 0.7, 0.05, 0.3, 0.4]
        } : this.getNeutralFeatures()
      }

      // 执行情感识别
      const emotionState: EmotionState = this.emotionRecognizer.recognize(simulatedFeatures)
      this.currentEmotion = emotionState.dominantEmotion
      this.emotionConfidence = emotionState.confidence
      this.valence = emotionState.valence
      this.arousal = emotionState.arousal
      this.emotionEmoji = this.getEmotionEmoji(this.currentEmotion)

      // 获取趋势
      const trend = this.emotionRecognizer.getEmotionTrend()
      this.emotionTrend = trend.direction === 'improving' ? '好转 ↑' :
                          trend.direction === 'declining' ? '下降 ↓' : '稳定 →'

      // 根据情感状态自适应调整
      this.adaptToEmotion(emotionState)
    }, 33)
  }

  // 根据情感状态自适应调整阅读体验
  private adaptToEmotion(state: EmotionState) {
    switch (state.dominantEmotion) {
      case 'sad':
        // 悲伤时:增大字号、暖色调背景、降低阅读速度
        this.fontSize = 18
        this.lineHeight = 2.0
        this.bgColor = '#1f1a2e'  // 微暖
        this.fontColor = '#f0e0ff'
        this.adaptationMessage = '检测到低落情绪,已优化阅读舒适度'
        break
      case 'angry':
        // 愤怒时:冷色调、增加间距、推荐休息
        this.fontSize = 16
        this.lineHeight = 2.2
        this.bgColor = '#1a2e2e'  // 冷色
        this.fontColor = '#e0fff0'
        this.adaptationMessage = '建议暂停阅读,深呼吸放松'
        break
      case 'happy':
        // 开心时:正常阅读,可以加快
        this.fontSize = 16
        this.lineHeight = 1.8
        this.bgColor = '#1a1a2e'
        this.fontColor = '#e0e0ff'
        this.adaptationMessage = '心情不错,享受阅读吧!'
        break
      case 'surprise':
        // 惊讶时:暂停自动滚动
        this.adaptationMessage = '检测到惊讶,内容是否有趣?'
        break
      default:
        // 中性:默认设置
        this.fontSize = 16
        this.lineHeight = 1.8
        this.bgColor = '#1a1a2e'
        this.fontColor = '#e0e0ff'
        this.adaptationMessage = ''
    }

    // 唤醒度过低(疲劳)时提醒
    if (state.arousal < -0.3 && state.duration > 60) {
      this.adaptationMessage = '看起来有些疲劳,建议休息一下'
    }
  }

  // 获取中性特征
  private getNeutralFeatures(): FacialFeatures {
    return {
      au1: 0.1, au4: 0.1, au6: 0.2, au12: 0.15,
      au15: 0.1, au25: 0.1, mouthWidth: 0.3,
      motionIntensity: 0.02,
      featureVector: [0.1, 0.1, 0.2, 0.15, 0.1, 0.1, 0.3]
    }
  }

  // 获取情绪概率(简化)
  private getEmotionProb(emotion: string): number {
    if (this.currentEmotion === emotion) return this.emotionConfidence
    return (1 - this.emotionConfidence) / 6
  }

  // 获取情绪Emoji
  private getEmotionEmoji(emotion: string): string {
    const map: Record<string, string> = {
      'happy': '😊', 'sad': '😢', 'angry': '😠',
      'surprise': '😲', 'fear': '😨', 'disgust': '🤢', 'neutral': '😐'
    }
    return map[emotion] || '😐'
  }

  // 获取情绪中文标签
  private getEmotionLabel(emotion: string): string {
    const map: Record<string, string> = {
      'happy': '开心', 'sad': '悲伤', 'angry': '愤怒',
      'surprise': '惊讶', 'fear': '恐惧', 'disgust': '厌恶', 'neutral': '平静'
    }
    return map[emotion] || '未知'
  }

  // 显示情绪日记
  private showDiary() {
    const history = this.emotionRecognizer.getEmotionHistory()
    // 简化:记录当前情绪
    this.emotionDiary.push({
      time: new Date().toLocaleTimeString(),
      emotion: this.currentEmotion,
      valence: this.valence,
      arousal: this.arousal
    })
  }
}

// 日记条目
interface DiaryEntry {
  time: string
  emotion: string
  valence: number
  arousal: number
}

四、踩坑与注意事项

4.1 光照条件对识别的影响

问题:在暗光环境下,面部关键点检测准确率大幅下降,导致表情分类结果不稳定。

解决方案

  • 在模型输入前进行图像增强(直方图均衡化、Gamma校正)
  • 增加置信度阈值,低光照时降低识别灵敏度
  • 提示用户改善光照条件
// 简单的图像亮度检测
private checkBrightness(imageData: PixelMap): boolean {
  // 采样中心区域的平均亮度
  // 如果亮度低于阈值,提示用户
  const avgBrightness = this.calculateAvgBrightness(imageData)
  return avgBrightness > 40  // 亮度阈值
}

4.2 面部遮挡问题

问题:用户戴口罩、眼镜时,下半脸或眼部关键点丢失,影响表情识别。

解决方案

  • 根据可见关键点数量动态调整识别策略
  • 口罩遮挡时,仅使用上半脸特征(眉毛+眼睛)进行分类
  • 眼镜遮挡时,降低眼部AU的权重
// 根据可见关键点调整策略
private adjustStrategy(keypoints: Array<Point>): RecognitionStrategy {
  const visibleLowerFace = this.checkLowerFaceVisibility(keypoints)
  const visibleEyes = this.checkEyeVisibility(keypoints)

  if (!visibleLowerFace) {
    // 口罩模式:仅使用上半脸特征
    return { mode: 'upper_face_only', weights: { au1: 0.4, au4: 0.4, au6: 0.2 } }
  }
  if (!visibleEyes) {
    // 眼镜模式:降低眼部AU权重
    return { mode: 'no_eyes', weights: { au12: 0.5, au15: 0.3, au25: 0.2 } }
  }
  return { mode: 'full_face', weights: { au1: 0.15, au4: 0.15, au6: 0.15, au12: 0.2, au15: 0.15, au25: 0.2 } }
}

4.3 隐私合规

问题:面部数据属于敏感个人信息,处理不当可能违反隐私法规。

注意事项

  • 所有推理必须在端侧完成,禁止将面部图像上传云端
  • 关键点坐标数据也应视为敏感数据,不应持久化存储
  • 必须在首次使用前获取用户明确授权
  • 提供随时关闭情感识别的开关

4.4 情绪标签的文化差异

问题:不同文化背景下,相同的面部表情可能表达不同的情绪。比如某些亚洲文化中微笑可能表示尴尬而非开心。

解决方案

  • 结合上下文信息(用户行为、应用场景)综合判断
  • 提供情绪校准功能,让用户自定义表情-情绪映射
  • 避免仅依赖单一表情做重要决策

五、HarmonyOS 6适配

5.1 人脸检测API变更

变更项 HarmonyOS 5 HarmonyOS 6
人脸检测 @ohos.ai.faceDetection @ohos.ai.faceDetectionV2
关键点数量 最多68点 最多106点(更精细)
表情识别 需自行实现 内置 ExpressionClassifier
活体检测 需自行实现 内置 LivenessDetection

5.2 新增情感计算框架

HarmonyOS 6新增了 @ohos.ai.emotion 模块,提供开箱即用的情感计算能力:

// HarmonyOS 6 新增:内置情感计算API
import { emotion } from '@ohos.ai.emotion'

// 创建情感识别器
const recognizer = emotion.createEmotionRecognizer({
  mode: emotion.RecognizerMode.REAL_TIME,  // 实时模式
  smoothing: emotion.SmoothingStrategy.EMA, // EMA平滑
  smoothingFactor: 0.3                      // 平滑系数
})

// 订阅情感状态变化
recognizer.on('emotionChange', (state: emotion.EmotionState) => {
  console.info(`当前情绪: ${state.dominantEmotion}`)
  console.info(`效价: ${state.valence}, 唤醒度: ${state.arousal}`)
})

// 启动识别
recognizer.start()

5.3 迁移要点

  1. 关键点升级:68点→106点,需要更新特征提取逻辑中的关键点索引
  2. 内置分类器:可直接使用 ExpressionClassifier,无需自行训练模型
  3. 活体检测:新增 LivenessDetection,可防止照片/视频欺骗
  4. 隐私增强:HarmonyOS 6要求情感识别必须在"隐私沙箱"中运行,数据不会泄露到应用层

六、总结

mindmap
  root((表情识别与情感计算))
    面部关键点
      68点标准模型
      Action Unit特征
      运动向量计算
      归一化处理
    表情分类
      六种基本情绪
      基于规则分类
      AU加权组合
      概率归一化
    情感追踪
      EMA时序平滑
      效价-唤醒度模型
      情绪变化趋势
      情绪持续时间
    自适应交互
      字体大小调整
      色调适配
      阅读速度调节
      休息提醒
    关键技巧
      光照条件处理
      面部遮挡适配
      隐私合规
      文化差异考量
    HarmonyOS 6
      106点关键点
      内置ExpressionClassifier
      活体检测API
      情感计算框架
    
    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
知识点 核心内容
FACS体系 44种Action Unit,每种对应特定面部肌肉运动
特征提取 从68关键点计算AU激活度,归一化消除距离影响
表情分类 基于AU加权组合的规则分类,七种基本情绪
时序平滑 EMA平滑避免单帧抖动,状态机约束防止频繁跳变
效价-唤醒度 Valence(正面/负面)× Arousal(平静/激动),二维情绪空间
自适应策略 根据情绪调整字号、色调、间距、阅读速度
隐私合规 端侧推理、不持久化面部数据、用户授权、关闭开关
HarmonyOS 6 106点关键点、内置分类器、活体检测、情感计算框架

表情识别与情感计算让应用从"被动响应"进化为"主动感知"。掌握AU特征提取和时序平滑机制,你就能构建出真正"懂你"的自适应应用。但永远记住:情感数据是敏感信息,隐私保护是第一原则。下一篇,我们将探索视线追踪与眼动交互,让设备知道你在"看"什么。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。