Распознание блоков текста в IOS-приложении с помощью Vision
Работая над приложением, связанным с финансовыми операциями, возникла необходимость распознать и выделить суммы на чеках. Начиная с 11-ой версии в IOS-разработке появился нативный фреймворк Vision, который позволяет распознавать различные объекты на изображениях, без задействования сторонних сервисов.
В данной статье представлен личный опыт разработки приложения, использующего Vision.
Что такое Vision
Из документации Apple: "Vision применяет алгоритмы "компьютерного зрения" для выполнения множества задач с входными изображениями и видео. Фреймворк Vision выполняет распознание лиц, обнаружение текста, распознавание штрих-кодов, регистрацию изображений. Vision также позволяет использовать пользовательские модели CoreML для таких задач, как классификация или обнаружение объектов."
Анализируя документацию Apple, можно предположить, что Vision - это один из этапов подготовки таких продуктов как Apple glasses или шлем смешанной реальности. Забегая вперед, следует подчеркнуть, что данный фреймворк потребляет изрядное количество ресурсов. Обработка статичного изображения может занимать десятки секунд, следовательно, работа с видео в реальном времени будет предельно ресурсоемким процессом, над оптимизацией которого инженерам Apple еще предстоит поработать.
В рамках поставленной задачи, необходимо было решить следующую проблему: распознание блоков текста с помощью Vision.
Разработка
Проект построен на UIKit, который в данной статье детально рассматриваться не будет. Основное внимание уделяется блокам кода, связанным с фреймворком Vision. Приведенные листинги снабжены комментариями, позволяющими разработчикам детальнее понять принцип работы с фреймворком.
В MainViewController, который будет взаимодействовать с фреймворком Vision, нужно объявить две переменные:
//Recognition queue
let textRecognitionWorkQueue = DispatchQueue(label: "TextRecognitionQueue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
//Request for text recognition
var textRecognitionRequest: VNRecognizeTextRequest?
Очередь для задач Vision не вызывает никаких затруднений у разработчиков. Именно в ней будут выполняться все задачи фреймворка.
Объявляется переменная типа VNRecognizeTextRequet. Инициализируется объект из ViewDidLoad (или из init), так как он должен быть активен на протяжении всей жизни ViewController. Этот объект отвечает за работу с Vision, поэтому необходимо разобрать его инициализацию подробнее:
//Set textRecognitionRequest from ViewDidLoad
func 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 handler
func 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криншоты полученного приложения для распознавания цифровых значений на различных изображениях представлены ниже:
Для ознакомления проект можно скачать из репозитория.