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

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

Попробовать

Подготовка

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

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. После установки или обновления Pods откройте проект 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.");
                                }];
}

Советы по улучшению распознавания текста

Точность распознавания текста может различаться в зависимости от языка. Точность также зависит от стиля письма. Хотя Digital Ink Recognition обучен распознавать разные стили письма, результаты могут отличаться от пользователя к пользователю.

Вот несколько способов повысить точность распознавания текста. Обратите внимание, что эти методы не применяются к классификаторам рисунков для эмодзи, авторисования и фигур.

Область для письма

Во многих приложениях есть специальная область для рукописного ввода. Значение символа частично определяется его размером относительно размера области письма, в которой он находится. Например, разница между строчной и заглавной буквами "о" или "с", а также между запятой и косой чертой.

Если указать ширину и высоту области письма, точность распознавания может повыситься. Однако распознаватель предполагает, что область письма содержит только одну строку текста. Если физическая область письма достаточно велика, чтобы пользователь мог написать две или более строк, вы можете получить лучшие результаты, передав WritingArea с высотой, которая является вашей наилучшей оценкой высоты одной строки текста. Объект WritingArea, который вы передаете распознавателю, не обязательно должен точно соответствовать физической области письма на экране. Изменение высоты WritingArea таким образом работает лучше на некоторых языках, чем на других.

При указании области письма укажите ее ширину и высоту в тех же единицах, что и координаты штриха. Для аргументов координат x,y не требуется указывать единицы измерения – API нормализует все единицы, поэтому важны только относительный размер и положение штрихов. Вы можете передавать координаты в любом масштабе, который подходит для вашей системы.

Предварительный контекст

Предконтекст – это текст, который непосредственно предшествует штрихам в Ink, которые вы пытаетесь распознать. Вы можете помочь распознавателю, указав предварительный контекст.

Например, прописные буквы "п" и "и" часто путают. Если пользователь уже ввел часть слова "arg", он может продолжить ввод штрихами, которые могут быть распознаны как "ument" или "nment". Указание контекста "arg" устраняет неоднозначность, поскольку слово "argument" более вероятно, чем "argnment".

Предварительный контекст также может помочь распознавателю определить пробелы между словами. Вы можете ввести пробел, но не можете его нарисовать. Как же распознаватель определяет, где заканчивается одно слово и начинается другое? Если пользователь уже написал "привет" и продолжает писать "мир", без предварительного контекста распознаватель возвращает строку "мир". Однако если вы укажете предварительный контекст "hello", модель вернет строку " world" с пробелом в начале, поскольку "hello world" имеет больше смысла, чем "helloword".

Укажите как можно более длинную строку предварительного контекста (до 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 содержит прямоугольник и эллипс рядом друг с другом, распознаватель может вернуть один или другой (или что-то совершенно другое) в качестве результата, поскольку один кандидат на распознавание не может представлять две фигуры.