Использование набора ML Kit для машинного обучения в Flutter-приложениях
Разработкой функций, связанных с использованием методов машинного обучения, обычно занимается отдельная команда специалистов. Но если вы работаете самостоятельно или в небольшой команде, и при этом хотите интегрировать в свое приложение продвинутые опции машинного обучения, вам поможет набор ML Kit.
В состав пакета ML Kit входит комплект API для работы с различными моделями машинного обучения. Все API при этом запускаются непосредственно на устройстве, подключение к интернету не требуется. В этой статье мы поговорим о том, как использовать ML Kit для распознавания текста в Flutter-приложении.
Мы подробно разберем следующие темы:
- что такое ML Kit;
- предоставление приложению доступа к камере пользовательского устройства;
- использование ML Kit для распознавания текста;
- распознавание email адресов на изображениях;
- применение виджета CustomPaint для выделения найденного текста.
Что такое 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.