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:
{
"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.