iOS에서 ML Kit를 사용한 디지털 잉크 인식

ML Kit의 디지털 잉크 인식을 사용하면 스케치를 분류할 수 있습니다.

사용해 보기

  • 샘플 앱을 사용하여 이 API의 사용 예를 참조하세요.

시작하기 전에

  1. Podfile에 다음 ML Kit 라이브러리를 포함합니다.

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.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()
}

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 애플리케이션의 필요에 따라 조정해야 합니다 이때 선분을 그릴 때 길이가 0인 선분이 소문자 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.");
                                }];
}

텍스트 인식 정확도를 개선하기 위한 도움말

텍스트 인식의 정확성은 언어에 따라 다를 수 있습니다. 정확성은 살펴봤습니다 디지털 잉크 인식은 다양한 종류의 쓰기 스타일을 처리하도록 훈련되었지만 사용자마다 다를 수 있습니다

다음은 텍스트 인식기의 정확성을 개선하는 몇 가지 방법입니다. 이러한 기법은 그림 이모티콘, 자동 그리기, 도형의 그리기 분류 기준에 적용되지 않습니다.

쓰기 영역

많은 애플리케이션에는 사용자 입력을 위한 쓰기 영역이 잘 정의되어 있습니다. 기호의 의미는 이는 텍스트가 포함된 쓰기 영역의 크기를 기준으로 한 크기에 의해 부분적으로 결정됩니다. 예를 들어 소문자와 대문자 'o'의 차이를 보면 또는 'c', 쉼표와 a 슬래시를 사용합니다.

인식기에 쓰기 영역의 너비와 높이를 알려주면 정확성을 개선할 수 있습니다. 하지만 인식기는 쓰기 영역에 한 줄의 텍스트만 포함되어 있다고 가정합니다. 만약 쓰기 영역이 사용자가 두 줄 이상을 쓸 수 있을 만큼 충분히 크면 더 높이에 대한 최적의 추정치인 높이가 있는 WriteArea를 전달하여 결과를 줄일 수 있습니다 인식기에 전달하는 WriteArea 객체는 화면의 실제 쓰기 영역과 정확히 일치합니다. 이런 방식으로 WriteArea 높이를 변경 특정 언어에서 더 나은 결과를 얻을 수 있습니다

쓰기 영역을 지정할 때 쓰기 영역의 너비와 높이를 획과 같은 단위로 지정합니다. 좌표입니다. x,y 좌표 인수에는 단위 요구사항이 없습니다. API는 따라서 중요한 것은 획의 상대적 크기와 위치뿐입니다. 언제든지 시스템에 적합한 배율로 좌표를 전달할 수 있습니다.

사전 맥락

사전 컨텍스트는 개발자가 작성한 Ink에서 획 바로 앞에 오는 텍스트입니다. 인식할 수 있습니다. 사전 컨텍스트에 관해 알려주면 인식기에 도움이 될 수 있습니다.

예를 들어 필기체 'n'은 및 'u' 서로 착각하는 경우가 많습니다 사용자가 단어 'arg'의 일부만 입력한 경우 다음과 같이 인식될 수 있는 획으로 계속될 수 있습니다. 'ument' 또는 'nment'입니다. 사전 컨텍스트 'arg' 지정 는 "인수" 가 'argnment'보다 가능성이 높습니다.

또한 사전 컨텍스트는 인식기가 단어 사이의 공백인 단어 구분을 식별하는 데 도움이 될 수 있습니다. 다음과 같은 작업을 할 수 있습니다. 공백 문자를 입력하되 그릴 수 없는 경우 인식기가 한 단어가 끝나는 시점을 어떻게 판단할 수 있을까요? 다음 질문이 시작될까요? 사용자가 이미 'hello'라고 작성한 경우 쓴 단어로 이어지며 'world'를 지정하면 사전 컨텍스트가 없으면 인식기가 'world' 문자열을 반환합니다. 그러나 'hello'를 호출하는 경우 모델은 ' 'hello'라고 되어 있으므로 World" 'helloword'보다 더 의미가 있습니다.

문맥에 맞는 사전 문자열을 최대한 길게(영문 기준 최대 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개 결과의 점수는 가까워집니다. 또한 도형 분류기는 전체 Ink를 단일 도형입니다. 예를 들어 Ink에 직사각형과 각 인식기는 둘 중 하나 (또는 완전히 다른 것)를 단일 인식 후보가 두 개의 셰이프를 나타낼 수 없기 때문입니다.