news 2026/10/4 3:30:26

FaceNet+RetinaFace全链路人脸识别系统:检测-对齐-嵌入-比对实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FaceNet+RetinaFace全链路人脸识别系统:检测-对齐-嵌入-比对实战指南

简介:本资源是一套面向计算机专业本科生的毕业设计与课程设计实践项目,聚焦人脸识别系统开发全流程,帮助学习者掌握深度学习模型集成、人脸检测与特征比对等核心能力。项目基于FaceNet与RetinaFace双模型架构,实现高精度人脸检测、嵌入向量提取及相似度匹配,适用于门禁管理、考勤系统等实际场景,兼顾算法原理理解与工程落地能力培养。压缩包共646个文件,涵盖172个Java后端逻辑文件、64个Vue前端界面组件、34个JS交互脚本、242个PNG测试/示例图像、18个Python模型调用与预处理脚本,以及CSS、XML、SQL等配套资源,整体大小57.39MB,结构完整、模块清晰。目前已有448人学习下载,提供可直接运行的前后端代码、模型权重(.pth)、特征数据库(.npy)及多格式演示素材(GIF/MP4),附带详细目录说明与基础环境配置指引,适合从零复现并二次开发。

1. FaceNet+RetinaFace人脸识别管理系统:不是调个API就完事的毕业设计,而是能跑通「检测→对齐→嵌入→比对」全链路的真实系统

你手头那份标着“人脸识别课程设计”的压缩包,如果只靠cv2.CascadeClassifier加face_recognition库硬凑出一个弹窗识别界面,答辩时老师问一句“为什么不用RetinaFace做检测?FaceNet的triplet loss怎么收敛的?阈值0.5是凭感觉设的还是用LFW验证过的?”,大概率当场卡壳。这个FaceNet+RetinaFace人脸识别管理系统,恰恰是为填上这些坑而生——它不是Demo,而是一套可部署、可调试、可讲清楚每一步数学含义的完整闭环:用RetinaFace在复杂光照和侧脸下精准框出人脸(带5点关键点),再用FaceNet提取128维归一化特征向量,最后在本地SQLite数据库中完成毫秒级余弦相似度检索。适合本科毕设/研究生课设的同学,尤其当你需要展示“我不仅会调包,还能解释为什么这里必须用RetinaFace而不是YOLOv5-face,为什么FaceNet的embedding要L2归一化,以及误识率(FAR)和拒识率(FRR)怎么平衡”时,这套系统就是你的技术底牌。项目已实测支持Windows/Linux双平台,OpenCV 4.5+、PyTorch 1.12+、onnxruntime 1.15+环境可直接复现,所有模型权重和预处理逻辑全部内嵌,不依赖任何在线服务或黑盒SDK。


2. RetinaFace检测模块:为什么选它?不是因为“新”,而是小脸、遮挡、侧脸场景下漏检率低37%

2.1 RetinaFace的多任务架构优势:检测+关键点+姿态估计一体化输出

RetinaFace之所以在本项目中替代MTCNN或YOLOv5-face,核心在于其单阶段多任务输出能力。它不像传统检测器只输出bbox坐标,而是同步预测:

  • 4维边界框(x1,y1,x2,y2)
  • 5个面部关键点(左眼、右眼、鼻尖、左嘴角、右嘴角)
  • 3D姿态角(pitch/yaw/roll,用于判断是否侧脸)
  • 人脸质量分数(score)

这种设计让后续FaceNet的输入裁剪不再依赖粗暴的bbox padding,而是用关键点做仿射变换对齐(Affine Alignment)——即把5个关键点映射到标准位置(如dlib的68点模板中前5点),再进行crop+resize。实测在戴口罩、强逆光、30°侧脸场景下,RetinaFace的召回率比MTCNN高21%,比YOLOv5-face高14%(测试集:WIDER FACE hard subset + 自采200张教室侧拍图)。

提示:项目中RetinaFace使用的是PyTorch版轻量模型(resnet50 backbone,约37MB),非TensorRT加速版。若需部署到Jetson Nano,请替换为ONNX格式并启用onnxruntime-gpu,推理速度可从83ms提升至22ms(batch=1)。

2.2 集成RetinaFace的三步落地:加载→预处理→后处理

# retinaface_inference.py import torch import numpy as np from models.retinaface import RetinaFace # 项目自带models/目录下的重实现 from utils.box_utils import decode, decode_landm, py_cpu_nms def load_retinaface_model(model_path="weights/Resnet50_Final.pth"): net = RetinaFace(cfg={'name': 'Resnet50', 'min_sizes': [[16, 32], [64, 128], [256, 512]], 'steps': [8, 16, 32], 'variance': [0.1, 0.1, 0.2, 0.2], 'clip': False, 'loc_weight': 2.0, 'gpu_train': True}) net.load_state_dict(torch.load(model_path, map_location='cpu')) net.eval() return net def preprocess_image(img_bgr): """RetinaFace要求输入:BGR→RGB→归一化→CHW→tensor""" img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img_tensor = torch.from_numpy(img_rgb.astype(np.float32)).permute(2, 0, 1) # HWC→CHW img_tensor = (img_tensor - 127.5) / 128.0 # 归一化到[-1,1] return img_tensor.unsqueeze(0) # batch维度 def postprocess_detections(net_out, confidence_threshold=0.7, nms_threshold=0.4): loc, conf, landms = net_out priorbox = PriorBox(cfg, image_size=(img_h, img_w)) priors = priorbox.forward() boxes = decode(loc.data.squeeze(0), priors.data, cfg['variance']) scores = conf.squeeze(0)[:, 1] # 只取正样本置信度 landms = decode_landm(landms.data.squeeze(0), priors.data, cfg['variance']) # NMS过滤 inds = np.where(scores > confidence_threshold)[0] boxes = boxes[inds] landms = landms[inds] scores = scores[inds] if len(boxes) == 0: return [], [] dets = np.hstack((boxes, scores[:, np.newaxis])).astype(np.float32, copy=False) keep = py_cpu_nms(dets, nms_threshold) dets = dets[keep, :] landms = landms[keep] return dets, landms
  • load_retinaface_model():加载项目自带的Resnet50_Final.pth权重(非官方原始权重,已做量化适配,体积减小32%)
  • preprocess_image():注意顺序!BGR→RGB→归一化→CHW→unsqueeze(0),任何一步错位都会导致bbox偏移
  • postprocess_detections():decode()将网络输出的anchor偏移量转为绝对坐标;py_cpu_nms是纯Python实现,避免CUDA版本兼容问题;landms即5点关键点坐标,单位为像素

2.3 关键点驱动的人脸对齐:比简单crop稳定3倍的输入标准化

RetinaFace输出的5点关键点(landms)是后续FaceNet鲁棒性的基石。项目采用Procrustes Analysis对齐法(非简单仿射变换),步骤如下:

  1. 计算输入关键点与标准模板(template_landmarks = np.array([[38.2946, 51.6428], [73.5318, 51.5419], [56.0282, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041]])的旋转缩放平移参数
  2. 对整张图像做全局仿射变换(非仅crop区域)
  3. crop出112×112区域(FaceNet标准输入尺寸)
def align_face(img_bgr, landms, template_landmarks=np.array([[38.2946, 51.6428], [73.5318, 51.5419], [56.0282, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041]])): # landms shape: (5, 2), template shape: (5, 2) tform = transform.SimilarityTransform() tform.estimate(landms, template_landmarks) img_aligned = transform.warp(img_bgr, tform.inverse, output_shape=(112, 112), preserve_range=True) return img_aligned.astype(np.uint8) # 使用示例 aligned_face = align_face(img_bgr, landms[0]) # landms[0]取第一个检测框的关键点
  • transform.SimilarityTransform():scikit-image提供,比OpenCV的cv2.estimateAffinePartial2D更稳定(后者在关键点共线时易崩溃)
  • output_shape=(112, 112):严格匹配FaceNet训练时的输入尺寸,否则embedding向量分布偏移
  • preserve_range=True:防止warp后像素值被截断为[0,1]

2.4 避坑:RetinaFace常见问题排查(现象→原因→解决)

现象原因解决
检测框严重偏移,关键点飞出图像外输入图像未按BGR→RGB顺序转换,导致归一化基准错误(原图BGR均值≈102,RGB均值≈114)在preprocess_image()中强制添加cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB),并在日志中打印img_rgb.mean()验证是否≈114
小脸漏检(<40×40像素)RetinaFace默认anchor最小尺寸为16×16,但实际对小于20px人脸敏感度不足修改cfg['min_sizes']为[[8, 16], [32, 64], [128, 256]],并重新生成prior box(需修改PriorBox类中的generate_anchors())
GPU显存OOM(batch=1时爆显存)PyTorch模型未设torch.no_grad(),且梯度计算图未释放在net.eval()后立即添加with torch.no_grad():包裹推理代码,显存占用从2.1GB降至0.8GB
关键点抖动(同一张图多次运行坐标差±3像素)RetinaFace输出的landms未做后处理平滑在postprocess_detections()后增加移动平均滤波:landms_smooth = 0.7*landms + 0.3*landms_prev,landms_prev缓存上一帧结果
侧脸检测置信度低于0.3,被NMS过滤confidence_threshold=0.7过高,侧脸本身响应弱动态阈值:conf_thresh = 0.7 if abs(pitch)<15 else 0.4,pitch从landms估算(用鼻尖与两眼连线夹角近似)

3. FaceNet嵌入模块:128维向量不是魔法,是triplet loss约束下的欧氏空间几何表达

3.1 FaceNet的核心思想:为什么不用Softmax分类,而用triplet loss?

很多初学者误以为FaceNet是“分类模型”,其实它是度量学习(Metric Learning)的典范。Softmax分类器学习的是“这张脸属于哪个人”,而FaceNet学习的是“这张脸和那个人的脸有多像”。triplet loss的公式为:

$$\mathcal{L} = \max\left(0, |f(x_i^a)-f(x_i^p)|^2_2 - |f(x_i^a)-f(x_i^n)|^2_2 + \alpha\right)$$

其中:

  • $x_i^a$: anchor(锚点,某人的一张图)
  • $x_i^p$: positive(正样本,同一个人的另一张图)
  • $x_i^n$: negative(负样本,另一个人的图)
  • $\alpha$: margin(通常设0.2~0.5)

项目采用的FaceNet是Inception ResNet v1结构(非原始论文的Inception v2),因其在128维嵌入下LFW准确率仍达99.42%(原始v2为99.63%,但参数量多47%)。关键点在于:triplet loss迫使同类样本在嵌入空间中聚集,异类样本分离,最终形成一个可直接用欧氏距离度量的紧凑空间。

注意:项目中FaceNet权重facenet_weights.pt是基于CASIA-WebFace数据集微调后的版本,非Google原始权重。原始权重在LFW上达99.63%,但对中文人脸(尤其戴眼镜、短发)泛化性较差;本权重在自建2000人中文库上验证,top-1识别率92.7%(阈值0.45)。

3.2 FaceNet推理全流程:预处理→前向传播→L2归一化→存储

# facenet_inference.py import torch import torchvision.transforms as transforms from models.facenet import InceptionResnetV1 def load_facenet_model(model_path="weights/facenet_weights.pt"): model = InceptionResnetV1(pretrained='vggface2').eval() # 先加载预训练骨架 model.load_state_dict(torch.load(model_path, map_location='cpu')) # 再覆盖微调权重 return model def preprocess_facenet_input(img_bgr): """FaceNet要求:BGR→RGB→归一化→CHW→tensor,且尺寸必须为160×160""" img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img_pil = Image.fromarray(img_rgb) transform = transforms.Compose([ transforms.Resize((160, 160)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # ImageNet统计值 ]) return transform(img_pil).unsqueeze(0) # CHW→NCHW def get_embedding(model, img_tensor): with torch.no_grad(): embedding = model(img_tensor).cpu().numpy() # [1, 512] → [1, 128] via PCA or linear layer # 项目中已集成PCA降维层,输出128维 return embedding / np.linalg.norm(embedding, ord=2, axis=1, keepdims=True) # L2归一化 # 使用示例 model = load_facenet_model() img_tensor = preprocess_facenet_input(aligned_face) # aligned_face来自2.3节 emb = get_embedding(model, img_tensor) # shape: (1, 128)
  • preprocess_facenet_input():注意Resize((160,160))不可省略,FaceNet对输入尺寸极其敏感(159×159会导致embedding偏移)
  • get_embedding():L2归一化是关键!未归一化时,余弦相似度=dot(a,b);归一化后,余弦相似度=dot(a,b)=cosθ,且范围严格在[-1,1],便于设定阈值

3.3 特征数据库设计:SQLite不是妥协,而是兼顾速度、事务与可调试性的最优解

人脸识别系统常被建议用FAISS或Annoy,但本项目坚持用SQLite,原因有三:

  1. 可调试性:SELECT * FROM embeddings WHERE person_id=123直接查向量,无需额外工具解析二进制索引
  2. 事务安全:注册新人时,需同时插入person_info表和embeddings表,SQLite支持ACID事务
  3. 零依赖部署:单文件数据库,拷贝即用,无Redis/MongoDB等外部服务

数据库schema如下:

CREATE TABLE IF NOT EXISTS persons ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, student_id TEXT UNIQUE, register_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS embeddings ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL, embedding BLOB NOT NULL, -- 存储128×4=512字节的float32数组 image_hash TEXT, -- 图片MD5,防重复注册 FOREIGN KEY(person_id) REFERENCES persons(id) );

插入embedding的Python代码:

def insert_embedding(conn, person_id, embedding, image_hash): # embedding: np.ndarray of shape (128,), dtype=float32 emb_bytes = embedding.astype(np.float32).tobytes() conn.execute("INSERT INTO embeddings (person_id, embedding, image_hash) VALUES (?, ?, ?)", (person_id, emb_bytes, image_hash)) conn.commit() # 查询最相似的top-k def search_similar(conn, query_emb, top_k=5, threshold=0.45): # 将query_emb转为bytes q_bytes = query_emb.astype(np.float32).tobytes() # SQLite不支持向量运算,用Python计算余弦相似度 cur = conn.cursor() cur.execute("SELECT id, person_id, embedding FROM embeddings") results = [] for row in cur.fetchall(): stored_emb = np.frombuffer(row[2], dtype=np.float32) sim = np.dot(query_emb, stored_emb) # 已归一化,dot即cosine similarity if sim >= threshold: results.append((row[0], row[1], float(sim))) results.sort(key=lambda x: x[2], reverse=True) return results[:top_k]
  • embedding BLOB:直接存二进制,避免JSON序列化精度损失(float32→str→float32会引入1e-6误差)
  • search_similar():虽不如FAISS快,但1万条记录下平均耗时8.2ms(i5-8250U),满足门禁系统实时性

3.4 避坑:FaceNet常见问题排查(现象→原因→解决)

现象原因解决
同一人脸多次提取embedding,余弦相似度仅0.82(应>0.98)RetinaFace对齐后未做直方图均衡,光照差异导致FaceNet输入分布偏移在align_face()后添加cv2.equalizeHist(cv2.cvtColor(aligned_face, cv2.COLOR_RGB2GRAY)),再转回RGB
注册新人后,查询返回空列表insert_embedding()中未commit,或conn对象被意外关闭在insert_embedding()末尾强制conn.commit(),并在注册函数入口处assert conn.total_changes > 0校验
embedding向量全为nanGPU推理时显存不足触发NaN传播,或输入图像全黑(像素值全0)在get_embedding()前添加assert img_tensor.std() > 0.01,并用torch.autograd.set_detect_anomaly(True)开启异常检测
LFW验证准确率仅89%(远低于宣称99%)测试时未用LFW标准协议(6000对,半数同人,半数不同人)使用lwf_eval.py脚本,严格按pairs.txt划分train/test,计算True Accept Rate @ False Accept Rate=0.001
CPU推理慢(>200ms/帧)模型未启用torch.jit.trace或onnxruntime将FaceNet导出为ONNX:torch.onnx.export(model, dummy_input, "facenet.onnx", opset_version=11),推理速度提升至32ms/帧

4. 系统级联与业务逻辑:从单帧识别到可运行的门禁管理界面

4.1 检测-嵌入-比对流水线:如何让RetinaFace和FaceNet协同不掉帧?

实时系统最大的陷阱是“模块割裂”——RetinaFace跑50ms,FaceNet跑120ms,叠加IO等待,单帧耗时200ms,根本无法满足30fps门禁需求。本项目采用三级缓冲流水线:

  1. Capture Thread:OpenCVVideoCapture持续读帧,写入queue.Queue(maxsize=2)
  2. Detect Thread:从队列取帧,运行RetinaFace,输出bbox+landms,写入detect_queue
  3. Embed Thread:从detect_queue取结果,对每个bbox做对齐+FaceNet推理,写入emb_queue
  4. Main Thread:从emb_queue取embedding,执行search_similar(),更新GUI状态
# pipeline_manager.py import threading import queue from retinaface_inference import RetinaFaceDetector from facenet_inference import FaceNetEmbedder class RecognitionPipeline: def __init__(self): self.cap_queue = queue.Queue(maxsize=2) self.detect_queue = queue.Queue(maxsize=5) self.emb_queue = queue.Queue(maxsize=10) self.detector = RetinaFaceDetector() self.embedder = FaceNetEmbedder() # 启动工作线程 threading.Thread(target=self._capture_loop, daemon=True).start() threading.Thread(target=self._detect_loop, daemon=True).start() threading.Thread(target=self._embed_loop, daemon=True).start() def _capture_loop(self): cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: continue try: self.cap_queue.put_nowait(frame) # 非阻塞,满则丢弃旧帧 except queue.Full: pass def _detect_loop(self): while True: frame = self.cap_queue.get() bboxes, landms = self.detector.detect(frame) for i in range(len(bboxes)): self.detect_queue.put((frame, bboxes[i], landms[i])) def _embed_loop(self): while True: frame, bbox, landm = self.detect_queue.get() aligned = align_face(frame, landm) emb = self.embedder.get_embedding(aligned) self.emb_queue.put((emb, bbox))
  • queue.Queue(maxsize=N):控制内存占用,避免线程积压导致OOM
  • put_nowait()/get():非阻塞操作,确保线程不挂起
  • daemon=True:主线程退出时自动终止子线程

4.2 门禁业务逻辑:不只是“识别成功”,而是“谁在何时以何种置信度通过”

一个合格的门禁系统,必须记录决策依据而非仅结果。项目定义的通行事件结构体:

class AccessEvent: def __init__(self, person_id, person_name, similarity, bbox, timestamp, device_id="door001"): self.person_id = person_id self.person_name = person_name self.similarity = similarity # 余弦相似度,0.0~1.0 self.bbox = bbox # [x1,y1,x2,y2] self.timestamp = timestamp # datetime.now() self.device_id = device_id self.is_allowed = similarity >= 0.45 # 动态阈值可配置 def to_dict(self): return { "person_id": self.person_id, "person_name": self.person_name, "similarity": round(self.similarity, 4), "bbox": [int(x) for x in self.bbox], "timestamp": self.timestamp.strftime("%Y-%m-%d %H:%M:%S"), "device_id": self.device_id, "is_allowed": self.is_allowed } # 写入日志文件(非数据库,保证高速写入) def log_access_event(event: AccessEvent, log_file="logs/access.log"): with open(log_file, "a") as f: f.write(json.dumps(event.to_dict()) + "\n")
  • similarity字段:答辩时可展示“该次识别置信度0.72,高于阈值0.45,故放行”,比单纯“识别成功”更有说服力
  • log_access_event():追加写入文本日志,避免数据库IO成为瓶颈;日志可被ELK或Grafana采集分析

4.3 PyQt5 GUI界面:不是花哨动画,而是工程师该有的信息密度

课程设计常犯的错是过度追求UI美观,却忽略关键信息展示。本项目GUI遵循工程师优先原则:

  • 左侧:实时视频流(标注bbox+姓名+similarity)
  • 右侧上:当前识别结果卡片(大字体显示姓名,背景色编码置信度:>0.7绿色,0.5~0.7黄色,<0.5红色)
  • 右侧中:最近10条通行记录(含时间、设备、置信度)
  • 右侧下:系统状态栏(RetinaFace FPS、FaceNet FPS、数据库连接状态、当前阈值)
# gui_main.py class RecognitionWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("FaceNet+RetinaFace门禁系统") self.pipeline = RecognitionPipeline() # 视频显示label self.video_label = QLabel() self.video_label.setFixedSize(640, 480) # 结果卡片 self.result_card = QLabel("等待识别...") self.result_card.setStyleSheet("font-size: 24px; font-weight: bold;") # 日志表格 self.log_table = QTableWidget(10, 4) self.log_table.setHorizontalHeaderLabels(["时间", "姓名", "置信度", "设备"]) # 状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.update_status() # 布局 main_layout = QHBoxLayout() left_layout = QVBoxLayout() left_layout.addWidget(self.video_label) right_layout = QVBoxLayout() right_layout.addWidget(self.result_card) right_layout.addWidget(QLabel("最近通行记录:")) right_layout.addWidget(self.log_table) main_layout.addLayout(left_layout, 70) main_layout.addLayout(right_layout, 30) container = QWidget() container.setLayout(main_layout) self.setCentralWidget(container) # 启动定时器刷新画面 self.timer = QTimer() self.timer.timeout.connect(self.update_frame) self.timer.start(33) # ~30fps def update_frame(self): try: emb, bbox = self.pipeline.emb_queue.get_nowait() # 执行search_similar()... # 更新result_card和log_table self.update_result_card(name, sim) self.update_log_table(name, sim, timestamp) except queue.Empty: pass # 读取最新帧并显示 frame = self.get_latest_frame() # 从pipeline取缓存帧 self.display_frame(frame)
  • QTimer.timeout.connect(self.update_frame):33ms间隔,避免GUI线程阻塞
  • update_result_card():根据similarity动态设置QLabel.setStyleSheet(),用颜色直观传达可靠性
  • get_latest_frame():从pipeline的cap_queue取最新帧,而非每次都cap.read(),减少IO延迟

4.4 避坑:系统级联常见问题排查(现象→原因→解决)

现象原因解决
GUI卡顿,视频停在某一帧update_frame()中search_similar()耗时过长,阻塞Qt事件循环将search_similar()放入QThread,用moveToThread()分离,GUI线程只负责显示结果
识别结果频繁跳变(A→B→A)单帧检测多个脸,系统默认取最高置信度,但下一帧可能另一个脸置信度更高在_embed_loop()中增加跟踪ID:用IoU匹配连续帧的bbox,对同一ID的embedding做滑动平均
注册新人后,GUI不显示新名字persons表插入成功,但embeddings表未commit,或GUI未刷新person_list在注册函数末尾发送QEvent通知GUI重载persons表,或用QSqlTableModel绑定数据库
多摄像头接入时,线程间数据竞争多个RecognitionPipeline实例共享同一cv2.VideoCapture为每个摄像头创建独立VideoCapture对象,并在__init__中指定cv2.CAP_DSHOW后端(Windows)或cv2.CAP_V4L2(Linux)
日志文件暴涨(1小时1GB)log_access_event()未做轮转,且记录了原始embedding二进制改为记录to_dict()结果(文本),并用logging.handlers.RotatingFileHandler限制单文件10MB,最多保留5个

5. 阈值调优与性能验证:用LFW和自建库双验证,拒绝“调参玄学”

5.1 阈值不是固定值,而是FAR/FRR权衡曲线上的一个点

人脸识别系统没有“正确阈值”,只有在特定业务场景下可接受的误识率(FAR)与拒识率(FRR)组合。本项目提供threshold_tuning.py脚本,自动绘制DET曲线(Detection Error Tradeoff):

# threshold_tuning.py import numpy as np from sklearn.metrics import roc_curve, auc def calculate_metrics(y_true, y_score, thresholds): fars = [] frrs = [] for th in thresholds: y_pred = (y_score >= th).astype(int) # FAR = FP / (FP + TN) fp = np.sum((y_true == 0) & (y_pred == 1)) tn = np.sum((y_true == 0) & (y_pred == 0)) far = fp / (fp + tn) if (fp + tn) > 0 else 0 # FRR = FN / (FN + TP) fn = np.sum((y_true == 1) & (y_pred == 0)) tp = np.sum((y_true == 1) & (y_pred == 1)) frr = fn / (fn + tp) if (fn + tp) > 0 else 0 fars.append(far) frrs.append(frr) return np.array(fars), np.array(frrs) # 加载LFW pairs数据 y_true, y_score = load_lfw_pairs("data/lfw/pairs.txt", model) # 返回真实标签和相似度得分 thresholds = np.arange(0.1, 0.9, 0.01) fars, frrs = calculate_metrics(y_true, y_score, thresholds) # 绘制DET曲线 plt.figure(figsize=(8,6)) plt.plot(fars, frrs, 'b-', label='DET Curve') plt.xlabel('False Accept Rate (FAR)') plt.ylabel('False Reject Rate (FRR)') plt.title('FaceNet+RetinaFace DET Curve') plt.grid(True) plt.legend() plt.savefig('det_curve.png')
  • load_lfw_pairs():按LFW标准协议,生成6000对样本(3000同人,3000不同人),计算每对的余弦相似度
  • calculate_metrics():对每个阈值计算FAR/FRR,得到曲线上的点
  • 关键结论:在LFW上,阈值0.45对应FAR=0.0012,FRR=0.032;阈值0.55对应FAR=0.0003,FRR=0.089。门禁系统推荐0.45(宁可多拒识,不可误放行)

5.2 自建测试库验证:为什么LFW不够?因为你的用户不是好莱坞明星

LFW数据集全是高清正面照,而你的门禁场景是:

  • 教室监控(低分辨率、运动模糊)
  • 宿舍门口(逆光、戴口罩)
  • 实验室(戴护目镜、侧脸)

因此,项目附带test_dataset_builder.py,指导你构建场景化测试集:

  1. 用系统摄像头拍摄20人,每人10张图(正面/侧脸/戴口罩/强光/弱光)
  2. 标注每张图的person_id和quality_score(1~5分,人工打分)
  3. 运行evaluate_on_custom.py,输出分场景准确率:
场景样本数准确率主要失败原因
正面清晰20098.2%—
侧脸30°20089.1%RetinaFace关键点偏移
戴口罩20076.3%FaceNet对下半脸缺失敏感
强逆光20083.7%对齐后直方图未均衡

血泪经验:戴口罩场景准确率低,不是模型问题,而是训练数据缺失。解决方案:在CASIA-WebFace上叠加口罩合成数据(用albumentations的RandomShadow+RandomRain增强),微调FaceNet最后两层,准确率可提升至89.6%。

5.3 实时性能压测:不是“能跑”,而是“稳跑30fps”

课程设计答辩常被问:“系统能支持多少路摄像头?”答案不在理论,而在实测。项目提供stress_test.py:

# stress_test.py import time from threading import Thread def benchmark_pipeline(pipeline, duration_sec=60): start_time = time.time() frame_count = 0 while time.time() - start_time < duration_sec: try: # 模拟持续输入 frame = np.random.randint(0, 256, (480,640,3), dtype=np.uint8) pipeline.cap_queue.put_nowait(frame) frame_count += 1 except queue.Full: pass end_time = time.time() fps = frame_count / (end_time - start_time) print(f"Stress test: {fps:.2f} FPS over {duration_sec}s") return fps # 测试单路 pipeline1 = RecognitionPipeline() benchmark_pipeline(pipeline1, 60) # 输出: Stress test: 28.4 FPS over 60s # 测试四路(模拟4个摄像头) <p> <a href="https://download.csdn.net/download/m0_38106923/87581257" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 3:27:37

大模型上下文模式实战:从窗口管理到裁剪策略的完整指南

做过大模型应用落地的人&#xff0c;应该都有过这种体验&#xff1a;同一个Prompt&#xff0c;在A场景下表现完美&#xff0c;换个场景就频繁“失忆”&#xff1b;明明把上下文窗口调满了&#xff0c;模型反而回答得越来越差&#xff1b;为了塞更多信息加了长文本&#xff0c;结…

作者头像 李华
网站建设 2026/10/4 3:26:26

S7-1200数据日志原理与CSV乱码/下载失败实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 3:26:20

软件测试20个基础面试题拆解:用测试思维答出层次感

上周帮一个准备入行的朋友做模拟面试&#xff0c;二十个“基础题”问下来&#xff0c;他前面十题还能接住&#xff0c;后面越答越虚。尤其是被问到“你们项目里的自动化用例怎么选”“如果线上漏了一个bug你怎么办”这类题目的时候&#xff0c;明显开始绕圈子。这其实就是很多新…

作者头像 李华
网站建设 2026/10/4 3:25:49

脚本进了沙箱,模块却在宿主执行:vm2 CLI 漏洞与默认配置审计

脚本进了沙箱&#xff0c;模块却在宿主执行&#xff1a;vm2 CLI 漏洞与默认配置审计 一、先把时间线和影响范围说清 GitHub 已审核记录列出&#xff1a;CVE-2026-92950 影响 npm 包 vm2 <3.11.6&#xff0c;最低修复版本为 3.11.7。项目于 2026-08-24 发布相关公告&#x…

作者头像 李华
网站建设 2026/10/4 3:25:12

长沙曾食坊小吃培训的米线螺蛳粉:酸辣底味怎么调

本篇要点&#xff1a; 1. 酸笋处理与气味控制&#xff1b;2. 汤底层次与辣酸平衡&#xff1b;3. 配菜下锅顺序。螺蛳粉的门槛不在粉&#xff0c;而在那股"闻着冲、吃着香"的底味怎么调稳。本文补的是米线螺蛳粉在酸辣结构上的那一层&#xff1a;从酸笋怎么处理、汤底…

作者头像 李华
网站建设 2026/10/4 3:25:05

长沙曾食坊小吃培训的张家界学员:景区餐饮选品怎么定

本篇要点&#xff1a;- 景区客流结构&#xff1a;团队客与散客、淡旺季落差极大&#xff1b;- 菜单不宜过宽&#xff1a;快出餐与便携优先&#xff1b;- 租金与位置换手率&#xff1a;选品要匹配摊位流动成本。张家界做景区餐饮&#xff0c;客流和城区完全不是一个逻辑&#xf…

作者头像 李华