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

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

Попробуйте это

Прежде чем вы начнете

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

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. После установки или обновления модулей вашего проекта откройте проект 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()
}

Цель-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)
}

Цель-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)
    }
  )
}

Цель-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)

Цель-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.");
    })
}

Цель-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» и запятой и косой чертой.

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

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

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

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

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

Предварительный контекст также может помочь распознавателю идентифицировать разрывы слов, пробелы между словами. Вы можете ввести пробел, но не можете его нарисовать, так как же распознавателю определить, когда заканчивается одно слово и начинается следующее? Если пользователь уже написал «привет» и продолжает писать слово «мир», без предварительного контекста распознаватель возвращает строку «мир». Однако если вы укажете предконтекст «привет», модель вернет строку «мир» с пробелом в начале, поскольку «привет, мир» имеет больше смысла, чем «привет, слово».

Вы должны предоставить максимально длинную строку предварительного контекста, до 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)")
    }
  })

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