เรียนรู้เกี่ยวกับหมึกดิจิทัลด้วย 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));

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

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

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

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

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

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