Reconnaître l'encre numérique avec ML Kit sur iOS

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

Grâce à la reconnaissance d'encre numérique de ML Kit, vous pouvez reconnaître du texte manuscrit sur une surface numérique dans des centaines de langues, et classer des dessins.

Essayer

Avant de commencer

  1. Incluez les bibliothèques ML Kit suivantes dans votre Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Après avoir installé ou mis à jour les pods de votre projet, ouvrez votre projet Xcode à l'aide de son fichier .xcworkspace. ML Kit est compatible avec Xcode version 13.2.1 ou ultérieure.

Vous êtes maintenant prêt à reconnaître du texte dans Ink objets.

Créer un objet Ink

La méthode principale pour créer un objet Ink consiste à le dessiner sur un écran tactile. Sur iOS, vous pouvez utiliser un UIImageView avec des gestionnaires d'événements tactiles qui tracent les traits à l'écran et stockent également les points des traits pour créer l'objet Ink. Ce schéma général est illustré dans l'extrait de code suivant. Consultez l'application de démarrage rapide pour obtenir un exemple plus complet, qui sépare la gestion des événements tactiles, le dessin d'écran et la gestion des données de trait.

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

Notez que l'extrait de code inclut un exemple de fonction permettant d'attirer le trait dans UIImageView, qui doit être adapté à votre application si nécessaire. Nous vous recommandons d'utiliser des majuscules pour dessiner les segments de ligne, de sorte que les segments de longueur nulle soient représentés par un point (pensez au point sur une lettre i minuscule). La fonction doRecognition() est appelée après chaque trait et sera définie ci-dessous.

Obtenir une instance de DigitalInkRecognizer

Pour effectuer la reconnaissance, nous devons transmettre l'objet Ink à une instance DigitalInkRecognizer. Pour obtenir l'instance DigitalInkRecognizer, nous devons d'abord télécharger le modèle de reconnaissance pour le langage souhaité, puis le charger dans la RAM. Pour ce faire, utilisez l'extrait de code suivant, qui est placé dans la méthode viewDidLoad() et utilise un nom de langage codé en dur : Consultez l'application de démarrage rapide pour découvrir comment présenter la liste des langues disponibles à l'utilisateur et télécharger la langue sélectionnée.

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

Les applications de démarrage rapide incluent du code supplémentaire qui montre comment traiter plusieurs téléchargements en même temps et comment déterminer quel téléchargement a abouti en gérant les notifications de fin.

Reconnaître un objet Ink

Nous arrivons ensuite à la fonction doRecognition(), qui est appelée touchesEnded() par souci de simplicité. Dans d'autres applications, il peut être judicieux d'appeler la reconnaissance uniquement après un délai d'inactivité ou lorsque l'utilisateur a appuyé sur un bouton pour déclencher la reconnaissance.

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

Gérer les téléchargements de modèles

Nous avons déjà vu comment télécharger un modèle de reconnaissance. Les extraits de code suivants montrent comment vérifier si un modèle a déjà été téléchargé ou comment le supprimer lorsqu'il n'est plus nécessaire pour récupérer de l'espace de stockage.

Vérifier si un modèle a déjà été téléchargé

Swift

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

Objective-C

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

Supprimer un modèle téléchargé

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

Conseils pour améliorer la reconnaissance de texte

La précision de la reconnaissance de texte peut varier selon les langues. La précision dépend également du style d'écriture. Bien que la reconnaissance d'encre numérique soit entraînée à gérer de nombreux types de styles d'écriture, les résultats peuvent varier d'un utilisateur à l'autre.

Voici quelques conseils pour améliorer la précision de la reconnaissance de texte. Notez que ces techniques ne s'appliquent pas aux classificateurs de dessin pour les emoji, les dessins automatiques et les formes.

Zone d'écriture

De nombreuses applications disposent d'une zone d'écriture bien définie pour les entrées utilisateur. La signification d'un symbole est partiellement déterminée par sa taille par rapport à la taille de la zone d'écriture qui le contient. Par exemple, la différence entre une lettre minuscule ou majuscule "o" ou "c", et une virgule par rapport à une barre oblique.

Indiquer à l'outil de reconnaissance la largeur et la hauteur de la zone d'écriture peut améliorer la précision. Toutefois, l'outil de reconnaissance suppose que la zone d'écriture ne contient qu'une seule ligne de texte. Si la zone d'écriture physique est suffisamment grande pour permettre à l'utilisateur d'écrire au moins deux lignes, vous pouvez obtenir de meilleurs résultats en indiquant une zone d'écriture avec une hauteur qui correspond à votre estimation de la hauteur d'une seule ligne de texte. L'objet WRITEArea que vous transmettez à l'outil de reconnaissance ne doit pas nécessairement correspondre exactement à la zone d'écriture physique à l'écran. Cette modification fonctionne mieux dans certaines langues que dans d'autres.

Lorsque vous spécifiez la zone d'écriture, spécifiez sa largeur et sa hauteur dans les mêmes unités que les coordonnées du trait. Les arguments de coordonnées x et y n'ont aucune exigence d'unité. L'API normalise toutes les unités. La seule chose qui compte est donc la taille et la position relatives des traits. Vous êtes libre de transmettre des coordonnées à l'échelle qui convient le mieux à votre système.

Pré-contexte

Le pré-contexte est le texte qui précède immédiatement les traits du Ink que vous essayez de reconnaître. Vous pouvez aider l'outil de reconnaissance en lui présentant le contexte.

Par exemple, les lettres cursives "n" et "u" sont souvent confondues. Si l'utilisateur a déjà saisi le mot partiel "arg", il peut continuer avec des traits qui peuvent être reconnus comme "ument" ou "nment". Spécifier le pré-contexte "arg" permet de résoudre l'ambiguïté, car le mot "argument" est plus susceptible de remplacer "argnment".

Le pré-contexte peut également aider l'outil de reconnaissance à identifier les sauts de mot, c'est-à-dire les espaces entre les mots. Vous pouvez saisir un espace, mais vous ne pouvez pas en tracer un. Comment un outil de reconnaissance peut-il déterminer quand un mot se termine et où le suivant commence ? Si l'utilisateur a déjà écrit "hello" et continue avec le mot "world", sans le contexte, l'outil de reconnaissance renvoie la chaîne "world". Toutefois, si vous spécifiez le pré-contexte "hello", le modèle renvoie la chaîne "world", avec un espace au début, car "hello world" est plus logique que "helloword".

Vous devez fournir la chaîne de pré-contexte la plus longue possible (jusqu'à 20 caractères, espaces compris). Si la chaîne est plus longue, l'outil de reconnaissance n'utilise que les 20 derniers caractères.

L'exemple de code ci-dessous montre comment définir une zone d'écriture et spécifier un pré-contexte à l'aide d'un objet 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);
                         }];

Ordre des traits

La précision de la reconnaissance est sensible à l'ordre des traits. Les reconnaissances s'attendent à ce que les traits soient saisis dans l'ordre que les gens écrivent naturellement (de gauche à droite pour l'anglais, par exemple). Tout cas qui part de ce modèle, par exemple l'écriture d'une phrase en anglais commençant par le dernier mot, donne des résultats moins précis.

Autre exemple : lorsqu'un mot au milieu d'un élément Ink est supprimé et remplacé par un autre mot. La révision est probablement au milieu d'une phrase, mais les traits de la révision sont à la fin de la séquence de trait. Dans ce cas, nous vous recommandons d'envoyer le nouveau mot séparément à l'API et de fusionner le résultat avec les reconnaissances précédentes à l'aide de votre propre logique.

Gérer les formes ambiguës

Dans certains cas, la signification de la forme fournie au programme de reconnaissance est ambiguë. Par exemple, un rectangle avec des bords très arrondis peut être considéré comme un rectangle ou une ellipse.

Ces cas peu clairs peuvent être gérés en utilisant des scores de reconnaissance lorsqu'ils sont disponibles. Seuls les outils de classification de formes fournissent des scores. Si le modèle est très confiant, le score du meilleur résultat sera nettement supérieur au deuxième meilleur résultat. En cas d'incertitude, les scores des deux premiers résultats seront proches. De plus, n'oubliez pas que les outils de classification de formes interprètent l'ensemble de l'élément Ink comme une seule forme. Par exemple, si Ink contient un rectangle et une ellipse l'un à côté de l'autre, la fonction de reconnaissance peut renvoyer l'un ou l'autre des résultats (ou un élément complètement différent), car un seul candidat de reconnaissance ne peut pas représenter deux formes.