เรียนรู้เกี่ยวกับหมึกดิจิทัลด้วย ML Kit บน Android

การจดจำด้วยหมึกดิจิทัลของ ML Kit ช่วยให้คุณจดจำข้อความที่เขียนด้วยลายมือบนพื้นผิวดิจิทัลได้หลายร้อยภาษา รวมถึงจำแนกประเภทภาพร่างได้

ลองเลย

ก่อนเริ่มต้น

  1. ในไฟล์ build.gradle ระดับโปรเจ็กต์ อย่าลืมรวมที่เก็บ Maven ของ Google ไว้ทั้งในส่วน buildscript และ allprojects
  2. เพิ่มทรัพยากร Dependency สำหรับไลบรารี ML Kit Android ลงในไฟล์ Gradle ระดับแอปของโมดูล ซึ่งปกติคือ app/build.gradle
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

คุณพร้อมที่จะเริ่มจดจำข้อความในออบเจ็กต์ Ink แล้ว

สร้างออบเจ็กต์ Ink

วิธีหลักในการสร้างวัตถุ Ink คือการวาดวัตถุบนหน้าจอสัมผัส ใน Android คุณสามารถใช้ Canvas เพื่อวัตถุประสงค์นี้ได้ เครื่องจัดการเหตุการณ์การแตะควรเรียกใช้เมธอด addNewTouchEvent() ที่แสดงข้อมูลโค้ดต่อไปนี้เพื่อเก็บจุดในเส้นที่ผู้ใช้วาดลงในออบเจ็กต์ Ink

รูปแบบทั่วไปนี้จะแสดงในข้อมูลโค้ดต่อไปนี้ ดูตัวอย่างที่สมบูรณ์มากขึ้นได้ในตัวอย่างการเริ่มต้น ML Kit อย่างรวดเร็ว

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

หากต้องการจดจำ ให้ส่งอินสแตนซ์ Ink ไปยังออบเจ็กต์ DigitalInkRecognizer โค้ดด้านล่างแสดงวิธีสร้างอินสแตนซ์โปรแกรมจดจำดังกล่าวจากแท็ก BCP-47

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

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));

โค้ดตัวอย่างด้านบนจะถือว่ามีการดาวน์โหลดโมเดลการจดจำแล้ว ดังที่อธิบายในส่วนถัดไป

การจัดการการดาวน์โหลดโมเดล

แม้ว่า API การรู้จำหมึกดิจิทัลจะรองรับภาษาหลายร้อยภาษา แต่แต่ละภาษาจำเป็นต้องดาวน์โหลดข้อมูลบางอย่างก่อนการจดจำ ต้องใช้พื้นที่เก็บข้อมูลประมาณ 20 MB ต่อภาษา ซึ่งระบบจะจัดการโดยออบเจ็กต์ RemoteModelManager

ดาวน์โหลดโมเดลใหม่

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));

ตรวจสอบว่าดาวน์โหลดโมเดลแล้วหรือไม่

Kotlin

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

Java

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

ลบโมเดลที่ดาวน์โหลด

การนำรุ่นออกจากพื้นที่เก็บข้อมูลของอุปกรณ์จะช่วยเพิ่มพื้นที่ว่าง

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));

เคล็ดลับในการปรับปรุงความแม่นยำในการจดจำข้อความ

ความถูกต้องของการจดจำข้อความอาจแตกต่างกันไปตามภาษาต่างๆ ความถูกต้องยังขึ้นอยู่กับ สไตล์การเขียนด้วย แม้ว่า Digital Ink Recognition จะได้รับการฝึกให้รองรับการเขียนรูปแบบต่างๆ แต่ผลการค้นหาก็อาจแตกต่างกันไปตามผู้ใช้แต่ละคน

วิธีปรับปรุงความแม่นยำของโปรแกรมจดจำข้อความมีดังนี้ โปรดทราบว่าเทคนิคเหล่านี้ไม่ใช้กับตัวแยกประเภทภาพวาดสำหรับอีโมจิ, AutoDraw และรูปร่าง

พื้นที่สำหรับเขียน

แอปพลิเคชันจำนวนมากมีพื้นที่การเขียนที่กำหนดไว้อย่างชัดเจนสำหรับป้อนข้อมูลของผู้ใช้ ความหมายของสัญลักษณ์กำหนดบางส่วนจากขนาดที่สัมพันธ์กับขนาดของพื้นที่เขียนข้อความ เช่น ความแตกต่างระหว่างอักษรตัวพิมพ์เล็กหรือตัวพิมพ์ใหญ่ "o" หรือ "c" กับเครื่องหมายคอมมากับเครื่องหมายทับ

การบอกโปรแกรมจดจำว่าความกว้างและความสูงของพื้นที่การเขียนจะช่วยเพิ่มความแม่นยำได้ อย่างไรก็ตาม เครื่องมือจดจำจะถือว่าพื้นที่สำหรับการเขียนมีเพียงบรรทัดเดียว หากพื้นที่การเขียนมีขนาดใหญ่พอที่จะให้ผู้ใช้เขียนได้ตั้งแต่ 2 บรรทัดขึ้นไป คุณอาจได้ผลลัพธ์ที่ดีขึ้นโดยการส่งผ่าน WritingArea ที่มีความสูงเท่ากับความสูงของข้อความบรรทัดเดียวโดยประมาณ ออบเจ็กต์ WritingArea ที่คุณส่งไปยังเครื่องมือรู้จำไม่จำเป็นต้องสอดคล้องกับพื้นที่การเขียนจริงบนหน้าจอทุกประการ การเปลี่ยนความสูงของ WritingArea ด้วยวิธีนี้ จะมีประสิทธิภาพในบางภาษามากกว่าภาษาอื่นๆ

เมื่อคุณระบุพื้นที่การเขียน ให้ระบุความกว้างและความสูงเป็นหน่วยเดียวกับพิกัดของเส้น อาร์กิวเมนต์พิกัด x,y ไม่มีข้อกำหนดหน่วย เนื่องจาก API จะปรับหน่วยทั้งหมดให้เป็นมาตรฐานเดียวกัน ดังนั้นสิ่งเดียวที่สำคัญคือขนาดและตำแหน่งของเส้นโครงร่าง คุณสามารถส่งพิกัดได้ ในระดับที่เหมาะสมกับระบบของคุณ

ก่อนบริบท

ก่อนบริบทคือข้อความที่อยู่ก่อนเส้นโครงร่างใน Ink ที่คุณพยายามจดจำ คุณช่วยเครื่องมือจดจำได้โดยบอกเกี่ยวกับบริบทเบื้องต้น

ตัวอย่างเช่น ตัวอักษรหยาบ "n" และ "u" มักเข้าใจผิดว่าเป็นอักขระอื่น หากผู้ใช้ป้อนคำว่า "arg" บางส่วนไปแล้ว ผู้ใช้อาจดำเนินการต่อด้วยเส้นที่จดจำได้ว่าเป็น "ument" หรือ "nment" การระบุก่อนบริบทเป็น "อาร์กิวเมนต์" จะช่วยแก้ไขความกำกวม เนื่องจากคำว่า "อาร์กิวเมนต์" มีแนวโน้มที่จะเป็น "อาร์กิวเมนต์"

นอกจากนี้ บริบทเบื้องต้นยังช่วยให้ระบบจดจำระบุตัวแบ่งคำหรือการเว้นวรรคระหว่างคำได้ด้วย คุณพิมพ์อักขระเว้นวรรคได้ แต่วาดอักขระหนึ่งไม่ได้ แล้วโปรแกรมรู้จำจะบอกได้อย่างไรว่าคำหนึ่งสิ้นสุดและคำถัดไปเริ่มต้นเมื่อใด หากผู้ใช้เขียนคำว่า "hello" แล้วต่อด้วยคำว่า "world" ที่เขียนไว้ โดยไม่มีบริบทล่วงหน้า เครื่องมือจดจำจะแสดงสตริง "world" อย่างไรก็ตาม หากคุณระบุคำนำหน้า "hello" โมเดลจะแสดงผลสตริง " world" ที่มีช่องว่างนำหน้า เนื่องจาก " Hello World" นั้นเหมาะสมกว่า "helloword"

คุณควรระบุสตริงก่อนบริบทที่ยาวที่สุดเท่าที่จะเป็นไปได้ โดยมีความยาวไม่เกิน 20 อักขระ รวมการเว้นวรรค หากสตริงยาวกว่า เครื่องมือจดจำจะใช้เพียง 20 อักขระสุดท้าย

ตัวอย่างโค้ดด้านล่างแสดงวิธีกำหนดพื้นที่สำหรับเขียนและใช้ออบเจ็กต์ RecognitionContext เพื่อระบุบริบทล่วงหน้า

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);

การจัดเรียงสโตรก

ความถูกต้องในการจดจำจะคำนึงถึงลำดับของเส้น โปรแกรมจดจำจะคาดหวังการลากเส้นตามลำดับที่ผู้ใช้เขียนเอง เช่น การเขียนจากซ้ายไปขวาสำหรับภาษาอังกฤษ กรณีใดก็ตามที่ออกจากรูปแบบนี้ เช่น การเขียนประโยคภาษาอังกฤษโดยเริ่มต้นด้วยคำสุดท้าย จะให้ผลลัพธ์ที่แม่นยำน้อยลง

อีกตัวอย่างหนึ่งคือเมื่อมีการนำคำที่อยู่ตรงกลาง Ink ออกและแทนที่ด้วยคำอื่น การแก้ไขอาจอยู่ระหว่างกลางประโยค แต่เส้นสำหรับการแก้ไขจะอยู่ที่ตอนท้ายของลำดับเส้นขีด ในกรณีนี้ เราขอแนะนำให้ส่งคำที่เขียนใหม่แยกต่างหากไปยัง API และรวมผลลัพธ์กับการจดจำก่อนหน้าโดยใช้ตรรกะของคุณเอง

การจัดการกับรูปร่างที่ไม่ชัดเจน

มีบางกรณีที่ความหมายของรูปร่างที่ให้ไว้กับเครื่องมือจดจำนั้นไม่ชัดเจน ตัวอย่างเช่น สี่เหลี่ยมผืนผ้าที่มีขอบมนมากอาจเห็นเป็นสี่เหลี่ยมผืนผ้าหรือวงรี

คุณใช้คะแนนการจดจำเสียงเมื่อข้อมูลมีการจัดการเคสที่ไม่ชัดเจนเหล่านี้ได้ มีเพียงตัวแยกประเภทรูปร่างเท่านั้นที่จะให้คะแนน หากโมเดลมั่นใจมาก คะแนนของผลลัพธ์อันดับต้นๆ จะดีกว่ามากเป็นอันดับ 2 หากไม่แน่นอน คะแนนสำหรับผลลัพธ์ 2 อันดับแรกจะปิดลง นอกจากนี้ โปรดทราบว่าตัวแยกประเภทรูปร่างจะตีความ Ink ทั้งหมดเป็นรูปร่างเดียว ตัวอย่างเช่น ถ้า Ink มีรูปสี่เหลี่ยมผืนผ้าและวงรีอยู่ข้างๆ กัน เครื่องมือรู้จำอาจแสดงผลอย่างใดอย่างหนึ่ง (หรืออย่างอื่นโดยสิ้นเชิง) เนื่องจากคำแนะนำการจดจำเดี่ยวไม่สามารถแสดงรูปร่าง 2 รูปได้