С помощью распознавания цифровых чернил ML Kit вы можете распознавать текст, написанный от руки на цифровой поверхности, на сотнях языков, а также классифицировать эскизы.
Попробовать
- Попробуйте поработать с примером приложения, чтобы понять, как использовать этот API.
Подготовка
Добавьте в Podfile следующие библиотеки ML Kit:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'После установки или обновления Pod-файлов проекта откройте проект Xcode, используя его файл
.xcworkspace. ML Kit поддерживается в Xcode версии 13.2.1 и более поздних.
Теперь вы можете распознавать текст в объектах Ink.
Как создать объект Ink
Основной способ создания объекта Ink – нарисовать его на сенсорном экране. В iOS можно использовать UIImageView вместе с обработчиками событий касания, которые рисуют штрихи на экране и сохраняют их точки для создания объекта Ink. Ниже приведен фрагмент кода, демонстрирующий этот общий принцип. Более полный пример, в котором обработка событий касания, рисование на экране и управление данными штрихов выполняются отдельно, можно найти в приложении для быстрого запуска.
Swift
@IBOutlet weak var mainImageView: UIImageView! var kMillisecondsPerTimeInterval = 1000.0 var lastPoint = CGPoint.zero private var strokes: [Stroke] = [] private var points: [StrokePoint] = [] func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) { UIGraphicsBeginImageContext(view.frame.size) guard let context = UIGraphicsGetCurrentContext() else { return } mainImageView.image?.draw(in: view.bounds) context.move(to: fromPoint) context.addLine(to: toPoint) context.setLineCap(.round) context.setBlendMode(.normal) context.setLineWidth(10.0) context.setStrokeColor(UIColor.white.cgColor) context.strokePath() mainImageView.image = UIGraphicsGetImageFromCurrentImageContext() mainImageView.alpha = 1.0 UIGraphicsEndImageContext() } override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard let touch = touches.first else { return } lastPoint = touch.location(in: mainImageView) let t = touch.timestamp points = [StrokePoint.init(x: Float(lastPoint.x), y: Float(lastPoint.y), t: Int(t * kMillisecondsPerTimeInterval))] drawLine(from:lastPoint, to:lastPoint) } override func touchesMoved(_ touches: Set , with event: UIEvent?) { guard let touch = touches.first else { return } let currentPoint = touch.location(in: mainImageView) let t = touch.timestamp points.append(StrokePoint.init(x: Float(currentPoint.x), y: Float(currentPoint.y), t: Int(t * kMillisecondsPerTimeInterval))) drawLine(from: lastPoint, to: currentPoint) lastPoint = currentPoint } override func touchesEnded(_ touches: Set , with event: UIEvent?) { guard let touch = touches.first else { return } let currentPoint = touch.location(in: mainImageView) let t = touch.timestamp points.append(StrokePoint.init(x: Float(currentPoint.x), y: Float(currentPoint.y), t: Int(t * kMillisecondsPerTimeInterval))) drawLine(from: lastPoint, to: currentPoint) lastPoint = currentPoint strokes.append(Stroke.init(points: points)) self.points = [] doRecognition() }
Objective-C
// Interface @property (weak, nonatomic) IBOutlet UIImageView *mainImageView; @property(nonatomic) CGPoint lastPoint; @property(nonatomic) NSMutableArray*strokes; @property(nonatomic) NSMutableArray *points; // Implementations static const double kMillisecondsPerTimeInterval = 1000.0; - (void)drawLineFrom:(CGPoint)fromPoint to:(CGPoint)toPoint { UIGraphicsBeginImageContext(self.mainImageView.frame.size); [self.mainImageView.image drawInRect:CGRectMake(0, 0, self.mainImageView.frame.size.width, self.mainImageView.frame.size.height)]; CGContextMoveToPoint(UIGraphicsGetCurrentContext(), fromPoint.x, fromPoint.y); CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), toPoint.x, toPoint.y); CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound); CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 10.0); CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 1, 1, 1, 1); CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal); CGContextStrokePath(UIGraphicsGetCurrentContext()); CGContextFlush(UIGraphicsGetCurrentContext()); self.mainImageView.image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } - (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event { UITouch *touch = [touches anyObject]; self.lastPoint = [touch locationInView:self.mainImageView]; NSTimeInterval time = [touch timestamp]; self.points = [NSMutableArray array]; [self.points addObject:[[MLKStrokePoint alloc] initWithX:self.lastPoint.x y:self.lastPoint.y t:time * kMillisecondsPerTimeInterval]]; [self drawLineFrom:self.lastPoint to:self.lastPoint]; } - (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint currentPoint = [touch locationInView:self.mainImageView]; NSTimeInterval time = [touch timestamp]; [self.points addObject:[[MLKStrokePoint alloc] initWithX:currentPoint.x y:currentPoint.y t:time * kMillisecondsPerTimeInterval]]; [self drawLineFrom:self.lastPoint to:currentPoint]; self.lastPoint = currentPoint; } - (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint currentPoint = [touch locationInView:self.mainImageView]; NSTimeInterval time = [touch timestamp]; [self.points addObject:[[MLKStrokePoint alloc] initWithX:currentPoint.x y:currentPoint.y t:time * kMillisecondsPerTimeInterval]]; [self drawLineFrom:self.lastPoint to:currentPoint]; self.lastPoint = currentPoint; if (self.strokes == nil) { self.strokes = [NSMutableArray array]; } [self.strokes addObject:[[MLKStroke alloc] initWithPoints:self.points]]; self.points = nil; [self doRecognition]; }
Обратите внимание, что фрагмент кода содержит пример функции для рисования штриха в UIImageView, которую необходимо адаптировать для вашего приложения. Рекомендуем использовать закругленные концы линий, чтобы сегменты нулевой длины выглядели как точки (например, как точка над буквой "i"). Функция doRecognition() вызывается после каждого штриха и будет определена ниже.
Получите экземпляр DigitalInkRecognizer
Чтобы выполнить распознавание, нужно передать объект Ink экземпляру DigitalInkRecognizer. Чтобы получить экземпляр DigitalInkRecognizer, сначала нужно скачать модель распознавания для нужного языка и загрузить ее в ОЗУ. Это можно сделать с помощью следующего фрагмента кода, который для простоты размещен в методе viewDidLoad() и использует жестко заданное название языка. В приложении для быстрого запуска приведен пример того, как показать пользователю список доступных языков и скачать выбранный язык.
Swift
override func viewDidLoad() { super.viewDidLoad() let languageTag = "en-US" let identifier = DigitalInkRecognitionModelIdentifier(forLanguageTag: languageTag) if identifier == nil { // no model was found or the language tag couldn't be parsed, handle error. } let model = DigitalInkRecognitionModel.init(modelIdentifier: identifier!) let modelManager = ModelManager.modelManager() let conditions = ModelDownloadConditions.init(allowsCellularAccess: true, allowsBackgroundDownloading: true) modelManager.download(model, conditions: conditions) // Get a recognizer for the language let options: DigitalInkRecognizerOptions = DigitalInkRecognizerOptions.init(model: model) recognizer = DigitalInkRecognizer.digitalInkRecognizer(options: options) }
Objective-C
- (void)viewDidLoad { [super viewDidLoad]; NSString *languagetag = @"en-US"; MLKDigitalInkRecognitionModelIdentifier *identifier = [MLKDigitalInkRecognitionModelIdentifier modelIdentifierForLanguageTag:languagetag]; if (identifier == nil) { // no model was found or the language tag couldn't be parsed, handle error. } MLKDigitalInkRecognitionModel *model = [[MLKDigitalInkRecognitionModel alloc] initWithModelIdentifier:identifier]; MLKModelManager *modelManager = [MLKModelManager modelManager]; [modelManager downloadModel:model conditions:[[MLKModelDownloadConditions alloc] initWithAllowsCellularAccess:YES allowsBackgroundDownloading:YES]]; MLKDigitalInkRecognizerOptions *options = [[MLKDigitalInkRecognizerOptions alloc] initWithModel:model]; self.recognizer = [MLKDigitalInkRecognizer digitalInkRecognizerWithOptions:options]; }
В приложениях для быстрого запуска есть дополнительный код, который показывает, как обрабатывать несколько скачиваний одновременно и как определять, какое из них завершилось успешно, с помощью уведомлений о завершении.
Распознавание объекта Ink
Далее идет функция doRecognition(), которая для простоты вызывается из touchesEnded(). В других приложениях распознавание может запускаться только после истечения тайм-аута или когда пользователь нажимает кнопку.
Swift
func doRecognition() { let ink = Ink.init(strokes: strokes) recognizer.recognize( ink: ink, completion: { [unowned self] (result: DigitalInkRecognitionResult?, error: Error?) in var alertTitle = "" var alertText = "" if let result = result, let candidate = result.candidates.first { alertTitle = "I recognized this:" alertText = candidate.text } else { alertTitle = "I hit an error:" alertText = error!.localizedDescription } let alert = UIAlertController(title: alertTitle, message: alertText, preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) self.present(alert, animated: true, completion: nil) } ) }
Objective-C
- (void)doRecognition { MLKInk *ink = [[MLKInk alloc] initWithStrokes:self.strokes]; __weak typeof(self) weakSelf = self; [self.recognizer recognizeInk:ink completion:^(MLKDigitalInkRecognitionResult *_Nullable result, NSError *_Nullable error) { typeof(weakSelf) strongSelf = weakSelf; if (strongSelf == nil) { return; } NSString *alertTitle = nil; NSString *alertText = nil; if (result.candidates.count > 0) { alertTitle = @"I recognized this:"; alertText = result.candidates[0].text; } else { alertTitle = @"I hit an error:"; alertText = [error localizedDescription]; } UIAlertController *alert = [UIAlertController alertControllerWithTitle:alertTitle message:alertText preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; [strongSelf presentViewController:alert animated:YES completion:nil]; }]; }
Как управлять скачиванием моделей
Мы уже видели, как скачать модель распознавания. В приведенных ниже фрагментах кода показано, как проверить, была ли уже скачана модель, или удалить модель, когда она больше не нужна, чтобы освободить место в хранилище.
Как проверить, скачана ли модель
Swift
let model : DigitalInkRecognitionModel = ... let modelManager = ModelManager.modelManager() modelManager.isModelDownloaded(model)
Objective-C
MLKDigitalInkRecognitionModel *model = ...; MLKModelManager *modelManager = [MLKModelManager modelManager]; [modelManager isModelDownloaded:model];
Как удалить скачанную модель
Swift
let model : DigitalInkRecognitionModel = ... let modelManager = ModelManager.modelManager() if modelManager.isModelDownloaded(model) { modelManager.deleteDownloadedModel( model!, completion: { error in if error != nil { // Handle error return } NSLog(@"Model deleted."); }) }
Objective-C
MLKDigitalInkRecognitionModel *model = ...; MLKModelManager *modelManager = [MLKModelManager modelManager]; if ([self.modelManager isModelDownloaded:model]) { [self.modelManager deleteDownloadedModel:model completion:^(NSError *_Nullable error) { if (error) { // Handle error. return; } NSLog(@"Model deleted."); }]; }
Советы по улучшению распознавания текста
Точность распознавания текста может различаться в зависимости от языка. Точность также зависит от стиля письма. Хотя функция распознавания рукописного ввода обучена распознавать разные стили письма, результаты могут отличаться в зависимости от пользователя.
Вот несколько способов повысить точность распознавания текста. Обратите внимание, что эти методы не применяются к классификаторам рисунков для эмодзи, авторисования и фигур.
Область ввода
Во многих приложениях есть четко определенная область для ввода текста. Значение символа частично определяется его размером относительно размера области письма, в которой он находится. Например, разница между строчной и прописной буквами "о" или "с", а также между запятой и косой чертой.
Если указать ширину и высоту области ввода, распознавание может стать точнее. Однако распознаватель предполагает, что в области письма содержится только одна строка текста. Если физическая область ввода достаточно велика, чтобы пользователь мог написать две или более строк, вы можете получить более точные результаты, передав объект WritingArea с высотой, которая, по вашим оценкам, соответствует высоте одной строки текста. Объект WritingArea, который вы передаете распознавателю, не обязательно должен точно соответствовать физической области ввода на экране. Изменение высоты WritingArea таким образом работает лучше на некоторых языках, чем на других.
При указании области письма укажите ее ширину и высоту в тех же единицах, что и координаты штриха. Аргументы координат x и y не требуют указания единиц измерения – API нормализует все единицы, поэтому имеет значение только относительный размер и положение штрихов. Вы можете передавать координаты в любой системе координат, которая подходит для вашей системы.
Предварительный контекст
Предконтекст – это текст, который непосредственно предшествует штрихам в Ink, которые вы пытаетесь распознать. Вы можете помочь распознавателю, предоставив ему информацию о контексте.
Например, рукописные буквы "n" и "u" часто путают. Если пользователь уже ввел часть слова "arg", он может продолжить ввод символами, которые распознаются как "ument" или "nment". Указание предварительного контекста "arg" устраняет неоднозначность, поскольку слово "argument" более вероятно, чем "argnment".
Предварительный контекст также может помочь распознавателю определить пробелы между словами. Вы можете ввести пробел, но не можете нарисовать его. Как распознаватель определяет, где заканчивается одно слово и начинается другое? Если пользователь уже написал "hello" и продолжает писать слово "world", то без предварительного контекста распознаватель вернет строку "world". Однако если вы укажете предварительный контекст "привет", модель вернет строку "мир" с пробелом в начале, поскольку "привет мир" имеет больше смысла, чем "приветмир".
Укажите максимально длинную строку предконтекста (до 20 символов, включая пробелы). Если строка длиннее, распознаватель использует только последние 20 символов.
В приведенном ниже примере кода показано, как определить область ввода и использовать объект RecognitionContext, чтобы задать контекст.
Swift
let ink: Ink = ...; let recognizer: DigitalInkRecognizer = ...; let preContext: String = ...; let writingArea = WritingArea.init(width: ..., height: ...); let context: DigitalInkRecognitionContext.init( preContext: preContext, writingArea: writingArea); recognizer.recognizeHandwriting( from: ink, context: context, completion: { (result: DigitalInkRecognitionResult?, error: Error?) in if let result = result, let candidate = result.candidates.first { NSLog("Recognized \(candidate.text)") } else { NSLog("Recognition error \(error)") } })
Objective-C
MLKInk *ink = ...; MLKDigitalInkRecognizer *recognizer = ...; NSString *preContext = ...; MLKWritingArea *writingArea = [MLKWritingArea initWithWidth:... height:...]; MLKDigitalInkRecognitionContext *context = [MLKDigitalInkRecognitionContext initWithPreContext:preContext writingArea:writingArea]; [recognizer recognizeHandwritingFromInk:ink context:context completion:^(MLKDigitalInkRecognitionResult *_Nullable result, NSError *_Nullable error) { NSLog(@"Recognition result %@", result.candidates[0].text); }];
Порядок штрихов
Точность распознавания зависит от порядка штрихов. Распознаватели ожидают, что штрихи будут наноситься в том порядке, в котором люди обычно пишут, например слева направо для английского языка. Если вы напишете предложение на английском языке, начиная с последнего слова, результаты будут менее точными.
Другой пример: слово в середине Ink удаляется и заменяется другим словом. Исправление, вероятно, находится в середине предложения, но штрихи для исправления находятся в конце последовательности штрихов.
В этом случае мы рекомендуем отправить новое слово в API отдельно и объединить результат с предыдущими распознаваниями, используя собственную логику.
Как работать с неоднозначными фигурами
В некоторых случаях значение фигуры, предоставленной распознавателю, неоднозначно. Например, прямоугольник с очень скругленными краями можно принять за прямоугольник или эллипс.
В таких случаях можно использовать оценки распознавания, если они доступны. Оценки предоставляют только классификаторы фигур. Если модель уверена в результате, то оценка лучшего варианта будет намного выше, чем оценка второго по качеству варианта. Если результаты неоднозначные, оценки для двух лучших вариантов будут близки. Кроме того, помните, что классификаторы форм интерпретируют весь символ Ink как одну форму. Например, если на изображении Ink рядом друг с другом находятся прямоугольник и эллипс, распознаватель может вернуть в качестве результата один из них (или что-то совершенно другое), поскольку один кандидат на распознавание не может представлять две фигуры.