前阵子接手了一台10.1寸的安卓刷脸终端,需求是在设备上完成人脸识别门禁,但团队里没有Android原生开发人力,UI和逻辑全是Web技术栈。最后落地的方式就是在物联网浏览器(IoTBrowser)里用JavaScript把整条人脸识别链路跑通:摄像头采集、画面预览、人脸检测、特征提取、底库比对、闸机联动,全部在这个Web页面里完成。这篇文章就是这次实战的完整复盘,包括方案取舍、核心代码、性能调优思路和一批实打实的坑。想用纯前端技术在边缘设备上做刷脸功能的朋友,可以参考这条路线。
1. 为什么把刷脸逻辑放进浏览器:选型考量和能力边界
1.1 浏览器刷脸的底层依赖:摄像头、GPU与本地推理
先回答一个很多人会问的问题:物联网浏览器凭什么能做人脸识别?传统印象里浏览器是个“显示壳子”,但IoTBrowser这类定制浏览器和普通浏览器的本质区别在于,它把设备的硬件能力通过原生桥接注入了Web层。摄像头取流、GPIO输出、串口读写这些能力,页面里的JS调用一段桥接API就能拿到。用人脸识别真正依赖的三样东西——摄像头帧数据、GPU矩阵运算能力、本地推理运行时——在IoTBrowser里都有对应的JS入口。
摄像头对应 navigator.mediaDevices.getUserMedia,这是W3C标准API,Chromium内核的IoTBrowser普遍支持;GPU对应WebGL,TensorFlow.js等推理库依赖它做并行矩阵运算;本地推理则通过WebAssembly和WebGL的组合实现,TinyFaceDetector、FaceRecognitionNet这类模型可以在浏览器离线跑。这三样凑齐了,人脸识别就有戏。
需要先确认你手上的IoTBrowser是不是Chromium内核。目前市面上面向商业交互终端、智能门禁、智慧屏的IoTBrowser,绝大多数是基于Chromium二次开发的,支持ES6、WebGL和WebRTC。如果还停留在老式WebView,那后面讲的方案基本跑不动。
1.2 两条技术路线:本地推理与端云协同
有了浏览器这个环境,人脸识别在架构上就分出了两条路:本地推理和端云协同。
本地推理是指模型文件直接部署在设备上,检测和特征提取全部在浏览器内用JS完成。实时性最好,不依赖网络,人脸特征不出设备,隐私合规上压力小。代价是设备的CPU和GPU要扛得住推理负载,对低端设备不友好。
端云协同则是由浏览器采集摄像头画面,把帧上传到服务端的人脸识别服务,识别完成后回传结果。IoTBrowser只负责“看”,不负责“想”。好处是设备端算力要求低、模型更新灵活,坏处是延迟受网络影响大,而且人脸数据要过网,隐私和数据合规就得费心思处理。
实际项目里还有第三种折中:前端做检测(框出人脸位置),后端做比对(验证身份)。因为检测模型通常比识别模型轻量得多,前端用TinyFaceDetector找脸成本很低,后端只处理裁剪后的人脸小图,云侧压力也小。
我这次的项目由于门禁场景要求断网可用,而且设备主板带一颗不错的GPU,最终选了纯本地推理方案。
1.3 什么场景适合IoTBrowser+JS,什么场景建议换原生
聊点掏心窝的话。IoTBrowser+JS这套组合是有适用边界的,不能什么项目都往上套。
适合的场景有:交互式刷脸终端(门禁机、访客机)、需要频繁改UI的智能设备、家电面板、物流柜、充电桩这类“业务逻辑在云端、终端做人机交互”的产品。这些产品UI迭代速度快,用Web开发改版成本低,OTA升级只推HTML/CSS/JS文件就行,不用走固件包升级。人脸识别在这里是“功能之一”,不是全部。
不适合的场景要认清:高安全级别的支付级刷脸(金融合规要求极高,浏览器环境很难过检测)、超低端硬件(主控是单核Cortex-A53级别、没有GPU的板子,JS推理大概率卡成幻灯片)、以及需要极低功耗的电池设备。这些场景老老实实用C/C++或者厂商SDK写原生逻辑,别拿浏览器硬扛。
判断标准很简单:看你的设备能不能稳定跑起WebGL,以及有没有至少1GB可用内存给浏览器进程。达不到这两个条件,后面所有优化都是空中楼阁。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 打通摄像头的第一公里:设备调试与画面采集
2.1 IoTBrowser如何把硬件能力暴露给JS
拿到开发板的第一件事不是写人脸识别,而是先确认摄像头能不能在浏览器里正常出画面。IoTBrowser的硬件能力一般通过两种方式暴露:一是标准的WebRTC接口,直接支持getUserMedia;二是厂商自定义的桥接对象,常见的是在window下面注入一个IoTBridge或hardware对象,里面有开灯、继电器控制、读取串口这类私有API。
人脸识别主要用的是前者。如果navigator.mediaDevices在设备上返回undefined,大概率是IoTBrowser的WebRTC模块被裁剪掉了,这时候只能联系厂商要带WebRTC的版本,或者走厂商自定义的视频预览接口。我碰到过一台设备,标准接口拿不到视频流,但厂商暴露了一个startPreview(videoEl, options)的桥接API,把YUV数据直接画到指定元素上,这条路也能走通。
2.2 摄像头采集的JS骨架
假设IoTBrowser支持标准接口,采集代码比想象中简单,但有几个参数必须显式设置:
javascript复制async function initCamera(videoElement, width = 1280, height = 720) {
// Android设备上preferredCamera能避免每次刷新都挑错镜头
const cameras = await navigator.mediaDevices.enumerateDevices();
const faceCamera = cameras.find(d => d.kind === 'videoinput' && d.label.includes('front'));
const stream = await navigator.mediaDevices.getUserMedia({
video: {
deviceId: faceCamera ? { exact: faceCamera.deviceId } : undefined,
width: { ideal: width },
height: { ideal: height },
frameRate: { ideal: 15, max: 20 },
facingMode: 'user'
},
audio: false
});
videoElement.srcObject = stream;
await videoElement.play();
return stream;
}
几个容易踩的细节:
-
分辨率别贪高。很多开发者默认用1920x1080采集,以为画质越高识别越准。实际上人脸检测模型输入分辨率一般是640x480甚至更低,高分辨率画面缩放后并不会带来精度提升,反而增加CPU解码负担。720p足够。
-
帧率设置15fps左右就够了。人脸检测不需要每秒30帧,门禁场景一般要求识别速度在1秒以内,15fps的帧流配合每2-3帧做一次检测,体验完全够。
-
摄像头镜像问题。门禁机屏幕如果显示的是镜像画面,检测框和画面位置会相反。检测完要不要翻转坐标,取决于你的视频元素是否被CSS做了
transform: scaleX(-1),判断逻辑要和UI层的镜像设置保持一致。
2.3 网络调试与页面加载策略
设备端调试最烦的是每次改代码都要重新打包安装。我在这个项目里搭了三层调试通道:
第一层是IoTBrowser自带的远程调试,一般通过adb forward把浏览器调试端口映射到电脑,然后用Chrome的chrome://inspect连上去,可以看console和DOM,和开发普通网页一样。
第二层是本地静态服务器。把整个前端工程放在设备SD卡里,用Python起一个http.server,IoTBrowser加载http://127.0.0.1:8080/,改完文件不用重启App,刷新页面就生效。后期还可以在局域网里直接从电脑上访问设备IP加载页面,开发效率提升明显。
第三层是vendor提供的远程配置接口。量产机的IoTBrowser通常支持云端下发一个URL作为首页,把设备指向测试服务器地址,就能远程加载新版本页面。但这一步要谨慎,一旦页面挂了,设备可能变砖,建议保留本地默认页作为兜底,配置里加一个加载失败自动reload本地的逻辑。
3. 浏览器内的人脸检测与特征提取:从模型选型到逐帧管线
3.1 在IoTBrowser上真正跑得动的几种方案
有人脸识别需求的JS生态,能选的无非这几种,我逐一跑过,做个客观对比:
| 方案 | 检测能力 | 特征提取 | 模型体积 | 设备实测感受 |
|---|---|---|---|---|
| face-api.js | TinyFaceDetector | FaceRecognitionNet | 加起来约6MB | 最省事,API封装好,适合快速验证,但底层tfjs版本较旧,WebGL适配偶有坑 |
| MediaPipe FaceMesh / FaceDetection | BlazeFace系列 | —(需自行接比对) | 约2MB-8MB | 精度和速度都很稳,输出468个关键点,做活体检测有天然优势 |
| TensorFlow.js + BlazeFace | BlazeFace | 自行加载识别模型 | 可拆得很细 | 自由度最高,可以自己拼装检测和识别模型,但代码量大 |
| OpenCV.js + Haar级联 | 传统方法 | 不适用 | 约8MB | 检测率感人,逆光几乎不可用,不建议用在门禁 |
我最后选的是face-api.js做主体,理由是这个项目要同时输出人脸框、关键点、特征向量三样东西,face-api.js的API设计几乎就是照这个需求来的。虽然社区一直有人诟病它维护不活跃,但用在稳定的IoTBrowser内核上,问题不大。
3.2 模型加载与初始化:把内存吃满的问题压下去
face-api.js用起来最需要注意的不是识别本身,而是模型加载。首次进入页面直接把六个模型全加载,内存瞬间飙到300MB以上,低配设备直接卡死。
经过压测,我只加载三个必要的模型:
javascript复制import * as faceapi from 'face-api.js';
// 只加载3个模型,人脸框、68关键点、识别特征
const MODEL_URL = '/models';
await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL),
faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL)
]);
Init完成后,关键一步是设置faceapi.env.monkeyPatch来做环境兼容。IoTBrowser里有些API行为不标准,比如Canvas拿不到2D context,需要手动patch一下:
javascript复制faceapi.env.monkeyPatch({
Canvas: HTMLCanvasElement,
Image: HTMLImageElement,
ImageData: ImageData,
Video: HTMLVideoElement,
createCanvasElement: () => document.createElement('canvas'),
createImageElement: () => document.createElement('img')
});
这个patch在部分IoTBrowser上是“不补必崩”的程度。第一次在设备上跑demo时,代码在PC Chrome一切正常,上了设备就报getContext is not a function,排查了一圈就是这里缺了patch。
内存控制还有一个实用经验:模型加载后建议立刻做一次tf.engine().startScope()配合显式dispose管理张量,避免特征提取的中间张量堆积。我在设备上对比过,不主动释放张量的版本,跑2小时后内存增加150MB,明显泄漏。
3.3 逐帧识别的管线实现
整个识别管线在代码层是一个持续运行的任务循环,核心逻辑是:从视频帧取图,送入检测模型,框出人脸并提取特征,再和底库比对。实现上我用了requestAnimationFrame加节流,而不是setInterval。原因是RAF由GPU刷新同步触发,可以避免掉帧时重复执行无用检测:
javascript复制let detecting = false;
let lastDetectTime = 0;
const DETECT_INTERVAL = 100; // 100ms检测一次
async function detectLoop(timestamp) {
if (!detecting && timestamp - lastDetectTime >= DETECT_INTERVAL) {
detecting = true;
try {
const result = await detectFaceFromVideo(videoEl);
handleDetectResult(result);
} finally {
detecting = false;
lastDetectTime = timestamp;
}
}
requestAnimationFrame(detectLoop);
}
注意detecting标志位。人脸识别是一个异步过程,如果上一帧还没算完就启动下一帧,会在设备上堆积大量未完成的Promise,最终把WebGL上下文拖垮。这块我在开发初期吃过亏,设备运行20分钟后就黑屏,查到最后是异步检测任务堆积导致的GPU崩溃。
真正执行检测的函数是这个:
javascript复制async function detectFaceFromVideo(video) {
// useTiny: 用轻量检测器
// 每帧只取单张脸,门禁场景优先识别最大的人脸
const detections = await faceapi
.detectAllFaces(video, new faceapi.TinyFaceDetectorOptions({
inputSize: 320, // 输入尺寸320,识别速度和精度平衡点
scoreThreshold: 0.5
}))
.withFaceLandmarks()
.withFaceDescriptors();
if (!detections.length) return null;
// 按人脸框面积排序,取最大的人脸
detections.sort((a, b) => {
const areaA = a.detection.box.width * a.detection.box.height;
const areaB = b.detection.box.width * b.detection.box.height;
return areaB - areaA;
});
return detections[0];
}
3.4 特征比对与阈值:误识和拒识的平衡
拿到FaceDescriptor(128维特征向量)后,底库比对用欧氏距离。face-api.js和face-recognition.js的模型输出都是L2归一化后的128维向量,理论值范围在0到2之间,距离越小越像。我一开始设的阈值是0.4,上线后误识率低了但拒识率偏高,用户在光线差一点的地方就刷不上脸,后来逐步放宽到0.55。
0.55这个值是拿设备实测200组数据调出来的,不同设备的摄像头感光芯片差异很大,建议量产前在目标硬件上跑一批真实人脸样本,画一下正负样本的距离分布曲线,取两类分布交点作为初始阈值。不要盲目抄网上的经验值。
底库比对都是线性扫描,底库小于1000人时性能没问题,超过这个量就要考虑用KDTree或者改向量数据库。门禁设备的底库一般几十到几百人,线性扫描完全够。
javascript复制function compareDescriptors(targetDescriptor, dbDescriptors) {
let bestMatch = null;
let bestDistance = Infinity;
for (const person of dbDescriptors) {
const distance = faceapi.euclideanDistance(targetDescriptor, person.descriptor);
if (distance < bestDistance) {
bestDistance = distance;
bestMatch = person;
}
}
return {
person: bestMatch,
distance: bestDistance,
accepted: bestDistance < MATCH_THRESHOLD // 0.55
};
}
比对的另一个陷阱是特征向量的“新鲜度”。摄像头画面中的人脸角度偏转超过30度时,提取到的特征向量会和正脸差距很大。我加了质量判断:detection.detection.score低于0.6就不做比对,直接提示“请面向屏幕”,而不是硬着头皮比对后误拒。
4. 识别事件之后的业务联动:门禁、考勤与陌生人告警
4.1 把“识别成功”交还给设备主控
人脸识别只是第一步,刷脸之后要干的活才是业务核心。门禁场景里,识别成功要开闸机;考勤场景要记录打卡;访客场景要联动后台通知接待人。IoTBrowser页面和硬件主控之间的通信,通常走两条路:全局WebSocket/MQTT和厂商桥接API。
MQTT是我们项目的主力通信方式,好处是设备端逻辑和云端解耦。识别成功后的通知逻辑大概这样:
javascript复制function notifyAccessApproved(person, confidence) {
// 通过IoTBridge调用原生MQTT发送消息到主控
if (window.IoTBridge && IoTBridge.mqttPublish) {
IoTBridge.mqttPublish('device/access/command', JSON.stringify({
action: 'open_door',
personId: person.id,
personName: person.name,
confidence: confidence,
timestamp: Date.now()
}));
}
}
如果你用的桥接API没有MQTT能力,还有一个更简单的方案:在页面里直接向设备的本地服务发HTTP请求。很多门禁主控板会暴露一个局域网HTTP接口,比如http://127.0.0.1:8080/openDoor,页面里fetch一下就能触发开锁。注意要用TCP回环地址而不是局域网IP,避免路由问题。
识别失败的联动同样重要。我在系统里加了陌生人告警逻辑:连续3次检测到人脸但比对不通过,页面截取一张现场照片,通过桥接API存到设备本地目录,同时上报云端,方便事后排查。
4.2 活体检测:防照片和视频攻击的折中做法
人脸识别门禁最怕的就是一张A4纸打印照片刷开闸机。纯浏览器环境下做3D结构光活体检测不现实(需要专用深度摄像头),但做2D活体检测是可行的,有两条路:
一是行为活体,让用户按照屏幕提示眨眼、张嘴。这个用face-api.js的68个关键点就能实现,计算眼睛纵横比(EAR值)判断眨眼:
javascript复制function getEyeAspectRatio(landmarks) {
const leftEye = landmarks.positions.slice(36, 42);
const rightEye = landmarks.positions.slice(42, 48);
const ear = (eye) => {
const A = dist(eye[1], eye[5]);
const B = dist(eye[2], eye[4]);
const C = dist(eye[0], eye[3]);
return (A + B) / (2 * C);
};
return (ear(leftEye) + ear(rightEye)) / 2;
}
EAR值正常范围在0.2到0.35之间,眨眼时EAR值会快速跌落到0.15以下再恢复,捕捉到这个下降沿就可以判定是活体。照片攻击时EAR值恒定不变,直接否决。
二是红外活体,IoTBrowser如果接的是支持红外/可见光双通道的摄像头,可以通过切换光源或者读取红外帧来判断画面是否有真实深度信息。但这种方案依赖特定硬件,通用性不强,我这个项目里用的是行为活体加识别的组合。
4.3 人脸框与状态机:UI不能拖后腿
识别过程中UI层的体验直接影响用户会不会站在机器前不知所措。我做了三个阶段的状态切换:
待机状态:屏幕上不画人脸框,只显示半透明的检测区域提示“请面向屏幕”。这个阶段检测到人脸后,播放一个短促的“嘀”声提示开始识别。
识别中状态:检测框变成绿色,加上一个转圈动画。动画不要用CSS动画,因为人脸识别跑起来后CPU占用率已经很高了,CSS动画在低端设备上会掉帧。我用Canvas每帧重绘替代:
javascript复制function drawDetectionBox(ctx, box, label, color = '#00FF66') {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// 绘制人脸框
ctx.strokeStyle = color;
ctx.lineWidth = 4;
ctx.strokeRect(box.x, box.y, box.width, box.height);
// 绘制标签
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(box.x, box.y - 30, box.width, 30);
ctx.fillStyle = '#FFFFFF';
ctx.font = '16px sans-serif';
ctx.fillText(label, box.x + 8, box.y - 8);
}
状态机用简单的enum实现,识别结果的每个流转都打日志,上线后出了问题能快速从日志定位是检测失败还是比对失败还是设备联动失败:
javascript复制const FaceStatus = {
IDLE: 'idle',
DETECTING: 'detecting',
RECOGNIZING: 'recognizing',
PASSED: 'passed',
REJECTED: 'rejected'
};
5. 真实设备上的性能调优与排坑记录
5.1 掉帧不等于算力不足:先查GPU上下文
项目跑到第二周,现场反馈设备每天下午开始变卡,刷脸从1秒变成3秒。第一反应是CPU算力瓶颈,上去一看CPU占用才40%,但页面交互已经明显掉帧。
后来发现是WebGL上下文的问题。IoTBrowser的GPU进程在长时间运行后,如果页面频繁创建Canvas而不释放,GPU显存会被吃光,WebGL上下文直接丢失。表现就是页面还活着,但所有绘制都不刷新。
解决方案是全局只维护一个Canvas实例,每次绘制前先检查webgl.getContextAttributes()是否正常返回,如果拿不到上下文就主动重建。另外在页面可见性变化时,要销毁旧的GPU资源:
javascript复制document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 页面切后台时释放GPU资源
tf.engine().disposeVariables();
tf.engine().reset();
}
});
5.2 内存泄漏:Canvas和Float32Array的隐形膨胀
前面提到过特征向量比对是线性扫描,但还有一个更隐蔽的内存泄漏点:每帧人脸检测都会产生大量的Float32Array中间对象,这些对象如果被某个全局数组引用着不释放,内存会线性增长。
排查方式很朴素,在设备上跑一个半小时,用IoTBrowser的开发者工具Memory面板抓三次heap snapshot,对比增加的对象类型。我抓下来发现是FaceDescriptor数组一直在追加,原因是每次比对成功后人脸特征被当成“识别记录”保存在内存里,没有写库也没有删除。
修复方法是在识别记录模块里加一个最大长度限制,超出后用环形数组覆盖最旧的数据:
javascript复制class RecognitionRingBuffer {
constructor(maxSize = 100) {
this.buffer = [];
this.maxSize = maxSize;
}
push(item) {
if (this.buffer.length >= this.maxSize) {
this.buffer.shift();
}
this.buffer.push(item);
}
}
5.3 逆光与夜间:预处理比换模型更值
现场反馈最集中的一个问题:傍晚逆光时刷脸率暴跌。门禁机装在楼道入口,人站在门口时背后是天空,脸是黑的,检测模型直接找不到脸。
在人脸上做图像增强是最直接的方案。我一开始尝试用Canvas处理每一帧:先转灰度,再做直方图均衡化,然后把处理后的帧喂给检测模型。这个方法有效但性能开销大,一帧处理要额外消耗30ms,帧率掉了很多。
后来换了个思路:既然检测模型吃的是480p以下的图,那我就在采集层压低曝光补偿。IoTBrowser的getUserMedia支持手动设置曝光和对焦:
javascript复制const constraints = {
video: {
// ...
advanced: [
{ exposureMode: 'manual', exposureCompensation: 2.0 },
{ whiteBalanceMode: 'continuous' },
{ focusMode: 'continuous' }
]
}
};
实测把曝光补偿调到+2.0后,逆光场景的人脸检测率从52%提升到88%。如果设备摄像头不支持曝光调节,再退回到Canvas直方图均衡化方案,两条路都留着,根据机型动态选择。
5.4 首次加载策略:模型文件如何先落地
人脸识别模型文件加一起约6MB,在弱网环境下首次加载会让人等很久。我做了两级优化:
第一级是模型文件本地化。开发阶段模型放在Web服务器上,在生产环境把模型文件直接打包进IoTBrowser的本地资源目录,页面加载时优先读本地文件,绕开网络请求。关键是代码里判断环境的逻辑:
javascript复制const isProduction = location.protocol === 'file:' || location.hostname === '127.0.0.1';
const MODEL_URL = isProduction ? './models' : window.APP_CONFIG.modelUrl;
第二级是页面进入正式识别前先做一次“模型预热”,加载完成后用一张内置的测试图片跑一次推理,确认WebGL和模型都正常再开启摄像头检测。这样做的原因是避免用户已经站到识别区域了,页面还在默默加载模型,第一帧检测直接卡住。
6. 从Demo到量产:模型安全、底库下发与远程排障
6.1 模型防篡改:别拿裸模型文件直接发布
如果产品要量产交付到客户现场,人脸识别模型文件的安全就必须考虑了。裸放在资源目录里的ONNX或JSON权重文件,懂行的人用adb拉取出来就能逆向你的特征提取逻辑。
我采用的办法是把模型文件打进一个加密包,由IoTBrowser在启动时解密后写入内存目录。加密操作走桥接API调用,密钥存在设备安全区里:
javascript复制// 在原生桥接中封装native解密方法
const modelBuffer = await IoTBridge.decryptAsset('face_model.pkg');
const blob = new Blob([modelBuffer], { type: 'application/octet-stream' });
const modelUrl = URL.createObjectURL(blob);
当然有人会说“反正模型在浏览器里跑,必然可以被抓取”,这话没错,但增加解密这一层可以把逆向门槛从“随手拿走”提高到“需要专门设备逆向”,对商业项目来说这个成本投入是值得的。如果项目有更强的安全要求,建议直接放弃纯JS方案,把模型放到原生层执行。
6.2 人脸底库下发:本地增量与云端统一下发
底库管理的核心问题是:人脸数据从哪里来,怎么分发到每台设备上。
云端统一下发适合联网设备,新增一个员工,云端把照片或特征向量推到所有门禁机上。本地增量采集适合不联网场景,管理员在设备本地上传照片,录入到设备本地底库。
我建议在特征向量层面做同步,而不是同步原始照片。原因有两个:一是特征向量体积小,一张人脸照片几百KB,一个128维向量只有不到1KB;二是隐私合规,特征向量属于脱敏数据,业务侧配合刷脸核验,原始照片只在录入时短暂使用,不落库、不出设备。
下发协议用版本号加增量更新的方式。设备保存底库版本号,云端有新的底库时推送一个version字段,设备拉取完整底库替换旧数据,避免增量合并时出现脏数据。
6.3 日志与可观测性:现场问题不再靠猜
量产后的排障难度比Demo阶段大一个数量级。现场设备出现了识别不通过的问题,你不可能跑到现场去连调试端口。所以代码里从第一天就要把日志系统打好。
我在每个关键环节输出结构化日志,包括时间戳、识别阶段、检测分数、距离值、最终决策、设备运行时长。日志统一写入IoTBrowser的本地日志文件,通过桥接API归档,配合一个云端日志上传接口,遇到无法复现的问题时,让现场人员操作触发“上报日志”按钮,数据自动到云端。
日志字段里一定要包含模型版本和设备固件版本。我踩过一个坑,升级底库算法后没有同步更新设备的模型文件,新旧模型的特征向量维度不匹配,所有比对全部失败,表现就是每张脸都识别不出来。如果日志里有模型版本字段,这种问题几分钟就能定位。
6.4 隐私合规:数据就地处理,少碰原始人脸照片
最后聊一个容易忽视但必须放在心上的问题:合规。在国家《个人信息保护法》的框架下,人脸信息属于敏感个人信息,处理人脸数据的合规要求很高。IoTBrowser+JS这套方案有一个先天优势:识别过程全程在设备本地完成,人脸特征不出设备,原始照片只短暂存在于内存帧中,不落盘。这比把摄像头画面都传到云端的方案合规压力小得多。
但“不传到云端”不等于“完全合规”。门禁设备的使用场景要在醒目位置告知用户“进入区域进行人脸识别”,录入底库时要获得个人同意,这些用户告知和授权环节,可以直接做进Web页面流程里。设备端识别日志要定期清理,不要无限积累人脸事件记录。这些都是实际项目中必须落地的细节,不能只盯着识别率。
量产这台设备之后我最大的体会是:在IoTBrowser里用JS做人脸识别,真正难的不是“识别”本身,而是识别之外的整套工程问题——性能怎么压、内存怎么控、模型怎么发、底库怎么管、现场问题怎么排查。把这些想清楚了,浏览器里的刷脸功能,也能成为一台稳定运行的门禁设备核心。最后再分享一个小技巧:如果你手头设备的主控性能吃紧,可以在正式代码里加一个动态帧率降级开关,检测到设备温度过高或者内存不足时,自动把检测间隔从100ms拉长到300ms,识别速度慢一点,但设备不会死机,这个策略在我们现场救过好几次急。
