HarmonyOS 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 情感计算流程
从原始图像到情感状态输出,完整流程如下:
- 人脸检测 → 定位人脸区域
- 关键点提取 → 获取68个面部关键点坐标
- 特征计算 → 基于关键点计算AU特征向量
- 表情分类 → 将特征向量输入分类模型
- 情感追踪 → 对分类结果进行时序平滑和趋势分析
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 迁移要点
- 关键点升级:68点→106点,需要更新特征提取逻辑中的关键点索引
- 内置分类器:可直接使用
ExpressionClassifier,无需自行训练模型 - 活体检测:新增
LivenessDetection,可防止照片/视频欺骗 - 隐私增强: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特征提取和时序平滑机制,你就能构建出真正"懂你"的自适应应用。但永远记住:情感数据是敏感信息,隐私保护是第一原则。下一篇,我们将探索视线追踪与眼动交互,让设备知道你在"看"什么。
- 点赞
- 收藏
- 关注作者
评论(0)