前言
电商平台上架要求白底主图。人工抠图一张 3–5 分钟,一天两百张不现实。
目标很明确:选一张杂背景商品图 → 输出抠好的前景 → 垫白底 → 存图,全程端侧,不上传原图。
鸿蒙给的能力是主体分割(subjectSegmentation),归属 @kit.CoreVisionKit。功能本身一步到位,但我踩了两个坑,都是默认关闭的开关造成的,而且不报错。
这篇把八步走完,顺便把两个开关说清楚。
能用在哪些场景
- 单一主体的商品图换白底
- 证件照换底色
- 课件、海报配图去背景
- 隐私虚化前的主体分离
不适合的情况也要说在前面:多物体分别抠取它做不到(实测只给一个主主体,见后面),需要像素级精修发丝的场景也不合适——它给的是羽化软边缘,不是硬边。
接口
import { subjectSegmentation } from '@kit.CoreVisionKit';
subjectSegmentation.init(): Promise<boolean>
subjectSegmentation.release(): Promise<void>
subjectSegmentation.doSegmentation(vi, cfg?): Promise<SegmentationResult>
interface SegmentationConfig {
maxCount?: number;
enableSubjectDetails?: boolean;
enableSubjectForegroundImage?: boolean;
}
interface SubjectResult {
foregroundImage: image.PixelMap;
mattingList: Int32Array;
subjectRectangle: Rectangle;
}
interface SegmentationResult {
subjectCount: number;
fullSubject: SubjectResult;
subjectDetails?: Array<SubjectResult>;
}
- 主体分割(Subject Segmentation):把图像中的主要对象从背景里分离出来,输出前景图和逐像素透明度蒙版。
- mattingList(蒙版):描述每个像素属于主体的程度,这里是一个
Int32Array。- 软 alpha:边缘不是"要么透明要么不透明",而是连续过渡值,所以看起来是羽化的。
版本:phone / tablet 自 5.0.0(12),2in1 自 5.0.1(13)。不是 API 26 新增,鸿蒙 7 上继续可用。
八步
第一步:起服务
init() 全局一次,别每次抠图都调:
private inited: boolean = false;
async init(): Promise<void> {
if (this.inited) { return; }
this.inited = await subjectSegmentation.init();
}第二步:解码成 PixelMap
相册图先拷进沙箱再解。注意尺寸:实测 720×540 输入耗时约 900 ms,尺寸越大越久,建议先把长边压到 1024 以内。
第三步:显式打开前景图开关
这是第一个坑。 传 {} 时 foregroundImage 是空的,界面上一片白,不报任何错误、不打任何日志。必须写:
const cfg: subjectSegmentation.SegmentationConfig = { enableSubjectForegroundImage: true };第四步:调用分割
const vi: subjectSegmentation.VisionInfo = { pixelMap: pm };
const res: subjectSegmentation.SegmentationResult = await subjectSegmentation.doSegmentation(vi, cfg);第五步:取前景图
const fg: image.PixelMap = res.fullSubject.foregroundImage;第六步:垫白底
UI 预览用叠层即可:下层白色 Column,上层 Image(fg),objectFit(ImageFit.Contain)。
要落盘成一张 JPEG,则需把 fg 用 ImagePacker.packToData 编成带 alpha 的 PNG,再在合成时铺白底。直接 packToData(fg, {format:'image/jpeg'}) 会把透明区压成黑色,这个坑很典型。
第七步:读 mask 做后处理
mattingList 是逐像素数组,长度等于宽×高。要二次描边、收紧边缘、局部修补,从这里取:
const mask: Int32Array = res.fullSubject.mattingList;
// mask.length === 720 * 540 === 388800第八步:释放
aboutToDisappear 里 release()。
真机数据
测试机:HUAWEI MatePad Pro(MRDI-W00),HarmonyOS 7.0.0.107,API 26。输入是一张 720×540 的桌面杂背景商品图(马克杯 + 键盘 + 纸张 + 水瓶)。
| 配置 | subjectCount | mask 长度 | mask 取值数 | details | 前景图 | 耗时 |
|---|---|---|---|---|---|---|
{} 默认 |
1 | 388800 | 253 | 0 | 无 | 888 ms |
{enableSubjectForegroundImage:true} |
1 | 388800 | 253 | 0 | 有 | 998 ms |
{maxCount:4, enableSubjectDetails:true} |
1 | 388800 | 253 | 1 | 无 | 803 ms |
| 首次调用(冷启动) | 1 | 388800 | 253 | 0 | 无 | 1154 ms |
主体矩形三种配置一致:left 206, top 95, 377 × 345。
开前景图开关后的效果——左边原图,右边抠图垫白底:

默认配置下的表现——前景图为空,右栏全白,状态栏显示"前景图 无":

四条必须记住的结论
结论一:enableSubjectForegroundImage 默认不开
不写这个字段,foregroundImage 拿不到内容,界面空白,无异常无日志。这是本文踩得最久的一步。只要功能目标是拿到图,就必须显式写 true。
结论二:mattingList 是软 alpha,不是二值 mask
长度 388800 正好等于 720×540,即逐像素;取值 253 种,说明它在 0–255 之间连续过渡。
含义有三点:
- 边缘自带羽化,直接当前景 alpha 用,不要自己再套一层模糊
- 想做硬边抠图,得自己设阈值二值化
- 它是
Int32Array,每像素 4 字节。720×540 就是 1.48 MB,1920×1080 输入是 8.3 MB。批量处理时这是主要的内存压力点,处理完立即释放引用
结论三:maxCount 不会给你更多主体
传 maxCount: 4,subjectCount 仍是 1。图里的键盘、水瓶、纸张、笔都没有被当作独立主体返回。
所以它的语义是"找主主体",不是"分割所有物体"。要多物体拆分,见下一节。
结论四:enableSubjectDetails 控制的是数组填充,不是主体数量
不开时 subjectDetails 是空数组(长度为 0,不是 undefined);开了之后长度为 1。
想区分"没检测到"和"没开详情",判 subjectCount 而不是 subjectDetails.length。
把 mask 变成可用的 alpha 通道
多数情况下 foregroundImage 已经够用,不需要自己动 mask。要自己动手的只有三种场景:收紧边缘、局部修补、只要轮廓。
二值化收紧边缘。 软 alpha 在深色背景上会露出一圈原背景的 fringe。做法是把过渡带压窄:
const mask: Int32Array = res.fullSubject.mattingList;
const tight: Uint8Array = new Uint8Array(mask.length);
for (let i = 0; i < mask.length; i++) {
const v: number = mask[i];
tight[i] = v <= 20 ? 0 : (v >= 235 ? 255 : Math.round((v - 20) * 255 / 215));
}20 和 235 是经验起点:0–20 区间像素数量最多(纯背景),235 以上是纯主体,中间过渡带只占边缘一两像素宽。换一批图要重新看分布再调。
读某一行的透明度分布。 mask 是行主序,第 y 行第 x 列即 mask[y * width + x]。用它可以在合成前定位 fringe 出现在哪一侧,不必等出图后肉眼找。
只要轮廓。 遍历 mask,把 v > 127 且四邻域存在 v <= 127 的像素标出来,就得到轮廓点集,可用于描边或计算主体周长。
要多抠几个物体怎么办
subjectSegmentation 只给一个主主体,这是能力边界,不是参数没调对。要拆多个物体,走两段式:先用 objectDetection 拿到每个物体的框,再对每个框裁出来单独送进分割。
// 1) 检测拿框
const objs: objectDetection.Object[] = await objectDetection.detect({ pixelMap: pm });
// 2) 逐框裁剪 + 逐框分割
for (let i = 0; i < objs.length; i++) {
const crop: image.PixelMap = await pm.clone();
await crop.crop(objs[i].boundingBox);
const r: subjectSegmentation.SegmentationResult =
await subjectSegmentation.doSegmentation({ pixelMap: crop },
{ enableSubjectForegroundImage: true });
parts.push(r.fullSubject.foregroundImage);
crop.release();
}代价要说清楚:每个框一次分割调用,按实测 1 秒/张算,五个物体就是五秒。所以这条路适合离线批处理,不适合交互场景。
另外 objectDetection 返回的是通用物体框,本例的键盘、水瓶能被框出来,但框的贴合度不如主主体分割精细,裁出来的小图边缘会带进相邻物体的像素,需要再收紧一轮。
成本账
冷启动 1154 ms,热态 888 ms(不开前景)/ 998 ms(开前景)。
- 生成前景图的成本约 +110 ms。只要功能上需要图,就别为省这 110 ms 省掉开关。
- 首次调用多约 250–350 ms,是模型加载。批量场景预热一次再进循环。
- 单张 1 秒,意味着不能做实时预览,只能做"提交后处理"。要做成拍照即出图,得降输入分辨率换速度。
白底合规检查
平台通常还要求白底纯净、主体占比合适。抠完顺手校验,别等审核打回:
- 底色纯度:采样输出图四角像素,RGB 均应 ≥ 250
- 主体占比:
subjectRectangle.width / imgW,多数平台要求 60%–80%。本文样图占比 52.4%(377/720),偏小,需要居中放大后再补白边 - 边缘残留:沿
subjectRectangle内侧一圈取样,若出现背景色(如本例的木色桌面),说明 mask 有漏,按上面说的收紧后重合成 - 尺寸:常见要求 800×800 以上正方形,不足则补白边而非拉伸,拉伸会让商品比例失真被驳回
总结
什么场景值得用它:商品图换白底、证件照换底色、课件配图去背景这类"画面里就一个主主体"的活。
不适合:一张图里逐个抠多个物体——它只认主主体,maxCount 调大也没用,得改走目标检测拿框再逐框分割。
💡 一句话:两个默认关闭的开关决定成败——
enableSubjectForegroundImage不给图,maxCount给不了多主体。
参考文献
- Core Vision Kit 开发指南 · 主体分割:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-subject-segmentation
- Core Vision Kit API 参考 · subjectSegmentation(主体分割):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-subjectsegmentation-api
- Core Vision Kit 开发指南 · 多目标识别(多物体拆分用):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-object-detection