# 红底蓝底一秒切换 # 智绘鸿蒙·如 7 而至#

## 前言

证件照换底色是个很具体的需求：报名系统要红底，签证要白底，工卡要蓝底，而手机里只有同一张正面照。

主体分割这个能力之前已经把接口跑通了，这篇只做一件事：**把 mask 变成能用的合成结果**，并且把三种边缘处理方式放在一起比数据。

结论先放这里：模型给的 alpha 已经是软的，再羽化几乎没有收益；真正能改变过渡带宽的是阈值收缩。但在人像这种"主体和背景对比强"的图上，三种做法肉眼看不出差别——所以别默认加滤镜，先测。

## 清单

### 1. 素材与目标

一张 640×640 的正面人像，灰色棚拍背景。目标：输出红底（#D9001B）、蓝底（#438EDB）、白底三张，加两个边缘对照组。

### 2. 分割

```typescript
const vi: subjectSegmentation.VisionInfo = { pixelMap: pm };
const res = await subjectSegmentation.doSegmentation(vi, {});
const mask: Int32Array = res.fullSubject.mattingList;
```

配置传空对象即可。**这次不需要 `enableSubjectForegroundImage`**——我们要的是 mask 本身，前景图是给"只想直接拿抠好的图"的场景用的。

### 3. 读 mask

| 项 | 实测值 |
| --- | --- |
| `mask.length` | 409600，正好等于 640×640 |
| 取值种类 | 256 |
| 过渡像素（0 < a < 255） | 12703，占 3.10% |

三个要点：

- 类型是 `Int32Array`，不是 `Uint8Array`，但值域是 0~255。当字节流处理会读错。
- 逐像素，一一对应行优先排列，不需要额外缩放。
- 256 种取值 + 3.1% 的过渡带，说明它给的是软 alpha。这一点决定了后面所有事。

### 4. 读原始像素

```typescript
const buf: ArrayBuffer = new ArrayBuffer(w * h * 4);
await pm.readPixelsToBuffer(buf);
const p: Uint8Array = new Uint8Array(buf);
```

RGBA_8888，四个字节一像素。

### 5. 合成（over 运算）

```typescript
const t: number = alpha[i] / 255;
d[i * 4]     = Math.round(bk.r * (1 - t) + p[i * 4] * t);
d[i * 4 + 1] = Math.round(bk.g * (1 - t) + p[i * 4 + 1] * t);
d[i * 4 + 2] = Math.round(bk.b * (1 - t) + p[i * 4 + 2] * t);
d[i * 4 + 3] = 255;
```

注意最后一位直接写 255：底色是不透明的，输出不需要 alpha。留着 alpha 的话，白底图放到深色界面上会透出界面颜色。

### 6. 从 buffer 造回 PixelMap

```typescript
const opts: image.InitializationOptions = {
  size: { width: w, height: h },
  srcPixelFormat: image.PixelMapFormat.RGBA_8888,
  pixelFormat: image.PixelMapFormat.RGBA_8888,
  editable: true
};
return await image.createPixelMap(out, opts);
```

`srcPixelFormat` 必须显式给。不给的话缓冲区会被按默认格式解释，颜色直接错掉，而且不报错——这是最容易踩的一条。

### 7. 三种边缘处理，比数据

| 做法 | 公式 | 过渡像素 | 占比 |
| --- | --- | --- | --- |
| 直接用 | `a` | 12703 | 3.10% |
| 3×3 均值羽化 | `mean(a)` | 13950 | 3.41% |
| 阈值收缩 | `clamp((a-128)*2.2+128)` | 2391 | **0.58%** |

三条结论：

1. **羽化让过渡带变宽了**（3.10% → 3.41%），方向反了。模型输出的边缘本来就只有一两个像素宽，再卷积一次只会把边界抹开。
2. 阈值收缩把过渡带压窄到 0.58%，是原来的 1/5.3。这才是"想让边缘利落"该用的手段。
3. 但在这张人像上，四张成图肉眼几乎分不出来。原因是主体（深色衣服、黑头发）和灰底对比足够强，边缘本来就没有可见的残留色。

所以顺序应该是：**先看有没有 fringe，再决定要不要处理**。收缩不是免费的——它会把本来正确的半透明像素强行推到 0 或 255，头发丝这种细节会变脆。

### 8. 展示

四列并排：硬边红底、羽化红底、羽化蓝底、羽化白底，再加一列收缩红底做对照。白底最容易暴露问题，因为残留的是浅灰，和红底比对比度低。

## 耗时

| 环节 | 实测 |
| --- | --- |
| 分割 `doSegmentation` | 986 ~ 1112ms |
| 单张合成（640×640，逐像素 JS 循环） | 231ms |
| 三张连做 | 674ms |

231ms 是纯 ArkTS 循环跑出来的，41 万次迭代。分辨率上去之后这个数会线性增长，但**我没有测更高分辨率的实际值**，所以不给公式。真要上生产，两条路：把循环挪到 worker，或者用 Canvas 的图层混合代替手写合成。

## 为什么不用现成的前景图

`SegmentationConfig` 里有个 `enableSubjectForegroundImage`，开了会直接返回一张抠好的前景 `PixelMap`。看起来正好是换底色要的东西，但这次没用它，三个理由：

1. 拿到的是一张带 alpha 的图，还要自己乘一遍底色才能得到不透明成品，省不掉那 41 万次循环。
2. 看不到中间量。换底色出问题（毛边、透底、边缘残留原背景色）时，需要知道是 mask 的问题还是合成的问题。自己拿 mask 才能把这两步分开测。
3. 边缘策略要改 mask。阈值收缩、局部腐蚀这些操作只能作用在 alpha 上，拿到成品图就只能整体处理，会把头发丝一起削掉。

所以规则是：**只要需要对边缘做主，就自己拿 `mattingList`**；只是想去掉背景、不关心边缘质量的，用现成前景图更省事。

## 阈值收缩那一步在做什么

```typescript
let v: number = (raw[i] - 128) * 2.2 + 128;
v = v < 0 ? 0 : (v > 255 ? 255 : v);
```

这是围绕 128 做对比度拉伸：把 128 以下压到 0，128 以上抬到 255，中间只有 1/2.2 的宽度还保留过渡。效果是过渡带从 3.10% 缩到 0.58%，边界更"硬"。

代价有两个方向。一是原来正确的半透明像素被强行推到端点，头发丝、宠物毛这类地方会断；二是 128 这个中心点是假设，如果模型对某类主体给的 alpha 整体偏低（比如半透明衣物），以 128 为中心收缩会直接把主体吃掉一块。

真要上线，这个中心点和倍率都得做成可调参数，并且**在灰底、白底、复杂背景三类样本上各测一遍**再定默认值。这次只有一张灰底人像，不足以定参数。

## 还差的几步

这篇只覆盖到"能换底色"。离一张能交的证件照还差：

- 尺寸与头部占比裁剪（一寸/二寸有明确头部高度比例，靠 `subjectRectangle` 定位）
- 白底的光晕检查（浅灰背景最容易残留）
- 打印色彩管理（RGB 转 CMYK 的偏色，端侧不管这个）

这三项本轮没做，不猜结论。

## 真机数据

MatePad Pro（HarmonyOS 7 / API 26，序列号略），样本为 640×640 灰底人像。

![四张合成结果与指标面板](assets/01-four.png)

![加入阈值收缩后的五列对照](assets/02-five.png)

## 总结

换底色这件事，接口只提供了"哪里是人"，剩下的 alpha 合成、格式声明、边缘策略全是自己的。清单里最容易翻车的是第 6 步的 `srcPixelFormat`，错了不报错只是颜色不对。

适合证件照、工卡、电商主图换背景这类"主体明确、背景单一"的活。发丝、毛皮、半透明衣物这类边缘，软 alpha 也救不了，别指望调参解决。

> 💡 **一句话**：模型给的 alpha 已经是软的，再羽化只会把边缘抹糊；想利落该用阈值收缩，但先确认图上真的有毛边。

## 参考文献

1. 主体分割（开发指南）：https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-subject-segmentation
2. subjectSegmentation API 参考：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-references/errorcode-core-vision
4. Core Vision Kit 目录：https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-kit-guide
