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

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

Deneyin

Başlamadan önce

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

    pod 'GoogleMLKit/DigitalInkRecognition', '7.0.0'
    
    
  2. Projenizin Pod'larını 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 nesnelerindeki metni tanımaya hazırsınız.

Ink nesnesi oluşturma

Ink nesnesi oluşturmanın en iyi yolu, dokunmatik ekranda çizmektir. iOS'te, Ink nesnesini oluşturmak için ekranda çizgiler çizen ve çizgilerin noktalarını depolayan dokunma etkinliği işleyicileri ile birlikte bir UIImageView kullanabilirsiniz. Bu genel kalıp aşağıdaki kod snippet'inde gösterilmektedir. Dokunma etkinliği işleme, ekran çizme ve vuruş verisi yönetimini birbirinden ayıran daha kapsamlı 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, UIImageView'e çizgi çizmek için bir örnek işlev bulunduğunu ve bu işlevin uygulamanız için gerektiği şekilde uyarlanması gerektiğini unutmayın. Sıfır uzunluktaki segmentlerin nokta olarak çizilmesi için çizgi segmentlerini çizerken yuvarlak uçlu kapaklar kullanmanızı öneririz (küçük harf i ü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 alma

Tanımayı 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ımlayıcı modelini indirip modeli RAM'e yüklememiz gerekir. Bu işlem, basitlik açısından viewDidLoad() yöntemine yerleştirilen ve kodlanmış bir dil adı kullanan aşağıdaki kod snippet'i kullanılarak yapılabilir. Kullanıcıya kullanılabilen dillerin listesini 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 tamamlanma bildirimlerini işleyerek hangi indirme işleminin başarılı olduğunu nasıl belirleyeceğinizi gösteren ek kod içerir.

Ink nesnesini tanıma

Ardından, basitlik açısından touchesEnded() olarak adlandırılan doRecognition() işlevine geliriz. Diğer uygulamalarda, tanıma işlemini yalnızca zaman aşımından sonra veya kullanıcı tanıma işlemini tetiklemek için bir düğmeye bastığında başlatmak 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 indirme işlemlerini yönetme

Tanımlama modelinin nasıl indirileceğini daha önce görmüştük. Aşağıdaki kod snippet'leri, bir modelin daha önce indirilip indirilmediğini nasıl kontrol edeceğinizi veya depolama alanını kurtarmak için artık gerekli olmayan bir modeli nasıl sileceğinizi 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ımanın doğruluğu farklı diller arasında 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 için eğitilmiş olsa da sonuçlar kullanıcıdan kullanıcıya değişiklik gösterebilir.

Metin tanımlayıcı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ı 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, "o" veya "c" harflerinin küçük veya büyük harf olması, virgül ile eğik çizgi arasındaki fark gibi.

Tanımlayıcıya yazım alanının genişliğini ve yüksekliğini söylemek doğruluğu artırabilir. Ancak tanımlayıcı, yazma alanının yalnızca tek bir satır metin 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ğiyle ilgili en iyi tahmininizin yüksekliğine sahip bir WritingArea ileterek daha iyi sonuçlar elde edebilirsiniz. Tanımlayıcıya ilettiğiniz WritingArea nesnesinin ekrandaki fiziksel yazma alanına tam olarak karşılık gelmesi gerekmez. WritingArea yüksekliğinin bu şekilde değiştirilmesi bazı dillerde diğerlerinden 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ştirir. Bu nedenle, önemli olan tek şey vuruşların göreceli boyutu ve konumudur. Koordinatları, sisteminiz için uygun olan ölçekte iletebilirsiniz.

Önceki bağlam

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

Örneğin, el yazısı "n" ve "u" harfleri genellikle birbirine karıştırılır. Kullanıcı "arg" kısmi kelimesini zaten girdiyse "ument" veya "nment" olarak algılanabilir vuruşlarla devam edebilir. "arg" ön bağlamını belirtmek, "argnment" yerine "argument" kelimesinin kullanılmasının daha olası olması nedeniyle belirsizliği ortadan kaldırır.

Önceki bağlam, tanımlayıcının kelime aralarını (kelimeler arasındaki boşlukları) belirlemesine de yardımcı olabilir. Boşluk karakterini yazabilirsiniz ancak çizemezsiniz. Bu nedenle, bir tanımlayıcı bir kelimenin ne zaman bittiğini ve bir sonrakinin ne zaman başladığını nasıl belirleyebilir? Kullanıcı önceden "merhaba" yazmışsa ve "dünya" kelimesini yazmaya devam ederse tanımlayıcı, ön bağlam olmadan "dünya" dizesini döndürür. Ancak "merhaba" ön bağlamını belirtirseniz model, "merhabadünya" "merhabasöz"den daha anlamlı olduğu için "dünya" dizesini başında boşluk olacak şekilde döndürür.

Boşluklar dahil en fazla 20 karakter uzunluğunda mümkün olan en uzun bağlam öncesi dizeyi sağlamanız gerekir. Dize daha uzunsa tanımlayı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ımlama doğruluğu, vuruşların sırasına duyarlıdır. Tanımlayıcılar, vuruşların insanların doğal olarak yazacağı sırada gerçekleşmesini bekler. Örneğin, İngilizce için soldan sağa. Bu kalıptan farklı olan her durum (ör. İngilizce bir cümleyi son kelimeden başlayarak yazmak) 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 gösterilebilir. Düzeltme muhtemelen bir cümlenin ortasındadır ancak düzeltmeyle ilgili vuruşların bulunduğu vuruş dizisi sona doğrudur. Bu durumda, yeni yazılan kelimeyi API'ye ayrı olarak göndermenizi ve sonucu kendi mantığınızı kullanarak önceki tanımlarla birleştirmenizi öneririz.

Muğlak şekillerle başa çıkma

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

Bu net olmayan durumlar, mevcut olduğunda tanıma puanları kullanılarak ele alınabilir. Yalnızca şekil sınıflandırıcıları puan sağlar. Model çok eminse en iyi sonucun puanı, ikinci en iyi sonuçtan çok daha iyi olur. Belirsizlik varsa ilk iki sonucun puanları birbirine yakın olur. Ayrıca, şekil sınıflandırıcıların Ink'ün tamamını tek bir şekil olarak yorumladığını unutmayın. Örneğin, Ink bir dikdörtgen ve yanına bir elips içeriyorsa tek bir tanıma adayı iki şekli temsil edemediği için tanımlayıcı sonuç olarak bunlardan birini (veya tamamen farklı bir şey) döndürebilir.