网页端, 6.5MB人脸识别模型, 谷歌框架, 又快又准

高端的小区往往只需要简单的门禁系统,

简单到什么程度呢?

哈哈, 浏览器加载个小模型就够了。

本demo人像为ai生成, 仅仅用来测试


5行核心代码

bash 复制代码
// demo/4/static/index.html
const r = await faceapi.detectSingleFace(img).withFaceDescriptor()
console.log("128 维特征:", r.descriptor.length)
const d = Math.sqrt([...r.descriptor].reduce((s, v) => s + v * v, 0))
console.log("模长:", d.toFixed(3))

图片选完,浏览器内出 128 维向量。不上服务器,隐私保住。

流程图

bash 复制代码
        上传照片
              │
              ▼
        整图模糊 + 加载圈
              │
              ▼
        检测人脸 + 68 关键点
              │
              ▼
        提 128 维特征
              │
              ▼
        跟白名单比对
        距离 < 0.6 → 开门
        距离 ≥ 0.6 → 拒绝

痛点

公司让做门禁。厂商报价一台闸机几千。后端还要 GPU 服务器。我想,能不能前端搞定?数据不出本机,零成本。调研一圈,找到 face-api.js

1. face-api.js 是什么

face-api.js 基于 TensorFlow.js, 谷歌的深度学习框架。三个模型:

| 模型 | 干什么 | 大小 | | :-- | :-- | :-- | | tinyFaceDetector | 检测人脸 | 190KB | | faceLandmark68Net | 68 关键点 | 90KB | | faceRecognitionNet | 128 维特征 | 6.3MB |

加一起 6.5MB。

2. 模型

在 jsDelivr CDN下载。

bash 复制代码
✓ tiny_face_detector_model-shard1 (190KB)
✓ face_landmark_68_model-shard1 (90KB)
✓ face_recognition_model-shard1 (6.3MB)
✓ face_recognition_model-shard2 (6.3MB)
全部下完,约 6.5MB

模型放 static/models/

3. 起服务

bash 复制代码
// demo/4/server.ts
Bun.serve({ port: 3003, async fetch(req) {
  if (url.pathname === "/api/upload") return uploadProxy(req)
  return new Response(Bun.file(join(dir, "static", url.pathname)))
}})

不配 BACKEND_URL 走 mock,配了转发真实后端。

bash 复制代码
$ bun run dev
  http://localhost:3003

4. 加载模型

bash 复制代码
const MODEL_URL = "./models"
await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL)
await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)
await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL)

3 秒内全加载完。

5. 选图

为了演示方便用上传图片。生产可以换 navigator.mediaDevices.getUserMedia 摄像头流。

bash 复制代码
<input id="fileInput" type="file" accept="image/*" style="display:none" />
<button id="btnSelect">选择人脸照片</button>
bash 复制代码
fileInput.onchange = async (e) => {
  const file = e.target.files[0]
  if (file.size / 1024 / 1024 > 30) return setStatus("超过 30MB", "err")
  if (!file.type.startsWith("image/")) return setStatus("非图片", "err")
  previewImg.src = await fileToBase64(file)
}

限制 30MB,只接 image/*

6. 上传到后端

bash 复制代码
const res = await fetch("/api/upload", {
  method: "POST",
  body: JSON.stringify({ name: file.name, fileBase64: base64 }),
})
return (await res.json()).data

mock 模式回传 base64,real 模式转发后端。

7. 整图模糊 + 加载圈

识别前先做高斯模糊,让人看不出原图。

bash 复制代码
function startLoading() {
  overlayCtx.filter = "blur(14px)"
  overlayCtx.drawImage(previewImg, 0, 0, w, h)
  // 中间画旋转金色弧 + "正在识别人脸..." + elapsed 0.0s
  loadingAnimId = requestAnimationFrame(frame)
}

8. 检测人脸

bash 复制代码
const results = await faceapi
  .detectAllFaces(previewImg, new faceapi.TinyFaceDetectorOptions({
    inputSize: 320, scoreThreshold: 0.5,
  }))
  .withFaceLandmarks()
  .withFaceDescriptors()
console.log(`${results.length} 张脸`)

9. 画 68 关键点(5 色)

按部位上色,远看也分得清。

bash 复制代码
const groups = [
  { from:  0, to: 16, color: "#00e5ff" }, // 下颌
  { from: 17, to: 26, color: "#ffeb3b" }, // 眉
  { from: 27, to: 35, color: "#76ff03" }, // 鼻
  { from: 36, to: 47, color: "#ff1744" }, // 眼
  { from: 48, to: 67, color: "#e040fb" }, // 嘴
]

10. 提取 128 维 + 白名单

bash 复制代码
const d = await faceapi.detectSingleFace(previewImg).withFaceDescriptor()
console.log("维度:", d.descriptor.length)  // 128

localStorage,关页面不丢。

bash 复制代码
const WL_KEY = "tangchen_whitelist_v1"
const whitelist = new Map(JSON.parse(localStorage.getItem(WL_KEY) || "{}"))

btnCapture.onclick = async () => {
  const name = nameInput.value.trim()                  // "张三 2-1602"
  const d = await faceapi.detectSingleFace(previewImg).withFaceDescriptor()
  whitelist.set(name, Array.from(d.descriptor))
  localStorage.setItem(WL_KEY, JSON.stringify(Object.fromEntries(whitelist)))
}

11. 欧氏距离 + 阈值

bash 复制代码
function euclideanDistance(a, b) {
  return Math.sqrt(a.reduce((s, v, i) => s + (v - b[i]) ** 2, 0))
}

const MATCH_THRESHOLD = 0.6
const main = results.reduce((max, r) =>
  r.detection.box.area > max.detection.box.area ? r : max)
let bestDist = Infinity, bestName = "陌生人"
for (const [name, stored] of whitelist) {
  const d = euclideanDistance(main.descriptor, stored)
  if (d < bestDist) { bestDist = d; bestName = name }
}
const accepted = bestDist < MATCH_THRESHOLD
bash 复制代码
张三 vs 李四: 0.834
张三 vs 张三: 0.000

12. 门禁结果

bash 复制代码
if (accepted) setGate("open", `欢迎回家,${bestName}`, `距离 ${bestDist.toFixed(3)}`)
else         setGate("deny", "非本小区人员",          `最近 ${bestName}`)
bash 复制代码
┌──────────────────┐
│      开          │
│  欢迎回家,张三  │
│  距离 0.321      │
└──────────────────┘

13. 项目结构

bash 复制代码
demo/4/
├── server.ts              # Bun 服务
├── scripts/download-models.ts
└── static/
    ├── index.html
    └── models/            # 6.5MB

代码 demo/4/,下完模型跑。

最终效果

相关推荐
Asize38 分钟前
AI 协作开发新范式:我用 SDD 做了个排版 npm 包
前端·人工智能
kyriewen2 小时前
我把今年流传的前端 AI 面试题整理了一遍——4 类场景题+回答框架(附速查表)
前端·面试·程序员
计算机魔术师3 小时前
国产多模态模型正面硬刚Opus旗舰:差距从30%缩到3%
前端
风骏时光牛马3 小时前
程序员进阶:深度思考,解锁职场成长的底层逻辑
前端
IT_陈寒3 小时前
用了Proxy才发现以前的JavaScript白写了
前端·人工智能·后端
唐青枫3 小时前
别把 ArrayList 当成会自动管理内存的 List:Zig 动态数组从入门到实战
后端
鸿蒙开发3 小时前
我为 HarmonyOS 做了一个统一大模型 SDK:@hmkit/ai 正式开源
后端
爱丶不疚3 小时前
Eval: Agent 说的 Eval 是什么?从单测、TDD 到 Sentry 聊起
前端·ai编程·vibecoding
求道於盲3 小时前
python中的类型标注
前端
计算机魔术师3 小时前
从硅谷测试到全球铺开,ChatGPT广告的10亿美元秘密
前端