APP人脸识别增值版Harmony Demo实操与关键代码解析

本文基于 ArcfaceDemo HarmonyOS 示例工程,围绕 ArcSoft ArcFace SDK 在 HarmonyOS ArkTS 项目中的接入、激活、相机实时流处理、人脸注册识别、活体检测、人脸属性分析、图片 1:1 比对和人脸管理进行实操拆解。示例工程的核心思路是:页面负责交互和绘制,Camera Worker 负责采集帧,算法 Worker 负责人脸检测、活体和特征处理,文件 Worker 负责注册照和特征文件落盘。

一、Demo 功能概览

从首页入口可以看到,该 Demo 覆盖了人脸识别增值版常见能力:

  1. 人脸识别:实时预览中检测人脸,支持现场注册、历史人脸批量加载和识别比对。
  2. 活体检测:包含 RGB 活体、交互式动作活体、炫光活体、交互式炫光活体。
  3. 人脸属性:对图片做人脸检测,并输出年龄、性别、活体等属性。
  4. 图片 1:1 比对:选择注册照和识别照,提取特征后计算相似度。
  5. 人脸管理:展示沙箱中的注册照和特征文件,支持删除、更新特征。
  6. 参数设置:通过 AppStoragePreferences 管理活体开关和活体阈值。
  7. 引擎激活:输入 APP_IDSDK_KEY 后调用 SDK 在线激活接口。

相关入口配置在 entry/src/main/resources/base/profile/router_map.json,首页菜单在 entry/src/main/ets/pages/Index.ets 中定义。

二、工程接入与运行准备

运行环境

说明
操作系统 HarmonyOS 5.0.5(API 17),runtimeOS: HarmonyOS
开发工具 DevEco Studio(建议最新正式版)
开发语言 ArkTS(Stage 模型)
编译工具 hvigor(工程内置)
SDK 依赖 arcsoft_face 本地 HAR 包,位于 entry/libs/arcsoft_face.har
目标设备 真机,需具备前置摄像头并已授予相机权限(模拟器无摄像头不可用)
签名 build-profile.json5 中配置 default 签名(调试证书 .cer/.p7b/.p12

SDK 版本与兼容版本均锁定在 5.0.5(17),定义在根 build-profile.json5products 中:

json 复制代码
"targetSdkVersion": "5.0.5(17)",
"compatibleSdkVersion": "5.0.5(17)"

Demo 的 SDK 依赖通过本地 HAR 包接入,配置文件是 entry/oh-package.json5

json 复制代码
{
  "dependencies": {
    "arcsoft_face": "file:../entry/libs/arcsoft_face.har"
  }
}

模块权限配置在 entry/src/main/module.json5。当前源码中主要声明了相机权限:

bash 复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

实操时建议按下面步骤运行:

  1. 使用 DevEco Studio 打开 samplecode/ArcfaceDemo
  2. 确认 entry/libs/arcsoft_face.har 存在,且 oh-package.json5 能正确解析本地依赖。
  3. 在真机上运行,确保设备有前置摄像头并已授予相机权限。
  4. Constants.ets 中替换为自己的 APP_IDSDK_KEY,不要把生产密钥写入公开文章或代码仓库。
  5. 进入首页的"激活引擎",完成 SDK 激活后再体验识别、活体等功能。

如果实际产品需要在线激活或访问网络接口,应结合业务补充网络权限和隐私合规说明。

三、启动链路

应用入口是 entry/src/main/ets/entryability/EntryAbility.ets。启动时会先初始化参数管理器并加载用户协议同意状态:

csharp 复制代码
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
  ParamManager.init(this.context);
  await preferencesUtil.loadPreferences(this.context, Constants.AGREE_NAME);
}

窗口创建后加载首页:

javascript 复制代码
windowStage.loadContent('pages/Index', (err) => {
  if (err.code) {
    return;
  }
});

首页 Index.ets 做了三件关键事:

  1. 首次进入时弹出隐私协议弹窗。
  2. 用户同意后调用 PermissionManager.request 申请相机权限。
  3. 使用 FaceEngine.init 做基础初始化并读取 SDK 版本。

关键代码如下:

csharp 复制代码
async onAccept() {
  await preferencesUtil.putPreferencesValue(Constants.AGREE_NAME, Constants.IS_AGREE_NAME, true);
  await PermissionManager.request(Constants.PERMISSIONS, this.context);
  await this.initFaceEngine();
}
​
private async initFaceEngine() {
  const initRes = await this.faceEngine.init(
    this.context,
    DetectMode.ASF_DETECT_MODE_IMAGE,
    OrientPriority.ASF_OP_ALL_OUT,
    10,
    CombinedMask.ASF_FACE_DETECT | CombinedMask.ASF_FACE_RECOGNITION
  );
}

这里使用的是图片检测模式 ASF_DETECT_MODE_IMAGE,主要用于首页校验 SDK 状态和获取版本号。实时视频识别和活体页面会在 Worker 中重新以视频模式初始化引擎。

四、激活引擎

激活页面位于 entry/src/main/ets/pages/ActiveEngine.ets,激活流程分三步:

  1. 初始化引擎:页面打开时初始化一个能力更完整的引擎,包含检测、识别、年龄、性别和活体能力。
kotlin 复制代码
await this.faceEngine.init(
  this.context,
  DetectMode.ASF_DETECT_MODE_IMAGE,
  OrientPriority.ASF_OP_ALL_OUT,
  10,
  CombinedMask.ASF_FACE_DETECT |
  CombinedMask.ASF_FACE_RECOGNITION |
  CombinedMask.ASF_AGE |
  CombinedMask.ASF_GENDER |
  CombinedMask.ASF_LIVENESS
);
  1. 在线激活 :点击激活按钮调用 SDK 的 active 接口,成功后更新激活状态。
kotlin 复制代码
let res: number = await this.faceEngine.active(this.context, this.appId, this.sdkKey);
if (res !== ErrorInfo.ARC_OK) {
  Utils.showToast(Utils.getErrorMessage2(res));
  return;
}
​
ParamManager.update(Constants.ACTIVE_NAME, "已激活");
  1. 授权门禁 :首页通过 @StorageLink('activeStatus') 读取激活状态,未激活时点击需授权功能会自动跳转到激活页面。
javascript 复制代码
navigateTo(module: ModuleItem) {
  if (module.requiresActive && this.activeStatus === '未激活') {
    this.pageInfos.pushPath({ name: 'activeEngine' });
  } else {
    this.pageInfos.pushPath({ name: module.name });
  }
}

这个设计把"授权门禁"放在首页统一处理,避免每个业务页面重复判断。

五、相机实时流

实时识别和活体检测都依赖相机帧。Demo 将相机逻辑拆成 CameraServiceCameraWorker

  1. 双路预览CameraService.ets 创建两条预览流,一路用于页面显示,一路用于获取帧数据交给算法 Worker。
kotlin 复制代码
// 第一条预览流绑定到 XComponent 的 Surface,用于页面显示
this.previewOutput =
  this.cameraManager.createPreviewOutput(previewProfilesObj, XComponentSurfaceId);
​
// 第二条预览流绑定到 ImageReceiver,用于获取帧数据交给算法 Worker
this.receiver = image.createImageReceiver(size, image.ImageFormat.JPEG, 8);
let imageReceiverSurfaceId = await this.receiver.getReceivingSurfaceId();
this.previewOutput2 =
  this.cameraManager.createPreviewOutput(previewProfilesObj2, imageReceiverSurfaceId);
  1. 帧回调 :帧到达后,onImageArrival 读取 ImageReceiver 中的图像数据,并通过回调交给 CameraWorker
javascript 复制代码
receiver.on('imageArrival', () => {
  receiver.readNextImage((err, nextImage) => {
    nextImage.getComponent(image.ComponentType.JPEG, (err, imgComponent) => {
      if (this.onImageCallback) {
        this.onImageCallback(imgComponent.byteBuffer as ArrayBuffer, size.width, size.height);
      }
      nextImage.release();
    });
  });
});
  1. 共享内存双缓冲CameraWorker.ets 使用 SharedArrayBuffer 做双缓冲,缓冲区头部用 4 个 Int32 保存控制信息 [seq, writeIdx, readIdx, status]
ini 复制代码
const HEADER_INTS = 4; // [seq, writeIdx, readIdx, status]
headerView = new Int32Array(sab, 0, HEADER_INTS);
frameBytes = Math.ceil((width * height * 3) / 2) + 2048;
frame0Offset = HEADER_BYTES;
frame1Offset = HEADER_BYTES + frameBytes;
  1. 写入帧 :Camera Worker 选择另一个缓冲区写入,并把状态标记为"新帧就绪",然后发 FRAME_READY 通知算法 Worker。
ini 复制代码
let writeIdx = (Atomics.load(headerView, 1) + 1) % 2;
const dstOffset = writeIdx === 0 ? frame0Offset : frame1Offset;
​
const pixelView = new Uint8Array(sab, dstOffset, frameBytes);
pixelView.set(u8Buffer.subarray(0, frameBytes));
​
const newSeq = Atomics.add(headerView, 0, 1) + 1;
Atomics.store(headerView, 1, writeIdx);
Atomics.store(headerView, 2, writeIdx);
Atomics.store(headerView, 3, 2);
​
workerPort.postMessage({ action: WorkerActions.FRAME_READY });

这个设计的好处是主线程不直接处理大块图像数据,Worker 之间也不用每帧复制完整图片对象,只需共享同一块内存并通过 Atomics 协调读写状态。实际适配不同设备时,要重点确认 ImageReceiver 输出格式与 SDK 侧 ImageFormat.CP_PAF_NV21 的输入格式一致。

六、人脸识别

人脸识别页面是 entry/src/main/ets/pages/RecognizeAndRegister.ets,整体链路为 CameraWorker -> FRWorker -> FTWorker -> FileWorker。Demo 共有 6 个 Worker,下表统一列出各自职责,后续活体章节不再重复。

Worker 职责

Worker 初始化能力(CombinedMask) 职责
CameraWorker 无引擎 相机双路预览 + 帧采集,写入 SharedArrayBuffer,发 FRAME_READY 通知算法 Worker
FDWorker ASF_FACE_DETECT(视频模式) 轻量人脸检测,返回人脸坐标 FD_RESULT,供活体页面绘制人脸框
FLWorker `ASF_FACE_DETECT ASF_LIVENESS
FRWorker `ASF_FACE_DETECT ASF_FACE_RECOGNITION`(视频模式)
FTWorker `ASF_FACE_DETECT ASF_FACE_RECOGNITION`(视频模式)
FileWorker 无引擎 保存 .dat 特征文件并按人脸框裁剪注册头像到沙箱 filesDir/registerImages

人脸识别链路只用其中 CameraWorker -> FRWorker -> FTWorker -> FileWorker 四个;活体链路用 CameraWorker + FDWorker + FLWorker 三个。所有算法 Worker 均以 DetectMode.ASF_DETECT_MODE_VIDEO(视频模式)初始化,区别于首页的图片模式引擎。

  1. 初始化:页面创建相机 Worker、识别管理器和共享缓冲区,并批量加载已注册人脸。
ini 复制代码
const FRAME_BYTES = Math.ceil(Constants.IMAGE_WIDTH * Constants.IMAGE_HEIGHT * 3 / 2) + 2048;
const SAB_SIZE = HEADER_BYTES + FRAME_BYTES * 2;
let sab = new SharedArrayBuffer(SAB_SIZE);
​
this.cameraWorker.postMessage({
  action: WorkerActions.INIT_CAMERA,
  context,
  surfaceId,
  sab
});
​
this.faceRecognitionManager?.initialize(context, sab);
this.faceRecognitionManager?.registerAllFace(this.context.filesDir + "/registerImages");
  1. 帧分发 :Camera Worker 发出 FRAME_READY 时,页面不直接跑算法,而是交给 FaceRecognitionManager,由它转发给 FRWorker
ini 复制代码
this.cameraWorker.onmessage = (e: MessageEvents): void => {
  if (e.data.action === WorkerActions.FRAME_READY) {
    this.faceRecognitionManager?.processFrame(context);
  }
};

FaceRecognitionManager 还维护了几个 UI 状态映射:

  1. registeredFaces:已注册人脸信息。
  2. faceDrawMap:每张脸在当前帧中的绘制状态。
  3. pendingRecognitionMap:识别失败后的重试次数。
  4. faceIdToSearchId:实时人脸 ID 和注册库 searchId 的映射。
  5. 检测FRWorker 检测当前帧中的人脸,并根据是否触发注册来决定后续提取类型(注册或识别)。
ini 复制代码
const code = frEngine.detectFaces(
  frameCopy,
  imageInfo.width,
  imageInfo.height,
  DetectModel.ASF_DETECT_MODEL_RGB,
  imageInfo.imageFormat,
  faceInfoList
);
​
ftWorker.postMessage({
  action: WorkerActions.EXTRACT_FEATURE,
  extractType: registerNextFrame ? ExtractType.ASF_REGISTER : ExtractType.ASF_RECOGNITION,
  seq,
  sab,
  offset,
  frameBytes,
  imageInfo,
  faceInfoList,
  context,
  appConfig
});
  1. 特征提取FTWorker 负责特征提取。
arduino 复制代码
const extractRes = ftEngine.extractFaceFeature(
  frameCopy,
  width,
  height,
  imageFormat,
  singleInfo,
  faceFeature
);
  1. 注册 :注册时调用 registerFaceFeature,并把注册照和特征文件交给 FileWorker 保存。
ini 复制代码
const featureOfReg: FaceRegisterFeature = {
  searchId,
  feature: { featureSize: faceFeature.featureSize, feature: faceFeature.feature },
  tag: `User_${searchId}`
};
​
const registerRes = ftEngine.registerFaceFeature(featureOfReg);
fileWorker.postMessage(saveMsg);
  1. 识别 :识别时调用 searchFaceFeatureFRWorker 收到搜索结果后以相似度大于 0.8 作为识别成功条件。
ini 复制代码
const resSearch = ftEngine.searchFaceFeature(
  faceFeature.feature,
  CompareModel.ASF_LIFE_PHOTO,
  searchResult
);
php 复制代码
workerPort.postMessage({
  action: WorkerActions.FR_RESULT,
  type: 'recognize',
  success: similarity > 0.8,
  faceId: payload.faceId,
  searchId: matchedSearchId,
  name: searchResult?.featureInfo?.tag,
  similarity
});
  1. 落盘 :注册头像和特征落盘由 FileWorker.ets 完成,保存 .dat 特征文件并按人脸框裁剪当前帧头像,最终文件位于 filesDir/registerImages 目录,图片名类似 User_xxx.jpg,特征文件名类似 xxx.dat
css 复制代码
await ImageUtils.saveFeatureFile(msg.context, msg.searchId, msg.faceFeature);
​
const headImgPath = await ImageUtils.cropImage(
  msg.context,
  frameCopy.buffer,
  left,
  top,
  right - left,
  bottom - top,
  msg.tag
);

七、人脸框绘制

实时预览中,SDK 返回的人脸框坐标属于原始图像坐标系。页面上显示的是前置摄像头预览,存在旋转、缩放和镜像问题。Demo 把绘制逻辑集中在 entry/src/main/ets/utils/DrawHelper.ets

ini 复制代码
const ratio = surfaceWidth / imageHeight;
​
const scaleLeft = Math.floor(px2vp(ratio * left));
const scaleTop = Math.floor(px2vp(ratio * top));
const scaleRight = Math.floor(px2vp(ratio * right));
const scaleBottom = Math.floor(px2vp(ratio * bottom));
​
const x = canvasWidth - scaleBottom;
const y = isMirror ? (canvasHeight - scaleRight) : scaleLeft;
const recWidth = scaleBottom - scaleTop;
const recHeight = scaleRight - scaleLeft;

这里的核心是把 640x480 帧坐标转换到当前屏幕宽度下,并根据前置镜像计算最终矩形位置。Demo 还用四角线段代替普通矩形框,让识别框更接近真实业务里的视觉效果。

八、活体检测

活体检测入口在 mainLiveness.ets(二级列表,四项功能均为独立页面),共用 CameraWorker + FDWorker + FLWorker 三组 Worker(职责见第六节总表)。所有活体页面的相机/Worker 初始化模式一致:XComponent.onLoad 时创建 SharedArrayBuffer,初始化 CameraWorker + FDWorker + FLWorkerCameraWorker 每帧 FRAME_READY 时把帧分发给 FDWorker 和 FLWorker,页面销毁时统一发 STOP_SEND_FRAME 释放。FLWorker 用一个引擎同时承载三种活体能力,通过消息标志位切换检测模式,避免重复初始化引擎。

1. RGB 活体(rgbLivenessDetect)

RgbLivenessDetect.ets 每帧触发 DO_PROCESS,FLWorker 调用 process + getLiveness 返回活体结果,页面用 300ms 节流更新框色和 ALIVE/NOT_ALIVE 文案。

关键点:RGB 活体每帧额外发 DO_PROCESS,因为 FRAME_READY 只触发 detectFaces,RGB 活体还需 process + getLiveness,故用 DO_PROCESSrgbFlag = true

kotlin 复制代码
// 页面:每帧额外触发 process
this.cameraWorker.onmessage = (e: MessageEvents): void => {
  if (e.data.action === WorkerActions.FRAME_READY) {
    this.fdWorker.postMessage({ action });
    this.flWorker.postMessage({ action });
    this.flWorker.postMessage({ action: WorkerActions.DO_PROCESS });  // 触发 rgbFlag
  }
};
​
// FLWorker process(): 设阈值 -> process -> getLiveness/getAge/getGender -> PROCESS_RESULT
const livenessParam = new LivenessParam();
livenessParam.thresholdModelBgr = Number(appConfig.livenessThreshold);
flEngine.setLivenessParam(livenessParam);
flEngine.process(frame, width, height, imageFormat, combinedMask, detectFaceInfoList);
flEngine.getLiveness(livenessList);
​
// 页面:300ms 节流更新框色
if (data?.livenessList[0].isLive === LivenessType.ALIVE) {
  this.drawHelper.setBorderColor(BorderColor.GREEN);
  this.drawHelper.setDynamicText("ALIVE");
} else {
  this.drawHelper.setBorderColor(BorderColor.YELLOW);
  this.drawHelper.setDynamicText("NOT_ALIVE");
}

2. 交互式动作活体(interactiveLiveness)

InteractiveLiveness.ets 让用户勾选动作(眨眼/张嘴/左摇头/右摇头),按顺序依次发送 DO_ACTION 给 FLWorker,当前动作通过后 currentIndex++ 进入下一个,全部通过后完成。

关键点:动作码映射 {眨眼:1, 张嘴:2, 左摇头:3, 右摇头:4};FLWorker 每帧调用 livenessInteractiveDetectisFirst 参数在动作首帧由 actionFirstCall 控制,SDK 内部持续追踪直到完成或超时。

kotlin 复制代码
// 页面:发送动作给 FLWorker
showNextAction() {
  const currentAction = this.selectedValues[this.currentIndex];
  this.flWorker.postMessage({
    action: WorkerActions.DO_ACTION,
    actions: this.ActionMap[currentAction],
    detectGlare: false, reset: true
  });
}
​
// FLWorker 每帧做交互式检测
flEngine.livenessInteractiveDetect(
  frameCopy, width, height, ImageFormat.CP_PAF_NV21,
  singleInfo, currentAction, isFirst, detectResult
);
workerPort.postMessage({
  action: WorkerActions.INTERACTIVE_RESULT,
  success: detectResult.isLive === LivenessType.ALIVE,
});
​
// 页面:success 推进动作序列
if (data.success) {
  this.currentIndex++;
  if (isLastAction) this.handleDetectionComplete();
  else this.showNextAction();  // 继续下一个动作
}

3. 炫光活体(colorLiveness)

ColorLiveness.ets 用双 Canvas(XOR 合成模式在遮罩上挖出圆形窗口),随机生成颜色序列后按 200ms 间隔循环切换,每切换一个颜色通知 FLWorker 调用 livenessGlareDetect。Worker 侧用 isReadyForNextColor 标志位和 180ms 节流,确保与 UI 颜色切换时序协调。

关键点:UI 每 200ms 切换颜色,Worker 每 180ms 处理一次,isReadyForNextColor 确保不重复检测;currentGlareIndex === 0 作为 isFirstColor 传给 SDK;结果三态 ALIVE / NOT_ALIVE / -4(人脸角度过大)。额外处理人脸切换:FDWorker 检测到人脸 ID 变化时发 RESET_STATE,FLWorker 清空活体状态变量确保新人脸从头检测。

arduino 复制代码
// 页面:XOR 遮罩 + 颜色循环
ctx.globalCompositeOperation = 'xor';
ctx.rect(0, 0, width, height);  // 全屏填充 -> ctx.arc() 挖出圆形窗口
setTimeout(() => { this.currentGlareIndex++; this.showNextGlareColor(); }, 200);
​
// 通知 FLWorker
this.flWorker.postMessage({
  action: WorkerActions.SET_CURRENT_COLOR, color, index: this.currentGlareIndex
});
​
// FLWorker 侧:180ms 节流 + isReadyForNextColor
if (detectGlare && currentColor && !isReadyForNextColor &&
  (currentTime - lastProcessTime) >= 180) {
  flEngine.livenessGlareDetect(
    frameCopy, width, height, ImageFormat.CP_PAF_NV21, singleInfo,
    glareOrderMode, currentColor, currentGlareIndex === 0, detectResult2
  );
  isReadyForNextColor = true;
}

4. 交互式炫光活体(interactiveColorLiveness)

InteractiveColorLiveness.ets 是前两种活体的组合:随机选一个动作完成交互式检测,通过后切换到炫光检测。其 Worker 架构、SharedArrayBuffer 双缓冲、Canvas XOR 遮罩、颜色循环逻辑均与上面相同,这里聚焦组合调度逻辑。

关键点:通过 DO_ACTION 中的 detectGlare 参数和 STOP_ACTION_DETECT/START_ACTION_DETECT 消息,在同一组 Worker 上无缝串联两种活体策略,无需重新初始化引擎。

kotlin 复制代码
// 阶段1:动作检测 --- 随机选择动作
this.flWorker.postMessage({
  action: WorkerActions.DO_ACTION,
  actions: this.ActionMap[action],  // {眨眼:1, 张嘴:2, 左摇头:3, 右摇头:4}
  detectGlare: false, reset: true
});
​
// 阶段2:动作通过后切换到炫光
if (data.action === WorkerActions.INTERACTIVE_RESULT && data.success) {
  this.flWorker.postMessage({ action: WorkerActions.STOP_ACTION_DETECT });
  setTimeout(() => this.startGlareDetection(), 200);
}
​
// 阶段3:炫光检测 --- 与 ColorLiveness 相同
this.flWorker.postMessage({
  action: WorkerActions.DO_ACTION,
  actions: null, detectGlare: true, orderMode, reset: true
});
​
// 阶段4:最后一帧炫光结果判定
if (data.action === WorkerActions.GLARE_RESULT && data.index === 3) {
  this.glareDetectionResult = data.isLive;
  this.checkFinalResult();  // ALIVE / NOT_ALIVE / -4(人脸角度过大)
}

完整时序:startDetection()DO_ACTION(actions, detectGlare:false)INTERACTIVE_RESULT(success)STOP_ACTION_DETECTDO_ACTION(detectGlare:true) → 颜色循环 → GLARE_RESULT(index:3) → 最终判定。

九、图片检测

图片模式不需要相机帧和 Worker,页面直接使用 FaceEngine。图片先通过 ImageUtils.convertNv21 转成 SDK 处理格式:

javascript 复制代码
const imageData = await ImageUtils.convertNv21(this.context, this.defaultImageSrc);
let uint8ArrayData = new Uint8Array(imageData.readBuffer);

1. 人脸属性(faceAttr)

FaceAttr.ets 的核心流程是:

  1. detectFaces 检测图片中的人脸。
  2. DrawHelper.drawImageCanvas 绘制图片上的人脸框。
  3. process 计算年龄、性别和活体。
  4. 调用 getAgegetGendergetLiveness 读取结果。

关键代码:

arduino 复制代码
this.faceEngine.detectFaces(
  uint8ArrayData,
  imageData.width,
  imageData.height,
  DetectModel.ASF_DETECT_MODEL_RGB,
  ImageFormat.CP_PAF_NV21,
  faceInfoList
);
​
this.faceEngine.process(
  uint8ArrayData,
  imageData.width,
  imageData.height,
  ImageFormat.CP_PAF_NV21,
  CombinedMask.ASF_AGE | CombinedMask.ASF_GENDER | CombinedMask.ASF_LIVENESS,
  faceInfoList
);

2. 图片 1:1 比对(faceCompare)

FaceCompare.ets 流程分三步:

  1. 选择注册照,提取注册特征。
  2. 选择识别照,提取识别特征。
  3. 调用 faceFeatureCompare 计算相似度。
kotlin 复制代码
this.faceEngine.extractFaceFeature(
  uint8ArrayData,
  imageData.width,
  imageData.height,
  ImageFormat.CP_PAF_NV21,
  singleInfo,
  this.featureRegister
);
​
this.faceEngine.faceFeatureCompare(
  this.featureRegister.feature,
  this.featureRecognize.feature,
  CompareModel.ASF_LIFE_PHOTO,
  faceSimilar
);

页面展示相似度、年龄、性别等信息。这个模块适合用来验证静态图片质量、特征提取和比对阈值。

十、人脸管理

FaceManage.ets 负责读取沙箱中的注册人脸文件,并把特征重新注册到内存中。整体流程分四步:

  1. 读取沙箱目录 :从 filesDir/registerImages 加载已注册的人脸图片和特征文件。
ini 复制代码
let dirPath = getContext().filesDir + "/registerImages";
this.imageList = await ImageUtils.getImagesFromSandbox(dirPath);
await this.registerAllFaceFeature(dirPath);
  1. 匹配图片与特征 :批量注册时,按后缀名区分 .jpg/.jpeg 图片和 .dat 特征文件。
ini 复制代码
const imageFiles = filenames.filter(file => file.endsWith('.jpg') || file.endsWith('.jpeg'));
const featureFiles = filenames.filter(file => file.endsWith('.dat'));
  1. 构造特征并注册 :读取 .dat 文件后构造 FaceRegisterFeature,调用 SDK 注册到内存引擎。
yaml 复制代码
const featureOfReg: FaceRegisterFeature = {
  searchId: Number(searchId),
  feature: {
    featureSize: fileSize,
    feature: featureData
  },
  tag: `User_${searchId}`
};
​
this.faceEngine.registerFaceFeature(featureOfReg);
  1. 删除人脸 :同时删除图片和对应的 .dat 文件,并调用 SDK 的 removeFaceFeature 从内存特征库移除。
ini 复制代码
const res = this.faceEngine.removeFaceFeature(Number(searchId));
fs.unlinkSync(filePath);

这说明 Demo 的"注册库"由两部分组成:运行时内存中的 SDK 特征库,以及沙箱目录里的注册照和特征文件。应用重启后,需要重新从沙箱加载并注册特征到 SDK 引擎。

十一、参数设置

全局参数初始化在 ParamManager.ets,采用 AppStorage + Preferences 双写联动,分三步:

  1. 启动时加载 :从 Preferences 读取配置并写入 AppStorage,保证重启后配置仍在。
csharp 复制代码
await preferencesUtil.loadPreferences(context, Constants.APP_CONFIG);
​
AppStorage.setOrCreate(
  Constants.IS_LIVENESS_ON,
  await preferencesUtil.getPreferencesValue(Constants.APP_CONFIG, Constants.IS_LIVENESS_ON, false)
);
​
AppStorage.setOrCreate(
  Constants.LIVENESS_THRESHOLD,
  await preferencesUtil.getPreferencesValue(Constants.THRESHOLD_NAME, Constants.LIVENESS_THRESHOLD, '0.50')
);
  1. 页面响应式绑定 :页面通过 @StorageLink 绑定配置,AppStorage 变化时 UI 自动更新。
ini 复制代码
@StorageLink(Constants.IS_LIVENESS_ON) isLivenessOpen: boolean = false;
@StorageLink(Constants.LIVENESS_THRESHOLD) livenessThreshold: string = "0.50";
  1. 更新时双写 :更新参数时同时写入 AppStorage(实时生效)和 Preferences(持久化)。
vbnet 复制代码
static async update(key: string, value: string | boolean) {
  AppStorage.setOrCreate(key, value);
  await preferencesUtil.putPreferencesValue(Constants.APP_CONFIG, key, value);
}

这套设计可以让实时页面立即读取最新阈值,同时保证应用重启后配置仍然存在。

十二、实战注意点

  1. 不要在主线程跑实时算法。Demo 把相机、检测、特征、活体、文件保存拆到 Worker,是实时体验能跑顺的关键。
  2. 引擎初始化要和能力掩码匹配。只做人脸检测时不需要初始化识别、年龄、性别和活体;组合功能再按需增加 CombinedMask
  3. 特征文件属于敏感生物特征数据。生产环境应加密存储,提供删除能力,并在隐私政策中明确告知采集、存储和用途。
  4. APP_ID 和 SDK_KEY 不应硬编码到公开仓库。文章、日志和截图中也应避免暴露真实密钥。
  5. 活体阈值不能直接照搬 Demo。应结合摄像头、光照、设备性能、业务风险做灰度测试和阈值校准。
  6. 预览帧格式要仔细确认。当前 Demo 算法侧按 CP_PAF_NV21 处理,适配不同设备或相机输出格式时应做格式验证。
  7. 人脸框绘制要处理旋转和镜像。前置摄像头尤其容易出现框偏移,需要像 DrawHelper 这样统一处理坐标转换。
  8. 多人脸场景要有业务策略。Demo 多数实时页面通过 Utils.getLargestFace 选择最大人脸,适合单人认证场景;如果是多人签到或门禁,需要扩展为多目标管理。

结语

这个 Harmony Demo 的价值不只是展示 ArcFace SDK API,更重要的是给出了一套适合移动端实时人脸业务的工程拆分方式:入口层做协议、权限和路由,页面层负责交互和绘制,Camera Worker 做帧采集,算法 Worker 做检测、活体、特征和搜索,File Worker 做注册资产持久化。按这个结构扩展到真实 APP 时,可以继续补充加密存储、异常恢复、网络授权、日志脱敏和业务态风控,让 Demo 能平滑过渡到可上线的人脸识别能力。

相关推荐
a1117761 小时前
唯美花朵风格的黑胶唱片音乐播放器
前端·css·css3
LEO111102 小时前
HarmonyOS应用开发实战:猫猫大作战-样式抽成可复用类,一处定义、多处复用,还能针对不同状态(正常/按下/禁用)应用不同样式
harmonyos·鸿蒙
Hilaku2 小时前
工作 5 年后,决定你薪资上限的究竟是什么?
前端·javascript·程序员
Revolution612 小时前
页面更新后为什么出现 Loading chunk failed:旧页面如何请求了已删除的构建产物
前端·面试·前端工程化
JavaGuide2 小时前
GitHub 9.8 万 Star!把整个代码仓库变成知识图谱,这个 AI Coding 工具太适合 Claude Code / Codex 了
前端·后端·ai编程
hunterandroid2 小时前
[鸿蒙从零到一] HarmonyOS 通知与提醒实战:消息发布、点击跳转与定时触达
前端
Lxinz2 小时前
vscode调试ts代码思路
前端
极梦网络无忧2 小时前
real-ai-editor:一款轻量、智能的纯前端 AI 富文本与 Markdown 编辑器
前端·人工智能·编辑器
ClickHouseDB2 小时前
ClickHouse托管Postgres:OLTP+OLAP,新能力解锁最佳数据平台
java·前端·数据库