相册照片不用上传:HarmonyOS Core Vision 端侧识别动物与植物
真机环境:ALN-AL80,OpenHarmony 7.0.0.32(Beta2),API 26。测试照片来自系统相册,识别结果由同一台真机上的 Core Vision Kit 返回。
为什么相册应用需要先理解照片
照片进入应用以后,如果仍然只能按时间浏览,数量一多就很难整理。文件名通常只是时间戳,拍摄备注也不一定存在,依赖人工逐张添加"动物""植物""建筑"等标签,实际使用中很难坚持。
双镜记忆相机需要在照片导入后完成一轮本地内容分析:识别照片中的常见目标,生成可用于分类、筛选和封面推荐的基础标签。这个过程不需要把原图上传到服务器,也不需要读取整套系统图库。用户在系统选择器里选中哪张照片,应用就分析哪张。
Core Vision Kit 官方能力介绍把多目标识别列为基础视觉能力之一,可识别图片中的动物、植物、建筑物、文本等常见目标,并返回目标区域。本文用相册中的绿植和鹦鹉照片完成两轮真机测试,重点观察三个结果:业务标签、有效目标数量,以及更换照片后结果是否会随之刷新。
实现链路
完整流程包含四步:
- 用
PhotoViewPicker让用户选择一张照片; - 把选择器返回的 URI 复制到应用沙箱;
- 解码为
PixelMap,交给ObjectDetector; - 过滤低置信度结果,把数字类别转换成相册可用的中文标签。
这四步都在真机本地完成。相册选择器只授权本次选中的照片,符合最小访问范围原则。华为的应用隐私保护指南也建议使用 Picker 获取用户明确选择的图片,避免为了单张照片申请整库访问权限。
第一步:只获取用户选中的照片
选择器限制为图片类型和单选:
ts
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1;
const picker = new photoAccessHelper.PhotoViewPicker();
const result = await picker.select(options);
const sourceUri = result.photoUris?.[0];
sourceUri 适合读取本次选择结果,但不应被当成应用长期管理的普通文件路径。双镜记忆相机在用户完成选择后立即把内容复制到应用沙箱,后续的解码、识别和记录保存都使用沙箱文件。
ts
const sourceFile = await fs.open(sourceUri, fs.OpenMode.READ_ONLY);
const targetFile = fs.openSync(
localPath,
fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC
);
await fs.copyFile(sourceFile.fd, targetFile.fd);
这里要保留 try/finally,无论复制成功还是失败,都关闭源文件和目标文件。连续导入多张照片时,如果文件描述符没有及时释放,后面的选择和解码会出现难以复现的失败。
第二步:控制解码尺寸
手机相册原图可能达到数千万像素。目标识别不需要先把完整原图以 RGBA 形式全部展开到内存,因此解码前先读取尺寸,把最长边限制在 1600 像素:
ts
const sourceData = readFile(localPath);
const imageSource = image.createImageSource(sourceData);
const sourceInfo = await imageSource.getImageInfo();
const scale = Math.min(
1,
1600 / Math.max(sourceInfo.size.width, sourceInfo.size.height)
);
const pixelMap = await imageSource.createPixelMap({
editable: false,
desiredSize: {
width: Math.floor(sourceInfo.size.width * scale),
height: Math.floor(sourceInfo.size.height * scale)
},
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
});
缩放只作用于分析输入,沙箱中的原始照片没有被覆盖。这样既保留后续查看和导出的原图,又避免识别阶段占用过多内存。
第三步:调用 ObjectDetector
ObjectDetector 接收包含 PixelMap 的请求,返回目标数组。每个目标包含类别编号、置信度和区域坐标。
ts
const detector = await objectDetection.ObjectDetector.create();
const request: visionBase.Request = {
inputData: { pixelMap }
};
const response = await detector.process(request);
const objects = response.objects;
分析器、PixelMap 和 ImageSource 都属于需要显式释放的资源:
ts
try {
// 创建 PixelMap 并执行 detector.process()
} finally {
await detector?.destroy();
await pixelMap?.release();
await imageSource?.release();
}
页面没有直接展示底层数字类别。识别结果先经过阈值过滤,再转换成相册业务标签:
ts
const visibleObjects = response.objects.filter(item =>
Number.isFinite(item.score) && item.score >= 0.32
);
function getSceneLabel(code: number): string {
switch (code) {
case 0: return '风景';
case 1: return '动物';
case 2: return '植物';
case 3: return '建筑';
case 9: return '猫咪';
case 10: return '狗狗';
case 11: return '美食';
case 12: return '汽车';
default: return '';
}
}
阈值不是越低越好。低阈值会让弱结果大量进入相册标签,后续搜索和分类反而变得不可靠。双镜记忆相机当前使用 0.32 作为通用目标下限,猫狗等容易影响分类的标签还会设置更高门槛。
第一轮真机测试:绿植照片
第一张照片包含成片的绿色叶片和粉色花朵。系统选择器完成授权后,照片被复制到应用沙箱,ObjectDetector 返回一个有效目标,类别转换后显示"植物"。

图 1:原图、识别状态和"植物"标签在同一张真机截图中;本轮返回目标 1、人脸 0。
页面中的"封面评分 34%"不是 Core Vision 的原始字段,而是双镜记忆相机根据目标置信度、标签数量、人脸构图等信息计算的业务评分。模型结果和应用计算结果分开标注,避免把产品策略误写成系统 API 返回值。
第二轮真机测试:鹦鹉照片
不退出实验页,重新打开系统选择器并改选鹦鹉照片。第二轮分析完成后,上一轮的"植物"被替换为"动物",有效目标仍为 1,人脸数量为 0。

图 2:更换为鹦鹉照片后,真机返回"动物";页面同时显示本轮分析时间和目标数量。
这次结果没有读取文件名,也没有根据页面文案预置标签。两张照片经过同一个 pick → copy → decode → process 流程,输入内容变化后,模型结果从"植物"切换为"动物"。
识别结果怎样进入相册业务
目标识别完成后,双镜记忆相机把分析结果和照片记录一起保存:
ts
interface GallerySceneAnalysis {
labels: Array<string>;
labelCodes: Array<number>;
primaryLabel: string;
objects: Array<GallerySceneObject>;
faceCount: number;
analyzedAt: number;
provider: 'core-vision';
}
这份结构可以直接支持三个功能:
- 相册筛选:点击"动物"或"植物"查看对应照片;
- 搜索召回:把本地目标标签作为结构化检索条件;
- 封面选择:结合目标置信度、人脸构图和清晰度挑选更合适的封面。
导入任务采用串行分析队列。用户一次选择多张照片时,先完成文件复制和记录创建,再逐张执行端侧识别,避免同时解码多张大图造成内存峰值。识别失败不会阻止照片入库,记录会保留"待分析"状态,下一次启动后可以继续补齐。
实践中容易踩的三个坑
1. 把 Picker URI 当作长期文件路径
选择器 URI 用于访问用户本次授权的内容。需要持久处理时,应复制到应用沙箱,再保存沙箱路径;不要对 URI 做字符串裁剪后交给普通文件接口。
2. 原图尺寸直接解码
高像素照片完整解码会显著增加内存占用。先读取尺寸、按最长边计算缩放比例,再创建分析用 PixelMap,原图文件仍可完整保留。
3. 忽略空结果和低置信度结果
process() 成功只表示分析流程完成,不表示每张照片都一定有可用目标。页面应同时处理"有标签""无有效目标"和"分析失败"三种状态,业务层还要使用统一阈值过滤弱结果。
最终效果
本次 API 26 真机实践完成了从系统相册授权到端侧目标识别的完整闭环:绿植照片返回"植物",鹦鹉照片返回"动物",两轮结果都包含一个有效目标,并随照片更换实时刷新。原图没有上传,应用也没有申请读取整个系统图库。
对于照片管理类应用,这条链路的价值不止是展示一个 AI 标签。它把"用户选择照片、应用获得稳定文件、端侧理解内容、结果进入分类与搜索"连接成了可落地的数据入口,后续的相册筛选、封面推荐和本地检索都可以复用同一份分析结果。