在 iOS 上使用 ML Kit 辨識數位墨水

有了 ML Kit 數位墨水辨識功能,您就能辨識手寫文字 支援數百種語言,以及分類草圖。

立即試用

事前準備

  1. 在 Podfile 中納入下列 ML Kit 程式庫:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. 安裝或更新專案的 Pod 後,請開啟 Xcode 專案 使用 .xcworkspace。Xcode 版本支援 ML Kit 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 例項, 我們必須先下載所需語言的辨識工具模型 在 RAM 中使用以下程式碼即可 為簡單起見,置於 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.");
                                }];
}

提高文字辨識準確度的訣竅

文字辨識的準確度可能因語言而異。準確度也會取決於 創作風格數位墨水辨識經過充分訓練,可處理多種寫作風格, 每個使用者的結果都可能不同

你可以透過以下幾種方式提高文字辨識工具的準確度。請注意,這些技巧 不適用於表情符號、自動繪圖和圖形的繪圖分類器。

寫作領域

許多應用程式都有明確定義的使用者輸入內容寫入區域。符號的意義是 部分取決於其大小,相對於包含該元素的撰寫區域大小。 例如小寫或大寫英文字母「o」還有半形逗號和 a 的 。

告知辨識器能改善書寫區域的寬度和高度。不過 辨識器會假設書寫區域只包含一行文字。如果 撰寫區域範圍夠大,讓使用者能寫兩行以上的程式碼, 結果,方法是傳入帶有高度的 WriteArea ,預估高度 單行文字。您傳遞至辨識工具的 WriterArea 物件不需要對應至 並且與螢幕上的實際書寫區域完全相符。以這種方式變更 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 包含矩形,且每個項目旁邊都有一個刪節號 其他,辨識工具可能會傳回彼此 (或完全不同的) 做為 結果,因為單一辨識候選字詞無法代表兩個形狀。