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

Makine Öğrenimi Kiti'nin dijital mürekkep tanıma özelliğiyle yüzlerce dilde dijital yüzeylerde elle yazılmış metinleri tanıyabilir ve çizimleri sınıflandırabilirsiniz.

Deneyin

Başlamadan önce

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

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Projenizin kapsüllerini yükledikten veya güncelledikten sonra .xcworkspace kullanarak Xcode projenizi açın. ML Kit, Xcode 13.2.1 veya sonraki sürümlerde desteklenir.

Artık Ink nesnedeki metinleri tanımaya başlamak için hazırsınız.

Ink nesnesi oluşturun

Bir Ink nesnesi oluşturmanın ana yolu, onu dokunmatik ekranda çizmektir. iOS'ta, Ink nesnesini oluşturmak için ekrandaki darbeleri çizen ve aynı zamanda fırça noktalarını depolayan dokunma etkinliği işleyicileri ile birlikte bir UIImageView öğesi kullanabilirsiniz. Bu genel kalıp, aşağıdaki kod snippet'inde gösterilmektedir. Dokunma etkinliği işleme, ekran çizimi ve fırça veri yönetimini ayıran daha eksiksiz bir örnek için hızlı başlangıç uygulamasına göz atı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'inin, fırçayı UIImageView öğesine çizmek için örnek bir işlev içerdiğini unutmayın. Bu işlev, uygulamanız için gereken şekilde uyarlanmalıdır. Sıfır uzunluktaki segmentler nokta olarak çizilecek şekilde çizgi segmentlerini çizerken yuvarlak başlık kullanmanızı öneririz (küçük i harfinin üzerindeki noktayı düşünün). doRecognition() işlevi, her çizgi yazıldıktan sonra çağrılır ve aşağıda tanımlanacaktır.

DigitalInkRecognizer örneği al

Tanıma işlemi gerçekleştirmek için Ink nesnesini bir DigitalInkRecognizer örneğine iletmemiz gerekir. DigitalInkRecognizer örneğini elde etmek için öncelikle istenen dile ait tanıyıcı modelini indirip modeli RAM'e yüklememiz gerekir. Bu işlem, kolaylık sağlamak için viewDidLoad() yöntemine yerleştirilmiş ve sabit kodlu bir dil adı kullanan aşağıdaki kod snippet'i kullanılarak gerçekleştirilebilir. Kullanıcıya sunulan dillerin listesini nasıl göstereceğinize ve seçilen dili nasıl indireceğinize dair örnek 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 indirmenin nasıl işleneceğini ve tamamlama bildirimleri işlenerek hangi indirme işleminin başarılı olduğunun nasıl belirleneceğini gösteren ek kod içerir.

Ink nesnesini tanı

Şimdi, basitlik için touchesEnded()'dan çağrılan doRecognition() işlevine geliyoruz. Diğer uygulamalarda, bir kullanıcı tanımayı yalnızca bir zaman aşımından sonra veya kullanıcı, tanımayı tetiklemek için bir düğmeye bastığında çağırmak isteyebilir.

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 indirmelerini yönetme

Tanıma modelinin nasıl indirileceğini zaten görmüştük. Aşağıdaki kod snippet'leri, bir modelin önceden 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 önceden 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ştirmeye yönelik ipuçları

Metin tanımanın doğruluğu diller arasında farklılık gösterebilir. Doğruluk yazı stiline de bağlıdır. Dijital Mürekkep Tanıma, pek çok yazma stilini işleyecek şekilde eğitilmiş olsa da sonuçlar kullanıcıdan kullanıcıya değişiklik gösterebilir.

Metin tanıyıcının doğruluğunu artırmanın bazı yolları aşağıda verilmiştir. Bu tekniklerin emoji, otomatik çizim ve şekillere yönelik çizim sınıflandırıcıları için geçerli olmadığını unutmayın.

Yazma alanı

Birçok uygulamanın, kullanıcı girişi için iyi tanımlanmış bir yazma alanı vardır. Bir simgenin anlamı, kısmen simgelerin bulunduğu yazı alanının boyutuna göre belirlenir. Örneğin, küçük veya büyük harf "o" veya "c" ile virgül ile düz 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ının yalnızca tek bir metin satırı içerdiğini varsayar. Fiziksel yazma 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ğine dair en iyi tahmininiz olan bir yükseklikle bir ComposeArea'dan geçerek daha iyi sonuçlar alabilirsiniz. Tanıyıcıya ilettiğiniz WriteArea nesnesinin, ekrandaki fiziksel yazma alanına tam olarak karşılık gelmesi gerekmez. WriteArea 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 fırça koordinatlarıyla aynı birimlerde belirtin. x,y koordinat bağımsız değişkenlerinin birim gereksinimi yoktur. API tüm birimleri normalleştirdiğinden, tek önemli nokta çizgilerin göreli boyutu ve konumudur. Sisteminiz için uygun olan ölçekte koordinatlar verebilirsiniz.

Bağlam öncesi

Bağlam ön bilgisi, Ink içinde tanımaya çalıştığınız darbelerden hemen önce gelen metindir. Tanıyıcıya bağlam öncesi hakkında bilgi vererek yardımcı olabilirsiniz.

Örneğin, el yazısı harfleri "n" ve "u" genellikle birbiriyle karıştırılır. Kullanıcı kısmi olarak "arg" kelimesini girmişse "ument" veya "nment" olarak algılanabilen darbelerle devam edebilir. Bağlam öncesi "bağımsız değişken" kelimesinin belirtilmesi, "bağımsız değişken" kelimesinin "bağımsız değişken" kelimesinden daha olası olduğu için belirsizliği ortadan kaldırır.

Bağlam ön bilgisi, tanıyıcının kelime sonlarını, yani kelimeler arasındaki boşlukları belirlemesine de yardımcı olabilir. Boşluk karakteri yazabilirsiniz ama bir karakter çizemezsiniz. Öyleyse tanıyıcı bir kelimenin bitip bir sonrakinin başladığını nasıl belirleyebilir? Kullanıcı zaten "merhaba" yazdıysa ve "dünya" kelimesiyle devam ediyorsa bağlam öncesi "dünya" dizesini döndürür. Ancak bağlam öncesi "hello" ifadesini belirtirseniz "helloworld", "helloword"den daha anlamlı olduğu için model, başında boşlukla birlikte "world" dizesini döndürür.

Boşluklar dahil olmak üzere en fazla 20 karakterden oluşan mümkün olan en uzun bağlam öncesi 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 bağlam öncesi belirtmek için bir 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);
                         }];

Çizgi sıralama

Tanıma doğruluğu kulaçların sırasına göre hassastır. Tanımlayıcılar, darbelerin insanların doğal yazdıkları sırayla gerçekleşmesini bekler. Örneğin, İngilizce için soldan sağa doğru yazılırlar. Bu kalıptan farklı olan herhangi bir durumda (örneğin, son kelimeyle başlayan İngilizce bir cümle yazmak) daha az doğru sonuçlar verir.

Başka bir örnek de Ink kelimesinin 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 darbeleri çizgi dizisinin sonundadır. Bu durumda, yeni yazılan kelimeyi API'ye ayrı olarak göndermenizi ve kendi mantığınızı kullanarak sonucu ö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, kenarları çok yuvarlanmış bir dikdörtgen, dikdörtgen veya elips olarak görülebilir.

Net olmayan bu destek kayıtları, mevcut olduğunda tanıma puanları kullanılarak ele alınabilir. Yalnızca şekil sınıflandırıcılar puan verir. Model çok güvenliyse en yüksek sonucun puanı, ikinci en iyiden çok daha iyi olur. Belirsizlik varsa ilk iki sonucun puanları yakın olur. Ayrıca, şekil sınıflandırıcılarının tüm Ink işaretini tek bir şekil olarak yorumladığını unutmayın. Örneğin, Ink öğesinde bir dikdörtgen ve birbirine bitişik bir elips varsa tek bir tanıma adayı iki şekli temsil edemediğinden sonuç olarak tanıyıcı ikisinden birini (ya da tamamen farklı bir şeyi) döndürebilir.