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

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. Android'de bu amaçla tuval kullanabilirsiniz. Dokunma etkinliği işleyicileriniz, kullanıcının Ink nesnesine çizdiği çizgilerdeki noktaları 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ıç kılavuzu ö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ımayı gerçekleştirmek için Ink örneğini bir DigitalInkRecognizer nesnesine gönderin. Aşağıdaki kodda, bir BCP-47 etiketinden böyle bir tanımlayıcının nasıl oluşturulacağı 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 sonraki bölümde açıklandığı gibi önceden indirilmiş olduğu varsayılmaktadır.

Model indirme işlemlerini yönetme

Dijital mürekkep tanıma API'si yüzlerce dili desteklese de her dilin tanınmadan önce bazı verilerin indirilmesi gerekir. Dil başına yaklaşık 20 MB depolama alanı gerekir. Bu işlem RemoteModelManager nesnesi tarafından yönetilir.

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

Bir modeli cihazın depolama alanından kaldırarak yer açabilirsiniz.

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 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.

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ı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.