brianmtully / flutter_google_ml_vision

Flutter Plugin for Google ML Kit Vision
BSD 3-Clause "New" or "Revised" License
47 stars 46 forks source link

Face detection for iOS not working #15

Closed CoderJava closed 3 years ago

CoderJava commented 3 years ago

Thanks for created this plugin. It's really useful. But i have a problem with this plugin. I used the example code but face detection totally not working for iOS. On Android it's working.

https://user-images.githubusercontent.com/17062085/119457275-b327ab00-bd65-11eb-92aa-89f6e955c9ac.mp4

Device info:

log ``` [Nusawork] findWriterForTypeAndAlternateType:119: unsupported file format 'public.heic' [Nusawork] findWriterForTypeAndAlternateType:119: unsupported file format 'public.heic' flutter: path: /private/var/mobile/Containers/Data/Application/33276A8E-8373-41B4-8CA8-389B492DFD52/tmp/image_picker_EBC530C9-94FB-4F43-A3E4-ED922EB80521-2839-000001DA9BE2991F.jpg flutter: [] 2021-05-25 2:20:28.351 PM Nusawork[2839/0x1057d3880] [lvl=3] +[MLKITx_CCTClearcutUploader crashIfNecessary] Multiple instances of CCTClearcutUploader were instantiated. Multiple uploaders function correctly but have an adverse affect on battery performance due to lock contention. Initialized TensorFlow Lite runtime. [Nusawork] findWriterForTypeAndAlternateType:119: unsupported file format 'public.heic' [Nusawork] findWriterForTypeAndAlternateType:119: unsupported file format 'public.heic' flutter: path: /private/var/mobile/Containers/Data/Application/33276A8E-8373-41B4-8CA8-389B492DFD52/tmp/image_picker_7C9F0886-5B5A-4C57-84AC-E49A0B2B62F5-2839-000001DAB1875B05.jpg ```
flutter doctor -v ``` [✓] Flutter (Channel stable, 2.2.0, on Mac OS X 10.15.7 19H2 darwin-x64, locale en-EC) • Flutter version 2.2.0 at /Users/yudisetiawan/Downloads/flutter • Framework revision b22742018b (10 days ago), 2021-05-14 19:12:57 -0700 • Engine revision a9d88a4d18 • Dart version 2.13.0 [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2) • Android SDK at /Users/yudisetiawan/Library/Android/sdk • Platform android-30, build-tools 30.0.2 • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS • Xcode at /Applications/Xcode.app/Contents/Developer • Xcode 12.4, Build version 12D4e • CocoaPods version 1.10.1 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 4.1) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495) [✓] IntelliJ IDEA Community Edition (version 2020.2.3) • IntelliJ at /Applications/IntelliJ IDEA CE.app • Flutter plugin version 55.1.2 • Dart plugin version 202.8443 [✓] VS Code (version 1.55.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.21.0 [✓] Connected device (3 available) • Yudi’s iPhone (mobile) • 1b7890540306c7d8155bacffabc03043d4c28bf9 • ios • iOS 14.4.1 • macOS (desktop) • macos • darwin-x64 • Mac OS X 10.15.7 19H2 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 90.0.4430.212 • No issues found! ```
brianmtully commented 3 years ago

on iOS contours need to be enabled.

final FaceDetector _faceDetector = GoogleVision.instance .faceDetector(FaceDetectorOptions(enableContours: true));

CoderJava commented 3 years ago

on iOS contours need to be enabled.

final FaceDetector _faceDetector = GoogleVision.instance .faceDetector(FaceDetectorOptions(enableContours: true));

@brianmtully I have updated the code to this. But still it's not working.

// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: PictureScanner(),
    );
  }
}

class PictureScanner extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _PictureScannerState();
}

class _PictureScannerState extends State<PictureScanner> {
  File? _imageFile;
  Size? _imageSize;
  dynamic _scanResults;
  Detector _currentDetector = Detector.text;
  final BarcodeDetector _barcodeDetector = GoogleVision.instance.barcodeDetector();
  final FaceDetector _faceDetector = GoogleVision.instance.faceDetector(FaceDetectorOptions(enableContours: true));
  final ImageLabeler _imageLabeler = GoogleVision.instance.imageLabeler();
  final TextRecognizer _recognizer = GoogleVision.instance.textRecognizer();

  Future<void> _getAndScanImage() async {
    setState(() {
      _imageFile = null;
      _imageSize = null;
    });

    final pickedImage = await ImagePicker().getImage(source: ImageSource.gallery);
    debugPrint('path: ${pickedImage!.path}');
    final imageFile = File(pickedImage.path);

    setState(() {
      _imageFile = imageFile;
    });

    if (imageFile != null) {
      await Future.wait([
        _getImageSize(imageFile),
        _scanImage(imageFile),
      ]);
    }
  }

  Future<void> _getImageSize(File imageFile) async {
    final Completer<Size> completer = Completer<Size>();

    final Image image = Image.file(imageFile);
    image.image.resolve(const ImageConfiguration()).addListener(
      ImageStreamListener((ImageInfo info, bool _) {
        completer.complete(Size(
          info.image.width.toDouble(),
          info.image.height.toDouble(),
        ));
      }),
    );

    final Size imageSize = await completer.future;
    setState(() {
      _imageSize = imageSize;
    });
  }

  Future<void> _scanImage(File imageFile) async {
    setState(() {
      _scanResults = null;
    });

    final GoogleVisionImage visionImage = GoogleVisionImage.fromFile(imageFile);

    dynamic results;
    switch (_currentDetector) {
      case Detector.barcode:
        results = await _barcodeDetector.detectInImage(visionImage);
        break;
      case Detector.face:
        results = await _faceDetector.processImage(visionImage);
        break;
      case Detector.label:
        results = await _imageLabeler.processImage(visionImage);
        break;
      case Detector.text:
        results = await _recognizer.processImage(visionImage);
        print(results.blocks);
        for (final TextBlock block in results.blocks) {
          for (final TextLine line in block.lines) {
            for (final TextElement element in line.elements) {
              print(element.text);
            }
          }
        }

        break;
      default:
        return;
    }

    setState(() {
      _scanResults = results;
    });
  }

  CustomPaint _buildResults(Size imageSize, dynamic results) {
    CustomPainter? painter;

    switch (_currentDetector) {
      case Detector.barcode:
        painter = BarcodeDetectorPainter(_imageSize!, results);
        break;
      case Detector.face:
        painter = FaceDetectorPainter(_imageSize!, results);
        break;
      case Detector.label:
        painter = LabelDetectorPainter(_imageSize!, results);
        break;
      case Detector.text:
        painter = TextDetectorPainter(_imageSize!, results);
        break;
      default:
        break;
    }

    return CustomPaint(
      painter: painter,
    );
  }

  Widget _buildImage() {
    return Container(
      constraints: const BoxConstraints.expand(),
      decoration: BoxDecoration(
        image: DecorationImage(
          image: Image.file(_imageFile!).image,
          fit: BoxFit.fitWidth,
        ),
      ),
      child: _imageSize == null || _scanResults == null
          ? const Center(
              child: Text(
                'Scanning...',
                style: TextStyle(
                  color: Colors.green,
                  fontSize: 30,
                ),
              ),
            )
          : _buildResults(_imageSize!, _scanResults),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Picture Scanner'),
        actions: <Widget>[
          PopupMenuButton<Detector>(
            onSelected: (Detector result) {
              _currentDetector = result;
              if (_imageFile != null) _scanImage(_imageFile!);
            },
            itemBuilder: (BuildContext context) => <PopupMenuEntry<Detector>>[
              const PopupMenuItem<Detector>(
                value: Detector.barcode,
                child: Text('Detect Barcode'),
              ),
              const PopupMenuItem<Detector>(
                value: Detector.face,
                child: Text('Detect Face'),
              ),
              const PopupMenuItem<Detector>(
                value: Detector.label,
                child: Text('Detect Label'),
              ),
              const PopupMenuItem<Detector>(
                value: Detector.text,
                child: Text('Detect Text'),
              ),
            ],
          ),
        ],
      ),
      body: _imageFile == null ? const Center(child: Text('No image selected.')) : _buildImage(),
      floatingActionButton: FloatingActionButton(
        onPressed: _getAndScanImage,
        tooltip: 'Pick Image',
        child: const Icon(Icons.add_a_photo),
      ),
    );
  }

  @override
  void dispose() {
    _barcodeDetector.close();
    _faceDetector.close();
    _imageLabeler.close();
    _recognizer.close();
    super.dispose();
  }
}

enum Detector {
  barcode,
  face,
  label,
  cloudLabel,
  text,
}

class BarcodeDetectorPainter extends CustomPainter {
  BarcodeDetectorPainter(this.absoluteImageSize, this.barcodeLocations);

  final Size absoluteImageSize;
  final List<Barcode> barcodeLocations;

  @override
  void paint(Canvas canvas, Size size) {
    final double scaleX = size.width / absoluteImageSize.width;
    final double scaleY = size.height / absoluteImageSize.height;

    Rect scaleRect(Barcode barcode) {
      return Rect.fromLTRB(
        barcode.boundingBox!.left * scaleX,
        barcode.boundingBox!.top * scaleY,
        barcode.boundingBox!.right * scaleX,
        barcode.boundingBox!.bottom * scaleY,
      );
    }

    final Paint paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;

    for (final Barcode barcode in barcodeLocations) {
      paint.color = Colors.green;
      canvas.drawRect(scaleRect(barcode), paint);
    }
  }

  @override
  bool shouldRepaint(BarcodeDetectorPainter oldDelegate) {
    return oldDelegate.absoluteImageSize != absoluteImageSize || oldDelegate.barcodeLocations != barcodeLocations;
  }
}

class FaceDetectorPainter extends CustomPainter {
  FaceDetectorPainter(this.absoluteImageSize, this.faces);

  final Size absoluteImageSize;
  final List<Face> faces;

  @override
  void paint(Canvas canvas, Size size) {
    final double scaleX = size.width / absoluteImageSize.width;
    final double scaleY = size.height / absoluteImageSize.height;

    final Paint paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0
      ..color = Colors.red;

    for (final Face face in faces) {
      canvas.drawRect(
        Rect.fromLTRB(
          face.boundingBox.left * scaleX,
          face.boundingBox.top * scaleY,
          face.boundingBox.right * scaleX,
          face.boundingBox.bottom * scaleY,
        ),
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(FaceDetectorPainter oldDelegate) {
    return oldDelegate.absoluteImageSize != absoluteImageSize || oldDelegate.faces != faces;
  }
}

class LabelDetectorPainter extends CustomPainter {
  LabelDetectorPainter(this.absoluteImageSize, this.labels);

  final Size absoluteImageSize;
  final List<ImageLabel> labels;

  @override
  void paint(Canvas canvas, Size size) {
    final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
      ui.ParagraphStyle(textAlign: TextAlign.left, fontSize: 23, textDirection: TextDirection.ltr),
    );

    builder.pushStyle(ui.TextStyle(color: Colors.green));
    for (final ImageLabel label in labels) {
      builder.addText('Label: ${label.text}, '
          'Confidence: ${label.confidence!.toStringAsFixed(2)}\n');
    }
    builder.pop();

    canvas.drawParagraph(
      builder.build()
        ..layout(ui.ParagraphConstraints(
          width: size.width,
        )),
      const Offset(0, 0),
    );
  }

  @override
  bool shouldRepaint(LabelDetectorPainter oldDelegate) {
    return oldDelegate.absoluteImageSize != absoluteImageSize || oldDelegate.labels != labels;
  }
}

// Paints rectangles around all the text in the image.
class TextDetectorPainter extends CustomPainter {
  TextDetectorPainter(this.absoluteImageSize, this.visionText);

  final Size absoluteImageSize;
  final VisionText visionText;

  @override
  void paint(Canvas canvas, Size size) {
    final double scaleX = size.width / absoluteImageSize.width;
    final double scaleY = size.height / absoluteImageSize.height;

    Rect scaleRect(TextContainer container) {
      return Rect.fromLTRB(
        container.boundingBox!.left * scaleX,
        container.boundingBox!.top * scaleY,
        container.boundingBox!.right * scaleX,
        container.boundingBox!.bottom * scaleY,
      );
    }

    final Paint paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;

    for (final TextBlock block in visionText.blocks) {
      print(block.text);
      for (final TextLine line in block.lines) {
        for (final TextElement element in line.elements) {
          paint.color = Colors.green;
          canvas.drawRect(scaleRect(element), paint);
        }

        paint.color = Colors.yellow;
        canvas.drawRect(scaleRect(line), paint);
      }

      paint.color = Colors.red;
      canvas.drawRect(scaleRect(block), paint);
    }
  }

  @override
  bool shouldRepaint(TextDetectorPainter oldDelegate) {
    return oldDelegate.absoluteImageSize != absoluteImageSize || oldDelegate.visionText != visionText;
  }
}
CoderJava commented 3 years ago

@brianmtully I think the problem not in contours setting. Because when I'm tried with different images the face detection it's working. I don't know why this is happening.

https://user-images.githubusercontent.com/17062085/119591553-000c8f80-be01-11eb-8c47-8fcf79f7c63b.MP4

CoderJava commented 3 years ago

I found the solution. So the problem is that image from camera Flutter is not taken with correct orientation. So, I'm just added this code and it's working for iOS.

import 'package:image/image.dart' as img;

...

final img.Image capturedImage = img.decodeImage(await File(path).readAsBytes());
final img.Image orientedImage = img.bakeOrientation(capturedImage);
await File(path).writeAsBytes(img.encodeJpg(orientedImage));

Source