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

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

امتحانش کن.

قبل از اینکه شروع کنی

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

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

اکنون آماده‌اید تا متن را در اشیاء Ink تشخیص دهید.

ساخت یک شیء Ink

روش اصلی برای ساخت یک شیء Ink ، ترسیم آن روی صفحه لمسی است. در iOS، می‌توانید از یک UIImageView به همراه کنترل‌کننده‌های رویداد لمسی استفاده کنید که خطوط را روی صفحه ترسیم می‌کنند و همچنین نقاط خطوط را برای ساخت شیء Ink ذخیره می‌کنند. این الگوی کلی در قطعه کد زیر نشان داده شده است. برای مثال کامل‌تر، که مدیریت رویداد لمسی، ترسیم صفحه و مدیریت داده‌های خطوط را از هم جدا می‌کند، به برنامه شروع سریع مراجعه کنید.

سویفت

@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 است که باید در صورت لزوم برای برنامه شما تطبیق داده شود. توصیه می‌کنیم هنگام ترسیم پاره خط‌ها از حروف گرد استفاده کنید تا پاره خط‌های با طول صفر به عنوان یک نقطه ترسیم شوند (به نقطه روی حرف کوچک i فکر کنید). تابع doRecognition() پس از نوشتن هر خط دور فراخوانی می‌شود و در زیر تعریف خواهد شد.

یک نمونه از DigitalInkRecognizer دریافت کنید

برای انجام تشخیص، باید شیء Ink را به یک نمونه DigitalInkRecognizer ارسال کنیم. برای به دست آوردن نمونه DigitalInkRecognizer ، ابتدا باید مدل تشخیص را برای زبان مورد نظر دانلود کنیم و مدل را در RAM بارگذاری کنیم. این کار را می‌توان با استفاده از قطعه کد زیر انجام داد که برای سادگی در متد viewDidLoad() قرار داده شده و از یک نام زبان کدگذاری شده استفاده می‌کند. برای مثالی از نحوه نمایش لیست زبان‌های موجود به کاربر و دانلود زبان انتخاب شده، به برنامه شروع سریع مراجعه کنید.

سویفت

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() فراخوانی می‌شود. در برنامه‌های دیگر، ممکن است بخواهیم تشخیص را فقط پس از یک مهلت زمانی یا زمانی که کاربر دکمه‌ای را برای فعال کردن تشخیص فشار می‌دهد، فراخوانی کنیم.

سویفت

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];
  }];
}

مدیریت دانلود مدل‌ها

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

بررسی کنید که آیا یک مدل قبلاً دانلود شده است یا خیر

سویفت

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

هدف-سی

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

حذف یک مدل دانلود شده

سویفت

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" ادامه دهد، بدون پیش‌زمینه، تشخیص‌دهنده رشته "world" را برمی‌گرداند. با این حال، اگر پیش‌زمینه "hello" را مشخص کنید، مدل رشته "world" را با یک فاصله در ابتدا برمی‌گرداند، زیرا "hello world" از "helloword" معنی بیشتری دارد.

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

نمونه کد زیر نحوه تعریف یک ناحیه نوشتاری و استفاده از شیء RecognitionContext برای مشخص کردن پیش‌زمینه (pre-context) را نشان می‌دهد.

سویفت

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