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

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

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

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

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

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

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

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

ก่อนบริบท

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

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

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