
HarmonyOS 真机实战:一张普通照片怎样自动抠出人物并生成旅行海报
真机环境:ALN-AL80,OpenHarmony 7.0.0.32(Beta2),API 26;人物素材为专门生成的虚构成年旅行者测试图,不包含真实用户和私人相册内容。
为什么旅行合成最难的是第一步
把人物放到海岸、雪山或古镇模板上,看起来像一次普通的图片叠加。真正影响结果的却是人物边缘:头发、背包带、衣服轮廓只要带着原背景,换景后就会出现明显灰边;让用户用手指一点点涂抹,又很难成为相册里的高频功能。
双镜记忆相机已经有旅行模板和图片编辑页,缺少的是从普通照片直接得到透明前景的入口。本次实践使用 Core Vision Kit 主体分割,从相册详情发起抠图,得到人物前景后进入模板编辑,最后导出一张新的 JPEG,并保留原照片。
验证链路按四个画面连续进行:原始人物照片、主体分割后的默认模板、切换模板并调整大小、导出后回到真实照片详情。
先确认官方能力边界
Core Vision Kit 官方能力页将"主体分割"描述为检测并提取图片中区别于背景的前景物体或区域;Vision Kit 官方能力页也把主体分割用于应用内图片浏览和抠图场景。
本次使用的是 @kit.CoreVisionKit 中的 subjectSegmentation:输入一张 PixelMap,配置需要的主体数量和前景图,接口返回主体数量、主体明细以及带透明信息的前景图。旅行模板、人物缩放、位置调整和 JPEG 导出仍由应用完成。
这条边界很重要。主体分割结果是可继续编辑的前景素材,最终画面质量还取决于模板构图、人物比例、边缘融合和输出编码。
准备一张可公开复现的人物图
可复现的抠图测试需要固定、可公开使用的输入素材。本次使用一名虚构成年旅行者:蓝色外套、浅色长裤、橙色背包,人物完整站立在灰色背景前。服装与背景有足够色差,手脚也没有被画面裁断,便于观察主体轮廓。

图 1:从双镜记忆相机的照片详情进入"旅行画境",输入图仍是带灰色背景的完整照片。
真实产品中,人物照片可能存在逆光、头发与背景同色、肢体被遮挡等情况。入口应允许服务返回"未识别到清晰主体",不能用原图矩形直接覆盖模板来伪装抠图成功。
从 JPEG 解码到主体前景
主体分割服务接收 PixelMap。沙箱中的 JPEG 需要先打开为只读文件,再按 RGBA_8888 解码:
ts
const sourceFile = await fs.open(sourcePath, fs.OpenMode.READ_ONLY);
const imageSource = image.createImageSource(sourceFile.fd);
const sourcePixelMap = await imageSource.createPixelMap({
editable: false,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
});
随后初始化模型,并明确要求返回一个主体的明细和前景图:
ts
const initialized = await subjectSegmentation.init();
if (!initialized) {
throw new Error('主体分割服务初始化失败');
}
const result = await subjectSegmentation.doSegmentation(
{ pixelMap: sourcePixelMap },
{
maxCount: 1,
enableSubjectDetails: true,
enableSubjectForegroundImage: true
}
);
if (result.subjectCount <= 0) {
throw new Error('没有识别到清晰人物');
}
const foreground = result.subjectDetails?.[0]?.foregroundImage
?? result.fullSubject.foregroundImage;
subjectCount 是进入编辑器前的硬条件。分割没有返回主体时,页面保留原照片并提示换一张轮廓更清晰的图片,不创建空白成片。
文件、ImageSource、源 PixelMap 和模型都在 finally 中释放。大尺寸照片连续处理时,如果只关注前景结果而忘记释放输入,几次操作就可能累积明显内存压力。
分割成功后直接进入模板编辑
真机完成主体分割后,灰色背景从人物周围消失,编辑页把透明前景放到默认旅行模板中。截图里人物背包、外套和腿部轮廓已经与新背景分离。

图 2:Core Vision Kit 返回人物前景,编辑页显示默认模板及可移动、缩放的人物图层。
编辑页不再读取原始 JPEG 作为人物层,而是持有分割后的前景 PixelMap。模板层负责背景,人物层负责平移、缩放和镜像,两个图层在导出阶段合成。这样切换模板时不需要再次运行主体分割。
人物初始比例通常只是可用起点。横向海岸模板需要人物略小,竖向城市模板则可能需要上移。页面把比例限制在合理范围内,避免缩成一个点或放大到完全超出画布。
切换海岸模板并调整人物比例
第二步选择"海岸珊瑚落日",把人物比例调到 85%。这次调整只改变编辑模型,透明前景没有重新生成。

图 3:模板已经切换为海岸落日,人物比例为 85%,脚部位置落在前景岩石区域。
模板切换和主体分割分开后,交互会更稳定:分割是耗时的模型任务,只执行一次;拖动、镜像和缩放是本地画布操作,可以实时响应。导出前还要把人物位置限制在成片画布内,避免手势结束后只剩一部分身体。
合成时遵循固定顺序:先绘制模板,再根据编辑参数绘制人物前景,最后生成一张新的 PixelMap。原照片不会参与覆盖写入。
导出新照片,不覆盖原图
完成编辑后,应用把合成 PixelMap 以质量 94 编码为 JPEG,写入独立的 travel_compositions 目录:
ts
const outputDirectory = `${context.filesDir}/travel_compositions`;
TravelComposeOutputService.ensureDirectory(outputDirectory);
const safeTemplateId = templateId.replace(/[^0-9a-zA-Z_-]/g, '_');
const outputPath =
`${outputDirectory}/travel_card_${safeTemplateId}_${Date.now()}.jpg`;
const outputFile = fs.openSync(
outputPath,
fs.OpenMode.CREATE |
fs.OpenMode.WRITE_ONLY |
fs.OpenMode.TRUNC
);
const packer = image.createImagePacker();
await packer.packToFile(composite, outputFile.fd, {
format: 'image/jpeg',
quality: 94
});
输出成功后,新文件作为一条独立相册记录写入双镜记忆相机,并继承必要的旅行信息;原始人物照片仍保留在相册中。写文件或编码失败时会清理未完成的目标文件,避免图库里出现零字节成片。

图 4:导出的海岸旅行成片已经进入照片详情,页面提示原照片仍保留在相册中。
这张图完成了结果闭环:图 1 的灰底人物经过主体分割,图 2 进入模板,图 3 完成构图调整,图 4 则是实际写出的新 JPEG,而不是编辑页上的临时预览。
实践中最容易踩的几个点
1. 只要蒙版,不请求前景图
旅行合成需要直接绘制透明人物。配置中应打开 enableSubjectForegroundImage;只取区域或蒙版还要增加一次像素处理,并承担边缘和透明通道的组合工作。
2. 把模型任务放进手势更新
主体分割应在进入编辑器时完成一次。缩放、移动和切换模板只更新本地编辑参数,不能在每次手势变化时重新调用模型。
3. 导出覆盖输入文件
覆盖原图会让用户失去拍摄记录,也会使相册索引和缓存失效。新成片使用独立路径和记录 ID,成功写入后再刷新相册。
4. 测试素材本身不适合抠图
人物被裁断、主体太小、衣服与背景完全同色时,分割边缘容易失真。公开测试素材保持完整人物和清晰轮廓;真实相册仍需要失败提示与重新选图入口。
最终效果
ALN-AL80 真机已经完成从普通人物照片到旅行成片的完整路径:JPEG 解码为 RGBA_8888,Core Vision Kit 返回透明人物前景,编辑页切换海岸模板并把人物调整到 85%,最后输出新的 JPEG 并进入真实照片详情。
这套实现把模型能力放在最适合的位置:主体分割负责提供可编辑前景,画面设计由模板和本地编辑器完成,文件服务负责生成独立成片。四张连续真机图分别对应输入、分割、编辑和输出,最终结果可以在相册中继续查看、分享或制作回忆电影。