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 bản phác thảo.
Dùng thử
- Hãy dùng thử ứng dụng mẫu để xem ví dụ về cách sử dụng API này.
Trước khi bắt đầu
Đưa các thư viện sau đây của Bộ công cụ học máy vào Podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'Sau khi cài đặt hoặc cập nhật Pod của dự án, hãy mở dự án Xcode bằng
.xcworkspace. 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 đố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 cảm ứng
để vẽ các nét trên màn hình, đồng thời lưu trữ các điểm của nét để 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
khởi động nhanh để biết ví dụ
đầy đủ hơn, trong đó tách biệt quy trình xử lý sự kiện cảm ứng, vẽ trên màn hình,
và quản lý dữ liệu nét.
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]; }
Xin lưu ý rằng đoạn mã này có một hàm mẫu để vẽ nét vào
UIImageView,
Bạn nên điều chỉnh hàm này theo nhu cầu của ứng dụng. Bạn nên sử dụng roundcaps 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 mỗi nét được viết và sẽ được xác định bên dưới.
Nhận một thực thể của DigitalInkRecognizer
Để thực hiện quy trình nhận dạng, chúng ta cần truyền đối tượng Ink đến một
DigitalInkRecognizer thực thể. Để nhận thực thể DigitalInkRecognizer, trước tiên, chúng ta cần tải mô hình nhận dạng xuố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 đoạn mã sau. Để đơn giản, đoạn mã này được đặt trong phương thức viewDidLoad() và sử dụng tên ngôn ngữ được mã hoá cứng. Hãy xem ứng dụng
khởi động nhanh để biết
ví dụ về cách hiển thị danh sách ngôn ngữ hiện có 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]; }
Các ứng dụng khởi động nhanh có thêm mã cho biết cách xử lý nhiều lượt tải xuống cùng lúc và cách xác định lượt tải xuống nào thành công bằng cách xử lý thông báo hoàn tất.
Nhận dạng đối tượng Ink
Tiếp theo, chúng ta sẽ đến với hàm doRecognition(). Để đơn giản, hàm này được gọi từ touchesEnded(). Trong các ứng dụng khác, bạn có thể chỉ muốn gọi quy trình 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 quy trình 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ý lượt tải mô hình xuống
Chúng ta đã thấy cách tải 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 cách xoá một mô hình khi không còn cần nữa để khôi phục dung lượng bộ nhớ.
Kiểm tra xem một mô hình đã được tải xuống hay 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 quy trình nhận dạng văn bản
Độ chính xác của quy trình nhận dạng văn bản có thể khác 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ù tính năng Nhận dạng mực kỹ thuật số được huấn luyện để xử lý nhiều loại phong cách viết, nhưng kết quả có thể khác nhau giữa người dùng.
Dưới đâ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 bộ phân loại bản vẽ cho biểu tượng cảm xúc, tính năng tự động vẽ và hình dạng.
Khu vực viết
Nhiều ứng dụng có một khu vực viết được xác định rõ ràng để hoạt động đầu vào của người dùng. Ý nghĩa của một ký hiệu được xác định một phần theo kích thước của ký hiệu đó so với kích thước của khu vực viết chứa ký hiệu đó. Ví dụ: sự khác biệt giữa chữ "o" hoặc "c" viết thường hoặc viết hoa và dấu phẩy so với dấu gạch chéo.
Việc cho trình nhận dạng biết chiều rộng và chiều cao của khu vực viết có thể cải thiện độ chính xác. Tuy nhiên, trình nhận dạng giả định rằng khu vực viết chỉ chứa một dòng văn bản. Nếu khu vực viết thực tế đủ lớn để cho phép người dùng viết hai dòng trở lên, bạn có thể nhận được kết quả tốt hơn bằng cách truyền một WritingArea có chiều cao là ước tính tốt nhất của bạn về chiều cao của một dòng văn bản. Đối tượng WritingArea mà bạn truyền đến trình nhận dạng không nhất thiết phải tương ứng chính xác với khu vực viết thực tế trên màn hình. Việc thay đổi chiều cao của WritingArea theo cách này sẽ 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 khu vực viết, hãy chỉ định chiều rộng và chiều cao của khu vực đó 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 sẽ 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 các nét. Bạn có thể truyền toạ độ theo bất kỳ tỷ lệ nào phù hợp với hệ thống của mình.
Bối cảnh trước
Bối cảnh trước là văn bản 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 trình nhận dạng biết về bối cảnh trước.
Ví dụ: các chữ cái viết tay "n" và "u" thường bị nhầm lẫn với nhau. Nếu người dùng đã nhập từ "arg" (một phần của từ), họ có thể tiếp tục với các nét có thể được nhận dạng là "ument" hoặc "nment". Việc chỉ định bối cảnh trước là "arg" sẽ giải quyết sự mơ hồ, vì từ "argument" (lý lẽ) có khả năng xuất hiện cao hơn từ "argnment".
Bối cảnh trước cũng có thể giúp trình nhận dạng xác định dấu ngắt từ, khoảng cách giữa các từ. Bạn có thể nhập ký tự khoảng trắng nhưng không thể vẽ ký tự đó. Vậy làm cách nào để trình nhận dạng 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 "hello" và tiếp tục với từ "world" (viết tay), thì nếu không có bối cảnh trước, trình nhận dạng sẽ trả về chuỗi "world". Tuy nhiên, nếu bạn chỉ định bối cảnh trước là "hello", mô hình sẽ trả về chuỗi " world" (có khoảng trắng ở đầu), vì "hello world" (xin chào thế giới) có ý nghĩa hơn "helloword".
Bạn nên cung cấp chuỗi bối cảnh trước dài nhất có thể, 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 sẽ chỉ sử dụng 20 ký tự cuối cùng.
Mã mẫu bên dưới cho biết cách xác định khu vực viết và sử dụng đối tượng RecognitionContext để chỉ định bối cảnh trước.
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); }];
Sắp xếp thứ tự nét
Độ chính xác của quy trình nhận dạng phụ thuộc vào thứ tự của các nét. Trình nhận dạng mong đợi các nét xuất hiện theo thứ tự mà mọi người sẽ viết một cách tự nhiên; ví dụ: từ trái sang phải đối với tiếng Anh. Bất kỳ trường hợp nào 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, đều 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ể ở giữa câu, nhưng các nét cho bản sửa đổi lại ở cuối chuỗi 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 quy trình nhận dạng trước đó bằng logic của riêng bạn.
Xử lý các hình dạng mơ hồ
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 là mơ hồ. Ví dụ: một hình chữ nhật có các cạnh rất tròn có thể được coi là hình chữ nhật hoặc hình elip.
Bạn có thể xử lý những trường hợp không rõ ràng này bằng cách sử dụng điểm nhận dạng khi có. Chỉ bộ phân loại hình dạng mới cung cấp điểm. Nếu mô hình rất tự tin, thì điểm của kết quả hàng đầu sẽ tốt hơn nhiều so với kết quả tốt thứ hai. Nếu có sự không chắc chắn, thì điểm của 2 kết quả hàng đầu sẽ gần nhau. Ngoài ra, hãy lưu ý rằng bộ phân loại hình dạng sẽ diễn giải toàn bộ Ink dưới dạng 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ề hình này hoặc hình kia (hoặc một hình hoàn toàn khác) làm kết quả, vì một ứng viên nhận dạng duy nhất không thể đại diện cho 2 hình dạng.