تشخیص دادن جوهر دیجیتال با ML Kit در iOS

با تشخیص جوهر دیجیتال ML Kit، می‌توانید نوشتار دست‌نویس روی سطح دیجیتال را به صدها زبان تشخیص دهید و همچنین طرح‌ها را طبقه‌بندی کنید.

امتحان کنید

  • با برنامه نمونه بازی کنید تا نمونه‌ای از استفاده از این API را ببینید.

قبل‌از شروع

  1. کتابخانه‌های زیر ML Kit را در Podfile خود بگنجانید:

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. پس‌از نصب یا به‌روزرسانی «واحدها» پروژه، پروژه 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()
}

آبجکتیو-سی

// 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 استفاده کنید تا بخش‌های با طول صفر به‌صورت نقطه رسم شوند (به نقطه روی حرف کوچک 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)
}

آبجکتیو-سی

- (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)
    }
  )
}

آبجکتیو-سی

- (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)

آبجکتیو-سی

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.");
    })
}

آبجکتیو-سی

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.");
                                }];
}

نکته‌هایی برای بهبود دقت تشخیص نوشتار

دقت تشخیص نوشتار در زبان‌های مختلف متفاوت است. دقت به سبک نوشتن نیز بستگی دارد. هرچند «تشخیص دست‌نویسی دیجیتال» برای مدیریت انواع مختلف سبک‌های نوشتن آموزش داده شده است، نتایج می‌تواند از کاربری به کاربر دیگر متفاوت باشد.

در اینجا چند روش برای بهبود دقت تشخیص‌دهنده نوشتار آورده شده است. توجه داشته باشید که این تکنیک‌ها برای طبقه‌بندی‌کننده‌های طراحی اموجی‌ها، طراحی خودکار، و اشکال اعمال نمی‌شود.

محدوده نوشتن

بسیاری از برنامه‌ها ناحیه نوشتاری مشخصی برای ورودی کاربر دارند. معنی نماد تاحدودی براساس اندازه آن نسبت به اندازه ناحیه نوشتاری حاوی آن تعیین می‌شود. برای مثال، تفاوت بین حرف کوچک یا بزرگ «o» یا «c»، و ویرگول دربرابر خط مایل.

گفتن عرض و ارتفاع ناحیه نوشتن به تشخیص‌دهنده می‌تواند دقت را بهبود بخشد. بااین‌حال، بازشناس فرض می‌کند که ناحیه نوشتن فقط حاوی یک خط نوشتار است. اگر فضای فیزیکی نوشتن به‌اندازه کافی بزرگ باشد که به کاربر اجازه دهد دو یا چند خط بنویسد، با ارسال WritingArea با ارتفاعی که بهترین تخمین شما از ارتفاع یک خط متن است، ممکن است نتایج بهتری دریافت کنید. شیء WritingArea که به تشخیص‌دهنده ارسال می‌کنید لازم نیست دقیقاً با ناحیه نوشتن فیزیکی روی صفحه مطابقت داشته باشد. تغییر ارتفاع «فضای نوشتن» به این روش در برخی‌از زبان‌ها بهتر از زبان‌های دیگر کار می‌کند.

وقتی ناحیه نوشتن را مشخص می‌کنید، عرض و ارتفاع آن را با همان واحد مختصات ضربه مشخص کنید. آرگومان‌های مختصات x و y نیازی به واحد ندارند - API همه واحدها را نرمال‌سازی می‌کند، بنابراین تنها چیزی که اهمیت دارد اندازه و موقعیت نسبی خطوط است. می‌توانید مختصات را در هر مقیاسی که برای سیستم شما منطقی است وارد کنید.

پیش‌زمینه

پیش‌زمینه نوشتاری است که بلافاصله قبل‌از ضربه‌ها در Ink که می‌خواهید تشخیص دهید قرار دارد. می‌توانید با ارائه اطلاعات درباره پیش‌زمینه به تشخیص‌دهنده کمک کنید.

برای مثال، حروف دست‌نویس «n» و «u» اغلب با یکدیگر اشتباه گرفته می‌شوند. اگر کاربر قبلاً کلمه ناقص «arg» را وارد کرده باشد، ممکن است با ضربه‌هایی که به‌عنوان «ument» یا «nment» قابل‌تشخیص هستند ادامه دهد. مشخص کردن پیش‌زمینه «arg» ابهام را برطرف می‌کند، زیرا کلمه «argument» محتمل‌تر از «argnment» است.

پیش‌زمینه همچنین می‌تواند به تشخیص‌دهنده کمک کند تا وقفه‌های کلمه‌ای، یعنی فاصله‌های بین کلمات را شناسایی کند. می‌توانید نویسه فاصله را تایپ کنید اما نمی‌توانید آن را بکشید، بنابراین چگونه تشخیص‌دهنده می‌تواند تشخیص دهد که یک کلمه کجا تمام می‌شود و کلمه بعدی کجا شروع می‌شود؟ اگر کاربر قبلاً «سلام» را نوشته باشد و با نوشتن کلمه «دنیا» ادامه دهد، بدون پیش‌زمینه، تشخیص‌دهنده رشته «دنیا» را برمی‌گرداند. بااین‌حال، اگر پیش‌زمینه «hello» را مشخص کنید، مدل رشته « world» را با یک فاصله در ابتدا برمی‌گرداند، زیرا «hello world» منطقی‌تر از «helloword» است.

باید طولانی‌ترین رشته پیش‌زمینه‌ای ممکن را، تا ۲۰ نویسه، ازجمله فاصله‌ها ارائه دهید. اگر رشته طولانی‌تر باشد، تشخیص‌دهنده فقط از ۲۰ نویسه آخر استفاده می‌کند.

نمونه کد زیر نشان می‌دهد که چگونه ناحیه نوشتاری را تعریف کنید و از 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)")
    }
  })

آبجکتیو-سی

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 ارسال کنید و نتیجه را با منطق خودتان با تشخیص‌های قبلی ادغام کنید.

پرداختن به شکل‌های مبهم

در مواردی، معنای شکلی که به تشخیص‌دهنده ارائه می‌شود مبهم است. برای مثال، مستطیلی با لبه‌های بسیار گرد می‌تواند به‌عنوان مستطیل یا بیضی دیده شود.

این موارد نامشخص را می‌توان بااستفاده از امتیازهای تشخیص زمانی که دردسترس هستند مدیریت کرد. فقط طبقه‌بندی‌کننده‌های شکل امتیاز ارائه می‌دهند. اگر مدل بسیار مطمئن باشد، امتیاز نتیجه برتر بسیار بهتر از دومین نتیجه برتر خواهد بود. اگر عدم قطعیت وجود داشته باشد، امتیازهای دو نتیجه برتر به هم نزدیک خواهد بود. همچنین به‌خاطر داشته باشید که طبقه‌بندهای شکل کل Ink را به‌عنوان یک شکل واحد تفسیر می‌کنند. برای مثال، اگر Ink حاوی مستطیل و بیضی در کنار یکدیگر باشد، تشخیص‌دهنده ممکن است یکی از آن‌ها (یا چیزی کاملاً متفاوت) را به‌عنوان نتیجه برگرداند، زیرا یک نامزد تشخیص نمی‌تواند دو شکل را نشان دهد.