Nhận dạng mực kỹ thuật số bằng Bộ công cụ học máy trên iOS

Với tính năng nhận dạng mực kỹ thuật số của Bộ công cụ học máy, bạn có thể nhận dạng văn bản viết tay trên một bề mặt kỹ thuật số bằng hàng trăm ngôn ngữ, cũng như phân loại các bản phác thảo.

Dùng thử

Trước khi bắt đầu

  1. Đưa các thư viện sau đây của Bộ công cụ học máy vào Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Sau khi bạn cài đặt hoặc cập nhật các Nhóm của dự án, hãy mở dự án Xcode bằng .xcworkspace của dự án đó. Bộ công cụ học máy được hỗ trợ trong Xcode phiên bản 13.2.1 trở lên.

Giờ đây, bạn đã sẵn sàng bắt đầu nhận dạng văn bản trong các đối tượng Ink.

Tạo một đối tượng Ink

Cách chính để tạo đối tượng Ink là vẽ đối tượng đó trên màn hình cảm ứng. Trên iOS, bạn có thể sử dụng UIImageView cùng với trình xử lý sự kiện chạm để vẽ những nét trên màn hình, đồng thời lưu trữ các điểm của nét vẽ để tạo đối tượng Ink. Mẫu chung này được minh hoạ trong đoạn mã sau. Hãy xem ứng dụng bắt đầu nhanh để biết ví dụ hoàn chỉnh hơn, trong đó phân tách hoạt động xử lý sự kiện chạm, vẽ màn hình và quản lý dữ liệu nét vẽ.

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];
}

Lưu ý rằng đoạn mã bao gồm một hàm mẫu để vẽ nét vẽ vào UIImageView. Hàm này sẽ được điều chỉnh khi cần thiết cho ứng dụng của bạn. Bạn nên sử dụng dấu ngoặc tròn khi vẽ các đoạn thẳng để các đoạn có độ dài bằng 0 sẽ được vẽ dưới dạng dấu chấm (hãy nghĩ đến dấu chấm trên chữ i viết thường). Hàm doRecognition() được gọi sau khi viết mỗi nét vẽ và sẽ được xác định ở bên dưới.

Nhận một thực thể của DigitalInkRecognizer

Để thực hiện nhận dạng, chúng ta cần truyền đối tượng Ink đến thực thể DigitalInkRecognizer. Để có được thực thể DigitalInkRecognizer, trước tiên, chúng ta cần tải mô hình trình nhận dạng cho ngôn ngữ mong muốn và tải mô hình đó vào RAM. Bạn có thể thực hiện việc này bằng cách sử dụng đoạn mã sau đây. Đoạn mã này được đặt trong phương thức viewDidLoad() và sử dụng tên ngôn ngữ được cố định giá trị trong mã để đơn giản hoá. Hãy xem ứng dụng bắt đầu nhanh để biết ví dụ về cách hiển thị danh sách các ngôn ngữ có sẵn cho người dùng và tải ngôn ngữ đã chọn xuống.

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];
}

Ứng dụng bắt đầu nhanh bao gồm mã bổ sung cho biết cách xử lý nhiều tệp tải xuống cùng lúc và cách xác định tệp tải xuống nào thành công bằng cách xử lý thông báo hoàn thành.

Nhận dạng đối tượng Ink

Tiếp theo, chúng ta đến với hàm doRecognition(). Hàm này được gọi từ touchesEnded() để đơn giản hoá. Trong các ứng dụng khác, có thể bạn chỉ muốn gọi tính năng nhận dạng sau khi hết thời gian chờ hoặc khi người dùng nhấn một nút để kích hoạt tính năng nhận dạng.

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];
  }];
}

Quản lý việc tải mô hình xuống

Chúng ta đã xem cách tải một mô hình nhận dạng xuống. Các đoạn mã sau đây minh hoạ cách kiểm tra xem một mô hình đã được tải xuống hay chưa hoặc để xoá một mô hình khi không còn cần khôi phục dung lượng lưu trữ.

Kiểm tra xem mô hình đã được tải xuống chưa

Swift

let model : DigitalInkRecognitionModel = ...
let modelManager = ModelManager.modelManager()
modelManager.isModelDownloaded(model)

Objective-C

MLKDigitalInkRecognitionModel *model = ...;
MLKModelManager *modelManager = [MLKModelManager modelManager];
[modelManager isModelDownloaded:model];

Xoá mô hình đã tải xuống

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

Mẹo cải thiện độ chính xác của tính năng nhận dạng văn bản

Độ chính xác của tính năng nhận dạng văn bản có thể không giống nhau giữa các ngôn ngữ. Độ chính xác cũng phụ thuộc vào phong cách viết. Mặc dù công nghệ Nhận dạng mực kỹ thuật số được huấn luyện để xử lý nhiều loại kiểu viết, nhưng kết quả có thể khác nhau giữa các người dùng.

Sau đây là một số cách để cải thiện độ chính xác của trình nhận dạng văn bản. Xin lưu ý rằng các kỹ thuật này không áp dụng cho các thuật toán phân loại bản vẽ biểu tượng cảm xúc, tự động vẽ và hình dạng.

Vùng viết

Nhiều ứng dụng có khu vực viết được xác định rõ để người dùng nhập hoạt động vào. Ý nghĩa của ký hiệu được xác định một phần bởi kích thước của biểu tượng so với kích thước vùng viết chứa ký hiệu đó. Ví dụ: sự khác biệt giữa chữ cái viết thường hoặc viết hoa "o" hoặc "c" và dấu phẩy so với dấu gạch chéo lên.

Việc cho trình nhận dạng biết chiều rộng và chiều cao của vùng viết có thể giúp cải thiện độ chính xác. Tuy nhiên, trình nhận dạng giả định rằng vùng viết chỉ chứa một dòng văn bản duy nhất. Nếu vùng viết thực tế đủ lớn để cho phép người dùng viết hai hoặc nhiều dòng, bạn có thể nhận được kết quả tốt hơn bằng cách truyền vào ViếtArea với chiều cao là ước tính tốt nhất về chiều cao của một dòng văn bản. Đối tượng ViếtArea mà bạn truyền đến trình nhận dạng không cần phải tương ứng chính xác với vùng ghi thực tế trên màn hình. Việc thay đổi chiều cao ViếtArea theo cách này hoạt động hiệu quả hơn ở một số ngôn ngữ so với các ngôn ngữ khác.

Khi bạn chỉ định vùng viết, hãy chỉ định chiều rộng và chiều cao của vùng đó theo cùng đơn vị với toạ độ nét. Các đối số toạ độ x, y không có yêu cầu về đơn vị – API chuẩn hoá tất cả các đơn vị, vì vậy, điều duy nhất quan trọng là kích thước và vị trí tương đối của nét vẽ. Bạn có thể tuỳ ý truyền toạ độ ở bất kỳ tỷ lệ nào phù hợp với hệ thống của mình.

Trước ngữ cảnh

Ngữ cảnh trước là văn bản đứng ngay trước các nét trong Ink mà bạn đang cố gắng nhận dạng. Bạn có thể giúp trình nhận dạng bằng cách cho biết về ngữ cảnh trước.

Ví dụ: các chữ cái bằng chữ thảo "n" và "u" thường bị nhầm lẫn với nhau. Nếu người dùng đã nhập một phần từ "arg", họ có thể tiếp tục với các nét vẽ có thể được nhận dạng là "ument" hoặc "nment". Việc chỉ định trước ngữ cảnh "arg" sẽ giải quyết không rõ ràng, vì từ "đối số" có nhiều khả năng hơn so với "argnment".

Ngữ cảnh trước cũng có thể giúp trình nhận dạng xác định các dấu ngắt từ, khoảng cách giữa các từ. Bạn có thể nhập một ký tự dấu cách nhưng không thể vẽ, vậy làm cách nào trình nhận dạng có thể xác định thời điểm một từ kết thúc và từ tiếp theo bắt đầu? Nếu người dùng đã viết "xin chào" và tiếp tục với từ "world" đã viết, không có ngữ cảnh trước, thì trình nhận dạng sẽ trả về chuỗi "world". Tuy nhiên, nếu bạn chỉ định ngữ cảnh trước "hello", mô hình sẽ trả về chuỗi "world" với dấu cách ở đầu, vì "helloworld" sẽ có ý nghĩa hơn so với từ "helloword".

Bạn nên cung cấp chuỗi dài nhất có thể trước ngữ cảnh, tối đa 20 ký tự, bao gồm cả dấu cách. Nếu chuỗi dài hơn, trình nhận dạng chỉ sử dụng 20 ký tự cuối cùng.

Mã mẫu dưới đây cho thấy cách xác định vùng viết và sử dụng đối tượng RecognitionContext để chỉ định trước ngữ cảnh.

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

Thứ tự nét chữ

Độ chính xác của khả năng nhận dạng phụ thuộc vào thứ tự của các nét vẽ. Trình nhận dạng dự kiến các nét vẽ sẽ xảy ra theo thứ tự mà mọi người sẽ viết một cách tự nhiên; ví dụ như từ trái sang phải đối với tiếng Anh. Mọi trường hợp khác với mẫu này (chẳng hạn như viết một câu tiếng Anh bắt đầu bằng từ cuối cùng) sẽ cho kết quả kém chính xác hơn.

Một ví dụ khác là khi một từ ở giữa Ink bị xoá và thay thế bằng một từ khác. Bản sửa đổi có thể nằm ở giữa câu, nhưng các nét cho bản sửa đổi nằm ở cuối trình tự nét. Trong trường hợp này, bạn nên gửi riêng từ mới viết đến API và hợp nhất kết quả với các giá trị nhận dạng trước đó bằng logic của riêng mình.

Xử lý các hình dạng không rõ ràng

Có những trường hợp ý nghĩa của hình dạng được cung cấp cho trình nhận dạng không rõ ràng. Ví dụ: hình chữ nhật có cạnh rất tròn có thể được xem là hình chữ nhật hoặc hình elip.

Những trường hợp không rõ ràng này có thể được xử lý bằng cách sử dụng điểm nhận dạng (nếu có). Chỉ thuật toán phân loại hình dạng mới cung cấp điểm số. Nếu mô hình này rất tự tin, thì điểm của kết quả hàng đầu sẽ cao hơn nhiều so với điểm số cao thứ hai. Nếu không chắc chắn, điểm số cho hai kết quả hàng đầu sẽ gần giống nhau. Ngoài ra, hãy lưu ý rằng các thuật toán phân loại hình dạng diễn giải toàn bộ Ink là một hình dạng duy nhất. Ví dụ: nếu Ink chứa một hình chữ nhật và một hình elip cạnh nhau, thì trình nhận dạng có thể trả về kết quả này (hoặc một đối tượng hoàn toàn khác), vì một đề xuất nhận dạng không thể đại diện cho hai hình dạng.