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

0 阅读2分钟

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

简单到什么程度呢? 

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

图片

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


5行核心代码

// 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 维向量。不上服务器,隐私保住。

流程图

        上传照片
              │
              ▼
        整图模糊 + 加载圈
              │
              ▼
        检测人脸 + 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下载。

✓ 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. 起服务

// 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,配了转发真实后端。

$ bun run dev
  http://localhost:3003

4. 加载模型

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 摄像头流。

<input id="fileInput" type="file" accept="image/*" style="display:none" />
<button id="btnSelect">选择人脸照片</button>
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. 上传到后端

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. 整图模糊 + 加载圈

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

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

8. 检测人脸

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

9. 画 68 关键点(5 色)

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

图片

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 维 + 白名单

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

存 localStorage,关页面不丢。

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. 欧氏距离 + 阈值

function euclideanDistance(a, b) {
  return Math.sqrt(a.reduce((s, v, i) => s + (v - b[i]) ** 20))
}

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
张三 vs 李四: 0.834
张三 vs 张三: 0.000

12. 门禁结果

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

13. 项目结构

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

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

最终效果

图片