← 返回技术札记
ON-DEVICE AI · 2026.09.01

在浏览器里完成照片人物分类

图派人物分类的实现记录:使用 MediaPipe、SFace 和 ONNX Runtime Web,在摄影师的浏览器里完成人脸检测与相似人物分组。

一场婚礼、毕业典礼或家庭活动拍完,订单里通常有几百张照片。同一个人散落在不同环节、不同机位和不同合影中,客户选片时常常要来回翻很多遍。

图派最近加入了人物分类。摄影师在订单里启动扫描后,系统会把相似的人脸归到同一组。客户打开选片页面,也可以从人物头像进入,只看与自己相关的照片。

这项功能从一开始就确定了一个实现原则:人脸特征在摄影师当前使用的浏览器里计算。服务端保存照片与人物组之间的关系,不保存人脸特征向量,也不会给人物添加真实身份。

从一张合影开始

人物分类最终呈现得很简单:人物 1、人物 2、人物 3。点击头像,就能过滤出这个人物出现过的照片。

背后的处理流程分为五步:下载订单预览图、检测人脸、对齐面部、提取特征、按照相似度分组。检测和计算都由浏览器完成,分组结果确认后再保存到订单。

01订单预览图逐张下载
02MediaPipe人脸与关键点
03SFace · WASM本机特征向量
04余弦相似度订单内聚类
05人物筛选网页与小程序

整个过程只处理当前订单。重新运行人物分类时,也会从这个订单的照片重新开始。

先找到脸,再摆到相同的位置

扫描开始后,浏览器会逐张获取较大的预览图。图片进入检测前会调整尺寸,最长边控制在 1600 像素以内,同时尽量保证短边达到 720 像素。这个尺寸兼顾了处理速度,也能为合影中的小脸保留更多细节。

第一轮检测使用 MediaPipe Face Landmarker,得到人脸边界和眼睛、鼻尖、嘴角等关键点。整张图没有找到人脸时,还会进行一次局部检测:画面切成左、中、右和上、下组合的六个重叠区域,每个区域放大后再送入模型。

这个补充步骤对大合影很有用。合影缩小到浏览器检测尺寸后,远处的人脸占用像素很少;局部放大能提高被发现的机会。检测结果随后映射回原图坐标,并根据重叠程度去掉重复项。

短边不足 48 像素的人脸会被跳过。这类人脸提供的细节有限,参与分组后容易产生不稳定结果。

找到人脸之后,系统使用五个关键点计算相似变换,将每张脸旋转、缩放到 112 × 112 的统一画布。这样可以减小头像大小、轻微歪头和拍摄角度对特征的影响。

const transform = solveSimilarityTransform(sourceKeypoints);
context.setTransform(
  transform.a, transform.b,
  transform.c, transform.d,
  transform.e, transform.f
);
context.drawImage(imageSource, 0, 0);

SFace 留在浏览器里运行

对齐后的人脸会转换为 [1, 3, 112, 112] 的浮点张量,再交给量化后的 SFace ONNX 模型。推理由 onnxruntime-web 的 WebAssembly 后端执行,目前使用单线程运行。

模型与运行时文件由图派站点提供,在摄影师点击“人物分类”后加载。每张脸经过模型后会得到一组特征数值,并在内存中完成 L2 归一化:

const norm = Math.sqrt(values.reduce(
  (sum, value) => sum + value * value,
  0
));

const embedding = values.map(value => value / norm);

归一化后的向量可以使用余弦相似度进行比较。数值越接近 1,两张脸在模型看来越相似。这些向量只服务于本次订单的分组,页面关闭后不会作为人物资料留在服务端。

扫描采用逐张处理。图片解码、Canvas 绘制和模型推理都发生在页面中,控制并发可以让界面响应和内存占用更平稳。进度也会随处理结果实时更新,摄影师能看到当前扫描到哪一张。

把相似的人脸放到一起

当前订单的人脸特征准备好后,系统开始增量聚类。每张新脸都会与现有各组的中心向量比较;最高相似度达到 0.48 时加入对应分组,否则创建一个新组。新成员加入后,分组的中心向量会随之更新。

合影带来了一个很实际的问题:同一张照片中可能同时出现长相相近的家人。为此,聚类增加了一条约束,同一张照片里的两张脸不会进入同一个人物组。

if (cluster.faces.some(member =>
  String(member.pictureId) === String(face.pictureId)
)) return;

分组完成后,人物会按照所含照片数量排序。每组选择面积较大的一张脸作为代表头像,同时记录人脸在照片中的归一化坐标。网页和小程序可以直接使用原有缩略图,按照这个位置显示头像。

人物分类的结果主要用于缩小查找范围。摄影师可以快速浏览每组照片,再决定是否发布给客户。

保存结果,以及何时展示给客户

浏览器内存中包含用于相似度计算的人脸向量和分组中心。提交分组结果时,保留下来的数据只有:

  • 照片 ID;
  • 人脸在照片中的序号和位置;
  • 人物组的代表照片;
  • 每组包含的照片数量。

服务端保存接口会检查提交内容。请求中一旦包含 embedding 字段或特征向量,就会直接拒绝;同时还会确认照片属于当前订单、人脸坐标处于合法范围。

浏览器内存:人脸特征、分组中心
                         ↓ 不提交
服务端存储:照片 ID、人物组、代表照片、人脸位置

完成分类后,人物组先保留在摄影师端。摄影师打开“发布到客户选图端”,网页和小程序才会显示人物头像筛选;关闭发布后,客户侧也不会获得这些分组数据。

客户看到的仍然是一套熟悉的选片界面,只是多了一个更快抵达相关照片的入口。对摄影师来说,几百张照片不再只能按照时间顺序从头翻到尾;对图派来说,本地推理让这项能力可以在清晰的数据边界内落地。

FIELD NOTES

更多工程记录,都在技术札记里。

只写已经发生的问题、验证过的方案,以及仍然存在的边界。

查看全部札记