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

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

ลองเลย

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

  1. ในไฟล์ build.gradle ระดับโปรเจ็กต์ โปรดตรวจสอบว่าได้รวมที่เก็บ Maven ของ Google ไว้ในส่วน buildscript และ allprojects แล้ว
  2. เพิ่มทรัพยากร Dependency สำหรับไลบรารี Android ของ ML Kit ไปยังไฟล์ 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 จะได้รับการฝึกให้จัดการกับรูปแบบการเขียนหลากหลายประเภท แต่ผลลัพธ์อาจแตกต่างกันไปสำหรับผู้ใช้แต่ละคน

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

พื้นที่การเขียน

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

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

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

ก่อนเริ่มบริบท

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

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

บริบทล่วงหน้ายังช่วยให้เครื่องมือจดจำระบุการแบ่งคำ ซึ่งก็คือการเว้นวรรคระหว่างคำได้ คุณสามารถพิมพ์เว้นวรรค แต่ไม่สามารถวาดอักขระได้ แล้วโปรแกรมจดจำจะทราบได้อย่างไรว่าคำหนึ่งสิ้นสุดเมื่อใดและคำถัดไปจะเริ่มต้นขึ้นเมื่อใด หากผู้ใช้เขียน "hello" ไว้แล้วและยังต่อด้วยคำว่า "world" โดยไม่มีบริบทก่อน เครื่องมือจดจำจะแสดงผลสตริง "world" อย่างไรก็ตาม หากคุณระบุคำว่า "hello" ก่อนบริบท โมเดลจะแสดงผลสตริง " world" พร้อมเว้นวรรคนำหน้า เนื่องจาก "helloworld" จะมีความหมายมากกว่า "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 อันดับแรกจะใกล้เคียงกัน นอกจากนี้ โปรดทราบว่าตัวแยกประเภทรูปร่างจะตีความ Ink ทั้งหมดเป็นรูปร่างเดียว ตัวอย่างเช่น หาก Ink มีรูปสี่เหลี่ยมผืนผ้าและมีวงรีอยู่ข้างๆ กัน ตัวจดจำอาจแสดงผลโดยให้อย่างใดอย่างหนึ่ง (หรือสิ่งที่แตกต่างโดยสิ้นเชิง) เป็นผลลัพธ์ เนื่องจากผู้สมัครจดจำรายการเดียวไม่สามารถแทนรูปร่าง 2 รูปได้