需求很朴素:一台放在办公区、24 小时开机的 Windows 主控机,别人走过来动它的时候能"认人"。不是要金融级生物识别,只是挡住顺手牵羊——所以设计取向从一开始就不是"更准",而是简单、fail-closed(判不出来就当拒绝)、先观察再拦截。
难度不在人脸模型。OpenCV 自带 YuNet + SFace 就够用,装完就能跑。真正耗时间的是后半程:怎么让它在 Windows 上"活着"、怎么让它在 SSH 断开后不死、怎么标定一个不会把自己挡在门外的阈值。这篇把可复用的坑按顺序摊开。
整条链路只有两个进程,刻意做小,为的是"每一段都能单独验":
摄像头(独占) ──► face_gate_server.py :8899 (YuNet 检测 + SFace 128 维比对)
│ GET /health 免鉴权(给看门狗用)
│ GET /whoami → 返回判定(核心,被调用方轮询)
│ POST /unlock → 应急口令换 30 分钟放行
│ POST /setmode → monitor / enforce / off
▲
主控机 gate_client.py ── 轮询显示判定;判不出时 getpass 问口令
采集骨架没有重写,直接照搬团队已在展厅迎宾机上验证过的 CamHub 做法:单采集线程独占设备 + 黑帧检测 + 单实例端口守卫 + 崩溃自动重开。这里有一条容易被忽略的硬约束:
用的都是现成件,不重造:
| 文件 | 说明 |
|---|---|
yunet.onnx(227 KB) | 人脸检测,cv2.FaceDetectorYN |
sface.onnx(38,696,353 B) | 特征提取,输出 128 维(不是 512,别按 512 写缓存) |
face_template.npy | 本人模板,np.load(...).reshape(-1) 后 L2 归一化 |
阈值怎么来的?用留出法:本人 50 帧多姿态样本,实测相似度区间 0.4883 ~ 0.8743,均值 0.7717。关键发现在下限:单帧最低只有 0.4883,也就是说你一侧头,分数就掉下去。所以判定绝不能只看一帧:
判定: >=0.45 放行 / 0.35~0.45 灰区静默 / <0.35 拒绝 多帧取最优: 首帧不过 → 1.6 秒内最多再判 5 帧,取最高分那一帧 fail-closed: 无脸 / 黑帧 / 多脸 / 脸太小 / 模型错 / 摄像头坏 → 一律 deny
SFace 这块还有两个必踩的 API 细节:
FaceRecognizerSF.alignCrop()。自己实现对齐会让人脸畸变,相似度直接崩坏——这不是"优化项",是"能不能用"的问题。feature(),5.0 仍可用;距离常量从 FR_COSINE_SIMILARITY 改名成了 FR_COSINE。另外 detect() 之前必须 setInputSize((w,h)),否则检测结果不稳。用 Start-Process 或 cmd /c start 起的 python,SSH 一断就死——因为子进程属于该会话的 Job Object。正解只有一个:用 NSSM 装成真正的 Windows 服务(见下一节)。这也是为什么"远程调试时一切正常、人一走服务就没了"。
schtasks /run 静默失败在 SSH 里手动触发计划任务,报错 错误: 不支持从虚终端会话启动任务,但 返回码却是 0。如果你只看 rc 判成功,会得到一个"任务跑了但什么都没发生"的幽灵故障。别用 SSH 去触发计划任务。
远程执行 .ps1,输出中文必乱码,某些情况下直接报语法错。对策:远程脚本一律纯 ASCII 输出——状态行写 STATUS / RESULT 这种英文标记,中文留在日志文件里用 python 读。
Expand-Archive / Invoke-WebRequest 会炸Write-Progress 会抛 ReadConsoleOutput,而且可能把下载截断(静默的半包)。对策:解压用 python zipfile、下载用 python urllib,文件一律先 SFTP 推上去再执行,别在远程 shell 里嵌套引号拼命令。
models/ 子目录路径必须做 fallback 解析,否则直接 RuntimeError: 缺文件,而 fail-closed 设计会让你只看到"不启动"、看不到原因。写清楚启动日志里"我找了哪几个路径"。
LogonUser机器是自动登录账户,第二道防线如果挂在系统口令上会直接失效。改成门禁自己认的应急口令:salt(16B) + pbkdf2_hmac('sha256', pwd, salt, 200000),只存 hash,比对用 hmac.compare_digest,与系统账户密码彻底解耦;POST /unlock 走请求体,不进 URL、不进访问日志。错 5 次锁 10 分钟。
nssm install FaceGate D:\facegate\py\python.exe nssm set FaceGate AppParameters "face_gate_server.py 8899 0 1280 720 --log=..\logs\gate.log" nssm set FaceGate AppDirectory D:\facegate nssm set FaceGate Start SERVICE_AUTO_START nssm set FaceGate AppRestartDelay 5000 nssm set FaceGate AppStdout ..\logs\svc_stdout.log nssm set FaceGate AppStderr ..\logs\svc_stderr.log nssm start FaceGate nssm status FaceGate # SERVICE_RUNNING 即成功
AppRestartDelay / AppStdout / AppStderr。写成 RestartDelay / stdout 会报 Invalid parameter——它不猜你的意图。另外机器是 x64,务必用 win64\nssm.exe。Python 环境用的是绿色版(3.11.9 + cv2 5.0.0),不写注册表、删目录即恢复,这对"随时能退场"的部署很关键。
| 模式 | 行为 | 用途 |
|---|---|---|
monitor(默认) | 只记日志不拦截,逐条写 logs/decision.csv | 标定期,收集真实分布 |
enforce | 真拦截,非本人静默拒绝 | 确认阈值不会误伤后切换 |
off | 完全不判 | 应急停门禁 |
在线切换:POST /setmode {"mode":"enforce"},立即生效并写回 config.json。
上线流程(别跳):先 monitor 跑几天 → 翻 logs/decision.csv 看本人相似度分布和误伤率 → 确认 0.45 这个阈值不会把自己挡在门外 → 再切 enforce。config.json 里还能配 allow_ips 白名单(默认含 127.0.0.1 与两台主控机)。
人脸判不清时要有后手。我们做了两套,都能换 30 分钟放行(reason 统一记 admin_passphrase):
getpass 隐藏输入,口令不进对话记录。vault.json 一条 = {qid, q, salt_hex, hash_hex},答案同样只存 PBKDF2 hash,归一化 = 去空白 + 转小写;错 3 次提示、连错 5 次锁 10 分钟;题库为空时返回 vault_empty,客户端直接提示去录题,不会误判成故障。vault.json 与 gate_admin.bin 是无密码账户机器上的唯一后门,重装系统前必须单独备份,别跟着工作目录一起格式化掉。我们把旧机器上的 50 帧训练集和 8 张演示抓帧归档了,本意是"以后识别不准时拿它当参考"。实测发现这个想法是错的:
归档帧是同一时刻、同一姿态拍的,它们彼此的相似度中位数高达 0.90;但新鲜帧对上去只有 0.49~0.69。拿"内部自洽的旧样本"当基准,只会让你看到一片高分,然后得出错误结论。
所以识别突然不准时,正确优先级是:
decision.csv 的真实分布再调,别盲调;重建模板的脚本支持留一法算平均模板与相似度分布,直接给出可落地的阈值建议,并可选 --apply 覆盖线上模板。远程跑脚本有一条铁律:先 SFTP 推最新副本再执行,否则跑的是旧版;且 cmd /c 的控制台输出是 GBK 乱码,把 stdout 重定向到文件再 SFTP 读回来。
| 机器 | 单帧耗时 |
|---|---|
| 主控机(i9-13900HX) | 约 39 ms/帧 |
| 旧办公机(i5-3470) | 100~120 ms/帧 |
验收只看三个信号,不靠"感觉好像行":
GET /health → engine_ok:true 且 camera:"cam0 1280x720";GET /whoami 对着本人 → decision:allow;no_face 后静默(不刷错误、不弹窗)。纯锁屏式门禁有个体验问题:正常干活也要被拦。更合理的方向是按操作敏感度分级放行——做 PPT、剪视频、查文档正常放行;涉及复制数据库、动凭证库、外发文件才触发人脸门禁。我们落了两件东西:
sensitive.json:any_path / any_cmd 子串命中即敏感,默认 normal;命中则要求本人在场证明,一次通过管 30 分钟(缓存),非交互环境下的敏感操作一律拒绝,绝不默认放行,全部动作写 guard_audit.csv。.dump、客户数据目录)而不是动作词("导出""复制"),调用方必须传真实操作路径而不是一句话描述。门禁永远只是第二道,第一道还是目录权限、加密和网络隔离。我们站内在售的苏州话标准词库(3550 条)与数字人素材,是完全不同的东西,但方法一样——先把基线标定出来,再谈效果。如果你也在做无人值守终端、访问控制或数字人落地,带着具体链路问题来找我们更高效。
看看苏州话标准词库(含免费样本)→ 终端/数字人定制服务 →因为它是从 SSH 会话的 Job Object 里派生的子进程,会话结束就被回收。Start-Process、cmd /c start 都逃不掉。正解是用 NSSM(或同等工具)把它装成真正的 Windows 服务,Start SERVICE_AUTO_START + AppRestartDelay 5000 实现开机自启与崩溃自愈。
schtasks /run 明明返回 0,任务却没跑,为什么?虚终端会话不支持启动计划任务,PowerShell/CMD 会报"不支持从虚终端会话启动任务",但返回码仍是 0。判断成功不能只看 rc,要去看任务实际产生的副作用(日志文件、输出文件)。
PowerShell 5.1 会把 UTF-8 当 GBK 处理。最省事的办法是远程脚本只输出纯 ASCII(STATUS/RESULT 之类的英文标记),中文写进日志文件再由 python 以 utf-8 读取。不要试图在远程 shell 里靠 chcp 打补丁。
没有通用值,必须自己标定。我们的实测是本人 50 帧多姿态 0.4883~0.8743、均值 0.7717,取 >=0.45 放行 / 0.35~0.45 灰区 / <0.35 拒绝,并且强制多帧取最优(1.6 秒内最多再判 5 帧)。单帧最低 0.4883 说明侧头就掉分,只看一帧必然误伤。
因为归档样本是同一时刻、同一姿态连拍的,彼此高度自洽,用它当参考基准没有意义。识别不准时的正确顺序是:在当前光照姿态下重新采 30 帧再注册 > 依据 decision.csv 分布放宽阈值 > 私密问答兜底。
不会。非交互环境下的敏感操作一律拒绝,绝不默认放行——这是 fail-closed 原则的直接推论。这也是为什么需要 monitor 观察期先把误伤率摸清楚。
不能,也不该这么定位。它的设计目的是挡顺手牵羊与误操作。sensitive.json 是文本规则,换说法或换路径可能绕过;真正的第一道防线是目录权限、加密与网络隔离,门禁只是第二道。