DEV Community

Cover image for face-api.js Alternative: Face Recognition in Node.js with an API
AI Engine
AI Engine

Posted on Originally published at ai-engine.net

face-api.js Alternative: Face Recognition in Node.js with an API

face-api.js made face recognition in JavaScript easy, but its last release (0.22.2) shipped in March 2020 and still pins TensorFlow.js core 1.7.0. The best-known fork, @vladmandic/face-api, is now archived too, yet the two packages still pull about 465,000 and 518,000 npm downloads a month.

Here is how to replace it with a face recognition API from Node.js 20: built-in fetch and FormData, no SDK, no model weights, no canvas or tfjs-node build. All code below ran against the live API.

Want to run it on your own photos? Get a key for the Face Analyzer API.

face-api.js calls mapped to endpoints

face-api.js Face Analyzer API
nets.*.loadFromDisk("./models") nothing to load
detectAllFaces().withFaceLandmarks() /faceanalysis (boundingBox, landmarks)
.withAgeAndGender(), .withFaceExpressions() /faceanalysis (AgeRange, Gender, Emotions)
euclideanDistance(d1, d2) < 0.6 /compare-faces (matchedFaces, unmatchedFaces)
new FaceMatcher(labeledDescriptors) /save-face-in-repository with external_id
faceMatcher.findBestMatch() /search-face-in-repository (label + Similarity)

One helper for every endpoint

// face.mjs
import { openAsBlob } from "node:fs";

const BASE = "https://faceanalyzer-ai.p.rapidapi.com";
const HEADERS = {
  "x-rapidapi-host": "faceanalyzer-ai.p.rapidapi.com",
  "x-rapidapi-key": process.env.RAPIDAPI_KEY,
};

export async function callApi(endpoint, files = {}, fields = {}) {
  const form = new FormData();
  for (const [name, path] of Object.entries(files)) {
    form.append(name, await openAsBlob(path), path);
  }
  for (const [name, value] of Object.entries(fields)) {
    form.append(name, value);
  }
  const res = await fetch(`${BASE}${endpoint}`, {
    method: "POST",
    headers: HEADERS,
    body: form,
  });
  return (await res.json()).body;
}
Enter fullscreen mode Exit fullscreen mode

Detection and attributes

import { callApi } from "./face.mjs";

const { faces } = await callApi("/faceanalysis", { image: "team.jpg" });
console.log(faces.length, "faces");
for (const face of faces) {
  const { Gender, Eyeglasses, Emotions } = face.facialFeatures;
  console.log(Gender, Eyeglasses ? "glasses" : "no glasses", Emotions.join(", "));
}
Enter fullscreen mode Exit fullscreen mode
5 faces
Female glasses HAPPY
Male no glasses HAPPY
Male glasses HAPPY
Male glasses CALM
Female no glasses HAPPY
Enter fullscreen mode Exit fullscreen mode

Compare two faces

async function samePerson(sourcePath, targetPath) {
  const body = await callApi("/compare-faces", {
    source_image: sourcePath,
    target_image: targetPath,
  });
  return body.matchedFaces.length > 0;
}

console.log(await samePerson("alex_badge.jpg", "alex_today.jpg")); // true
console.log(await samePerson("alex_badge.jpg", "maya.jpg"));       // false
Enter fullscreen mode Exit fullscreen mode

No threshold to tune. On a photo of a colleague, the API even found Alex out of focus in the background, half hidden behind an arm.

Face comparison API results from Node.js: a match, a no-match among three faces, and a match on a blurred background face

Replace FaceMatcher

const REPO = "team-faces";
await callApi("/create-facial-repository", {}, { repository_id: REPO });

const team = { alex: "alex_badge.jpg", maya: "maya.jpg", sam: "sam.jpg" };
for (const [label, path] of Object.entries(team)) {
  await callApi("/save-face-in-repository", { image: path }, {
    repository_id: REPO,
    external_id: label,
    max_faces: "1",
  });
}

async function whoIs(path) {
  const { FaceMatches = [] } = await callApi(
    "/search-face-in-repository",
    { image: path },
    { repository_id: REPO },
  );
  const best = FaceMatches.reduce(
    (a, b) => (!a || b.Similarity > a.Similarity ? b : a),
    null,
  );
  return best && best.Similarity >= 90
    ? `${best.ExternalImageId} (${best.Similarity.toFixed(2)})`
    : "unknown";
}

console.log(await whoIs("alex_office.jpg")); // alex (100.00)
console.log(await whoIs("visitor.jpg"));     // unknown
Enter fullscreen mode Exit fullscreen mode

Browser uploads: proxy through your server

Keep the API key out of the browser with a small Express route (npm i express multer):

import express from "express";
import multer from "multer";

const app = express();
const upload = multer({ limits: { fileSize: 5 * 1024 * 1024 } });

app.post("/api/faces", upload.single("image"), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: "image is required" });
  const form = new FormData();
  form.append("image", new Blob([req.file.buffer], { type: req.file.mimetype }), req.file.originalname);
  const r = await fetch("https://faceanalyzer-ai.p.rapidapi.com/faceanalysis", {
    method: "POST",
    headers: {
      "x-rapidapi-host": "faceanalyzer-ai.p.rapidapi.com",
      "x-rapidapi-key": process.env.RAPIDAPI_KEY,
    },
    body: form,
  });
  res.status(r.status).json((await r.json()).body);
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Migration tips

  • Drop face-api.js, canvas, @tensorflow/tfjs-node and the models folder
  • Re-enroll reference photos with your existing labels as external_id (descriptors cannot be imported)
  • Scale boxes by image size: coordinates come back from 0 to 1
  • No face means empty lists, not an error

Full guide with the mapping details: face-api.js alternative on ai-engine.net.

Sources: face-api.js, @vladmandic/face-api, Node.js fs.openAsBlob.

👉 Grab a key from the Face Analyzer API page and run the scripts on your own photos.

Top comments (0)