# 白底主图：八步做完，两个开关必须显式打开 # 智绘鸿蒙·如 7 而至#

## 前言

电商平台上架要求白底主图。人工抠图一张 3–5 分钟，一天两百张不现实。

目标很明确：选一张杂背景商品图 → 输出抠好的前景 → 垫白底 → 存图，全程端侧，不上传原图。

鸿蒙给的能力是**主体分割**（`subjectSegmentation`），归属 `@kit.CoreVisionKit`。功能本身一步到位，但我踩了两个坑，都是**默认关闭的开关**造成的，而且不报错。

这篇把八步走完，顺便把两个开关说清楚。

## 能用在哪些场景

- 单一主体的商品图换白底
- 证件照换底色
- 课件、海报配图去背景
- 隐私虚化前的主体分离

不适合的情况也要说在前面：**多物体分别抠取它做不到**（实测只给一个主主体，见后面），需要像素级精修发丝的场景也不合适——它给的是羽化软边缘，不是硬边。

## 接口

```typescript
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()` 全局一次，别每次抠图都调：

```typescript
private inited: boolean = false;
async init(): Promise<void> {
  if (this.inited) { return; }
  this.inited = await subjectSegmentation.init();
}
```

### 第二步：解码成 PixelMap

相册图先拷进沙箱再解。注意尺寸：实测 720×540 输入耗时约 900 ms，尺寸越大越久，建议先把长边压到 1024 以内。

### 第三步：显式打开前景图开关

**这是第一个坑。** 传 `{}` 时 `foregroundImage` 是空的，界面上一片白，**不报任何错误、不打任何日志**。必须写：

```typescript
const cfg: subjectSegmentation.SegmentationConfig = { enableSubjectForegroundImage: true };
```

### 第四步：调用分割

```typescript
const vi: subjectSegmentation.VisionInfo = { pixelMap: pm };
const res: subjectSegmentation.SegmentationResult = await subjectSegmentation.doSegmentation(vi, cfg);
```

### 第五步：取前景图

```typescript
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` 是逐像素数组，长度等于宽×高。要二次描边、收紧边缘、局部修补，从这里取：

```typescript
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`。

开前景图开关后的效果——左边原图，右边抠图垫白底：

![](assets/01-foreground.png)

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

![](assets/02-default-empty.png)

## 四条必须记住的结论

### 结论一：`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。做法是把过渡带压窄：

```typescript
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` 拿到每个物体的框，再对每个框裁出来单独送进分割。

```typescript
// 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
