HarmonyOS开发:视线追踪与眼动交互
HarmonyOS开发:视线追踪与眼动交互
核心要点:掌握视线追踪技术原理,理解眼动交互设计范式,实现基于注视点的无接触交互系统
一、背景与动机
想象一下这样的场景:你正在做饭,双手沾满了面粉,手机放在台面上播放菜谱视频。你想暂停视频,但手根本没法碰屏幕。这时候,如果你只需要看一眼暂停按钮,视频就自动暂停了——是不是很酷?
或者你是一位运动障碍人士,双手无法灵活操作触屏。但你的眼睛是自由的。如果设备能"看到"你在看哪里,并且根据你的注视来执行操作,那你就拥有了和普通人一样的数字生活能力。
这就是**视线追踪(Gaze Tracking)**技术的价值。它通过检测瞳孔位置和眼球运动方向,推算出用户正在注视屏幕上的哪个位置,从而实现"看哪里点哪里"的无接触交互。
在HarmonyOS上,视线追踪的实现路径是:前置摄像头捕获面部图像 → 人脸检测定位眼部区域 → 瞳孔定位计算注视方向 → 映射到屏幕坐标。整个过程在端侧完成,延迟可以控制在100ms以内。
视线追踪不仅是便利功能,更是无障碍交互的核心技术。对于肢体障碍用户来说,眼动交互可能是他们唯一的数字交互方式。作为开发者,掌握这项技术不仅是技术能力的提升,更是社会责任的体现。
二、核心原理
2.1 视线追踪技术路线
视线追踪有两种主要技术路线:基于外观(Appearance-based)和基于特征(Feature-based)。
flowchart TB
A[视线追踪技术路线] --> B[基于外观 Appearance-based]
A --> C[基于特征 Feature-based]
B --> B1[端到端深度学习]
B --> B2[输入: 眼部图像]
B --> B3[输出: 注视点坐标]
B --> B4[优点: 精度高]
B --> B5[缺点: 需训练数据]
C --> C1[瞳孔中心检测]
C --> C2[角膜反射光斑]
C --> C3[几何模型计算]
C --> C4[优点: 计算量小]
C --> C5[缺点: 受光照影响]
A --> D[HarmonyOS实现方案]
D --> D1[人脸检测定位眼部]
D --> D2[瞳孔中心定位]
D --> D3[头部姿态估计]
D --> D4[注视方向映射]
D --> D5[屏幕坐标校准]
classDef primary fill:#4F46E5,stroke:#3730A3,color:#fff
classDef warning fill:#F59E0B,stroke:#D97706,color:#fff
classDef error fill:#EF4444,stroke:#DC2626,color:#fff
classDef info fill:#06B6D4,stroke:#0891B2,color:#fff
classDef purple fill:#8B5CF6,stroke:#7C3AED,color:#fff
class A primary
class B,B1,B2,B3,B4,B5 info
class C,C1,C2,C3,C4,C5 warning
class D,D1,D2,D3,D4,D5 purple
2.2 瞳孔中心定位
基于特征的方法核心是瞳孔中心定位。基本思路:
- 从人脸关键点中提取眼部区域
- 对眼部图像进行灰度化和二值化
- 通过边缘检测找到瞳孔轮廓
- 计算瞳孔中心坐标
在HarmonyOS上,我们可以利用人脸检测API返回的眼部关键点来简化这个过程。关键点中包含了左右眼的中心位置,这本身就是瞳孔位置的近似值。
2.3 注视方向映射
有了瞳孔位置后,如何将其映射到屏幕上的注视点?这需要解决坐标映射问题:
- 瞳孔相对位置:瞳孔在眼眶中的偏移方向反映了注视方向
- 头部姿态补偿:头部转动时瞳孔位置也会变化,需要区分是眼球转动还是头部转动
- 个人校准:每个人的眼球特征不同,需要简短的校准流程
2.4 眼动交互事件模型
视线追踪的交互模型与触摸交互不同,需要定义新的"眼动事件":
| 事件类型 | 触发条件 | 交互含义 |
|---|---|---|
| GazeEnter | 注视点进入组件区域 | 鼠标hover的等效 |
| GazeLeave | 注视点离开组件区域 | 取消hover |
| GazeFixation | 注视点在同一区域停留>300ms | “注视点击” |
| GazeSaccade | 注视点快速移动 | 视觉搜索行为 |
| GazeBlink | 检测到眨眼 | 确认/取消操作 |
其中最核心的是GazeFixation(注视固定)——当用户持续注视某个区域超过一定时间,就触发"点击"操作。这是眼动交互最常用的确认方式。
三、代码实战
3.1 视线追踪引擎
这个示例实现了基于面部关键点的视线追踪引擎,包含瞳孔定位、注视方向计算和屏幕坐标映射。
// GazeTracker.ets - 视线追踪引擎
// 功能:基于面部关键点的实时视线追踪,支持个人校准
export class GazeTracker {
// 校准参数
private calibrationData: CalibrationData | null = null
// 是否已校准
private isCalibrated: boolean = false
// 注视点平滑
private smoothGazeX: number = 0.5
private smoothGazeY: number = 0.5
// 平滑系数
private readonly SMOOTH_FACTOR: number = 0.4
// 上一帧瞳孔位置(用于检测眨眼)
private prevPupilDistance: number = 0
// 眨眼检测
private blinkCount: number = 0
private lastBlinkTime: number = 0
// 追踪视线:输入面部关键点,输出注视点坐标
trackGaze(keypoints: FacialKeypoints): GazeResult {
// 安全检查
if (!keypoints || !keypoints.leftEye || !keypoints.rightEye) {
return this.getDefaultResult()
}
// 1. 计算瞳孔相对位置(在眼眶中的偏移)
const leftPupilOffset = this.calculatePupilOffset(
keypoints.leftEye, keypoints.leftEyeCorner1, keypoints.leftEyeCorner2
)
const rightPupilOffset = this.calculatePupilOffset(
keypoints.rightEye, keypoints.rightEyeCorner1, keypoints.rightEyeCorner2
)
// 2. 合并左右眼的注视方向
const avgOffsetX = (leftPupilOffset.x + rightPupilOffset.x) / 2
const avgOffsetY = (leftPupilOffset.y + rightPupilOffset.y) / 2
// 3. 头部姿态补偿
const headPoseCompensation = this.calculateHeadPoseCompensation(keypoints)
const compensatedX = avgOffsetX - headPoseCompensation.x * 0.3
const compensatedY = avgOffsetY - headPoseCompensation.y * 0.3
// 4. 映射到屏幕坐标
let screenX: number, screenY: number
if (this.isCalibrated && this.calibrationData) {
// 使用校准数据做线性映射
screenX = this.applyCalibration(compensatedX, this.calibrationData.xMapping)
screenY = this.applyCalibration(compensatedY, this.calibrationData.yMapping)
} else {
// 未校准时使用简单线性映射
screenX = 0.5 + compensatedX * 2.0
screenY = 0.5 + compensatedY * 2.0
}
// 5. 平滑处理
this.smoothGazeX = this.SMOOTH_FACTOR * screenX + (1 - this.SMOOTH_FACTOR) * this.smoothGazeX
this.smoothGazeY = this.SMOOTH_FACTOR * screenY + (1 - this.SMOOTH_FACTOR) * this.smoothGazeY
// 6. 限制在屏幕范围内
const clampedX = Math.max(0, Math.min(1, this.smoothGazeX))
const clampedY = Math.max(0, Math.min(1, this.smoothGazeY))
// 7. 眨眼检测
const eyeOpenness = this.calculateEyeOpenness(keypoints)
const isBlinking = this.detectBlink(eyeOpenness)
return {
gazeX: clampedX,
gazeY: clampedY,
isBlinking: isBlinking,
blinkCount: this.blinkCount,
confidence: this.calculateConfidence(keypoints),
eyeOpenness: eyeOpenness
}
}
// 计算瞳孔在眼眶中的相对偏移
// 返回值:x∈[-1,1](左→右),y∈[-1,1](上→下)
private calculatePupilOffset(
pupil: Point, eyeCorner1: Point, eyeCorner2: Point
): { x: number, y: number } {
// 眼眶宽度(两眼角之间的距离)
const eyeWidth = Math.sqrt(
Math.pow(eyeCorner2.x - eyeCorner1.x, 2) + Math.pow(eyeCorner2.y - eyeCorner1.y, 2)
)
if (eyeWidth < 0.001) return { x: 0, y: 0 }
// 瞳孔在眼角连线上的投影位置
const eyeCenterX = (eyeCorner1.x + eyeCorner2.x) / 2
const eyeCenterY = (eyeCorner1.y + eyeCorner2.y) / 2
// 归一化偏移
const offsetX = (pupil.x - eyeCenterX) / (eyeWidth / 2)
const offsetY = (pupil.y - eyeCenterY) / (eyeWidth / 2) // 用宽度归一化Y
return {
x: Math.max(-1, Math.min(1, offsetX)),
y: Math.max(-1, Math.min(1, offsetY))
}
}
// 计算头部姿态补偿
private calculateHeadPoseCompensation(keypoints: FacialKeypoints): { x: number, y: number } {
// 使用鼻尖相对面部中心的偏移来估计头部转动
const faceCenterX = (keypoints.leftEye.x + keypoints.rightEye.x) / 2
const faceCenterY = (keypoints.leftEye.y + keypoints.rightEye.y) / 2
const noseOffsetX = (keypoints.noseTip.x - faceCenterX) * 2
const noseOffsetY = (keypoints.noseTip.y - faceCenterY) * 2
return { x: noseOffsetX, y: noseOffsetY }
}
// 应用校准映射
private applyCalibration(value: number, mapping: LinearMapping): number {
return mapping.offset + value * mapping.scale
}
// 计算眼睛睁开程度
private calculateEyeOpenness(keypoints: FacialKeypoints): number {
// 上眼睑到下眼睑的距离,归一化
const leftEyeHeight = Math.abs(keypoints.leftEyeUpper.y - keypoints.leftEyeLower.y)
const rightEyeHeight = Math.abs(keypoints.rightEyeUpper.y - keypoints.rightEyeLower.y)
const avgHeight = (leftEyeHeight + rightEyeHeight) / 2
return Math.min(1, avgHeight * 10) // 归一化到[0,1]
}
// 检测眨眼
private detectBlink(eyeOpenness: number): boolean {
const BLINK_THRESHOLD = 0.15 // 眼睛闭合阈值
const now = Date.now()
const isBlinking = eyeOpenness < BLINK_THRESHOLD
// 检测完整的眨眼动作(闭合→睁开)
if (this.prevPupilDistance > BLINK_THRESHOLD && isBlinking) {
// 刚开始闭眼
if (now - this.lastBlinkTime > 300) { // 防抖:300ms内不重复计数
this.blinkCount++
this.lastBlinkTime = now
}
}
this.prevPupilDistance = eyeOpenness
return isBlinking
}
// 计算置信度
private calculateConfidence(keypoints: FacialKeypoints): number {
// 基于关键点检测质量评估
let confidence = 1.0
if (!keypoints.leftEye || !keypoints.rightEye) confidence *= 0.3
if (!keypoints.noseTip) confidence *= 0.7
return confidence
}
// 获取默认结果
private getDefaultResult(): GazeResult {
return {
gazeX: 0.5, gazeY: 0.5,
isBlinking: false, blinkCount: 0,
confidence: 0, eyeOpenness: 1
}
}
// 执行校准:收集校准点数据
// 用户需要依次注视屏幕上的5个校准点
private calibrationSamples: Array<{ rawX: number, rawY: number, targetX: number, targetY: number }> = []
addCalibrationSample(rawX: number, rawY: number, targetX: number, targetY: number) {
this.calibrationSamples.push({ rawX, rawY, targetX, targetY })
}
// 完成校准:计算映射参数
finishCalibration(): boolean {
if (this.calibrationSamples.length < 5) {
return false // 至少需要5个校准点
}
// 简单线性回归:rawX → targetX, rawY → targetY
this.calibrationData = {
xMapping: this.linearRegression(
this.calibrationSamples.map(s => s.rawX),
this.calibrationSamples.map(s => s.targetX)
),
yMapping: this.linearRegression(
this.calibrationSamples.map(s => s.rawY),
this.calibrationSamples.map(s => s.targetY)
)
}
this.isCalibrated = true
return true
}
// 简单线性回归
private linearRegression(x: Array<number>, y: Array<number>): LinearMapping {
const n = x.length
const sumX = x.reduce((a, b) => a + b, 0)
const sumY = y.reduce((a, b) => a + b, 0)
const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0)
const sumX2 = x.reduce((acc, xi) => acc + xi * xi, 0)
const denominator = n * sumX2 - sumX * sumX
if (Math.abs(denominator) < 0.0001) {
return { scale: 1, offset: 0.5 - sumX / n }
}
const scale = (n * sumXY - sumX * sumY) / denominator
const offset = (sumY - scale * sumX) / n
return { scale, offset }
}
// 重置追踪状态
reset() {
this.smoothGazeX = 0.5
this.smoothGazeY = 0.5
this.blinkCount = 0
this.prevPupilDistance = 0
this.calibrationSamples = []
}
}
// 视线追踪结果
export interface GazeResult {
gazeX: number // 注视点X坐标(归一化[0,1])
gazeY: number // 注视点Y坐标(归一化[0,1])
isBlinking: boolean // 是否正在眨眼
blinkCount: number // 眨眼次数
confidence: number // 置信度
eyeOpenness: number // 眼睛睁开程度
}
// 面部关键点(简化版,仅包含视线追踪所需的关键点)
export interface FacialKeypoints {
leftEye: Point // 左瞳孔中心
rightEye: Point // 右瞳孔中心
leftEyeCorner1: Point // 左眼左角
leftEyeCorner2: Point // 左眼右角
rightEyeCorner1: Point // 右眼左角
rightEyeCorner2: Point // 右眼右角
leftEyeUpper: Point // 左上眼睑
leftEyeLower: Point // 左下眼睑
rightEyeUpper: Point // 右上眼睑
rightEyeLower: Point // 右下眼睑
noseTip: Point // 鼻尖
}
// 点数据结构
export interface Point {
x: number
y: number
}
// 线性映射参数
interface LinearMapping {
scale: number // 缩放系数
offset: number // 偏移量
}
// 校准数据
interface CalibrationData {
xMapping: LinearMapping
yMapping: LinearMapping
}
3.2 注视交互框架:眼动事件系统
这个示例实现了基于注视的交互框架,支持注视进入/离开、注视固定(注视点击)和眨眼确认等眼动事件。
// GazeInteraction.ets - 注视交互框架
// 功能:将视线追踪结果转化为交互事件,支持注视点击和眨眼确认
import { GazeResult } from './GazeTracker'
export class GazeInteractionManager {
// 注视固定检测
private fixationTimer: number = -1
// 注视固定触发阈值(毫秒)
private readonly FIXATION_DURATION: number = 500
// 注视固定区域半径(归一化)
private readonly FIXATION_RADIUS: number = 0.03
// 当前注视的组件ID
private currentHoverComponent: string = ''
// 注视固定起始位置
private fixationStartPos: { x: number, y: number } = { x: 0, y: 0 }
// 注视固定累计时间
private fixationAccumulatedTime: number = 0
// 上次更新时间
private lastUpdateTime: number = 0
// 眨眼确认模式
private blinkConfirmEnabled: boolean = true
// 注册的交互区域
private interactiveRegions: Map<string, InteractiveRegion> = new Map()
// 事件回调
private eventCallbacks: Map<string, Array<(event: GazeEvent) => void>> = new Map()
// 注册交互区域
registerRegion(id: string, region: InteractiveRegion) {
this.interactiveRegions.set(id, region)
}
// 注销交互区域
unregisterRegion(id: string) {
this.interactiveRegions.delete(id)
}
// 注册事件回调
on(eventType: string, callback: (event: GazeEvent) => void) {
if (!this.eventCallbacks.has(eventType)) {
this.eventCallbacks.set(eventType, [])
}
this.eventCallbacks.get(eventType)!.push(callback)
}
// 更新视线追踪结果(每帧调用)
updateGaze(gazeResult: GazeResult) {
const now = Date.now()
const deltaTime = this.lastUpdateTime > 0 ? now - this.lastUpdateTime : 0
this.lastUpdateTime = now
// 1. 检测注视点所在的交互区域
const hoveredComponent = this.findHoveredComponent(gazeResult.gazeX, gazeResult.gazeY)
// 2. 处理注视进入/离开事件
if (hoveredComponent !== this.currentHoverComponent) {
// 离开旧组件
if (this.currentHoverComponent) {
this.emitEvent('gazeLeave', {
type: 'gazeLeave',
componentId: this.currentHoverComponent,
gazeX: gazeResult.gazeX,
gazeY: gazeResult.gazeY,
timestamp: now
})
// 重置注视固定
this.fixationAccumulatedTime = 0
}
// 进入新组件
if (hoveredComponent) {
this.emitEvent('gazeEnter', {
type: 'gazeEnter',
componentId: hoveredComponent,
gazeX: gazeResult.gazeX,
gazeY: gazeResult.gazeY,
timestamp: now
})
// 开始新的注视固定计时
this.fixationStartPos = { x: gazeResult.gazeX, y: gazeResult.gazeY }
this.fixationAccumulatedTime = 0
}
this.currentHoverComponent = hoveredComponent
}
// 3. 注视固定检测
if (this.currentHoverComponent) {
// 计算注视点与固定起始位置的距离
const distance = Math.sqrt(
Math.pow(gazeResult.gazeX - this.fixationStartPos.x, 2) +
Math.pow(gazeResult.gazeY - this.fixationStartPos.y, 2)
)
if (distance < this.FIXATION_RADIUS) {
// 注视仍在固定区域内,累加时间
this.fixationAccumulatedTime += deltaTime
// 触发注视进度更新
this.emitEvent('gazeProgress', {
type: 'gazeProgress',
componentId: this.currentHoverComponent,
gazeX: gazeResult.gazeX,
gazeY: gazeResult.gazeY,
timestamp: now,
progress: Math.min(1, this.fixationAccumulatedTime / this.FIXATION_DURATION)
})
// 注视固定完成
if (this.fixationAccumulatedTime >= this.FIXATION_DURATION) {
this.emitEvent('gazeFixation', {
type: 'gazeFixation',
componentId: this.currentHoverComponent,
gazeX: gazeResult.gazeX,
gazeY: gazeResult.gazeY,
timestamp: now
})
// 重置固定计时
this.fixationAccumulatedTime = 0
}
} else {
// 注视移出了固定区域,重置
this.fixationStartPos = { x: gazeResult.gazeX, y: gazeResult.gazeY }
this.fixationAccumulatedTime = 0
}
}
// 4. 眨眼确认检测
if (this.blinkConfirmEnabled && gazeResult.isBlinking && this.currentHoverComponent) {
this.emitEvent('blinkConfirm', {
type: 'blinkConfirm',
componentId: this.currentHoverComponent,
gazeX: gazeResult.gazeX,
gazeY: gazeResult.gazeY,
timestamp: now
})
}
}
// 查找注视点所在的交互区域
private findHoveredComponent(gazeX: number, gazeY: number): string {
for (const [id, region] of this.interactiveRegions) {
if (gazeX >= region.x && gazeX <= region.x + region.width &&
gazeY >= region.y && gazeY <= region.y + region.height) {
return id
}
}
return ''
}
// 触发事件
private emitEvent(eventType: string, event: GazeEvent) {
const callbacks = this.eventCallbacks.get(eventType)
if (callbacks) {
callbacks.forEach(cb => cb(event))
}
}
// 重置交互状态
reset() {
this.currentHoverComponent = ''
this.fixationAccumulatedTime = 0
this.lastUpdateTime = 0
}
}
// 交互区域定义
export interface InteractiveRegion {
x: number // 左上角X(归一化)
y: number // 左上角Y(归一化)
width: number // 宽度(归一化)
height: number // 高度(归一化)
}
// 眼动事件
export interface GazeEvent {
type: string // 事件类型
componentId: string // 组件ID
gazeX: number // 注视点X
gazeY: number // 注视点Y
timestamp: number // 时间戳
progress?: number // 注视进度(仅gazeProgress事件)
}
3.3 眼动交互应用:注视选择器
这个示例将视线追踪引擎和注视交互框架组合起来,构建一个完整的注视选择器应用——用户只需注视按钮即可触发操作。
// GazeSelector.ets - 注视选择器应用
// 功能:通过注视和眨眼实现无接触UI交互
import { GazeTracker, GazeResult, FacialKeypoints, Point } from './GazeTracker'
import { GazeInteractionManager, InteractiveRegion, GazeEvent } from './GazeInteraction'
@Entry
@Component
struct GazeSelector {
// 注视点位置
@State gazeX: number = 180 // 注视点X(像素)
@State gazeY: number = 400 // 注视点Y(像素)
@State gazeVisible: boolean = true
// 按钮状态
@State buttonStates: Map<string, ButtonState> = new Map([
['btn_music', { label: '🎵 音乐', hovered: false, progress: 0, activated: false }],
['btn_weather', { label: '🌤 天气', hovered: false, progress: 0, activated: false }],
['btn_news', { label: '📰 新闻', hovered: false, progress: 0, activated: false }],
['btn_settings', { label: '⚙️ 设置', hovered: false, progress: 0, activated: false }],
['btn_camera', { label: '📷 相机', hovered: false, progress: 0, activated: false }],
['btn_calendar', { label: '📅 日历', hovered: false, progress: 0, activated: false }]
])
// 状态信息
@State statusMessage: string = '请注视按钮进行选择'
@State blinkCount: number = 0
@State isCalibrating: boolean = false
@State calibrationStep: number = 0
@State calibrationTarget: { x: number, y: number } = { x: 0.5, y: 0.5 }
// 引擎实例
private gazeTracker: GazeTracker = new GazeTracker()
private interactionManager: GazeInteractionManager = new GazeInteractionManager()
// 模拟定时器
private simulationTimer: number = -1
// 屏幕尺寸
private screenWidth: number = 360
private screenHeight: number = 780
aboutToAppear() {
this.setupInteractionCallbacks()
this.registerButtonRegions()
this.startSimulation()
}
aboutToDisappear() {
if (this.simulationTimer !== -1) clearInterval(this.simulationTimer)
}
build() {
Stack() {
Column() {
// 顶部状态栏
Row() {
Text('👁 眼动交互')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#e0e0ff')
Blank()
Text(`眨眼: ${this.blinkCount}`)
.fontSize(12)
.fontColor('#808090')
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('#16213e')
// 状态消息
Text(this.statusMessage)
.fontSize(14)
.fontColor('#a0a0cc')
.margin({ top: 12, bottom: 12 })
// 按钮网格
Grid() {
ForEach(Array.from(this.buttonStates.entries()), (entry: [string, ButtonState]) => {
GridItem() {
this.GazeButton(entry[0], entry[1])
}
}, (entry: [string, ButtonState]) => entry[0])
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('90%')
.height(280)
.margin({ top: 20 })
// 注视固定说明
Column() {
Text('💡 交互说明')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#a0a0cc')
Text('注视按钮0.5秒即可选择')
.fontSize(12)
.fontColor('#808090')
.margin({ top: 4 })
Text('眨眼可快速确认当前选项')
.fontSize(12)
.fontColor('#808090')
}
.margin({ top: 30 })
.alignItems(HorizontalAlign.Center)
Blank()
// 底部操作栏
Row() {
Button(this.isCalibrating ? '校准中...' : '开始校准')
.fontSize(14)
.backgroundColor(this.isCalibrating ? '#F59E0B' : '#2d2d4e')
.fontColor(this.isCalibrating ? '#000' : '#a0a0cc')
.onClick(() => this.startCalibration())
}
.width('100%')
.height(60)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.Center)
.backgroundColor('#16213e')
}
.width('100%')
.height('100%')
// 注视点指示器(叠加层)
if (this.gazeVisible) {
Stack() {
// 外圈光晕
Circle()
.width(40)
.height(40)
.fill('#4F46E5')
.opacity(0.15)
// 内圈
Circle()
.width(16)
.height(16)
.fill('#4F46E5')
.opacity(0.6)
// 中心点
Circle()
.width(4)
.height(4)
.fill('#fff')
}
.position({ x: this.gazeX - 20, y: this.gazeY - 20 })
}
// 校准目标点
if (this.isCalibrating) {
Circle()
.width(30)
.height(30)
.fill('#EF4444')
.opacity(0.8)
.position({
x: this.calibrationTarget.x * this.screenWidth - 15,
y: this.calibrationTarget.y * this.screenHeight - 15
})
}
}
.width('100%')
.height('100%')
.backgroundColor('#0f0f23')
}
// 注视按钮组件
@Builder
GazeButton(id: string, state: ButtonState) {
Column() {
Text(state.label)
.fontSize(16)
.fontWeight(state.hovered ? FontWeight.Bold : FontWeight.Normal)
.fontColor(state.activated ? '#4F46E5' : (state.hovered ? '#e0e0ff' : '#808090'))
// 注视进度条
if (state.hovered && state.progress > 0) {
Progress({ value: state.progress * 100, total: 100, type: ProgressType.Linear })
.width('80%')
.height(4)
.color('#4F46E5')
.margin({ top: 8 })
}
// 激活指示
if (state.activated) {
Text('✓ 已选择')
.fontSize(10)
.fontColor('#4F46E5')
.margin({ top: 4 })
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.borderRadius(12)
.backgroundColor(state.hovered ? '#2a2a4e' : '#1a1a2e')
.border({
width: state.hovered ? 2 : 1,
color: state.activated ? '#4F46E5' : (state.hovered ? '#4F46E5' : '#2d2d4e')
})
.shadow(state.hovered ? { radius: 12, color: '#4F46E540', offsetX: 0, offsetY: 4 } : undefined)
}
// 设置交互回调
private setupInteractionCallbacks() {
this.interactionManager.on('gazeEnter', (event: GazeEvent) => {
const state = this.buttonStates.get(event.componentId)
if (state) {
state.hovered = true
this.buttonStates.set(event.componentId, state)
this.statusMessage = `注视: ${state.label}`
}
})
this.interactionManager.on('gazeLeave', (event: GazeEvent) => {
const state = this.buttonStates.get(event.componentId)
if (state) {
state.hovered = false
state.progress = 0
this.buttonStates.set(event.componentId, state)
}
})
this.interactionManager.on('gazeProgress', (event: GazeEvent) => {
const state = this.buttonStates.get(event.componentId)
if (state && event.progress !== undefined) {
state.progress = event.progress
this.buttonStates.set(event.componentId, state)
}
})
this.interactionManager.on('gazeFixation', (event: GazeEvent) => {
const state = this.buttonStates.get(event.componentId)
if (state) {
state.activated = true
state.progress = 0
this.buttonStates.set(event.componentId, state)
this.statusMessage = `✅ 已选择: ${state.label}`
}
})
this.interactionManager.on('blinkConfirm', (event: GazeEvent) => {
this.blinkCount++
const state = this.buttonStates.get(event.componentId)
if (state) {
state.activated = true
this.buttonStates.set(event.componentId, state)
this.statusMessage = `👁 眨眼确认: ${state.label}`
}
})
}
// 注册按钮交互区域
private registerButtonRegions() {
const buttonIds = ['btn_music', 'btn_weather', 'btn_news', 'btn_settings', 'btn_camera', 'btn_calendar']
const cols = 3
const gridWidth = 0.9
const gridHeight = 280 / this.screenHeight
const gridStartX = 0.05
const gridStartY = 140 / this.screenHeight
buttonIds.forEach((id, index) => {
const col = index % cols
const row = Math.floor(index / cols)
const cellWidth = gridWidth / cols
const cellHeight = gridHeight / 2
this.interactionManager.registerRegion(id, {
x: gridStartX + col * cellWidth,
y: gridStartY + row * cellHeight,
width: cellWidth,
height: cellHeight
})
})
}
// 启动模拟
private startSimulation() {
let simFrame = 0
this.simulationTimer = setInterval(() => {
simFrame++
// 模拟面部关键点(实际开发中从相机获取)
const keypoints = this.generateSimulatedKeypoints(simFrame)
// 执行视线追踪
const gazeResult: GazeResult = this.gazeTracker.trackGaze(keypoints)
// 更新注视点位置
this.gazeX = gazeResult.gazeX * this.screenWidth
this.gazeY = gazeResult.gazeY * this.screenHeight
this.blinkCount = gazeResult.blinkCount
// 更新交互管理器
this.interactionManager.updateGaze(gazeResult)
}, 33)
}
// 生成模拟面部关键点
private generateSimulatedKeypoints(frame: number): FacialKeypoints {
// 模拟注视点在屏幕上缓慢移动
const phase = frame * 0.02
const gazeOffsetX = Math.sin(phase) * 0.3 // 左右摆动
const gazeOffsetY = Math.cos(phase * 0.7) * 0.2 // 上下摆动
// 模拟眨眼(每3秒一次)
const isBlinking = (frame % 90) < 3
return {
leftEye: { x: 0.42 + gazeOffsetX * 0.1, y: 0.35 + gazeOffsetY * 0.05 },
rightEye: { x: 0.58 + gazeOffsetX * 0.1, y: 0.35 + gazeOffsetY * 0.05 },
leftEyeCorner1: { x: 0.38, y: 0.35 },
leftEyeCorner2: { x: 0.46, y: 0.35 },
rightEyeCorner1: { x: 0.54, y: 0.35 },
rightEyeCorner2: { x: 0.62, y: 0.35 },
leftEyeUpper: { x: 0.42, y: isBlinking ? 0.35 : 0.33 },
leftEyeLower: { x: 0.42, y: isBlinking ? 0.35 : 0.37 },
rightEyeUpper: { x: 0.58, y: isBlinking ? 0.35 : 0.33 },
rightEyeLower: { x: 0.58, y: isBlinking ? 0.35 : 0.37 },
noseTip: { x: 0.5 + gazeOffsetX * 0.3, y: 0.45 + gazeOffsetY * 0.2 }
}
}
// 开始校准
private startCalibration() {
this.isCalibrating = true
this.calibrationStep = 0
this.gazeTracker.reset()
this.moveToNextCalibrationPoint()
}
// 移动到下一个校准点
private moveToNextCalibrationPoint() {
const calibrationPoints = [
{ x: 0.2, y: 0.2 }, // 左上
{ x: 0.8, y: 0.2 }, // 右上
{ x: 0.5, y: 0.5 }, // 中心
{ x: 0.2, y: 0.8 }, // 左下
{ x: 0.8, y: 0.8 } // 右下
]
if (this.calibrationStep < calibrationPoints.length) {
this.calibrationTarget = calibrationPoints[this.calibrationStep]
this.statusMessage = `校准步骤 ${this.calibrationStep + 1}/5:请注视红点`
// 模拟2秒后自动完成当前校准点
setTimeout(() => {
// 在实际应用中,这里会收集当前帧的原始注视数据
this.calibrationStep++
this.moveToNextCalibrationPoint()
}, 2000)
} else {
// 校准完成
const success = this.gazeTracker.finishCalibration()
this.isCalibrating = false
this.statusMessage = success ? '✅ 校准完成!' : '❌ 校准失败,请重试'
}
}
}
// 按钮状态
interface ButtonState {
label: string
hovered: boolean
progress: number
activated: boolean
}
四、踩坑与注意事项
4.1 注视点抖动
问题:视线追踪输出的注视点坐标抖动严重,导致频繁触发gazeEnter/gazeLeave事件。
解决方案:
- 增大EMA平滑系数(但会增大延迟)
- 增大注视固定区域半径
- 使用"停留确认"机制:注视点在区域内停留一定时间后才触发事件
// 增强平滑:双阶段平滑
private smoothGazeX1: number = 0.5 // 第一阶段平滑
private smoothGazeX2: number = 0.5 // 第二阶段平滑
// 先做快速平滑,再做慢速平滑
this.smoothGazeX1 = 0.5 * rawX + 0.5 * this.smoothGazeX1 // 快速跟踪
this.smoothGazeX2 = 0.2 * this.smoothGazeX1 + 0.8 * this.smoothGazeX2 // 慢速稳定
4.2 校准精度问题
问题:校准后注视点偏移仍然较大,特别是在屏幕边缘区域。
解决方案:
- 增加校准点数量(5点→9点)
- 使用非线性映射(多项式拟合)替代线性映射
- 定期重新校准
4.3 眨眼检测误触发
问题:自然眨眼频率约15-20次/分钟,容易误触发眨眼确认。
解决方案:
- 增加眨眼持续时间阈值(只检测有意闭眼,>200ms)
- 使用"双重确认":先注视固定,再眨眼确认
- 提供眨眼确认开关
4.4 戴眼镜的影响
问题:眼镜镜片反射和折射会影响瞳孔定位精度。
解决方案:
- 使用红外滤光片减少镜片反射
- 增加瞳孔搜索区域容差
- 为戴眼镜用户提供专用校准模式
五、HarmonyOS 6适配
5.1 视线追踪API变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 视线追踪 | 需自行实现 | 内置 @ohos.ai.gazeTracking |
| 注视点精度 | ±3°(自实现) | ±1°(内置,含NPU加速) |
| 校准方式 | 手动5点校准 | 自动1点快速校准 |
| 眨眼检测 | 需自行实现 | 内置 BlinkDetector |
| 多人支持 | 不支持 | 支持多人注视追踪 |
5.2 新增无障碍眼动API
HarmonyOS 6在无障碍框架中新增了眼动交互支持:
// HarmonyOS 6 新增:内置视线追踪API
import { gazeTracking } from '@ohos.ai.gazeTracking'
// 创建视线追踪器
const tracker = gazeTracking.createTracker({
precision: gazeTracking.Precision.HIGH,
smoothing: true,
calibrationMode: gazeTracking.CalibrationMode.QUICK
})
// 订阅注视点更新
tracker.on('gazeUpdate', (gazePoint: gazeTracking.GazePoint) => {
console.info(`注视点: (${gazePoint.x}, ${gazePoint.y}), 置信度: ${gazePoint.confidence}`)
})
// 订阅眨眼事件
tracker.on('blink', (event: gazeTracking.BlinkEvent) => {
console.info(`眨眼: 持续${event.duration}ms`)
})
// 启动追踪
tracker.start()
5.3 迁移要点
- API替换:自实现的GazeTracker可替换为内置
gazeTrackingAPI,精度和稳定性大幅提升 - 校准简化:从5点校准简化为1点快速校准,用户体验更好
- 无障碍集成:眼动事件可直接接入HarmonyOS无障碍框架,无需自行处理事件分发
- 隐私增强:内置API自动在可信执行环境(TEE)中处理眼部数据,应用层无法获取原始眼部图像
六、总结
mindmap
root((视线追踪与眼动交互))
技术路线
基于外观深度学习
基于特征瞳孔定位
混合方案
核心算法
瞳孔中心定位
注视方向映射
头部姿态补偿
个人校准
交互事件
GazeEnter注视进入
GazeLeave注视离开
GazeFixation注视固定
GazeSaccade快速扫视
BlinkConfirm眨眼确认
关键技巧
注视点抖动平滑
校准精度优化
眨眼误触防护
戴眼镜适配
无障碍
肢体障碍用户
注视点击替代触摸
眨眼确认替代按键
隐私保护
HarmonyOS 6
内置gazeTracking API
1点快速校准
无障碍框架集成
TEE隐私保护
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
| 知识点 | 核心内容 |
|---|---|
| 技术路线 | 基于外观(端到端DL)vs 基于特征(瞳孔定位+几何模型) |
| 瞳孔定位 | 瞳孔在眼眶中的相对偏移反映注视方向 |
| 头部补偿 | 区分眼球转动和头部转动,用鼻尖偏移估计头部姿态 |
| 个人校准 | 5点校准+线性回归,映射原始偏移到屏幕坐标 |
| 注视固定 | 注视点在同一区域停留>300ms触发"点击" |
| 眨眼检测 | 眼睛闭合度<阈值 + 防抖机制 |
| 抖动平滑 | EMA平滑 + 增大固定区域 + 停留确认 |
| 隐私保护 | 端侧推理、不持久化眼部数据、用户授权 |
| HarmonyOS 6 | 内置gazeTracking API、1点校准、TEE隐私保护 |
视线追踪是智能交互的"最后一公里"——让设备知道你在看哪里。从瞳孔定位到注视点映射,从注视固定到眨眼确认,每个环节都需要精心打磨。更重要的是,这项技术为肢体障碍用户打开了数字世界的大门,这是技术最温暖的价值。下一篇,我们将总结智能交互的设计模式与无障碍实践。
- 点赞
- 收藏
- 关注作者
评论(0)