Mengenali tinta digital dengan ML Kit di iOS

Dengan pengenalan tinta digital ML Kit, Anda dapat mengenali teks tulisan tangan pada platform digital dalam ratusan bahasa, serta mengklasifikasikan sketsa.

Cobalah

Sebelum memulai

  1. Sertakan library ML Kit berikut di Podfile Anda:

    pod 'GoogleMLKit/DigitalInkRecognition', '7.0.0'
    
    
  2. Setelah Anda menginstal atau mengupdate Pod project, buka project Xcode menggunakan .xcworkspace-nya. ML Kit didukung di Xcode versi 13.2.1 atau yang lebih baru.

Sekarang Anda siap untuk mulai mengenali teks dalam objek Ink.

Membuat objek Ink

Cara utama untuk membuat objek Ink adalah menggambarnya di layar sentuh. Di iOS, Anda dapat menggunakan UIImageView bersama dengan pengelola peristiwa sentuh yang menggambar goresan di layar dan juga menyimpan titik goresan untuk membuat objek Ink. Pola umum ini ditunjukkan dalam cuplikan kode berikut. Lihat aplikasi panduan memulai untuk contoh yang lebih lengkap, yang memisahkan penanganan peristiwa sentuh, gambar layar, dan pengelolaan data goresan.

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

Perhatikan bahwa cuplikan kode menyertakan fungsi contoh untuk menggambar goresan ke dalam UIImageView, yang harus disesuaikan sesuai kebutuhan untuk aplikasi Anda. Sebaiknya gunakan roundcaps saat menggambar segmen garis sehingga segmen dengan panjang nol akan digambar sebagai titik (bayangkan titik pada huruf i kecil). Fungsi doRecognition() dipanggil setelah setiap goresan ditulis dan akan ditentukan di bawah.

Mendapatkan instance DigitalInkRecognizer

Untuk melakukan pengenalan, kita harus meneruskan objek Ink ke instance DigitalInkRecognizer. Untuk mendapatkan instance DigitalInkRecognizer, kita harus mendownload model pengenal untuk bahasa yang diinginkan terlebih dahulu, dan memuat model ke RAM. Hal ini dapat dilakukan menggunakan cuplikan kode berikut, yang untuk memudahkan ditempatkan dalam metode viewDidLoad() dan menggunakan nama bahasa yang di-hardcode. Lihat aplikasi memulai untuk mengetahui contoh cara menampilkan daftar bahasa yang tersedia kepada pengguna dan mendownload bahasa yang dipilih.

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

Aplikasi memulai menyertakan kode tambahan yang menunjukkan cara menangani beberapa download secara bersamaan, dan cara menentukan download mana yang berhasil dengan menangani notifikasi penyelesaian.

Mengenali objek Ink

Selanjutnya, kita akan membahas fungsi doRecognition(), yang untuk memudahkannya dipanggil dari touchesEnded(). Dalam aplikasi lain, Anda mungkin ingin memanggil pengenalan hanya setelah waktu tunggu habis, atau saat pengguna menekan tombol untuk memicu pengenalan.

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

Mengelola download model

Kita telah melihat cara mendownload model pengenalan. Cuplikan kode berikut menggambarkan cara memeriksa apakah model telah didownload, atau menghapus model jika tidak lagi diperlukan untuk memulihkan ruang penyimpanan.

Memeriksa apakah model telah didownload

Swift

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

Objective-C

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

Menghapus model yang didownload

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

Tips untuk meningkatkan akurasi pengenalan teks

Akurasi pengenalan teks dapat bervariasi di berbagai bahasa. Akurasi juga bergantung pada gaya penulisan. Meskipun Pemindaian Tinta Digital dilatih untuk menangani berbagai gaya tulisan, hasilnya dapat bervariasi dari pengguna ke pengguna.

Berikut beberapa cara untuk meningkatkan akurasi pengenal teks. Perhatikan bahwa teknik ini tidak berlaku untuk pengklasifikasi gambar untuk emoji, autodraw, dan bentuk.

Area penulisan

Banyak aplikasi memiliki area penulisan yang ditentukan dengan baik untuk input pengguna. Makna simbol sebagian ditentukan oleh ukurannya relatif terhadap ukuran area penulisan yang berisinya. Misalnya, perbedaan antara huruf kecil atau besar "o" atau "c", dan koma versus garis miring.

Memberi tahu pengenal lebar dan tinggi area penulisan dapat meningkatkan akurasi. Namun, pengenal mengasumsikan bahwa area penulisan hanya berisi satu baris teks. Jika area penulisan fisik cukup besar untuk memungkinkan pengguna menulis dua baris atau lebih, Anda mungkin mendapatkan hasil yang lebih baik dengan meneruskan WritingArea dengan tinggi yang merupakan estimasi terbaik Anda dari tinggi satu baris teks. Objek WritingArea yang Anda teruskan ke pengenal tidak harus sama persis dengan area penulisan fisik di layar. Mengubah tinggi WritingArea dengan cara ini berfungsi lebih baik dalam beberapa bahasa daripada bahasa lainnya.

Saat Anda menentukan area penulisan, tentukan lebar dan tingginya dalam satuan yang sama dengan koordinat goresan. Argumen koordinat x,y tidak memiliki persyaratan unit - API menormalisasi semua unit, sehingga satu-satunya hal yang penting adalah ukuran dan posisi goresan relatif. Anda bebas meneruskan koordinat dalam skala apa pun yang sesuai untuk sistem Anda.

Pra-konteks

Pra-konteks adalah teks yang langsung mendahului goresan dalam Ink yang Anda coba kenali. Anda dapat membantu pengenal dengan memberi tahu tentang pra-konteks.

Misalnya, huruf kursif "n" dan "u" sering kali disalahartikan satu sama lain. Jika pengguna telah memasukkan kata parsial "arg", mereka dapat melanjutkan dengan goresan yang dapat dikenali sebagai "ument" atau "nment". Menentukan pra-konteks "arg" akan menyelesaikan ambiguitas, karena kata "argument" lebih mungkin daripada "argnment".

Pra-konteks juga dapat membantu pengenal mengidentifikasi jeda kata, spasi di antara kata. Anda dapat mengetik karakter spasi, tetapi tidak dapat menggambarnya. Jadi, bagaimana pengenal dapat menentukan kapan satu kata berakhir dan kata berikutnya dimulai? Jika pengguna telah menulis "halo" dan melanjutkan dengan kata tertulis "dunia", tanpa pra-konteks, pengenal akan menampilkan string "dunia". Namun, jika Anda menentukan pre-konteks "hello", model akan menampilkan string "world", dengan spasi di awal, karena "hello world" lebih masuk akal daripada "helloword".

Anda harus memberikan string pra-konteks yang paling panjang, hingga 20 karakter, termasuk spasi. Jika string lebih panjang, pengenal hanya menggunakan 20 karakter terakhir.

Contoh kode di bawah menunjukkan cara menentukan area penulisan dan menggunakan objek RecognitionContext untuk menentukan pra-konteks.

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

Urutan goresan

Akurasi pengenalan sensitif terhadap urutan goresan. Pengenal mengharapkan goresan terjadi dalam urutan yang biasa ditulis orang; misalnya, kiri ke kanan untuk bahasa Inggris. Setiap kasus yang menyimpang dari pola ini, seperti menulis kalimat bahasa Inggris yang dimulai dengan kata terakhir, akan memberikan hasil yang kurang akurat.

Contoh lainnya adalah saat kata di tengah Ink dihapus dan diganti dengan kata lain. Revisi mungkin berada di tengah kalimat, tetapi goresan untuk revisi berada di akhir urutan goresan. Dalam hal ini, sebaiknya kirim kata yang baru ditulis secara terpisah ke API dan gabungkan hasilnya dengan pengenalan sebelumnya menggunakan logika Anda sendiri.

Menangani bentuk ambigu

Ada kalanya makna bentuk yang diberikan ke pengenal bersifat ambigu. Misalnya, persegi panjang dengan tepi yang sangat membulat dapat dilihat sebagai persegi panjang atau elips.

Kasus yang tidak jelas ini dapat ditangani dengan menggunakan skor pengenalan jika tersedia. Hanya pengklasifikasi bentuk yang memberikan skor. Jika model sangat yakin, skor hasil teratas akan jauh lebih baik daripada hasil terbaik kedua. Jika ada ketidakpastian, skor untuk dua hasil teratas akan dekat. Selain itu, perlu diingat bahwa pengklasifikasi bentuk menafsirkan seluruh Ink sebagai satu bentuk. Misalnya, jika Ink berisi persegi panjang dan elips di samping satu sama lain, pengenal dapat menampilkan salah satu dari keduanya (atau sesuatu yang sama sekali berbeda) sebagai hasil, karena satu kandidat pengenalan tidak dapat mewakili dua bentuk.