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

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

ลองเลย

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

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

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. หลังจากติดตั้งหรืออัปเดตพ็อดของโปรเจ็กต์แล้ว ให้เปิดโปรเจ็กต์ Xcode โดยใช้ .xcworkspace เวอร์ชัน Xcode รองรับ ML Kit 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 ซึ่งควรมีการปรับเปลี่ยนตามความจําเป็นสําหรับการสมัครของคุณ เราขอแนะนำให้ใช้ วงกลมรอบเมื่อวาดส่วนของเส้นตรงเพื่อให้ส่วนของเส้นตรงที่ความยาวเป็นศูนย์ วาดเป็นจุด (ลองนึกถึงจุดบนตัวอักษรพิมพ์เล็ก 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.");
                                }];
}

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

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

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

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

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

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

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

ก่อนบริบท

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

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

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

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