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

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

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

วิธีหลักในการสร้างออบเจ็กต์ Ink คือการวาดบนหน้าจอสัมผัส ใน Android คุณสามารถใช้ผืนผ้าใบเพื่อวัตถุประสงค์นี้ได้ แฮนเดิลเหตุการณ์การแตะ ควรเรียกใช้เมธอด 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));

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

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

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

พื้นที่เขียน

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