Распознавание рукописного ввода с помощью ML Kit на iOS

С помощью распознавания цифровых чернил ML Kit вы можете распознавать текст, написанный от руки на цифровой поверхности, на сотнях языков, а также классифицировать эскизы.

Попробовать

Подготовка

  1. Добавьте в Podfile следующие библиотеки ML Kit:

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. После установки или обновления 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 рядом друг с другом находятся прямоугольник и эллипс, распознаватель может вернуть в качестве результата один из них (или что-то совершенно другое), поскольку один кандидат на распознавание не может представлять две фигуры.