เรียนรู้เกี่ยวกับหมึกดิจิทัลด้วย 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:19.0.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" การระบุบริบทก่อนหน้า "arg" จะช่วยแก้ความกำกวมได้ เนื่องจากคำว่า "argument" มีแนวโน้มที่จะถูกต้องมากกว่า "argnment"

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