Classification d'images avec React Native et CoreML sur iOS

13 minutes de lecture

Classification d'images avec React Native et CoreML sur iOS

Retour 8 ans en arrière où cet ami non-développeur venait vous voir en sortant cette fameuse phrase :

« J’ai une idée d’application ! Elle permettrait de détecter les types de plantes à partir d’une photo ? Ça se fait facilement ? »

Question à laquelle vous répondiez par la négative, sans savoir que quelques années plus tard tout aurait bien changé… Hier réservé au plus aguerris d’entre nous, le machine learning (ML) est désormais à la porté de tout développeur grâce à l’apparition d’outils d’automatisation. Il est ainsi possible de créer des modèles afin de répondre à de nouvelles problématiques, difficilement solutionnables avec du code « traditionnel ».

Je vous propose aujourd’hui de développer une application permettant de reconnaitre des modèles de chaussures Vans.

Au programme :

    1. 🖼 Récolter des images pour entraîner son modèle (Google Images)
    1. 🧠 Entraîner et créer son modèle CoreML (Create ML)
    1. ✨ Créer notre module natif de prédiction (Xcode)
    1. 📸 Envoyer une photo via le bridge et faire la prédiction (React Native)

Modèle CoreML

CoreML est le framework ML d’Apple permettant entre autre d’intégrer des modèles au sein d’applications iOS. Il existe une multitude de frameworks pour créer des modèles, parmis eux TensorFlow, Keras, Caffe.

CoreML ne se place pas en concurrent face à ces frameworks mais vient en complément, permettant d’intégrer plus facilement des modèles TensorFlow ou Keras sur iOS. Il propose ainsi un outil de conversion vers le format .mlmodel utilisé par CoreML. Pour notre application, nous utiliserons directement l’outil Create ML disponible dans Xcode afin de générer notre modèle. Les outils sont de plus en plus faciles à utiliser : profitons-en !

Créer le dataset

Nous allons détecter les 4 modèles principaux de la marque Vans, à savoir :

Modèles de vans

La précision d’un modèle réside en grande partie dans la qualité du dataset : images, labels, bounding boxes, masques… Lors de l’entraînement d’un modèle, l’algorithme va essayer d’extraire des features pour chaque classe. Dans notre cas les features peuvent-être :

  • Chaussure haute + trait sur le côté pour la Sk8-hi
  • Chaussure basse + trait sur le côté pour la Old Skool
  • Chaussure basse sans trait pour la Era
  • Chaussure basse sans lacet (et souvent des damniers) pour la Slip-on

Il nous faut donc trouver un nombre de photos assez conséquent pour chaque modèle de Vans. En général, plus le nombre de photos est grand et les photos variées, plus le modèle sera performant. Il existe de nombreux dataset où vous pouvez récupérer des images tels que l’Open Images Dataset ou encore ImageNet.

Nous concernant, il est facile de se procurer des photos de Vans : Google Images fera l’affaire. En recherchant simplement vans sk8-hi, nous pouvons récupérer suffisament d’images afin de construire notre dataset puis répéter l’opération pour chaque modèle.

Il existe de nombreux outils pour télécharger automatiquement les images remontées par Google Images. J’utilise l’extension Firefox FirefoxGoogleImageDownloader afin de télécharger environ 400 photos par modèle en quelques clics :

Modèles de vans

Il est important d’avoir à peu près le même nombre d’images pour chaque classe. Nous avons désormais les images, passons à la création de notre modèle de classification.

Entraîner et créer son modèle

Il existe différentes manières de créer son modèle CoreML (Turicreate, Custom Vision, AutoML Vision). Pour notre part, nous allons utiliser l’outil fourni par Xcode, à savoir Create ML. Pour le lancer, ouvrez Xcode puis sélectionnez dans le menu :

Xcode > Open Developer Tools > Create ML

Une fois lancé, créez un nouveau projet de type Image Classifier. Donnez un nom à votre projet, un emplacement puis validez, l’écran suivant s’affiche alors :

Création d'un projet dans Create ML

Il nous faut maintenant fournir des images d’entraînement (Training Data) ainsi que des images de test (Testing Data).

  • Les Training Data représentent les images qui serviront pour entraîner le modèle.
  • Les Testing Data représentent les images de référence qui serviront à calculer la précision de prédiction de votre modèle. À la fin de chaque entraînement, Create ML effectura des prédictions sur ces images dont le type est connu, et pourra calculer un taux d’erreur. Ces images de test permettent également de mettre en évidence d’éventuelles régressions suite à l’ajout de nouvelles images.

Pour chaque modèle de chaussures, nous séparons les images en deux parties : 80% d’images d’entraînement et 20% d’images de test. Pour cela, organisez vos images dans des dossiers de la manière suivante :

Création d'un projet dans Create ML

Les noms des dossiers dans Training Data et Testing Data correspondent à nos classes (nos modèles de chaussures donc). Il ne vous reste plus qu’à sélectionner ces deux répertoires dans la section Data Inputs de Create ML (laissez la section Validation Data à auto).

Nous sommes prêts à entraîner notre modèle ! Mais avant, voyons les quelques réglages que nous offre Create ML :

  • Maximum iterations : plus le nombre sera élevé plus votre modèle sera précis (mais l’entraînement plus long). Augmenter le nombre d’itérations permet au modèle de mieux appréhender la complexité de vos images et cerner les différences entre les classes.
  • Augmentation Data : en cochant les options (Crop, Rotate, Blur…), vous pouvez demander à Create ML de générer automatiquement de nouvelles images à partir de celles de référence en appliquant des rotations par exemple. C’est une manière rapide et souvent efficace d’augmenter le nombre d’images de votre dataset « gratuitement ».

Il ne vous reste plus qu’à appuyer sur le bouton Train

Training

Laissez chauffer quelques minutes… Ding ! Votre modèle est prêt. L’onglet Output permet de le tester directement en sélectionnant une image. Ici la chaussure a été reconnue comme un modèle Old Skool avec un taux de confiance de 98% :

Training

Pour récupérer le fichier .mlmodel, glissez-déposez simplement l’icône Output (en haut à droite) sur votre bureau. Nous avons un modèle, certe perfectible, mais fonctionnel !

Utiliser son modèle avec React Native

L’étape suivante est d’utiliser le modèle au sein d’une application React Native. De par sa nature, un modèle CoreML fonctionne uniquement sous iOS, c’est pourquoi je détaillerai uniquement cette partie. Il existe différents moyens d’utiliser votre modèle au sein d’une application :

  • Via une API, dans ce cas le modèle est herbergé sur votre serveur et l’application ne fait qu’interroger cette API. Cette solution a le mérite de ne pas dépendre de la puissance du téléphone pour effectuer les calculs. Elle permet également de mettre à jour le modèle sans déployer de nouvelle version de l’app ;
  • Soit embarqué directement dans votre application. Cette dernière peut alors être utilisée sans connexion internet et sera très efficace avec un téléphone récent doté d’une puce ML.

C’est cette deuxième solution que nous allons choisir, car c’est bien entendu la partie React Native qui nous intéresse ici.

Commençons par créer une nouvelle application (version 0.61) :

npx react-native init VansDetectorApp

Une fois votre projet initialisé, passons à la création de notre module natif. Celui-ci se chargera de prédire le type de Vans en passant une photo à notre modèle précédemment créé.

Créer un module natif

Ouvrez le workspace de votre projet iOS dans Xcode (ios/VansDetector.xcworkspace). Par défaut les projets React Native sont en Objective-C. Afin de nous faciliter la vie, nous allons écrire notre module en Swift. Créez un nouveau fichier Detector.swift dans votre projet :

Swift

Après la validation, Xcode nous demande de créer un bridge entre Swift et Objective-C :

Would you like to configure an Objective-C bridging header?

Répondez par l’affirmative, cela permettra à notre fichier Objective-C d’exporter notre module Swift. Un fichier VansDetector-Bridging-Header.h est alors créé, ajoutez-y l’import suivant :

#import <React/RCTBridgeModule.h>

Ajoutons désormais le code de note module dans le fichier Detector.swift :

import Foundation
import UIKit
import CoreML

@objc(Detector)
class Detector: NSObject {
  @objc(detect:resolver:rejecter:)
  func detect(_ filePath: String, resolver resolve: RCTPromiseResolveBlock, rejecter reject:RCTPromiseRejectBlock) -> Void {
    resolve("hello")
  }
}

Notre module exposera une méthode detect prenant en argument (filePath) le chemin de l’image et retournant une promesse. Pour l’instant nous résolvons directement la promesse en appelant la callback resolve avec une chaîne de caractères.

Derrnière chose à faire : exposer notre module au bridge afin de le rendre accessible côté JavaScript. Pour cela, créez un fichier Objective-C nommé Detector.m :

#import "React/RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(Detector, NSObject)

RCT_EXTERN_METHOD(detect:(NSString *) filePath
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
@end

À ce stade, nous avons un module exposant une méthode detect accessible côté JavaScript. Testons-le !

Utiliser le module natif côté JS

Pour cela, il nous faut importer l’objet NativeModules contenant notre module Detector. Vous pouvez dès lors appeler la méthode detect :

import React, { useEffect, useState } from 'react';
import { SafeAreaView, ScrollView, Text, NativeModules } from 'react-native';
const App = () => {
  const [label, setLabel] = useState();

  useEffect(() => {
    const makePrediction = async () => {
      const detectedLabel = await NativeModules.Detector.detect('path/to/image');      setLabel(detectedLabel);
    };

    makePrediction();
  }, []);

  return (
    <SafeAreaView>
      <ScrollView style={{ padding: 20 }}>
        <Text>{label}</Text>
      </ScrollView>
    </SafeAreaView>
  );
};

export default App;

La promesse est bien validée avec la chaîne Hello :

Bridge

Pour plus d’informations sur la communication entre la partie native et JavaScript, je vous invite à lire la documentation de React Native. Notre composant natif fonctionne correctement, nous pouvons passer à l’étape suivante : le branchement de notre modèle.

Appeler le modèle CoreML

Commencez par importer le fichier mlmodel au sein de votre projet Xcode. Celui-ci va automatiquement générer une classe pour utiliser le modèle. Xcode génère la classe en Objective-C (car c’est la language de départ de notre projet). Nous pouvons lui indiquer de la générer au format Swift. Pour cela, rendez-vous dans l’onglet Build Settings, puis sélectionnez Swift dans la rubrique CoreML Model Class Generation Language :

Objective C

Afin de re-générer la classe, il est souvent nécessaire de lancer un build de votre application. Pour vérifier, cliquez sur le modèle dans votre projet, vous verrez alors des informations le concernant :

Objective C Bridge

Notre modèle est importé, notre classe générée, exploitons-le dans notre module Swift. Comme l’indique Xcode, le modèle accepte en entrée une image de 299px par 299px. Complétons notre méthode detect (explications en dessous) :

import Foundation
import UIKit
import CoreML

@objc(Detector)
class Detector: NSObject {
  @objc(detect:resolver:rejecter:)
  func detect(_ filePath: String, resolver resolve: RCTPromiseResolveBlock, rejecter reject:RCTPromiseRejectBlock) -> Void {

    let model = VansModel()

    let url = NSURL(string: filePath)
    let data = NSData(contentsOf: url! as URL)
    let image = UIImage(data: data! as Data)

    let size = CGSize(width: 299, height: 299)
    guard let buffer = image?.resize(to: size)?.pixelBuffer() else {
      fatalError("Scaling or converting to pixel buffer failed!")
    }

    guard let result = try? model.prediction(image: buffer) else {
      fatalError("Prediction failed!")
    }

    resolve(result.classLabel)
  }
}

1. Instanciation du modèle

La classe VansModel a été générée par Xcode et est instanciable de cette manière :

let model = VansModel()

2. Création de l’image à partir du chemin

Nous créons ensuite une image à partir du chemin passé côté JavaScript :

let url = NSURL(string: filePath)
let data = NSData(contentsOf: url! as URL)
let image = UIImage(data: data! as Data)

3. Transformation en CVPixelBuffer et redimensionnement

Afin de redimensionner notre image et la transformer en un buffer, nous allons récupérer une classe utilitaire ajoutant ces fonctionnalités à la classe UIImage. Créez un fichier UIImage+CVPixelBuffer.swift avec ce code. Nous pouvez maintenant appeler les méthodes resize et pixelBuffer:

let size = CGSize(width: 299, height: 299)
guard let buffer = image?.resize(to: size)?.pixelBuffer() else {
  fatalError("Scaling or converting to pixel buffer failed!")
}

4. Effectuer la prédiction

Enfin, nous appellons la méthode prediction de notre modèle puis résolvons notre promesse avec le label récupéré :

guard let result = try? model.prediction(image: buffer) else {
  fatalError("Prediction failed!")
}

resolve(result.classLabel)

Il nous reste à envoyer le chemin de notre photo côté JS.

Photo avec React Native

Nous allons utiliser la très bonne librairie react-native-image-picker afin de sélectionner ou prendre une photo avec la caméra :

yarn add react-native-image-picker

Plus besoin de link, nous utilisons la version 0.61 de React Native qui gère maintenant les dépendances natives iOS via CocoaPods :

$ cd ios && pod install

Enfin n’oubliez pas d’ajouter les droits nécessaires dans votre fichier Info.plist :

<plist version="1.0">
  <dict>
    ...
    <key>NSPhotoLibraryUsageDescription</key>
    <string>$(PRODUCT_NAME) would like access to your photo gallery</string>
    <key>NSCameraUsageDescription</key>
    <string>$(PRODUCT_NAME) would like to use your camera</string>
  </dict>
</plist>

Complétons notre code pour envoyer le chemin de l’image à notre module :

import React, { useState } from 'react';
import { SafeAreaView, ScrollView, Text, NativeModules, Button } from 'react-native';
import ImagePicker from 'react-native-image-picker';

const App = () => {
  const [label, setLabel] = useState();

  const handleOnPress = () => {
    ImagePicker.showImagePicker({}, async response => {
      if (!response.didCancel) {
        const detectedLabel = await NativeModules.Detector.detect(response.uri);
        setLabel(detectedLabel);
      }
    });
  };

  return (
    <SafeAreaView>
      <ScrollView style={{ padding: 20 }}>
        <Button title="Pick" onPress={handleOnPress} />
        <Text>{label}</Text>
      </ScrollView>
    </SafeAreaView>
  );
};

export default App;

Félicitation, votre app est désormais dôtée d’un mini cerveau 🧠✨.



L’ensemble du projet React Native est disponible sur ce repo GitHub.

Et ensuite ?

Nous avons vu comment communiquer avec la partie native iOS, vous pouvez utiliser les nombreuses fonctionnalités natives de la librairie CoreML telles que la détection de texte dans une images ou bien la reconnaissance de visage et landmarks. Il existe également des centaines de modèles open-sources aux fonctionnalités variées :

  • Détection de texte ;
  • Estimation de pose ;
  • Prédiction d’âge/sexe ;
  • Transfert de style artistique ;
  • Détection de nudité ;

Vous pouvez trouver une liste de modèles sur ce repo GitHub. Il est également possible de convertir plus ou moins facilement des modèles Keras ou Caffe grâce à la librairie coremltools (Python).

👋

Continuer la discussion sur Twitter