智绘鸿蒙 · 如 7 而至端侧 AI 能力实战手记

白底主图:八步做完,两个开关必须显式打开

智绘鸿蒙·如 7 而至

前言

电商平台上架要求白底主图。人工抠图一张 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,则需把 fgImagePacker.packToData 编成带 alpha 的 PNG,再在合成时铺白底。直接 packToData(fg, {format:'image/jpeg'}) 会把透明区压成黑色,这个坑很典型。

第七步:读 mask 做后处理

mattingList 是逐像素数组,长度等于宽×高。要二次描边、收紧边缘、局部修补,从这里取:

const mask: Int32Array = res.fullSubject.mattingList;
// mask.length === 720 * 540 === 388800

第八步:释放

aboutToDisappearrelease()

真机数据

测试机: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: 4subjectCount 仍是 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 给不了多主体。

参考文献

  1. Core Vision Kit 开发指南 · 主体分割:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-subject-segmentation
  2. Core Vision Kit API 参考 · subjectSegmentation(主体分割):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-subjectsegmentation-api
  3. Core Vision Kit 开发指南 · 多目标识别(多物体拆分用):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-object-detection