跪拜 Guibai
← Back to the summary

A 6.5MB Face Recognition Model Runs an Access Gate Entirely in the Browser

High-end residential communities often only need a simple access control system.

How simple?

Haha, just loading a small model in the browser is enough.

Image

The portrait in this demo is AI-generated and used only for testing.


5 lines of core code

// demo/4/static/index.html
const r = await faceapi.detectSingleFace(img).withFaceDescriptor()
console.log("128-dimensional feature:", r.descriptor.length)
const d = Math.sqrt([...r.descriptor].reduce((s, v) => s + v * v, 0))
console.log("Magnitude:", d.toFixed(3))

After selecting an image, the browser outputs a 128-dimensional vector. No server upload, privacy preserved.

Flowchart

        Upload photo
             │
             ▼
   Full-image blur + loading spinner
             │
             ▼
   Detect face + 68 keypoints
             │
             ▼
  Extract 128-dimensional feature
             │
             ▼
   Compare against whitelist
   Distance < 0.6 → Open door
   Distance ≥ 0.6 → Deny access

Pain Point

The company wanted an access control system. Vendor quotes were several thousand yuan per gate. The backend also required a GPU server. I wondered, could the frontend handle it? Data stays on the device, zero cost. After some research, I found face-api.js.

1. What is face-api.js

face-api.js is based on TensorFlow.js, Google's deep learning framework. Three models:

Model Purpose Size
tinyFaceDetector Detect faces 190KB
faceLandmark68Net 68 keypoints 90KB
faceRecognitionNet 128-dimensional feature 6.3MB

Total: 6.5MB.

2. Models

Downloaded from the 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)
All downloaded, approximately 6.5MB

Models are placed in static/models/.

3. Starting the Service

// 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)))
}})

If BACKEND_URL is not configured, it uses mock mode; if configured, it forwards to the real backend.

$ bun run dev
  http://localhost:3003

4. Loading Models

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)

All loaded within 3 seconds.

5. Selecting an Image

For demo convenience, image upload is used. In production, this can be switched to navigator.mediaDevices.getUserMedia for camera streaming.

<input id="fileInput" type="file" accept="image/*" style="display:none" />
<button id="btnSelect">Select Face Photo</button>
fileInput.onchange = async (e) => {
  const file = e.target.files[0]
  if (file.size / 1024 / 1024 > 30) return setStatus("Exceeds 30MB", "err")
  if (!file.type.startsWith("image/")) return setStatus("Not an image", "err")
  previewImg.src = await fileToBase64(file)
}

Limit 30MB, only accepts image/*.

6. Uploading to Backend

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

Mock mode returns base64; real mode forwards to the backend.

7. Full-Image Blur + Loading Spinner

Before recognition, apply a Gaussian blur so the original image is not visible.

function startLoading() {
  overlayCtx.filter = "blur(14px)"
  overlayCtx.drawImage(previewImg, 0, 0, w, h)
  // Draw rotating golden arc in the center + "Recognizing face..." + elapsed 0.0s
  loadingAnimId = requestAnimationFrame(frame)
}

8. Detecting Faces

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

9. Drawing 68 Keypoints (5 Colors)

Colored by region, distinguishable even from a distance.

Image

const groups = [
  { from:  0, to: 16, color: "#00e5ff" }, // Jawline
  { from: 17, to: 26, color: "#ffeb3b" }, // Eyebrows
  { from: 27, to: 35, color: "#76ff03" }, // Nose
  { from: 36, to: 47, color: "#ff1744" }, // Eyes
  { from: 48, to: 67, color: "#e040fb" }, // Mouth
]

10. Extracting 128-Dimensional Feature + Whitelist

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

Stored in localStorage, persists after closing the page.

const WL_KEY = "tangchen_whitelist_v1"
const whitelist = new Map(JSON.parse(localStorage.getItem(WL_KEY) || "{}"))

btnCapture.onclick = async () => {
  const name = nameInput.value.trim()                  // "Zhang San 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. Euclidean Distance + Threshold

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 = "Stranger"
for (const [name, stored] of whitelist) {
  const d = euclideanDistance(main.descriptor, stored)
  if (d < bestDist) { bestDist = d; bestName = name }
}
const accepted = bestDist < MATCH_THRESHOLD
Zhang San vs Li Si: 0.834
Zhang San vs Zhang San: 0.000

12. Access Control Result

if (accepted) setGate("open", `Welcome home, ${bestName}`, `Distance ${bestDist.toFixed(3)}`)
else         setGate("deny", "Non-resident",               `Nearest ${bestName}`)
┌──────────────────┐
│      Open        │
│  Welcome home,   │
│    Zhang San     │
│  Distance 0.321  │
└──────────────────┘

13. Project Structure

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

Code is in demo/4/. Run after downloading the models.

Final Result

Image