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

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

Попробуйте!

Прежде чем начать

  1. Включите следующие библиотеки ML Kit в свой Podfile:

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

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

Точность распознавания текста может различаться в зависимости от языка. Точность также зависит от стиля письма. Хотя система распознавания цифровых чернил обучена обрабатывать множество различных стилей письма, результаты могут отличаться от пользователя к пользователю.

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

зона для письма

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

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