
AI 相册识别原理,一句话搜图是怎么做到的
1 序言:先看它解决了什么问题
自部署相册(Immich、MT Photos、飞牛相册)这两年集体上线了一个能力:不用翻文件夹、不用记文件名,输入一句话就能把照片捞出来。
-
在搜索框输入「灯火通明的街道」,能返回对应照片;
-
输入「海边看日落」,能跨过所有相册把场景找齐;
-
相册自动把同一个人的照片聚合在一起,生成「人物相册」,点开就是 TA 从小到大的记录。
这套能力背后不是某个单一模型,而是两类模型 + 一套检索基础设施的分工:
| 能力 | 用的模型 | 回答的问题 |
|---|---|---|
| 语义搜图 | CLIP 双塔模型 | 一句话和一张图「像不像」 |
| 人物相册 | 人脸检测 + 人脸识别 + 聚类 | 两张脸是不是同一个人 |
| 向量检索 | pgvector / VectorChord 等 | 百万向量里怎么快速找最近的 |
先说一个很多人会问的问题:这背后是信号系统还是傅里叶变换? 答案在第三章,都不是 —— 真正的主角是线性代数里的向量内积和机器学习里的对比学习。傅里叶在这条链路里确实存在,但它管的是 JPEG 压缩,不负责「理解」。
2 全局架构:三套件各司其职
先看一张总图,把整条链路串起来:
┌────────────────────────────────────┐ │ 本地推理服务 │ 照片入库 │ │ │ 扫描 + 生成预览图 │ CLIP 图像编码器 ──► 图像向量(512维) │ ▼ │ 人脸检测/对齐/特征 ─► 人脸向量(512维) │ 图片库 ─────────────►│ │ └──────────────┬─────────────────────┘ │ 写入 ▼ 向量存储(pgvector 等) ▲ 用户输入「海边看日落」 │ 查询 ──► CLIP 文本编码器 ──► 文本向量 ─┘ │ ▼ 余弦相似度排序,取 top-k 返回两个要点:
- 离线阶段把图「翻译」成向量。照片入库时先跑一遍推理,把每张图变成一串数字(embedding),存进数据库。用户搜索时不用再碰原图,只做向量计算。
- 全程本地推理。三个产品的模型都跑在 NAS 自己的 CPU/GPU 上,照片不出内网 —— 这是自部署相册相对 iCloud/Google Photos 的核心差异。
3 数学内核:不是信号系统,也不是傅里叶变换
先直接回答标题里的问题。 AI 相册的数学核心不是傅里叶变换,也不是经典信号处理,而是四件事:向量内积、对比学习、仿射变换、密度聚类。 它们恰好分布在链路的不同环节:
| 数学工具 | 用在哪 | 管什么 |
|---|---|---|
| 余弦相似度 / 向量内积 | 语义检索、人脸对比 | 「像不像」的量化度量 |
| 对比学习(InfoNCE 损失) | CLIP 模型训练 | 学出一个「语义即距离」的向量空间 |
| 仿射 / 相似变换 | 人脸对齐 | 把歪着的脸几何摆正 |
| 密度聚类(DBSCAN) | 人物相册聚合 | 无监督地把人脸自动分组 |
| DCT(离散余弦变换,傅里叶家族) | JPEG/WebP 压缩、缩略图 | 频域压缩,不参与语义理解 |
| 卷积(CNN 内部) | 图像编码器 | 可学习的局部特征提取 |
| 下面把每个公式讲透,都是从「一张图 = 一个向量」这个起点推导出来的。 |
3.1 余弦相似度:相册里出现频率最高的公式
一切的起点是:把图、句子、人脸都表示成向量。有了向量,相似度就有了统一的度量 —— 夹角余弦:
其中 是内积(对应分量相乘再求和), 是 L2 范数(向量的长度)。
几何含义:两个向量夹角的余弦。方向越一致,值越接近 1;方向越正交,越接近 0;方向相反,接近 -1。它天然在 ,不随向量长度变化 —— 这正是我们要的:判断「语义像不像」只关心方向,不关心图的亮度、对比度这类幅度信息。
举个 3 维的数值例子(真实系统是 512 维,道理一样)。两张同一个人的人脸向量:
再来一个不同人的人脸向量:
设判定阈值 : → 同一个人; → 不同人。「对比人物图片」在数学上就是这么一句话:算内积,比阈值。
工程上还有个常用技巧 ——L2 归一化:先把每个向量除以自己的长度,让 ,再算内积:
归一化后,内积直接等于余弦相似度。归一化有两个好处:一是所有向量落在同一个单位球面上,距离比较公平;二是省一次除法—— 模型直接输出归一化向量,查询时一个点积完事。insightface 的 normed_embedding 就是这个东西。
3.2 对比学习:CLIP 是怎么「学」出这个空间的
余弦相似度只是尺子,尺子本身是训练出来的。CLIP 的核心不是模型结构,而是它的训练目标 —— 对比学习(contrastive learning)。
取一个 batch 的 对图文(图 配文 ),算 的相似度矩阵,对角线是正确配对。损失用带温度系数的 InfoNCE(也叫多类 NCE):
其中 是温度系数。这个式子等价于:对第 行做 softmax,目标是对角线位置概率为 1。逐项看:
-
分子 :正确配对要尽量相似,分数越高越好;
-
分母 :所有配对(含错误的)一起归一化,逼着模型把负样本的分数压低;
-
温度 :缩放整个相似度分布。 越小,softmax 越尖锐,模型对「难分」的负样本越敏感,训练越激进; 太大则梯度消失,学不动。
文本1 文本2 文本3图1 ✓ ✗ ✗图2 ✗ ✓ ✗ ← 对角线为正确配对(InfoNCE 拉近)图3 ✗ ✗ ✓ 非对角线为负样本(推远)训练完成后,模型学到了一个嵌入函数 **** :任何图、任何句子,经过它都映射成 512 维向量,并且语义相近的对象在向量空间里距离近。这整套思路在机器学习里叫度量学习(metric learning)—— 不直接学「这是猫」,而是学「猫和猫的向量该靠多近」。
3.3 傅里叶变换到底在哪:它管压缩,不管理解
既然你问到傅里叶,那就把它在相册链路里的位置讲清楚 ——它真实存在,但负责的是图像压缩,不是语义识别:
-
JPEG / WebP 的 DCT(离散余弦变换):DCT 是离散傅里叶变换(DFT)的实值变体。压缩时把图像切成 8×8 的像素块,每个块做 DCT,得到 64 个频域系数 —— 低频在前、高频在后,然后丢掉人眼不敏感的高频分量,实现几倍到几十倍的压缩。你在相册里看到的预览图、缩略图,全都经过这一层;
-
清晰度 / 模糊检测:模糊照片在频域上就是高频能量衰减,可以用频域指标评估;
-
CNN 卷积与傅里叶的联系:卷积定理 说「时域卷积 = 频域相乘」,理论上可以用 FFT 加速卷积。但 ONNX Runtime 实际推理用的是直接卷积和 Winograd 算法,不依赖 FFT—— 这是实现细节,不是设计思想。
一句话总结:傅里叶管「像素的频域结构」,深度学习管「像素的语义」。CLIP 和人脸模型要回答的是「画面里有什么、这是谁」,这种高层语义没有固定的频域特征,只能靠数据驱动学出来。而信号系统(滤波、卷积)在 CNN 内部确实存在 —— 每个卷积核就是一组可学习的滤波器,但它只是构建特征的手段,不是相册系统设计的主线数学。
4 语义搜索:CLIP 双塔模型
4.1 为什么不能用「打标签」
传统图片搜索是给每张图人工打标签(海滩、日落、人),再按标签匹配。这条路在个人相册场景直接死掉:
-
标签穷举不完。「灯火通明的街道」这种描述,事前根本想不到要打这个标签;
-
组合语义没法枚举。「海边看日落」是「海 + 日落 + 人在场」的语义组合,不是三个标签的简单叠加;
-
召回靠运气。标签没打准,照片就永远搜不到。
CLIP 的思路完全不同:不生成任何标签,直接把图和句子映射到同一个向量空间,让「一张图」和「一句描述它的话」在空间里离得足够近。
4.2 双塔编码器
CLIP(Contrastive Language–Image Pre-training)由两个独立编码器组成:
-
图像编码器:用 ViT(如 ViT-B/32)或 ResNet 系列,把图片编码成向量;
-
文本编码器:基于 Transformer,把句子编码成向量。
两个编码器的输出经过投影层对齐到同一维度(如 512 维)。训练目标就是 3.2 节的 InfoNCE:「猫的图片」和「一只猫」这两个向量在空间里距离很近,而与「一辆车」的向量距离很远。这样一来,图片和文字就被统一到了同一把「尺子」上。
4.3 对比学习训练
训练数据是海量「图文对」(OpenAI 原始版本用了约 4 亿对网络图文)。流程:
-
取一个 batch 的 对图文,图像、文本分别过两个编码器,得到 相似度矩阵;
-
对角线是「正确配对」(图 1 ↔ 文 1),其余是负样本;
-
按 3.2 节的 InfoNCE 损失反向传播,拉近对角线、推远非对角线。
训练完成后,CLIP 天然获得零样本分类能力:把候选类别写成句子(a photo of a dog),与图片向量算相似度,最高者即预测类别。
4.4 推理:一句话变向量
搜索时只做两件事:
-
把用户输入的一句话(如「夕阳下的沙滩」)过文本编码器,得到一个文本向量;
-
用这个向量和库里所有图片向量算余弦相似度(3.1 节公式),按分数从高到低排序,取 top-k 返回。
关键点:CLIP 匹配的是「语义」而不是「关键词」。图片里没有「夕阳」这个字的标签,但只要画面语义接近,向量距离就近。Immich 官方 FAQ 也明确说明:系统不会生成任何 tags、labels 或 descriptions,搜索完全靠向量相似度。
4.5 三家用的 CLIP 模型
| 产品 | 默认语义模型 | 特点 |
|---|---|---|
| Immich | CLIP ViT-B-32(可切换其他 CLIP 变体) | 开源、社区模型多,官方提供切换列表 |
| MT Photos | XLM-Roberta-Base-ViT-B-32(LAION-5B 训练) | 多语言文本编码器,中文理解更好;官方说明该模型「解决了不认识颜色的问题」 |
| 飞牛相册 | 智能识别模型(基础 / 增强两档) | 按硬件推荐,增强模型理解更精准、更占资源 |
中文用户选模型时要注意:CLIP 的文本编码器决定它「懂不懂中文」。用英文训练集的原始 ViT-B-32 搜中文,效果会明显打折;MT Photos 之所以换多语言模型,就是为了修这个问题。
5 向量检索:从「全库遍历」到「最近邻」
向量算出来了,接下来是检索问题:几十万张照片,怎么快速找出与查询向量最接近的一批?
5.1 存哪、怎么查
最简单的方式是全库暴力遍历 ——512 维向量的余弦计算非常快,十几万条也只是毫秒级,小相册完全够用。规模再大,就需要专门的向量存储:
-
pgvector:PostgreSQL 的向量扩展,把 embedding 当普通列存,SQL 里直接做最近邻查询。Immich 早期版本用它;
-
VectorChord:pgvector 的磁盘优化重写版(解决向量膨胀和写入放大)。Immich 新版文档明确说明语义搜索由 VectorChord 驱动;
-
更大规模还可以上 HNSW / IVF 这类近似最近邻索引,用一点精度换数量级速度。
数学上,最近邻问题就是:给定查询向量 ,在集合 里找
距离和相似度只是互为 1 的补:。距离越小越像,这个换算在后面飞牛阈值那里还会用到。
5.2 一行 SQL 的事
pgvector 风格的真实查询长这样:
-- <=> 是余弦距离算子,按距离升序取最近的 100 个SELECT asset_idFROM asset_embeddingORDER BY embedding <=> $1LIMIT 100;对用户来说这是「一句话搜图」;对系统来说,这就是一次向量距离排序。整个搜索过程没有碰任何图片文件。
6 人脸识别:检测、对齐、特征、聚类
「人物相册」和「对比两张图是不是同一个人」是同一套流水线的两个阶段,完整链路是:
图片 ─► 人脸检测(找脸+关键点) ─► 对齐(摆正) ─► 特征提取(512维向量) │ ├─ 两张图:算余弦相似度 → 阈值判断「是不是同一人」 └─ 全库:聚类 → 生成人物相册6.1 检测:先找到脸在哪
第一步从图片里找出人脸框(bounding box)和 5 个关键点(两眼、鼻尖、两口角),并给出置信度。
Immich 和 MT Photos 都用 insightface 家族的 buffalo_l 模型包:检测器是 SCRFD(RetinaFace 一脉的轻量高效检测器),在 CPU 上也能跑得动。飞牛官方把它称为「人脸识别 - 标准模型」。
这一步还负责过滤:检测置信度低于阈值就当作「没有人脸」,直接跳过。飞牛的人脸设置里那个 置信度阈值(默认 0.65) 管的就是这个环节。
6.2 对齐:把脸摆正(几何变换)
侧脸、低头、仰角都会让特征提取质量下降。用检测出的 5 个关键点做一次几何变换,把眼睛、鼻子、嘴巴「转」到标准位置,再裁出人脸区域。对齐是特征稳定的前提,跳过这一步,同一张脸在不同角度下特征会漂移。
数学上是标准的相似变换(旋转 + 等比缩放 + 平移,保形不变),矩阵形式:
其中 是缩放、 是旋转角、 是平移。用标准 5 点(左右眼、鼻尖、左右口角)和检测出的 5 点解这个方程,求出 、、,把原图像素按矩阵映射到标准位置即可。insightface 内部做的就是这件事。
6.3 特征:512 维人脸指纹
对齐后的人脸图送入识别模型(buffalo_l 内是 ArcFace 系的 w600k_r50),输出一个 512 维的归一化向量—— 相当于这张脸的「数字指纹」。同一个人不同照片的指纹相似,不同人差异大。
为什么是 512 维?这是经验与工程权衡的结果:维度越高,容纳的区分信息越多,但存储、计算、过拟合风险也同步上升。人脸领域在 128–1024 维之间都有人用,ArcFace 论文验证 512 维在精度和速度上平衡最好。
insightface 官方 API 的用法就是直白的「取向量、算点积」:
from insightface.app import FaceAnalysisimport cv2
app = FaceAnalysis(name="buffalo_l")
a = app.get(cv2.imread("a.jpg"))[0].normed_embedding # 人脸A的512维向量b = app.get(cv2.imread("b.jpg"))[0].normed_embedding # 人脸B的512维向量
score = a @ b # 归一化向量的点积 = 余弦相似度,0~1is_same = score > 0.5 # 超过阈值判定为同一人6.4 对比:两张图是不是同一个人
这就是「对比人物图片」的原理,数学版本:
提取两张脸各自的 512 维特征向量,按 3.1 节公式算余弦相似度
超过阈值
就是同一个人。
阈值是召回与误判的平衡点:
-
阈值调低 → 更容易把不同的人判成同一人(漏放行);
-
阈值调高 → 更严格,但同一个人可能被拆成多个相册。
这里有个容易混淆的点要拆开:相似度阈值和距离阈值是互补的。距离 ,所以「差异阈值」和「相似度门槛」互为补数:
飞牛默认差异阈值 ⇒ 相似度门槛 ;差异阈值调到 ⇒ 相似度门槛 (更严格)。飞牛官方说明「调低差异阈值,相似度要求会提高」—— 按这个公式看就完全自洽了:差异阈值越低,门槛越高,只有长得更像的才会被并进同一个人物相册。
6.5 聚类:自动生成人物相册(DBSCAN)
相册里可能有几千张脸,不可能两两人工对比。系统把这步交给无监督聚类:把所有 512 维向量丢进聚类算法,让「距离近」的脸自动归成一组,一组就是一个人物相册。
Immich 官方说明其聚类算法是 DBSCAN 的变体。DBSCAN 是纯密度聚类,不需要预知有多少个人,数学定义很干净:
逐条拆开:
-
是 的 邻域:以 为圆心、 为半径内的所有点;
-
核心点:邻域内至少有 个点;
-
密度直达: 且 是核心点;
-
密度可达:存在一条核心点链 ;
-
噪声:不属于任何簇的点(孤立点)。
直观理解:把每个人脸当成空间里的一个点,凡是「周围挤得够密」的区域各自成一簇;稀稀拉拉的点直接判为噪声—— 正好对应「只出现过一次的路人甲不生成人物相册」。
对照飞牛的设置就更好懂了:「生成人物的最少照片数(默认 2)」≈ —— 一个人至少出现几次才配拥有人物相册;「相似度差异阈值(默认 0.36)」≈ —— 脸像到什么程度算同一人(半径内算邻居)。
聚类只是初筛,最后一步永远是人类兜底:命名、合并(同一个人被拆开)、移除(混入陌生人)。飞牛和 Immich 都提供这些手动修正操作。
7 三家实现横向拆解
| 维度 | Immich | MT Photos | 飞牛相册 |
|---|---|---|---|
| 语义搜图 | CLIP(默认 ViT-B-32,可换) | CLIP(多语言模型,中文友好) | 智能识别模型(基础 / 增强) |
| 人脸识别 | insightface buffalo_l | insightface buffalo_l | 人脸识别 - 标准模型 |
| OCR | 支持(本地 OCR) | PP-OCRv5_mobile | 未单列 |
| 向量存储 | Postgres + pgvector → VectorChord | 服务端内置索引 | 本地存储 |
| 架构形态 | server + 独立 machine-learning 容器 | 服务端进程 / 可接外部 API | 相册应用内置模块 |
| 任务调度 | 队列 + 后台任务 | 后台任务列表(人脸特征、CLIP、场景识别) | 安装模型后自动执行,可手动补扫 |
| 开源 | ✅ 开源 | ❌ 商业(免费) | ❌ 闭源(随 fnOS 提供) |
几个值得注意的差异:
-
MT Photos 把流程拆成了可见的后台任务:人脸特征识别 → 人脸特征对比(生成人物相册)→ CLIP 识别 → 照片场景识别。场景识别还基于 CLIP 识别结果二次加工,比如识别出「室内 / 户外」这类粗分类;
-
飞牛把「增强识别」做成了选项:视频默认只做基础识别,开启「视频增强识别」后对视频内容加深理解,搜索能捞到更多视频项,代价是计算资源占用上升;
-
Immich 的开源生态最灵活:模型可换、向量库可换、还能用外部 ML API 替代本地推理。
8 部署与性能:模型跑在哪
三个产品的模型都跑在本地,技术栈高度一致:ONNX Runtime + 预训练模型文件(insightface、CLIP 都是 ONNX 格式),好处是 CPU 可跑、有 GPU 时自动加速。
硬件门槛以飞牛官方给的最低要求为准(三家量级接近):
| 项目 | 要求 |
|---|---|
| 内存 | 最低 2 GB;智能识别 - 增强模型需 8 GB |
| 模型占用空间 | 预留 20 GB 以上 |
| GPU | 人脸识别支持 Intel GPU 与 NVIDIA(GeForce 700 以上);智能识别同样支持 |
| CPU 兜底 | 无 GPU 也能跑,只是慢 |
性能上还有三条通用优化手段:
-
送缩略图,不送原图。推理输入的是预览图(几百像素),不是原尺寸照片,推理速度与内存占用差一个数量级;
-
视频抽帧。视频先抽关键帧 / 间隔帧,只对帧做人脸与内容识别,识别结果回挂到整个视频;
-
任务队列分批执行。入库高峰时排队跑,避免推理服务把 NAS 的 CPU 打满。
9 局限与优化方向
原理清楚了,也该说清楚它的边界:
-
CLIP 是「语义近似」不是「精确理解」。「红色车子左边的那个人」这类空间 / 数量关系,CLIP 表现很弱;它擅长的是「整体像不像」,不是「逻辑对不对」;
-
聚类永远是启发式的。同一个人被拆成多个相册、路人被聚成「人物」,是常态,必须有手动合并 / 移除兜底;
-
阈值是玄学。检测阈值、相似度阈值、最少照片数,每个都影响结果,改完还得重置重跑;
-
视频识别计算量大。全量视频增强识别,小内存 NAS 会明显卡顿;
-
中文语义模型的差距。用英文 CLIP 搜中文描述会明显变差,选模型时优先多语言版本(如 MT Photos 默认款)。
可以预见的方向:CLIP 往 SigLIP 这类更高效的双塔演进、向量库往更省磁盘的方向走(VectorChord 已经是这个方向)、人脸模型针对亚洲面孔持续微调(MT Photos 选 insightface 的理由之一就是「对亚洲人脸准确率更高」)。
10 总结
-
一句话搜图 = CLIP 双塔模型把图和句子映射到同一向量空间 + 向量检索按余弦相似度取 top-k;
-
人物相册 = 人脸检测(找脸)→ 相似变换对齐(摆正)→ ArcFace 提取 512 维特征 → DBSCAN 密度聚类自动分组;
-
对比两张图是不是同一个人 = 两个 512 维向量算余弦相似度,超阈值即同一人;
-
数学内核不是傅里叶:向量内积(余弦相似度)、对比学习(InfoNCE)、仿射变换(对齐)、密度聚类(DBSCAN)才是主角;DCT 只负责 JPEG 压缩;
-
全程本地推理是自部署相册与云相册的分水岭,照片不出 NAS,换来的是隐私,付出的是算力。
参考
-
Immich 官方文档:Facial Recognition / Searching / FAQ —— https://docs.immich.app
-
MT Photos 文档:后台任务 / 使用其他 CLIP 模型 / 添加人脸识别 API —— https://mtmt.tech/docs
-
飞牛帮助中心:AI 相册功能说明 —— https://help.fnnas.com/zh-CN/articles/v1/photo/photo-ai
-
OpenAI:CLIP: Connecting text and images —— https://openai.com/index/clip
-
InsightFace:人脸检测与识别模型库 —— https://github.com/deepinsight/insightface
-
Deng et al.:ArcFace: Additive Angular Margin Loss for Deep Face Recognition(CVPR 2019)
-
Ester et al.:A Density-Based Algorithm for Discovering Clusters(KDD 1996,DBSCAN 原始论文)
Created with ❤️ by 张萌萌
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
最后更新于 ,距今已过 4 天
部分内容可能已过时
评论