Использование набора ML Kit для машинного обучения в Flutter-приложениях

Разработкой функций, связанных с использованием методов машинного обучения, обычно занимается отдельная команда специалистов. Но если вы работаете самостоятельно или в небольшой команде, и при этом хотите интегрировать в свое приложение продвинутые опции машинного обучения, вам поможет набор ML Kit.

В состав пакета ML Kit входит комплект API для работы с различными моделями машинного обучения. Все API при этом запускаются непосредственно на устройстве, подключение к интернету не требуется. В этой статье мы поговорим о том, как использовать ML Kit для распознавания текста в Flutter-приложении.

Мы подробно разберем следующие темы:

  • что такое ML Kit;
  • предоставление приложению доступа к камере пользовательского устройства;
  • использование ML Kit для распознавания текста;
  • распознавание email адресов на изображениях;
  • применение виджета CustomPaint для выделения найденного текста.

Что такое ML Kit

Что такое ML Kit

ML Kit – это набор инструментов для работы с моделями машинного обучения, созданный разработчиками корпорации Google. Этот пакет работает непосредственно на пользовательском устройстве, без подключения к облачным сервисам. Это дает возможность создавать быстрые и надежные приложения, работающие в режиме реального времени и не передающие конфиденциальные данные в интернет.

Примечание: пакет ML Kit для Flutter на данный момент находится в стадии разработки и доступен только для платформы Android.

Существует пакет под названием firebase_ml_vision, обладающий похожей функциональностью. Этот набор использует облачный сервис машинного обучения, который подключается к базе данных Firebase и поддерживает обе платформы, (Android и iOS). К сожалению, разработка этого проекта прекращена, и его набор API отсутствует в последней версии Firebase SDK.

Создание нового проекта Flutter

Для создания нового проекта выполните приведенную ниже команду:

flutter create flutter_mlkit_vision

Откройте созданный проект в любой IDE-среде. Для открытия в редакторе VS Code выполните следующую команду:

code flutter_mlkit_vision

Обзор проекта

Приложение, которое мы создадим в рамках этого руководства, состоит из двух экранов. Первый экран CameraScreenвключает в себя предварительный просмотр области, видимой камере, и кнопку для фотографирования. Второй экран DetailScreen будет показывать результаты анализа изображения, отделяя текст от графики.

Скриншоты приложения

Готовое приложение будет выглядеть следующим образом:

Скриншоты приложения

Доступ к камере устройства

Для использования камеры в Flutter- приложении вам понадобится плагин под названием camera. Добавьте плагин в файл pubspec.yaml, выполнив следующую команду:

camera: ^0.8.1

Последнюю версию плагина можно найти на pub.dev.

Вместо демонстрационного кода, расположенного в файле main.dart вставьте в него код, приведенный ниже:

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'camera_screen.dart';
// Глобальная переменная для хранения списка доступных камер
List<CameraDescription> cameras = [];

Future<void> main() async {
  // Создать список доступных камер до инициализации приложения
  try {
    WidgetsFlutterBinding.ensureInitialized();
    cameras = await availableCameras();
  } on CameraException catch (e) {
    debugPrint('CameraError: ${e.description}');
  }
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter MLKit Vision',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CameraScreen(),
    );
  }
}

В приведенном выше коде я использовал метод availableCameras() для получения списка доступных камер.

Теперь мы создадим код для экрана приложения, который выводит предварительный просмотр видимой области и кнопку фотографирования.

class CameraScreen extends StatefulWidget {
  @override
  _CameraScreenState createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter MLKit Vision'),
      ),
      body: Container(),
    );
  }
}
1. Создаем объект CameraController:
    // Внутри _CameraScreenState класса
    late final CameraController _controller;
  • Создаем метод _initializeCamera(), инициализируем _controller контроллер камеры внутри него:
    // Инициализация контроллера камеры для вывода предварительного просмотра на экран
    void _initializeCamera() async {
      final CameraController cameraController = CameraController(
        cameras[0],
        ResolutionPreset.high,
      );
      _controller = cameraController;

      _controller.initialize().then((_) {
        if (!mounted) {
          return;
        }
        setState(() {});
      });
    }

У контроллера CameraController() есть два обязательных параметра.

CameraDescription – тип камеры (фронтальная, задняя). Здесь вы задаете тип камеры, к которой хотите получить доступ:

  • 1 – для фронтальной;
  • 0 – для задней.

ResolutionPreset – разрешение по умолчанию. Здесь вы задаете качество фотографий, снимаемых камерой.

  • Вызываем метод внутри initState():
    @override
    void initState() {
      _initializeCamera();
      super.initState();
    }
  • Для предотвращения утечки памяти останавливаем _controller:
    @override
    void dispose() {
      _controller.dispose();
      super.dispose();
    }
  • Определим метод _takePicture() для фотографирования и сохранения изображения в файле. Метод будет возвращать путь к сохраненному файлу.
    // Делает снимок выбранной камерой
    // Возвращает путь к файлу
    Future<String?> _takePicture() async {
      if (!_controller.value.isInitialized) {
        print("Контроллер не инициализирован");
        return null;
      }

      String? imagePath;

      if (_controller.value.isTakingPicture) {
        print("Сохраняем фото...");
        return null;
      }

      try {
        // Отключение вспышки камеры
        _controller.setFlashMode(FlashMode.off);
        // Сохранение в кроссплатформенном формате
        final XFile file = await _controller.takePicture();
        // Возвращение пути к файлу
        imagePath = file.path;
      } on CameraException catch (e) {
        print("Камера недоступна");
        return null;
      }

      return imagePath;
    }
  • Теперь мы готовы приступить к созданию интерфейса для экрана захвата изображения CameraScreen. Интерфейс состоит из предварительного просмотра области, доступной камере, и кнопки для фотографирования. После того, как снимок сделан, экран приложения сменится на следующий, DetailScreen.
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Flutter MLKit Vision'),
        ),
        body: _controller.value.isInitialized
            ? Stack(
                children: <Widget>[
                  CameraPreview(_controller),
                  Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Container(
                      alignment: Alignment.bottomCenter,
                      child: ElevatedButton.icon(
                        icon: Icon(Icons.camera),
                        label: Text("Click"),
                        onPressed: () async {
                          // Если появился путь к файлу
                          // перейти на экран DetailScreen
                          await _takePicture().then((String? path) {
                            if (path != null) {
                              Navigator.push(
                                context,
                                MaterialPageRoute(
                                  builder: (context) => DetailScreen(
                                    imagePath: path,
                                  ),
                                ),
                              );
                            } else {
                              print('Путь к файлу не найден!');
                            }
                          });
                        },
                      ),
                    ),
                  )
                ],
              )
            : Container(
                color: Colors.black,
                child: Center(
                  child: CircularProgressIndicator(),
                ),
              ),
      );
    }

На этом процесс создания функций по работе с камерой закончен. На следующем этапе мы будем анализировать сделанные снимки, и распознавать текст на них. Результаты анализа будут отображаться на экране DetailScreen.

Интеграция ML Kit в приложение

Импортируйте плагин google_ml_kit в свой файл pubspec.yaml:

google_ml_kit: ^0.3.0

Вам необходимо передать путь к файлу снимка в коде экрана DetailScreen. Основная структура экрана анализа изображения DetailScreen определяется следующим образом:

// Внутри image_detail.dart файла
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:google_ml_kit/google_ml_kit.dart';

class DetailScreen extends StatefulWidget {
  final String imagePath;

  const DetailScreen({required this.imagePath});

  @override
  _DetailScreenState createState() => _DetailScreenState();
}

class _DetailScreenState extends State<DetailScreen> {
  late final String _imagePath;
  late final TextDetector _textDetector;
  Size? _imageSize;
  List<TextElement> _elements = [];

  List<String>? _listEmailStrings;

  Future<void> _getImageSize(File imageFile) async {
    // Размер изображения выводится здесь
  }

  void _recognizeEmails() async {
    // Инициализация распознавания текста происходит здесь
    // анализ текста для поиска email адресов
  }

  @override
  void initState() {
    _imagePath = widget.imagePath;
    // Initializing the text detector
    _textDetector = GoogleMlKit.vision.textDetector();
    _recognizeEmails();
    super.initState();
  }

  @override
  void dispose() {
    // Закрытие детектора текста после использования
    _textDetector.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Image Details"),
      ),
      body: Container(),
    );
  }
}

В приведенном выше фрагменте кода мы провели инициализацию детектора текста ML Kit внутри метода initState(), а потом закрыли его в методе dispose(). Теперь вам потребуется определить два метода:

  • _getImageSize() – для получения размера сделанного снимка;
  • _recognizeEmails() – для распознавания текста и выявления в нем email адресов.

Получение размера снимка

Внутри метода _getImageSize() мы сначала получим снимок с помощью пути к файлу, а затем определим размер фотографии.

// Определение размера фото
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;
  });
}

Распознавание email адресов

Внутри метода _recognizeEmails() мы опишем всю процедуру распознавания и извлечения необходимой информации из фотографии. Я пошагово покажу, как получить адреса электронной почты из распознанного текста.

  • Получаем снимок с использованием пути к файлу, вызываем метод _getImageSize():
   void _recognizeEmails() async {
      _getImageSize(File(_imagePath));
    }
  • Создаем объект InputImage, используя путь к файлу, и запускаем распознавание текста на снимке:
    // Создание объекта InputImage с использованием пути к файлу снимка
    final inputImage = InputImage.fromFilePath(_imagePath);
    // Получение распознанного текста из объекта
    final text = await _textDetector.processImage(inputImage);
  • Теперь нам надо получить текст из объекта RecognisedText, и выделить из текста адреса электронной почты. Текст находится в blocks -> lines -> text (блоки -> строки -> текст).
    // Стандартное выражение для поиска email адресов
    String pattern = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)*$";
    RegExp regEx = RegExp(pattern);

    List<String> emailStrings = [];

    // Обнаружение и сохранение строк
    for (TextBlock block in text.textBlocks) {
      for (TextLine line in block.textLines) {
        if (regEx.hasMatch(line.lineText)) {
          emailStrings.add(line.lineText);
        }
      }
    }
  • Сохраняем извлеченный текст в переменной _listEmailStrings.
    setState(() {
      _listEmailStrings = emailStrings;
    });

Создаем пользовательский интерфейс

Мы определили все методы и готовы перейти к созданию интерфейса для экрана DetailScreen, показывающего результаты анализа снимка. Интерфейс состоит из двух виджетов – первый отображает снимок, второй – демонстрирует распознанные email адреса.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Image Details"),
    ),
    body: _imageSize != null
        ? Stack(
            children: [
              Container(
                width: double.maxFinite,
                color: Colors.black,
                child: AspectRatio(
                  aspectRatio: _imageSize!.aspectRatio,
                  child: Image.file(
                    File(_imagePath),
                  ),
                ),
              ),
              Align(
                alignment: Alignment.bottomCenter,
                child: Card(
                  elevation: 8,
                  color: Colors.white,
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Padding(
                          padding: const EdgeInsets.only(bottom: 8.0),
                          child: Text(
                            "Identified emails",
                            style: TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                        Container(
                          height: 60,
                          child: SingleChildScrollView(
                            child: _listEmailStrings != null
                                ? ListView.builder(
                                    shrinkWrap: true,
                                    physics: BouncingScrollPhysics(),
                                    itemCount: _listEmailStrings!.length,
                                    itemBuilder: (context, index) =>
                                        Text(_listEmailStrings![index]),
                                  )
                                : Container(),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          )
        : Container(
            color: Colors.black,
            child: Center(
              child: CircularProgressIndicator(),
            ),
          ),
  );
}

Если переменная _imageSize окажется пустой, на экране появится индикатор загрузки изображения CircularProgressIndicator.

Экран анализа снимка до использования виджета для выделения текста выглядит следующим образом:

Создаем пользовательский интерфейс

Цветное выделение текста

Для выделения распознанных адресов электронной почты цветной рамкой можно использовать виджет CustomPaint.

  • Сначала нам потребуется модифицировать метод _recognizeEmails(), чтобы получить элементы текста TextElement из каждой строки.
    List<TextElement> _elements = [];
    void _recognizeEmails() async {
      // ...
      // Обнаружение и сохранение текстовых строк и элементов
      for (TextBlock block in text.textBlocks) {
        for (TextLine line in block.textLines) {
          if (regEx.hasMatch(line.lineText)) {
            emailStrings.add(line.lineText);
            // Извлечение текстовых элементов и сохранение их в списке
            for (TextElement element in line.textElements) {
              _elements.add(element);
            }
          }
        }
      }

      // ...
    }
  • Поместите виджет AspectRatio, содержащий изображение, внутрь виджета CustomPaint.
    CustomPaint(
      foregroundPainter: TextDetectorPainter(
        _imageSize!,
        _elements,
      ),
      child: AspectRatio(
        aspectRatio: _imageSize!.aspectRatio,
        child: Image.file(
          File(_imagePath),
        ),
      ),
    ),
  • Теперь нужно определить класс TextDetectorPainter, который будет расширением виджета CustomPainter.
    // Помогает нарисовать цветную рамку
    // окружающую адреса электронной почты на изображении
    class TextDetectorPainter extends CustomPainter {
      TextDetectorPainter(this.absoluteImageSize, this.elements);

      final Size absoluteImageSize;
      final List<TextElement> elements;

      @override
      void paint(Canvas canvas, Size size) {
        // TODO: Define painter
      }

      @override
      bool shouldRepaint(TextDetectorPainter oldDelegate) {
        return true;
      }
    }	
  • Внутри метода paint() получаем размер отображаемой области изображения:
    final double scaleX = size.width / absoluteImageSize.width;
    final double scaleY = size.height / absoluteImageSize.height;
  • Определяем метод scaleRect(), который поможет окружить обнаруженный текст цветной рамкой.
    Rect scaleRect(TextElement container) {
      return Rect.fromLTRB(
        container.rect.left * scaleX,
        container.rect.top * scaleY,
        container.rect.right * scaleX,
        container.rect.bottom * scaleY,
      );
    }
  • Определяем объект Paint:
    final Paint paint = Paint()
       ..style = PaintingStyle.stroke
       ..color = Colors.red
       ..strokeWidth = 2.0;
  • Используем TextElement для рисования прямоугольных цветных рамок:
    for (TextElement element in elements) {
      canvas.drawRect(scaleRect(element), paint);
    }

После добавления цветного выделения экран анализа в приложении выглядит следующим образом:

Цветное выделение текста

Запуск приложения

Перед запуском следует убедиться в правильности настроек конфигурации приложения.

Настройки для Android

Перейдите в директорию проекта, откройте android -> app -> build.gradle и установите 26-ю версию minSdkVersion:

minSdkVersion 26

Теперь все готово к запуску.

Настройки для iOS

Плагин пока что не работает на этой платформе. Как только ML Kit начнет поддерживать iOS, информация о настройках появится в репозитории плагина на GitHub.

Заключение

Пакет ML Kit предоставляет набор моделей машинного обучения, которые легко интегрируются в любое мобильное приложение. Среди возможностей пакета – распознавание лиц и поз, идентификация дорожных ориентиров, сканирование штрих-кодов, маркировка изображений и многое другое. Для изучения всех возможностей пакета ML Kit посетите официальный сайт проекта.

Исходный код приложения, разработанного в рамках данного руководства, доступен на странице автора в GitHub.

Наталья Кайдаавтор-переводчик статьи «Perform on-device machine learning using ML Kit in Flutter»