鸿蒙学习实战之路-Core Vision Kit主体分割实现指南

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

害,说起主体分割,我有个做电商的朋友之前让我帮他做个功能——用户上传商品图,自动把商品从背景里抠出来换个纯白背景。他找第三方 SDK 花了不少钱对接,后来发现鸿蒙自带这个能力,肠子都悔青了!今天这篇,我就手把手带你用 Core Vision Kit 实现主体分割,全程不超过 10 分钟~


适用场景

主体分割技术可以检测并提取图片中的显著前景物体,说的直白点就是"把主体从背景里抠出来"。这个能力在很多场景下都特别有用:

  • 主体贴纸:把人物或物体从背景里抠出来,做成贴纸表情包
  • 背景替换:抠出主体后换成旅游名胜、纯色背景等,做成打卡照片
  • 显著性检测:快速定位图片中的重点区域,方便后续处理
  • 图片编辑辅助:单独对主体进行美化处理,比如只给人物磨皮而不影响背景

效果示例:

在这里插入图片描述


约束与限制

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

约束项 具体说明
设备支持 不支持模拟器
主体大小 物体占比不小于原图千分之五才会被识别
图像质量 建议 720p 以上,20px < 高度 < 9000px,20px < 宽度 < 9000px
宽高比例 建议 3:1 以下(高度小于宽度的 3 倍)
内容限制 不建议处理包含较多文字内容的图片

🥦 西兰花警告
这里有个坑要注意!主体分割对文字内容不太友好,如果图片里文字太多,可能会影响主体识别的准确率。另外,物体太小也不行,得占原图的千分之五以上——也就是 1000x1000 的图里,主体至少得 50x50 像素左右。


开发步骤

好,铺垫完了咱们开始上代码!整体流程和人脸比对差不多:导入依赖 → 设计页面 → 初始化引擎 → 选择图片 → 执行分割 → 处理结果。

1. 导入依赖

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

import { subjectSegmentation } from "@kit.CoreVisionKit";

就一行?对的,主体分割的核心模块就是这个,其他图片加载、图库选择的模块跟之前一样。

2. 页面布局设计

页面结构稍微复杂一点,要显示原始图片、分割结果文本、分割后的主体图,还有一个输入框设置最大主体数量:

Column() {
  // 原始图片显示
  Image(this.chooseImage)
    .objectFit(ImageFit.Fill)
    .height('30%')
    .accessibilityDescription("待分割图片")

  // 分割结果文本显示
  Scroll() {
    Text(this.dataValues)
      .copyOption(CopyOptions.LocalDevice)
      .margin(10)
      .width('100%')
  }
  .height('20%')

  // 分割后主体显示
  Image(this.segmentedImage)
    .objectFit(ImageFit.Fill)
    .height('30%')
    .accessibilityDescription("分割后的主体图像")

  // 最大主体数量设置
  Row() {
    Text('最大主体数量:')
      .fontSize(16)
    TextInput({ placeholder: '输入最大主体数量', text: this.maxNum })
      .type(InputType.Number)
      .placeholderColor(Color.Gray)
      .fontSize(16)
      .backgroundColor(Color.White)
      .onChange((value: string) => {
        this.maxNum = value
      })
  }
  .width('80%')
  .margin(10)

  // 选择图片按钮
  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.doImageSegmentation())
}
.width('100%')
.height('80%')
.justifyContent(FlexAlign.Center)

布局逻辑很清晰:上面是原始图片,中间是结果文字和分割效果图,下面是输入框和两个按钮。这里用 Scroll 包裹 Text 是因为结果文字可能比较长,需要滚动查看。

3. 初始化与资源释放

主体分割引擎也需要在页面生命周期里初始化和释放,这点和之前的 人脸比对 一致:

// 初始化主体分割引擎
async aboutToAppear(): Promise<void> {
  const initResult = await subjectSegmentation.init();
  hilog.info(0x0000, TAG, `主体分割初始化结果: ${initResult}`);
}

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

这两个钩子函数的作用和人脸比对时一样,页面出现时初始化,页面消失时释放,养成好习惯!

4. 图片选择与加载

图片选择和加载的代码和人脸比对里的差不多,只不过这里只需要选一张图片:

// 选择图片
private async selectImage() {
  const uris = await this.openPhoto();
  if (uris && uris.length > 0) {
    this.loadImage(uris);
  } else {
    this.dataValues = "未选择图片,请重试";
  }
}

// 打开图库选择图片
private openPhoto(): Promise<Array<string>> {
  return new Promise<Array<string>>((resolve, reject) => {
    const PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    PhotoSelectOptions.maxSelectNumber = 1; // 选择1张图片

    const photoPicker = new photoAccessHelper.PhotoViewPicker();
    photoPicker.select(PhotoSelectOptions)
      .then((PhotoSelectResult) => {
        resolve(PhotoSelectResult.photoUris);
      })
      .catch((err: BusinessError) => {
        hilog.error(0x0000, TAG, `选择图片失败: ${err.code} - ${err.message}`);
        reject();
      });
  });
}

// 加载图片并转换为PixelMap
private loadImage(uris: string[]) {
  setTimeout(async () => {
    try {
      const fileSource = await fileIo.open(uris[0], 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,因为一次只处理一张图片,比人脸比对简单一些。

5. 主体分割实现

终于到了最关键的主体分割环节!这里需要配置分割参数,然后调用 API 执行分割:

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

  try {
    // 配置分割参数
    const visionInfo: subjectSegmentation.VisionInfo = {
      pixelMap: this.chooseImage
    };

    const config: subjectSegmentation.SegmentationConfig = {
      maxCount: parseInt(this.maxNum) || 20, // 最大主体数量
      enableSubjectDetails: true, // 启用主体详细信息
      enableSubjectForegroundImage: true // 启用前景图像输出
    };

    // 执行主体分割
    const result: subjectSegmentation.SegmentationResult =
      await subjectSegmentation.doSegmentation(visionInfo, config);

    // 处理分割结果
    this.processSegmentationResult(result, config);

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

// 处理分割结果
private processSegmentationResult(result: subjectSegmentation.SegmentationResult, config: subjectSegmentation.SegmentationConfig) {
  let outputString = `检测到主体数量: ${result.subjectCount}\n`;
  outputString += `最大主体数量限制: ${config.maxCount}\n`;
  outputString += `是否输出详细信息: ${config.enableSubjectDetails ? '是' : '否'}\n\n`;

  // 完整主体区域信息
  const fullSubjectBox = result.fullSubject.subjectRectangle;
  outputString += `完整主体区域:\n`;
  outputString += `Left: ${fullSubjectBox.left}, Top: ${fullSubjectBox.top}\n`;
  outputString += `Width: ${fullSubjectBox.width}, Height: ${fullSubjectBox.height}\n\n`;

  // 单个主体详细信息
  if (config.enableSubjectDetails && result.subjectDetails) {
    outputString += '单个主体区域信息:\n';
    result.subjectDetails.forEach((detail, index) => {
      const box = detail.subjectRectangle;
      outputString += `主体 ${index + 1}:\n`;
      outputString += `Left: ${box.left}, Top: ${box.top}\n`;
      outputString += `Width: ${box.width}, Height: ${box.height}\n\n`;
    });
  }

  // 更新UI显示
  this.dataValues = outputString;

  // 显示分割后的前景图像
  if (result.fullSubject && result.fullSubject.foregroundImage) {
    this.segmentedImage = result.fullSubject.foregroundImage;
  } else {
    this.segmentedImage = undefined;
    outputString += "\n未获取到前景图像";
  }
}

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

  • doImageSegmentation():构造 VisionInfo 和 SegmentationConfig,然后调用 doSegmentation 执行分割
  • processSegmentationResult():解析分割结果,把主体数量、区域坐标等信息格式化输出,还会把前景图像显示在 UI 上

🥦 西兰花小贴士
SegmentationConfig 有三个关键参数:maxCount 是最多检测多少个主体,enableSubjectDetails 是否返回每个主体的详细信息,enableSubjectForegroundImage 是否输出前景图像。如果你只想快速看结果,可以把后两个设为 false 提升性能。


完整代码示例

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

import { subjectSegmentation } 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, InputType, ItemAlign, Row, Scroll, Text, TextInput } from '@kit.ArkUI';

const TAG: string = "ImageSegmentationSample";

@Entry
@Component
struct SubjectSegmentationPage {
  @State chooseImage: PixelMap | undefined = undefined;
  @State segmentedImage: PixelMap | undefined = undefined;
  @State dataValues: string = '';
  @State maxNum: string = '20'; // 默认最大主体数量

  build() {
    Column() {
      // 原始图片显示
      Image(this.chooseImage)
        .objectFit(ImageFit.Fill)
        .height('30%')
        .accessibilityDescription("待分割图片")

      // 分割结果文本显示
      Scroll() {
        Text(this.dataValues)
          .copyOption(CopyOptions.LocalDevice)
          .margin(10)
          .width('100%')
      }
      .height('20%')

      // 分割后主体显示
      Image(this.segmentedImage)
        .objectFit(ImageFit.Fill)
        .height('30%')
        .accessibilityDescription("分割后的主体图像")

      // 最大主体数量设置
      Row() {
        Text('最大主体数量:')
          .fontSize(16)
        TextInput({ placeholder: '输入最大主体数量', text: this.maxNum })
          .type(InputType.Number)
          .placeholderColor(Color.Gray)
          .fontSize(16)
          .backgroundColor(Color.White)
          .onChange((value: string) => {
            this.maxNum = value
          })
      }
      .width('80%')
      .margin(10)

      // 选择图片按钮
      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.doImageSegmentation())
    }
    .width('100%')
    .height('80%')
    .justifyContent(FlexAlign.Center)
  }

  async aboutToAppear(): Promise<void> {
    const initResult = await subjectSegmentation.init();
    hilog.info(0x0000, TAG, `主体分割初始化结果: ${initResult}`);
  }

  async aboutToDisappear(): Promise<void> {
    await subjectSegmentation.release();
    hilog.info(0x0000, TAG, '主体分割资源已释放');
  }

  private async selectImage() {
    const uris = await this.openPhoto();
    if (uris && uris.length > 0) {
      this.loadImage(uris);
    } else {
      this.dataValues = "未选择图片,请重试";
    }
  }

  private openPhoto(): Promise<Array<string>> {
    return new Promise<Array<string>>((resolve, reject) => {
      const PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
      PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
      PhotoSelectOptions.maxSelectNumber = 1;

      const photoPicker = new photoAccessHelper.PhotoViewPicker();
      photoPicker.select(PhotoSelectOptions)
        .then((PhotoSelectResult) => {
          resolve(PhotoSelectResult.photoUris);
        })
        .catch((err: BusinessError) => {
          hilog.error(0x0000, TAG, `选择图片失败: ${err.code} - ${err.message}`);
          reject();
        });
    });
  }

  private loadImage(uris: string[]) {
    setTimeout(async () => {
      try {
        const fileSource = await fileIo.open(uris[0], 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);
  }

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

    try {
      const visionInfo: subjectSegmentation.VisionInfo = {
        pixelMap: this.chooseImage
      };

      const config: subjectSegmentation.SegmentationConfig = {
        maxCount: parseInt(this.maxNum) || 20,
        enableSubjectDetails: true,
        enableSubjectForegroundImage: true
      };

      const result: subjectSegmentation.SegmentationResult =
        await subjectSegmentation.doSegmentation(visionInfo, config);

      this.processSegmentationResult(result, config);

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

  private processSegmentationResult(result: subjectSegmentation.SegmentationResult, config: subjectSegmentation.SegmentationConfig) {
    let outputString = `检测到主体数量: ${result.subjectCount}\n`;
    outputString += `最大主体数量限制: ${config.maxCount}\n`;
    outputString += `是否输出详细信息: ${config.enableSubjectDetails ? '是' : '否'}\n\n`;

    const fullSubjectBox = result.fullSubject.subjectRectangle;
    outputString += `完整主体区域:\n`;
    outputString += `Left: ${fullSubjectBox.left}, Top: ${fullSubjectBox.top}\n`;
    outputString += `Width: ${fullSubjectBox.width}, Height: ${fullSubjectBox.height}\n\n`;

    if (config.enableSubjectDetails && result.subjectDetails) {
      outputString += '单个主体区域信息:\n';
      result.subjectDetails.forEach((detail, index) => {
        const box = detail.subjectRectangle;
        outputString += `主体 ${index + 1}:\n`;
        outputString += `Left: ${box.left}, Top: ${box.top}\n`;
        outputString += `Width: ${box.width}, Height: ${box.height}\n\n`;
      });
    }

    this.dataValues = outputString;

    if (result.fullSubject && result.fullSubject.foregroundImage) {
      this.segmentedImage = result.fullSubject.foregroundImage;
    } else {
      this.segmentedImage = undefined;
      this.dataValues += "\n未获取到前景图像";
    }
  }
}

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


分割结果说明

主体分割完成后,返回的 SegmentationResult 对象包含以下关键信息:

属性 类型 描述
subjectCount number 检测到的主体数量
fullSubject SubjectInfo 完整主体信息(包含整体区域和前景图像)
subjectDetails SubjectInfo[] 单个主体详细信息列表(当 enableSubjectDetails 为 true 时)

SubjectInfo 对象结构:

属性 类型 描述
subjectRectangle Rect 主体矩形区域坐标(left, top, width, height)
confidence number 主体检测置信度
foregroundImage PixelMap 主体前景图像(当 enableSubjectForegroundImage 为 true 时)

🥦 西兰花小贴士
foregroundImage 就是抠出来的前景图,背景是透明的。你可以把它保存为 PNG,或者叠加到其他背景图上,实现各种有趣的效果!


下一步

主体分割做完了,Core Vision Kit 的视觉能力咱们已经聊了四个:通用文字识别、人脸检测、人脸比对、主体分割。还有两个有意思的能力值得捣鼓:

  • 多目标识别:一次识别图片里的多种物体,动物、植物、建筑物都能认出来
  • 骨骼点检测:检测人体关键点,做动作识别、体态分析

推荐资料

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


我是盐焗西兰花,

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

下期见!🥦

Logo

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

更多推荐