human_security 0.2.1 copy "human_security: ^0.2.1" to clipboard
human_security: ^0.2.1 copied to clipboard

On-device face liveness verification with ML Kit active challenges, TFLite anti-spoofing, and optional face identity matching.

example/lib/main.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:human_security/human_security.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const FaceLivenessExampleApp());
}

class FaceLivenessExampleApp extends StatelessWidget {
  const FaceLivenessExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Face Liveness Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1B5E20)),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _urlController = TextEditingController();
  final _picker = ImagePicker();
  bool _enableFaceMatch = false;
  File? _referenceFile;
  String? _referenceUrl;

  @override
  void dispose() {
    _urlController.dispose();
    super.dispose();
  }

  ReferenceImageSource? get _referenceSource {
    if (!_enableFaceMatch) return null;
    if (_referenceFile != null) {
      return ReferenceImageSource.file(_referenceFile!);
    }
    final url = _referenceUrl ?? _urlController.text.trim();
    if (url.isNotEmpty) {
      return ReferenceImageSource.url(url);
    }
    return null;
  }

  Future<void> _pickReferenceImage() async {
    final picked = await _picker.pickImage(
      source: ImageSource.gallery,
      imageQuality: 90,
      maxWidth: 1920,
    );
    if (picked == null) return;
    setState(() {
      _referenceFile = File(picked.path);
      _referenceUrl = null;
      _urlController.clear();
    });
  }

  void _openLivenessCheck({required bool passive}) {
    final reference = _referenceSource;
    if (_enableFaceMatch && (reference == null || reference.isEmpty)) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Pick a reference image or enter an image URL first.'),
        ),
      );
      return;
    }

    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => LivenessCheckPage(
          enablePassiveLiveness: passive,
          referenceImage: reference,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Face Liveness Demo'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Icon(Icons.verified_user, size: 72),
            const SizedBox(height: 24),
            const Text(
              'Test live face verification',
              style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 12),
            const Text(
              'Runs active ML Kit challenges and passive TFLite anti-spoofing. '
              'Optionally compare the live capture against a reference photo.',
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 24),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Compare with reference image'),
              subtitle: const Text(
                'Match live face against uploaded or URL image',
              ),
              value: _enableFaceMatch,
              onChanged: (value) => setState(() => _enableFaceMatch = value),
            ),
            if (_enableFaceMatch) ...[
              OutlinedButton.icon(
                onPressed: _pickReferenceImage,
                icon: const Icon(Icons.photo_library_outlined),
                label: Text(
                  _referenceFile == null
                      ? 'Pick reference from gallery'
                      : 'Reference: ${_referenceFile!.path.split('/').last}',
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _urlController,
                decoration: const InputDecoration(
                  labelText: 'Or paste image URL',
                  border: OutlineInputBorder(),
                  hintText: 'https://example.com/photo.jpg',
                ),
                onChanged: (_) {
                  if (_urlController.text.trim().isNotEmpty) {
                    setState(() {
                      _referenceFile = null;
                      _referenceUrl = _urlController.text.trim();
                    });
                  }
                },
              ),
              if (_referenceFile != null ||
                  (_referenceUrl?.isNotEmpty ?? false))
                Padding(
                  padding: const EdgeInsets.only(top: 12),
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(12),
                    child: _referenceFile != null
                        ? Image.file(
                            _referenceFile!,
                            height: 120,
                            fit: BoxFit.cover,
                          )
                        : Image.network(
                            _referenceUrl!,
                            height: 120,
                            fit: BoxFit.cover,
                            errorBuilder: (context, error, stackTrace) =>
                                const SizedBox(
                                  height: 120,
                                  child: Center(
                                    child: Text('Could not preview URL'),
                                  ),
                                ),
                          ),
                  ),
                ),
              const SizedBox(height: 24),
            ],
            FilledButton.icon(
              onPressed: () => _openLivenessCheck(passive: true),
              icon: const Icon(Icons.face_retouching_natural),
              label: const Text('Start Full Liveness Check'),
              style: FilledButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
            ),
            const SizedBox(height: 12),
            OutlinedButton.icon(
              onPressed: () => _openLivenessCheck(passive: false),
              icon: const Icon(Icons.motion_photos_on),
              label: const Text('Active Only (No TFLite)'),
              style: OutlinedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class LivenessCheckPage extends StatefulWidget {
  const LivenessCheckPage({
    super.key,
    required this.enablePassiveLiveness,
    this.referenceImage,
  });

  final bool enablePassiveLiveness;
  final ReferenceImageSource? referenceImage;

  @override
  State<LivenessCheckPage> createState() => _LivenessCheckPageState();
}

class _LivenessCheckPageState extends State<LivenessCheckPage> {
  void _onSuccess(File image, FaceMatchReport? report) {
    if (!mounted) return;
    Navigator.of(context).pushReplacement(
      MaterialPageRoute<void>(
        builder: (_) => ResultPage.success(image: image, report: report),
      ),
    );
  }

  void _onFailure(String reason) {
    if (!mounted) return;
    Navigator.of(context).pushReplacement(
      MaterialPageRoute<void>(
        builder: (_) => ResultPage.failure(
          reason: reason,
          enablePassiveLiveness: widget.enablePassiveLiveness,
          referenceImage: widget.referenceImage,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: FaceLivenessDetector(
          enablePassiveLiveness: widget.enablePassiveLiveness,
          referenceImage: widget.referenceImage,
          onSuccess: _onSuccess,
          onFailure: _onFailure,
        ),
      ),
    );
  }
}

class ResultPage extends StatelessWidget {
  const ResultPage._({
    required this.isSuccess,
    this.image,
    this.report,
    this.reason,
    this.enablePassiveLiveness = true,
    this.referenceImage,
  });

  factory ResultPage.success({required File image, FaceMatchReport? report}) {
    return ResultPage._(isSuccess: true, image: image, report: report);
  }

  factory ResultPage.failure({
    required String reason,
    bool enablePassiveLiveness = true,
    ReferenceImageSource? referenceImage,
  }) {
    return ResultPage._(
      isSuccess: false,
      reason: reason,
      enablePassiveLiveness: enablePassiveLiveness,
      referenceImage: referenceImage,
    );
  }

  final bool isSuccess;
  final File? image;
  final FaceMatchReport? report;
  final String? reason;
  final bool enablePassiveLiveness;
  final ReferenceImageSource? referenceImage;

  @override
  Widget build(BuildContext context) {
    final matchEnabled = report?.referenceImage != null;

    return Scaffold(
      appBar: AppBar(title: Text(isSuccess ? 'Verified' : 'Failed')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Icon(
              isSuccess ? Icons.check_circle : Icons.cancel,
              size: 80,
              color: isSuccess ? Colors.green : Colors.red,
            ),
            const SizedBox(height: 16),
            Text(
              isSuccess
                  ? 'Live face verified successfully.'
                  : reason ?? 'Verification failed.',
              textAlign: TextAlign.center,
              style: const TextStyle(fontSize: 18),
            ),
            if (report != null) ...[
              const SizedBox(height: 24),
              _ScoreCard(
                label: 'Liveness',
                value: report!.isLive ? 'Passed' : 'Failed',
                progress: report!.livenessResult?.combinedScore ?? 0,
              ),
              if (matchEnabled) ...[
                const SizedBox(height: 12),
                _ScoreCard(
                  label: 'Face match',
                  value: '${(report!.similarityScore * 100).round()}%',
                  progress: report!.similarityScore,
                  subtitle: report!.isSamePerson
                      ? 'Same person'
                      : 'Different person',
                ),
              ],
            ],
            if (isSuccess && image != null) ...[
              const SizedBox(height: 24),
              if (matchEnabled && report?.referenceImage != null)
                Row(
                  children: [
                    Expanded(
                      child: _ImagePreview(
                        label: 'Reference',
                        image: report!.referenceImage!,
                      ),
                    ),
                    const SizedBox(width: 12),
                    Expanded(
                      child: _ImagePreview(
                        label: 'Live capture',
                        image: image!,
                      ),
                    ),
                  ],
                )
              else
                ClipRRect(
                  borderRadius: BorderRadius.circular(12),
                  child: Image.file(
                    image!,
                    height: 240,
                    width: double.infinity,
                    fit: BoxFit.cover,
                  ),
                ),
            ],
            const SizedBox(height: 32),
            FilledButton(
              onPressed: () {
                Navigator.of(context).pushAndRemoveUntil(
                  MaterialPageRoute<void>(builder: (_) => const HomePage()),
                  (_) => false,
                );
              },
              child: const Text('Back to Home'),
            ),
            const SizedBox(height: 8),
            if (!isSuccess)
              TextButton(
                onPressed: () {
                  Navigator.of(context).pushReplacement(
                    MaterialPageRoute<void>(
                      builder: (_) => LivenessCheckPage(
                        enablePassiveLiveness: enablePassiveLiveness,
                        referenceImage: referenceImage,
                      ),
                    ),
                  );
                },
                child: const Text('Try Again'),
              ),
          ],
        ),
      ),
    );
  }
}

class _ScoreCard extends StatelessWidget {
  const _ScoreCard({
    required this.label,
    required this.value,
    required this.progress,
    this.subtitle,
  });

  final String label;
  final String value;
  final double progress;
  final String? subtitle;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  label,
                  style: const TextStyle(fontWeight: FontWeight.w600),
                ),
                Text(value),
              ],
            ),
            if (subtitle != null) ...[
              const SizedBox(height: 4),
              Text(subtitle!, style: Theme.of(context).textTheme.bodySmall),
            ],
            const SizedBox(height: 8),
            LinearProgressIndicator(value: progress.clamp(0, 1)),
          ],
        ),
      ),
    );
  }
}

class _ImagePreview extends StatelessWidget {
  const _ImagePreview({required this.label, required this.image});

  final String label;
  final File image;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
        const SizedBox(height: 8),
        ClipRRect(
          borderRadius: BorderRadius.circular(12),
          child: Image.file(
            image,
            height: 140,
            width: double.infinity,
            fit: BoxFit.cover,
          ),
        ),
      ],
    );
  }
}
2
likes
160
points
166
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

On-device face liveness verification with ML Kit active challenges, TFLite anti-spoofing, and optional face identity matching.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

camera, flutter, google_mlkit_face_detection, http, image, path, path_provider, rxdart, tflite_flutter

More

Packages that depend on human_security