facecore 0.3.0
facecore: ^0.3.0 copied to clipboard
Self-contained Flutter plugin for camera-based face liveness, depth, and anti-spoof checks before capturing a photo on Android and iOS.
facecore #
A self-contained Flutter plugin with two native capture flows on Android and iOS:
- Liveness capture — front-camera face liveness with challenge actions (blink, turn left, turn right) plus motion + depth anti-spoof gating.
- Anti-spoof capture — generic capture on either lens with a shutter button gated by depth, moiré, refresh-banding, and screen-edge checks. Use it when you want to make sure the user is photographing a real-world scene and not a phone screen, monitor, or printed banner.
Your Dart code calls one method per flow; the platform side handles camera permission, preview, signal analysis, and image capture.
Features #
- Native UI on Android (CameraX + ML Kit) and iOS (AVFoundation + Vision).
- Generic anti-spoof flow with shutter gated by:
- 2D-FFT moiré detection (catches screen subpixel patterns).
- Row-mean refresh-banding detection (catches LCD/OLED refresh stripes).
- Depth analysis — real
AVDepthDataon TrueDepth / LiDAR iOS devices, parallax-based depth proxy elsewhere. - Rectangular screen-edge detection.
- Configurable fail-closed vs fail-open behaviour when the device has no depth sensor.
- Returns the saved JPEG path plus per-signal telemetry.
Platform support #
| Platform | Minimum version |
|---|---|
| Android | API 21 |
| iOS | 13.0 |
Installation #
dependencies:
facecore: ^0.2.0
Android #
Camera permission is declared by the plugin. No additional setup required.
iOS #
Add a usage description to your app's ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to verify your identity.</string>
Liveness capture #
import 'package:facecore/facecore.dart';
final facecore = Facecore();
final result = await facecore.captureWithLiveness(
config: const LivenessConfig(
actions: [
LivenessAction.blink,
LivenessAction.turnLeft,
LivenessAction.turnRight,
],
title: 'Verify your identity',
subtitle: 'Follow the prompts to confirm you are present.',
),
);
if (result == null) {
return; // User cancelled.
}
print('Saved to: ${result.imagePath}');
Detecting depth-camera availability #
requireDepthHardware: true will refuse to enable the shutter on any
device without a depth-capable lens. Decide which mode to use at runtime:
final hasDepth = await facecore.hasDepthCamera(lens: CameraLens.back);
final result = await facecore.captureWithAntiSpoof(
config: AntiSpoofConfig(
lens: CameraLens.back,
requireDepthHardware: hasDepth, // fail-closed only if we can actually use depth
),
);
On iOS this checks for TrueDepth / LiDAR cameras; on Android it scans for
a camera reporting the DEPTH_OUTPUT capability.
Anti-spoof capture #
final result = await facecore.captureWithAntiSpoof(
config: const AntiSpoofConfig(
lens: CameraLens.back,
enableDepth: true,
requireDepthHardware: false, // fail-open on non-depth devices
enableMoire: true,
enableBanding: true,
enableScreenEdge: true,
title: 'Capture',
subtitle: 'Hold steady — verifying the scene is real.',
),
);
if (result == null) {
return; // User cancelled.
}
print('Saved to: ${result.imagePath}');
print('Moiré score: ${result.moireScore}');
print('Banding score: ${result.bandingScore}');
print('Depth range: ${result.depthRange}');
The native screen shows a "Verifying…" overlay and keeps the shutter
button disabled until every enabled signal passes for a few consecutive
frames. The user then taps the shutter to capture. captureWithAntiSpoof
returns null on cancel and throws FaceCaptureException on failure
(permission denied, camera unavailable, depth hardware required but
missing).
See example/ for a runnable demo with toggles for every signal.
AntiSpoofConfig knobs #
| Field | Default | Purpose |
|---|---|---|
lens |
back |
Which camera lens to use (front or back). |
enableDepth |
true |
Run the depth analysis signal. |
requireDepthHardware |
false |
Fail-closed on devices without a depth sensor. |
enableMoire |
true |
Run 2D-FFT moiré detection. |
enableBanding |
true |
Run row-mean refresh-banding detection. |
enableScreenEdge |
true |
Run rectangular screen-edge detection. |
minDepthRange |
8.0 |
Minimum depth range across the scene. |
maxMoireScore |
0.45 |
Maximum moiré score (0 = none, 1 = strong). |
maxBandingScore |
0.40 |
Maximum banding score. |
maxScreenEdgeFraction |
0.70 |
Maximum frame fraction occupied by a detected screen edge. |
jpegQuality |
90 |
JPEG quality for the saved capture. |
title, subtitle |
null |
Optional native screen labels. |
⚠️ Tuning note: anti-spoof thresholds are starting points. Validate against your target hardware and adjust — a tight
maxMoireScorecan false-positive on patterned fabric; a loosemaxBandingScoremay miss high-refresh OLED screens.
LivenessConfig knobs #
| Field | Default | Purpose |
|---|---|---|
actions |
[blink, turnLeft, turnRight] |
Ordered list of liveness challenges to run. |
requireDepth |
false |
Require depth data; rejects flat-image spoofs on supported devices. |
minFaceFraction |
0.18 |
Minimum face area as a fraction of the frame. |
maxFaceFraction |
0.85 |
Maximum face area (too close). |
minMotionStdDev |
1.5 |
Minimum motion variance to accept the capture. |
minDepthRange |
6.0 |
Minimum depth range when requireDepth is true. |
holdFrames |
6 |
Frames the user must hold the target pose. |
jpegQuality |
90 |
JPEG quality (1–100) for the saved capture. |
title, subtitle |
null |
Optional native screen labels. |
License #
MIT — see LICENSE.