鸿蒙学习实战之路-Core Vision Kit多目标识别实现指南

Core Vision Kit(基础视觉服务)提供了机器视觉相关的基础能力,例如通用文字识别(即 OCR,Optical Character Recognition,也称为光学字符识别)、人脸检测、人脸比对以及主体分割等能力。开发者可以结合 Vision Kit 的 UI 控件能力(例如:人脸活体检测),提升应用的智能化、便捷化交互体验。

场景介绍

Core Vision Kit 可应用于各种场景,提升用户体验和应用效率。以下是一些典型的应用场景:

  • 通用文字识别:可用于扫描和识别文档、名片、票据等印刷品中的文字内容,方便用户快速录入和存储信息。
  • 人脸检测:应用于相册管理、照片美化等功能中,也可以用于自动检测和定位照片中的人脸。
  • 人脸比对:常用于人脸认证、考勤打卡、门禁系统等需要验证用户身份的场景。
  • 主体分割:可以检测出图片中区别于背景的前景物体或区域(即"显著主体"),并将其从背景中分离出来,适用于需要识别和提取图像主要信息的场景,广泛使用于前景目标检测和前景主体分离的场景。
  • 多目标识别:帮助开发者从图片中识别常见的目标对象(动物、植物、建筑物、人、人脸、文本、表格等)并给出位置信息。通常用于端到端业务场景的前置检测功能,根据检测结果完成后续功能业务的入口提示,比如视觉搜索,文本检测。
  • 骨骼点检测:人体骨骼关键点检测,主要检测人体的一些关键点,通过关键点描述人体骨骼信息。具体应用主要集中在智能视频监控,病人监护系统,人机交互,虚拟现实,人体动画,智能家居,智能安防,运动员辅助训练等等。

害,说到多目标识别,我得先讲个事儿。上次我拿自己拍的猫咪照片做测试,本来以为能识别出"猫"来,结果它给我检测出了"动物"“哺乳动物”"宠物"三个标签,把我整不会了!后来研究了一下才发现,多目标识别就是这么个工作方式——它会从不同维度把画面里的东西都标出来。咱们今天就来看看这个能力怎么用!

今天这篇,我就手把手带你用 Core Vision Kit 实现多目标识别,全程不超过 10 分钟~


适用场景

多目标识别技术可以同时检测图片中的多种物体,并返回它们的位置信息。这个能力在很多场景下都特别有用:

  • 图像内容分析与理解:让应用"看懂"图片里有什么,自动给照片打标签、做分类
  • 智能相册管理:按内容自动归类照片,比如把所有包含"狗"的照片放进"萌宠相册"
  • 场景感知与物体计数:数一数画面里有几个苹果、几个人,超市盘点都能用
  • 辅助视觉导航:帮机器人或自动驾驶识别路上的车辆、行人、红绿灯

效果示例:

在这里插入图片描述

从图中可以看到,系统一次识别出了多个人、多把椅子,甚至还有窗户和绿植。这就是多目标识别的魅力——一张图能给你扫出七八个目标来。


约束与限制

开始写代码之前,有些限制条件咱们得先搞清楚,省得做到一半发现不支持:

约束项 具体说明
设备支持 不支持模拟器
图像质量 建议 720p 以上,100px < 高度 < 10000px,100px < 宽度 < 10000px
高宽比例 建议 5:1 以下(高度小于宽度的 5 倍)
物体占比 图片中的物体占比需要大于 0.1%
检测范围 支持风景、动物、植物、建筑、人脸、表格、文本等多种物体类型

🥦 西兰花警告
和多目标识别打交道,你得注意两件事:第一,图片里的物体太小不行,得占原图的千分之一以上;第二,不支持模拟器调试!这意味着你必须在真机上测试,没得商量。


开发步骤

好,铺垫完了咱们开始上代码!整体流程和之前的主体分割、人脸比对差不多:导入依赖 → 设计页面 → 选择图片 → 创建检测器 → 执行识别 → 处理结果。

1. 导入依赖

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

import { image } from "@kit.ImageKit";
import { hilog } from "@kit.PerformanceAnalysisKit";
import { BusinessError } from "@kit.BasicServicesKit";
import { fileIo } from "@kit.CoreFileKit";
import { objectDetection, visionBase } from "@kit.CoreVisionKit";
import { photoAccessHelper } from "@kit.MediaLibraryKit";

多目标识别需要导入的模块比主体分割多一些,objectDetection 是核心,visionBase 是基础类型定义。

2. 页面布局设计

页面结构比主体分割简单一些,不需要显示分割结果图,只需要显示原始图片和识别结果文字:

Column() {
  // 显示待识别图片
  Image(this.chooseImage)
    .objectFit(ImageFit.Fill)
    .height('60%')

  // 显示识别结果
  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.startDetection())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)

布局很直观:上面六成高度显示原始图片,中间显示识别结果,下面两个按钮分管选择图片和开始识别。

3. 图片选择与加载

图片选择和加载的代码和之前的章节差不多:

// 选择图片
private async selectImage() {
  try {
    const uri = await this.openPhoto();
    if (uri) {
      this.loadImage(uri);
    } else {
      hilog.error(0x0000, TAG, "未获取到图片URI");
      this.dataValues = "未获取到图片,请重试";
    }
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(0x0000, TAG, `选择图片失败: ${error.code} - ${error.message}`);
    this.dataValues = `选择图片失败: ${error.message}`;
  }
}

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

// 加载图片并转换为PixelMap
private loadImage(uri: string) {
  setTimeout(async () => {
    try {
      const fileSource = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
      const imageSource = image.createImageSource(fileSource.fd);
      this.chooseImage = await imageSource.createPixelMap();
      await fileIo.close(fileSource);
      this.dataValues = "图片加载完成,请点击开始多目标识别";
    } catch (error) {
      hilog.error(0x0000, TAG, `图片加载失败: ${error}`);
      this.dataValues = "图片加载失败,请重试";
    }
  }, 100);
}

这里 maxSelectNumber = 1,因为一次只处理一张图片,和主体分割的选择逻辑一致。

4. 执行多目标识别

多目标识别的核心在于创建检测器实例,然后调用 process 方法执行识别:

private async startDetection() {
  if (!this.chooseImage) {
    this.dataValues = "请先选择图片";
    return;
  }

  try {
    // 创建检测器实例
    const detector = await objectDetection.ObjectDetector.create();

    // 配置检测请求参数
    const request: visionBase.Request = {
      inputData: { pixelMap: this.chooseImage }
    };

    // 执行多目标识别
    const result: objectDetection.ObjectDetectionResponse = await detector.process(request);

    // 处理识别结果
    this.processDetectionResult(result);

  } catch (error) {
    const err = error as BusinessError;
    hilog.error(0x0000, TAG, `识别失败: ${err.code} - ${err.message}`);
    this.dataValues = `识别失败: ${err.message}`;
  }
}

// 处理识别结果
private processDetectionResult(result: objectDetection.ObjectDetectionResponse) {
  if (!result || !result.objects || result.objects.length === 0) {
    this.dataValues = "未检测到任何物体";
    return;
  }

  // 格式化输出识别结果
  let output = `检测到 ${result.objects.length} 个物体:\n\n`;
  result.objects.forEach((obj, index) => {
    output += `物体 ${index + 1}:\n`;
    output += `类型: ${obj.type || '未知'}\n`;
    output += `置信度: ${obj.confidence?.toFixed(2) || '未知'}\n`;
    output += `位置: x:${obj.boundingBox.left.toFixed(2)}, y:${obj.boundingBox.top.toFixed(2)}, `;
    output += `宽:${obj.boundingBox.width.toFixed(2)}, 高:${obj.boundingBox.height.toFixed(2)}\n\n`;
  });

  this.dataValues = output;
}

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

  • startDetection():创建 ObjectDetector 实例,构建请求参数,调用 process 执行识别
  • processDetectionResult():遍历识别结果,把每个物体的类型、置信度、位置坐标格式化输出

🥦 西兰花小贴士
confidence 是置信度,范围 0 到 1。数值越高说明模型越"确信"这个识别结果是对的。如果你想过滤掉不准的识别,可以只显示置信度大于 0.7 的结果。


完整代码示例

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

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

const TAG: string = 'ObjectDetectionSample';

@Entry
@Component
struct ObjectDetectionPage {
  private imageSource: image.ImageSource | undefined = undefined;
  @State chooseImage: PixelMap | undefined = undefined;
  @State dataValues: string = '';

  build() {
    Column() {
      Image(this.chooseImage)
        .objectFit(ImageFit.Fill)
        .height('60%')

      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.startDetection())
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  private async selectImage() {
    try {
      const uri = await this.openPhoto();
      if (uri) {
        this.loadImage(uri);
      } else {
        hilog.error(0x0000, TAG, "未获取到图片URI");
        this.dataValues = "未获取到图片,请重试";
      }
    } catch (err) {
      const error = err as BusinessError;
      hilog.error(0x0000, TAG, `选择图片失败: ${error.code} - ${error.message}`);
      this.dataValues = `选择图片失败: ${error.message}`;
    }
  }

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

  private loadImage(uri: string) {
    setTimeout(async () => {
      try {
        const fileSource = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
        this.imageSource = image.createImageSource(fileSource.fd);
        this.chooseImage = await this.imageSource.createPixelMap();
        await fileIo.close(fileSource);
        this.dataValues = "图片加载完成,请点击开始多目标识别";
      } catch (error) {
        hilog.error(0x0000, TAG, `图片加载失败: ${error}`);
        this.dataValues = "图片加载失败,请重试";
      }
    }, 100);
  }

  private async startDetection() {
    if (!this.chooseImage) {
      this.dataValues = "请先选择图片";
      return;
    }

    try {
      const detector = await objectDetection.ObjectDetector.create();

      const request: visionBase.Request = {
        inputData: { pixelMap: this.chooseImage }
      };

      const result: objectDetection.ObjectDetectionResponse = await detector.process(request);

      this.processDetectionResult(result);

    } catch (error) {
      const err = error as BusinessError;
      hilog.error(0x0000, TAG, `识别失败: ${err.code} - ${err.message}`);
      this.dataValues = `识别失败: ${err.message}`;
    }
  }

  private processDetectionResult(result: objectDetection.ObjectDetectionResponse) {
    if (!result || !result.objects || result.objects.length === 0) {
      this.dataValues = "未检测到任何物体";
      return;
    }

    let output = `检测到 ${result.objects.length} 个物体:\n\n`;
    result.objects.forEach((obj, index) => {
      output += `物体 ${index + 1}:\n`;
      output += `类型: ${obj.type || '未知'}\n`;
      output += `置信度: ${obj.confidence?.toFixed(2) || '未知'}\n`;
      output += `位置: x:${obj.boundingBox.left.toFixed(2)}, y:${obj.boundingBox.top.toFixed(2)}, `;
      output += `宽:${obj.boundingBox.width.toFixed(2)}, 高:${obj.boundingBox.height.toFixed(2)}\n\n`;
    });

    this.dataValues = output;
  }
}

这段代码把前面的功能整合到了一起,可以直接运行看效果。


识别结果说明

多目标识别完成后,返回的 ObjectDetectionResponse 对象包含以下关键信息:

属性 类型 描述
objects DetectedObject[] 检测到的物体列表
version string 算法版本号

DetectedObject 对象结构:

属性 类型 描述
type string 物体类型(如"person"“car”"dog"等)
confidence number 识别置信度(0-1 之间)
boundingBox Rect 物体边界框坐标(left, top, width, height)
trackingId number 跟踪 ID(用于视频序列中的物体跟踪)

🥦 西兰花小贴士
trackingId 这个字段在处理视频时特别有用,它可以帮你追踪同一物体在不同帧之间的移动轨迹。如果你做的是实时视频分析,这个字段一定要利用起来!


下一步

多目标识别做完了,Core Vision Kit 的视觉能力咱们已经聊了五个:通用文字识别、人脸检测、人脸比对、主体分割、多目标识别。还剩最后一个值得捣鼓:

  • 骨骼点检测:检测人体关键骨骼点,做动作识别、体态分析、智能安防

骨骼点检测的应用场景可广了——智能监控检测异常行为、健身 app 纠正你的深蹲姿势、虚拟试衣间追踪身体动作……下篇咱们就聊聊这个!


推荐资料

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


我是盐焗西兰花,

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

下期见!🥦

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐