flutter-ml / google_ml_kit_flutter

A flutter plugin that implements Google's standalone ML Kit
MIT License
907 stars 709 forks source link

Code not working profile or release mode #630

Closed SanketKudale closed 1 month ago

SanketKudale commented 1 month ago

I am using face smiling and left and right eye open probability code which works in debug mode but not giving proper probability in release mode or profile mode // ignore_for_file: unused_field, unnecessary_null_comparison


import 'dart:convert';
import 'dart:developer';
import 'dart:io';

import 'package:auto_route/auto_route.dart';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_face_api/face_api.dart' as face_api;
import 'package:google_ml_vision/google_ml_vision.dart';
import 'package:image/image.dart' as img;
import 'package:onboarding/routes/app_router.dart';
import 'package:onboarding/utils/colors.dart';
import 'package:onboarding/utils/notification/notification_manager.dart';
import 'package:onboarding/utils/utils.dart';
import 'package:onboarding/view_models/kyc/ekyc_view_model.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';

import '../../utils/ekyc/indian_document.dart';

double _overlayWidth = 0.0;
double _overlayHeight = 0.0;

@RoutePage()
class LivenessScanPage extends StatefulWidget {
  const LivenessScanPage({Key? key}) : super(key: key);

  @override
  State<LivenessScanPage> createState() => _LivenessScanState();
}

class _LivenessScanState extends State<LivenessScanPage>
    with WidgetsBindingObserver {
  CameraController? _controller;
  late GoogleVision _vision;
  bool _isBlinking = false;
  bool _isSmiling = false;

  List<CameraDescription>? cameras;
  final cameraResolution = ResolutionPreset.veryHigh;

  bool _task1Message = false;
  bool _task2Message = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _initializeCamera();
    _vision = GoogleVision.instance;
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    switch (state) {
      case AppLifecycleState.detached:
        break;
      case AppLifecycleState.resumed:
        break;
      case AppLifecycleState.inactive:
        triggerNotification();
        break;
      case AppLifecycleState.hidden:
        break;
      case AppLifecycleState.paused:
        break;
    }
  }

  void _initializeCamera() async {
    availableCameras().then((cameras) {
      CameraDescription backDescription = cameras.firstWhere(
        (camera) => camera.lensDirection == CameraLensDirection.front,
        orElse: () => throw Exception('No front camera found'),
      );

      _controller = CameraController(backDescription, cameraResolution);
      _controller?.initialize().then((_) {
        if (!mounted) return;
        setState(() {
          _detectLiveness();
        });
      }).catchError((e) {
        if (kDebugMode) {
          print('Error initializing camera: $e');
        }
      });
    }).catchError((e) {
      if (kDebugMode) {
        print('Error getting available cameras: $e');
      }
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    clearCacheDirectory();
    _controller!.dispose();
    super.dispose();
  }

  Widget _cameraPreviewWidget() {
    if (_controller == null || !_controller!.value.isInitialized) {
      return const Text(
        'Tap a camera',
        style: TextStyle(
          color: Colors.white,
          fontSize: 24.0,
          fontWeight: FontWeight.w900,
        ),
      );
    }

    return Stack(
      alignment: Alignment.center,
      children: <Widget>[
        (_controller?.value.isInitialized ?? false)
            ? CameraPreview(_controller!)
            : Container(),
        LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            _overlayWidth = constraints.maxWidth;
            _overlayHeight = constraints.maxHeight;
            return CustomPaint(
              size: Size(constraints.maxWidth, constraints.maxHeight),
              painter: OverlayPainter(),
            );
          },
        ),
      ],
    );
  }

  void _detectLiveness() async {
    try {
      String percent = "initial";
      if (!(_controller?.value.isInitialized ?? false)) {
        if (kDebugMode) {
          print('Error: Camera not initialized');
        }
        return;
      }
      XFile image = await _controller!.takePicture();

      final GoogleVisionImage visionImage =
          GoogleVisionImage.fromFile(File(image.path));
      final FaceDetector faceDetector = _vision.faceDetector(
          const FaceDetectorOptions(
              enableTracking: true,
              enableContours: true,
              enableClassification: true));
      final List<Face> faces = await faceDetector.processImage(visionImage);

      Face face = faces.first;

      if (_task1Message == false) {
        if (face.leftEyeOpenProbability != null &&
            face.rightEyeOpenProbability != null) {
          log("Left Eye - ${face.leftEyeOpenProbability} Right Eye - ${face.rightEyeOpenProbability}", level: 2000);
          if (face.leftEyeOpenProbability! < 0.97 &&
              face.rightEyeOpenProbability! < 0.97) {
            setState(() {
              _isBlinking = true;
              _task1Message = true;
              _detectLiveness();
            });
          } else {
            setState(() {
              _isBlinking = false;
            });

            _detectLiveness();
          }
        } else {
          _detectLiveness();
        }
      } else {
        if (_task2Message == false) {
          if (face.smilingProbability != null) {
            log("Face Smiling Probability - ${face.smilingProbability}", level: 2000);
            print("Face Smiling Probability - ${face.smilingProbability}");
            if (face.smilingProbability! > 0.9) {
              setState(() async {
                _isSmiling = true;
                _task2Message = true;

                final Directory tempDir = await getTemporaryDirectory();
                final String cacheDirPath = tempDir.path;
                String filePath = '$cacheDirPath/live_face.png';

                img.Image? originalImage =
                    img.decodeImage(await File(image.path).readAsBytes());
                if (originalImage != null) {
                  File imageFile = await File(filePath)
                      .writeAsBytes(img.encodePng(originalImage));
                  detectAndCropFace(imageFile, live: true);

                  face_api.MatchFacesImage? image1 =
                      await _convertTempFileToMatchFacesImage(
                          '$cacheDirPath/live_face.png');

                  face_api.MatchFacesImage? image2 =
                      await _convertTempFileToMatchFacesImage(
                          '$cacheDirPath/card_face.png');

                  percent = await _matchFaces(image1!, image2!);
                } else {
                  percent = "Original Image Null";
                }

                print("Sake Percent - $percent");
                clearCacheDirectory();
                await context.router
                    .popAndPush(KycApprovalRoute(percent: percent.toString()));
              });
            } else {
              _detectLiveness();
              setState(() {});
            }
          } else {
            _detectLiveness();
          }
        }
      }
    } catch (e) {
      if (kDebugMode) {
        print('Error: $e');
        _detectLiveness();
      }
    }
  }

  Future<face_api.MatchFacesImage?> _convertTempFileToMatchFacesImage(
      String filePath) async {
    try {
      File file = File(filePath);
      if (!await file.exists()) {
        if (kDebugMode) {
          print('File does not exist');
        }
        return null;
      }

      final bytes = await file.readAsBytes();
      final bitmap = base64Encode(bytes);

      return face_api.MatchFacesImage()
        ..bitmap = bitmap
        ..imageType = 1
        ..detectAll = true;
    } catch (e) {
      if (kDebugMode) {
        print('Error converting file: $e');
      }
      return null;
    }
  }

  Future<face_api.MatchFacesImage> _convertXFileToMatchFacesImage(
      XFile file) async {
    final bytes = await file.readAsBytes();
    final bitmap = base64Encode(bytes);

    return face_api.MatchFacesImage()
      ..bitmap = bitmap
      ..imageType = 1
      ..detectAll = true;
  }

  Future<String> _matchFaces(
      face_api.MatchFacesImage image1, face_api.MatchFacesImage image2) async {
    if (image1.bitmap == null || image1.bitmap == "") {
      if (kDebugMode) {
        print("Image 1 null");
      }
      return Future.value("Image 1 not found");
    }
    if (image2.bitmap == null || image2.bitmap == "") {
      if (kDebugMode) {
        print("Image 2 null");
      }
      return Future.value("Image 2 not found");
    }

    var request = face_api.MatchFacesRequest();
    request.images = [image1, image2];
    String srcValue = await face_api.FaceSDK.matchFaces(jsonEncode(request));
    var response = face_api.MatchFacesResponse.fromJson(json.decode(srcValue));
    String respValue =
        await face_api.FaceSDK.matchFacesSimilarityThresholdSplit(
            jsonEncode(response!.results), 0.35);

    face_api.MatchFacesSimilarityThresholdSplit? split =
        face_api.MatchFacesSimilarityThresholdSplit.fromJson(
            json.decode(respValue));

    if (split != null) {
      if (split.matchedFaces.isEmpty) {
        return Future.value("Matched Faces are Empty");
      } else {
        String similarity =
            "${(split.matchedFaces[0]!.similarity! * 100).toStringAsFixed(2)}%";
        return Future.value(similarity);
      }
    } else {
      return Future.value("Split is Null");
    }
  }

  @override
  Widget build(BuildContext context) {
    if (!(_controller?.value.isInitialized ?? false)) {
      return Container();
    }
    return PopScope(
      onPopInvoked: (bool value) {
        dispose();
      },
      child: Scaffold(
        backgroundColor: Colors.white,
        body: Stack(children: [
          _cameraPreviewWidget(),
          Align(
            alignment: Alignment.topCenter,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                if (_task1Message == false) taskMessage("!! Please blink !!"),
                if (_task1Message == true && _task2Message == false)
                  taskMessage("!! Please Smile !!")
              ],
            ),
          ),
        ]),
      ),
    );
  }

  taskMessage(String msg) {
    return Container(
      padding: const EdgeInsets.all(10),
      margin: const EdgeInsets.only(top: 80),
      decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(10),
          boxShadow: [
            BoxShadow(offset: const Offset(4, 4), color: grey),
            BoxShadow(offset: const Offset(-2, -2), color: grey)
          ]),
      child: Text(
        msg,
        style: const TextStyle(fontSize: 23),
      ),
    );
  }

  triggerNotification() async {
    /*NotificationManager.instance.scheduleNotification(5, "Onboarding KYC", "Proceed with the face scanning", "face_scan");*/
    String customerId = await getCustomerId();
    await incrementLoginCount();
    context.read<EkycViewModel>().statusUpdateAPI(customerId, "FACE_DETECTION_FAILED");
  }
}

class OverlayPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.green
      ..style = PaintingStyle.fill;
    final paint1 = Paint()
      ..color = primary
      ..style = PaintingStyle.stroke
      ..strokeWidth = 6
      ..strokeJoin = StrokeJoin.miter
      ..strokeCap = StrokeCap.square;

    canvas.drawCircle(
        Offset(_overlayWidth * 0.5, _overlayHeight * 0.5), 160, paint1);

    var path = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));

    var documentCircle = Rect.fromCircle(
        center: Offset(_overlayWidth * 0.5, _overlayHeight * 0.5), radius: 160);
    path.addOval(documentCircle);

    path.fillType = PathFillType.evenOdd;

    canvas.drawPath(path, paint..color = Colors.white);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
} ```