ARTICLE DETAIL

资讯详情

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

RetinaFace+FaceNet人脸识别会议签到系统实战:从模型选型到部署避坑

RetinaFace+FaceNet人脸识别会议签到系统实战:从模型选型到部署避坑 简介这份资源是一套基于深度学习的人脸识别会议签到系统完整项目包面向具备Java基础、希望将人工智能落地到实际业务场景的开发者与学习者。系统以RetinaFace完成复杂光照、遮挡与表情变化下的人脸检测和关键点定位再由FaceNet提取特征向量并映射到欧氏空间通过比对预存参会者信息实现自动签到同时可扩展活体检测以防欺诈。压缩包整体约63.66MB文件类型以Java源码、深度学习模型文件及配置脚本为主分别承担后台服务、模型集成与数据存储等职责目录结构便于按模块查阅。目前已有224人学习下载。读者可从中获得从模型构建、训练到Java平台部署的完整思路理解TensorFlow Java等库的集成方式并掌握人脸检测、特征比对与签到流程的工程实现适合作为课程设计或企业级应用开发的参考范例。1. 从一张会议签到表说起RetinaFaceFaceNet 到底解决了什么一场两百人的行业会议签到台前排着长队工作人员拿着扫码枪一个个核对工牌偶尔还要手动输入手机号后四位。这种场景你大概率见过甚至亲手搭过。问题不在于签到这件事本身而在于「确认站在摄像头前的人就是报名的那个人」这一步传统方案要么依赖二维码可以转发要么依赖人工核验效率低要么用指纹接触式疫情期间谁都不想按。基于深度学习的人脸识别会议签到系统核心就是把这一步换成「无感通行」参会者走到签到终端前摄像头抓拍一张人脸系统在几百毫秒内完成检测、对齐、特征提取、比对然后自动标记签到成功。整条链路里RetinaFace 负责「找到脸并对齐」FaceNet 负责「把脸变成可以比对的向量」两者串起来就是一套完整的人脸识别流水线。这套方案适合谁如果你正在做课程设计、企业内部会议系统、小型活动现场签到工具或者想找一个端到端的人脸识别项目练手它是个不错的切入点。它不需要 GPU 集群一台带普通摄像头的笔记本就能跑通全流程但它也不是玩具——RetinaFace 在 WIDER FACE 上的检测精度和 FaceNet 在 LFW 上的验证准确率放到真实场景里依然能打。接下来我会把这条链路拆开从模型选型到代码落地再到实际部署时那些让人头疼的坑一步步讲清楚。2. RetinaFace 与 FaceNet 的选型逻辑与最小可跑通链路2.1 为什么是 RetinaFace 而不是 MTCNN 或 YOLO 人脸人脸检测这个环节可选方案不少。MTCNN 是老牌方案级联结构速度快但在侧脸、遮挡、小尺度人脸上的召回率明显下滑。YOLO 系列做人脸检测需要额外训练虽然推理快但精度调优成本高。RetinaFace 的优势在于它把「检测框」和「五点关键点」放在一个单阶段网络里联合回归一次前向就能拿到人脸位置和眼睛、鼻尖、嘴角坐标——这五个点恰好是后续对齐需要的全部信息。更关键的是 RetinaFace 的多尺度特征融合策略。它用 FPN 结构把浅层高分辨率特征和深层语义特征拼在一起小脸和大脸都能兼顾。实际测试中一张 1080P 的会议现场照片RetinaFace 能稳定检出 20 到 30 个人脸包括后排只占几十个像素的小脸而 MTCNN 在同一张图上会漏掉三到五个。对于签到场景漏检意味着有人签不上这是不可接受的。选型建议很直接如果你追求开箱即用的检测精度RetinaFace 是当前性价比最高的选择。模型权重文件常见的是 ResNet50 backbone 版本和 MobileNet0.25 版本前者精度高但推理慢后者适合边缘设备。会议签到场景我一般用 MobileNet0.25 版本在 CPU 上单帧推理能压到 80 毫秒以内精度损失在可接受范围内。2.2 FaceNet 把脸变成向量之后比对逻辑怎么写FaceNet 的核心思想是 triplet loss让同一个人的不同照片在嵌入空间里尽可能靠近不同人的照片尽可能远离。最终输出是一个 128 维也有 512 维版本的浮点向量这个向量就是人脸的「数字指纹」。比对时不需要分类器直接算两个向量的欧氏距离或余弦相似度就行。这里有个容易翻车的地方很多人以为 FaceNet 输出的是概率或类别其实它输出的是嵌入向量。你需要自己维护一个「已注册人脸库」每个参会者注册时存下他的特征向量签到的时候拿现场抓拍的特征去和库里所有向量算距离取最小距离对应的那个人。如果最小距离超过阈值就判定为「陌生人」。阈值怎么设这是血泪经验。LFW 数据集上论文报告的准确率是在特定阈值下测的但真实会议场景的光照、角度、摄像头质量都和 LFW 不一样。我一般会先用一批测试照片跑一遍画出类内距离和类间距离的分布图然后取两者交界处偏类内一侧的值。经验值余弦相似度阈值设在 0.6 到 0.7 之间欧氏距离阈值设在 1.0 到 1.2 之间。低于这个值可能把同一个人判成不同人拒真高于这个值可能把不同人判成同一个人认假。签到场景宁可拒真也不能认假所以阈值要偏保守。2.3 从摄像头到签到记录一条最小可跑通的代码链路下面这段代码把 RetinaFace 检测、对齐、FaceNet 特征提取、比对、写签到记录串成一条线。依赖库用retinaface-pytorch和facenet-pytorch这两个是社区维护比较活跃的版本。import cv2 import numpy as np import torch from retinaface import RetinaFace from facenet_pytorch import InceptionResnetV1, MTCNN from scipy.spatial.distance import cosine # 初始化 FaceNet 模型使用预训练权重 # 注意facenet-pytorch 的 InceptionResnetV1 默认输出 512 维向量 # 如果要用 128 维版本需要加载对应的 .pth 权重文件 model InceptionResnetV1(pretrainedvggface2).eval() # 已注册人脸库{姓名: 特征向量} registered_faces {} def register_face(name, image_path): 注册人脸检测 对齐 提取特征 入库 img cv2.imread(image_path) # RetinaFace 检测返回人脸框和五点关键点 faces RetinaFace.detect_faces(img) if not isinstance(faces, dict) or len(faces) 0: raise ValueError(f未检测到人脸: {image_path}) # 取置信度最高的人脸 best_face max(faces.values(), keylambda x: x[score]) # 用五点关键点做仿射变换对齐到 160x160 aligned align_face(img, best_face[landmarks]) # 归一化到 [-1, 1]FaceNet 要求输入范围 tensor torch.tensor(aligned).permute(2, 0, 1).float().unsqueeze(0) / 127.5 - 1.0 with torch.no_grad(): embedding model(tensor).numpy().flatten() registered_faces[name] embedding return embedding def align_face(img, landmarks): 根据五点关键点做相似变换对齐到标准人脸姿态 # 标准人脸五点位置160x160 输入 dst_pts np.array([ [54.0, 62.0], # 左眼 [106.0, 62.0], # 右眼 [80.0, 88.0], # 鼻尖 [58.0, 112.0], # 左嘴角 [102.0, 112.0] # 右嘴角 ], dtypenp.float32) src_pts np.array([ landmarks[left_eye], landmarks[right_eye], landmarks[nose], landmarks[mouth_left], landmarks[mouth_right] ], dtypenp.float32) # 计算相似变换矩阵并应用 M, _ cv2.estimateAffinePartial2D(src_pts, dst_pts) aligned cv2.warpAffine(img, M, (160, 160)) return aligned def recognize(img): 签到识别返回匹配到的姓名和相似度 faces RetinaFace.detect_faces(img) if not isinstance(faces, dict) or len(faces) 0: return None, 0.0 best_face max(faces.values(), keylambda x: x[score]) aligned align_face(img, best_face[landmarks]) tensor torch.tensor(aligned).permute(2, 0, 1).float().unsqueeze(0) / 127.5 - 1.0 with torch.no_grad(): embedding model(tensor).numpy().flatten() # 与库中所有人比对取余弦相似度最高者 best_name, best_sim None, -1.0 for name, ref_emb in registered_faces.items(): sim 1 - cosine(embedding, ref_emb) if sim best_sim: best_sim sim best_name name # 阈值判断低于 0.65 视为陌生人 if best_sim 0.65: return None, best_sim return best_name, best_sim这段代码的逻辑分三层。第一层是register_face它把注册照片里的人脸检测出来、对齐、提特征、存进字典。第二层是align_face用五点关键点做相似变换把任意角度的人脸扭正到标准姿态——这一步直接影响后续特征质量不做对齐的话识别率会掉 10 到 15 个百分点。第三层是recognize对现场抓拍图做同样处理然后遍历已注册库算余弦相似度取最高分并做阈值判断。参数方面有几个关键点。InceptionResnetV1的pretrainedvggface2表示用 VGGFace2 数据集预训练的权重这个版本在亚洲人脸上的表现比casia-webface版本更稳。输入尺寸固定 160x160这是 FaceNet 的标准输入不要随意改。归一化用/ 127.5 - 1.0把像素值映射到 [-1, 1]和训练时保持一致。阈值 0.65 是余弦相似度的经验值实际部署时建议用一批已知身份的测试集跑 ROC 曲线来微调。提示RetinaFace 的detect_faces返回的 landmarks 字典里键名是left_eye、right_eye、nose、mouth_left、mouth_right不同版本的 retinaface 库可能有细微差异用之前先打印一下确认。3. 把模型塞进会议签到流程注册、抓拍、比对、落库3.1 参会者注册环节的批量处理与特征库设计会议签到系统的注册环节通常发生在会前参会者上传一张证件照或自拍系统提取特征入库。这个环节的工程问题不是算法而是批量处理的稳定性和特征库的存储结构。如果参会者只有几十人用 Python 字典存内存里完全够用。但如果是几百人甚至上千人每次识别都遍历全库算余弦相似度耗时会线性增长。假设库里有 1000 人每次识别要算 1000 次余弦距离单次约 0.1 毫秒总共 100 毫秒——还能接受。但如果库扩大到 10000 人就是 1 秒签到体验直接崩掉。常见做法是用 FAISS 或 Milvus 做向量索引。FAISS 的IndexFlatIP做内积搜索10000 个 512 维向量单次查询能压到 1 毫秒以内。如果追求更高性能可以用IndexIVFFlat做聚类索引但需要额外训练。对于会议签到这种规模IndexFlatIP足够了。特征库的存储格式建议用二进制文件或数据库 BLOB 字段。不要存成 CSV 或 JSON浮点数转文本再转回来会有精度损失而且文件体积膨胀三到五倍。我一般用 numpy 的.npy格式存向量矩阵用另一个 JSON 文件存姓名到行号的映射。import numpy as np import json import faiss class FaceGallery: def __init__(self, dim512): # 使用内积索引余弦相似度需要先做 L2 归一化 self.index faiss.IndexFlatIP(dim) self.names [] def add(self, name, embedding): # L2 归一化后内积等价于余弦相似度 vec embedding / np.linalg.norm(embedding) self.index.add(vec.reshape(1, -1).astype(np.float32)) self.names.append(name) def search(self, embedding, top_k1): vec embedding / np.linalg.norm(embedding) sims, idxs self.index.search(vec.reshape(1, -1).astype(np.float32), top_k) results [] for sim, idx in zip(sims[0], idxs[0]): if idx 0: results.append((self.names[idx], float(sim))) return results def save(self, path): faiss.write_index(self.index, f{path}.index) with open(f{path}.names.json, w) as f: json.dump(self.names, f) def load(self, path): self.index faiss.read_index(f{path}.index) with open(f{path}.names.json) as f: self.names json.load(f)这个FaceGallery类封装了增量和查询。add方法先做 L2 归一化这样 FAISS 的内积搜索结果直接就是余弦相似度。search返回 top_k 个候选签到场景取 top_1 就行但如果要做「疑似同一人」的二次确认可以取 top_3 看距离分布。save和load把索引和姓名列表分开存加载时先读索引再读姓名顺序不能反。3.2 签到终端的抓拍策略与活体检测的取舍签到终端一般是一台带摄像头的平板或一体机放在签到台。抓拍策略直接影响用户体验如果每帧都跑检测和识别CPU 占用高而且同一个人会被反复识别。常见做法是「运动检测触发 冷却时间」用 OpenCV 的背景减除法检测画面中是否有人脸大小的运动区域有运动才触发识别识别成功后对该人设置 30 秒冷却避免重复签到。活体检测是另一个绕不开的话题。照片攻击、视频回放攻击在真实场景里确实存在但完整的活体检测方案如静默活体、动作活体会显著增加工程复杂度。对于内部会议签到我的建议是如果参会者都是同事或已知身份可以不做活体检测靠人工抽查兜底如果是公开活动至少加一个「眨眼检测」或「随机动作」的轻量级活体用 MediaPipe Face Mesh 就能实现不需要额外模型。import cv2 import time class SignInTerminal: def __init__(self, gallery, threshold0.65, cooldown30): self.gallery gallery self.threshold threshold self.cooldown cooldown self.last_seen {} # {name: timestamp} self.bg_subtractor cv2.createBackgroundSubtractorMOG2() def process_frame(self, frame): # 运动检测只在有运动时触发识别 fg_mask self.bg_subtractor.apply(frame) motion_pixels cv2.countNonZero(fg_mask) if motion_pixels 500: # 运动区域太小跳过 return None # 调用识别函数 name, sim recognize(frame) if name is None: return None # 冷却检查 now time.time() if name in self.last_seen and now - self.last_seen[name] self.cooldown: return None self.last_seen[name] now return {name: name, similarity: sim, timestamp: now}这段代码里bg_subtractor是 OpenCV 自带的 MOG2 背景减除器motion_pixels阈值 500 是个经验值对应画面中大约一个人脸大小的运动区域。冷却时间 30 秒是防止同一个人站在摄像头前被反复签到。recognize函数就是上一节定义的那个。注意背景减除法在光照突变比如有人开灯时会产生大量误检实际部署时建议加一个「连续 N 帧都有运动」的确认逻辑或者直接用 RetinaFace 的检测结果做触发牺牲一点性能换稳定性。3.3 签到记录落库与 Java 侧对接的常见做法签到记录最终要落到数据库供后台查询和导出。如果整个系统是 Python 写的直接用 SQLite 或 MySQL 的 Python 驱动就行。但很多会议签到系统的后台是 Java 写的热词里 Java 出现频率很高这时候 Python 侧和 Java 侧的对接就成了一个工程问题。常见做法有两种。第一种是 Python 侧只做识别识别结果通过 HTTP 接口推给 Java 后台。Python 用 Flask 或 FastAPI 起一个轻量服务Java 侧用 HttpClient 或 RestTemplate 调用。这种方式的优点是解耦Python 侧可以独立部署和升级缺点是多了网络开销而且需要处理并发和超时。第二种是 Python 侧直接写数据库Java 侧读同一个库。这种方式简单直接但要注意事务和并发写入的问题。如果签到终端只有一台直接写没问题如果有多台终端同时写建议用消息队列如 RabbitMQ做缓冲或者让 Java 侧提供一个写入接口Python 侧调用。// Java 侧接收签到结果的 Controller 示例 RestController RequestMapping(/api/signin) public class SignInController { Autowired private SignInRecordRepository repository; PostMapping(/record) public ResponseEntityString recordSignIn(RequestBody SignInRequest request) { // 幂等检查同一人同一会议在冷却时间内不重复记录 OptionalSignInRecord existing repository .findByMeetingIdAndNameAndTimestampAfter( request.getMeetingId(), request.getName(), request.getTimestamp() - 30000 ); if (existing.isPresent()) { return ResponseEntity.ok(duplicate); } SignInRecord record new SignInRecord(); record.setMeetingId(request.getMeetingId()); record.setName(request.getName()); record.setSimilarity(request.getSimilarity()); record.setTimestamp(request.getTimestamp()); repository.save(record); return ResponseEntity.ok(success); } }Java 侧这段代码的关键是幂等检查。Python 侧虽然有冷却时间但网络重试或终端重启可能导致重复推送Java 侧再做一次 30 秒内的去重保证数据库里不会出现同一个人连续两条签到记录。SignInRequest是一个简单的 DTO包含会议 ID、姓名、相似度、时间戳四个字段。如果 Java 后台需要生成签到报表可以用 POI 库导出 Excel。热词里有人问「java poi word 能生成图表吗」答案是 POI 对 Word 图表的支持比较有限但 Excel 图表支持得很好。签到报表用 Excel 导出更合适POI 的XSSFChart可以生成柱状图和饼图展示签到率和迟到分布。4. 部署时最容易翻车的五个地方4.1 检测不到人脸先看光照再看输入尺寸现象摄像头画面里明明有人但 RetinaFace 返回空字典或者只检测到部分人脸。原因最常见的是光照问题。RetinaFace 在训练时用的 WIDER FACE 数据集以自然场景为主对逆光和侧光有一定鲁棒性但如果摄像头正对窗户或强光源人脸区域过曝或过暗检测置信度会掉到阈值以下。另一个常见原因是输入图像尺寸。RetinaFace 的默认输入是原图如果原图是 4K 分辨率模型内部会缩放到固定尺寸小脸在缩放后可能只剩几个像素直接丢失。解决先检查摄像头曝光设置开启自动曝光或手动调低曝光补偿。如果光照无法改变可以在检测前做直方图均衡化或 CLAHE。输入尺寸方面建议在送入 RetinaFace 之前把图像长边缩放到 1024 到 1280 之间既保留小脸细节又不至于推理太慢。如果还是检测不到把detect_faces的threshold参数从默认的 0.8 降到 0.5 试试但要注意误检率会上升。4.2 识别成陌生人对齐没做对或者阈值设太高现象同一个人注册时用的照片和签到时的抓拍识别相似度只有 0.4 到 0.5被判定为陌生人。原因九成情况是对齐环节出了问题。RetinaFace 返回的五点关键点如果顺序搞错或者estimateAffinePartial2D计算出的变换矩阵有误对齐后的人脸是歪的FaceNet 提取的特征自然对不上。另一个原因是注册照片和抓拍照片的拍摄条件差异太大比如注册用的是精修证件照抓拍是低分辨率摄像头特征分布偏移。解决先把对齐后的人脸图保存下来肉眼检查确认眼睛在水平线上、人脸居中。如果对齐没问题检查注册照片的质量尽量用和签到终端同款摄像头拍的照片做注册。阈值方面如果确认对齐无误但相似度还是偏低可以把阈值从 0.65 降到 0.55但每降 0.05 都要用测试集验证认假率是否可接受。4.3 CPU 占用飙到 100%模型加载和推理的优化点现象签到终端运行几分钟后风扇狂转识别延迟从 100 毫秒涨到 1 秒以上。原因RetinaFace 和 FaceNet 默认都用 PyTorch 的 CPU 推理如果每次识别都重新加载模型或者没有设置torch.no_grad()计算图会不断累积内存和 CPU 都扛不住。另一个原因是输入图像没有做尺寸限制4K 图直接送进网络推理时间成倍增加。解决模型在程序启动时加载一次全局复用。推理时务必包在torch.no_grad()里。输入图像先缩放到长边 1280 以内。如果还是慢可以把 RetinaFace 换成 MobileNet0.25 版本FaceNet 用InceptionResnetV1的classifyFalse模式只输出嵌入不做分类。另外OpenCV 的cv2.setNumThreads(4)可以限制线程数避免和 PyTorch 抢 CPU。4.4 多人同时出现在画面里检测框排序和去重现象签到台前同时站了三个人系统只识别到一个人或者把三个人的特征混在一起。原因detect_faces返回多个人脸时代码里用max(faces.values(), keylambda x: x[score])只取了置信度最高的一个。如果三个人里有一个侧脸导致置信度低系统可能识别到的是旁边的人。另外如果两个人脸框重叠严重RetinaFace 可能返回两个高度重叠的框对应同一个人。解决多人场景下应该对每个人脸框分别做识别然后取相似度最高的那个作为签到结果。如果多个人都超过阈值说明画面里有多个人同时签到可以都记录或者提示「请一人上前」。去重方面用 NMS非极大值抑制对检测框做后处理IoU 阈值设 0.4 到 0.5把重叠框合并。4.5 数据库写入冲突多终端并发时的锁和重试现象两台签到终端同时工作偶尔出现签到记录丢失或重复。原因如果两台终端都直接写同一个 SQLite 文件SQLite 的写锁会导致其中一个终端的写入失败。如果用的是 MySQL默认隔离级别下并发插入一般没问题但如果 Java 侧有「先查后插」的逻辑两个请求同时查到「不存在」然后同时插入就会产生重复记录。解决SQLite 场景下要么改用 WAL 模式提高并发写能力要么让一台终端做主库另一台通过 HTTP 接口写入。MySQL 场景下给(meeting_id, name, timestamp)加唯一索引插入时用INSERT IGNORE或ON DUPLICATE KEY UPDATE让数据库层面保证幂等。Java 侧的「先查后插」改成「直接插入 捕获唯一键冲突异常」减少竞态窗口。5. 把识别准确率再往上推一档特征归一化与阈值自适应前面讲的都是「能跑通」这一章讲「跑得稳」。人脸识别系统上线后最常收到的反馈不是「识别不了」而是「有时候能识别有时候不能」。这种玄学问题的根源八成在特征归一化和阈值策略上。先讲特征归一化。FaceNet 输出的 512 维向量不同照片的模长差异可能很大。注册照片光线好、人脸大模长可能是 20 到 30抓拍照片光线差、人脸小模长可能只有 10 到 15。如果直接算余弦相似度模长的影响被消除了这是好事。但如果你用的是欧氏距离模长差异会直接反映到距离上导致同一个人在不同条件下的距离波动很大。所以无论用哪种距离度量我都建议先对特征向量做 L2 归一化把模长统一到 1。这一步在FaceGallery.add和search里已经做了但很多人自己写比对逻辑时会漏掉。再讲阈值自适应。固定阈值 0.65 在大多数场景下能用但遇到以下情况会翻车会议现场光线比注册时暗很多所有人的相似度整体下移 0.05 到 0.1固定阈值会导致大量拒真。解决办法是用「相对阈值」每次识别时不仅看最高相似度是否超过绝对阈值还看最高相似度和第二高相似度的差值。如果最高分是 0.62第二高是 0.35差值 0.27 很大说明最高分对应的那个人大概率是对的可以放宽绝对阈值到 0.55。如果最高分 0.62第二高 0.58差值只有 0.04说明系统分不清这两个人应该拒绝识别并提示「请靠近摄像头再试一次」。def adaptive_recognize(embedding, gallery, abs_threshold0.65, margin0.1): 自适应阈值识别 - 如果 top1 相似度 abs_threshold直接返回 - 如果 top1 和 top2 的差值 margin且 top1 abs_threshold - 0.1也返回 - 否则返回 None提示重试 results gallery.search(embedding, top_k2) if not results: return None, 0.0 top1_name, top1_sim results[0] if top1_sim abs_threshold: return top1_name, top1_sim if len(results) 2: top2_sim results[1][1] if top1_sim - top2_sim margin and top1_sim abs_threshold - 0.1: return top1_name, top1_sim return None, top1_sim这个adaptive_recognize函数的核心思想是当绝对相似度不够高时看相对差距。如果 top1 明显领先 top2说明系统对 top1 的身份有足够信心可以适当放宽绝对阈值。如果 top1 和 top2 咬得很紧说明系统在两个人之间犹豫这时候宁可让用户重试也不要冒险认错。margin参数设 0.1 是个经验值。设太小如 0.05区分度不够设太大如 0.2很多本来能识别的场景会被拒绝。实际调参时用一批标注好的测试数据画出 top1-top2 差值的分布取类间和类内分布的交界处。还有一个进阶技巧多帧投票。签到终端是连续视频流同一个人会在多帧里出现。与其只取一帧的识别结果不如对连续 5 到 10 帧分别识别取出现次数最多的姓名作为最终结果。这能把单帧的随机误差平均掉准确率通常能提升 3 到 5 个百分点。代价是延迟增加从单帧的 100 毫秒变成 500 到 1000 毫秒但对于签到场景用户站在摄像头前等半秒到一秒是可以接受的。from collections import Counter class MultiFrameVoter: def __init__(self, window7, min_votes4): self.window window self.min_votes min_votes self.buffer [] def add_result(self, name, sim): self.buffer.append((name, sim)) if len(self.buffer) self.window: self.buffer.pop(0) def get_decision(self): if len(self.buffer) self.window: return None, 0.0 names [n for n, _ in self.buffer if n is not None] if not names: return None, 0.0 counter Counter(names) most_common, count counter.most_common(1)[0] if count self.min_votes: avg_sim np.mean([s for n, s in self.buffer if n most_common]) return most_common, avg_sim return None, 0.0MultiFrameVoter维护一个长度为 7 的滑动窗口每次识别结果入队窗口满后统计出现次数最多的姓名。如果某个姓名出现至少 4 次超过半数就判定签到成功相似度取该姓名所有帧的平均值。window和min_votes可以根据实际帧率调整帧率越高窗口可以越大。最后说一个我自己的习惯每次部署新场景前先跑一轮「影子测试」。让系统正常识别但不写数据库只把识别结果和人工判断的结果都记下来跑一天后对比。如果准确率低于 95%先调阈值和对齐不要急着上线。这个习惯帮我避免了好几次「上线当天被投诉」的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表