HarmonyOS开发:视线追踪与眼动交互

举报
Jack20 发表于 2026/06/21 14:20:41 2026/06/21
【摘要】 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 瞳孔中心定位

基于特征的方法核心是瞳孔中心定位。基本思路:

  1. 从人脸关键点中提取眼部区域
  2. 对眼部图像进行灰度化和二值化
  3. 通过边缘检测找到瞳孔轮廓
  4. 计算瞳孔中心坐标

在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 迁移要点

  1. API替换:自实现的GazeTracker可替换为内置 gazeTracking API,精度和稳定性大幅提升
  2. 校准简化:从5点校准简化为1点快速校准,用户体验更好
  3. 无障碍集成:眼动事件可直接接入HarmonyOS无障碍框架,无需自行处理事件分发
  4. 隐私增强:内置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隐私保护

视线追踪是智能交互的"最后一公里"——让设备知道你在看哪里。从瞳孔定位到注视点映射,从注视固定到眨眼确认,每个环节都需要精心打磨。更重要的是,这项技术为肢体障碍用户打开了数字世界的大门,这是技术最温暖的价值。下一篇,我们将总结智能交互的设计模式与无障碍实践。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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