大家好,我是陈杨,8 年前端老兵转型鸿蒙开发,也是一名鸿蒙极客。从前端到鸿蒙,我靠的是 “三天上手 ArkTS” 的技术嗅觉,以及 “居安思危” 的转型魄力。这三年,我不玩虚的,封装了开源组件库「莓创图表」,拿过创新赛大奖,更带着团队上架了 11 款自研 APP,涵盖工具、效率、创意等多个领域。想体验我的作品?欢迎搜索体验:指令魔方、JLPT、REFLEX PRO、国潮纸刻、Wss 直连、ZenithDocs Pro、圣诞相册、CSS 特效。

在指令魔方 APP 里,有个深受用户喜欢的创意功能:用户上传一张图片,比如风景照里的人物、商品图里的物品,APP 能自动把主体抠出来,换成自定义背景,或者生成专属贴纸。这个 “一键抠图” 的功能,核心就是鸿蒙 Core Vision Kit 的主体分割能力。今天就用通俗的语言,带大家拆解这个功能的实现逻辑,附上可直接复用的代码,新手也能快速落地。

一、先搞懂:主体分割到底能做啥?

简单说,主体分割就是 “让 APP 自动识别图片里的核心物体,然后把它和背景分开”。这里的 “主体” 可以是人物、动物、植物、商品等任何图片里的核心元素,分割后能得到不带背景的纯主体图像。

它的适用场景特别广,指令魔方里就用到了这些:

  1. 创意指令生成:用户抠出人物主体,换成鸿蒙系统界面背景,生成 “假装在鸿蒙系统里操作” 的创意指令;
  2. 贴纸制作:抠出商品、卡通形象等主体,生成自定义贴纸,用于指令配图;
  3. 背景替换:用户上传的指令配图背景杂乱,抠出主体后换成纯色或自定义背景,让指令更清晰;
  4. 多主体提取:如果图片里有多个核心物体(比如两只宠物、多个商品),也能一次性识别并分割。

而且它的识别能力很靠谱,不管主体和背景的对比度高不高,都能精准抠图,不过要注意,目前这个能力不支持模拟器,开发时得用真实设备测试。

二、核心逻辑:从 “选图” 到 “抠图” 的 5 步走

实现主体分割的流程和之前讲的 OCR 很像,核心就 5 个步骤,指令魔方也是这么做的:

  1. 初始化主体分割服务:打开抠图功能的 “开关”,准备好所需资源;
  2. 选择图片:用户从图库选一张需要抠图的图片;
  3. 图片格式转换:把选中的图片转换成主体分割能识别的 PixelMap 格式;
  4. 配置分割参数:设置最多识别多少个主体、是否输出抠图后的纯主体图像;
  5. 调用分割接口:传入图片和配置,等待分割结果,最后显示抠图后的效果。

三、可直接复用的代码

下面的代码是结合指令魔方 APP 里提炼的简化版,保留了核心的抠图功能,去掉了复杂的业务逻辑,新手可以直接复制使用。

完整代码实现

// 导入需要的工具包
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';

const TAG = "SubjectSegmentationDemo";

@Entry
@Component
struct ImageSubjectSegmentor {
  // 选中的原始图片(PixelMap格式)
  @State originalImage: PixelMap | undefined = undefined;
  // 分割后的主体图像(抠图结果)
  @State segmentedImage: PixelMap | undefined = undefined;
  // 分割结果信息(比如主体个数、位置等)
  @State segmentInfo: string = "分割结果会显示在这里...";
  // 最大识别主体个数(用户可输入)
  @State maxSubjectCount: string = "20";
  // 图片资源对象
  private imageSource: image.ImageSource | undefined = undefined;

  // 第一步:页面加载时初始化主体分割服务
  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, '主体分割服务已释放');
  }

  build() {
    Column({ space: 20 }) {
      // 显示原始图片
      Image(this.originalImage)
        .objectFit(ImageFit.Contain)
        .height('25%')
        .width('90%')
        .border({ width: 2, color: 0x317AE7, radius: 8 })
        .backgroundColor('#F5F5F5')
        .accessibilityDescription("原始图片")

      // 显示分割后的主体图像(抠图结果)
      Image(this.segmentedImage)
        .objectFit(ImageFit.Contain)
        .height('25%')
        .width('90%')
        .border({ width: 2, color: 0x317AE7, radius: 8 })
        .backgroundColor('#F5F5F5')
        .accessibilityDescription("抠图结果")

      // 显示分割信息(支持复制)
      Scroll() {
        Text(this.segmentInfo)
          .copyOption(CopyOptions.LocalDevice)
          .margin(10)
          .width('90%')
      }
      .height('15%')
      .border({ width: 1, color: '#E0E0E0', radius: 8 })
      .width('90%')

      // 配置最大主体个数
      Row({ space: 10 }) {
        Text('最大识别主体数:')
          .fontSize(16)
          .alignSelf(ItemAlign.Center)
        TextInput({
          placeholder: '输入数字(默认20)',
          text: this.maxSubjectCount
        })
        .type(InputType.Number)
        .fontSize(16)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#E0E0E0', radius: 4 })
        .width('40%')
        .onChange((value: string) => {
          this.maxSubjectCount = value;
        })
      }
      .width('90%')

      // 选择图片按钮
      Button('从图库选择图片')
        .type(ButtonType.Capsule)
        .backgroundColor(0x317AE7)
        .fontColor(Color.White)
        .width('90%')
        .height(45)
        .onClick(() => this.selectImageFromGallery())

      // 开始分割按钮
      Button('一键抠图')
        .type(ButtonType.Capsule)
        .backgroundColor(0x317AE7)
        .fontColor(Color.White)
        .width('90%')
        .height(45)
        .onClick(() => this.startSubjectSegmentation())
    }
    .padding(20)
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  // 第二步:从图库选择图片
  private async selectImageFromGallery() {
    try {
      const photoOptions = new photoAccessHelper.PhotoSelectOptions();
      photoOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 只选图片
      photoOptions.maxSelectNumber = 1; // 最多选1张

      const photoPicker = new photoAccessHelper.PhotoViewPicker();
      const selectResult = await photoPicker.select(photoOptions);
      const imageUris = selectResult.photoUris;

      if (imageUris && imageUris.length > 0) {
        await this.loadImageToPixelMap(imageUris[0]); // 转换图片格式
      } else {
        this.segmentInfo = "未选中图片,请重新选择";
      }
    } catch (err: BusinessError | any) {
      hilog.error(0x0000, TAG, `选图失败:${err.message}`);
      this.segmentInfo = `选图失败:${err.message}(错误码:${err.code})`;
    }
  }

  // 第三步:将图片转换为主体分割能识别的PixelMap格式
  private async loadImageToPixelMap(uri: string) {
    try {
      // 打开图片文件
      const file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
      // 创建图片资源对象
      this.imageSource = image.createImageSource(file.fd);
      // 转换为PixelMap格式
      this.originalImage = await this.imageSource.createPixelMap();
      // 重置结果
      this.segmentedImage = undefined;
      this.segmentInfo = "已选中图片,点击「一键抠图」开始分割...";
    } catch (err: BusinessError | any) {
      hilog.error(0x0000, TAG, `图片加载失败:${err.message}`);
      this.segmentInfo = `图片加载失败:${err.message}`;
    }
  }

  // 第四步:配置参数并调用主体分割接口
  private startSubjectSegmentation() {
    // 先判断是否选中了图片
    if (!this.originalImage) {
      this.segmentInfo = "请先选择一张图片!";
      return;
    }

    // 验证最大主体个数是否为有效数字
    const maxCount = parseInt(this.maxSubjectCount);
    if (isNaN(maxCount) || maxCount < 1) {
      this.segmentInfo = "请输入有效的最大主体数(至少1)";
      return;
    }

    // 配置分割参数
    const segmentConfig: subjectSegmentation.SegmentationConfig = {
      maxCount: maxCount, // 最大识别主体个数
      enableSubjectDetails: true, // 是否输出每个主体的详细信息(比如位置)
      enableSubjectForegroundImage: true // 是否输出抠图后的前景图(核心参数)
    };

    // 传入图片和配置,调用分割接口
    subjectSegmentation.doSegmentation(
      { pixelMap: this.originalImage }, // 待分割的图片
      segmentConfig
    )
    .then((result: subjectSegmentation.SegmentationResult) => {
      // 处理分割结果,拼接信息
      let info = `识别到的主体个数:${result.subjectCount}\n`;
      info += `最大支持主体个数:${segmentConfig.maxCount}\n\n`;

      // 拼接整体主体的位置信息(左上角坐标、宽高)
      const fullSubjectBox = result.fullSubject.subjectRectangle;
      info += `整体主体位置:\n`;
      info += `左上角X:${fullSubjectBox.left},Y:${fullSubjectBox.top}\n`;
      info += `宽度:${fullSubjectBox.width},高度:${fullSubjectBox.height}\n\n`;

      // 如果开启了详细信息,拼接每个主体的位置
      if (segmentConfig.enableSubjectDetails && result.subjectDetails) {
        info += `各主体详细位置:\n`;
        result.subjectDetails.forEach((detail, index) => {
          const box = detail.subjectRectangle;
          info += `主体${index + 1}:X=${box.left},Y=${box.top},宽=${box.width},高=${box.height}\n`;
        });
      }

      // 显示结果信息
      this.segmentInfo = info;
      // 显示抠图后的主体图像
      this.segmentedImage = result.fullSubject.foregroundImage;

      hilog.info(0x0000, TAG, `分割成功:${info}`);
    })
    .catch((error: BusinessError) => {
      // 处理错误
      this.segmentInfo = `分割失败:${error.message}(错误码:${error.code})`;
      this.segmentedImage = undefined;
      hilog.error(0x0000, TAG, `分割失败:${error.message},错误码:${error.code}`);
    });
  }
}

代码关键部分解析

  1. 初始化和释放:和 OCR 一样,aboutToAppear 初始化服务,aboutToDisappear 释放,避免内存泄漏;
  2. 图片处理:主体分割只认 PixelMap 格式,所以必须通过 image.createImageSource 转换图片;
  3. 核心配置参数:
    • maxCount:最多识别多少个主体,比如输入 5,就最多识别 5 个核心物体;
    • enableSubjectForegroundImage:是否输出抠图后的前景图,必须设为 true,否则只能得到主体位置,得不到抠图结果;
    • enableSubjectDetails:是否输出每个主体的位置信息,方便做后续的精准操作;
  4. 结果处理:result.fullSubject.foregroundImage 就是抠图后的纯主体图像,直接用 Image 组件显示即可。

四、避坑指南:这些问题一定要注意

  1. 设备支持:不支持模拟器!必须用真实的鸿蒙设备测试,不然会报 “服务不可用”;
  2. 图片格式:只能识别 PixelMap 格式,不能直接传图片 URI,一定要做格式转换;
  3. 参数验证:maxCount 必须是大于等于 1 的数字,否则会识别失败,代码里要加验证;
  4. 结果判断:如果图片里没有可识别的主体(比如纯背景图),result.subjectCount 会是 0,foregroundImage 会为空,要给用户提示 “未识别到主体”;
  5. 性能优化:主体分割对图片分辨率有一定要求,太大的图片识别速度会变慢,可在选图后适当压缩图片尺寸。

五、开发总结:新手也能快速落地

鸿蒙的主体分割能力上手特别简单,核心就是 “初始化 -> 选图 -> 转格式 -> 配参数 -> 调用接口”,代码逻辑和 OCR 高度相似,只要掌握了之前的 OCR 开发,这个功能半天就能落地。

如果想扩展功能,还可以加这些:

  1. 背景替换:给抠图后的主体添加自定义背景(比如纯色、图片背景);
  2. 主体保存:让用户把抠图后的主体保存为图片,或分享给好友;
  3. 多主体选择:如果识别到多个主体,让用户选择要保留的主体;
  4. 拍照抠图:除了图库选图,还能调用相机实时拍照,拍完直接抠图。

如果你也在做鸿蒙 APP,需要抠图、背景替换等功能,直接拿上面的代码改一改就能用~ 有问题欢迎留言交流,也可以下载指令魔方 APP,体验实际的抠图效果!

Logo

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

更多推荐