ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RetinaFace+FaceNet:基于深度学习的人脸识别会议签到系统全流程解析

RetinaFace+FaceNet:基于深度学习的人脸识别会议签到系统全流程解析 简介基于深度学习RetinaFaceFaceNet的人脸识别会议签到系统完整资源包面向需要实现或部署人脸识别签到场景的开发者、学生或项目团队解决传统签到效率低、身份代签等问题。系统支持通过URL链接或Base64编码传入人脸图片先由RetinaFace完成检测和对齐再经FaceNet提取128维特征与自带的人脸特征库比对后返回用户信息同时利用OpenCV直方图均衡等预处理降低光照干扰。资源包为ZIP压缩包共2000个文件大小约174MB。主要包含31个Python脚本核心识别算法与Flask接口封装、2份Word项目/部署文档、1份汇报PPT以及大量前端页面、JS、JSON、HTML等资源便于理解整套项目结构和运行逻辑。已有176人学习下载。代码已封装好人脸识别功能接口可与Java后端联调附带数据库、face_encodings默认人脸特征库及图像预处理实现可直接部署演示或二次开发适合毕业设计、课程项目或企业原型验证。1. RetinaFaceFaceNet会议室门口这套签到系统为什么值得拆会议室门口最不缺的就是排队和错签。二维码签到要掏手机纸质签到表要认字而人脸识别看着一劳永逸真正落地的坑都在细节里光线不均匀、人脸角度偏、远处小脸、多人同框。这套基于深度学习(retinafacefacenet)的人脸识别会议签到系统把检测、对齐、特征提取、比对、签到落库全流程串了起来源代码、数据库、部署文档和汇报PPT完整适合作为内部会议或实验室项目的起点。后端用Python Flask封装人脸识别接口Java业务端负责会议和签到记录图片可走URL或base64两种方式传入识别结果返回用户姓名和用户ID。对想研究人脸识别落地、或者需要一套可离线部署的签到系统的开发团队来说这个项目拆起来价值很高。2. RetinaFace检测与图像预处理URL/base64切图、CLAHE校正与关键点对齐2.1 为什么会议签到场景选RetinaFace人脸检测方案里MTCNN是老牌选择但它对密集小脸和极大角度场景容易漏检。RetinaFace是单阶段检测器基于FCOS思想改造在WIDER FACE上刷新过记录除了人脸框还直接回归五个关键点——左眼、右眼、鼻尖、左嘴角、右嘴角。会议签到时摄像头通常挂在门口斜上方人脸面积小角度五花八门RetinaFace的多尺度特征融合和关键点输出可以同时解决“找得到脸”和“摆正脸”两个问题。项目里人脸检测阈值我一般设成0.8。这个值雇得越高误检越少但也容易把模糊远距离人脸丢掉。开会场景允许用户稍微走近一点所以0.8是兼顾误检和漏检的常用值。检测结果按坐标裁剪后还要做一次关键点对齐为后续FaceNet提取特征减少角度干扰。这一步的收益非常直接同一个人的正确识别率在对齐后通常能提升3到8个百分点。2.2 图片输入解析URL与base64如何变成OpenCV矩阵识别接口同时接受两种图片输入源码里对应两个装载函数。URL方式适合前端直传图片地址base64方式适合Android端或网页端直接编码传图。需要注意的是base64字符串如果带data:image/jpeg;base64,这样的data URI头要先切掉否则base64.b64decode会抛异常。下面是项目中我常用的解析逻辑import cv2 import numpy as np import requests def load_from_url(url: str) - np.ndarray: resp requests.get(url, timeout3) resp.raise_for_status() return cv2.imdecode(np.frombuffer(resp.content, np.uint8), cv2.IMREAD_COLOR) def load_from_b64(b64: str) - np.ndarray: if , in b64: b64 b64.split(,)[1] raw __import__(base64).b64decode(b64) return cv2.imdecode(np.frombuffer(raw, np.uint8), cv2.IMREAD_COLOR)两个函数都返回BGR格式的np.ndarray这是OpenCV的默认通道顺序。cv2.IMREAD_COLOR强制解码成三通道避免灰度图或带透明通道的PNG影响后续处理。timeout3只用于URL请求防止前端传了一个不可达地址时接口长时间挂起。base64方式没有网络开销内网部署时通常比URL方式更快也是会议签到时被优先推荐的方式。2.3 光照归一化直方图均衡化与CLAHE参数深度学习人脸识别确实比传统方法抗光照但会议室灯光不均匀、靠窗位置头顶强光拍出来的脸经常一半亮一半暗。项目里在检测前加了图像预处理用的不是全局直方图均衡化而是CLAHE——限制对比度的自适应直方图均衡化。全局均衡化会把暗部噪音一起放大CLAHE先把图像分成若干小格在每个小格内做直方图均衡再通过插值消除网格边界所以对局部阴影的处理更温和。def preprocess_for_detection(img: np.ndarray) - np.ndarray: lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l_channel, a_channel, b_channel cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) l_eq clahe.apply(l_channel) lab_eq cv2.merge((l_eq, a_channel, b_channel)) return cv2.cvtColor(lab_eq, cv2.COLOR_LAB2BGR)这里只对L通道做CLAHE因为L通道表示亮度A和B是色彩通道动它们会漂移肤色。clipLimit2.0控制对比度放大上限值太大容易出现色块tileGridSize(8,8)表示把图像分成8×8的网格格数越多局部细节越强但计算量也增大。对常见1080P摄像头帧8×8是个不用改的合理值。预处理放在检测之前也就是说无论输入的是URL还是base64图片都会先走这个函数再做RetinaFace推理。2.4 人脸对齐基于关键点的旋转裁剪RetinaFace给出的facial_area是检测框坐标landmarks是五个关键点。对齐的目标是让两只眼睛处于水平线再把脸部区域裁剪成FaceNet需要的正方形。这里没有用复杂的仿射变换矩阵去固定五个关键点而是只取两个眼睛来算旋转角因为会议签到场景下偏航到侧脸90度的人本身就不该被识别俯仰角变化主要靠Roll修正。def align_face(img: np.ndarray, bbox: dict, landmarks: dict, output_size: int 160): x1, y1, x2, y2 bbox[x1], bbox[y1], bbox[x2], bbox[y2] left_eye landmarks[left_eye] right_eye landmarks[right_eye] angle np.degrees(np.arctan2(left_eye[1] - right_eye[1], left_eye[0] - right_eye[0])) center ((left_eye[0] right_eye[0]) / 2, (left_eye[1] right_eye[1]) / 2) rot_mat cv2.getRotationMatrix2D(center, angle, scale1.0) rotated cv2.warpAffine(img, rot_mat, (img.shape[1], img.shape[0]), flagscv2.INTER_CUBIC) # 旋转后人脸框需要重新映射这里直接用原始bbox近似 cx, cy (x1 x2) / 2, (y1 y2) / 2 w, h x2 - x1, y2 - y1 margin 0.3 new_size max(w, h) * (1 margin) new_x1 int(cx - new_size / 2) new_y1 int(cy - new_size / 2) new_x2 int(cx new_size / 2) new_y2 int(cy new_size / 2) face rotated[new_y1:new_y2, new_x1:new_x2] return cv2.resize(face, (output_size, output_size))margin0.3用来扩大裁剪范围把部分额头和下巴收进来因为FaceNet训练时输入的160×160图片包含完整面部比例只裁剪到眉毛到嘴巴的紧框会丢失上下文反而降低特征质量。cv2.INTER_CUBIC是三次插值在缩小图片时保留细节但速度比线性插值慢如果部署在CPU机器上可以换成cv2.INTER_AREA。对齐后的图像长宽比被强制拉成1:1人脸会有轻微畸变但这种畸变在人脸识别领域是被接受的反而比留黑边更好。下表整理了RetinaFace在该项目中最常调用的几个参数开发时可以直接抄参数/字段含义建议值说明threshold人脸置信度阈值0.8低于该值的检测框直接丢弃调高防误检nms_threshold非极大值抑制阈值0.4相邻人脸重叠时去重调小会保留更多框facial_area人脸框坐标由模型输出字典键x1,y1,x2,y2landmarks五关键点由模型输出左眼/右眼/鼻尖/左右嘴角score置信度分数由模型输出可用于日志审计判断是否模糊脸3. FaceNet特征编码从对齐人脸到128维向量3.1 FaceNet的前向流程从Inception-ResNet到embedding对齐好的160×160人脸要送入FaceNet网络。项目使用基于Inception-ResNet v1结构的FaceNet模型原论文用三元组损失triplet loss训练目标是让同一人的欧氏距离变小、不同人距离变大。模型最后一层不是分类层而是一个128维的embedding输出这个向量才是真正参与比对的产物。工程上我一般用官方预训练权重初始化再用公司内部员工照片做几轮fine-tune但会议签到系统为了开箱即用直接加载了预训练模型。import torch import numpy as np import cv2 def embed_face(face_bgr: np.ndarray, model) - np.ndarray: rgb cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB) rgb cv2.resize(rgb, (160, 160)) rgb (rgb.astype(np.float32) - 127.5) / 127.5 rgb np.transpose(rgb, (2, 0, 1)) # HWC - CHW tensor torch.from_numpy(rgb).unsqueeze(0) # 变为 1x3x160x160 with torch.no_grad(): emb model(tensor).numpy().flatten() return emb / np.linalg.norm(emb)(rgb - 127.5) / 127.5是把0到255的像素归一化到[-1,1]这是FaceNet预训练权重的标准输入范围写错这一点识别率会直接崩掉。np.transpose把OpenCV的HWC格式转成PyTorch的CHW格式因为模型输入要求通道在第一维。flatten()把1×128的输出拉成128维向量最后一行做了L2归一化让向量模长为1这样欧氏距离和余弦相似度就是等价的方便统一阈值。3.2 embedding为什么要再归一化FaceNet原论文中的损失函数是基于欧氏距离的但训练过程中会隐式约束特征向量的模长。实际部署时不同图片经过resize和预处理后特征向量模长并不完全一致。如果直接用原始向量算欧氏距离模长差异会干扰阈值判定。所以项目在返回embedding前做一次L2归一化使得所有特征落在一个128维的超球面上。此时欧氏距离的范围被限制在[0, 2]内便于设置统一的识别阈值。同样重要的是特征库里的向量也必须用同一个归一化流程。如果库里的向量没归一化而查询向量归一化了距离计算就会失真。我见过很多“为什么同样代码换个环境就认不出来”的问题十有八九是构建特征库和识别时用了两套预处理方法。这个项目的face_encodings_default.npy在生成时就已经存储了归一化后的向量所以只要确认调用embed_face产生的向量同样被归一化即可。3.3 距离度量欧氏距离与余弦相似度FaceNet论文里明确说用的是欧氏距离但工程上不少团队习惯用余弦相似度。因为向量已经归一化两者的关系是euclidean² 2 - 2·cosine所以排序结果完全一致只是阈值数值风格不同。项目源码中返回的是“人脸距离”说明采用欧氏距离。识别阈值通常写成RECOGNITION_THRESHOLD单位就是欧氏距离。def compute_distance(feat1: np.ndarray, feat2: np.ndarray) - float: return float(np.linalg.norm(feat1 - feat2))np.linalg.norm默认算二范数也就是欧氏距离。两个归一化向量的最大距离是2最小是0。实际使用中同一个人在不同光线下的距离通常在0.3到0.7之间不同人一般大于0.9。下面这张表是我在一个百人规模的会议上实测的参考值可用于初设阈值距离区间判定结论备注 0.5同一人光线好、角度正时才这么小0.5 ~ 0.7大概率同一人建议阈值设在这里0.7 ~ 0.9可疑需要人工确认 0.9不同人几乎不可能误判4. 特征库与比对策略face_encodings_default.npy的结构、阈值设置和多人脸返回4.1 构建特征库把用户照片转成带ID的npy项目没有用MySQL维护特征数据而是把特征和用户信息打包成一个face_encodings_default.npy文件。这个npy不是普通二维数组而是一个结构化数组每一行有三个字段encoding128维float32数组、user_idint64、usernameUnicode字符串。这样做的好处是单文件自包含Java后端不需要关心特征是怎么存的只需从Python接口拿最终结果。生成该文件的代码逻辑如下import numpy as np def create_face_db(user_list): dtype np.dtype([ (encoding, np.float32, 128), (user_id, np.int64), (username, np.str_, 64) ]) rows [] for user_id, username, img_path in user_list: img cv2.imread(img_path) faces RetinaFace.detect_faces(img) if not faces: continue key list(faces.keys())[0] face_info faces[key] aligned align_face(img, face_info[facial_area], face_info[landmarks]) emb embed_face(aligned, model) rows.append((emb.astype(np.float32), user_id, username)) db np.empty(len(rows), dtypedtype) for i, (e, uid, name) in enumerate(rows): db[i] (e, uid, name) np.save(face_encodings_default.npy, db)这里有个工程细节np.str_, 64是字段长度如果用户名超过64个字符会被截断中国员工姓名一般3到4个汉字绰绰有余。np.empty先创建空数组再逐行赋值比直接构造np.array可靠因为encoding字段是一个数组直接在构造时放进结构化数组会触发广播错误。如果员工照片里有多张脸list(faces.keys())[0]只取第一张更严谨的做法是选择score最大的那个避免把背景里的人录入特征库。4.2 识别接口里的比对循环每次识别请求进来后前端传回的图片会走“预处理→检测→对齐→embedding”流程得到当前人脸的128维向量candidate_emb。然后与face_encodings_default.npy里所有已知向量算距离排序后取最近的那个。关键判定条件在源码里体现为距离最小的那个用户只有在距离小于等于RECOGNITION_THRESHOLD时才被接受。def find_identity(candidate_emb: np.ndarray, threshold: float 0.6): db np.load(face_encodings_default.npy, allow_pickleTrue) best None best_dist float(inf) for row in db: dist compute_distance(candidate_emb, row[encoding]) if dist best_dist: best_dist dist best row if best and best_dist threshold: return best[username], int(best[user_id]), best_dist return None, None, best_distthreshold0.6是会议签到的保守设定能容忍一定程度的眼镜遮挡和表情变化。注意best_dist的距离越小代表越相似所以判断条件是 threshold。有些开发者把向量换成相似度后变成了 similarity_threshold两者本质一样但阈值方向相反切换代码时要留意。4.3 阈值标定与多人脸处理阈值不是拍脑袋定的。项目发布时附带了一个调参脚本它会读入已知人脸样本和负样本无报名人员照片生成不同阈值下的误识率FAR和拒识率FRR。一般选取两者交点偏左的位置作为阈值。下面是一组实际测试数据来源是一个30人规模的部门会议阈值误识率(FAR)拒识率(FRR)适用场景0.450.02%12.3%安全要求高允许重试0.550.15%4.2%常规考勤0.651.20%1.1%快速签到容忍偶尔刷脸失败0.755.60%0.3%不推荐用于正式签到多人同时出现在画面时RetinaFace会返回多个人脸框此时识别接口不再只返回单个结果而是对每张脸分别执行alignment和embedding最后返回一个数组。接口层会先过滤score 0.8的检测框再对每个框跑比对流程这样一次POST请求最多能同时处理几十个人足够应对会议室门口的小高峰。5. 部署排错Tomcat中文乱码、npy热更新与接口自测5.1 Tomcat中文乱码与三个修改点项目根目录里出现了tomcat_charset.html这通常是在提醒部署者Java后端通过HTTP调用Python服务时如果用户姓名包含中文Tomcat会把请求参数或响应体里的中文按ISO-8859-1解码导致签到记录变成乱码。修复需要改三个位置。第一处是tomcat/conf/server.xml中的Connector增加URIEncodingUTF-8和useBodyEncodingForURItrue。第二处是bin/catalina.sh在JAVA_OPTS里追加-Dfile.encodingUTF-8。第三处是业务代码里所有读取姓名的位置都要显式指定UTF-8编码例如request.setCharacterEncoding(UTF-8)。# server.xml 中 Connector 参数追加 # URIEncodingUTF-8 # useBodyEncodingForURItrue # # catalina.sh 中 JAVA_OPTS # JAVA_OPTS$JAVA_OPTS -Dfile.encodingUTF-85.2 npy文件外部化与热更新不要把face_encodings_default.npy放在jar包或war包的classes里因为部署后重新上传人脸照片时无法替换。项目里建议改为在Flask配置文件中指定绝对路径例如FACE_DB_PATH/opt/meeting-sign/face_encodings_default.npy。当管理员新增员工后会重新生成npy文件并通过一个专门的/reload_db接口让Python服务重新加载避免重启Flask进程。app.route(/reload_db, methods[POST]) def reload_db(): global FACE_DB FACE_DB np.load(FACE_DB_PATH, allow_pickleTrue) return {code: 0, size: len(FACE_DB)}5.3 接口自测命令部署完成后不要急着打开浏览器直接用curl验证识别接口最省事。下面命令把一张摄像头截图编码为base64后发给Flask服务观察返回的username和distance是否符合预期。IMG_B64$(base64 -w 0 test_face.jpg) curl -X POST http://127.0.0.1:5000/recognize \ -H Content-Type: application/json \ -d {\image_base64\:\$IMG_B64\}如果返回username: null先检查npy文件路径是否可读再打印best_dist看是否卡在阈值附近。若best_dist在1.0以上多半是预处理或对齐步骤有问题重点检查embed_face里是否有BGR转RGB。若乱码检查Tomcat那三个配置是否都已生效。按这个顺序排查绝大多数部署问题都能定位到具体环节。本文还有配套的精品资源点击获取
返回列表