HarmonyOS开发:人脸识别与活体检测技术
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点法不够精确。
解:
- 使用5点最小二乘法代替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的静默检测本质上是在做纹理分析,高清打印照片和屏幕播放视频可以骗过大部分规则。
解:
- 红外活体:利用红外摄像头检测皮肤反射特征,照片和屏幕无法模拟
- 深度活体:利用ToF/结构光获取3D深度信息,平面攻击无效
- 配合式活体:要求用户做随机动作(眨眼、张嘴、转头),增加攻击难度
- 多模态融合:RGB + 红外 + 深度,三重验证
4.4 特征向量存储安全
坑:人脸特征向量明文存储在沙箱中,存在被窃取的风险。
解:
- 使用HarmonyOS的**安全区域(TEE)**存储特征向量
- 特征向量加密后再存储
- 比对操作在TEE中完成,特征不出安全区域
- 使用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 迁移指南
- 使用系统人脸检测API(HarmonyOS 6推荐):
// HarmonyOS 6 系统级人脸检测
import { faceRecognition } from '@kit.FaceRecognitionKit';
const detector = faceRecognition.createFaceDetector();
const faces = await detector.detect(pixelMap);
// faces: FaceInfo[],包含位置、关键点、姿态角
- 系统级活体检测:
// HarmonyOS 6 红外活体检测
const livenessDetector = faceRecognition.createLivenessDetector({
type: faceRecognition.LivenessType.IR // 红外活体
});
const livenessResult = await livenessDetector.detect(irPixelMap);
- 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安全存储是防特征泄露的底线。端侧人脸识别不是简单的"比对两张脸",而是一个系统工程——检测要准、对齐要精、活体要严、存储要安全,四者缺一不可。
- 点赞
- 收藏
- 关注作者
评论(0)