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

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

امتحان کنید

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

قبل‌از شروع

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

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. پس‌از نصب یا به‌روزرسانی Pods پروژه، پروژه Xcode را بااستفاده از .xcworkspace آن باز کنید. ML Kit در Xcode نسخه ۱۳.۲.۱ یا بالاتر پشتیبانی می‌شود.

اکنون آماده‌اید تا تشخیص نوشتار را در اشیاء 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 که به تشخیص‌دهنده ارسال می‌کنید لازم نیست دقیقاً با ناحیه نوشتن فیزیکی روی صفحه‌نمایش مطابقت داشته باشد. تغییر ارتفاع 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 حاوی یک مستطیل و یک بیضی در کنار یکدیگر باشد، تشخیص‌دهنده ممکن است یکی یا دیگری (یا چیزی کاملاً متفاوت) را به‌عنوان نتیجه برگرداند، زیرا یک نامزد تشخیص واحد نمی‌تواند دو شکل را نشان دهد.