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.
example/lib/main.dart
import 'dart:io';
import 'package:facecore/facecore.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Facecore Demo',
home: const HomePage(),
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _facecore = Facecore();
final Set<LivenessAction> _selectedActions = {LivenessAction.blink};
CameraLens _lens = CameraLens.back;
bool _enableDepth = true;
bool _requireDepthHw = false;
bool _enableMoire = true;
bool _enableBanding = true;
bool _enableScreenEdge = true;
bool? _hasDepthCameraForLens;
FaceCaptureResult? _livenessResult;
AntiSpoofResult? _antiSpoofResult;
String? _error;
bool _busy = false;
@override
void initState() {
super.initState();
_refreshDepthCapability();
}
Future<void> _refreshDepthCapability() async {
final has = await _facecore.hasDepthCamera(lens: _lens);
if (!mounted) return;
setState(() {
_hasDepthCameraForLens = has;
if (!has) _requireDepthHw = false;
});
}
Future<void> _runLiveness() async {
if (_selectedActions.isEmpty) {
setState(() => _error = 'Pick at least one liveness action.');
return;
}
setState(() {
_busy = true;
_error = null;
_antiSpoofResult = null;
});
try {
final result = await _facecore.captureWithLiveness(
config: LivenessConfig(
actions: [
for (final a in LivenessAction.values)
if (_selectedActions.contains(a)) a,
],
title: 'Verify your identity',
subtitle: 'Follow the prompts to confirm you are present.',
),
);
setState(() => _livenessResult = result);
} on FaceCaptureException catch (e) {
setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _runAntiSpoof() async {
setState(() {
_busy = true;
_error = null;
_livenessResult = null;
});
try {
final result = await _facecore.captureWithAntiSpoof(
config: AntiSpoofConfig(
lens: _lens,
enableDepth: _enableDepth,
requireDepthHardware: _requireDepthHw,
enableMoire: _enableMoire,
enableBanding: _enableBanding,
enableScreenEdge: _enableScreenEdge,
title: 'Anti-spoof capture',
subtitle: 'Hold steady — verifying the scene is real.',
),
);
setState(() => _antiSpoofResult = result);
} on FaceCaptureException catch (e) {
setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
String _label(LivenessAction action) => switch (action) {
LivenessAction.blink => 'Blink',
LivenessAction.turnLeft => 'Turn left',
LivenessAction.turnRight => 'Turn right',
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Facecore Demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('Liveness capture',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
for (final action in LivenessAction.values)
FilterChip(
label: Text(_label(action)),
selected: _selectedActions.contains(action),
onSelected: _busy
? null
: (selected) => setState(() {
if (selected) {
_selectedActions.add(action);
} else {
_selectedActions.remove(action);
}
}),
),
],
),
const SizedBox(height: 8),
FilledButton(
onPressed: _busy ? null : _runLiveness,
child: const Text('Run liveness'),
),
const Divider(height: 32),
const Text('Anti-spoof capture',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
const SizedBox(height: 8),
SegmentedButton<CameraLens>(
segments: const [
ButtonSegment(value: CameraLens.back, label: Text('Back')),
ButtonSegment(value: CameraLens.front, label: Text('Front')),
],
selected: {_lens},
onSelectionChanged: _busy
? null
: (s) {
setState(() => _lens = s.first);
_refreshDepthCapability();
},
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
_hasDepthCameraForLens == null
? 'Checking depth camera…'
: _hasDepthCameraForLens!
? 'Depth camera detected on this lens.'
: 'No depth camera on this lens — fail-closed is disabled.',
style: TextStyle(
color: _hasDepthCameraForLens == false
? Colors.orange
: Colors.grey.shade700,
fontSize: 12,
),
),
),
SwitchListTile(
value: _enableDepth,
title: const Text('Depth check'),
onChanged: _busy ? null : (v) => setState(() => _enableDepth = v),
),
SwitchListTile(
value: _requireDepthHw,
title: const Text('Require depth hardware (fail-closed)'),
subtitle: _hasDepthCameraForLens == false
? const Text('Disabled: device has no depth camera for this lens.')
: null,
onChanged: (_busy ||
!_enableDepth ||
_hasDepthCameraForLens == false)
? null
: (v) => setState(() => _requireDepthHw = v),
),
SwitchListTile(
value: _enableMoire,
title: const Text('Moiré detection'),
onChanged: _busy ? null : (v) => setState(() => _enableMoire = v),
),
SwitchListTile(
value: _enableBanding,
title: const Text('Refresh-banding detection'),
onChanged: _busy ? null : (v) => setState(() => _enableBanding = v),
),
SwitchListTile(
value: _enableScreenEdge,
title: const Text('Screen-edge detection'),
onChanged: _busy
? null
: (v) => setState(() => _enableScreenEdge = v),
),
FilledButton(
onPressed: _busy ? null : _runAntiSpoof,
child: const Text('Run anti-spoof'),
),
const SizedBox(height: 16),
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
if (_livenessResult != null) _LivenessSummary(result: _livenessResult!),
if (_antiSpoofResult != null)
_AntiSpoofSummary(result: _antiSpoofResult!),
],
),
);
}
}
class _LivenessSummary extends StatelessWidget {
final FaceCaptureResult result;
const _LivenessSummary({required this.result});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text('Saved: ${result.imagePath}'),
Text('Size: ${result.width}×${result.height}'),
Text('Motion stddev: ${result.motionStdDev.toStringAsFixed(2)}'),
if (result.depthRange != null)
Text('Depth range: ${result.depthRange!.toStringAsFixed(2)}'),
const SizedBox(height: 8),
SizedBox(
height: 240,
child: Image.file(File(result.imagePath), fit: BoxFit.contain),
),
],
);
}
}
class _AntiSpoofSummary extends StatelessWidget {
final AntiSpoofResult result;
const _AntiSpoofSummary({required this.result});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text('Saved: ${result.imagePath}'),
Text('Size: ${result.width}×${result.height}'),
if (result.moireScore != null)
Text('Moiré score: ${result.moireScore!.toStringAsFixed(3)}'),
if (result.bandingScore != null)
Text('Banding score: ${result.bandingScore!.toStringAsFixed(3)}'),
if (result.depthRange != null)
Text('Depth range: ${result.depthRange!.toStringAsFixed(2)}'),
if (result.screenEdgeDetected != null)
Text('Screen edge detected: ${result.screenEdgeDetected}'),
const SizedBox(height: 8),
SizedBox(
height: 240,
child: Image.file(File(result.imagePath), fit: BoxFit.contain),
),
],
);
}
}