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

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

آن را امتحان کنید

قبل از شروع

  1. کتابخانه های ML Kit زیر را در فایل پادفایل خود قرار دهید:

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

اکنون برای شروع به تشخیص متن در اشیاء 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()
}

هدف-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() پس از نوشتن هر stroke فراخوانی می شود و در زیر تعریف می شود.

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

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

سویفت

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

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

سویفت

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

هدف-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];
  }];
}

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

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

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

سویفت

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

هدف-C

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

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

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

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

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

منطقه نوشتن

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

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

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

پیش زمینه

پیش زمینه متنی است که بلافاصله قبل از ضربه های موجود در Ink است که می خواهید تشخیص دهید. می توانید با گفتن پیش زمینه به تشخیص دهنده کمک کنید.

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

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

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

نمونه کد زیر نحوه تعریف ناحیه نوشتن و استفاده از یک شی RecognitionContext برای تعیین پیش زمینه را نشان می دهد.

سویفت

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

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

برخورد با اشکال مبهم

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

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