Detect faces

Find every face in a photo and get its box. The library only detects; what counts as a good photo is your decision, and the recipes below show the common ones.

Setup

Follow installation. On Android, enable vision and rebuild:

app.jsonjson
{
  "expo": {
    "plugins": [["expo-ai-kit", { "vision": true }]]
  }
}

iOS uses Apple Vision on a physical device; Simulator is not supported. Android uses bundled ML Kit Face Detection. No model download, preparation, or permission is required, and the llm option is not needed.

Detect faces

import { detectFaces } from 'expo-ai-kit';

const { width, height, faces } = await detectFaces({ uri: photo.uri });
console.log(`${faces.length} face(s) in a ${width}×${height} image`);
for (const face of faces) {
  console.log(face.bounds, face.pixelBounds);
}

Pass a local file:// URI or absolute path. Android also accepts content:// URIs. Remote URLs are rejected. Detection runs at full resolution on the upright image (EXIF orientation applied), and calls may run concurrently.

The result

type FaceDetectionResult = {
  width: number;          // upright image size in pixels
  height: number;
  faces: DetectedFace[];  // largest first; [] when none
};
type DetectedFace = {
  bounds: { x: number; y: number; width: number; height: number };      // normalized 0–1, origin top-left
  pixelBounds: { x: number; y: number; width: number; height: number }; // upright image pixels
  confidence?: number;    // 0–1, Apple Vision only
};

Both coordinate spaces describe the same box, clamped to the image. Use bounds to draw over a scaled preview and pixelBounds to crop the original. Faces are sorted by area, so faces[0] is the largest.

Recipes

Accept a profile photo when it is big enough and shows one clearly dominant face, ignoring small faces in the background:

type PhotoStatus = 'READY' | 'NO_FACE' | 'MULTIPLE_FACES' | 'LOW_QUALITY';

async function checkProfilePhoto(uri: string): Promise<PhotoStatus> {
  const { width, height, faces } = await detectFaces({ uri });
  if (width * height < 500_000) return 'LOW_QUALITY';
  if (faces.length === 0) return 'NO_FACE';
  const area = (f: (typeof faces)[number]) => f.pixelBounds.width * f.pixelBounds.height;
  const largest = area(faces[0]);
  const dominant = faces.filter((f) => area(f) / largest > 0.2).length;
  return dominant === 1 ? 'READY' : 'MULTIPLE_FACES';
}

Crop to the largest face with some margin, for an avatar thumbnail:

const { width, height, faces } = await detectFaces({ uri });
if (faces[0]) {
  const { x, y, width: w, height: h } = faces[0].pixelBounds;
  const margin = Math.round(Math.max(w, h) * 0.4);
  const crop = {
    originX: Math.max(0, x - margin),
    originY: Math.max(0, y - margin),
    width: Math.min(width, x + w + margin) - Math.max(0, x - margin),
    height: Math.min(height, y + h + margin) - Math.max(0, y - margin),
  };
  // Pass `crop` to your image manipulator of choice.
}

Count people in a frame: faces.length, optionally ignoring boxes below a size you choose.

Errors

Invalid input rejects before native work. Decode and detector failures throw ModelError with IMAGE_DECODE_FAILED or VISION_FAILED. An Android build without vision throws VISION_NOT_ENABLED; the iOS Simulator throws DEVICE_NOT_SUPPORTED. Detection does not identify a person or verify liveness.