高端的小区往往只需要简单的门禁系统,
简单到什么程度呢?
哈哈, 浏览器加载个小模型就够了。

本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/,下完模型跑。
最终效果
