face_overlay 0.1.0 copy "face_overlay: ^0.1.0" to clipboard
face_overlay: ^0.1.0 copied to clipboard

Real-time AR face overlay for Flutter. Uses CameraX (Android) and AVFoundation (iOS) with Google MediaPipe Face Landmarker to detect 478 facial landmarks and render any PNG image as an overlay on dete [...]

example/lib/main.dart

import 'package:face_overlay/face_overlay.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Face Overlay Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(useMaterial3: true),
      home: const _PermissionGate(),
    );
  }
}

// ── Permission gate ────────────────────────────────────────────────────────

class _PermissionGate extends StatefulWidget {
  const _PermissionGate();

  @override
  State<_PermissionGate> createState() => _PermissionGateState();
}

class _PermissionGateState extends State<_PermissionGate> {
  PermissionStatus _status = PermissionStatus.denied;

  @override
  void initState() {
    super.initState();
    _request();
  }

  Future<void> _request() async {
    final s = await Permission.camera.request();
    if (mounted) setState(() => _status = s);
  }

  @override
  Widget build(BuildContext context) {
    if (_status.isGranted) return const DemoScreen();

    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.camera_alt_outlined, size: 72, color: Colors.white38),
              const SizedBox(height: 24),
              const Text('Camera permission required',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
              const SizedBox(height: 12),
              Text(
                _status.isPermanentlyDenied
                    ? 'Permission permanently denied.\nOpen Settings to enable the camera.'
                    : 'This app uses the camera to detect your\nface and place an image overlay on it.',
                textAlign: TextAlign.center,
                style: const TextStyle(color: Colors.white54),
              ),
              const SizedBox(height: 28),
              FilledButton.icon(
                onPressed: _status.isPermanentlyDenied ? openAppSettings : _request,
                icon: Icon(_status.isPermanentlyDenied ? Icons.settings : Icons.camera),
                label: Text(_status.isPermanentlyDenied ? 'Open Settings' : 'Grant permission'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ── Demo screen ────────────────────────────────────────────────────────────

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

  @override
  State<DemoScreen> createState() => _DemoScreenState();
}

class _DemoScreenState extends State<DemoScreen> {
  late FaceOverlayController _controller;
  bool _debug = false;
  int  _faceCount = 0;
  String? _initError;

  @override
  void initState() {
    super.initState();
    _buildController();
  }

  void _buildController() {
    _controller = FaceOverlayController(
      // modelAssetPath defaults to the plugin-bundled model — no override needed.
      config: const FaceOverlayConfig(
        maxFaces: 2,
        minFaceDetectionConfidence: 0.5,
        minTrackingConfidence: 0.5,
      ),
      overlays: const [
        OverlayAsset(assetPath: 'assets/face_overlay.png'),
      ],
    );

    _controller.addListener(() {
      if (!mounted) return;
      if (_controller.lastResult.faceCount != _faceCount) {
        setState(() => _faceCount = _controller.lastResult.faceCount);
      }
    });

    _controller.initialize().catchError((e) {
      if (mounted) setState(() => _initError = e.toString());
    });
  }

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

  // ── Build ────────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        backgroundColor: Colors.black45,
        elevation: 0,
        title: _FaceCountChip(count: _faceCount),
        actions: [
          IconButton(
            tooltip: 'Toggle landmark debug',
            icon: Icon(_debug ? Icons.visibility_off : Icons.grain,
                color: Colors.white70),
            onPressed: () => setState(() => _debug = !_debug),
          ),
          const SizedBox(width: 8),
        ],
      ),
      body: _initError != null
          ? Center(
              child: Text(_initError!,
                  style: const TextStyle(color: Colors.redAccent)))
          : FaceOverlayView(
              controller: _controller,
              isFrontCamera: false,
              debugLandmarks: _debug,
            ),
      bottomNavigationBar: _BottomInfo(faceCount: _faceCount),
    );
  }
}

// ── Supporting widgets ─────────────────────────────────────────────────────

class _FaceCountChip extends StatelessWidget {
  final int count;
  const _FaceCountChip({required this.count});

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      duration: const Duration(milliseconds: 200),
      child: Text(
        count == 0 ? 'Searching for face…'
            : count == 1 ? '1 face detected'
            : '$count faces detected',
        key: ValueKey(count),
        style: const TextStyle(color: Colors.white, fontSize: 15),
      ),
    );
  }
}

class _BottomInfo extends StatelessWidget {
  final int faceCount;
  const _BottomInfo({required this.faceCount});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black54,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      child: SafeArea(
        child: Text(
          faceCount > 0
              ? 'MediaPipe Face Landmarker · 478 landmarks · AR overlay active'
              : 'Point the front camera at a face to start detection',
          textAlign: TextAlign.center,
          style: const TextStyle(color: Colors.white38, fontSize: 12),
        ),
      ),
    );
  }
}
0
likes
140
points
103
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Real-time AR face overlay for Flutter. Uses CameraX (Android) and AVFoundation (iOS) with Google MediaPipe Face Landmarker to detect 478 facial landmarks and render any PNG image as an overlay on detected faces via CustomPainter.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on face_overlay

Packages that implement face_overlay