Благодаря функции распознавания цифровых чернил ML Kit вы сможете распознавать рукописный текст на цифровой поверхности на сотнях языков, а также классифицировать рисунки.
Попробуйте это
- Поэкспериментируйте с образцом приложения , чтобы увидеть пример использования этого API.
Прежде чем начать
Включите в свой Podfile следующие библиотеки ML Kit:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
После установки или обновления Pods вашего проекта откройте проект Xcode, используя его
.xcworkspace
. ML Kit поддерживается в Xcode версии 13.2.1 или выше.
Теперь вы готовы начать распознавать текст в объектах Ink
.
Создание объекта Ink
Основной способ создания объекта Ink
— нарисовать его на сенсорном экране. В iOS вы можете использовать UIImageView вместе с обработчиками событий касания , которые рисуют штрихи на экране, а также сохраняют точки штрихов для создания объекта Ink
. Этот общий шаблон показан в следующем фрагменте кода. См. приложение быстрого старта для более полного примера, который разделяет обработку событий касания, рисование на экране и управление данными штрихов.
Быстрый
@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()
и использует жестко закодированное имя языка. Пример того, как показать пользователю список доступных языков и загрузить выбранный язык, см. в приложении быстрого запуска.
Быстрый
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()
. В других приложениях может потребоваться вызывать распознавание только после тайм-аута или когда пользователь нажимает кнопку для запуска распознавания.
Быстрый
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]; }]; }
Управление загрузками моделей
Мы уже видели, как загрузить модель распознавания. Следующие фрагменты кода иллюстрируют, как проверить, была ли уже загружена модель, или удалить модель, когда она больше не нужна, чтобы освободить место для хранения.
Проверьте, была ли уже загружена модель
Быстрый
let model : DigitalInkRecognitionModel = ... let modelManager = ModelManager.modelManager() modelManager.isModelDownloaded(model)
Objective-C
MLKDigitalInkRecognitionModel *model = ...; MLKModelManager *modelManager = [MLKModelManager modelManager]; [modelManager isModelDownloaded:model];
Удалить загруженную модель
Быстрый
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."); }]; }
Советы по повышению точности распознавания текста
Точность распознавания текста может различаться в зависимости от языка. Точность также зависит от стиля письма. Хотя Digital Ink Recognition обучен обрабатывать множество стилей письма, результаты могут различаться от пользователя к пользователю.
Вот несколько способов улучшить точность распознавателя текста. Обратите внимание, что эти методы не применяются к классификаторам рисунков для эмодзи, авторисунков и фигур.
Письменная зона
Во многих приложениях есть четко определенная область ввода для пользователя. Значение символа частично определяется его размером относительно размера области ввода, которая его содержит. Например, разница между строчной или заглавной буквой "o" или "c", а также запятой и косой чертой.
Сообщив распознавателю ширину и высоту области письма, можно повысить точность. Однако распознаватель предполагает, что область письма содержит только одну строку текста. Если физическая область письма достаточно велика, чтобы позволить пользователю написать две или более строк, вы можете получить лучшие результаты, передав WritingArea с высотой, которая является вашей наилучшей оценкой высоты одной строки текста. Объект WritingArea, который вы передаете распознавателю, не обязательно должен точно соответствовать физической области письма на экране. Изменение высоты WritingArea таким образом работает лучше в некоторых языках, чем в других.
При указании области письма укажите ее ширину и высоту в тех же единицах, что и координаты штриха. Аргументы координат x, y не имеют требований к единицам измерения — API нормализует все единицы, поэтому единственное, что имеет значение, — это относительный размер и положение штрихов. Вы можете передавать координаты в любом масштабе, который имеет смысл для вашей системы.
Предварительный контекст
Предварительный контекст — это текст, который непосредственно предшествует штрихам в Ink
, которые вы пытаетесь распознать. Вы можете помочь распознавателю, рассказав ему о предварительном контексте.
Например, курсивные буквы "n" и "u" часто путают друг с другом. Если пользователь уже ввел часть слова "arg", он может продолжить с помощью штрихов, которые можно распознать как "ument" или "nment". Указание предконтекста "arg" устраняет неоднозначность, поскольку слово "argument" более вероятно, чем "argnment".
Предварительный контекст также может помочь распознавателю определить разрывы слов, пробелы между словами. Вы можете ввести пробел, но не можете его нарисовать, так как же распознаватель может определить, когда заканчивается одно слово и начинается следующее? Если пользователь уже написал "hello" и продолжает писать слово "world", без предварительного контекста распознаватель вернет строку "world". Однако, если вы укажете предварительный контекст "hello", модель вернет строку "world" с начальным пробелом, поскольку "hello world" имеет больше смысла, чем "helloword".
Вам следует предоставить максимально длинную строку предконтекста, до 20 символов, включая пробелы. Если строка длиннее, распознаватель использует только последние 20 символов.
В примере кода ниже показано, как определить область письма и использовать объект RecognitionContext
для указания предварительного контекста.
Быстрый
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
содержит прямоугольник и эллипс рядом друг с другом, распознаватель может вернуть один или другой (или что-то совершенно другое) в качестве результата, поскольку один кандидат на распознавание не может представлять две формы.