flutter_face_liveness 3.3.0
flutter_face_liveness: ^3.3.0 copied to clipboard
Production-ready Flutter SDK for face detection, liveness verification, and anti-spoof protection using ML Kit, TensorFlow Lite, and an optional YOLOv8n-face backend.
flutter_face_liveness #

Production-ready AI-powered Flutter SDK for real-time face liveness detection, replay attack prevention, and persistent face identity โ powered by Google ML Kit, TensorFlow Lite, and an optional YOLOv8n-face detector backend. All processing runs entirely on-device with zero server calls (except one-time model downloads).
๐ New in v3.3.0 โ YOLOv8n-face detector backend #
Select
FaceDetectorBackend.yolov8to run a YOLOv8n-face TFLite model โ on its own background isolate, never blocking the camera โ as an alternative to ML Kit for the identity/embedding pipeline. Works out of the box, no setup required. Also new:FaceCaptureServicefor enrolling/verifying a face from a single captured photo, no liveness session needed; andenableBestFrontalCaptureto auto-capture an upright JPEG of the user's best-angle frame alongside the liveness result.
Table of Contents #
- Features
- Replay Attack Detection
- Use Cases
- Getting Started
- Quick Start
- Face Identity (Face ID)
- Face Detector Backends (ML Kit / YOLOv8)
- Face Capture โ Photo Enrollment & Verification
- Best-Frontal Capture
- LivenessConfig Reference
- Liveness Actions
- LivenessResult Fields
- LivenessController API
- TFLite Integration
- Architecture
- Performance
- Lighting & Brightness
- Security
- Example App
- Changelog
Features #
| Category | Feature |
|---|---|
| Liveness | 7 challenge actions โ blink, turn left/right, look up/down, smile, open mouth |
| Face Landmarks | 10 ML Kit landmark positions per frame (leftEyePosition, rightEyePosition, noseBasePosition, cheeks, mouth corners, ears) |
| Face ID | Same face โ always same ID, across sessions and restarts. Powered by FaceNet TFLite (auto-downloaded, ~23 MB) |
| Face Detector Backends | FaceDetectorBackend.mlkit (default) or .yolov8 โ select which model supplies the face box/keypoints feeding the identity pipeline. Liveness actions always use ML Kit either way |
| Photo-Based Enrollment | FaceCaptureService โ enroll/verify from a single captured photo (no liveness challenge) using the same gallery and models as the live session flow |
| Best-Frontal Capture | enableBestFrontalCapture: true โ auto-captures the most-frontal frame of the session as an upright JPEG (LivenessResult.bestFrontalImageBytes), e.g. for a KYC review screen |
| New/Returning | isFaceIdNew flag โ first-time or returning face |
| Anti-Spoof | 9-signal composite engine โ eye variance, geometry, pose, micro-motion, quality, tracking, brightness variance, motion jitter |
| 8-Signal Replay Detection | Five new pure-Dart signals (S5โS8) run alongside MiniFASNet. Final score = min of all signals โ must defeat every layer simultaneously |
| Screen Detection | Specular highlight density + skin chromatic warmth (iOS) + temporal backlight stability |
| Optical Flow | 32ร32 face thumbnail block-MAD: stasis detection + spatial variance for static/rigid replay |
| Face Geometry | 3-D depth via cos(yaw) correlation ยท eye-ratio consistency ยท landmark velocity naturalness |
| TFLite Models | FaceAntiSpoofing (3.9 MB) + MiniFASNet-V2 (1.7 MB) โ both auto-download & run in background isolates |
| Frame Quality | BT.601 platform-correct brightness (NV21 + BGRA8888), blur, overexposure โ 6-frame debounce |
| Face Mesh | MediaPipe Face Mesh (468 3-D landmarks) via enableFaceMesh: true โ depth score exposed via liveMeshDepthScore |
| Replay Guard | FNV-1a frame hashing detects looped / static-image attacks |
| Session Security | Cryptographically unique session IDs via Random.secure() |
| Action Randomisation | Fisher-Yates shuffle prevents predictable replay attacks |
| Isolate ML | YUVโNV21 conversion, quality analysis, TFLite inference โ all in background isolates |
| Theming | Dark / light / system mode via LivenessConfig.themeMode |
| Debug Overlay | 8 real-time signal scores + Euler angles + eye/smile probabilities |
Replay Attack Detection #
v3.1.0 introduces a full 8-signal on-device replay detection pipeline. All signals run locally โ no server, no network calls during verification.
How it works #
Every frame is analysed by up to 8 independent signals. At session end, the minimum score across all signals is the final replay decision. An attacker must simultaneously defeat every single layer.
| # | Signal | Type | What it catches |
|---|---|---|---|
| S1 | Spatial Laplacian variance | Pixel analysis | H.264 compression smooths skin micro-texture (pores, wrinkles) |
| S2 | Temporal brightness variance | History | Screen backlight is perfectly stable; real rooms fluctuate |
| S3 | Motion heterogeneity CVยฒ | 9-region AEC-invariant | Uniform AEC gain = screen; non-uniform regional motion = real face |
| S4 | MiniFASNet-V2 TFLite | Deep learning | Learned anti-spoof features across face texture + geometry |
| S5 | ReplayAnalyzer | Multi-signal | Perceptual fingerprint (loop detection) + angular micro-jitter (stabilised video) + motion direction entropy + blink consistency |
| S6 | ScreenArtifactDetector | Pixel analysis | Specular highlights (screen glare) + skin chromatic warmth (LCD blue boost) + backlight stability |
| S7 | OpticalFlowAnalyzer | Frame differencing | Stasis (static photo) + rigid-body motion (replay on tripod) |
| S8 | FaceGeometryAnalyzer | Landmark-based | Flat surface (no 3-D depth via cos(yaw)) + no micro-tremor (landmark velocity) + eye asymmetry |
Enable it #
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft],
config: LivenessConfig(
enableVideoReplayDetection: true, // activates all 8 signals
videoReplayThreshold: 0.50, // score below this = rejected
),
onSuccess: (result) => print('Live: ${result.videoReplayScore}'),
onFailed: (reason) => print('Rejected: $reason'),
)
Debug overlay (8 signals) #
Enable showDebugOverlay: true to see all signals live during development:
VR-B: 45.2% โ โ S2 temporal brightness variance
LAP: 312 ok โ S1 Laplacian texture variance
HET: 0.0312 ok โ S3 motion heterogeneity CVยฒ
TF: 78.4% real โ S4 MiniFASNet TFLite
RA: 82.1% ok โ S5 ReplayAnalyzer
SCR: 91.3% ok โ S6 ScreenArtifactDetector
FLOW: 67.8% ok โ S7 OpticalFlowAnalyzer
GEO: 73.5% ok โ S8 FaceGeometryAnalyzer
Tuning for your target devices #
Replay detection performance depends on camera sensor quality. For best results:
- Good lighting โ low-light scenes reduce texture variance (S1) and may lower the score on genuine faces. Move to a well-lit area or lower
videoReplayThresholdslightly. - Low-end devices โ older camera sensors produce noisier frames. If you see occasional false rejections, adjust
videoReplayThresholdfrom0.50down to0.40โ0.45. - High-security apps โ raise
videoReplayThresholdto0.60+ and combine withenableTFLite: truefor maximum protection.
// Standard (balanced)
config: LivenessConfig(
enableVideoReplayDetection: true,
videoReplayThreshold: 0.50,
)
// More lenient โ older / low-end devices
config: LivenessConfig(
enableVideoReplayDetection: true,
videoReplayThreshold: 0.42,
)
// High-security
config: LivenessConfig(
enableVideoReplayDetection: true,
videoReplayThreshold: 0.60,
enableTFLite: true,
)
Use Cases #
KYC (Know Your Customer) #
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft, LivenessAction.turnRight],
config: LivenessConfig(
enableAntiSpoof: true,
enableFaceId: true,
enableVideoReplayDetection: true,
randomizeActions: true,
),
onSuccess: (result) {
final faceId = result.faceId; // "FID-3A9F2B1C4E8Dโฆ"
final isNew = result.isFaceIdNew; // true = first time, false = returning
final sessionId = result.sessionId; // "LV-018F3A2B9C4E-D7E31F08"
final score = result.confidenceScore;
},
onFailed: (reason) => showError(reason),
)
Banking / Fintech #
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft, LivenessAction.smile],
config: LivenessConfig(
enableFaceId: true,
faceIdSimilarityThreshold: 0.72,
enableAntiSpoof: true,
enableVideoReplayDetection: true,
sessionTimeoutMs: 30000,
),
onSuccess: (result) {
if (result.isFaceIdNew == false && result.faceId == storedFaceId) {
authoriseTransaction();
} else {
flagForReview();
}
},
onFailed: (reason) => showError(reason),
)
Attendance / Access Control #
FlutterFaceLiveness(
actions: [LivenessAction.blink],
config: LivenessConfig(
enableFaceId: true,
faceIdMode: FaceIdMode.auto,
enableAntiSpoof: true,
enableVideoReplayDetection: true,
),
onSuccess: (result) {
if (result.isFaceIdNew == true) {
db.enrolEmployee(result.faceId!);
} else {
db.markAttendance(result.faceId!, DateTime.now());
}
},
onFailed: (reason) => showError(reason),
)
Getting Started #
1. Add the dependency #
dependencies:
flutter_face_liveness: ^3.3.0
2. Platform permissions #
Android โ android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<!-- Required only when enableFaceId: true or enableVideoReplayDetection: true -->
<uses-permission android:name="android.permission.INTERNET" />
iOS โ ios/Runner/Info.plist
<key>NSCameraUsageDescription</key>
<string>Camera is required for face liveness verification.</string>
3. Minimum SDK versions #
| Platform | Minimum | Notes |
|---|---|---|
| Android | API 26 (Android 8.0) | Required by TFLite Flutter 0.12+ |
| iOS | iOS 13.0 | |
| Dart | 3.0.0 | |
| Flutter | 3.10.0 |
Android โ android/app/build.gradle:
defaultConfig {
minSdk 26
}
4. Fix tflite_flutter for Dart 3.4+ #
# pubspec.yaml
dependency_overrides:
tflite_flutter:
git:
url: https://github.com/tensorflow/flutter-tflite.git
ref: main
5. iOS Swift Package Manager (optional) #
If your app has flutter config --enable-swift-package-manager enabled, this plugin's Package.swift product name follows Flutter's required hyphenated naming convention (flutter-face-liveness) โ no extra setup needed, just make sure you're on v3.3.0+ (earlier versions used an underscored product name that fails SPM resolution with product 'flutter-face-liveness' ... not found).
Quick Start #
import 'package:flutter_face_liveness/flutter_face_liveness.dart';
FlutterFaceLiveness(
actions: [
LivenessAction.blink,
LivenessAction.turnLeft,
LivenessAction.turnRight,
],
config: LivenessConfig(
randomizeActions: true,
enableAntiSpoof: true,
enableVideoReplayDetection: true, // full 8-signal protection
),
onSuccess: (LivenessResult result) {
print('Session : ${result.sessionId}');
print('Confidence: ${(result.confidenceScore * 100).toStringAsFixed(1)}%');
print('Replay : ${result.videoReplayDetected ? "BLOCKED" : "PASSED"}');
},
onFailed: (String reason) => print('Failed: $reason'),
)
Face Identity (Face ID) #
Key guarantee: A Face ID (
FID-XXXX) is permanently tied to one physical person's face โ across sessions, restarts, days, and lighting changes. Embeddings are stored encrypted on-device (XOR stream cipher, per-installation key).Day 1 โ FID-3A9F2B1C4E8D7F62 isFaceIdNew: true Day 7 โ FID-3A9F2B1C4E8D7F62 isFaceIdNew: false โ same ID Day 30 โ FID-3A9F2B1C4E8D7F62 isFaceIdNew: false โ same ID Different person โ FID-A817C3F0B24E9D51 isFaceIdNew: true
Operation modes (FaceIdMode) #
| Mode | Behaviour | Use case |
|---|---|---|
FaceIdMode.auto (default) |
Match existing face โ return its ID. Unknown face โ register and return new ID | Combined login + registration flows |
FaceIdMode.registrationOnly |
Register only. Rejects if face already exists (faceAlreadyRegistered: true) |
One-time enrolment โ guarantees one ID per person |
FaceIdMode.verificationOnly |
Match only. Unknown faces fail with "Face not recognized" โ never registers |
Pure login flows where enrolment is separate |
Enable it #
// Auto โ match or register (default)
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft],
config: LivenessConfig(
enableFaceId: true,
faceIdMode: FaceIdMode.auto,
),
onSuccess: (result) {
print(result.isFaceIdNew! ? 'Registered: ${result.faceId}' : 'Welcome back: ${result.faceId}');
print('Match score: ${result.faceMatchScore}');
},
onFailed: (reason) => print('Failed: $reason'),
)
// Registration only โ duplicate prevention
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft],
config: LivenessConfig(
enableFaceId: true,
faceIdMode: FaceIdMode.registrationOnly,
),
onSuccess: (result) => print('Enrolled: ${result.faceId}'),
onFailed: (reason) => print(reason), // "Face already registered"
)
// Verification only โ login flow
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft],
config: LivenessConfig(
enableFaceId: true,
faceIdMode: FaceIdMode.verificationOnly,
),
onSuccess: (result) => print('Login OK: ${result.faceId}'),
onFailed: (reason) => print(reason), // "Face not recognized โ please register first"
)
Managing stored faces #
await controller.clearFaceIdentities(); // delete all on logout
final service = FaceIdentityService();
await service.initialize();
List<String> ids = service.registeredFaceIds; // all enrolled face IDs
int total = service.totalEmbeddingCount; // total embeddings stored
await service.removeFace('FID-3A9F2Bโฆ');
await service.clearAllFaces();
service.dispose();
Cosine similarity thresholds #
| Threshold | Behaviour |
|---|---|
0.72 |
Lenient |
0.82 |
Default โ gallery-based best-of-5 matching |
0.86 |
Stricter โ recommended for banking / high-security |
registrationDuplicateThreshold(default0.75) โ used only inregistrationOnlymode. Intentionally lower thanfaceIdSimilarityThresholdso borderline cases are rejected rather than double-registered.
minEmbeddingQuality(default0.50) โ embeddings below this quality score (L2 norm + variance check) are discarded before averaging. Prevents degenerate low-light or motion-blur crops from polluting the gallery.
Face Detector Backends (ML Kit / YOLOv8) #
Scope: this setting only affects which detector supplies the face box + eye keypoints fed into the identity/embedding pipeline (
enableFaceId: true). Liveness actions (blink, smile, head turns) always run on ML Kit, regardless of this setting โ only ML Kit exposes the eye-open/smiling classification they need. Embedding and matching (FaceNet + gallery search) are identical either way; the backend choice never affectsFaceIdModebehaviour or thresholds.
enum FaceDetectorBackend { mlkit, yolov8 }
| Backend | Behaviour | Extra setup |
|---|---|---|
FaceDetectorBackend.mlkit (default) |
Reuses ML Kit's own landmarks โ zero extra cost, since ML Kit already runs every frame for liveness | None |
FaceDetectorBackend.yolov8 |
Runs a second model (YOLOv8n-face, TFLite) on identity-eligible frames only (roughly the first 7โ15 frames of a session, not continuously) โ may improve box/keypoint accuracy on some angles/conditions | See below |
Enable it #
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft],
config: LivenessConfig(
enableFaceId: true,
faceDetectorBackend: FaceDetectorBackend.yolov8,
yoloConfidenceThreshold: 0.5, // min detection confidence
yoloIouThreshold: 0.45, // NMS overlap threshold
),
onSuccess: (result) => print('Face ID: ${result.faceId}'),
onFailed: (reason) => print('Failed: $reason'),
)
Setup โ works out of the box #
YoloModelDownloader.bundledModelUrl already points to a YOLOv8n-face .tflite model hosted on this package's own GitHub Release (v3.2.0-models). No setup is required โ just set faceDetectorBackend: FaceDetectorBackend.yolov8 and the model downloads automatically on first use, exactly like the FaceNet/anti-spoof models.
If the model fails to download or load (no internet, blocked host, etc.), the yolov8 backend is silently disabled for that session and identity falls back to ML Kit's own landmarks โ a failed YOLO download never blocks camera or liveness from starting.
Using your own custom model (optional) #
Only needed if you want to swap in a different/fine-tuned YOLOv8-face model instead of the bundled one:
- Get or train a face-trained YOLOv8n model โ e.g.
akanametov/yolo-face(yolov8n-face.pt). - Export to TFLite:
(If your local Python/PyTorch/TensorFlow versions are incompatible with this export chain, run the same two lines in a free Google Colab notebook instead โ upload thepip install ultralytics yolo export model=yolov8n-face.pt format=tflite imgsz=640.pt, run the export, download the resulting.tflite.) - Host the resulting
.tflitefile somewhere reachable by HTTPS and pointLivenessConfig.yoloModelUrlat it:config: LivenessConfig( enableFaceId: true, faceDetectorBackend: FaceDetectorBackend.yolov8, yoloModelUrl: 'https://your-host.com/your-custom-yolov8-face.tflite', )
Architecture #
YoloFaceDetectorServiceโ runs the TFLite model in its own background isolate (same pattern as the anti-spoof/video-replay models) so inference never blocks camera-frame delivery.YoloFaceDetectionโ box + up to 5 keypoints (left eye, right eye, nose, mouth-left, mouth-right), decoded from the standard Ultralytics raw pose-export output layout ([1, 4+1+15, N]) with NMS applied.- Detects both NCHW (
[1,3,H,W]) and NHWC ([1,H,W,3]) model input layouts automatically at load time โ the layout depends on which export path produced your.tflitefile.
Face Capture โ Photo Enrollment & Verification #
FaceCaptureService runs the same detect โ align โ embed โ match pipeline as a live liveness session, but against a single captured photo instead of a camera stream โ no liveness challenge, no session. Useful for "upload a photo to register" flows, or verifying against an ID-document photo.
It shares the same FaceIdentityService gallery as LivenessController โ a person registered through a live session is matched by a photo capture, and vice versa.
import 'dart:io';
import 'package:flutter_face_liveness/flutter_face_liveness.dart';
final identity = FaceIdentityService(mode: FaceIdMode.auto);
await identity.initialize();
final capture = FaceCaptureService(
faceIdentity: identity,
backend: FaceDetectorBackend.mlkit, // or .yolov8
);
await capture.initialize();
final result = await capture.captureAndIdentify(File(photoPath));
if (result.isRejected) {
print('Rejected: ${result.rejectionReason}');
// e.g. "No face detected", "Image too blurry", "Face too small โ move closer"
} else {
final match = result.match!;
print('Outcome: ${match.outcome}'); // matched / registered / notFound / alreadyExists
print('Face ID: ${match.faceId}');
print('Similarity: ${match.similarity}');
}
await capture.dispose();
Pre-capture quality gate #
Before an embedding is even computed, the photo is checked against the same brightness/blur/size thresholds used for live frames (LivenessConfig.brightnessMin/Max, blurThreshold, faceTooFarRatio/faceTooCloseRatio โ pass matching values to FaceCaptureService's constructor if you've customised them elsewhere). This is a separate, earlier check than FaceIdentityService's post-hoc embeddingQuality() filter โ a bad photo is rejected before any inference runs on it.
EXIF orientation #
Captured photos carry an EXIF orientation tag rather than storing pixels upright; FaceCaptureService normalises this automatically before detection/cropping, so portrait photos (the common case) are handled correctly regardless of backend.
Best-Frontal Capture #
For a live liveness session, enableBestFrontalCapture automatically tracks the most-frontal frame seen (by |yaw|+|pitch|) and returns it as an upright JPEG on success โ useful when you want a human-viewable photo of the user (e.g. for a KYC agent review screen) without asking them to pose for a separate picture.
FlutterFaceLiveness(
actions: [LivenessAction.blink, LivenessAction.turnLeft],
config: LivenessConfig(
enableBestFrontalCapture: true,
bestFrontalJpegQuality: 85,
),
onSuccess: (result) {
final jpegBytes = result.bestFrontalImageBytes; // Uint8List?
if (jpegBytes != null) {
File('${dir.path}/capture.jpg').writeAsBytesSync(jpegBytes);
}
},
onFailed: (reason) => print('Failed: $reason'),
)
- Independent of
enableFaceIdโ works standalone, or shares the same frame-tracking at no extra cost when both are enabled together. - Only populated on a successful session โ a failed/timed-out session has no capture attached.
- Rotation-corrected to upright orientation regardless of device/camera sensor orientation.
- JPEG encoding runs on a background isolate (
Isolate.run) โ never blocks the UI thread, even for a full-resolution frame.
See the example app's "Best-Frontal Capture" preset for a working demo (result screen shows the captured photo).
LivenessConfig Reference #
LivenessConfig({
// Session
int sessionTimeoutMs = 60000,
bool randomizeActions = true,
// Camera
ResolutionPreset cameraResolution = ResolutionPreset.high,
int targetFps = 20,
// Anti-spoof (heuristic, 9 signals)
bool enableAntiSpoof = true,
double antiSpoofThreshold = 0.45,
// Frame quality
bool enableBrightnessCheck = true,
double brightnessMin = 0.12,
double brightnessMax = 0.92,
bool enableBlurDetection = true,
double blurThreshold = 80.0,
bool enableDuplicateFrameDetection = true,
int duplicateFrameWindowSize = 8,
// Face geometry
double faceTooFarRatio = 0.015,
double faceTooCloseRatio = 0.70,
// Face Mesh (MediaPipe 468 landmarks)
bool enableFaceMesh = false,
// Face Identity
bool enableFaceId = false,
FaceIdMode faceIdMode = FaceIdMode.auto,
double faceIdSimilarityThreshold = 0.82,
double registrationDuplicateThreshold = 0.75,
double minEmbeddingQuality = 0.50,
FaceDetectorBackend faceDetectorBackend = FaceDetectorBackend.mlkit,
String? yoloModelUrl = null,
double yoloConfidenceThreshold = 0.5,
double yoloIouThreshold = 0.45,
bool enableBestFrontalCapture = false,
int bestFrontalJpegQuality = 85,
// TFLite anti-spoof (FaceAntiSpoofing, 3.9 MB โ auto-download)
bool enableTFLite = false,
String? tfliteModelPath = null,
String? tfliteModelUrl = null,
int? tfliteInputSize = null,
double tfliteDeepfakeThreshold = 0.40,
// Video replay detection โ activates all 8 signals (MiniFASNet-V2, 1.7 MB โ auto-download)
bool enableVideoReplayDetection = false,
String? videoReplayModelPath = null,
String? videoReplayModelUrl = null,
int? videoReplayInputSize = null,
double videoReplayThreshold = 0.50,
// UI
ThemeMode themeMode = ThemeMode.dark,
bool showDebugOverlay = false,
})
Full parameter table #
| Parameter | Type | Default | Description |
|---|---|---|---|
sessionTimeoutMs |
int |
60000 |
Auto-fail after this many ms |
randomizeActions |
bool |
true |
Fisher-Yates shuffle per session |
cameraResolution |
ResolutionPreset |
high |
medium reduces CPU on low-end devices |
targetFps |
int |
20 |
Frame processing rate (1โ30 fps) |
enableAntiSpoof |
bool |
true |
9-signal composite heuristic |
antiSpoofThreshold |
double |
0.45 |
Minimum composite score to pass |
enableBrightnessCheck |
bool |
true |
Block too-dark or overexposed frames |
brightnessMin |
double |
0.12 |
BT.601 luminance below this = dark. 6-frame debounce |
brightnessMax |
double |
0.92 |
Luminance above this = overexposed. Same debounce |
enableBlurDetection |
bool |
true |
Block blurry frames |
blurThreshold |
double |
80.0 |
Y-plane variance below this = blurry |
enableDuplicateFrameDetection |
bool |
true |
FNV-1a sliding-window exact-duplicate detection |
duplicateFrameWindowSize |
int |
8 |
Sliding window size |
faceTooFarRatio |
double |
0.015 |
Bbox area ratio below which = too far |
faceTooCloseRatio |
double |
0.70 |
Bbox area ratio above which = too close |
enableFaceMesh |
bool |
false |
MediaPipe Face Mesh (468 3-D landmarks); exposes liveMeshDepthScore |
enableFaceId |
bool |
false |
Persistent face identity via FaceNet TFLite |
faceIdMode |
FaceIdMode |
auto |
auto ยท registrationOnly ยท verificationOnly |
faceIdSimilarityThreshold |
double |
0.82 |
Cosine similarity cutoff for matching (gallery best-of-5) |
registrationDuplicateThreshold |
double |
0.75 |
Duplicate block threshold for registrationOnly mode |
minEmbeddingQuality |
double |
0.50 |
Discard embeddings below this quality score before averaging |
faceDetectorBackend |
FaceDetectorBackend |
mlkit |
mlkit (reuse ML Kit landmarks, no extra cost) or yolov8 (second model, identity-eligible frames only) |
yoloModelUrl |
String? |
null |
Override download URL for the YOLOv8n-face model. null = YoloModelDownloader.bundledModelUrl |
yoloConfidenceThreshold |
double |
0.5 |
Minimum detection confidence for a YOLOv8n-face box to be kept |
yoloIouThreshold |
double |
0.45 |
IoU threshold for YOLOv8n-face non-max suppression |
enableBestFrontalCapture |
bool |
false |
Auto-capture the most-frontal frame as an upright JPEG on success โ see LivenessResult.bestFrontalImageBytes |
bestFrontalJpegQuality |
int |
85 |
JPEG quality (0โ100) for the best-frontal capture |
enableTFLite |
bool |
false |
FaceAntiSpoofing model (auto-downloads 3.9 MB, cached) |
tfliteModelPath |
String? |
null |
Override: asset key or absolute path |
tfliteModelUrl |
String? |
null |
Override: custom download URL |
tfliteInputSize |
int? |
null |
Override: null = auto (256 for bundled model) |
tfliteDeepfakeThreshold |
double |
0.40 |
TFLite score below this โ deepfakeDetected: true |
enableVideoReplayDetection |
bool |
false |
Activates all 8 signals + MiniFASNet-V2 (auto-downloads 1.7 MB) |
videoReplayModelPath |
String? |
null |
Override: local path for MiniFASNet model |
videoReplayModelUrl |
String? |
null |
Override: download URL |
videoReplayInputSize |
int? |
null |
Override: input size (default 80) |
videoReplayThreshold |
double |
0.50 |
Min score below this โ videoReplayDetected: true |
themeMode |
ThemeMode |
dark |
ThemeMode.system follows device |
showDebugOverlay |
bool |
false |
8 signal scores + face metrics |
Liveness Actions #
| Action | Enum | How it triggers |
|---|---|---|
| Blink | LivenessAction.blink |
Both eye probabilities drop below 0.60 โ fires on close, no re-open wait |
| Turn Left | LivenessAction.turnLeft |
Yaw > +12ยฐ held for โฅ 50 ms |
| Turn Right | LivenessAction.turnRight |
Yaw < โ12ยฐ held for โฅ 50 ms |
| Look Up | LivenessAction.lookUp |
Pitch > +12ยฐ held for โฅ 50 ms |
| Look Down | LivenessAction.lookDown |
Pitch < โ12ยฐ held for โฅ 50 ms |
| Smile | LivenessAction.smile |
Smile probability > 0.72 |
| Open Mouth | LivenessAction.openMouth |
Bbox height > 5% above 6-frame baseline OR smile probability > 0.65 (teeth visible), held 2 frames |
Recommended combinations #
// Quick (low friction)
actions: [LivenessAction.blink]
// Standard
actions: [LivenessAction.blink, LivenessAction.turnLeft, LivenessAction.turnRight]
// High-security KYC
actions: [LivenessAction.blink, LivenessAction.turnLeft,
LivenessAction.turnRight, LivenessAction.smile]
// Full challenge
actions: [LivenessAction.blink, LivenessAction.turnLeft, LivenessAction.turnRight,
LivenessAction.lookUp, LivenessAction.openMouth]
LivenessResult Fields #
class LivenessResult {
final bool isSuccess;
final List<LivenessAction> completedActions;
final double confidenceScore; // 0.0โ1.0 composite anti-spoof score
final bool isRealHuman;
final bool spoofDetected;
final bool deepfakeDetected; // true if TFLite score < tfliteDeepfakeThreshold
final double? tfliteScore; // FaceAntiSpoofing real-face probability
final double? videoReplayScore; // MiniFASNet real-face probability (min of 8 signals)
final bool videoReplayDetected; // true when videoReplayScore < videoReplayThreshold
final String? failureReason;
final int? sessionDurationMs;
final String? sessionId; // "LV-{12-char-hex}-{8-char-hex}"
// Face Identity โ non-null when enableFaceId: true
final String? faceId; // "FID-{24-char-hex}"
final bool? isFaceIdNew; // true = first-time, false = recognised
final bool? faceAlreadyRegistered; // true when registrationOnly + face already exists
final double? faceMatchScore; // cosine similarity from gallery search (0.0โ1.0)
// Best-Frontal Capture โ non-null when enableBestFrontalCapture: true and isSuccess
final Uint8List? bestFrontalImageBytes; // upright JPEG of the most-frontal frame
}
LivenessController API #
final controller = LivenessController(
actions: [LivenessAction.blink, LivenessAction.turnLeft],
config: LivenessConfig(enableFaceId: true, enableVideoReplayDetection: true),
onSuccess: (result) { ... },
onFailed: (reason) { ... },
);
await controller.initialize();
Public getters #
| Getter | Type | Description |
|---|---|---|
isInitialized |
bool |
True after camera + models ready |
isComplete |
bool |
True when all liveness actions finished |
status |
DetectionStatus |
Current detection state |
currentAction |
LivenessAction? |
Action user must perform now |
completedActions |
List<LivenessAction> |
Completed this session |
remainingActions |
List<LivenessAction> |
Still to complete |
completedCount |
int |
Number of completed actions |
progress |
double |
0.0โ1.0 completion fraction |
sessionId |
String? |
Current session ID (LV-โฆ) |
currentFace |
FaceData? |
Latest detected face (includes landmark positions) |
lastQuality |
FrameQuality? |
Latest frame quality |
tfliteWarning |
String? |
Non-null if TFLite model failed to load or inference errored |
tfliteModelDownloadProgress |
double? |
0.0โ1.0 while TFLite model is downloading |
faceIdModelDownloadProgress |
double? |
0.0โ1.0 while FaceNet model is downloading |
yoloModelDownloadProgress |
double? |
0.0โ1.0 while YOLOv8n-face model is downloading (only relevant when faceDetectorBackend: .yolov8) |
lastTfliteScore |
double? |
Latest FaceAntiSpoofing real-face probability |
lastVideoReplayScore |
double? |
Latest MiniFASNet raw real-face score |
liveHeuristicScore |
double? |
S2 rolling brightness-variance score |
liveLaplacianScore |
double? |
S1 rolling Laplacian texture variance |
liveHetScore |
double? |
S3 motion heterogeneity CVยฒ |
liveReplayScore |
double? |
S5 ReplayAnalyzer rolling score |
liveScreenScore |
double? |
S6 ScreenArtifactDetector rolling score |
liveFlowScore |
double? |
S7 OpticalFlowAnalyzer rolling score |
liveGeoScore |
double? |
S8 FaceGeometryAnalyzer rolling score |
liveMeshDepthScore |
double? |
Face Mesh 3-D depth score (non-null when enableFaceMesh: true) |
error |
String? |
Non-null if initialization failed |
cameraController |
CameraController? |
Underlying camera controller |
DetectionStatus values #
| Status | Meaning |
|---|---|
initializing |
Camera / models loading |
noFace |
No face detected |
multipleFaces |
More than one face visible |
faceTooFar |
Move closer |
faceTooClose |
Move back |
faceNotCentered |
Centre face in oval |
lowLight |
Too dark (6-frame debounce) |
overExposed |
Too bright (6-frame debounce) |
blurry |
Out of focus |
fakeDetected |
Spoof / duplicate-frame triggered |
ready |
Face detected and centred โ waiting for action to begin |
actionInProgress |
Performing challenge |
completed |
All actions done |
failed |
Timed out or manually failed |
Methods #
await controller.initialize();
await controller.reset();
await controller.clearFaceIdentities();
await controller.dispose();
TFLite Integration (Optional) #
Both models auto-download on first use, run in background isolates, and are cached permanently.
FaceAntiSpoofing (3.9 MB) #
config: LivenessConfig(
enableTFLite: true,
tfliteDeepfakeThreshold: 0.40,
)
MiniFASNet-V2 Video Replay (1.7 MB) #
Enables the full 8-signal replay detection pipeline.
config: LivenessConfig(
enableVideoReplayDetection: true,
videoReplayThreshold: 0.50,
)
onSuccess: (result) {
print('Replay score : ${result.videoReplayScore}'); // min of 8 signals
print('Replay attack: ${result.videoReplayDetected}'); // true = rejected
},
onFailed: (reason) => print('Rejected: $reason'),
// e.g. "Video replay attack detected (23.4% real)"
Custom model #
config: LivenessConfig(
enableTFLite: true,
tfliteModelUrl: 'https://your-cdn.com/custom_model.tflite',
tfliteInputSize: 128,
),
Architecture #
Camera stream (20 fps)
โ
โโ FrameProcessor (background isolate)
โ YUVโNV21 ยท brightness ยท blur ยท FNV-1a hash
โ
โโ ML Kit FaceDetector (main isolate, platform channel)
โ Euler angles ยท eye probabilities ยท 10 landmarks
โ
โโ Per-frame signals (main isolate, pure Dart)
โ S1 Laplacian variance (face crop texture)
โ S2 Brightness variance (AEC-sensitive)
โ S3 Motion heterogeneity (9-region CVยฒ, AEC-invariant)
โ S5 ReplayAnalyzer (fingerprint + jitter + entropy + blink)
โ S6 ScreenArtifactDetector (specular + warmth + stability)
โ S7 OpticalFlowAnalyzer (32ร32 block-MAD)
โ S8 FaceGeometryAnalyzer (landmarks + depth + velocity)
โ
โโ TFLite inference (persistent background isolates)
โ S4 MiniFASNet-V2 (video replay model)
โ FaceAntiSpoofing (deepfake model)
โ YoloFaceDetectorService (identity-eligible frames only, faceDetectorBackend: yolov8)
โ
โโ LivenessEngine
โ Active challenge tracking ยท action detection ยท timeout
โ
โโ Face Identity (enableFaceId: true)
โ FacePreprocessor โ FaceEmbeddingModel โ FaceIdentityService (gallery match)
โ Box/eyes from ML Kit, or from YoloFaceDetectorService if selected
โ
โโ LivenessController (ChangeNotifier)
Combine all signals ยท build LivenessResult ยท fire callbacks
FaceCaptureService (standalone โ photo, not a live session)
Decode photo โ detect (ML Kit or YoloFaceDetectorService) โ quality gate
โ FacePreprocessor โ FaceEmbeddingModel โ same FaceIdentityService gallery
Threading model:
| Work | Thread |
|---|---|
| ML Kit face detection | Main isolate (platform channel) |
| YUV โ NV21 + quality | Background isolate (compute()) |
| S1โS3, S5โS8 pixel analysis | Main isolate (pure Dart, < 2 ms/frame) |
| S4 TFLite inference | Persistent background isolate (zero-copy transfer) |
| YOLOv8n-face inference | Persistent background isolate (zero-copy transfer) โ same pattern as S4 |
| FaceNet embedding | Background isolate (compute()) |
| UI rendering | Main thread โ never blocked |
Performance #
| Metric | Value |
|---|---|
| Per-frame latency โ mid-range Android | 40โ65 ms |
| Per-frame latency โ iPhone 12+ | 20โ40 ms |
| S5โS8 signal computation (pure Dart) | < 2 ms/frame |
| OpticalFlow 32ร32 block-MAD | ~0.5 ms/frame |
| FaceNet inference (warm) | 30โ50 ms |
| YOLOv8n-face inference (warm) | Not yet benchmarked on-device โ runs on its own background isolate, so it adds isolate-worker overhead rather than blocking frame delivery; only invoked on identity-eligible frames (~7โ15 per session), not continuously |
| Memory โ base | ~45 MB |
| Memory โ with Face ID | ~90 MB |
Memory โ with faceDetectorBackend: yolov8 |
+ a second TFLite interpreter (~12 MB model) in its own isolate โ not yet measured in aggregate |
Tuning tips:
- Lower
targetFpsto15on low-end devices - Use
ResolutionPreset.mediumfor 60 fps UI on older phones - Set
enableFaceId: falseif you don't need identity โ saves ~45 MB RAM faceDetectorBackend: mlkit(default) has zero extra cost over the base Face ID path โ only switch toyolov8if you've measured a real accuracy benefit for your use case
Lighting & Brightness #
The SDK checks frame brightness on every camera frame using BT.601 platform-correct luminance (NV21 Y-plane on Android, weighted RGB on iOS). Poor lighting is one of the most common causes of slow or failed detection.
How it works #
| Condition | Status triggered | Threshold |
|---|---|---|
| Too dark | DetectionStatus.lowLight |
Luminance < brightnessMin (default 0.12) |
| Too bright / overexposed | DetectionStatus.overExposed |
Luminance > brightnessMax (default 0.92) |
Both statuses use a 6-frame debounce โ the camera must report bad brightness for 6 consecutive frames before the status changes. This absorbs auto-exposure settling time when the user first points the camera.
What gets affected #
ML Kit face detection
- Blink, turn, and smile detection all rely on accurate landmark positions. In very low light, ML Kit's keypoints become noisy or disappear entirely โ actions may not register.
Face ID embeddings
- Low-light or overexposed crops produce face embeddings with degenerate L2 norm or near-zero variance. These are automatically discarded by the
minEmbeddingQualityfilter (0.50default). If all collected frames are rejected, face matching cannot complete.
Anti-spoof score
- The
AntiSpoofEngineincludes brightness variance as one of its 9 heuristic signals. Extremely low or high brightness reduces the compositeconfidenceScore, which may push it belowantiSpoofThresholdand fail the session.
Video replay detection (S1, S2)
- S1 (Laplacian texture variance) drops in low light โ skin micro-texture is lost in noise. This can lower the replay score on genuine faces.
- S2 (temporal brightness variance) expects subtle room-light fluctuation. Pitch-black or blown-out environments produce flat variance scores.
Best lighting conditions #
- Soft indoor ceiling light or natural daylight facing the user (not behind them)
- Avoid strong backlighting (window behind the user) โ causes face underexposure
- Avoid direct sunlight into the camera โ causes overexposure
- Minimum ~100 lux equivalent; standard office lighting is ideal
Tuning #
config: LivenessConfig(
enableBrightnessCheck: true, // default โ always keep enabled
brightnessMin: 0.10, // lower if users are in dimmer environments
brightnessMax: 0.95, // raise if outdoor users hit false overexposure
)
// Disable entirely only for controlled kiosk setups with fixed lighting:
config: LivenessConfig(
enableBrightnessCheck: false,
)
Security #
| Threat | Mitigation |
|---|---|
| Printed photo | Eye variance + geometry (AntiSpoofEngine) ยท Laplacian variance (S1) ยท Stasis detection (S7) ยท Flat-surface depth check (S8) |
| Static image held to camera | FNV-1a duplicate-frame detection ยท Stasis (S7) ยท Landmark velocity (S8) |
| Pre-recorded video replay | MiniFASNet-V2 (S4) ยท Perceptual fingerprint (S5) ยท Temporal stability (S6) ยท Rigid-motion flow (S7) |
| Mobile/tablet screen replay | Specular highlights (S6) ยท Skin warmth (S6, iOS) ยท Screen backlight stability (S2, S6) ยท Angular micro-jitter (S5) |
| Stabilised/compressed video | Laplacian variance (S1) ยท Motion jitter (S5) ยท Optical flow variance (S7) |
| Deepfake / synthetic face | FaceAntiSpoofing TFLite (enableTFLite: true) |
| Looped video | FNV-1a frame hash ยท Perceptual fingerprint (S5) |
| Predictable action sequence | Fisher-Yates shuffle per session |
| Session replay | sessionId via Random.secure() |
| Identity spoofing | FaceNet cosine similarity + isFaceIdNew flag |
For high-assurance KYC (banking, government), pair
sessionIdandfaceIdwith a server-side signature step.
Example App #
cd example
flutter run
Eight challenge presets: Standard ยท Extended ยท Full ยท Face ID Auto ยท Register Face ยท Verify Face ยท With TFLite Anti-Spoof ยท Best-Frontal Capture.
Testing replay detection:
- Enable
showDebugOverlay: truein the example config - Run the check normally โ all 8 signal bars should be green (
ok) - Play a recording of yourself on another device and point the camera at it โ signals S1, S5, S6, S7 should drop below threshold and flag the session
Changelog #
See CHANGELOG.md for full release history.
Latest: v3.3.0 โ Optional YOLOv8n-face detector backend for the identity pipeline (FaceDetectorBackend), FaceCaptureService for photo-based enrollment/verification, both running on dedicated background isolates.
Previous: v3.2.0 โ Faster action detection, camera initialization race condition fix, replay attack tuning guidance.
License #
MIT โ see LICENSE
Author #
Developed by Sanjay Sharma
GitHub: sanjaysharmajw/flutter_face_liveness
Issues: github.com/sanjaysharmajw/flutter_face_liveness/issues
Support #
If this package saved you time, consider buying me a coffee โ