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

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

บริบทก่อน

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

เช่น ตัวอักษรตัวเขียน "น" และ "ว" มักทำให้เข้าใจผิดว่าเป็นคนละตัวกัน หากผู้ใช้ป้อนคำ "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 รูปร่าง