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 face detection and rebuild. Naming just this feature keeps the other vision clients and models out of your app:
{
"expo": {
"plugins": [["expo-ai-kit", { "vision": ["face-detection"] }]]
}
}Use "vision": true instead if you also want background removal, labels, or OCR.
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 on the upright image (EXIF orientation applied); Android decodes a downscaled copy for speed and memory and reports boxes in full-resolution pixels. 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.