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