Android'de 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. Proje düzeyindeki build.gradle dosyanızda, Google'ın Maven deposunu hem buildscript hem de allprojects bölümüne eklediğinizden emin olun.
  2. ML Kit Android kitaplıklarına ait bağımlılıkları, modülünüzün uygulama düzeyindeki Gradle dosyasına (genellikle app/build.gradle) ekleyin:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
}

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

Ink nesnesi oluşturma

Ink nesnesi oluşturmanın temel yolu, nesneyi dokunmatik ekranda çizmektir. Android'de bu amaçla Canvas'ı kullanabilirsiniz. Dokunma etkinliği işleyicileriniz, kullanıcının çizdiği vuruşlardaki noktaları Ink nesnesinde depolamak için aşağıdaki kod snippet'inde gösterilen addNewTouchEvent() yöntemini çağırmalıdır.

Bu genel kalıp, aşağıdaki kod snippet'inde gösterilmektedir. Daha kapsamlı bir örnek için ML Kit hızlı başlangıç örneğine bakın.

Kotlin

var inkBuilder = Ink.builder()
lateinit var strokeBuilder: Ink.Stroke.Builder

// Call this each time there is a new event.
fun addNewTouchEvent(event: MotionEvent) {
  val action = event.actionMasked
  val x = event.x
  val y = event.y
  var t = System.currentTimeMillis()

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  when (action) {
    MotionEvent.ACTION_DOWN -> {
      strokeBuilder = Ink.Stroke.builder()
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
    }
    MotionEvent.ACTION_MOVE -> strokeBuilder!!.addPoint(Ink.Point.create(x, y, t))
    MotionEvent.ACTION_UP -> {
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
      inkBuilder.addStroke(strokeBuilder.build())
    }
    else -> {
      // Action not relevant for ink construction
    }
  }
}

...

// This is what to send to the recognizer.
val ink = inkBuilder.build()

Java

Ink.Builder inkBuilder = Ink.builder();
Ink.Stroke.Builder strokeBuilder;

// Call this each time there is a new event.
public void addNewTouchEvent(MotionEvent event) {
  float x = event.getX();
  float y = event.getY();
  long t = System.currentTimeMillis();

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  int action = event.getActionMasked();
  switch (action) {
    case MotionEvent.ACTION_DOWN:
      strokeBuilder = Ink.Stroke.builder();
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_MOVE:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_UP:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      inkBuilder.addStroke(strokeBuilder.build());
      strokeBuilder = null;
      break;
  }
}

...

// This is what to send to the recognizer.
Ink ink = inkBuilder.build();

DigitalInkRecognizer örneği alma

Tanıma işlemi yapmak için Ink örneğini bir DigitalInkRecognizer nesnesine gönderin. Aşağıdaki kodda, BCP-47 etiketinden böyle bir tanıyıcıyı nasıl oluşturacağınız gösterilmektedir.

Kotlin

// Specify the recognition model for a language
var modelIdentifier: DigitalInkRecognitionModelIdentifier
try {
  modelIdentifier = DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US")
} catch (e: MlKitException) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}
var model: DigitalInkRecognitionModel =
    DigitalInkRecognitionModel.builder(modelIdentifier).build()


// Get a recognizer for the language
var recognizer: DigitalInkRecognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build())

Java

// Specify the recognition model for a language
DigitalInkRecognitionModelIdentifier modelIdentifier;
try {
  modelIdentifier =
    DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US");
} catch (MlKitException e) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}

DigitalInkRecognitionModel model =
    DigitalInkRecognitionModel.builder(modelIdentifier).build();

// Get a recognizer for the language
DigitalInkRecognizer recognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build());

Ink nesnesini işleme

Kotlin

recognizer.recognize(ink)
    .addOnSuccessListener { result: RecognitionResult ->
      // `result` contains the recognizer's answers as a RecognitionResult.
      // Logs the text from the top candidate.
      Log.i(TAG, result.candidates[0].text)
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error during recognition: $e")
    }

Java

recognizer.recognize(ink)
    .addOnSuccessListener(
        // `result` contains the recognizer's answers as a RecognitionResult.
        // Logs the text from the top candidate.
        result -> Log.i(TAG, result.getCandidates().get(0).getText()))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error during recognition: " + e));

Yukarıdaki örnek kodda, tanıma modelinin bir sonraki bölümde açıklandığı gibi daha önce indirildiği varsayılır.

Model indirme işlemlerini yönetme

Dijital mürekkep tanıma API'si yüzlerce dili desteklese de her dil için tanıma işleminden önce bazı verilerin indirilmesi gerekir. Her dil için yaklaşık 20 MB depolama alanı gerekir. Bu işlem RemoteModelManager nesnesi tarafından gerçekleştirilir.

Yeni bir model indirme

Kotlin

import com.google.mlkit.common.model.DownloadConditions
import com.google.mlkit.common.model.RemoteModelManager

var model: DigitalInkRecognitionModel =  ...
val remoteModelManager = RemoteModelManager.getInstance()

remoteModelManager.download(model, DownloadConditions.Builder().build())
    .addOnSuccessListener {
      Log.i(TAG, "Model downloaded")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while downloading a model: $e")
    }

Java

import com.google.mlkit.common.model.DownloadConditions;
import com.google.mlkit.common.model.RemoteModelManager;

DigitalInkRecognitionModel model = ...;
RemoteModelManager remoteModelManager = RemoteModelManager.getInstance();

remoteModelManager
    .download(model, new DownloadConditions.Builder().build())
    .addOnSuccessListener(aVoid -> Log.i(TAG, "Model downloaded"))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error while downloading a model: " + e));

Bir modelin daha önce indirilip indirilmediğini kontrol etme

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.isModelDownloaded(model)

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.isModelDownloaded(model);

İndirilen bir modeli silme

Modelin cihazın depolama alanından kaldırılmasıyla yer açılır.

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.deleteDownloadedModel(model)
    .addOnSuccessListener {
      Log.i(TAG, "Model successfully deleted")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while deleting a model: $e")
    }

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.deleteDownloadedModel(model)
                  .addOnSuccessListener(
                      aVoid -> Log.i(TAG, "Model successfully deleted"))
                  .addOnFailureListener(
                      e -> Log.e(TAG, "Error while deleting a model: " + e));

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.

Kotlin

var preContext : String = ...;
var width : Float = ...;
var height : Float = ...;
val recognitionContext : RecognitionContext =
    RecognitionContext.builder()
        .setPreContext(preContext)
        .setWritingArea(WritingArea(width, height))
        .build()

recognizer.recognize(ink, recognitionContext)

Java

String preContext = ...;
float width = ...;
float height = ...;
RecognitionContext recognitionContext =
    RecognitionContext.builder()
                      .setPreContext(preContext)
                      .setWritingArea(new WritingArea(width, height))
                      .build();

recognizer.recognize(ink, recognitionContext);

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.