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ış yüzlerce dilde metinleri tanıyabilir ve eskizleri sınıflandırabilirsiniz.

Deneyin

Başlamadan önce

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

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Projenizin Kapsüllerini yükledikten veya güncelledikten sonra .xcworkspace ile Xcode projenizi açın. Makine Öğrenimi Kiti, Xcode 13.2.1 veya üzeri sürümlerde desteklenir.

Artık Ink nesnedeki metinleri tanımaya başlamaya hazırsınız.

Ink nesnesi oluşturma

Ink nesnesi oluşturmanın ana yolu, nesneyi dokunmatik ekranda çizmektir. iOS'te, UIImageView öğesini, ekrandaki çizgileri çizen ve Ink nesnesini oluşturmak için çizgi noktalarını kaydeden dokunma etkinlik işleyicileriyle birlikte kullanabilirsiniz. Bu genel kalıp, aşağıdaki kod snippet'inde gösterilmiştir. Dokunmatik etkinlik 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'inin, çizgiyi UIImageView içine çizmek için örnek bir işlev içerdiğini ve bu işlevin, uygulamanız için gerektiği gibi uyarlanması gerektiğini unutmayın. Çizgi segmentlerini çizerken sıfır uzunluktaki segmentler nokta olarak çizilmesi için (küçük i harfindeki noktayı düşünebilirsiniz) yuvarlak büyük harf kullanmanızı öneririz. doRecognition() işlevi, her çizgi yazıldıktan sonra çağrılır ve aşağıda tanımlanacaktır.

DigitalInkRecognizer örneği alın

Tanıma işlemi gerçekleştirmek için Ink nesnesini bir DigitalInkRecognizer örneğine geçirmemiz gerekir. DigitalInkRecognizer örneğini elde etmek için önce istenen dile ait tanıyıcı modelini indirmemiz ve modeli RAM'e yüklememiz gerekir. Bu işlem, basitlik için viewDidLoad() yöntemine yerleştirilen ve sabit kodlu bir dil adı kullanan aşağıdaki kod snippet'i kullanılarak gerçekleştirilebilir. Kullanılabilir dillerin listesini kullanıcıya nasıl göstereceğinizi ve seçilen dili nasıl indireceğinizi gösteren örnek 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ı, birden çok indirme işleminin aynı anda nasıl işleneceğini ve tamamlanma bildirimleriyle hangi indirme işleminin başarılı olduğunun nasıl belirleneceğini gösteren ek kod içerir.

Ink nesnesini tanıma

Şimdi, basit olması için touchesEnded() adlı işlevden çağrılan doRecognition() işlevine geliyoruz. Diğer uygulamalarda kullanıcı, tanımayı yalnızca bir zaman aşımı sonrasında ya da kullanıcı, tanımayı tetiklemek için bir düğmeye bastığında çağırmak isteyebilirsiniz.

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 modelini nasıl indireceğimizi daha önce görmüştük. Aşağıdaki kod snippet'leri, bir modelin önceden indirilip indirilmediğinin nasıl kontrol edileceğini veya depolama alanında yer açmak için artık gerekli olmayan bir modelin nasıl silineceğini 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 iyileştirmeye yönelik ipuçları

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

Aşağıda metin tanıyıcının doğruluğunu iyileştirmeye yönelik bazı yöntemler verilmiştir. Bu tekniklerin emojiler, otomatik çizimler ve şekillere yönelik çizim sınıflandırıcıları için geçerli olmadığını unutmayın.

Yazma alanı

Birçok uygulamada kullanıcı girişleri için iyi tanımlanmış bir yazma alanı vardır. Simgenin anlamı, kısmen simgenin kendisini içeren yazı alanının boyutuna göre belirlenir. Örneğin, küçük veya büyük harf "o" veya "c", virgül ile öne eğik çizgi arasındaki fark.

Tanıyıcıya, yazma alanının genişliğini ve yüksekliğini söylemeniz doğruluğu artırabilir. Bununla birlikte, tanıyıcı, yazma alanının yalnızca tek bir metin satırı içerdiğini varsayar. Fiziksel yazı alanı, kullanıcının iki veya daha fazla satır yazmasına izin verecek kadar büyükse, tek bir satırlık metnin yüksekliğine ilişkin en iyi tahmininiz olan bir WriteArea ile bir yazı alanı geçirerek 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. Yazma Alanı 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 çizgi koordinatlarıyla aynı birim cinsinden belirtin. x,y koordinatı bağımsız değişkenlerinin birim gereksinimi yoktur. API tüm birimleri normalleştirdiğinden, önemli olan tek şey çizgilerin göreli boyutu ve konumudur. Sisteminiz için uygun olan ölçeklerde koordinatları aktarabilirsiniz.

Bağlam öncesi

Bağlam öncesi, tanımaya çalıştığınız Ink içindeki çizgilerden 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 çoğu zaman birbiriyle karıştırılır. Kullanıcı zaten "argüman" kısmi kelimesini girdiyse "ument" veya "nment" olarak tanınabilen çizgilerle devam edebilir. "bağımsız değişken" kelimesinin "bağımsız değişken"den daha olası olması nedeniyle, bağlam öncesi "bağımsız değişkenin" belirtilmesi belirsizliği giderir.

Bağlam ön bağlamı, tanıyıcının kelime boşluklarını, kelimeler arasındaki boşlukları tanımlamasına da yardımcı olabilir. Boşluk karakteri yazabilirsiniz, ancak bir karakter çizemezsiniz. Öyleyse, bir tanıyıcı bir kelimenin bitip bir sonrakinin ne zaman başlayacağını nasıl belirleyebilir? Kullanıcı zaten "merhaba" yazmışsa ve yazılı "dünya" kelimesiyle devam ediyorsa, tanıyıcı ön bağlam bilgisi olmadan "dünya" dizesini döndürür. Bununla birlikte, bağlam öncesi "merhaba" ifadesini belirtirseniz model, "merhaba" dizesini baştaki boşlukla birlikte "dünya" dizesini döndürür, çünkü "merhaba dünya", "merhaba"dan daha anlamlıdır.

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

Çizgi sıralaması

Tanıma doğruluğu, darbelerin sırası konusunda hassastır. Tanıyıcılar, çizgilerin insanların doğal olarak yazdıkları sırayla gerçekleşmesini beklerler; örneğin, İngilizce için soldan sağa. Bu kalıptan farklı olan herhangi bir kullanım (örneğin, son kelimeden başlayarak İngilizce bir cümle yazma) daha az doğru sonuçlar verir.

Başka bir örnek de Ink etiketinin 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üzeltmedeki çizgiler çizgi dizisinin 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 yuvarlatılmış kenarlara sahip bir dikdörtgen, dikdörtgen veya elips olarak görülebilir.

Bu belirsiz durumlar, mümkün olduğunda tanıma puanları kullanılarak ele alınabilir. Yalnızca şekil sınıflandırıcılar puan verir. Model çok güveniyorsa en iyi sonucun puanı, ikinci en iyi sonuca göre çok daha iyi olur. Belirsizlik varsa ilk iki sonucun puanları birbirine yakın olacaktır. 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, birbirine yan yana bir dikdörtgen ve elips içeriyorsa, tek bir tanıma adayı iki şekli temsil edemediğinden, tanıyıcı sonuç olarak bunlardan birini (veya tamamen farklı bir şeyi) döndürebilir.