iOS'te ML Kit ile dijital mürekkebi tanıma

ML Kit'in dijital mürekkep tanıma özelliği sayesinde, dijital bir yüzeyde el yazısıyla yazılmış metinleri yüzlerce dilde tanıyabilir ve çizimleri sınıflandırabilirsiniz.

Deneyin

Başlamadan önce

  1. Podfile'ınıza aşağıdaki ML Kiti kitaplıklarını ekleyin:

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. Projenizin Pod'larını yükledikten veya güncelledikten sonra .xcworkspace kullanarak Xcode projenizi açın. ML Kiti, Xcode 13.2.1 veya sonraki sürümlerde desteklenir.

Artık Ink nesnelerindeki metinleri tanımaya hazırsınız.

Ink nesnesi oluşturma

Ink nesnesi oluşturmanın ana yolu, nesneyi dokunmatik ekranda çizmektir. iOS'te, ekranda vuruşları çizen ve Ink nesnesini oluşturmak için vuruşların noktalarını da depolayan dokunma etkinliği işleyicileri ile birlikte UIImageView kullanabilirsiniz. Bu genel kalıp, aşağıdaki kod snippet'inde gösterilmektedir. Dokunma etkinliği işleme, ekran çizimi ve çizgi veri yönetimini ayıran daha eksiksiz bir örnek için hızlı başlangıç uygulamasına bakın.

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

Kod snippet'inde, vuruşu UIImageView'a çizmek için örnek bir işlev bulunduğunu ve bu işlevin uygulamanız için gerektiği şekilde uyarlanması gerektiğini unutmayın. Çizgi segmentlerini çizerken yuvarlak uçlar kullanmanızı öneririz. Böylece sıfır uzunluktaki segmentler nokta olarak çizilir (küçük i harfinin üzerindeki noktayı düşünün). doRecognition() işlevi, her vuruş yazıldıktan sonra çağrılır ve aşağıda tanımlanır.

DigitalInkRecognizer örneği alın

Tanıma işlemini gerçekleştirmek için Ink nesnesini bir DigitalInkRecognizer örneğine iletmemiz gerekir. DigitalInkRecognizer örneğini almak için önce istenen dil için tanıma modelini indirip RAM'e yüklememiz gerekir. Bu işlem, aşağıdaki kod snippet'i kullanılarak gerçekleştirilebilir. Basitlik açısından bu snippet, viewDidLoad() yöntemine yerleştirilmiştir ve sabit kodlu bir dil adı kullanır. Kullanılabilir dillerin listesini kullanıcıya gösterme ve seçilen dili indirme örneği için hızlı başlangıç uygulamasına bakı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];
}

Hızlı başlangıç uygulamaları, aynı anda birden fazla indirme işleminin nasıl yapılacağını ve tamamlama bildirimlerini işleyerek hangi indirme işleminin başarılı olduğunu nasıl belirleyeceğinizi gösteren ek kodlar içerir.

Ink nesnesini tanıma

Sırada doRecognition() işlevi var. Bu işlev, basitlik açısından touchesEnded() işlevinden çağrılır. Diğer uygulamalarda, tanıma işleminin yalnızca bir zaman aşımından sonra veya kullanıcı tanıma işlemini tetiklemek için bir düğmeye bastığında çağrılması istenebilir.

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

Model indirme işlemlerini yönetme

Tanıma modelini nasıl indireceğimizi daha önce görmüştük. Aşağıdaki kod snippet'leri, bir modelin indirilip indirilmediğini nasıl kontrol edeceğinizi veya depolama alanını kurtarmak için artık ihtiyaç duyulmayan bir modeli nasıl sileceğinizi gösterir.

Bir modelin indirilip indirilmediğini kontrol etme

Swift

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

Objective-C

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

İndirilen bir modeli silme

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

Metin tanıma doğruluğunu iyileştirmeyle ilgili ipuçları

Metin tanıma özelliğinin doğruluğu farklı dillerde değişiklik gösterebilir. Doğruluk, yazma stiline de bağlıdır. Dijital Mürekkep Tanıma, birçok yazı stilini işleyecek şekilde eğitilmiş olsa da sonuçlar kullanıcıdan kullanıcıya değişebilir.

Metin tanıma aracının doğruluğunu artırmanın bazı yolları aşağıda verilmiştir. Bu tekniklerin emojiler, otomatik çizim ve şekiller için çizim sınıflandırıcılarında geçerli olmadığını unutmayın.

Yazma alanı

Birçok uygulamada, kullanıcı girişi için iyi tanımlanmış bir yazma alanı bulunur. Bir sembolün anlamı, kısmen, içinde bulunduğu yazı alanının boyutuna göre belirlenir. Örneğin, küçük veya büyük harf "o" veya "c" arasındaki fark ve virgül ile eğik çizgi arasındaki fark.

Tanıyıcıya yazma alanının genişliğini ve yüksekliğini bildirmek doğruluğu artırabilir. Ancak tanıyıcı, yazma alanında yalnızca tek bir satır metin olduğunu varsayar. Fiziksel yazma alanı, kullanıcının iki veya daha fazla satır yazmasına izin verecek kadar büyükse WritingArea'yı tek bir metin satırının yüksekliğine dair en iyi tahmininiz olan bir yükseklikle ileterek daha iyi sonuçlar elde edebilirsiniz. Tanıyıcıya ilettiğiniz WritingArea nesnesinin, ekrandaki fiziksel yazma alanıyla tam olarak eşleşmesi gerekmez. WritingArea yüksekliğini bu şekilde değiştirmek, bazı dillerde diğerlerine göre daha iyi sonuç verir.

Yazma alanını belirtirken genişliğini ve yüksekliğini vuruş koordinatlarıyla aynı birimlerde belirtin. X,Y koordinat bağımsız değişkenleri için birim şartı yoktur. API tüm birimleri normalleştirdiğinden yalnızca vuruşların göreceli boyutu ve konumu önemlidir. Sisteminiz için anlamlı olan herhangi bir ölçekte koordinatları iletebilirsiniz.

Ön bağlam

Ön bağlam, tanımaya çalıştığınız Ink içindeki vuruşlardan hemen önce gelen metindir. Tanıyıcıya ön bağlam hakkında bilgi vererek yardımcı olabilirsiniz.

Örneğin, el yazısındaki "n" ve "u" harfleri genellikle birbiriyle karıştırılır. Kullanıcı "arg" kelime parçasını zaten girdiyse "ument" veya "nment" olarak tanınabilecek vuruşlarla devam edebilir. "arg" ön bağlamını belirtmek, "argüman" kelimesinin "argümen" kelimesinden daha olası olması nedeniyle belirsizliği giderir.

Ön bağlam, tanıma aracının kelime sonlarını (kelimeler arasındaki boşlukları) tanımlamasına da yardımcı olabilir. Boşluk karakterini yazabilirsiniz ancak çizemezsiniz. Bu durumda, tanıyıcı bir kelimenin nerede bitip diğerinin nerede başladığını nasıl belirleyebilir? Kullanıcı "merhaba" yazdıysa ve "dünya" kelimesiyle devam ederse tanıyıcı, ön bağlam olmadan "dünya" dizesini döndürür. Ancak ön bağlam olarak "merhaba" derseniz model, "merhaba dünya" ifadesi "merhabadünya" ifadesinden daha anlamlı olduğu için başında boşluk olan " dünya" dizesini döndürür.

Boşluklar dahil olmak üzere en fazla 20 karakterden oluşan, mümkün olan en uzun ön bağlam dizesini sağlamanız gerekir. Dize daha uzunsa tanıyıcı yalnızca son 20 karakteri kullanır.

Aşağıdaki kod örneğinde, bir yazma alanının nasıl tanımlanacağı ve ön bağlamı belirtmek için RecognitionContext nesnesinin nasıl kullanılacağı gösterilmektedir.

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

Vuruş sırası

Tanıma doğruluğu, vuruşların sırasına duyarlıdır. Tanıyıcılar, vuruşların kullanıcıların doğal olarak yazacağı sırada (ör. İngilizce için soldan sağa) gerçekleşmesini bekler. İngilizce bir cümleyi son kelimesinden başlayarak yazmak gibi bu kalıbın dışına çıkan durumlarda daha az doğru sonuçlar elde edilir.

Başka bir örnek de Ink ortasındaki bir kelimenin kaldırılıp başka bir kelimeyle değiştirilmesidir. Düzeltme muhtemelen bir cümlenin ortasındadır ancak düzeltme için yapılan vuruşlar, vuruş sırasının sonundadır. Bu durumda, yeni yazılan kelimeyi API'ye ayrı olarak göndermenizi ve sonucu kendi mantığınızı kullanarak önceki tanımalarla birleştirmenizi öneririz.

Belirsiz şekillerle başa çıkma

Tanıyıcıya sağlanan şeklin anlamının belirsiz olduğu durumlar vardır. Örneğin, çok yuvarlak kenarlı bir dikdörtgen, dikdörtgen veya elips olarak görülebilir.

Bu belirsiz durumlar, mevcut olduğunda tanıma puanları kullanılarak ele alınabilir. Yalnızca şekil sınıflandırıcılar puan sağlar. Modelin güven düzeyi çok yüksekse en iyi sonucun puanı, ikinci en iyi sonucun puanından çok daha yüksek olur. Belirsizlik varsa en iyi iki sonucun puanları birbirine yakın olur. Ayrıca, şekil sınıflandırıcıların tüm Ink öğesini tek bir şekil olarak yorumladığını unutmayın. Örneğin, Ink yan yana bir dikdörtgen ve bir elips içeriyorsa tanıyıcı, sonuç olarak bunlardan birini (veya tamamen farklı bir şeyi) döndürebilir. Çünkü tek bir tanıma adayı iki şekli temsil edemez.