smart_liveliness_detection 0.2.0
smart_liveliness_detection: ^0.2.0 copied to clipboard
This package helps you verify that a real person is present in front of the camera, not a photo, video, or mask.
import 'dart:developer';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import 'package:smart_liveliness_detection/smart_liveliness_detection.dart';
import 'package:smart_liveliness_detection/src/config/messages_config.dart';
import 'package:smart_liveliness_detection/src/utils/enums.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Lock orientation to portrait
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
// Optional: Set immersive mode
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
);
// Get available cameras
final cameras = await availableCameras();
runApp(FaceLivenessExampleApp(cameras: cameras));
}
class FaceLivenessExampleApp extends StatelessWidget {
final List<CameraDescription> cameras;
const FaceLivenessExampleApp({
super.key,
required this.cameras,
});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Face Liveness Detection Demo',
theme: ThemeData.dark(),
debugShowCheckedModeBanner: false,
home: HomeScreen(cameras: cameras),
);
}
}
class HomeScreen extends StatelessWidget {
final List<CameraDescription> cameras;
const HomeScreen({
super.key,
required this.cameras,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Face Liveness Examples'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Select a liveness detection example:',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 24),
_buildExampleButton(
context,
'Default Style',
'Use the package with default settings',
() => _navigateToLivenessScreen(
context,
const LivenessConfig(),
const LivenessTheme(),
),
),
_buildExampleButton(
context,
'Custom Theme',
'Custom colors and styling',
() => _navigateToLivenessScreen(
context,
const LivenessConfig(),
const LivenessTheme(
primaryColor: Colors.purple,
ovalGuideColor: Colors.purpleAccent,
successColor: Colors.green,
errorColor: Colors.redAccent,
overlayOpacity: 0.6,
progressIndicatorColor: Colors.purpleAccent,
instructionTextStyle: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
useOvalPulseAnimation: true,
),
),
),
_buildExampleButton(
context,
'Custom Challenges',
'Specific challenge sequence with custom messages',
() {
const customConfig = LivenessConfig(
initialZoomFactor: 0.1,
challengeTypes: [
ChallengeType.blink,
ChallengeType.zoom,
ChallengeType.turnRight,
ChallengeType.turnLeft,
ChallengeType.tiltUp,
ChallengeType.tiltDown,
ChallengeType.smile,
ChallengeType.normal,
//ChallengeType.nod,
],
challengeInstructions: {
ChallengeType.blink: 'Blink your eyes slowly',
ChallengeType.zoom: 'Bring your face closer slowly',
ChallengeType.turnRight: 'Turn your head to the right',
ChallengeType.turnLeft: 'Turn your head to the left',
ChallengeType.tiltUp: 'Tilt up your head',
ChallengeType.tiltDown: 'Tilt down your head',
ChallengeType.smile: 'Show me your best smile',
ChallengeType.normal: 'Center Your Face',
//ChallengeType.nod: 'Nod your head up and down',
},
);
_navigateToLivenessScreen(
context, customConfig, const LivenessTheme(
successColor: Colors.green,
errorColor: Colors.redAccent,
guidanceTextStyle: TextStyle(
//color: Color(0xFF2E38B7),
fontSize: 18,
fontWeight: FontWeight.w500,
),
));
},
),
_buildExampleButton(
context,
'Custom Challenges and Status Messages',
'Specific challenge sequence with custom messages',
() {
const customConfig = LivenessConfig(
initialZoomFactor: 0.1,
challengeTypes: [
ChallengeType.blink,
ChallengeType.zoom,
ChallengeType.turnRight,
ChallengeType.turnLeft,
ChallengeType.tiltUp,
ChallengeType.tiltDown,
ChallengeType.smile,
ChallengeType.normal,
//ChallengeType.nod,
],
challengeInstructions: {
ChallengeType.blink: 'Pisque os olhos lentamente',
ChallengeType.zoom: 'Aproxime o rosto lentamente',
ChallengeType.turnRight: 'Vire a cabeça para o lado direito',
ChallengeType.turnLeft: 'Vire a cabeça para o lado esquerdo',
ChallengeType.tiltUp: 'Incline a cabeça para cima',
ChallengeType.tiltDown: 'Incline a cabeça para baixo',
ChallengeType.smile: 'Mostre seu melhor sorriso',
ChallengeType.normal: 'Centralize seu rosto',
//ChallengeType.nod: 'Acene com a cabeça',
},
messages: LivenessMessages(
moveFartherAway: 'Afaste-se um pouco',
moveCloser: 'Aproxime-se um pouco',
moveLeft: 'Mova para a esquerda',
moveRight: 'Mova para a direita',
moveUp: 'Mova para cima',
moveDown: 'Mova para baixo',
perfectHoldStill: 'Perfeito! Fique parado',
noFaceDetected: 'Nenhum rosto detectado',
errorCheckingFacePosition: 'Ocorreu um erro no processamento',
initializing: 'Inicializando...',
initializingCamera: 'Inicializando a camera...',
errorInitializingCamera: 'Erro ao inicializar a câmera. Reinicie o aplicativo.',
initialInstruction: 'Posicione seu rosto no oval',
poorLighting: 'Mova-se para uma área mais iluminada',
processingVerification: 'Processando verificação...',
verificationComplete: 'Verificação de vivacidade concluída!',
spoofingDetected: "Possível falsificação detectada: rosto movido, mas dispositivo não",
errorProcessing: 'Ocorreu um erro de processamento',
),
);
_navigateToLivenessScreen(
context, customConfig, const LivenessTheme(
successColor: Colors.green,
errorColor: Colors.redAccent,
guidanceTextStyle: TextStyle(
//color: Color(0xFF2E38B7),
fontSize: 18,
fontWeight: FontWeight.w500,
),
//overlayColor: Colors.white,
// overlayOpacity: 0.85,
// primaryColor: Colors.white,
// ovalGuideColor: Colors.white, //.withValues(alpha: 0.87),
// appBarBackgroundColor: Colors.white,
// appBarTextColor: Colors.black87
));
},
),
_buildExampleButton(
context,
'Material Design',
'Theme based on Material Design',
() {
final materialTheme = LivenessTheme.fromMaterialColor(
Colors.teal,
brightness: Brightness.dark,
);
_navigateToLivenessScreen(
context, const LivenessConfig(), materialTheme);
},
),
_buildExampleButton(
context,
'Capture User Image',
'Take a photo after successful verification',
() => _navigateToLivenessWithImageCapture(context),
),
_buildExampleButton(
context,
'Custom Configuration',
'Modified thresholds and settings',
() {
const customConfig = LivenessConfig(
maxSessionDuration: Duration(minutes: 3),
eyeBlinkThresholdOpen: 0.8,
eyeBlinkThresholdClosed: 0.2,
smileThresholdSmiling: 0.8,
headTurnThreshold: 15.0,
ovalHeightRatio: 0.7,
ovalWidthRatio: 0.8,
strokeWidth: 5.0,
numberOfRandomChallenges: 2,
);
_navigateToLivenessScreen(
context, customConfig, const LivenessTheme());
},
),
],
),
),
);
}
Widget _buildExampleButton(
BuildContext context,
String title,
String description,
VoidCallback onPressed,
) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: InkWell(
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
description,
style: TextStyle(
color: Colors.grey[400],
),
),
],
),
),
),
);
}
void _navigateToLivenessScreen(
BuildContext context,
LivenessConfig config,
LivenessTheme theme,
) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LivenessDetectionScreen(
cameras: cameras,
config: config,
theme: theme,
onChallengeCompleted: (challengeType) {
log('Challenge completed: ${challengeType.name}');
},
onLivenessCompleted: (sessionId, isSuccessful, metadata) {
log('Liveness verification completed:');
log('Session ID: $sessionId');
log('Success: $isSuccessful');
},
onFaceDetected: (ChallengeType challengeType, CameraImage image, List<Face> faces, CameraDescription camera) {
log('onFaceDetected - current Challenge: ${challengeType.name}');
},
onFaceNotDetected: (ChallengeType challengeType, LivenessController controller) {
log('onFaceNotDetected - current Challenge: ${challengeType.name}');
// Reset session if face is not detected and the head is not turned
if(![ChallengeType.tiltDown, ChallengeType.tiltUp, ChallengeType.nod].contains(challengeType)) {
controller.resetSession();
}
},
),
),
);
}
void _navigateToLivenessWithImageCapture(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LivenessDetectionScreen(
cameras: cameras,
config: const LivenessConfig(
alwaysIncludeBlink: true,
challengeTypes: [
ChallengeType.turnLeft,
ChallengeType.turnRight,
ChallengeType.blink,
],
),
theme: LivenessTheme.fromMaterialColor(
Colors.blue,
brightness: Brightness.dark,
),
// Enable single final image capture
captureFinalImage: true,
// Show a button for manual capture as well
showCaptureImageButton: true,
showStatusIndicators: false,
showAppBar: false,
captureButtonText: 'Take Photo',
// Process the final verification image
onFinalImageCaptured: (sessionId, imageFile, metadata) {
log('Final image captured:');
log('Session ID: $sessionId');
log('Image path: ${imageFile.path}');
log('Metadata: $metadata');
// Show a dialog with the captured image
if (context.mounted) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Verification Complete'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Session ID: $sessionId'),
const SizedBox(height: 8),
Text('Image saved to: ${imageFile.path}'),
const SizedBox(height: 16),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(imageFile.path),
height: 200,
width: 200,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
log('Error loading image: $error');
return const Text('Could not load image preview');
},
),
),
const SizedBox(height: 8),
Text(
'Challenge count: ${(metadata['challenges'] as List).length}'),
Text('Duration: ${metadata['sessionDuration']} ms'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Close'),
),
],
),
);
}
},
// Handle manual capture separately
onManualImageCaptured: (sessionId, imageFile) {
log('Manual image captured:');
log('Session ID: $sessionId');
log('Image path: ${imageFile.path}');
if (context.mounted) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Manual Image Captured'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Session ID: $sessionId'),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(imageFile.path),
height: 200,
width: 200,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
log('Error loading image: $error');
return const Text('Could not load image preview');
},
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Close'),
),
],
),
);
}
},
onLivenessCompleted: (sessionId, isSuccessful, metadata) {
log('Liveness verification completed:');
log('Session ID: $sessionId');
log('Success: $isSuccessful');
},
),
),
);
}
}