สร้างการจดจำหมึกดิจิทัลด้วย ML Kit ใน iOS

การจดจำลายมือดิจิทัลของ ML Kit ช่วยให้คุณจดจำข้อความที่เขียนด้วยลายมือบน พื้นผิวแบบดิจิทัลได้ในหลายร้อยภาษา รวมถึงจัดประเภทภาพร่างได้ด้วย

ลองเลย

ก่อนเริ่มต้น

  1. ใส่ไลบรารี ML Kit ต่อไปนี้ใน Podfile

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. หลังจากติดตั้งหรืออัปเดต Pod ของโปรเจ็กต์แล้ว ให้เปิดโปรเจ็กต์ Xcode โดยใช้ .xcworkspace ML Kit รองรับใน Xcode เวอร์ชัน 13.2.1 ขึ้นไป

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

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

วิธีหลักในการสร้างออบเจ็กต์ Ink คือการวาดบนหน้าจอสัมผัส ใน iOS คุณสามารถใช้ UIImageView ร่วมกับ ตัวแฮนเดิลเหตุการณ์การแตะ ซึ่งวาดเส้นบนหน้าจอและจัดเก็บจุดของเส้นเพื่อสร้าง ออบเจ็กต์ Ink รูปแบบทั่วไปนี้แสดงให้เห็นในข้อมูลโค้ดต่อไปนี้ ดูตัวอย่างที่สมบูรณ์ยิ่งขึ้นได้ในคู่มือเริ่มต้นใช้งาน แอป ซึ่งแยกการจัดการเหตุการณ์การแตะ การวาดหน้าจอ และการจัดการข้อมูลลายเส้น

Swift

@IBOutlet weak var mainImageView: UIImageView!
var kMillisecondsPerTimeInterval = 1000.0
var lastPoint = CGPoint.zero
private var strokes: [Stroke] = []
private var points: [StrokePoint] = []

func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
  UIGraphicsBeginImageContext(view.frame.size)
  guard let context = UIGraphicsGetCurrentContext() else {
    return
  }
  mainImageView.image?.draw(in: view.bounds)
  context.move(to: fromPoint)
  context.addLine(to: toPoint)
  context.setLineCap(.round)
  context.setBlendMode(.normal)
  context.setLineWidth(10.0)
  context.setStrokeColor(UIColor.white.cgColor)
  context.strokePath()
  mainImageView.image = UIGraphicsGetImageFromCurrentImageContext()
  mainImageView.alpha = 1.0
  UIGraphicsEndImageContext()
}

override func touchesBegan(_ touches: Set, with event: UIEvent?) {
  guard let touch = touches.first else {
    return
  }
  lastPoint = touch.location(in: mainImageView)
  let t = touch.timestamp
  points = [StrokePoint.init(x: Float(lastPoint.x),
                             y: Float(lastPoint.y),
                             t: Int(t * kMillisecondsPerTimeInterval))]
  drawLine(from:lastPoint, to:lastPoint)
}

override func touchesMoved(_ touches: Set, with event: UIEvent?) {
  guard let touch = touches.first else {
    return
  }
  let currentPoint = touch.location(in: mainImageView)
  let t = touch.timestamp
  points.append(StrokePoint.init(x: Float(currentPoint.x),
                                 y: Float(currentPoint.y),
                                 t: Int(t * kMillisecondsPerTimeInterval)))
  drawLine(from: lastPoint, to: currentPoint)
  lastPoint = currentPoint
}

override func touchesEnded(_ touches: Set, with event: UIEvent?) {
  guard let touch = touches.first else {
    return
  }
  let currentPoint = touch.location(in: mainImageView)
  let t = touch.timestamp
  points.append(StrokePoint.init(x: Float(currentPoint.x),
                                 y: Float(currentPoint.y),
                                 t: Int(t * kMillisecondsPerTimeInterval)))
  drawLine(from: lastPoint, to: currentPoint)
  lastPoint = currentPoint
  strokes.append(Stroke.init(points: points))
  self.points = []
  doRecognition()
}

Objective-C

// Interface
@property (weak, nonatomic) IBOutlet UIImageView *mainImageView;
@property(nonatomic) CGPoint lastPoint;
@property(nonatomic) NSMutableArray *strokes;
@property(nonatomic) NSMutableArray *points;

// Implementations
static const double kMillisecondsPerTimeInterval = 1000.0;

- (void)drawLineFrom:(CGPoint)fromPoint to:(CGPoint)toPoint {
  UIGraphicsBeginImageContext(self.mainImageView.frame.size);
  [self.mainImageView.image drawInRect:CGRectMake(0, 0, self.mainImageView.frame.size.width,
                                                  self.mainImageView.frame.size.height)];
  CGContextMoveToPoint(UIGraphicsGetCurrentContext(), fromPoint.x, fromPoint.y);
  CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), toPoint.x, toPoint.y);
  CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
  CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 10.0);
  CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 1, 1, 1, 1);
  CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal);
  CGContextStrokePath(UIGraphicsGetCurrentContext());
  CGContextFlush(UIGraphicsGetCurrentContext());
  self.mainImageView.image = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();
}

- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event {
  UITouch *touch = [touches anyObject];
  self.lastPoint = [touch locationInView:self.mainImageView];
  NSTimeInterval time = [touch timestamp];
  self.points = [NSMutableArray array];
  [self.points addObject:[[MLKStrokePoint alloc] initWithX:self.lastPoint.x
                                                         y:self.lastPoint.y
                                                         t:time * kMillisecondsPerTimeInterval]];
  [self drawLineFrom:self.lastPoint to:self.lastPoint];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event {
  UITouch *touch = [touches anyObject];
  CGPoint currentPoint = [touch locationInView:self.mainImageView];
  NSTimeInterval time = [touch timestamp];
  [self.points addObject:[[MLKStrokePoint alloc] initWithX:currentPoint.x
                                                         y:currentPoint.y
                                                         t:time * kMillisecondsPerTimeInterval]];
  [self drawLineFrom:self.lastPoint to:currentPoint];
  self.lastPoint = currentPoint;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event {
  UITouch *touch = [touches anyObject];
  CGPoint currentPoint = [touch locationInView:self.mainImageView];
  NSTimeInterval time = [touch timestamp];
  [self.points addObject:[[MLKStrokePoint alloc] initWithX:currentPoint.x
                                                         y:currentPoint.y
                                                         t:time * kMillisecondsPerTimeInterval]];
  [self drawLineFrom:self.lastPoint to:currentPoint];
  self.lastPoint = currentPoint;
  if (self.strokes == nil) {
    self.strokes = [NSMutableArray array];
  }
  [self.strokes addObject:[[MLKStroke alloc] initWithPoints:self.points]];
  self.points = nil;
  [self doRecognition];
}

โปรดทราบว่าข้อมูลโค้ดมีฟังก์ชันตัวอย่างเพื่อวาดเส้นใน UIImageView ซึ่งควรปรับให้เหมาะกับแอปพลิเคชันของคุณตามความจำเป็น เราขอแนะนำให้ใช้ roundcaps เมื่อวาดส่วนของเส้นเพื่อให้ส่วนที่มีความยาวเป็น 0 จะวาดเป็นจุด (นึกถึงจุดบนตัวอักษร i พิมพ์เล็ก) ระบบจะเรียกใช้doRecognition() ฟังก์ชันหลังจากเขียนแต่ละเส้น และจะกำหนดไว้ด้านล่าง

รับอินสแตนซ์ของ DigitalInkRecognizer

หากต้องการทำการจดจำ เราต้องส่งออบเจ็กต์ Ink ไปยังอินสแตนซ์ DigitalInkRecognizer หากต้องการรับอินสแตนซ์ DigitalInkRecognizer เราต้องดาวน์โหลดโมเดลตัวจดจำสำหรับภาษาที่ต้องการก่อน และ โหลดโมเดลลงใน RAM คุณทำได้โดยใช้ข้อมูลโค้ดต่อไปนี้ ซึ่งวางไว้ในเมธอด viewDidLoad() เพื่อให้ง่ายต่อการใช้งาน และใช้ชื่อภาษาที่ ฮาร์ดโค้ด ดูการเริ่มต้นใช้งาน แอปเพื่อดู ตัวอย่างวิธีแสดงรายการภาษาที่พร้อมให้บริการแก่ผู้ใช้และดาวน์โหลด ภาษาที่เลือก

Swift

override func viewDidLoad() {
  super.viewDidLoad()
  let languageTag = "en-US"
  let identifier = DigitalInkRecognitionModelIdentifier(forLanguageTag: languageTag)
  if identifier == nil {
    // no model was found or the language tag couldn't be parsed, handle error.
  }
  let model = DigitalInkRecognitionModel.init(modelIdentifier: identifier!)
  let modelManager = ModelManager.modelManager()
  let conditions = ModelDownloadConditions.init(allowsCellularAccess: true,
                                         allowsBackgroundDownloading: true)
  modelManager.download(model, conditions: conditions)
  // Get a recognizer for the language
  let options: DigitalInkRecognizerOptions = DigitalInkRecognizerOptions.init(model: model)
  recognizer = DigitalInkRecognizer.digitalInkRecognizer(options: options)
}

Objective-C

- (void)viewDidLoad {
  [super viewDidLoad];
  NSString *languagetag = @"en-US";
  MLKDigitalInkRecognitionModelIdentifier *identifier =
      [MLKDigitalInkRecognitionModelIdentifier modelIdentifierForLanguageTag:languagetag];
  if (identifier == nil) {
    // no model was found or the language tag couldn't be parsed, handle error.
  }
  MLKDigitalInkRecognitionModel *model = [[MLKDigitalInkRecognitionModel alloc]
                                          initWithModelIdentifier:identifier];
  MLKModelManager *modelManager = [MLKModelManager modelManager];
  [modelManager downloadModel:model conditions:[[MLKModelDownloadConditions alloc]
                                                initWithAllowsCellularAccess:YES
                                                allowsBackgroundDownloading:YES]];
  MLKDigitalInkRecognizerOptions *options =
      [[MLKDigitalInkRecognizerOptions alloc] initWithModel:model];
  self.recognizer = [MLKDigitalInkRecognizer digitalInkRecognizerWithOptions:options];
}

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

จดจำInkออบเจ็กต์

จากนั้นเราจะมาที่ฟังก์ชัน doRecognition() ซึ่งเราจะเรียกว่า จาก touchesEnded() เพื่อให้เข้าใจง่าย ในแอปพลิเคชันอื่นๆ คุณอาจต้องการเรียกใช้การจดจำหลังจากหมดเวลาเท่านั้น หรือเมื่อผู้ใช้กดปุ่มเพื่อทริกเกอร์การจดจำ

Swift

func doRecognition() {
  let ink = Ink.init(strokes: strokes)
  recognizer.recognize(
    ink: ink,
    completion: {
      [unowned self]
      (result: DigitalInkRecognitionResult?, error: Error?) in
      var alertTitle = ""
      var alertText = ""
      if let result = result, let candidate = result.candidates.first {
        alertTitle = "I recognized this:"
        alertText = candidate.text
      } else {
        alertTitle = "I hit an error:"
        alertText = error!.localizedDescription
      }
      let alert = UIAlertController(title: alertTitle,
                                  message: alertText,
                           preferredStyle: UIAlertController.Style.alert)
      alert.addAction(UIAlertAction(title: "OK",
                                    style: UIAlertAction.Style.default,
                                  handler: nil))
      self.present(alert, animated: true, completion: nil)
    }
  )
}

Objective-C

- (void)doRecognition {
  MLKInk *ink = [[MLKInk alloc] initWithStrokes:self.strokes];
  __weak typeof(self) weakSelf = self;
  [self.recognizer
      recognizeInk:ink
        completion:^(MLKDigitalInkRecognitionResult *_Nullable result,
                     NSError *_Nullable error) {
    typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf == nil) {
      return;
    }
    NSString *alertTitle = nil;
    NSString *alertText = nil;
    if (result.candidates.count > 0) {
      alertTitle = @"I recognized this:";
      alertText = result.candidates[0].text;
    } else {
      alertTitle = @"I hit an error:";
      alertText = [error localizedDescription];
    }
    UIAlertController *alert =
        [UIAlertController alertControllerWithTitle:alertTitle
                                            message:alertText
                                     preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"OK"
                                              style:UIAlertActionStyleDefault
                                            handler:nil]];
    [strongSelf presentViewController:alert animated:YES completion:nil];
  }];
}

การจัดการการดาวน์โหลดโมเดล

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

ตรวจสอบว่ามีการดาวน์โหลดโมเดลแล้วหรือยัง

Swift

let model : DigitalInkRecognitionModel = ...
let modelManager = ModelManager.modelManager()
modelManager.isModelDownloaded(model)

Objective-C

MLKDigitalInkRecognitionModel *model = ...;
MLKModelManager *modelManager = [MLKModelManager modelManager];
[modelManager isModelDownloaded:model];

ลบโมเดลที่ดาวน์โหลด

Swift

let model : DigitalInkRecognitionModel = ...
let modelManager = ModelManager.modelManager()

if modelManager.isModelDownloaded(model) {
  modelManager.deleteDownloadedModel(
    model!,
    completion: {
      error in
      if error != nil {
        // Handle error
        return
      }
      NSLog(@"Model deleted.");
    })
}

Objective-C

MLKDigitalInkRecognitionModel *model = ...;
MLKModelManager *modelManager = [MLKModelManager modelManager];

if ([self.modelManager isModelDownloaded:model]) {
  [self.modelManager deleteDownloadedModel:model
                                completion:^(NSError *_Nullable error) {
                                  if (error) {
                                    // Handle error.
                                    return;
                                  }
                                  NSLog(@"Model deleted.");
                                }];
}

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

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

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

Swift

let ink: Ink = ...;
let recognizer: DigitalInkRecognizer =  ...;
let preContext: String = ...;
let writingArea = WritingArea.init(width: ..., height: ...);

let context: DigitalInkRecognitionContext.init(
    preContext: preContext,
    writingArea: writingArea);

recognizer.recognizeHandwriting(
  from: ink,
  context: context,
  completion: {
    (result: DigitalInkRecognitionResult?, error: Error?) in
    if let result = result, let candidate = result.candidates.first {
      NSLog("Recognized \(candidate.text)")
    } else {
      NSLog("Recognition error \(error)")
    }
  })

Objective-C

MLKInk *ink = ...;
MLKDigitalInkRecognizer *recognizer = ...;
NSString *preContext = ...;
MLKWritingArea *writingArea = [MLKWritingArea initWithWidth:...
                                              height:...];

MLKDigitalInkRecognitionContext *context = [MLKDigitalInkRecognitionContext
       initWithPreContext:preContext
       writingArea:writingArea];

[recognizer recognizeHandwritingFromInk:ink
            context:context
            completion:^(MLKDigitalInkRecognitionResult
                         *_Nullable result, NSError *_Nullable error) {
                               NSLog(@"Recognition result %@",
                                     result.candidates[0].text);
                         }];

การเรียงลำดับการลากเส้น

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

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

การจัดการกับรูปร่างที่คลุมเครือ

มีบางกรณีที่ความหมายของรูปร่างที่ส่งไปยังตัวจดจำไม่ชัดเจน ตัวอย่างเช่น สี่เหลี่ยมผืนผ้าที่มีขอบโค้งมนมากอาจมองได้ทั้งเป็นสี่เหลี่ยมผืนผ้าหรือวงรี

กรณีที่ไม่ชัดเจนเหล่านี้สามารถจัดการได้โดยใช้คะแนนการจดจำเมื่อพร้อมใช้งาน มีเพียงตัวแยกประเภทรูปร่างเท่านั้นที่ให้คะแนน หากโมเดลมีความมั่นใจมาก คะแนนของผลลัพธ์แรกจะดีกว่าผลลัพธ์ที่สองมาก หากมีความไม่แน่นอน คะแนนของผลการค้นหา 2 อันดับแรกจะ ใกล้เคียงกัน นอกจากนี้ โปรดทราบว่าตัวแยกประเภทรูปร่างจะตีความ Ink ทั้งหมดเป็นรูปร่างเดียว ตัวอย่างเช่น หาก Ink มีสี่เหลี่ยมผืนผ้าและวงรีอยู่ติดกัน ตัวจดจำอาจแสดงผลเป็นสี่เหลี่ยมผืนผ้าหรือวงรี (หรือรูปร่างอื่นที่แตกต่างออกไปโดยสิ้นเชิง) เนื่องจากรูปร่างเดียวไม่สามารถแสดงรูปร่าง 2 แบบได้