鸿蒙学习实战之路-Core Vision Kit人脸比对实现指南

Core Vision Kit(基础视觉服务)提供了机器视觉相关的基础能力,什么意思呢?通俗点说,就是让你的鸿蒙应用"长一双眼睛"——能看懂图片里的内容是人脸还是文字,甚至是通用物体。这套能力封装在 @kit.CoreVisionKit 这个包里,前面咱们聊过人脸检测,今天来说说它的好兄弟:人脸比对。

害,说起人脸比对,我脑子里第一个冒出来的就是各种娱乐 App 里的"你与明星的相似度"功能,当年可谓风靡一时!其实背后的原理就是人脸比对——提取两张脸的特征值,算算相似度是多少。今天这篇,我就手把手带你用 Core Vision Kit 实现人脸比对,全程不超过 10 分钟~


适用场景

人脸比对技术可以判断两张图片是否属于同一个人,返回相似度评分和比对结果。听起来挺高大上,其实应用场景离咱们很近:

  • 娱乐类 App 人脸相似度比较:你和迪丽热巴相似度 87%?这类趣味功能背后的技术就是这个
  • 身份验证场景中的 1:1 人脸匹配:刷脸登录、刷脸支付,都是在比对摄像头拍的脸和数据库里存的脸是不是同一个人
  • 社交应用中的趣味人脸对比功能:和朋友合个影,测测你们俩是不是"失散多年的兄妹"_

效果示例:

在这里插入图片描述


约束与限制

开始写代码之前,有些注意事项咱们得先讲清楚,省得写到一半踩坑:

约束项 具体说明
设备支持 不支持模拟器
比对模式 仅支持 1:1 人脸比对
图像要求 宽高比例建议 10:1 以下,接近手机屏幕比例为宜
并发处理 不支持多线程并发调用

🥦 西兰花警告
这里有个重点!人脸比对只支持 1:1 模式,也就是一次只能比两张脸。如果你想搞"人脸聚类"(比如从 100 张照片里找出同一个人),那得自己循环调用 API,Core Vision Kit 暂时不直接支持这种玩法。


开发步骤

好,接下来咱们一步步来实现人脸比对功能。整体流程和人脸检测差不多,但有两个关键区别:要比对两张图片,而且需要在页面生命周期里初始化和释放引擎。

1. 导入依赖

首先把要用到的模块 import 进来:

import { faceComparator } from '@kit.CoreVisionKit';

咦,怎么只有一行?害,因为 faceComparator 是人脸比对专用模块,其他图片加载、图库选择的模块和之前人脸检测用的是一样的,后面代码里会看到。

2. 页面布局设计

页面结构稍微复杂了一点,要显示两张比对图片:

Column() {
  // 显示第一张比对图片
  Image(this.chooseImage)
    .objectFit(ImageFit.Fill)
    .height('30%')
    .accessibilityDescription("第一张比对图片")
    
  // 显示第二张比对图片
  Image(this.chooseImage1)
    .objectFit(ImageFit.Fill)
    .height('30%')
    .accessibilityDescription("第二张比对图片")
    
  // 显示比对结果
  Text(this.dataValues)
    .copyOption(CopyOptions.LocalDevice)
    .height('15%')
    .margin(10)
    .width('60%')
    
  // 选择图片按钮
  Button('选择图片')
    .type(ButtonType.Capsule)
    .fontColor(Color.White)
    .alignSelf(ItemAlign.Center)
    .width('80%')
    .margin(10)
    .onClick(() => this.selectImage())
    
  // 人脸比对按钮
  Button('人脸比对')
    .type(ButtonType.Capsule)
    .fontColor(Color.White)
    .alignSelf(ItemAlign.Center)
    .width('80%')
    .margin(10)
    .onClick(() => this.compareFaces())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)

布局逻辑很清晰:两张图片上下排列,中间夹着结果文字,下面两个按钮。这个结构就像对照实验一样,把两张脸放在一起比个高下!

3. 图片选择与加载

这里有个关键点:人脸比对引擎需要在页面生命周期里初始化和释放,所以用到了 aboutToAppearaboutToDisappear 这两个钩子函数。

// 初始化人脸比对引擎
async aboutToAppear(): Promise<void> {
  const initResult = await faceComparator.init();
  hilog.info(0x0000, TAG, `初始化结果: ${initResult}`);
}

// 释放资源
async aboutToDisappear(): Promise<void> {
  await faceComparator.release();
  hilog.info(0x0000, TAG, '资源已释放');
}

// 选择两张比对图片
private async selectImage() {
  const uris = await this.openPhoto();
  if (uris && uris.length === 2) {
    this.loadImage(uris);
  } else {
    hilog.error(0x0000, TAG, "请选择两张图片进行比对");
  }
}

// 打开图库选择图片
private openPhoto(): Promise<string[]> {
  return new Promise<string[]>((resolve, reject) => {
    const photoPicker = new photoAccessHelper.PhotoViewPicker();
    photoPicker.select({
      MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
      maxSelectNumber: 2 // 最多选择2张图片
    }).then(res => {
      resolve(res.photoUris);
    }).catch((err: BusinessError) => {
      hilog.error(0x0000, TAG, `选择图片失败: ${err.code} - ${err.message}`);
      reject();
    });
  });
}

// 加载图片并转换为PixelMap
private loadImage(uris: string[]) {
  setTimeout(async () => {
    try {
      // 加载第一张图片
      const fileSource1 = await fileIo.open(uris[0], fileIo.OpenMode.READ_ONLY);
      const imageSource1 = image.createImageSource(fileSource1.fd);
      this.chooseImage = await imageSource1.createPixelMap();
      await fileIo.close(fileSource1);
      
      // 加载第二张图片
      const fileSource2 = await fileIo.open(uris[1], fileIo.OpenMode.READ_ONLY);
      const imageSource2 = image.createImageSource(fileSource2.fd);
      this.chooseImage1 = await imageSource2.createPixelMap();
      await fileIo.close(fileSource2);
    } catch (error) {
      hilog.error(0x0000, TAG, `图片加载失败: ${error}`);
    }
  }, 100);
}

解释一下这段代码在干嘛:

  • aboutToAppear():页面即将显示时,调用 faceComparator.init() 初始化比对引擎
  • aboutToDisappear():页面即将销毁时,调用 faceComparator.release() 释放资源
  • selectImage():调用图库选择器,让用户选两张图片
  • loadImage():把两张图片都加载成 PixelMap 格式

🥦 西兰花小贴士
maxSelectNumber: 2 这个参数很关键,默认是 1,你得改成 2 才能一次选两张图片。我第一次用的时候忘了改,结果只能选一张,还以为系统 Bug 了…o(╯□╰)o

4. 执行人脸比对

准备工作做完了,真正比对其实就几行代码:

private async compareFaces() {
  if (!this.chooseImage || !this.chooseImage1) {
    this.dataValues = "请先选择两张图片";
    return;
  }
  
  try {
    // 准备比对参数
    const visionInfo1: faceComparator.VisionInfo = {
      pixelMap: this.chooseImage
    };
    
    const visionInfo2: faceComparator.VisionInfo = {
      pixelMap: this.chooseImage1
    };
    
    // 执行比对
    const result: faceComparator.FaceCompareResult = 
      await faceComparator.compareFaces(visionInfo1, visionInfo2);
    
    // 处理比对结果
    const similarity = this.toPercentage(result.similarity);
    this.dataValues = `相似度: ${similarity}${result.isSamePerson ? '是' : '不是'}同一个人`;
    
  } catch (error) {
    const err = error as BusinessError;
    hilog.error(0x0000, TAG, `比对失败: ${err.code} - ${err.message}`);
    this.dataValues = `比对失败: ${err.message}`;
  }
}

// 转换为百分比格式
private toPercentage(value: number): string {
  return `${(value * 100).toFixed(2)}%`;
}

流程一目了然:构造两个 VisionInfo 对象 → 调用 compareFaces → 解析返回的相似度和是否同一人。toPercentage 方法把 0.8764 这种小数转成 87.64%,用户看起来更直观。


完整代码示例

上面拆开讲了一遍,现在把完整的可运行代码给大家,直接复制到 DevEco Studio 里就能跑:

import { faceComparator } from '@kit.CoreVisionKit';
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { Button, ButtonType, Column, Image, ImageFit, ItemAlign, Text } from '@kit.ArkUI';

const TAG: string = "FaceCompareSample";

@Entry
@Component
struct FaceComparisonPage {
  @State chooseImage: PixelMap | undefined = undefined;
  @State chooseImage1: PixelMap | undefined = undefined;
  @State dataValues: string = '';

  build() {
    Column() {
      Image(this.chooseImage)
        .objectFit(ImageFit.Fill)
        .height('30%')
        .accessibilityDescription("第一张比对图片")
        
      Image(this.chooseImage1)
        .objectFit(ImageFit.Fill)
        .height('30%')
        .accessibilityDescription("第二张比对图片")
        
      Text(this.dataValues)
        .copyOption(CopyOptions.LocalDevice)
        .height('15%')
        .margin(10)
        .width('60%')
        
      Button('选择图片')
        .type(ButtonType.Capsule)
        .fontColor(Color.White)
        .alignSelf(ItemAlign.Center)
        .width('80%')
        .margin(10)
        .onClick(() => this.selectImage())
        
      Button('人脸比对')
        .type(ButtonType.Capsule)
        .fontColor(Color.White)
        .alignSelf(ItemAlign.Center)
        .width('80%')
        .margin(10)
        .onClick(() => this.compareFaces())
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  // 页面初始化时初始化人脸比对引擎
  async aboutToAppear(): Promise<void> {
    const initResult = await faceComparator.init();
    hilog.info(0x0000, TAG, `初始化结果: ${initResult}`);
  }

  // 页面销毁时释放资源
  async aboutToDisappear(): Promise<void> {
    await faceComparator.release();
    hilog.info(0x0000, TAG, '资源已释放');
  }

  // 选择比对图片
  private async selectImage() {
    const uris = await this.openPhoto();
    if (uris && uris.length === 2) {
      this.loadImage(uris);
    } else {
      this.dataValues = "请选择两张图片进行比对";
    }
  }

  // 打开图库选择图片
  private openPhoto(): Promise<string[]> {
    return new Promise<string[]>((resolve, reject) => {
      const photoPicker = new photoAccessHelper.PhotoViewPicker();
      photoPicker.select({
        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 2
      }).then(res => {
        resolve(res.photoUris);
      }).catch((err: BusinessError) => {
        hilog.error(0x0000, TAG, `选择图片失败: ${err.code} - ${err.message}`);
        reject();
      });
    });
  }

  // 加载图片并转换为PixelMap
  private loadImage(uris: string[]) {
    setTimeout(async () => {
      try {
        // 加载第一张图片
        const fileSource1 = await fileIo.open(uris[0], fileIo.OpenMode.READ_ONLY);
        const imageSource1 = image.createImageSource(fileSource1.fd);
        this.chooseImage = await imageSource1.createPixelMap();
        await fileIo.close(fileSource1);
        
        // 加载第二张图片
        const fileSource2 = await fileIo.open(uris[1], fileIo.OpenMode.READ_ONLY);
        const imageSource2 = image.createImageSource(fileSource2.fd);
        this.chooseImage1 = await imageSource2.createPixelMap();
        await fileIo.close(fileSource2);
        
        this.dataValues = "图片加载完成,请点击人脸比对";
      } catch (error) {
        hilog.error(0x0000, TAG, `图片加载失败: ${error}`);
        this.dataValues = "图片加载失败,请重试";
      }
    }, 100);
  }

  // 执行人脸比对
  private async compareFaces() {
    if (!this.chooseImage || !this.chooseImage1) {
      this.dataValues = "请先选择两张图片";
      return;
    }
    
    try {
      const visionInfo1: faceComparator.VisionInfo = {
        pixelMap: this.chooseImage
      };
      
      const visionInfo2: faceComparator.VisionInfo = {
        pixelMap: this.chooseImage1
      };
      
      const result: faceComparator.FaceCompareResult = 
        await faceComparator.compareFaces(visionInfo1, visionInfo2);
      
      const similarity = this.toPercentage(result.similarity);
      this.dataValues = `相似度: ${similarity}${result.isSamePerson ? '是' : '不是'}同一个人`;
      
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(0x0000, TAG, `比对失败: ${err.code} - ${err.message}`);
      this.dataValues = `比对失败: ${err.message}`;
    }
  }

  // 转换为百分比格式
  private toPercentage(value: number): string {
    return `${(value * 100).toFixed(2)}%`;
  }
}

这段代码把前面的功能整合到了一起,还在 loadImage 里加了提示信息,用户体验更好。


比对结果说明

人脸比对完成后,返回的 FaceCompareResult 对象包含以下关键信息:

属性 类型 描述
similarity number 相似度分数(0-1 之间)
isSamePerson boolean 是否为同一人(true/false)

🥦 西兰花小贴士
similarity 是 0 到 1 之间的小数,1 表示完全一样。那阈值设多少判断"是同一人"呢?官方没有硬性规定,一般 0.6 到 0.8 之间比较常见。你可以自己调参数,找个最适合你业务的阈值。


下一步

人脸比对做完了,Core Vision Kit 的视觉能力咱们已经聊了检测和比对两个。还有几个有意思的能力值得捣鼓:

  • 主体分割:把图片里的主体从背景里抠出来,做证件照换背景可以用
  • 骨骼点检测:检测人体关键点,做动作识别、体态分析
  • 多目标识别:一次识别图片里的多种物体

推荐资料

📚 官方文档是个好东西!说三遍!


我是盐焗西兰花,

不教理论,只给你能跑的代码和避坑指南。

下期见!🥦

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐