Работая над приложением, связанным с финансовыми операциями,
возникла необходимость распознать и выделить суммы на чеках.
Начиная с 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?
-
Очередь для задач Vision не вызывает никаких затруднений у разработчиков. Именно в ней будут выполняться все задачи фреймворка.
-
Объявляется переменная типа 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криншоты полученного приложения для распознавания цифровых
значений на различных изображениях представлены ниже:
Для ознакомления проект можно скачать из репозитория.