HarmonyOS开发:人脸识别与活体检测技术

举报
Jack20 发表于 2026/06/21 13:58:23 2026/06/21
【摘要】 HarmonyOS开发:人脸识别与活体检测技术核心要点:本文深入讲解HarmonyOS端侧人脸识别与活体检测的完整技术方案,涵盖人脸检测(RetinaFace/MTCNN)、人脸对齐与特征提取(ArcFace)、活体检测(红外/深度/静默)三大核心模块,以及端侧特征比对的工程实现。 一、背景与动机你拿起手机,看一眼就解锁了——这是人脸识别最日常的应用场景。但人脸识别远不止"刷脸解锁"这么简...

HarmonyOS开发:人脸识别与活体检测技术

核心要点:本文深入讲解HarmonyOS端侧人脸识别与活体检测的完整技术方案,涵盖人脸检测(RetinaFace/MTCNN)、人脸对齐与特征提取(ArcFace)、活体检测(红外/深度/静默)三大核心模块,以及端侧特征比对的工程实现。


一、背景与动机

你拿起手机,看一眼就解锁了——这是人脸识别最日常的应用场景。但人脸识别远不止"刷脸解锁"这么简单。银行APP用活体检测确认"你是你",门禁系统用识别记录管理出入,社交APP用美颜滤镜实时追踪人脸关键点。这些场景背后,都是一套完整的人脸技术栈。

人脸识别的安全性问题尤为关键。如果只做"人脸比对",那一张照片就能骗过系统。所以活体检测是必须的——它要判断镜头前的是"真人"还是"假脸"(照片、视频、面具)。活体检测和识别必须紧密结合,才能构建安全可靠的人脸系统。

HarmonyOS在端侧人脸识别上有天然优势:NPU加速让实时检测成为可能,多摄像头(RGB+红外+深度)为活体检测提供了硬件基础,安全区域(TEE)为特征存储提供了安全保障。本文将完整讲解这套技术方案。


二、核心原理

2.1 人脸识别技术栈

完整的人脸识别系统包含三个核心模块,缺一不可:

flowchart TB
    A[输入图像/视频帧] --> B[人脸检测<br/>Face Detection]
    B --> C{是否检测到人脸?}
    C -->|| A
    C -->|| D[人脸对齐<br/>Face Alignment]
    D --> E[活体检测<br/>Liveness Detection]
    E --> F{是否为真人?}
    F -->|| G[拒绝访问<br/>提示假脸攻击]
    F -->|| H[特征提取<br/>Feature Extraction]
    H --> I[特征比对<br/>Feature Matching]
    I --> J{相似度 > 阈值?}
    J -->|| K[认证通过]
    J -->|| L[认证失败]

    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    class A,B primary
    class D,E warning
    class G,L error
    class H,I info
    class K purple

2.2 人脸检测算法

人脸检测是整个流程的第一步,常用的端侧算法有:

  • RetinaFace:单阶段检测器,输出人脸框+5个关键点(左眼、右眼、鼻尖、左嘴角、右嘴角),速度快、精度高
  • MTCNN:三阶段级联检测器(P-Net→R-Net→O-Net),逐步精炼检测结果
  • BlazeFace:Google提出的超轻量检测器,专为移动端优化

端侧推荐RetinaFace,它在速度和精度之间取得了最佳平衡。

2.3 人脸特征提取(ArcFace)

ArcFace(Additive Angular Margin Loss)是当前最流行的人脸识别算法。它的核心思想是在特征空间中增大不同人之间的角度间隔,使同一个人的特征更紧凑、不同人的特征更分散:

  • 输入:对齐后的112×112人脸图像
  • 输出:512维特征向量(embedding)
  • 比对:计算两个特征向量的余弦相似度,阈值通常为0.4-0.6

2.4 活体检测

活体检测是防止假脸攻击的关键防线,分为三种类型:

类型 原理 优势 劣势
配合式 要求用户做动作(眨眼、摇头、张嘴) 安全性高 体验差
红外式 使用红外摄像头检测皮肤反射 不受光照影响 需要红外硬件
深度式 使用ToF/结构光获取3D深度信息 最安全 硬件成本高
静默式 纯RGB图像分析(rPPG、纹理、边缘) 无需配合 安全性中等

HarmonyOS设备通常配备RGB+红外双摄,推荐红外+静默组合方案。


三、代码实战

3.1 人脸数据结构定义

// FaceTypes.ets - 人脸识别数据结构定义

/**
 * 人脸关键点(5点)
 */
export interface FaceLandmarks {
  leftEye: Point;      // 左眼中心
  rightEye: Point;     // 右眼中心
  nose: Point;         // 鼻尖
  leftMouth: Point;    // 左嘴角
  rightMouth: Point;   // 右嘴角
}

/**
 * 二维坐标点
 */
export interface Point {
  x: number;
  y: number;
}

/**
 * 人脸检测结果
 */
export interface FaceDetection {
  bbox: BoundingBox;          // 人脸边界框
  confidence: number;         // 检测置信度
  landmarks: FaceLandmarks;   // 5个关键点
}

/**
 * 边界框
 */
export interface BoundingBox {
  x1: number;
  y1: number;
  x2: number;
  y2: number;
}

/**
 * 人脸特征向量
 */
export class FaceEmbedding {
  vector: Float32Array;  // 512维特征向量
  personId: string;      // 人物ID

  constructor(vector: Float32Array, personId: string) {
    this.vector = vector;
    this.personId = personId;
  }

  /**
   * 计算与另一个特征的余弦相似度
   */
  cosineSimilarity(other: FaceEmbedding): number {
    let dotProduct = 0;
    let normA = 0;
    let normB = 0;

    for (let i = 0; i < this.vector.length; i++) {
      dotProduct += this.vector[i] * other.vector[i];
      normA += this.vector[i] * this.vector[i];
      normB += other.vector[i] * other.vector[i];
    }

    normA = Math.sqrt(normA);
    normB = Math.sqrt(normB);

    if (normA === 0 || normB === 0) return 0;
    return dotProduct / (normA * normB);
  }
}

/**
 * 活体检测结果
 */
export interface LivenessResult {
  isLive: boolean;        // 是否为真人
  score: number;          // 活体分数(0-1)
  livenessType: string;   // 检测类型
  attackType?: string;    // 如果是假脸,攻击类型
}

/**
 * 人脸识别配置
 */
export interface FaceRecognitionConfig {
  detectionModelPath: string;     // 人脸检测模型路径
  recognitionModelPath: string;   // 特征提取模型路径
  livenessModelPath: string;      // 活体检测模型路径
  inputSize: number;              // 输入尺寸
  embeddingSize: number;          // 特征维度
  similarityThreshold: number;    // 比对阈值
  livenessThreshold: number;      // 活体阈值
  maxFaces: number;               // 最大检测人脸数
}

/**
 * 人脸识别完整结果
 */
export interface FaceRecognitionResult {
  detection: FaceDetection;       // 检测结果
  liveness: LivenessResult;       // 活体结果
  embedding?: FaceEmbedding;      // 特征向量(活体通过时才有)
  matchedPerson?: string;         // 匹配到的人(如果有)
  matchScore?: number;            // 匹配分数
}

3.2 人脸检测与对齐引擎

// FaceDetector.ets - 人脸检测与对齐引擎
import { mindspore } from '@kit.MindSporeLiteKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
import { fs } from '@kit.CoreFileKit';
import { FaceDetection, FaceLandmarks, Point, BoundingBox } from './FaceTypes';

/**
 * 人脸检测器
 * 使用RetinaFace模型检测人脸位置和关键点
 */
export class FaceDetectorEngine {
  private context: common.Context;
  private session: mindspore.Session | null = null;
  private model: mindspore.Model | null = null;
  private isInitialized: boolean = false;
  private inputSize: number = 320;  // RetinaFace输入尺寸

  constructor(context: common.Context) {
    this.context = context;
  }

  /**
   * 初始化人脸检测模型
   */
  async initialize(): Promise<boolean> {
    try {
      const modelPath = await this.copyModelToSandbox('retinaface', 'retinaface.ms');

      const msContext: mindspore.Context = {};
      const npuDevice: mindspore.DeviceInfo = {
        deviceType: mindspore.DeviceType.kNPU,
        enableFloat16: true
      };
      const cpuDevice: mindspore.DeviceInfo = {
        deviceType: mindspore.DeviceType.kCPU,
        enableFloat16: true,
        cpuCores: [0, 1, 2, 3]
      };
      msContext.deviceInfos = [npuDevice, cpuDevice];

      this.model = new mindspore.Model();
      let result = this.model.loadModelFromFile(modelPath, msContext);
      if (result !== mindspore.kMSStatusSuccess) {
        msContext.deviceInfos = [cpuDevice];
        result = this.model.loadModelFromFile(modelPath, msContext);
        if (result !== mindspore.kMSStatusSuccess) return false;
      }

      this.session = this.model.createSession(msContext);
      this.isInitialized = this.session !== null;
      return this.isInitialized;
    } catch (error) {
      console.error(`[FaceDetector] 初始化失败: ${error}`);
      return false;
    }
  }

  /**
   * 拷贝模型到沙箱
   */
  private async copyModelToSandbox(name: string, filename: string): Promise<string> {
    const sandboxPath = `${this.context.filesDir}/${filename}`;
    if (fs.accessSync(sandboxPath)) return sandboxPath;
    const content = this.context.resourceMgr.getRawFileContentSync(`models/${filename}`);
    const file = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
    fs.writeSync(file.fd, content.buffer);
    fs.closeSync(file);
    return sandboxPath;
  }

  /**
   * 检测图像中的人脸
   */
  async detect(pixelMap: image.PixelMap): Promise<FaceDetection[]> {
    if (!this.isInitialized || this.session === null) return [];

    try {
      const imageInfo = pixelMap.getImageInfo();
      const { width: imgW, height: imgH } = imageInfo.size;

      // 预处理
      const inputData = this.preprocess(pixelMap);

      // 推理
      const inputs = this.session.getInputs();
      inputs[0].setData(inputData.buffer);
      this.session.run(inputs);

      // 获取输出(人脸框、关键点、置信度)
      const outputs = this.session.getOutputs();
      const bboxes = new Float32Array(outputs[0].getData());  // 人脸框
      const scores = new Float32Array(outputs[1].getData());  // 置信度
      const landmarks = new Float32Array(outputs[2].getData()); // 关键点

      // 后处理:解析检测结果
      return this.postprocess(bboxes, scores, landmarks, imgW, imgH);
    } catch (error) {
      console.error(`[FaceDetector] 检测失败: ${error}`);
      return [];
    }
  }

  /**
   * 预处理:Resize + Normalize
   */
  private preprocess(pixelMap: image.PixelMap): Float32Array {
    const size = this.inputSize;
    const imageInfo = pixelMap.getImageInfo();
    const pixelBytes = new Uint8Array(imageInfo.size.width * imageInfo.size.height * 4);
    pixelMap.readPixelsToBufferSync(pixelBytes.buffer);

    const inputData = new Float32Array(3 * size * size);
    const means = [123.0, 117.0, 104.0];  // RetinaFace使用的均值

    for (let h = 0; h < size; h++) {
      for (let w = 0; w < size; w++) {
        const srcH = Math.floor(h * imageInfo.size.height / size);
        const srcW = Math.floor(w * imageInfo.size.width / size);
        const srcIdx = (srcH * imageInfo.size.width + srcW) * 4;

        const r = pixelBytes[srcIdx] - means[0];
        const g = pixelBytes[srcIdx + 1] - means[1];
        const b = pixelBytes[srcIdx + 2] - means[2];

        const dstIdx = h * size + w;
        inputData[0 * size * size + dstIdx] = r;
        inputData[1 * size * size + dstIdx] = g;
        inputData[2 * size * size + dstIdx] = b;
      }
    }

    return inputData;
  }

  /**
   * 后处理:解析RetinaFace输出
   */
  private postprocess(
    bboxes: Float32Array,
    scores: Float32Array,
    landmarks: Float32Array,
    imgW: number,
    imgH: number
  ): FaceDetection[] {
    const results: FaceDetection[] = [];
    const confidenceThreshold = 0.8;
    const numAnchors = scores.length;

    for (let i = 0; i < numAnchors; i++) {
      if (scores[i] < confidenceThreshold) continue;

      // 解析边界框(从模型空间映射到原图空间)
      const scale = Math.max(imgW, imgH) / this.inputSize;
      const bbox: BoundingBox = {
        x1: Math.max(0, bboxes[i * 4] * scale),
        y1: Math.max(0, bboxes[i * 4 + 1] * scale),
        x2: Math.min(imgW, bboxes[i * 4 + 2] * scale),
        y2: Math.min(imgH, bboxes[i * 4 + 3] * scale)
      };

      // 解析5个关键点
      const faceLandmarks: FaceLandmarks = {
        leftEye: { x: landmarks[i * 10] * scale, y: landmarks[i * 10 + 1] * scale },
        rightEye: { x: landmarks[i * 10 + 2] * scale, y: landmarks[i * 10 + 3] * scale },
        nose: { x: landmarks[i * 10 + 4] * scale, y: landmarks[i * 10 + 5] * scale },
        leftMouth: { x: landmarks[i * 10 + 6] * scale, y: landmarks[i * 10 + 7] * scale },
        rightMouth: { x: landmarks[i * 10 + 8] * scale, y: landmarks[i * 10 + 9] * scale }
      };

      results.push({
        bbox: bbox,
        confidence: scores[i],
        landmarks: faceLandmarks
      });
    }

    // 按置信度降序排列
    results.sort((a, b) => b.confidence - a.confidence);
    return results;
  }

  /**
   * 人脸对齐:根据关键点进行仿射变换
   * 将人脸对齐到标准的112×112尺寸
   */
  alignFace(pixelMap: image.PixelMap, landmarks: FaceLandmarks): image.PixelMap | null {
    try {
      const imageInfo = pixelMap.getImageInfo();
      const dstSize = 112;

      // 计算仿射变换矩阵
      // 标准模板的关键点位置
      const template: FaceLandmarks = {
        leftEye: { x: 38.2946, y: 51.6963 },
        rightEye: { x: 73.5318, y: 51.5014 },
        nose: { x: 56.0252, y: 71.7366 },
        leftMouth: { x: 41.5493, y: 92.3655 },
        rightMouth: { x: 70.7299, y: 92.2041 }
      };

      // 使用最小二乘法求解仿射变换参数
      const transform = this.estimateAffineTransform(landmarks, template);

      // 读取原图像素
      const srcPixels = new Uint8Array(imageInfo.size.width * imageInfo.size.height * 4);
      pixelMap.readPixelsToBufferSync(srcPixels.buffer);

      // 创建对齐后的PixelMap
      const alignedInfo: image.ImageInfo = {
        size: { width: dstSize, height: dstSize },
        pixelFormat: image.PixelFormat.RGBA_8888,
        alphaType: image.AlphaType.OPAQUE
      };
      const alignedPixelMap = image.createPixelMapSync(
        new ArrayBuffer(dstSize * dstSize * 4), alignedInfo
      );
      const dstPixels = new Uint8Array(dstSize * dstSize * 4);

      // 仿射变换:对每个目标像素,反算源像素位置
      for (let y = 0; y < dstSize; y++) {
        for (let x = 0; x < dstSize; x++) {
          // 逆变换
          const srcX = transform[0] * x + transform[1] * y + transform[2];
          const srcY = transform[3] * x + transform[4] * y + transform[5];

          // 双线性插值
          if (srcX >= 0 && srcX < imageInfo.size.width && srcY >= 0 && srcY < imageInfo.size.height) {
            const x0 = Math.floor(srcX);
            const y0 = Math.floor(srcY);
            const x1 = Math.min(x0 + 1, imageInfo.size.width - 1);
            const y1 = Math.min(y0 + 1, imageInfo.size.height - 1);
            const fx = srcX - x0;
            const fy = srcY - y0;

            const dstIdx = (y * dstSize + x) * 4;
            for (let c = 0; c < 4; c++) {
              const v00 = srcPixels[(y0 * imageInfo.size.width + x0) * 4 + c];
              const v10 = srcPixels[(y0 * imageInfo.size.width + x1) * 4 + c];
              const v01 = srcPixels[(y1 * imageInfo.size.width + x0) * 4 + c];
              const v11 = srcPixels[(y1 * imageInfo.size.width + x1) * 4 + c];
              dstPixels[dstIdx + c] = Math.round(
                v00 * (1 - fx) * (1 - fy) + v10 * fx * (1 - fy) +
                v01 * (1 - fx) * fy + v11 * fx * fy
              );
            }
          }
        }
      }

      alignedPixelMap.writeBufferToPixelsSync(dstPixels.buffer);
      return alignedPixelMap;
    } catch (error) {
      console.error(`[FaceDetector] 对齐失败: ${error}`);
      return null;
    }
  }

  /**
   * 估计仿射变换矩阵(简化版最小二乘法)
   */
  private estimateAffineTransform(src: FaceLandmarks, dst: FaceLandmarks): number[] {
    // 使用5个关键点对求解2×3仿射矩阵
    // 简化实现:使用前3个点(两眼+鼻尖)估计
    const srcPts = [src.leftEye, src.rightEye, src.nose];
    const dstPts = [dst.leftEye, dst.rightEye, dst.nose];

    // 构建3×3线性方程组 A·t = b
    // 使用简化的2点+质心方法
    const srcCenter = {
      x: (srcPts[0].x + srcPts[1].x) / 2,
      y: (srcPts[0].y + srcPts[1].y) / 2
    };
    const dstCenter = {
      x: (dstPts[0].x + dstPts[1].x) / 2,
      y: (dstPts[0].y + dstPts[1].y) / 2
    };

    // 计算旋转角度
    const srcAngle = Math.atan2(srcPts[1].y - srcPts[0].y, srcPts[1].x - srcPts[0].x);
    const dstAngle = Math.atan2(dstPts[1].y - dstPts[0].y, dstPts[1].x - dstPts[0].x);
    const rotation = dstAngle - srcAngle;

    // 计算缩放
    const srcDist = Math.sqrt(
      Math.pow(srcPts[1].x - srcPts[0].x, 2) + Math.pow(srcPts[1].y - srcPts[0].y, 2)
    );
    const dstDist = Math.sqrt(
      Math.pow(dstPts[1].x - dstPts[0].x, 2) + Math.pow(dstPts[1].y - dstPts[0].y, 2)
    );
    const scale = dstDist / srcDist;

    // 构建逆仿射矩阵(从目标到源)
    const cos = Math.cos(-rotation) / scale;
    const sin = Math.sin(-rotation) / scale;

    return [
      cos, -sin, srcCenter.x - cos * dstCenter.x + sin * dstCenter.y,
      sin, cos, srcCenter.y - sin * dstCenter.x - cos * dstCenter.y
    ];
  }

  release(): void {
    if (this.session !== null) {
      this.model?.freeSession(this.session);
      this.session = null;
    }
    if (this.model !== null) {
      this.model.freeModel();
      this.model = null;
    }
    this.isInitialized = false;
  }
}

3.3 人脸特征提取与比对

// FaceRecognizer.ets - 人脸特征提取与比对引擎
import { mindspore } from '@kit.MindSporeLiteKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
import { fs } from '@kit.CoreFileKit';
import { FaceEmbedding } from './FaceTypes';

/**
 * 人脸特征库
 * 存储已注册的人脸特征向量
 */
export class FaceFeatureDatabase {
  private features: Map<string, FaceEmbedding> = new Map();

  /**
   * 注册人脸特征
   */
  register(personId: string, embedding: FaceEmbedding): void {
    this.features.set(personId, embedding);
    console.info(`[FaceDB] 注册人脸: ${personId}`);
  }

  /**
   * 删除已注册的人脸特征
   */
  unregister(personId: string): void {
    this.features.delete(personId);
  }

  /**
   * 查找最匹配的人脸
   * @returns 匹配的personId和相似度分数,如果没有匹配返回null
   */
  findBestMatch(query: FaceEmbedding, threshold: number = 0.5): { personId: string; score: number } | null {
    let bestMatch: { personId: string; score: number } | null = null;

    this.features.forEach((storedEmbedding, personId) => {
      const similarity = query.cosineSimilarity(storedEmbedding);
      if (similarity > threshold && (bestMatch === null || similarity > bestMatch.score)) {
        bestMatch = { personId, score: similarity };
      }
    });

    return bestMatch;
  }

  /**
   * 获取已注册人数
   */
  getRegisteredCount(): number {
    return this.features.size;
  }
}

/**
 * 人脸特征提取器
 * 使用ArcFace模型提取512维人脸特征
 */
export class FaceRecognizerEngine {
  private context: common.Context;
  private session: mindspore.Session | null = null;
  private model: mindspore.Model | null = null;
  private isInitialized: boolean = false;
  private featureDB: FaceFeatureDatabase = new FaceFeatureDatabase();

  constructor(context: common.Context) {
    this.context = context;
  }

  /**
   * 初始化特征提取模型
   */
  async initialize(): Promise<boolean> {
    try {
      const modelPath = await this.copyModelToSandbox('arcface', 'arcface.ms');

      const msContext: mindspore.Context = {};
      const npuDevice: mindspore.DeviceInfo = {
        deviceType: mindspore.DeviceType.kNPU,
        enableFloat16: true
      };
      const cpuDevice: mindspore.DeviceInfo = {
        deviceType: mindspore.DeviceType.kCPU,
        enableFloat16: true,
        cpuCores: [0, 1, 2, 3]
      };
      msContext.deviceInfos = [npuDevice, cpuDevice];

      this.model = new mindspore.Model();
      let result = this.model.loadModelFromFile(modelPath, msContext);
      if (result !== mindspore.kMSStatusSuccess) {
        msContext.deviceInfos = [cpuDevice];
        result = this.model.loadModelFromFile(modelPath, msContext);
        if (result !== mindspore.kMSStatusSuccess) return false;
      }

      this.session = this.model.createSession(msContext);
      this.isInitialized = this.session !== null;
      return this.isInitialized;
    } catch (error) {
      console.error(`[FaceRecognizer] 初始化失败: ${error}`);
      return false;
    }
  }

  private async copyModelToSandbox(name: string, filename: string): Promise<string> {
    const sandboxPath = `${this.context.filesDir}/${filename}`;
    if (fs.accessSync(sandboxPath)) return sandboxPath;
    const content = this.context.resourceMgr.getRawFileContentSync(`models/${filename}`);
    const file = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
    fs.writeSync(file.fd, content.buffer);
    fs.closeSync(file);
    return sandboxPath;
  }

  /**
   * 提取人脸特征向量
   * 输入:对齐后的112×112人脸图像
   * 输出:512维特征向量
   */
  async extractFeature(alignedFace: image.PixelMap): Promise<FaceEmbedding | null> {
    if (!this.isInitialized || this.session === null) return null;

    try {
      // 预处理:112×112 → NCHW + Normalize
      const imageInfo = alignedFace.getImageInfo();
      const pixelBytes = new Uint8Array(imageInfo.size.width * imageInfo.size.height * 4);
      alignedFace.readPixelsToBufferSync(pixelBytes.buffer);

      const size = 112;
      const inputData = new Float32Array(3 * size * size);
      const means = [127.5, 127.5, 127.5];
      const stds = [127.5, 127.5, 127.5];

      for (let h = 0; h < size; h++) {
        for (let w = 0; w < size; w++) {
          const srcIdx = (h * size + w) * 4;
          const r = (pixelBytes[srcIdx] - means[0]) / stds[0];
          const g = (pixelBytes[srcIdx + 1] - means[1]) / stds[1];
          const b = (pixelBytes[srcIdx + 2] - means[2]) / stds[2];

          const dstIdx = h * size + w;
          inputData[0 * size * size + dstIdx] = r;
          inputData[1 * size * size + dstIdx] = g;
          inputData[2 * size * size + dstIdx] = b;
        }
      }

      // 推理
      const inputs = this.session.getInputs();
      inputs[0].setData(inputData.buffer);
      this.session.run(inputs);

      // 获取512维特征
      const outputs = this.session.getOutputs();
      const featureData = new Float32Array(outputs[0].getData());

      // L2归一化
      let norm = 0;
      for (let i = 0; i < featureData.length; i++) {
        norm += featureData[i] * featureData[i];
      }
      norm = Math.sqrt(norm);

      const normalizedFeature = new Float32Array(featureData.length);
      for (let i = 0; i < featureData.length; i++) {
        normalizedFeature[i] = featureData[i] / norm;
      }

      return new FaceEmbedding(normalizedFeature, '');
    } catch (error) {
      console.error(`[FaceRecognizer] 特征提取失败: ${error}`);
      return null;
    }
  }

  /**
   * 注册人脸
   */
  async registerFace(personId: string, alignedFace: image.PixelMap): Promise<boolean> {
    const embedding = await this.extractFeature(alignedFace);
    if (embedding === null) return false;

    embedding.personId = personId;
    this.featureDB.register(personId, embedding);
    return true;
  }

  /**
   * 识别人脸
   */
  async recognizeFace(alignedFace: image.PixelMap, threshold: number = 0.5): Promise<{ personId: string; score: number } | null> {
    const embedding = await this.extractFeature(alignedFace);
    if (embedding === null) return null;

    return this.featureDB.findBestMatch(embedding, threshold);
  }

  /**
   * 获取特征库
   */
  getFeatureDB(): FaceFeatureDatabase {
    return this.featureDB;
  }

  release(): void {
    if (this.session !== null) {
      this.model?.freeSession(this.session);
      this.session = null;
    }
    if (this.model !== null) {
      this.model.freeModel();
      this.model = null;
    }
    this.isInitialized = false;
  }
}

3.4 活体检测与完整人脸识别页面

// FaceRecognitionPage.ets - 人脸识别完整页面
import { image } from '@kit.ImageKit';
import { camera } from '@kit.CameraKit';
import { picker } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { FaceDetection, LivenessResult, FaceRecognitionResult } from './FaceTypes';
import { FaceDetectorEngine } from './FaceDetector';
import { FaceRecognizerEngine } from './FaceRecognizer';

@Entry
@Component
struct FaceRecognitionPage {
  @State faceResults: FaceRecognitionResult[] = [];
  @State previewUri: string = '';
  @State isLoading: boolean = false;
  @State isDetectorReady: boolean = false;
  @State isRecognizerReady: boolean = false;
  @State statusMessage: string = '请选择包含人脸的图片';
  @State registeredCount: number = 0;

  private detector: FaceDetectorEngine | null = null;
  private recognizer: FaceRecognizerEngine | null = null;

  aboutToAppear() {
    this.initEngines();
  }

  aboutToDisappear() {
    this.detector?.release();
    this.recognizer?.release();
  }

  async initEngines() {
    const context = getContext(this) as common.Context;

    // 初始化人脸检测器
    this.detector = new FaceDetectorEngine(context);
    this.isDetectorReady = await this.detector.initialize();

    // 初始化特征提取器
    this.recognizer = new FaceRecognizerEngine(context);
    this.isRecognizerReady = await this.recognizer.initialize();

    if (this.isDetectorReady && this.isRecognizerReady) {
      this.statusMessage = '引擎就绪,请选择图片';
    }
  }

  /**
   * 简化的静默活体检测
   * 基于人脸关键点间距和角度判断
   */
  private simpleLivenessCheck(detection: FaceDetection): LivenessResult {
    const { landmarks, bbox } = detection;
    const faceWidth = bbox.x2 - bbox.x1;
    const faceHeight = bbox.y2 - bbox.y1;

    // 检查1:人脸宽高比(真人通常在0.7-1.3之间)
    const aspectRatio = faceWidth / faceHeight;
    if (aspectRatio < 0.6 || aspectRatio > 1.4) {
      return {
        isLive: false,
        score: 0.2,
        livenessType: '静默检测',
        attackType: '异常宽高比(可能是照片)'
      };
    }

    // 检查2:两眼间距与脸部宽度的比例
    const eyeDistance = Math.sqrt(
      Math.pow(landmarks.rightEye.x - landmarks.leftEye.x, 2) +
      Math.pow(landmarks.rightEye.y - landmarks.leftEye.y, 2)
    );
    const eyeRatio = eyeDistance / faceWidth;
    if (eyeRatio < 0.25 || eyeRatio > 0.6) {
      return {
        isLive: false,
        score: 0.3,
        livenessType: '静默检测',
        attackType: '异常眼睛间距'
      };
    }

    // 检查3:人脸面积占比(太小说明可能距离过远,太大说明可能是近距照片)
    const faceArea = faceWidth * faceHeight;
    const faceAreaRatio = faceArea / (640 * 640);  // 假设图像640×640
    if (faceAreaRatio < 0.05 || faceAreaRatio > 0.8) {
      return {
        isLive: false,
        score: 0.4,
        livenessType: '静默检测',
        attackType: '异常人脸面积'
      };
    }

    // 综合评分
    const score = 0.7 + Math.random() * 0.2;  // 简化评分
    return {
      isLive: true,
      score: score,
      livenessType: '静默检测'
    };
  }

  async pickAndRecognize() {
    if (!this.isDetectorReady) return;

    try {
      this.isLoading = true;
      this.faceResults = [];

      const photoSelectOptions = new picker.PhotoSelectOptions();
      photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 1;

      const photoViewPicker = new picker.PhotoViewPicker();
      const result = await photoViewPicker.select(photoSelectOptions);

      if (result.photoUris.length === 0) {
        this.isLoading = false;
        return;
      }

      this.previewUri = result.photoUris[0];

      // 解码图像
      const imageSource = image.createImageSource(result.photoUris[0]);
      const pixelMap = await imageSource.createPixelMap();

      // 第一步:人脸检测
      const detections = await this.detector!.detect(pixelMap);
      if (detections.length === 0) {
        this.statusMessage = '未检测到人脸';
        this.isLoading = false;
        return;
      }

      this.statusMessage = `检测到 ${detections.length} 张人脸`;

      // 第二步:对每张人脸进行识别
      const recognitionResults: FaceRecognitionResult[] = [];

      for (const detection of detections) {
        // 活体检测
        const liveness = this.simpleLivenessCheck(detection);

        const recResult: FaceRecognitionResult = {
          detection: detection,
          liveness: liveness
        };

        // 如果活体通过,进行特征提取和比对
        if (liveness.isLive && this.isRecognizerReady) {
          // 人脸对齐
          const alignedFace = this.detector!.alignFace(pixelMap, detection.landmarks);
          if (alignedFace !== null) {
            // 特征提取 + 比对
            const match = await this.recognizer!.recognizeFace(alignedFace);
            if (match !== null) {
              recResult.matchedPerson = match.personId;
              recResult.matchScore = match.score;
            }
          }
        }

        recognitionResults.push(recResult);
      }

      this.faceResults = recognitionResults;
      this.registeredCount = this.recognizer?.getFeatureDB().getRegisteredCount() || 0;
      this.isLoading = false;
    } catch (error) {
      console.error(`[Page] 识别失败: ${error}`);
      this.isLoading = false;
    }
  }

  build() {
    Scroll() {
      Column() {
        // 标题栏
        Row() {
          Text('人脸识别')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        .width('100%')
        .height(56)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#1A1A2E')

        // 状态信息
        Text(this.statusMessage)
          .fontSize(14)
          .fontColor('#4FC3F7')
          .margin({ top: 12 })

        // 操作按钮
        Button('选择图片识别')
          .width(200)
          .height(48)
          .fontSize(16)
          .backgroundColor('#4FC3F7')
          .fontColor('#1A1A2E')
          .borderRadius(24)
          .enabled(this.isDetectorReady && !this.isLoading)
          .onClick(() => this.pickAndRecognize())
          .margin({ top: 12 })

        // 人脸识别结果
        if (this.faceResults.length > 0) {
          Column() {
            Text('识别结果')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .margin({ bottom: 12 })

            ForEach(this.faceResults, (result: FaceRecognitionResult, index: number) => {
              Column() {
                // 人脸信息
                Row() {
                  Text(`人脸 #${index + 1}`)
                    .fontSize(15)
                    .fontWeight(FontWeight.Medium)
                    .fontColor('#FFFFFF')

                  Text(`置信度: ${(result.detection.confidence * 100).toFixed(1)}%`)
                    .fontSize(13)
                    .fontColor('#AAAAAA')
                    .margin({ left: 12 })
                }

                // 活体检测结果
                Row() {
                  Circle({ width: 10, height: 10 })
                    .fill(result.liveness.isLive ? '#4CAF50' : '#F44336')

                  Text(result.liveness.isLive ? '真人' : '假脸')
                    .fontSize(14)
                    .fontColor(result.liveness.isLive ? '#4CAF50' : '#F44336')
                    .margin({ left: 6 })

                  Text(`活体分数: ${(result.liveness.score * 100).toFixed(1)}%`)
                    .fontSize(12)
                    .fontColor('#AAAAAA')
                    .margin({ left: 12 })

                  if (result.liveness.attackType) {
                    Text(result.liveness.attackType)
                      .fontSize(11)
                      .fontColor('#F44336')
                      .margin({ left: 8 })
                  }
                }
                .margin({ top: 8 })

                // 匹配结果
                if (result.matchedPerson) {
                  Row() {
                    Text('✓ 匹配成功')
                      .fontSize(14)
                      .fontColor('#4CAF50')
                    Text(`${result.matchedPerson}`)
                      .fontSize(14)
                      .fontColor('#4FC3F7')
                      .margin({ left: 8 })
                    Text(`相似度: ${(result.matchScore! * 100).toFixed(1)}%`)
                      .fontSize(12)
                      .fontColor('#AAAAAA')
                      .margin({ left: 8 })
                  }
                  .margin({ top: 8 })
                } else if (result.liveness.isLive) {
                  Text('未匹配到已注册人脸')
                    .fontSize(13)
                    .fontColor('#FFB74D')
                    .margin({ top: 8 })
                }

                // 关键点信息
                Text(`关键点: 左眼(${result.detection.landmarks.leftEye.x.toFixed(0)},${result.detection.landmarks.leftEye.y.toFixed(0)}) ` +
                  `右眼(${result.detection.landmarks.rightEye.x.toFixed(0)},${result.detection.landmarks.rightEye.y.toFixed(0)})`)
                  .fontSize(11)
                  .fontColor('#666666')
                  .margin({ top: 6 })
              }
              .width('100%')
              .padding(12)
              .borderRadius(8)
              .backgroundColor('#1E1E3A')
              .margin({ bottom: 8 })
            })
          }
          .width('92%')
          .padding(16)
          .borderRadius(12)
          .backgroundColor('#16213E')
          .margin({ top: 16 })
        }

        if (this.isLoading) {
          LoadingProgress()
            .width(48)
            .height(48)
            .color('#4FC3F7')
            .margin({ top: 20 })
        }
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0F0F23')
  }
}

四、踩坑与注意事项

4.1 人脸对齐精度

:对齐后的人脸歪歪扭扭,特征提取效果差。

原因:仿射变换矩阵计算不准确,特别是当人脸有大角度偏转时,3点法不够精确。

  1. 使用5点最小二乘法代替3点法,提高对齐精度
  2. 对齐后检查两眼是否水平,如果不是则重新计算
  3. 推荐使用OpenCV的estimateAffinePartial2D算法(需移植到ArkTS)

4.2 特征比对阈值选择

:阈值设高了识别不到人,设低了误识率高。

原因:不同场景对安全性的要求不同,"刷脸支付"需要高阈值(0.6+),"考勤打卡"可以用低阈值(0.4)。

  • 高安全场景:阈值0.55-0.65,配合活体检测
  • 普通场景:阈值0.45-0.55
  • 宽松场景:阈值0.35-0.45
  • 建议提供"阈值调节"功能,让用户根据场景调整

4.3 活体检测安全性

:简单的静默活体检测容易被高清照片和视频攻击。

原因:纯RGB的静默检测本质上是在做纹理分析,高清打印照片和屏幕播放视频可以骗过大部分规则。

  1. 红外活体:利用红外摄像头检测皮肤反射特征,照片和屏幕无法模拟
  2. 深度活体:利用ToF/结构光获取3D深度信息,平面攻击无效
  3. 配合式活体:要求用户做随机动作(眨眼、张嘴、转头),增加攻击难度
  4. 多模态融合:RGB + 红外 + 深度,三重验证

4.4 特征向量存储安全

:人脸特征向量明文存储在沙箱中,存在被窃取的风险。

  1. 使用HarmonyOS的**安全区域(TEE)**存储特征向量
  2. 特征向量加密后再存储
  3. 比对操作在TEE中完成,特征不出安全区域
  4. 使用HUKS(Universal KeyStore)进行密钥管理
// 使用HUKS加密特征向量
import { huks } from '@kit.UniversalKeystoreKit';

async function encryptFeature(feature: Float32Array): Promise<Uint8Array> {
  const keyAlias = 'face_feature_key';
  const properties: huks.HuksOptions = {
    properties: [
      { tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES },
      { tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT },
      { tag: huks.HuksTag.HUKS_TAG_PADDING, value: huks.HuksKeyPadding.HUKS_PADDING_PKCS7 },
      { tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE, value: huks.HuksCipherMode.HUKS_MODE_CBC }
    ],
    inData: new Uint8Array(feature.buffer)
  };
  const result = huks.finishSession(keyAlias, properties);
  return new Uint8Array(result.outData);
}

五、HarmonyOS 6适配

5.1 新增特性

特性 说明
人脸检测系统API @kit.FaceRecognitionKit,无需自部署模型
红外活体内置 系统级红外活体检测,安全性更高
3D人脸支持 支持ToF深度摄像头的人脸识别
TEE特征存储 系统级安全存储,特征不出TEE
持续认证 解锁后持续监控人脸,离开自动锁定

5.2 迁移指南

  1. 使用系统人脸检测API(HarmonyOS 6推荐):
// HarmonyOS 6 系统级人脸检测
import { faceRecognition } from '@kit.FaceRecognitionKit';

const detector = faceRecognition.createFaceDetector();
const faces = await detector.detect(pixelMap);
// faces: FaceInfo[],包含位置、关键点、姿态角
  1. 系统级活体检测
// HarmonyOS 6 红外活体检测
const livenessDetector = faceRecognition.createLivenessDetector({
  type: faceRecognition.LivenessType.IR  // 红外活体
});
const livenessResult = await livenessDetector.detect(irPixelMap);
  1. TEE安全存储
// HarmonyOS 6 安全特征存储
const secureStore = faceRecognition.createSecureFeatureStore();
await secureStore.registerFeature(personId, embedding);
const matchResult = await secureStore.matchFeature(queryEmbedding);

六、总结

本文完整讲解了HarmonyOS端侧人脸识别与活体检测的技术方案,核心知识点回顾:

人脸识别与活体检测
├── 人脸检测
│   ├── RetinaFace:端侧首选,输出框+5关键点
│   ├── MTCNN:三阶段级联,精度高但慢
│   └── BlazeFace:超轻量,速度最快
├── 人脸对齐
│   ├── 5点仿射变换:对齐到112×112
│   ├── 最小二乘法求解变换矩阵
│   └── 双线性插值避免像素丢失
├── 特征提取
│   ├── ArcFace:512维特征向量
│   ├── L2归一化 + 余弦相似度比对
│   └── 阈值选择:高安全0.55+,普通0.45+
├── 活体检测
│   ├── 静默式:纹理/边缘分析,安全性中等
│   ├── 红外式:皮肤反射特征,安全性高
│   ├── 深度式:3D深度信息,安全性最高
│   └── 配合式:随机动作验证,用户体验差
├── 安全存储
│   ├── TEE安全区域存储特征
│   ├── HUKS加密特征向量
│   └── 比对在TEE中完成
└── 踩坑要点
    ├── 对齐精度影响识别率
    ├── 阈值需根据场景调整
    ├── 纯RGB活体容易被攻击
    └── 特征向量必须加密存储

一句话总结:人脸识别的安全链路是"检测→对齐→活体→提取→比对→存储",每一个环节都是安全防线。活体检测是防假脸攻击的关键,TEE安全存储是防特征泄露的底线。端侧人脸识别不是简单的"比对两张脸",而是一个系统工程——检测要准、对齐要精、活体要严、存储要安全,四者缺一不可。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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