指令魔方的 “抠图黑科技”:鸿蒙主体分割让图片编辑更简单
鸿蒙的主体分割能力上手特别简单,核心就是 “初始化 -> 选图 -> 转格式 -> 配参数 -> 调用接口”,代码逻辑和 OCR 高度相似,只要掌握了之前的 OCR 开发,这个功能半天就能落地。背景替换:给抠图后的主体添加自定义背景(比如纯色、图片背景);主体保存:让用户把抠图后的主体保存为图片,或分享给好友;多主体选择:如果识别到多个主体,让用户选择要保留的主体;拍照抠图:除了图库选图,还能调用
大家好,我是陈杨,8 年前端老兵转型鸿蒙开发,也是一名鸿蒙极客。从前端到鸿蒙,我靠的是 “三天上手 ArkTS” 的技术嗅觉,以及 “居安思危” 的转型魄力。这三年,我不玩虚的,封装了开源组件库「莓创图表」,拿过创新赛大奖,更带着团队上架了 11 款自研 APP,涵盖工具、效率、创意等多个领域。想体验我的作品?欢迎搜索体验:指令魔方、JLPT、REFLEX PRO、国潮纸刻、Wss 直连、ZenithDocs Pro、圣诞相册、CSS 特效。
在指令魔方 APP 里,有个深受用户喜欢的创意功能:用户上传一张图片,比如风景照里的人物、商品图里的物品,APP 能自动把主体抠出来,换成自定义背景,或者生成专属贴纸。这个 “一键抠图” 的功能,核心就是鸿蒙 Core Vision Kit 的主体分割能力。今天就用通俗的语言,带大家拆解这个功能的实现逻辑,附上可直接复用的代码,新手也能快速落地。
一、先搞懂:主体分割到底能做啥?
简单说,主体分割就是 “让 APP 自动识别图片里的核心物体,然后把它和背景分开”。这里的 “主体” 可以是人物、动物、植物、商品等任何图片里的核心元素,分割后能得到不带背景的纯主体图像。
它的适用场景特别广,指令魔方里就用到了这些:
- 创意指令生成:用户抠出人物主体,换成鸿蒙系统界面背景,生成 “假装在鸿蒙系统里操作” 的创意指令;
- 贴纸制作:抠出商品、卡通形象等主体,生成自定义贴纸,用于指令配图;
- 背景替换:用户上传的指令配图背景杂乱,抠出主体后换成纯色或自定义背景,让指令更清晰;
- 多主体提取:如果图片里有多个核心物体(比如两只宠物、多个商品),也能一次性识别并分割。
而且它的识别能力很靠谱,不管主体和背景的对比度高不高,都能精准抠图,不过要注意,目前这个能力不支持模拟器,开发时得用真实设备测试。
二、核心逻辑:从 “选图” 到 “抠图” 的 5 步走
实现主体分割的流程和之前讲的 OCR 很像,核心就 5 个步骤,指令魔方也是这么做的:
- 初始化主体分割服务:打开抠图功能的 “开关”,准备好所需资源;
- 选择图片:用户从图库选一张需要抠图的图片;
- 图片格式转换:把选中的图片转换成主体分割能识别的 PixelMap 格式;
- 配置分割参数:设置最多识别多少个主体、是否输出抠图后的纯主体图像;
- 调用分割接口:传入图片和配置,等待分割结果,最后显示抠图后的效果。
三、可直接复用的代码
下面的代码是结合指令魔方 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}`);
});
}
}
代码关键部分解析
- 初始化和释放:和 OCR 一样,
aboutToAppear初始化服务,aboutToDisappear释放,避免内存泄漏; - 图片处理:主体分割只认
PixelMap格式,所以必须通过image.createImageSource转换图片; - 核心配置参数:
maxCount:最多识别多少个主体,比如输入 5,就最多识别 5 个核心物体;enableSubjectForegroundImage:是否输出抠图后的前景图,必须设为true,否则只能得到主体位置,得不到抠图结果;enableSubjectDetails:是否输出每个主体的位置信息,方便做后续的精准操作;
- 结果处理:
result.fullSubject.foregroundImage就是抠图后的纯主体图像,直接用Image组件显示即可。
四、避坑指南:这些问题一定要注意
- 设备支持:不支持模拟器!必须用真实的鸿蒙设备测试,不然会报 “服务不可用”;
- 图片格式:只能识别
PixelMap格式,不能直接传图片 URI,一定要做格式转换; - 参数验证:
maxCount必须是大于等于 1 的数字,否则会识别失败,代码里要加验证; - 结果判断:如果图片里没有可识别的主体(比如纯背景图),
result.subjectCount会是 0,foregroundImage会为空,要给用户提示 “未识别到主体”; - 性能优化:主体分割对图片分辨率有一定要求,太大的图片识别速度会变慢,可在选图后适当压缩图片尺寸。
五、开发总结:新手也能快速落地
鸿蒙的主体分割能力上手特别简单,核心就是 “初始化 -> 选图 -> 转格式 -> 配参数 -> 调用接口”,代码逻辑和 OCR 高度相似,只要掌握了之前的 OCR 开发,这个功能半天就能落地。
如果想扩展功能,还可以加这些:
- 背景替换:给抠图后的主体添加自定义背景(比如纯色、图片背景);
- 主体保存:让用户把抠图后的主体保存为图片,或分享给好友;
- 多主体选择:如果识别到多个主体,让用户选择要保留的主体;
- 拍照抠图:除了图库选图,还能调用相机实时拍照,拍完直接抠图。
如果你也在做鸿蒙 APP,需要抠图、背景替换等功能,直接拿上面的代码改一改就能用~ 有问题欢迎留言交流,也可以下载指令魔方 APP,体验实际的抠图效果!
更多推荐



所有评论(0)