鸿蒙学习实战之路-Core Vision Kit主体分割实现指南
Core Vision Kit(基础视觉服务)提供了机器视觉相关的基础能力,什么意思呢?通俗点说,就是让你的鸿蒙应用"长一双眼睛"——能看懂图片里的内容是人脸还是文字,甚至是通用物体。这套能力封装在这个包里,前面咱们聊过了通用文字识别、人脸检测、人脸比对,今天来说说另一个实用功能:主体分割。害,说起主体分割,我有个做电商的朋友之前让我帮他做个功能——用户上传商品图,自动把商品从背景里抠出来换个纯白
鸿蒙学习实战之路-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 的视觉能力咱们已经聊了四个:通用文字识别、人脸检测、人脸比对、主体分割。还有两个有意思的能力值得捣鼓:
- 多目标识别:一次识别图片里的多种物体,动物、植物、建筑物都能认出来
- 骨骼点检测:检测人体关键点,做动作识别、体态分析
推荐资料
📚 官方文档是个好东西!说三遍!
- 官方主体分割文档:Core Vision Kit 主体分割
- API 参考手册:SubjectSegmentation API
- 进阶指南:解决主体分割图片背景非透明问题
我是盐焗西兰花,
不教理论,只给你能跑的代码和避坑指南。
下期见!🥦
更多推荐




所有评论(0)