Русский
Русский
English
Статистика
Реклама

Распознавание текста

Как мы научили мессенджер ТамТам распознавать адреса в тексте

01.02.2021 14:09:27 | Автор: admin
Привет! Меня зовут Юра Дорофеев, я работаю над Android-версией мессенджера ТамТам. Представьте, что вы договариваетесь о встрече с другом и он отправляет вам адрес. Но не отдельным сообщением, а посреди другого текста:


Как этот адрес быстро вставить в навигатор или карту? Приходится копировать всё сообщение, потом вырезать лишнее. А если вы за рулем, то проще переписать адрес с нуля или вообще озвучить его голосом. Ну и моё самое любимое поздравления с днем рождения. В текущих реалиях это номер банковской карты среди текста сообщения:


И тоже сидишь, копируешь, вырезаешь или просишь автора скинуть отдельным сообщением, чтобы весь чат не мучился так же, как и ты.

Письмо от Google


Проблема была известна, висела в беклоге, но до нее не доходили руки. Пока я не получил письмо от коллеги из отдела Platform Relations: Google разрабатывает решение для поиска сущностей в тексте и приглашает партнеров к тестированию, в том числе и нас. У компании есть набор библиотек Ml Kit, которые позволяют решать какие-то точечные задачи при помощи нейронных сетей. Например, нахождение лица на фотографии или считывание штрих-кода.

Все библиотеки из пакета Ml Kit работают с уже обученными моделями. Никакие данные не уходят на серверы Google, вся обработка происходит офлайн и локально на устройстве. А самое главное, это бесплатно! Google готовился к запуску новой библиотеки Entity Extraction, которая умеет находить сущности в тексте и классифицировать их. Вот пример:



Всего библиотека умеет находить 11 типов сущностей на 15 языках:



Принцип работы


Нахождение сущностей устроено следующим образом: вначале текст разбивается на слова. Дальше все слова объединятся во всевозможные последовательности с максимальной длиной 15 слов. И для каждой из этих последовательностей производится оценка, насколько этот набор слов похож на какую-либо сущность. Чем больше похож, тем ближе оценка к единице.


После этого из последовательностей, которые пересекаются, берутся те, у которых оценка больше. Последним этапом определяется, что именно за сущность скрыта в данной последовательности.


Интересно, что не для всех типов сущностей используются нейронные сети. Например, для распознавания времени, даты, номеров авиарейсов используются регулярные выражения.

Пробуем


Звучит очень круто, а самое главное должно помочь нам с решением проблемы копирования номера банковской карты из текста сообщения. Мы согласились поучаствовать в программе раннего доступа. Google выслал нам библиотеку с документацией. Было опасение, что библиотека окажется монструозная и использовать её будет очень сложно. Давайте проверим.

Чтобы начать работу, нам нужно инициализировать EntityExtractor. Это та самая штука, которая будет находить сущности в тексте. Инициализируется она очень просто: нужно передать опции с минимальным количеством аргументов язык, на котором написан исходный текст, и Executor. По умолчанию библиотека использует свой Executor, но можно перевести выполнение на любой другой.

private val entityExtractor: Lazy<EntityExtractor> = lazy {       EntityExtraction.getClient(           EntityExtractorOptions.Builder(buildModelLocale())               .setExecutor(executor)               .build()       )   }

Зачем указывать язык?


Под каждый язык обучена своя модель, и библиотеке нужно знать, какую модель использовать. Казалось бы, можно же автоматически определить язык? Да, для этого есть отдельная библиотека из набора ML Kit и можно её подключить. Она тоже довольно легкая и имеет простой интерфейс.

Как использовать EntityExtractor?


Всё просто. Вначале собираем параметры для работы экстрактора. Устанавливаем preferredLocale. Это не язык, на котором будет вестись распознавание, а языковой стандарт для форматирования различных сущностей. Например: 1.10.2021 в русском языке первое октября, а в английском десятое января, здесь разный порядок месяца и даты.

Далее устанавливаем список сущностей, которые мы готовы обработать. Настраиваем загрузку моделей. В простейшем случае выставляем downloadModelIfNeeded, что означает, что необходимая модель будет скачана, если будет такая необходимость. Если нужна более сложная логика, то есть механизм, позволяющий качать модели по желанию:

private fun entityAnnotationsSingle(text: String): Single<List<EntityAnnotation>> {       return Single.create { emitter: SingleEmitter<List<EntityAnnotation>> ->           // .........           val params = EntityExtractionParams.Builder(text)               .setPreferredLocale(userLocale)               .setEntityTypesFilter(SUPPORTED_TYPES)               .build()           entityExtractor               .downloadModelIfNeeded()               .onSuccessTask { entityExtractor.annotate(params) }               .addOnFailureListener(executor, onFailureListener)               .addOnSuccessListener(executor, onSuccessListener)       }   }

Скачивание моделей


В начале статьи я сказал, что библиотека работает полностью офлайн, а парой строчек выше я говорю, что нужно качать какие-то модели. Нестыковка. В самой библиотеке нет моделей, они скачиваются под каждый язык. Но это единственное, что вам нужно будет скачать, далее библиотека будет работать офлайн. Сами модели небольшие, примерно 600-700 Кб. Модель скачивается в папку files в директории приложения. Странно, конечно, что нельзя задать свой путь. Поэтому, если у вас в приложении есть какая-то очистка кэша, не забудьте настроить исключение на эту папку:



P.S. Уже во время использования выяснилось, что библиотека может падать на вызове downloadModelIfNeeded, не забудьте завернуть в try-catch.

Используем сущности


Окей, мы скачали модели, задействовали EntityExtractor, что дальше? А дальше просто выставляйте Span в ваш текст, настраивайте цвет отображения, действия по клику и всё, что вашей душе угодно:

fun addMlEntities(text: CharSequence): Maybe<CharSequence> {       return entityAnnotationsSingle(text.toString())           .onErrorReturnItem(emptyList())           .flatMapMaybe { entityAnnotations: List<EntityAnnotation> ->               if (entityAnnotations.isEmpty()) {                   return@flatMapMaybe Maybe.empty()               }               val spannable = text.spannable()               for (annotation in entityAnnotations) {                   if (annotation.entities.isNotEmpty()) {                       val span = MlSpan(annotation.entities[0], annotation.annotatedText, color)                       spannable.setSpan(span, annotation.start, annotation.end, SPAN_EXCLUSIVE_EXCLUSIVE)                   }               }               return@flatMapMaybe Maybe.just(spannable)           }   }

Всё просто и понятно. Так? Нет, не так. Скорость обработки одного сообщения колеблется от 8 до 100 мс. Это не так уж и быстро. Сообщения в чатах у нас грузятся чанками по 40 сообщений. В худшем случае обработка займет 4000 мс или 4 с. То есть потенциально можно задержать открытие чата на 4 с.


Пришлось сделать небольшой хак: изначально мы показываем сообщение как есть, и если в фоне оно обработано и там была найдена сущность, мы анимированно покажем найденный объект.

class MlSpan(...) : ClickableSpan() {   // ....   override fun updateDrawState(ds: TextPaint) {       if (!this::colorAnimator.isInitialized) {           colorAnimator = ValueAnimator.ofObject(argbEvaluator, ds.color, color)           colorAnimator.duration = 200           colorAnimator.addUpdateListener {               currentColor = it.animatedValue as Int               animationListener?.onAnimationUpdate()           }           colorAnimator.start()       }       ds.color = currentColor   }}

Выглядит неплохо, и не пришлось задерживать открытие чата:


Звонок в Google


После того, как библиотека была протестирована, мы собрали список багов и отправили в Google. Также заполнили специальную форму обратной связи. На что Google нам предложил созвониться и обсудить это голосом. Круто! На созвоне были менеджер проекта Ml Kit и автор библиотеки. Мы не погружались особо в технические детали, скорее, просто познакомились и обсудили статус и текущие вопросы. Очень круто, что Google так внимательно относится к проработке своих продуктов и поддержке партнеров.

Заключение


Сейчас в Google Play опубликована версия мессенджера ТамТам, которая включает все решения, описанные выше. Мы умеем находить в тексте:

  • адреса;
  • e-mail;
  • номера телефонов;
  • почтовые номера отслеживания;
  • номера банковских карт.

Кроме того, поскольку мы одними из первых приложений в мире успешно внедрили библиотеку, о ТамТам написал Google в своем девелоперском блоге:

Подробнее..

Распознание блоков текста в IOS-приложении с помощью Vision

18.02.2021 14:23:54 | Автор: admin

Работая над приложением, связанным с финансовыми операциями, возникла необходимость распознать и выделить суммы на чеках. Начиная с 13-ой версии в IOS-разработке появился нативный фреймворк Vision, который позволяет распознавать различные объекты на изображениях, без задействования сторонних сервисов.
В данной статье представлен личный опыт разработки приложения, использующего Vision.

Что такое Vision

Из документации Apple: "Vision применяет алгоритмы "компьютерного зрения" для выполнения множества задач с входными изображениями и видео. Фреймворк Vision выполняет распознание лиц, обнаружение текста, распознавание штрих-кодов, регистрацию изображений. Vision также позволяет использовать пользовательские модели CoreML для таких задач, как классификация или обнаружение объектов."
Анализируя документацию Apple, можно предположить, что Vision - это один из этапов подготовки таких продуктов как Apple glasses или шлем смешанной реальности. Забегая вперед, следует подчеркнуть, что данный фреймворк потребляет изрядное количество ресурсов. Обработка статичного изображения может занимать десятки секунд, следовательно, работа с видео в реальном времени будет предельно ресурсоемким процессом, над оптимизацией которого инженерам Apple еще предстоит поработать.
В рамках поставленной задачи, необходимо было решить следующую проблему: распознание блоков текста с помощью Vision.

Разработка

Проект построен на UIKit, который в данной статье детально рассматриваться не будет. Основное внимание уделяется блокам кода, связанным с фреймворком Vision. Приведенные листинги снабжены комментариями, позволяющими разработчикам детальнее понять принцип работы с фреймворком.
В MainViewController, который будет взаимодействовать с фреймворком Vision, нужно объявить две переменные:

//Recognition queuelet textRecognitionWorkQueue = DispatchQueue(label: "TextRecognitionQueue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)//Request for text recognitionvar textRecognitionRequest: VNRecognizeTextRequest?
  1. Очередь для задач Vision не вызывает никаких затруднений у разработчиков. Именно в ней будут выполняться все задачи фреймворка.

  2. Объявляется переменная типа VNRecognizeTextRequet. Инициализируется объект из ViewDidLoad (или из init), так как он должен быть активен на протяжении всей жизни ViewController. Этот объект отвечает за работу с Vision, поэтому необходимо разобрать его инициализацию подробнее:

//Set textRecognitionRequest from ViewDidLoadfunc setTextRequest() {    textRecognitionRequest = VNRecognizeTextRequest { request, error in        guard let observations = request.results as? [VNRecognizedTextObservation] else {            return        }        var detectedText = ""        self.textBlocks.removeAll()                    for observation in observations {            guard let topCandidate = observation.topCandidates(1).first else { continue }            detectedText += "\(topCandidate.string)\n"                        //Text block specific for this project            if let recognizedBlock = self.getRecognizedDoubleBlock(topCandidate: topCandidate.string, observationBox: observation.boundingBox) {                self.textBlocks.append(recognizedBlock)            }        }                    DispatchQueue.main.async {            self.textView.text = detectedText            self.removeLoader()            self.drawRecognizedBlocks()        }    }            //Individual recognition request settings    textRecognitionRequest!.minimumTextHeight = 0.011 // Lower = better quality    textRecognitionRequest!.recognitionLevel = .accurate}

Настройки объекта textRecognitionRequest. Описание всех доступных настроек можно найти в документации. Наиболее важным является параметр minimumTextHeight. Именно этот параметр отвечает за сочетание быстродействия и точности распознания текста. Для каждого проекта необходимо найти индивидуальное значение данного параметра, оно зависит от того, какие данные будет обрабатывать приложение.
Так как основной поставленной задачей являлось считывание текста с квитанций, для вычисления значения параметра minimumTextHeight в приложение были добавлены различные типы квитанций в различном состоянии (в том числе и основательно помятые). В результате тестирования было определено значение равное 0.011. В случае распознания текста с квитанций, это значение лучшим образом сочетает в себе быстродействие и точность. Однако нужно отметить, что текст с одного изображения распознается в среднем за пять секунд. Подобной скорости недостаточно для обработки информации в реальном времени и ее следует значительно оптимизировать инженерам Apple.
На основе представленного кода можно сделать вывод, что после операции распознания, объект типа VNRecognizeTextRequet получает блоки текста. Именно с ними и ведется дальнейшая работа, в зависимости от функций приложения. В рассматриваемом примере, каждый распознанный фрагмент текста был внесен в текстовое поле. Так как особенностью задействованного приложения является выделение суммы на квитанции, следовательно, сохранялись только блоки текста, которые можно преобразовать в тип Double. Помимо распознанного текстового значения сохраняются и координаты блока текста на изображении.
Представленный ниже метод отвечает за запуск работы запроса на распознание:

//Call text recognition request handlerfunc recognizeImage(cgImage: CGImage) {    textRecognitionWorkQueue.async {        let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])        do {            try requestHandler.perform([self.textRecognitionRequest!])        } catch {            DispatchQueue.main.async {                self.removeLoader()                print(error)            }        }    }}

В метод передается объект CGImage, в котором необходимо распознать текст. Вся работа по распознанию ведется в созданной для этого очереди. Создается объект VNImageRequestHandler, в который передается распознаваемый объект CGImage. В блоке do/try/catch запускается работа инициализированного объекта типа VNRecognizeTextRequet.
Описанные выше функции отвечают за распознание текста в приложении. Однако стоит еще остановится на методах, связанных с выделением нужных блоков текста.

func drawRecognizedBlocks() {    guard let image = invoiceImage?.image else  { return }        //transform from documentation    let imageTransform = CGAffineTransform.identity.scaledBy(x: 1, y: -1).translatedBy(x: 0, y: -image.size.height).scaledBy(x: image.size.width, y: image.size.height)            //drawing rects on cgimage    UIGraphicsBeginImageContextWithOptions(image.size, false, 1.0)    let context = UIGraphicsGetCurrentContext()!    image.draw(in: CGRect(origin: .zero, size: image.size))    context.setStrokeColor(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1))    context.setLineWidth(4)        for index in 0 ..< textBlocks.count {        let optimizedRect = textBlocks[index].recognizedRect.applying(imageTransform)        context.addRect(optimizedRect)        textBlocks[index].imageRect = optimizedRect    }    context.strokePath()            let result = UIGraphicsGetImageFromCurrentImageContext()    UIGraphicsEndImageContext()    invoiceImage?.image = result}

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

struct RecognizedTextBlock {    let doubleValue: Double    let recognizedRect: CGRect    var imageRect: CGRect = .zero}

При распознании блоков текста фреймворк Vision вычисляет ряд важных параметров в объекте VNRecognizedTextObservation. Для нужд рассматриваемого проекта необходимо было получить только значение типа Double и его координаты на изображении, сохраняемые в константе recognizedRect.
Для выделения блока текста на изображении, следует применить трансформацию к координатам из константы recognizedRect. Полученные координаты так же сохраняются в объекте RecognizedTextBlock в переменной imageRect, необходимой для обработки нажатий на выделенные блоки текста.
После сохранения точных координат выделяемых блоков на изображении, обработку нажатий на выделенные области можно осуществить несколькими способами:

  • Добавить необходимое количество невидимых кнопок на изображение, при помощи трансформации сохраненного объекта imageRect;

  • При каждом нажатии на изображение проверять массив блоков текста и искать совпадение координат нажатия с сохраненным объектом imageRect и др.

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

//UIImageView tap listener@objc func onImageViewTap(sender: UITapGestureRecognizer) {    guard let invoiceImage = invoiceImage, let image = invoiceImage.image else {        return    }            //get tap coordinates on image    let tapX = sender.location(in: invoiceImage).x    let tapY = sender.location(in: invoiceImage).y    let xRatio = image.size.width / invoiceImage.bounds.width    let yRatio = image.size.height / invoiceImage.bounds.height    let imageXPoint = tapX * xRatio    let imageYPoint = tapY * yRatio    //detecting if one of text blocks tapped    for block in textBlocks {        if block.imageRect.contains(CGPoint(x: imageXPoint, y: imageYPoint)) {            showTapAlert(doubleValue: block.doubleValue)            break        }    }}

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

Выводы

Представленная статья позволяет разработчикам IOS-приложений ознакомиться с фреймворком Vision, прежде всего с его функцией распознания текста. Тестовое приложение, полученное в результате работы с распознанием текста в Vision, является ключом к пониманию работы с такими возможностями фреймворка как распознание лиц, текста, штрих-кодов и др.
Cкриншоты полученного приложения для распознавания цифровых значений на различных изображениях представлены ниже:

Приложение распознающее блоки текста с помощью VisionПриложение распознающее блоки текста с помощью Vision

Для ознакомления проект можно скачать из репозитория.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru