Retinaface+CurricularFace实战教程:批量图片人脸比对脚本二次开发指南
你是不是也遇到过这样的需求:手头有几百张员工照片,需要快速找出哪些人和入职登记照最相似?或者在安防系统里,要从监控截图中批量匹配已知人员?又或者想给自己的相册自动打上“家人”“朋友”“同事”的标签?这些场景背后,其实都指向同一个技术动作——人脸比对。
但市面上大多数教程只教你怎么比对两张图,真要处理几十上百张图片时,就卡在了“怎么批量跑”这一步。今天这篇实战指南,不讲原理、不堆参数,直接带你把官方单图比对脚本改造成真正能干活的批量处理工具。整个过程不需要重写模型,不用调参,只要懂一点Python基础,就能让脚本自己读图、配对、打分、输出结果——而且每一步都有可运行代码、真实报错提示和避坑建议。
我们用的底座是CSDN星图镜像广场上预装好的Retinaface+CurricularFace 人脸识别模型镜像。它已经帮你把环境、依赖、模型权重全配好了,连CUDA版本都对齐了,你唯一要做的,就是把“一次比两张”变成“一次比一百对”。
1. 镜像环境与能力再确认:别急着写代码,先看清手里的工具
在动手改脚本前,得先搞清楚这个镜像到底“能做什么”“不能做什么”。很多人改着改着发现结果不准,最后发现不是代码问题,而是对模型能力边界理解有偏差。
这个镜像不是万能的人脸识别黑盒,它由两个明确分工的模块组成:
- RetinaFace:负责“找脸”。它能在一张图里精准定位多张人脸,并自动选出面积最大、最完整的一张作为后续处理对象。注意,它不抠图、不裁剪,只是框出位置并做仿射对齐。
- CurricularFace:负责“认人”。它把对齐后的人脸转成512维特征向量,再用余弦相似度计算两张脸的匹配程度。得分越接近1,说明越可能是同一个人。
两者组合起来,就实现了“输入任意尺寸原图→自动检测最大人脸→提取特征→比对打分”的端到端流程。这也是为什么你不用提前用PS抠图,也不用担心图片旋转或缩放——模型自己会处理。
镜像里所有东西都放在/root/Retinaface_CurricularFace目录下,环境已预装好,无需额外配置。你可以用下面这条命令快速验证是否一切正常:
cd /root/Retinaface_CurricularFace && conda activate torch25 && python inference_face.py如果终端输出类似Similarity score: 0.872 → Same person,并且没有报ModuleNotFoundError或CUDA out of memory,说明环境完全就绪,可以进入下一步。
关键提醒:这个镜像默认只支持单张图对单张图比对,且每次只取每张图中的最大人脸。如果你的图片里有多张清晰正面脸(比如合影),它只会比对其中最大的那张——这点在批量处理前必须心里有数。
2. 从单次比对到批量处理:三步改造核心脚本
官方脚本inference_face.py的逻辑非常干净:读两张图 → 检测人脸 → 提取特征 → 计算相似度 → 打印结果。我们要做的,就是把它“复制粘贴”式地扩展成能循环处理文件夹的能力。
2.1 第一步:理解原始脚本结构,找到可复用的核心函数
打开/root/Retinaface_CurricularFace/inference_face.py,你会发现它主要由三部分构成:
get_face_embedding(image_path):输入图片路径,返回512维特征向量calculate_similarity(embed1, embed2):输入两个向量,返回余弦相似度main():解析命令行参数、调用上面两个函数、输出结果
真正需要我们动的,只有main()函数。其他两个函数已经封装好,稳定可靠,不要重写,不要魔改,直接复用。
2.2 第二步:新增批量比对逻辑——支持三种常用模式
我们在原脚本末尾新增一个batch_compare()函数,支持以下三种实用场景:
- 模式A:单图 vs 文件夹内所有图(例如:用一张标准照,去比对整个员工库)
- 模式B:两个文件夹逐一对比(例如:昨天的考勤截图 vs 今天的,看谁没来)
- 模式C:按CSV列表配对(例如:Excel里列了100组“待验人-标准照”路径,一行一组)
下面这段代码可以直接复制进inference_face.py文件底部(放在if __name__ == "__main__":之前):
import os import glob import csv from pathlib import Path def batch_compare(mode="folder_vs_folder", input1=None, input2=None, threshold=0.4, output_csv="batch_result.csv"): """ 批量人脸比对主函数 mode: "single_vs_folder", "folder_vs_folder", "csv_pairs" input1: 单图路径 / 文件夹路径 / CSV路径 input2: 文件夹路径(仅mode=="single_vs_folder"时使用) """ results = [] if mode == "single_vs_folder": # 单图 vs 文件夹:input1是单图,input2是文件夹 assert os.path.isfile(input1), f"input1 must be a file: {input1}" assert os.path.isdir(input2), f"input2 must be a folder: {input2}" ref_emb = get_face_embedding(input1) img_list = sorted(glob.glob(os.path.join(input2, "*.jpg")) + glob.glob(os.path.join(input2, "*.png"))) print(f"[Batch] Comparing {os.path.basename(input1)} against {len(img_list)} images...") for i, img_path in enumerate(img_list): try: emb = get_face_embedding(img_path) score = calculate_similarity(ref_emb, emb) is_same = "Same person" if score >= threshold else "Different person" results.append([os.path.basename(input1), os.path.basename(img_path), f"{score:.3f}", is_same]) if i % 20 == 0: print(f" Processed {i}/{len(img_list)}...") except Exception as e: results.append([os.path.basename(input1), os.path.basename(img_path), "ERROR", str(e)]) elif mode == "folder_vs_folder": # 两个文件夹:按文件名顺序一一配对(要求文件名一致,如 a.jpg vs a.jpg) assert os.path.isdir(input1) and os.path.isdir(input2) list1 = sorted(glob.glob(os.path.join(input1, "*.jpg")) + glob.glob(os.path.join(input1, "*.png"))) list2 = sorted(glob.glob(os.path.join(input2, "*.jpg")) + glob.glob(os.path.join(input2, "*.png"))) assert len(list1) == len(list2), f"Folder sizes differ: {len(list1)} vs {len(list2)}" print(f"[Batch] Comparing {len(list1)} image pairs...") for p1, p2 in zip(list1, list2): try: emb1 = get_face_embedding(p1) emb2 = get_face_embedding(p2) score = calculate_similarity(emb1, emb2) is_same = "Same person" if score >= threshold else "Different person" results.append([os.path.basename(p1), os.path.basename(p2), f"{score:.3f}", is_same]) except Exception as e: results.append([os.path.basename(p1), os.path.basename(p2), "ERROR", str(e)]) elif mode == "csv_pairs": # CSV格式:每行两列,第一列是图1路径,第二列是图2路径 with open(input1, 'r', encoding='utf-8') as f: reader = csv.reader(f) for row in reader: if len(row) < 2: continue p1, p2 = row[0].strip(), row[1].strip() if not (os.path.isfile(p1) and os.path.isfile(p2)): results.append([p1, p2, "MISSING", "File not found"]) continue try: emb1 = get_face_embedding(p1) emb2 = get_face_embedding(p2) score = calculate_similarity(emb1, emb2) is_same = "Same person" if score >= threshold else "Different person" results.append([os.path.basename(p1), os.path.basename(p2), f"{score:.3f}", is_same]) except Exception as e: results.append([os.path.basename(p1), os.path.basename(p2), "ERROR", str(e)]) # 写入CSV结果 with open(output_csv, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(["Image1", "Image2", "Score", "Result"]) writer.writerows(results) print(f"[Done] Results saved to {output_csv} ({len(results)} rows)") return results这段代码做了几件关键的事:
- 自动识别图片路径(支持
.jpg和.png) - 对每张图加了异常捕获,避免一张图出错导致整个批次中断
- 每处理20张图就打印进度,防止你盯着屏幕干等
- 结果统一导出为CSV,方便Excel打开筛选
2.3 第三步:添加命令行入口,让批量功能像原脚本一样好用
继续在文件末尾追加以下代码,让它能通过命令行直接调用:
if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="RetinaFace+CurricularFace Batch Face Comparison") parser.add_argument("--mode", type=str, default="folder_vs_folder", choices=["single_vs_folder", "folder_vs_folder", "csv_pairs"], help="Batch mode: single_vs_folder, folder_vs_folder, or csv_pairs") parser.add_argument("--input1", type=str, required=True, help="Path to image/file/folder/CSV") parser.add_argument("--input2", type=str, help="Second path (required for single_vs_folder and folder_vs_folder)") parser.add_argument("--threshold", type=float, default=0.4, help="Similarity threshold (default: 0.4)") parser.add_argument("--output", type=str, default="batch_result.csv", help="Output CSV filename") args = parser.parse_args() if args.mode == "single_vs_folder" and not args.input2: raise ValueError("--input2 is required for single_vs_folder mode") if args.mode == "folder_vs_folder" and not args.input2: raise ValueError("--input2 is required for folder_vs_folder mode") if args.mode == "csv_pairs" and args.input2: print("Warning: --input2 ignored in csv_pairs mode") batch_compare( mode=args.mode, input1=args.input1, input2=args.input2, threshold=args.threshold, output_csv=args.output )保存文件后,你就可以用下面这些命令直接跑批量任务了:
# 模式A:用一张标准照,比对整个员工照片文件夹 python inference_face.py --mode single_vs_folder --input1 ./standard/employee_a.jpg --input2 ./employees/ # 模式B:两个考勤截图文件夹,按文件名一一比对(a_001.jpg vs b_001.jpg) python inference_face.py --mode folder_vs_folder --input1 ./day1/ --input2 ./day2/ # 模式C:按CSV里指定的100组路径配对(每行:/path/a.jpg,/path/b.jpg) python inference_face.py --mode csv_pairs --input1 ./pairs.csv3. 实战调试与常见问题:别让小错误毁掉一整天
即使代码写对了,实际运行时也常遇到几个“意料之中”的坑。我把它们整理成对照表,帮你省下至少两小时排查时间。
| 问题现象 | 根本原因 | 快速解决方法 |
|---|---|---|
RuntimeError: CUDA out of memory | 一次性加载太多图片,显存爆了 | 在batch_compare()函数里,把glob.glob(...)改成list(glob.glob(...))[:50]先试前50张;或在循环里加torch.cuda.empty_cache() |
ValueError: No face detected | 图片里没人脸,或人脸太小/太暗/侧脸严重 | 先用原脚本python inference_face.py --input1 xxx.jpg单独测试这张图;确认可用后再加入批量任务 |
OSError: [Errno 2] No such file or directory | 路径含中文、空格或符号,Linux下容易出错 | 把所有图片移到纯英文路径下(如/root/images/),或用os.path.abspath()处理路径 |
ImportError: cannot import name 'xxx' | 新增代码引用了未导入的模块 | 在文件开头补上import glob,import csv,from pathlib import Path |
CSV结果里全是ERROR | 某张图损坏或格式不支持 | 在except块里加一句print(f"Failed on {img_path}: {e}"),立刻定位哪张图有问题 |
还有一个隐藏但高频的问题:阈值设太高,导致大量“不同人”误判。官方默认0.4是在LFW数据集上平衡准确率和召回率的结果,但你的业务场景可能完全不同。比如:
- 身份核验(银行开户):建议调到
0.65以上,宁可拒真,不可认假 - 家庭相册归类:
0.35就够用,亲人之间特征相似度天然更高 - 考勤打卡(固定摄像头):
0.5~0.55最稳妥,兼顾光照变化和姿态差异
你可以用下面这个小脚本,快速画出当前数据集的分数分布,辅助选阈值:
# save as threshold_test.py import matplotlib.pyplot as plt from inference_face import get_face_embedding, calculate_similarity import glob # 取10张同一个人的不同照片 imgs = glob.glob("./same_person/*.jpg")[:10] embs = [get_face_embedding(p) for p in imgs] scores = [] for i in range(len(embs)): for j in range(i+1, len(embs)): scores.append(calculate_similarity(embs[i], embs[j])) plt.hist(scores, bins=20, alpha=0.7, label="Same person") plt.xlabel("Similarity Score") plt.ylabel("Count") plt.title("Score Distribution (Same Person)") plt.legend() plt.grid(True) plt.show()运行后你会看到一个集中在0.7~0.9区间的峰——这就是你业务场景下的合理阈值区间。
4. 进阶技巧:让批量脚本更聪明、更省心
上面的脚本已经能干活了,但如果想让它真正融入你的工作流,还需要加点“小心机”。
4.1 自动过滤低质量图:跳过模糊、过暗、过曝的图片
在get_face_embedding()调用前,加一段轻量级质量检查:
import cv2 import numpy as np def is_image_quality_ok(image_path, min_blur=100, min_brightness=30, max_brightness=220): """简单质量过滤:模糊度 + 亮度范围""" img = cv2.imread(image_path) if img is None: return False # 模糊度(拉普拉斯方差) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur_score = cv2.Laplacian(gray, cv2.CV_64F).var() # 亮度(灰度均值) brightness = np.mean(gray) return blur_score > min_blur and min_brightness < brightness < max_brightness # 在 batch_compare() 循环里调用: if not is_image_quality_ok(img_path): results.append([..., "SKIPPED", "Low quality"]) continue这样,模糊的监控截图、手机随手拍的过暗照片,会直接被标记为SKIPPED,不参与比对,避免拖慢速度还拉低准确率。
4.2 输出带高亮的HTML报告:一眼看出哪些对得准、哪些要复查
把CSV结果转成带颜色的HTML页面,比对着Excel划线高效十倍:
def save_html_report(results, html_file="report.html"): html = """<html><head><style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } .good { background-color: #d4edda; } .warn { background-color: #fff3cd; } .error { background-color: #f8d7da; } </style></head><body><h2>Face Comparison Report</h2> <table><tr><th>Image1</th><th>Image2</th><th>Score</th><th>Result</th></tr>""" for r in results: cls = "good" if r[2].startswith("0.") and float(r[2]) >= 0.6 else \ "warn" if r[2].startswith("0.") and 0.4 <= float(r[2]) < 0.6 else \ "error" html += f'<tr class="{cls}"><td>{r[0]}</td><td>{r[1]}</td><td>{r[2]}</td><td>{r[3]}</td></tr>' html += "</table></body></html>" with open(html_file, "w", encoding="utf-8") as f: f.write(html) print(f"HTML report saved to {html_file}") # 在 batch_compare() 结尾调用: save_html_report(results)生成的网页里,绿色行是高置信度匹配(≥0.6),黄色是需人工确认(0.4~0.6),红色是明显不匹配或报错——打开浏览器就能快速决策。
5. 总结:你现在已经拥有了一个可落地的人脸比对流水线
回看一下,我们到底完成了什么:
- 把官方单图比对脚本,扩展成支持三种批量模式的生产级工具
- 每种模式都有完整命令行接口,无需改代码就能切换场景
- 内置异常处理、进度提示、结果导出,拒绝“跑着跑着就没了”
- 提供阈值调优方法、质量过滤、HTML可视化等进阶能力
- 所有代码都在原镜像环境下直接运行,零环境配置成本
更重要的是,这个脚本不是一次性的Demo。它的结构清晰、职责分明,后续你可以轻松加上:
- 自动上传结果到数据库(加几行
sqlite3代码) - 邮件通知匹配成功(加
smtplib) - Web界面(用
gradio包裹一下batch_compare函数)
人脸识别从来不是目的,解决具体问题才是。当你不再纠结“模型能不能识别人脸”,而是专注“怎么让识别结果真正用起来”,技术才算真正落地。
现在,就打开终端,cd 到/root/Retinaface_CurricularFace,激活环境,跑起你的第一批批量任务吧。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。