Ludovic ROLAND

Blog technique sur mes expériences de développeur.

Android : De la reconnaissance d'images avec Moodstocks

5 septembre 2014

Je vous propose aujourd’hui un article dans lequel nous allons monter un projet utilisant la bibliothèque Moodstocks. Cette bibliothèque très impressionnante vous permet de mettre en place de la reconnaissance d’images dans vos applications Android, iOS ou encore Phonegap.

C’est à la partie Android que nous allons nous intéresser aujourd’hui.

Présentation de Moodstocks

Comme je vous le disais dans l’introduction de cet article, Moodstocks est une bibliothèque très impressionnante qui permet de mettre en place de la reconnaissance d’images dans des applications mobiles. Cette bibliothèque est d’autant plus intéressante et impressionnante car :

  • son utilisation est gratuite dans certains cas d’utilisation ;
  • la reconnaissance des images est d’une rapidité hallucinante ;
  • elle fonctionne en mode “off-line”.

Au cours de cet article, nous allons bien évidemment manipuler la bibliothèque dans le cadre d’une application Android, mais avant ça, nous allons voir comment configurer les images à reconnaître grâce au back-end et un petit logiciel proposé par les créateurs de cette solution logicielle.

Inscription et mise en place des images à reconnaître

L’inscription et la création d’une application

Avant toute chose, nous allons nous créer un compte sur le site de Moodstocks. Une fois votre compte validé, vous allez être invité à créer une application :

Le tableau de bord

Une fois l’application créée, vous devriez arriver sur le tableau de bord :

Comme vous pouvez le voir dans la capture d’écran ci-dessus, le tableau de bord vous permet de connaître les informations relatives à votre application comme par exemple :

  • le nombre d’image que vous pouvez mettre comme reconnaissables (10 dans mon cas) ;
  • le nombre de reconnaissances qu’il est possible de faire par mois (1000 dans mon cas) ;
  • les identifiants de votre application.

A tout moment, vous pouvez bien évidemment choisir de changer de plan pour un plus adapté à vos besoins !

Les images à reconnaître

Nous allons maintenant définir quelques images que notre application Android pourra reconnaître. Pour ça, il convient de charger sur les serveurs de Moodstocks les images.

Pour charger ces images sur les serveurs de Moodstocks, nous allons passer par un petit outil appelé Desktop uploader téléchargeable gratuitement depuis le tableau de bord de votre application :

Téléchargez puis installez la version du logiciel correspondant à votre système d’exploitation. Une fois l’installation terminée, ouvrez le logiciel :

Saisissez les codes donnés dans le tableau de bord de l’application puis glissez les images que vous souhaitez que votre application puisse reconnaître. Une fois terminé, cliquez sur synchronize :

A noter que vous pouvez personnaliser les identifiants des images en faisant un clic droit puis en choisissant Edit image ID.

L’application Android

Maintenant que la partie serveur est en place nous allons pouvoir entamer le développement de l’application Android. Commencez par créer une application Android classique.

Les bibliothèques

Avant de commencer à manipuler Moodstocks, nous allons intégrer les bibliothèques nécessaires à son fonctionnement dans notre projet Android. Pour ça, rendez-vous sur la page downloads du site et téléchargez la version correspondant à votre IDE. Dans mon cas, il s’agit d’Eclipse.

Ouvrez l’archive et découvrez le contenu du dossier libs :

Déplacez alors ce contenu dans le dossier libs de votre projet Android (que vous soyez sous Eclipse ou sous Android Studio le principe est le même) et n’oubliez pas d’ajouter les fichier jar au Build Path :

Le manifest

Afin que notre application puisse fonctionner sans problème, nous allons devoir ajouter quelques informations à notre fichier AndroidManifest.xml à savoir :

  • une demande d’accès à internet pour pouvoir synchroniser les informations des images à reconnaître avec le serveur ;
  • une demande d’accès à la caméra pour pouvoir l’utiliser pour scanner les objets ;
  • une demande d’accès à l’écriture sur la carte SD pour pouvoir persister les informations.

En plus de ces permissions, Moodstocks nous conseille d’ajouter quelques informations relatives à l’utilisation de la caméra sur le téléphone.

Voici ce à quoi doit ressembler votre fichier AndroidManifest.xml :

<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  package="fr.rolandl.blog.moodstocks"
  android:versionCode="1"
  android:versionName="1.0"
>

  <uses-sdk
    android:minSdkVersion="8"
    android:targetSdkVersion="19"
  />
    
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

  <uses-feature
    android:name="android.hardware.camera"
    android:required="false"
  />

  <uses-feature
    android:name="android.hardware.camera.any"
    android:required="false"
  />

  <application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme"
  >
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name"
    >
      <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>

La synchronisation avec le serveur

Maintenant que les bibliothèques ont été ajoutés et que notre AndroidManifest.xml est à jour, nous allons pouvoir passer à l’étape suivante à savoir la synchronisation avec le serveur.

Pour faciliter les choses, je vous propose de créer une nouvelle activité Splashscreen.java qui servira comme son nom semble l’indiquer de splaschscreen et qui s’occupera de synchroniser les informations du serveur avec l’application au démarrage de l’application.

Créez donc une activité :

public final class Splashscreen
    extends Activity
{

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_splash);
  }

}

Son layout est également très simple puisqu’il s’agit simplement d’uneRelativeLayout :

<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
/>

Finalement, il convient de déclarer notre Activité dans le fichier AndroidManifest.xml comme l’activité à lancer au démarrage :

<activity
  android:name=".Splashscreen"
  android:label="@string/app_name"
>
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>

Dans la méthode onCreate de notre Activité Splashscreen, nous allons afficher une ProgressDialog afin de montrer aux utilisateurs que l’application travaille :

ProgressDialog.show(this, "", "Chargement", true);

Suite à l’affichage de cette ProgressDialog,toujours dans la méthode onCreate, nous allons lancer la synchronisation de l’application avec le serveur de Moodstocks :

Voici donc ce que vous devriez avoir :

@Override
public void onCreate(Bundle savedInstanceState)
{
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_splash);

  ProgressDialog.show(this, "", "Chargement", true);
  
  //TODO : écrire la synchro ici.
}

La première étape consiste à récupérer un objet Scanner via la méthode statique get() de la classe Scanner :

final Scanner scanner = Scanner.get();

La deuxième étape consiste à préciser où est-ce que l’on souhaite que les données relatives à Moodstocks soient stockées. Pour cela, il convient d’utiliser la méthode statique pathFromFilesDir de la classe Scanner :

final String path = Scanner.pathFromFilesDir(Splashscreen.this, "scanner.db");

La troisième étape consiste à ouvrir le fameux objet Scanner et de préciser les informations relatives à votre compte en précisant votre Api key et votre Api secret :

scanner.open(path, "key", "secret");

La quatrième étape consiste à associer un SyncListener au Scanner. Les différentes callbacks de l’interface seront alors appelées pendant la synchronisation, ce qui nous permettra éventuellement de faire des retours visuels à l’utilisateur :

scanner.setSyncListener(new SyncListener()
{

  @Override
  public void onSyncStart()
  {
  }

  @Override
  public void onSyncProgress(int arg0, int arg1)
  {
  }

  @Override
  public void onSyncFailed(MoodstocksError arg0)
  {
  }

  @Override
  public void onSyncComplete()
  {
  }

});

A noter que votre activité peut également implémenter l’interface !

Dans notre cas, nous allons considérer que tout va bien se passer. Nous pouvons dès maintenant compléter la méthode onSyncComplete() en lançant l’activité suivante de notre application :

@Override
public void onSyncComplete()
{
  startActivity(new Intent(Splashscreen.this, MainActivity.class));
  finish();
}

Finalement, la dernière étape consiste à lancer la synchronisation :

scanner.sync();

Voici alors ce que vous devriez avoir si votre Activité implémente directement l’interface :

public final class Splashscreen
    extends Activity
    implements SyncListener
{

  private ProgressDialog progressDialog;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_splash);

    progressDialog = ProgressDialog.show(this, "", "Chargement", true);

    try
    {
      final Scanner scanner = Scanner.get();
      final String path = Scanner.pathFromFilesDir(Splashscreen.this, "scanner.db");
      scanner.open(path, "lybmqf4irvivfz7jbjg9", "5ngnP8gI0ocxL4zw");
      scanner.setSyncListener(this);
      scanner.sync();
    }
    catch (MoodstocksError exception)
    {
      exception.printStackTrace();
    }
  }

  @Override
  public void onSyncStart()
  {
    Log.d("Moodstocks", "onSyncStart");
  }

  @Override
  public void onSyncProgress(int total, int current)
  {
    final int percent = (int) ((float) current / (float) total * 100);
    Log.d("Moodstocks", "onSyncProgress : '" + percent + "' %");
  }

  @Override
  public void onSyncFailed(MoodstocksError exception)
  {
    Log.e("Moodstocks", "onSyncFailed", exception);
  }

  @Override
  public void onSyncComplete()
  {
    progressDialog.dismiss();
    startActivity(new Intent(Splashscreen.this, MainActivity.class));
    finish();
  }

}

Je vous laisse bien évidemment prendre soin de gérer correctement les différentes erreurs potentielles ;) !

La reconnaissance d’image

Maintenant que la synchronisation avec le serveur est en place, nous allons pouvoir attaquer la reconnaissance d’image à proprement parler.

La première chose à mettre en place est le layout de l’Activité ou du Fragment dans lequel vous souhaitez mettre en place la logique du scan. Dans mon cas, il s’agit d’un Fragment. Le layout est très simple et doit uniquement comporter une SurfaceView :

<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
>
  <SurfaceView
    android:id="@+id/surfaceView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
  />
</RelativeLayout>

Maintenant que le layout est en place, nous allons pouvoir nous attaquer au code-behind et pour commencer l’initialisation de la session de scan. Commencez par ajouter l’attribut suivant à votre Activité ou Fragment :

private AutoScannerSession autoScannerSession;

Nous allons maintenant instancier cet objet AutoScannerSession dans la méthode onCreateView du Fragment. Le constructeur de la classe AutoScannerSession accepte quatre paramètres :

  • une Activity ;
  • un Scanner ;
  • un Listener ;
  • une SurfaceView.

L’Activity, dans le cadre d’un Fragment se récupère très facilement via la méthode getActivity(). Le Scanner, nous l’avons vu dans l’étape précédente se récupère également très facilement via la méthode statique get(). La SurfaceView est celle contenue dans notre layout. Finalement, il ne nous manque que le paramètre Listener. Le mieux est que votre Activity ou votre Fragment implémente cette interface. Trois méthodes dont le nom est assez explicite doivent alors être surchargées :

@Override
public void onCameraOpenFailed(Exception exception)
{
}

@Override
public void onResult(Result result)
{
}

@Override
public void onWarning(String message)
{
}

Nous sommes donc maintenant en mesure d’instancier notre objet AutoScannerSession :

try
{
  autoScannerSession = new AutoScannerSession(getActivity(), Scanner.get(), this, (SurfaceView) rootView.findViewById(R.id.surfaceView));
}
catch (MoodstocksError exception)
{
  exception.printStackTrace();
}

Le Fragment complet quant à lui ressemble désormais à ça :

public static class PlaceholderFragment
    extends Fragment
    implements Listener
{

  private AutoScannerSession autoScannerSession;

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
  {
    View rootView = inflater.inflate(R.layout.fragment_main, container, false);

    try
    {
      autoScannerSession = new AutoScannerSession(getActivity(), Scanner.get(), this, (SurfaceView) rootView.findViewById(R.id.surfaceView));
    }
    catch (MoodstocksError exception)
    {
      exception.printStackTrace();
    }

    return rootView;
  }

  @Override
  public void onCameraOpenFailed(Exception exception)
  {
  }

  @Override
  public void onResult(Result result)
  {
  }

  @Override
  public void onWarning(String message)
  {
  }

}

Il est possible de configurer un peu notre instance de la classe AutoScannerSession. Par exemple le type d’images qui peuvent être reconnues :

autoScannerSession.setResultTypes(Result.Type.IMAGE | Result.Type.EAN8 | Result.Type.EAN13 | Result.Type.QRCODE | Result.Type.DATAMATRIX);

Nous allons également automatiser le lancement de la session de scan et son arrêt via les méthodes onResume et onPause du cycle de vie du Fragment ou de l’Activity :

@Override
public void onResume()
{
  super.onResume();

  if (autoScannerSession != null)
  {
    autoScannerSession.start();
  }
}

@Override
public void onPause()
{
  super.onPause();

  if (autoScannerSession != null)
  {
    autoScannerSession.stop();
  }
}

Nous allons donc maintenant pouvoir nous attarder sur la méthode onResult qui est automatiquement appelée quand une image a été reconnue. Via l’objet Result vous pouvez alors accéder à son identifiant. Par exemple, le morceau de code suivant affiche un message Toast avec l’identifiant de l’image :

@Override
public void onResult(Result result)
{
  Toast.makeText(getActivity(), result.getValue(), Toast.LENGTH_LONG).show();
}

A noter qu’une fois la méthode onResult appelé, la reconnaissance d’image n’est pas active. C’est à vous à appeler la méthode autoScannerSession.resume() pour continuer à scanner des images !

La démonstration

La démonstration va être brève et se limiter à une capture d’écran :

Commentaires