ML Kit의 디지털 잉크 인식을 사용하면 디지털 표면에 필기된 텍스트를 수백 개의 언어로 인식하고 스케치를 분류할 수 있습니다.
사용해 보기
- 샘플 앱을 사용해 이 API의 사용 예를 확인해 보세요.
시작하기 전에
Podfile에 다음 ML Kit 라이브러리를 포함합니다.
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'프로젝트의 pod를 설치하거나 업데이트한 후
.xcworkspace를 사용하여 Xcode 프로젝트를 엽니다. ML Kit는 Xcode 13.2.1 이상 버전에서 지원됩니다.
이제 Ink 객체에서 텍스트 인식을 시작할 수 있습니다.
Ink 객체 빌드
Ink 객체를 빌드하는 기본 방법은 터치스크린에 그리는 것입니다. iOS에서는 화면에 획을 그리고 획의 점을 저장하여 Ink 객체를 빌드하는 터치 이벤트 핸들러와 함께 UIImageView를 사용할 수 있습니다. 이 일반적인 패턴은 다음 코드 스니펫에 나와 있습니다. 터치 이벤트 처리, 화면 그리기, 획 데이터 관리를 분리하는 더 완전한 예는 빠른 시작
앱을 참고하세요.
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'와 쉼표와 슬래시의 차이입니다.
인식기에 쓰기 영역의 너비와 높이를 알려주면 정확도를 높일 수 있습니다. 그러나 인식기는 쓰기 영역에 텍스트 한 줄만 포함되어 있다고 가정합니다. 실제 쓰기 영역이 사용자가 두 줄 이상을 쓸 수 있을 만큼 큰 경우 한 줄 텍스트의 높이를 가장 잘 추정한 높이로 WritingArea를 전달하면 더 나은 결과를 얻을 수 있습니다. 인식기에 전달하는 WritingArea 객체는 화면의 실제 쓰기 영역과 정확히 일치하지 않아도 됩니다. 이러한 방식으로 WritingArea 높이를 변경하는 것은 일부 언어에서 더 효과적입니다.
쓰기 영역을 지정할 때는 획 좌표와 동일한 단위로 너비와 높이를 지정합니다. x,y 좌표 인수에는 단위 요구사항이 없습니다. API는 모든 단위를 정규화하므로 중요한 것은 획의 상대적 크기와 위치뿐입니다. 시스템에 적합한 모든 스케일로 좌표를 전달할 수 있습니다.
이전 컨텍스트
이전 컨텍스트는 인식하려는 Ink의 획 바로 앞에 있는 텍스트입니다. 이전 컨텍스트에 관해 인식기에 알려주면 인식기를 도울 수 있습니다.
예를 들어 필기체 문자 'n'과 'u'는 종종 서로 혼동됩니다. 사용자가 이미 'arg'라는 부분 단어를 입력한 경우 'ument' 또는 'nment'로 인식될 수 있는 획을 계속할 수 있습니다. 'argument'라는 단어가 'argnment'보다 더 가능성이 높으므로 이전 컨텍스트 'arg'를 지정하면 모호성이 해결됩니다.
이전 컨텍스트는 인식기가 단어 구분, 단어 간의 공백을 식별하는 데도 도움이 될 수 있습니다. 공백 문자를 입력할 수는 있지만 그릴 수는 없으므로 인식기가 한 단어가 끝나고 다음 단어가 시작되는 시점을 어떻게 결정할 수 있을까요? 사용자가 이미 'hello'를 작성하고 이전 컨텍스트 없이 'world'라는 단어를 계속 작성하면 인식기는 'world'라는 문자열을 반환합니다. 그러나 이전 컨텍스트 'hello'를 지정하면 'helloword'보다 'hello world'가 더 말이 되므로 모델은 선행 공백이 있는 ' world'라는 문자열을 반환합니다.
공백을 포함하여 최대 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에 별도로 전송하고 자체 논리를 사용하여 결과를 이전 인식과 병합하는 것이 좋습니다.
모호한 도형 처리
인식기에 제공된 도형의 의미가 모호한 경우가 있습니다. 예를 들어 모서리가 매우 둥근 사각형은 사각형 또는 타원으로 볼 수 있습니다.
이러한 불분명한 사례는 인식 점수를 사용할 수 있는 경우 인식 점수를 사용하여 처리할 수 있습니다. 도형 분류기만 점수를 제공합니다. 모델이 매우 확신하는 경우 상위 결과의 점수가 두 번째로 좋은 결과보다 훨씬 더 좋습니다. 불확실성이 있는 경우 상위 두 결과의 점수가 비슷합니다. 또한 도형 분류기는 전체 Ink를 단일 도형으로 해석한다는 점에 유의하세요. 예를 들어 Ink에 사각형과 타원이 서로 옆에 있는 경우 단일 인식 후보가 두 도형을 나타낼 수 없으므로 인식기는 결과로 둘 중 하나 또는 완전히 다른 것을 반환할 수 있습니다.