Благодаря функции распознавания цифровых чернил ML Kit вы сможете распознавать рукописный текст на цифровой поверхности на сотнях языков, а также классифицировать рисунки.
Попробуйте это
- Поэкспериментируйте с образцом приложения , чтобы увидеть пример использования этого API.
Прежде чем начать
Включите следующие библиотеки ML Kit в ваш Podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
После установки или обновления модулей вашего проекта откройте проект 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."); }]; }
Советы по повышению точности распознавания текста
Точность распознавания текста может различаться в зависимости от языка. Точность также зависит от стиля письма. Хотя система распознавания рукописного текста обучена обрабатывать множество стилей письма, результаты могут различаться у разных пользователей.
Вот несколько способов повысить точность распознавания текста. Обратите внимание, что эти методы не применимы к классификаторам рисунков эмодзи, авторисунков и фигур.
Зона для письма
Во многих приложениях есть чётко определённая область ввода для пользователя. Значение символа частично определяется его размером относительно размера области ввода, в которой он находится. Например, разница между строчной и прописной буквой «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
содержит прямоугольник и эллипс, расположенные рядом друг с другом, распознаватель может вернуть один из них (или что-то совершенно другое) в качестве результата, поскольку один кандидат на распознавание не может представлять две фигуры.