В настоящее время широкую популярность набирает машинное обучение. Так как обучение моделей трудозатратно, то к его применению в Android относятся с неохотой. Однако Google упростил нам эту задачу с появлением ML Kit. В этой статье кратко посмотрим на возможности ML Kit и напишем приложение, распознающее текст в изображении с дальнейшим копированием в буфер обмена.
Кратко о возможностях ML Kit
Это мобильное SDK от Google, позволяющая использовать машинное обучение под Android и IOS.
Для совсем новичков в машинном обучении Google предоставляет готовые модели, а для экспертов создавать кастомные при помощи Tensorflow Lite.
ML Kit позволяет нам работать с текстом(Vision Api):
1. Распознавать текст
2. Обнаружение лиц
3. Сканировать штрих-коды
4. Разметка изображений
5. Определять и отслеживать объекты
6. Распознать ландшафты
Также можно работать с NLP(Native Language Processing):
1. Определение языка
2. Перевод текста
3. Генерировать простые ответы на сообщения
Кроме того можно создавать свои кастомные модели, самостоятельно обучать и использовать в своем приложении.
Распознавание текста(Реализация)
Дана задача: определить текст по фото(фото будем брать из галереи), а потом этот текст скопировать в буфер обмена.
Перед тем как начать подключите Firebase к проекту(если не делали этого ранее).
Накидаем разметку в activity_main
<ImageView
android:id="@id/image_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:fitsSystemWindows="true"
app:layout_collapseMode="parallax"
android:contentDescription="@string/image_for_recognition"
/>
//...
<TextView
android:id="@+id/detected_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:textAlignment="center"
/>
//...
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/choose_image_from_gallery_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_horizontal_margin"
android:src="@drawable/ic_gallery_24dp"
app:layout_anchor="@id/main.appbar"
app:layout_anchorGravity="bottom|right|end"
app:rippleColor="@color/colorSecondary"
/>
image_holder — туда мы будем помещать изображение из галереи
detected_text_view — текст, распознанный с картинки
Накидаем разметку в bottom_sheet
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:text="@string/text_button"
android:onClick="recognizeText"
android:textAllCaps="false"
style="@style/RoundedCornerButton"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:text="@string/copy_button"
android:onClick="copyText"
android:textAllCaps="false"
style="@style/RoundedCornerButton"
/>
Добавляем зависимость в проект для распознавания текста
implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
Укажем метаданные в манифесте в <application>, чтобы модели загружались в маркете при нажатии кнопки «Install» в Google Play, иначе придется потом в фоне загружать.
<meta-data
android:name="com.google.firebase.ml.vision.DEPENDENCIES"
android:value="text"
/>
Откроем MainActivity.kt и добавим слушатель событий для choose_image_from_gallery_btn и вставим картинку в наш image_holder
choose_image_from_gallery_btn.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_DENIED){
val permissions = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
requestPermissions(permissions, PERMISSION_CODE)
}
else {
pickImageFromGallery()
}
}
}
private fun pickImageFromGallery() {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
startActivityForResult(intent, IMAGE_PICK_CODE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && requestCode == IMAGE_PICK_CODE){
image_holder.setImageURI(data?.data)
}
}
Картинку мы получили, теперь нам нужно распознать текст и вывести в наш detected_text_view.
fun recognizeText(view: View) {
if (image_holder.drawable == null){
Toast.makeText(this, R.string.image_null, Toast.LENGTH_LONG).show()
return
}
recognizeTextFromDevice()
}
private fun recognizeTextFromDevice() {
val detector = FirebaseVision.getInstance().onDeviceTextRecognizer // Получаем состояние FirebaseVisionTextRecognizer
val textImage = FirebaseVisionImage.fromBitmap((image_holder.drawable as BitmapDrawable).bitmap)
detector.processImage(textImage)
.addOnSuccessListener { firebaseVisionText ->
detected_text_view.text = TextProcessor.getInstance().process(firebaseVisionText) //Обрабатываем полученный текст
}
.addOnFailureListener {
// Handling error
}
}
Как вы могли заметить, мы распознаем изображение на устройсте, потому что на облаке нам пришлось бы обрабатывать подключение к сети, наличие интернета на устройсте да и к тому же пришлось бы в Firebase Console создавать платежный аккаунт(год бесплатно), но при этом на устройтве мы не можем распознавать русский язык(по крайней мере мне не удалось).
Создадим синглтонный класс TextProcessor, но для начала разберемся в теории.
Каждый текст(FirebaseVisionText) состоит из блоков(TextBlock), они в свои очередь из линий(Line), а линии из элементов(Element), то бишь из символов.
На примере ниже красным показаны блоки, синим — линии, фиолетовым — элементы

Создаем метод process(). Этот класс я решил написать на Java
public String process(FirebaseVisionText firebaseVisionText) {
StringBuilder resultText = new StringBuilder();
for (FirebaseVisionText.TextBlock block: firebaseVisionText.getTextBlocks()) {
for (FirebaseVisionText.Line line: block.getLines()) {
resultText.append(line.getText()).append("\n");
}
resultText.append("\n");
}
return resultText.toString().trim();
}
Здесь мы полученный текст после каждой линии переводился на следующую строку, а после каждого блока еще раз, чтобы был виден конец каждого «абзаца».
Осталось добавить копирование полученного текста в буфер обмена
private fun copyTextToClipBoard() {
val clipboardService = getSystemService(Context.CLIPBOARD_SERVICE)
val clipboardManager: ClipboardManager = clipboardService as ClipboardManager
val srcText: String = detected_text_view.text.toString()
val clipData = ClipData.newPlainText("Source Text", srcText)
clipboardManager.setPrimaryClip(clipData)
}
Можно использовать этот метод при клике на detected_text_view или же при нажатии отдельной кнопки.
Проверим приложение
Небольшие выводы
Мы создали простенькое приложение для распознавания текста из фото, добавили возможность копирования в буфер обмена при помощи небольшого количества кода. Можно улучшить приложение, добавив в него сохранение результатов предыдущих распознанных текстов или же обрезание фото.
Документация библиотеки: ML Kit
Приложение в Google Play: Text Recognizer Line
Гитхаб проекта: Hawoline

