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 nền tảng 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ử
- 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
Thêm các thư viện Bộ công cụ học máy sau đây vào Podfile của bạn:
pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
Sau khi bạn cài đặt hoặc cập nhật Nhóm của dự án, hãy mở dự án Xcode của bạn bằng cách sử dụng
.xcworkspace
. Bộ công cụ học máy được hỗ trợ trong phiên bản Xcode 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
sự kiện chạm
trình xử lý
Thao tác này sẽ vẽ các nét trên màn hình, đồng thời lưu trữ các nét đó điểm để xây dựng
đối tượng Ink
. Mẫu chung này được minh hoạ trong mã sau
đoạn trích. Xem phần bắt đầu nhanh
cho một
ví dụ hoàn chỉnh hơn, tách biệt việc xử lý sự kiện chạm, vẽ màn hình,
và quản lý dữ liệu đột quỵ.
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ã chứa một hàm mẫu để vẽ nét vẽ vào
UIImageView,
phải được điều chỉnh nếu cần cho ứng dụng của bạn. Bạn nên dùng
hình tròn khi vẽ các đoạn thẳng để các đoạn có độ dài bằng 0
được vẽ dưới dạng dấu chấm (hãy nghĩ đến dấu chấm trên một chữ cái i viết thường). doRecognition()
được gọi sau mỗi nét được viết và sẽ được định nghĩa ở bên dưới.
Nhận một thực thể của DigitalInkRecognizer
Để nhận dạng, chúng ta cần truyền đối tượng Ink
đến một
Thực thể DigitalInkRecognizer
. Để lấy thực thể DigitalInkRecognizer
,
trước tiên, chúng tôi cần tải xuống mô hì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 điều này bằng cách sử dụng đoạn mã sau
đoạn mã này, để đơn giản hoá được đặt trong phương thức viewDidLoad()
và sử dụng một
tên ngôn ngữ được cố định giá trị trong mã. Xem phần bắt đầu nhanh
ứng dụng cho một
ví dụ về cách hiển thị danh sách ngôn ngữ có sẵn cho người dùng và tải xuống
ngôn ngữ đã chọn.
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 khởi động nhanh có đoạn mã bổ sung cho biết cách xử lý nhiều tình huống lượt tải xuống nào cùng một 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ý các thông báo hoàn thành.
Nhận dạng đối tượng Ink
Tiếp theo, chúng ta sẽ tìm hiểu hàm doRecognition()
, để đơn giản hoá tên là
từ touchesEnded()
. Trong các ứng dụng khác, người dùng có thể muốn gọi
chỉ nhận dạng sau khi hết thời gian chờ hoặc khi người dùng nhấn nút để kích hoạt
nhận dạng cá nhân.
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 đã biết cách tải một mô hình nhận dạng xuống. Mã sau đây các đoạn mã 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 khôi phục dung lượng lưu trữ nữa.
Kiểm tra xem 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 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ác nhau giữa các ngôn ngữ. Độ chính xác cũng phụ thuộc về 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, kết quả có thể khác nhau tuỳ theo 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. Lưu ý rằng những kỹ thuật này không áp dụng cho bộ phân loại bản vẽ đối với biểu tượng cảm xúc, tính năng vẽ tự động và hình dạng.
Khu vực viết
Nhiều ứng dụng có vùng viết được xác định rõ ràng cho thao tác nhập của người dùng. Ý nghĩa của ký hiệu là được xác định một phần bởi kích thước tương ứng với kích thước của vùng viết có chứa ký tự đó. Ví dụ: sự khác biệt giữa chữ "o" viết thường hoặc viết hoa 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ể 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. Nếu khách hàng thực tế diện tích viết đủ lớn để cho phép người dùng viết hai hoặc nhiều dòng, bạn có thể cải thiện hơn kết quả bằng cách chuyển vào một WriteArea có chiều cao là số liệu ước tính chính xác nhất về chiều cao của một dòng văn bản. Đối tượng WriteArea 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 viết thực trên màn hình. Thay đổi chiều cao của WriteArea theo cách này hoạt động tốt 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 viết đó bằng cùng đơn vị với nét vẽ toạ độ. Đối số toạ độ x,y không có yêu cầu về đơn vị – API chuẩn hoá tất cả đơn vị, do đó, đ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 tuỳ ý chuyển toạ độ theo bất kỳ tỷ lệ nào phù hợp với hệ thống của bạn.
Bối cảnh trước
Bối cảnh trước là văn bản đứng ngay trước nét gạch trong Ink
mà bạn
đang cố nhận ra. Bạn có thể giúp trình nhận dạng bằng cách nói về bối cảnh trước.
Ví dụ: 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 có đã nhập một phần từ "arg", chúng có thể tiếp tục với các nét chữ có thể được nhận ra là "ument" hoặc "nment". Chỉ định "arg" trước ngữ cảnh giải quyết sự không rõ ràng, vì từ "đối số" có nhiều khả năng hơn là "argnation".
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ừ, dấu cách giữa các từ. Bạn có thể nhập một ký tự dấu cách nhưng bạn không thể vẽ một ký tự, vậy làm thế nào một trình nhận dạng có thể xác định thời điểm một từ kết thúc và câu hỏi 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ừ được viết "world", không có ngữ cảnh trước trình nhận dạng trả về chuỗi "world". Tuy nhiên, nếu bạn chỉ định "hello", mô hình sẽ trả về chuỗi " thế giới", với một khoảng trống dẫn đầu, vì "xin chào" thế giới" sẽ có ý nghĩa hơn từ "helloword".
Bạn nên cung cấp chuỗi ngữ cảnh trước dài nhất có thể, tối đa 20 ký tự, bao gồm không gian. Nếu chuỗi dài hơn thì trình nhận dạng chỉ sử dụng 20 ký tự cuối cùng.
Mã mẫu dưới đây cho biết cách xác định vùng viết và sử dụng
RecognitionContext
để chỉ định ngữ 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); }];
Thứ tự nét vẽ
Độ chính xác của tính năng nhận dạng phụ thuộc vào thứ tự nét vẽ. Thiết bị nhận dạng mong muốn nét diễn ra theo thứ tự mà mọi người thường viết; ví dụ từ trái sang phải cho tiếng Anh. Mọi trường hợp giống như 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 ra kết quả ít chính xác hơn.
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 của bản sửa đổi
ở cuối chuỗi nét vẽ.
Trong trường hợp này, bạn nên gửi riêng từ mới viết tới API và hợp nhất
kết quả nhờ những nhận dạng trước đó
bằng cách sử dụng logic của riêng bạn.
Xử lý những 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à không rõ ràng. Cho ví dụ: một hình chữ nhật với các cạnh rất tròn có thể được xem 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 số nhận dạng (nếu có). Chỉ
thuật toán phân loại hình dạng sẽ đưa ra điểm số. Nếu mô hình rất tự tin, điểm số của kết quả hàng đầu sẽ là
tốt hơn nhiều so với ứng dụng tốt thứ hai. Nếu không chắc chắn, điểm số của hai kết quả hàng đầu sẽ
ở gần nhau. Ngoài ra, xin lưu ý rằng các thuật toán phân loại hình dạng sẽ diễn giải toàn bộ Ink
dưới dạng một
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 bên cạnh mỗi
khác, trình nhận dạng có thể trả về một trong hai thuộc tính này (hoặc một nội dung nào đó hoàn toàn khác) dưới dạng một
do một ứng viên công nhận duy nhất không thể biểu thị hai hình dạng.