face-photo-search
从上万张照片里,把某一个人的所有照片挑出来。
给一个人的几张参考照片,它会扫描你的整个图库(手机备份、相机导出、NAS 相册), 用人脸识别找出这个人出现过的每一张,复制到新文件夹并附一份带相似度分数的报告。
原图一张不动 —— 只读扫描 + 复制,不移动、不重命名、不删除。
这是一个 WorkBuddy / Claude Code 格式的 Agent Skill, 装好之后直接对 AI 说「帮我把我妈的照片从 D:\相册 里挑出来,参考照片在 D:\参考」即可, 不需要记命令行参数。
实测
在一个 6321 张、16.2 GB 的手机相册备份上跑通:
| 指标 | 结果 | |---|---| | 图库规模 | 6321 张 / 16.2 GB | | 命中 | 268 张(相似度 ≥ 0.35,多数落在 0.75–0.86) | | 疑似 | 27 张(0.25–0.35,不复制,列进报告供人工核对) | | 无人脸自动跳过 | 4736 张 | | 读取失败 | 0 | | 稳态速度 | 约 0.6 秒/张(24 核 CPU,无需 GPU) |
过程中进程被中断三次,靠断点续跑无损恢复,结果一条没丢。
原理
InsightFace 的 buffalo_l 模型(ArcFace)把每张脸编码成 512 维向量,
参考照片取平均得到一个"这个人长什么样"的基准向量,
再和图库里每张脸算余弦相似度,超过阈值即判定命中。
参考照片文件夹 ──┐
待搜索图库 ──┼──► ArcFace 编码 ──► 余弦相似度 ──► 命中副本 + 评分报告
阈值 / 输出位置 ─┘
安装
git clone https://github.com/liyuechao2018/face-photo-search.git \
~/.workbuddy/skills/face-photo-search
Claude Code 用户放到 ~/.claude/skills/ 下同理。
依赖装在隔离 venv 里,安装顺序有讲究,照抄 SKILL.md 的「前置:环境搭建」一节。 简要说:
VENV="$HOME/.workbuddy/binaries/python/envs/default"
[ -d "$VENV" ] || python -m venv "$VENV"
PYBIN="$VENV/Scripts/python.exe"; [ -x "$PYBIN" ] || PYBIN="$VENV/bin/python"
M="https://pypi.tuna.tsinghua.edu.cn/simple"; H="pypi.tuna.tsinghua.edu.cn"
"$PYBIN" -m pip install -i $M --trusted-host $H numpy onnxruntime opencv-python-headless pillow tqdm
"$PYBIN" -m pip install -i $M --trusted-host $H --no-deps insightface
"$PYBIN" -m pip install -i $M --trusted-host $H onnx requests prettytable scikit-image scipy
首次运行会自动下载 buffalo_l 模型(约 270 MB)。
用法
对 AI 说人话就行。想手动跑也可以:
S="$HOME/.workbuddy/skills/face-photo-search/scripts"
W="./facework-某人" # 每个人用一个独立工作目录
# 1. 建参考向量,并对图库抽样冒烟测试(先看区分度,别急着跑全量)
"$PYBIN" "$S/build_ref.py" --ref "<参考照片文件夹>" --lib "<图库>" --work "$W"
# 2. 全量扫描 + 复制 + 出报告(可随时中断,重跑自动续)
"$PYBIN" "$S/find_person.py" --ref "<参考照片文件夹>" --lib "<图库>" \
--out "<结果文件夹>" --work "$W" --thr 0.35 --workers 8
# 3. 觉得阈值不合适?不用重扫,分数已缓存
"$PYBIN" "$S/find_person.py" ... --work "$W" --thr 0.45 --rebuild-only
阈值怎么定
| 阈值 | 效果 | |---|---| | 0.30 | 宁滥勿缺,适合找侧脸/远景/儿童跨年龄 | | 0.35 | 默认,实测漏检少、误判低 | | 0.45 | 严格,混入别人时调到这里 |
小孩照片跨年龄变化大,不建议超过 0.5。
设计上的几个决定
边扫边落盘。 每处理完一张就把分数追加写入 scores.tsv,而不是攒在内存里最后一次性写。
第一版就是攒内存的,跑到 1639 张时进程被回收,前面的判定结果全丢。
上万张的任务必然会被中断,crash-safe 不是优化项而是必需项。
分数全量缓存,输出可重建。 scores.tsv 存的是每张照片的原始相似度,不是"命中与否"。
所以改阈值只需重建输出(秒级),不用重扫几小时。
工作目录按「人 × 图库」隔离。 refvec.npy 和 scores.tsv 都绑定特定的人和图库。
换人却复用同一个 --work,会读到上一个人的向量、且缓存显示"全部已扫描",
于是静默产出上一个人的结果——不报错的错才最危险。
脚本用 owner.json 记录归属,参数对不上直接终止。
用 Pillow 而不是 cv2 读图。 cv2.imread 读不了华为手机的 MPO 双摄格式(返回 None),
部分普通 JPEG 在 headless 构建下也读不出。Pillow 全都能读,转 BGR 数组再喂给模型。
多进程,但工人数别贪多。 8 工人 × 3 线程和 12 工人 × 2 线程实测几乎无差异, 瓶颈在磁盘 IO 不在 CPU。另外小样本测速会骗人——40 张测出 7.2 秒/张, 其实大头是 8 个工人各自加载模型的启动开销,稳态是 0.6 秒/张。
隐私
人脸特征属于生物识别信息。这个工具完全离线运行,模型在本地推理,
参考向量 .npy 和分数 .tsv 只落在你指定的工作目录里,不上传任何地方。
请只用于处理你自己的照片。
License
MIT
Scan to join WeChat group