在 iOS 系统中使用机器学习套件识别数字手写内容

借助机器学习套件的数字手写识别功能,您可以识别 并支持数百种语言的数字化平面,以及对素描进行分类。

试试看

  • 您可以试用示例应用, 请查看此 API 的用法示例。

准备工作

  1. 在 Podfile 中添加以下机器学习套件库:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. 安装或更新项目的 Pod 之后,请打开您的 Xcode 项目 使用其 .xcworkspace。Xcode 版本支持机器学习套件 13.2.1 或更高版本。

现在,您可以开始识别 Ink 对象中的文本了。

构建 Ink 对象

构建 Ink 对象的主要方法是在触摸屏上绘制该对象。在 iOS 上 可以使用 UIImageView触摸事件 处理程序 它会在屏幕上绘制笔画,并存储笔画的构建 Ink 对象。以下代码演示了这种常规模式 代码段。请参阅快速入门 app 更完整的示例,它将触摸事件处理、屏幕绘制、 以及笔画数据管理

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 实例, 我们首先需要下载所需语言的识别器模型, 将模型加载到 RAM 中。可以使用以下代码实现这一目的 代码段,为简单起见,将其放置在 viewDidLoad() 方法中,并使用 硬编码语言名称。请参阅快速入门 app 示例:如何向用户显示可用语言列表并下载 所选语言。

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.");
                                }];
}

提高文字识别准确性的技巧

文本识别的准确性可能因语言而异。准确性还取决于 写作风格虽然数字墨水识别经过训练,能够处理多种书写风格, 结果可能因用户而异。

以下是一些可以提高文本识别器准确性的方法。请注意,这些技术 不适用于表情符号、Autodraw 和形状的绘制分类器。

书写区域

许多应用都有明确定义的写入区域,供用户输入。符号的含义是 该尺寸在一定程度上取决于其尺寸(相对于包含它的书写区域的大小)。 例如,小写或大写字母“o”之间的区别或“c”以及英文逗号和 a 正斜线。

让识别器知道书写区域的宽度和高度可以提高准确性。不过, 识别器会假定书写区域仅包含一行文本。如果 书写区域足够大,可供用户写两行或两行以上的内容, 您可以传递一个 WriteArea,其中的高度是您高度估计的 单行文本。您传递给识别器的 writingArea 对象不必对应 与屏幕上的实际书写区域完全一致。以这种方式更改 WriteArea 高度 在某些语言中效果更好。

指定书写区域时,请指定其宽度和高度(单位与描边单位相同) 坐标。x,y 坐标参数没有单位要求,API 会将所有 单位,因此唯一重要的是笔画的相对大小和位置。你随时都可以 传入对系统有意义的任何比例的坐标。

预先提供上下文

预上下文是指您Ink 识别对象。您可以通过告知识别器预先上下文来帮助识别器。

例如,书写字母“n”和“u”经常会互相混淆。如果用户 那么就可能继续以可被识别为“arg”的笔画继续。 “ument”或“nment”。指定预上下文“arg”消除了歧义, “参数”比“argnment”的概率更高。

预上下文还有助于识别器识别断字,即字词之间的空格。您可以 输入空格字符但无法画出空格,那么识别器如何确定一个字词的结尾 下一轮要开始了?如果用户已经写了“hello”接着用书面单词 “world”是指在没有预上下文的情况下,识别器会返回字符串“world”。但是,如果您指定 “hello”之前,模型将返回字符串“包含前导空格,例如“hello” 世界”比“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 包含一个矩形和一个椭圆形 识别器可能会返回其中一种(或完全不同的内容)作为 因为一个候选识别模型不能表示两种形状。