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

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

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

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

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

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

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

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

ก่อนบริบท

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

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

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

คุณควรระบุสตริงก่อนบริบทที่ยาวที่สุดเท่าที่จะเป็นไปได้ โดยไม่เกิน 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 รูปร่าง