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

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

Deneyin

Başlamadan önce

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

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

Artık Ink nesnelerindeki metni tanımaya başlayabilirsiniz.

Ink nesnesi oluşturma

Ink nesnesi oluşturmanın temel yolu, nesneyi dokunmatik ekranda çizmektir. iOS'te, ekranda fırça darbelerini çizen ve Ink nesnesini oluşturmak için fırça darbelerinin noktalarını da depolayan UIImageView ile birlikte dokunma etkinliği işleyicilerini kullanabilirsiniz. Bu genel kalıp, aşağıdaki kod snippet'inde gösterilmektedir. Dokunma etkinliği işleme, ekran çizme ve kontur verisi 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, konturu UIImageView'e çizmek için örnek bir işlev bulunduğunu ve bunun uygulamanız için gerektiği şekilde uyarlanması gerektiğini unutmayın. Çizgi parçalarını çizerken sıfır uzunluktaki parçaların nokta olarak çizilmesi için (küçük harf i'nin üzerindeki nokta gibi) yuvarlak uçlu çizim kullanmanızı öneririz. doRecognition() işlevi, her vuruş yazıldıktan sonra çağrılır ve aşağıda tanımlanır.

DigitalInkRecognizer örneği edinme

Tanıma işlemini gerçekleştirmek için Ink nesnesini bir DigitalInkRecognizer örneğine iletmemiz gerekir. DigitalInkRecognizer örneğini elde etmek için öncelikle istenen dilin tanıyıcı modelini indirip modeli RAM'e yüklememiz gerekir. Bu işlem, basitlik açısından viewDidLoad() yöntemine yerleştirilen ve sabit kodlanmış bir dil adı kullanan aşağıdaki kod snippet'i kullanılarak gerçekleştirilebilir. Kullanıcıya kullanılabilir dillerin listesini gösterme ve seçilen dili indirme örneği için hızlı başlangıç uygulamasına göz atı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 tamamlanma bildirimlerini işleyerek hangi indirme işleminin başarılı olduğunu belirlemeyi gösteren ek kodlar içerir.

Ink nesnesini tanıma

Ardından, basitlik için doRecognition() işlevi olarak adlandırılan touchesEnded() işlevine geçiyoruz. Diğer uygulamalarda, tanıma yalnızca zaman aşımından sonra veya kullanıcı tanımayı tetiklemek için bir düğmeye bastığında çağrılabilir.

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 modelinin nasıl indirileceğini daha önce görmüştük. Aşağıdaki kod snippet'leri, bir modelin daha önce indirilip indirilmediğini kontrol etme veya depolama alanını kurtarmak için artık gerekli olmayan bir modeli silme işlemlerinin nasıl yapılacağını gösterir.

Bir modelin daha önce 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 artırmaya yönelik ipuçları

Metin tanıma 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şlemek üzere 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 emoji, otomatik çizim ve şekil çizim sınıflandırıcıları için 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 onu içeren yazı alanının boyutuna göre belirlenir. Örneğin, küçük veya büyük harf "o" ya da "c" ile virgül ve eğik çizgi arasındaki fark.

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

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şkenlerinin birim gereksinimi yoktur. API tüm birimleri normalleştirdiğinden önemli olan tek şey vuruşların göreli boyutu ve konumudur. Koordinatları sisteminiz için anlamlı olan herhangi bir ölçekte iletebilirsiniz.

Ön bağlam

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

Örneğin, el yazısı "n" ve "u" harfleri genellikle birbirleriyle karıştırılır. Kullanıcı, "arg" kelimesinin bir kısmını zaten girmişse "ument" veya "nment" olarak tanınabilecek vuruşlarla devam edebilir. "arg" ön bağlamının belirtilmesi, "argument" kelimesi "argnment" kelimesinden daha olası olduğundan belirsizliği giderir.

Ön bağlam, tanıyıcının kelime sonlarını (kelimeler arasındaki boşluklar) belirlemesine de yardımcı olabilir. Boşluk karakteri yazabilirsiniz ancak boşluk çizemezsiniz. Peki bir tanıma aracı, bir kelimenin ne zaman bittiğini ve bir sonraki kelimenin ne zaman başladığını nasıl belirleyebilir? Kullanıcı zaten "hello" yazmışsa ve yazılı kelimeyle devam ediyorsa, ön bağlam olmadan tanıyıcı "world" dizesini döndürür. Ancak ön bağlam olarak "hello"yu belirtirseniz model, "hello world" ifadesi "helloword" ifadesinden daha anlamlı olduğundan başında boşluk bulunan " world" dizesini döndürür.

Boşluklar dahil olmak üzere 20 karaktere kadar olabilen en uzun ön bağlam dizesini sağlamalısınız. Dize daha uzunsa tanıyıcı yalnızca son 20 karakteri kullanır.

Aşağıdaki kod örneğinde, 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, çizgilerin doğal yazma sırasına göre (ör. İngilizce için soldan sağa) çizilmesini bekler. Bu kalıbın dışına çıkan durumlar (ör. son kelimeyle başlayan bir İngilizce cümle yazma) daha az doğru sonuçlar verir.

Bir Ink ortasındaki kelimenin kaldırılıp başka bir kelimeyle değiştirilmesi de buna örnek verilebilir. Düzeltme muhtemelen bir cümlenin ortasında yapılıyor ancak düzeltmeyle ilgili vuruşlar, vuruş dizisinin sonunda yer alıyor. 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 çalışma

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

Bu belirsiz durumlar, kullanılabilir olduğunda tanıma puanları kullanılarak ele alınabilir. Yalnızca şekil sınıflandırıcıları puan sağlar. Model çok güvenliyse en iyi sonucun puanı, ikinci en iyi sonucun puanından çok daha yüksek olur. Belirsizlik varsa ilk iki sonucun puanları birbirine yakın olur. Ayrıca, şekil sınıflandırıcıların Ink işaretinin tamamını tek bir şekil olarak yorumladığını unutmayın. Örneğin, Ink öğesi yan yana bir dikdörtgen ve elips içeriyorsa tek bir tanıma adayı iki şekli temsil edemeyeceğinden tanıyıcı sonuç olarak şekillerden birini veya tamamen farklı bir şekli döndürebilir.