ML Text Recognition with Flutter

Dario Varriale
6 min readFeb 12, 2024

--

In 2023, we witnessed the rise of Generative AI, with the proliferation of tools like ChatGPT, Midjourney, and many others made available to the general public.

From here on, we can only imagine how many more tools will emerge to assist us in our daily lives and work. That’s why it’s worth understanding these technologies as soon as possible to harness them to the fullest!

Today, we will integrate a pre-trained model to recognize text from an image within a Flutter application.

The model is made available with Google ML Kit and works entirely offline 🚀

💻 The complete project is available on GitHub here.

Flutter Project Setup

⚠️ Make sure you have Flutter installed on your system and that your development environment is correctly configured.

You can verify the Flutter installation by running the command:

flutter doctor

If the command returns warnings or errors, follow the instructions provided to resolve them.

Start a new Flutter project by running the command:

flutter create project_name

in your command line, replacing project_name with the desired name for your project.

In this guide, I named the project ml_text_recognition, so in the code blocks you will find file imports following this pattern: import 'package:ml_text_recognition/...' .

Remember to rename the import if your project_name is different!

Package Integration

Within our project, we will use google_ml_kit_text_recognition for text recognition and image_picker to select images from the gallery or take pictures with the camera.

Let’s implement the packages within the pubspec.yaml file:

...

dependencies:
flutter:
sdk: flutter

cupertino_icons: ^1.0.2

google_mlkit_text_recognition: ^0.11.0
image_picker: ^1.0.7

...

Package versions may change over time, so make sure to check for any updates.

Also, make sure you have followed all procedures for integrating image_picker for both Android and iOS as described in its guide (see here).

Let’s Build the App!

Now that we have integrated the necessary packages, let’s create our application!

We’ll start by creating the ImagePreview widget to help us display the image or a starting message.

Let’s create the image_preview.dart file:

import 'dart:io';
import 'package:flutter/material.dart';

class ImagePreview extends StatelessWidget {
const ImagePreview({
Key? key,
required this.imagePath,
}) : super(key: key);

final String? imagePath;

@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 300,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: Color.fromARGB(255, 203, 203, 203),
blurRadius: 10,
spreadRadius: 2,
offset: Offset(0, 3),
),
],
),
child: imagePath == null
? const Center(
child: Text(
"No image selected",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
)
: Image.file(
File(imagePath!),
fit: BoxFit.contain,
),
);
}
}

This widget will help improve readability within the HomeScreen later on.

Let’s now create an initial version of the HomeScreen where we will encapsulate the logic for text recognition from an image.

Create the home_screen.dart file and define our graphical interface:

import 'package:flutter/material.dart';
import 'package:ml_text_recognition/image_preview.dart';

class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);

@override
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
String? pickedImagePath;
String recognizedText = "";

bool isRecognizing = false;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ML Text Recognition'),
),
body: SafeArea(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: ImagePreview(imagePath: pickedImagePath),
),
ElevatedButton(
onPressed: isRecognizing ? null : () {},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Pick an image'),
if (isRecognizing) ...[
const SizedBox(width: 20),
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 1.5,
),
),
],
],
),
),
const Divider(),
Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Recognized Text",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(
Icons.copy,
size: 16,
),
onPressed: () {},
),
],
),
),
if (!isRecognizing) ...[
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Flexible(
child: SelectableText(
recognizedText.isEmpty
? "No text recognized"
: recognizedText,
),
),
],
),
),
),
),
],
],
),
),
);
}
}

Let’s now add the necessary variables and methods for text recognition within _HomeScreenState:

class _HomeScreenState extends State<HomeScreen> {
late TextRecognizer textRecognizer;
late ImagePicker imagePicker;

String? pickedImagePath;
String recognizedText = "";

bool isRecognizing = false;

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

textRecognizer = TextRecognizer();
imagePicker = ImagePicker();
}

void _pickImageAndProcess() async {
final pickedImage = await imagePicker.getImage(source: ImageSource.gallery);

if (pickedImage == null) {
return;
}

setState(() {
pickedImagePath = pickedImage.path;
isRecognizing = true;
});

final inputImage = InputImage.fromFilePath(pickedImage.path);
final RecognizedText recognizedText = await textRecognizer.processImage(inputImage);

setState(() {
recognizedText = recognizedText.text;
isRecognizing = false;
});
}

@override
Widget build(BuildContext context) {
// Build method remains the same
}
}

With the _pickImageAndProcess method, we prompt the user to choose how they want to acquire an image (from the gallery or by taking a picture with the camera).

We then call the _pickImageAndProcess method passing the chosen ImageSource. This is where the actual interpretation of the image occurs, extracting the text with the ML model.

Lastly, we add a method _copyTextToClipboard for the user (but not mandatory) to quickly copy the text recognized by the Machine Learning model.

The complete home_screen.dart file will look like this:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:image_picker/image_picker.dart';
import 'package:ml_text_recognition/image_preview.dart';

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

@override
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
late TextRecognizer textRecognizer;
late ImagePicker imagePicker;

String? pickedImagePath;
String recognizedText = "";

bool isRecognizing = false;

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

textRecognizer = TextRecognizer(script: TextRecognitionScript.latin);
imagePicker = ImagePicker();
}

void _pickImageAndProcess({required ImageSource source}) async {
final pickedImage = await imagePicker.pickImage(source: source);

if (pickedImage == null) {
return;
}

setState(() {
pickedImagePath = pickedImage.path;
isRecognizing = true;
});

try {
final inputImage = InputImage.fromFilePath(pickedImage.path);
final RecognizedText recognisedText =
await textRecognizer.processImage(inputImage);

recognizedText = "";

for (TextBlock block in recognisedText.blocks) {
for (TextLine line in block.lines) {
recognizedText += "${line.text}\n";
}
}
} catch (e) {
if (!mounted) {
return;
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error recognizing text: $e'),
),
);
} finally {
setState(() {
isRecognizing = false;
});
}
}

void _chooseImageSourceModal() {
showModalBottomSheet(
context: context,
builder: (context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Choose from gallery'),
onTap: () {
Navigator.pop(context);
_pickImageAndProcess(source: ImageSource.gallery);
},
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Take a picture'),
onTap: () {
Navigator.pop(context);
_pickImageAndProcess(source: ImageSource.camera);
},
),
],
),
);
},
);
}

void _copyTextToClipboard() async {
if (recognizedText.isNotEmpty) {
await Clipboard.setData(ClipboardData(text: recognizedText));
if (!mounted) {
return;
}

ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Text copied to clipboard'),
),
);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ML Text Recognition'),
),
body: SafeArea(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: ImagePreview(imagePath: pickedImagePath),
),
ElevatedButton(
onPressed: isRecognizing ? null : _chooseImageSourceModal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Pick an image'),
if (isRecognizing) ...[
const SizedBox(width: 20),
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 1.5,
),
),
],
],
),
),
const Divider(),
Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Recognized Text",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(
Icons.copy,
size: 16,
),
onPressed: _copyTextToClipboard,
),
],
),
),
if (!isRecognizing) ...[
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Flexible(
child: SelectableText(
recognizedText.isEmpty
? "No text recognized"
: recognizedText,
),
),
],
),
),
),
),
],
],
),
),
);
}
}

Let’s modify the main.dart file to call our HomeScreen:

import 'package:flutter/material.dart';
import 'package:ml_text_recognition/home_screen.dart';

void main() {
runApp(const MyApp());
}

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ML Text Recognition',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}

Conclusion

In this guide, we explored how to integrate text recognition within a Flutter app using the machine learning models provided by Google ML Kit.

Thanks to the combination of these technologies, we can develop intelligent and innovative apps quickly and effectively.

Google ML Kit and Flutter prove to be powerful partners for cross-platform application development, enabling developers to create advanced solutions with ease and flexibility.

--

--

Dario Varriale

iOS Developer && Flutter Developer @ Atobit | Co-organizer of Flutter Modena