mediapipe_face_mesh
Face detection and a 478-landmark face mesh pipeline, on device, in a few milliseconds per frame. Models and the TensorFlow Lite runtime ship inside the package.

Supported Platforms
| platform | requirement |
|---|---|
| Android | minSdk 24 (arm64-v8a, x86_64) |
| iOS | 13.0+ |
| Windows | x64 |
Requires Dart >=3.8.1 <4.0.0 and Flutter >=3.32.0.
Performance
Same device (Dimensity 9400, Android 16), same inputs. One call runs detection plus the full 468-landmark mesh in both packages:
| mediapipe_face_mesh | google_mlkit_face_mesh_detection 0.5.0 | |
|---|---|---|
| single image, per call | 2.9 ms | 43.7 ms |
| streaming, per frame | 1.54 ms | 49.6 ms |
| streaming, attention mesh (478, recommended) | 2.10 ms | - |
In streaming, mediapipe_face_mesh tracks the face between frames, while ML Kit re-runs detection every frame (it has no tracking mode). Single-image latency varies with device thermal state; streaming is the stable metric. Method, full matrix, and caveats in doc/BENCHMARKS.md.
Install
flutter pub add mediapipe_face_mesh
Usage
Create Face Detector Processor
import 'package:mediapipe_face_mesh/mediapipe_face_mesh.dart';
final faceDetectorProcessor = await FaceDetectorProcessor.create();
The model option selects the bundled detector model: shortRange
(default, near faces), fullRange (dense), or fullRangeSparse (sparse)
for faces farther from the camera. maxResults (default 1) caps the number
of detections.
Create Face Mesh Processor
import 'package:mediapipe_face_mesh/mediapipe_face_mesh.dart';
final faceMeshProcessor = await FaceMeshProcessor.create(
enableAttentionMesh: true, // recommended; default is false
);
enableAttentionMesh is the recommended configuration: it swaps the base
mesh model for the unified face_landmark_with_attention model, which
refines the lips, eyes, and irises in a single inference and returns 478
landmarks (10 iris points at indices 468..477). It is opt-in today to
keep existing setups unchanged, and is planned to become the default from
major version 3.
enableIris: true is the older alternative (base 468 mesh plus a separate
iris pass, same 478-point layout); it is ignored when enableAttentionMesh
is set.
Delegates
Every processor (FaceDetectorProcessor, FaceMeshProcessor,
FaceBlendshapesProcessor) accepts a delegate option:
FaceMeshDelegate.cpu(default)FaceMeshDelegate.xnnpackFaceMeshDelegate.gpuV2(deprecated, removed in 3.0.0)
The bundled runtime supports cpu and xnnpack, which benchmark within
noise of each other. gpuV2 currently falls back to CPU and is being removed: in our
benchmarks the GPU delegate was slower for these models and only added
binary size.
Input Formats
The package supports two image input types:
FaceMeshNv21ImageUse this for Android camera frames in NV21 layout.FaceMeshImageUse this for RGBA or BGRA buffers: iOS camera frames, desktop/USB (UVC) camera frames, or any decoded image.
On Android, camera frame streams are commonly delivered as YUV420-family buffers
in layouts such as single-plane NV21, Y + interleaved VU, or YUV420 Y/U/V
planes. The package provides FaceMeshNv21Image helpers for converting these
layouts into the NV21 input expected by processNv21(...). See the example
camera image adapter for usage.
Stream Inference
Use stream inference when processing continuous camera frames. Stream processors take a Stream of frames and return a Stream of results.
final pipeline = FaceMeshInferencePipeline(
detector: faceDetectorProcessor,
mesh: faceMeshProcessor,
landmarkSmoothing: const LandmarkSmoothingOptions(), // recommended; off by default
);
final inferenceStreamProcessor = FaceMeshInferenceStreamProcessor(pipeline);
final frameController = StreamController<FaceMeshNv21Image>();
bool _isBusy = false;
bool _isMeshActive = true; // e.g. driven by a UI toggle
inferenceStreamProcessor
.processNv21(
frameController.stream,
runMeshResolver: (_) => _isMeshActive,
rotationDegrees: rotationDegrees,
)
.listen(_handleInferenceResult, onError: onError);
void _handleInferenceResult(FaceMeshInferenceResult result) {
_isBusy = false;
// detectionResult is null on landmark-tracked frames (detector skipped).
final FaceDetectionResult? detections = result.detectionResult;
if (detections != null) {
onDetections(detections);
}
onMeshResult(result.meshResult);
}
void onCameraFrame(FaceMeshNv21Image frame) {
if (_isBusy) return;
_isBusy = true;
frameController.add(frame);
}
Use runMesh: false when an entire stream should run detector-only. Use
runMeshResolver when mesh execution should be decided per frame, such as a UI
toggle that can change while the stream is active.
rotationDegrees is fixed per subscription. When the camera rotation (or the
input source) changes, re-subscribe with the new value; see the example app
for a complete flow.
For BGRA / RGBA input, use process(...) instead of processNv21(...).
Landmark tracking
Tracking is on by default: the detector runs only to acquire or re-acquire
a face, and tracked frames report detectionResult as null. On face loss
the detector re-acquires on the next frame (isTracking reports the
state). Pass enableLandmarkTracking: false to run the detector on every
frame, and call resetTracking() when switching input sources.
For multi-face behavior, see Multi-Face Inference.
Landmark smoothing
landmarkSmoothing (used in the examples above) smooths output landmarks
across frames with a OneEuro filter, matching the official FaceLandmarker
stream-mode behavior: a still face stops jittering while fast movement
passes through with almost no lag. Off by default and recommended; planned
to become the default from major version 3. See LandmarkSmoothingOptions
and FaceLandmarkSmoother API docs for tuning and pipeline-free use.
Single Inference
Use single-frame inference in one call without a stream processor.
final pipeline = FaceMeshInferencePipeline(
detector: faceDetectorProcessor,
mesh: faceMeshProcessor,
landmarkSmoothing: const LandmarkSmoothingOptions(), // recommended; off by default
);
final result = pipeline.processNv21(
nv21Image,
rotationDegrees: rotationDegrees,
);
final meshResult = result.meshResult;
if (meshResult != null) {
onResult(meshResult);
}
For detector-only inference, pass runMesh: false.
Geometry and Measurements
FaceMeshResult includes helpers for 2D distances and estimated 3D face
geometry:
// 2D pixel distance between two landmarks
final pixelDistance = meshResult.distancePixels(33, 263);
// 3D geometry estimation (native call; one per frame is typical)
final geometry = meshResult.estimateGeometry();
// Pass actual camera FOV for more accurate centimeter estimates (default: 63°)
// final geometry = meshResult.estimateGeometry(verticalFovDegrees: 72.0);
// Head pose: yaw (left/right), pitch (up/down), roll (tilt)
final pose = geometry.headPose;
// pose.yawDegrees, pose.pitchDegrees, pose.rollDegrees
// Single centimeter distance between two landmarks
final eyeDistanceCm = geometry.distanceCm(33, 263);
// Preset bundle: computes all measurements at once
// faceWidth 234 ↔ 454 cheek-to-cheek
// faceHeight 10 ↔ 152 forehead-to-chin
// eyeOuterDistance 33 ↔ 263 outer eye corners
// eyeInnerDistance 133 ↔ 362 inner eye corners
// interpupillaryDistance 468 ↔ 473 pupils (iris only, else null)
// mouthWidth 61 ↔ 291
// noseWidth 98 ↔ 327
final measurements = geometry.measurements;
final faceWidthCm = measurements.faceWidth.valueCm;
Centimeter values are estimates based on the canonical face geometry model. Scale accuracy depends on the virtual camera assumption (default vertical FOV 63°) and will vary by device.
To look up landmark indices visually, use https://cornpip.github.io/mediapipe_landmark_viewer/
Face Blendshapes
Blendshapes are 52 ARKit-style expression coefficients (jaw open, eye blink,
smile, etc.), useful for avatars, AR filters, and expression detection.
Requires a mesh created with enableAttentionMesh: true (recommended) or
enableIris: true, since the model reads the iris landmarks.
final blendshapesProcessor = await FaceBlendshapesProcessor.create();
// Map<FaceBlendshape, double> with values in [0, 1];
// null when the frame had no face.
final blendshapes = blendshapesProcessor.process(meshResult);
if (blendshapes != null) {
final smile = (blendshapes[FaceBlendshape.mouthSmileLeft]! +
blendshapes[FaceBlendshape.mouthSmileRight]!) /
2;
if (smile > 0.5) {
// smiling
}
}
Call close() when the processor is no longer needed.
Multi-Face Inference
Multi-face inference tracks each face across frames with a stable trackId;
the detector runs only while fewer than maxMeshFaces faces are tracked.
The mesh processor must be created with createForMultiFace(...).
final faceMeshProcessor = await FaceMeshProcessor.createForMultiFace(
enableAttentionMesh: true, // recommended; default is false
);
final faceDetectorProcessor = await FaceDetectorProcessor.create(
maxResults: 4,
);
final pipeline = FaceMeshInferencePipeline(
detector: faceDetectorProcessor,
mesh: faceMeshProcessor,
landmarkSmoothing: const LandmarkSmoothingOptions(), // recommended; off by default
);
final inferenceStreamProcessor = FaceMeshInferenceStreamProcessor(pipeline);
inferenceStreamProcessor
.processNv21MultiFace(
frameController.stream,
maxMeshFaces: 2,
runMeshResolver: (_) => _isMeshActive,
rotationDegrees: rotationDegrees,
)
.listen(_handleMultiInferenceResult, onError: onError);
void _handleMultiInferenceResult(FaceMeshMultiInferenceResult result) {
// detectionResult is null while all face slots are served by tracking.
final FaceDetectionResult? detections = result.detectionResult;
if (detections != null) {
onDetections(detections);
}
for (final TrackedFaceMesh face in result.faces) {
onFaceMesh(face.trackId, face.mesh); // trackId is stable across frames
}
}
For BGRA / RGBA input, use processMultiFace(...) instead of
processNv21MultiFace(...). For single-frame multi-face inference, call
pipeline.processNv21MultiFace(...) directly.
Using an External Face Detector
The bundled detector is optional. If you already use another face detector
(e.g. ML Kit), pass its face box to FaceMeshProcessor.process(...) or
processNv21(...) as a FaceMeshBox or NormalizedRect and skip
FaceDetectorProcessor entirely.
Close Resource
Explicitly calling close() when the processors are no longer needed is recommended.
faceDetectorProcessor.close();
faceMeshProcessor.close();
Example app
A demo app lives in the example/ directory at the root of this
repository.
Bundled Runtime and Models
- TensorFlow Lite C runtime binaries for Android (
arm64-v8a,x86_64), iOS (devicearm64, simulatorarm64/x86_64), and Windows (x64) - Model source
Notes
- On Flutter older than 3.38.0, a debug
flutter runon a physical iOS 17+ device can hang atInstalling and launching.... Release and profile builds run fine, as does launching from Xcode; Flutter 3.38.0 fixesflutter runitself. See Running debug builds on a physical iOS device. This is Flutter tooling behavior and applies to any Flutter iOS project. - Upgrading from 2.6.0 or earlier on iOS: the bundled TensorFlow Lite runtime
changed from a fat
.frameworkto an.xcframework. If a build still fails withbuilding for 'iOS-simulator', but linking in object file built for 'iOS', a stale copy of the old framework is being picked up. Runpod deintegrate && pod installinios/and delete the build folder and DerivedData.