Utiliser et appliquer l'algorithme SIFT sur une image avec Android

Jonathan Odul

Cet article aura pour objectif de voir comment utiliser et appliquer l'algorithme SIFT pour la recherche de points-clés sur une image dans l'environnement Android. SIFT (Scale-invariant feature transform) est un algorithme de vision assistée par ordinateur permettant de détecter et décrire des zones d'intérêt dans une image. Pour plus d'information, et si ce n'est pas déjà fait, je vous prie d'aller jeter un coup d'œil sur cette page relatant de l'utilité et de l'intérêt de l'algorithme SIFT. L'objectif ici sera de créer une application fonctionnant sur toute plate-forme Android, permettant à l'utilisateur de prendre une photo avec la caméra de l'appareil, puis d'en extraire et de dessiner dessus les différents points-clés obtenus grâce à l'algorithme SIFT.

Commentez ♪

- Titre : Utiliser et appliquer l'algorithme SIFT sur une image avec Android

- Auteur : Jonathan Odul

- Parution : 8 août 2011

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2011 Jonathan Odul. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. SIFT

- II. JavaSIFT

- III. Mise en place du projet

- IV. Création de l'interface

- V. Lancer la caméra et récupérer la photo

- VI. Appliquer l'algorithme SIFT

- VII. Performance

- VIII. Pour continuer…

- IX. Remerciements

I - SIFT

SIFT (Scale-invariant feature transform) est un algorithme de vision assistée par ordinateur permettant de détecter et décrire des zones d'intérêts dans une image. Cet algorithme a été publié par David Lowe en 1999, et le propriétaire du brevet est l'Université de la Colombie-Britannique (en anglais, University of British Columbia, UBC). Cet algorithme inclut la reconnaissance d'objets et de mouvements, de la modélisation 3D, et du suivi vidéo. Il est aussi utilisé dans la robotique pour la gestion des déplacements par visualisation. Cet algorithme consiste à rechercher des points caractéristiques (appelés « features ») sur une photo qui seront décrits chacun par des coordonnées (x et y), une orientation, une échelle, ainsi que 128 descripteurs. L'idée générale de SIFT est donc de trouver des points-clés qui sont invariants à plusieurs transformations : rotation, échelle, illumination et changements mineurs du point de vue.

Aperçu des différents points-clés d'une photo

À noter qu'il s'agit ici non seulement de détecter, mais aussi de caractériser par des valeurs pour pouvoir reconnaitre (mettre en correspondance) par la suite ces zones ou points d'intérêt dans d'autres images de la même scène. Cet algorithme a eu un succès très important au sein de la communauté vision. Ainsi, ceci pourrait, par exemple, servir pour un programme de reconnaissance de bâtiments. En effet, si deux mêmes bâtiments sont pris en photo, certains points-clés retrouvés sur les deux photos seront concordants et nous permettront d'affirmer que celles-ci représentent le même bâtiment. De plus, le gros avantage de l'algorithme SIFT est, comme dit précédemment, qu'il reste invariant à l'échelle, par un éventuel changement d'angle de vue (± 30°), ainsi qu'à un changement de luminosité pouvant différer entre deux photos d'un même bâtiment.

Représentation des points concordants par SIFT

II - JavaSIFT

Après la sortie de l'article de David Lowe, l'algorithme SIFT a été retranscrit dans de nombreux langages de programmation. JavaSIFT, comme son nom l'indique, a été écrit en Java et sous licence GNU GPL par Stephan Saalfeld (http://fly.mpi-cbg.de/~saalfeld/javasift.html).

Cependant, JavaSIFT a été développé pour être utilisé en tant que plugin pour le programme ImageJ, logiciel de traitement et d'analyse d'images (calcul d'histogramme, transformation de Fourier…) et permettant l'ajout de nouvelles fonctions grâce à un système de plugins (plus d'information à propos d'ImageJ à cette adresse : http://rsbweb.nih.gov/ij/). Le plugin JavaSIFT permettait donc d'appliquer l'algorithme sur une image importée dans l'interface d'ImageJ, ce qui le rendait ainsi dépendant de ce logiciel, car en effet, le code source de JavaSIFT nécessitait les librairies, et fonctions proposées par ImageJ pour la gestion des ressources, ce qui rendait impossible l'intégration de cette librairie directement dans un projet quelconque sans l'utilisation auxiliaire d'ImageJ.

Les modifications que j'ai alors apportées au code ont permis de le rendre totalement indépendant de la bibliothèque d'ImageJ, afin d'être utilisable sous forme de librairie externe (jar) depuis n'importe quel projet Java.

III - Mise en place du projet

La première étape consiste, bien évidemment, à créer un nouveau projet sous Eclipse pour la plate-forme Android et qu'on nommera SIFT Camera. La version du SDK importe peu ici, sachant que même la version minimum est suffisante. Cependant, il est nécessaire de générer avec, une Activity qu'on appellera CameraActivity.
Afin de pouvoir utiliser SIFT, le projet requiert la librairie téléchargeable ci-dessous. Ainsi faut-il l'attacher au projet afin de pouvoir accéder aux différentes méthodes qu'elle propose.

javasift.jar (Miroir)

Voici donc à quoi devrait ressembler l'architecture du projet :

IV - Création de l'interface

L'interface du projet restera extrêmement simpliste et se résumera à un layout composé d'une ImageView, prenant toute la surface de l'écran, et servant à afficher la photo prise.

Voici le code du fichier main.xml présent dans le dossier res/layout:

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:gravity="center">
<ImageView android:layout_width="fill_parent"
android:layout_height="fill_parent" android:id="@+id/view" android:keepScreenOn="true"/>
</LinearLayout>

L'ImageView aura pour identifiant view.

Nous allons aussi mettre en place un menu composé d'un élément qui permettra à l'utilisateur de lancer la caméra pour prendre une photo.

Créez donc un fichier menu.xml que vous placerez dans res/menu; et placez-y ce code :

menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="Camera" android:icon="@drawable/ic_menu_camera" android:id="@+id/camera"></item>
</menu>

Pour finir avec cette section, on va faire aussi en sorte de fixer l'application en mode portrait afin d'éviter tout relancement intempestif de l'Activity durant un changement d'orientation du téléphone, ce qui aurait pour conséquence d'annuler toute action en cours (la recherche des points-clés dans notre cas).

Pour cela, il suffit d'ajouter dans le fichier AndroidManifest.xml dans la balise correspondant à notre Activity :

android:screenOrientation="portrait"

Donnant ainsi :

À l'intérieur de AndroidManifest.xml

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

V - Lancer la caméra et récupérer la photo

Dans ce chapitre, nous allons mettre en place la base du programme qui permettra à l'utilisateur, dans un premier temps, de pouvoir afficher le menu, prendre une photo, et afficher la photo dans l'ImageView. Pour cela, il faudra surcharger trois méthodes dans l'Activity principal :

- public boolean onCreateOptionsMenu(Menu menu) : affiche le menu quand l'utilisateur appuiera sur le bouton menu ;

- public boolean onOptionsItemSelected(MenuItem item) : lance la caméra quand l'utilisateur aura sélectionné l'élément du menu ;

- protected void onActivityResult(int requestCode, int resultCode, Intent data) : récupère la photo prise renvoyée en retour de la caméra.

CameraActivity.java

public class CameraActivity extends Activity {

private static final int PICTURE_RESULT = 9;

private Bitmap mPicture;
private ImageView mView;

public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);

mView = (ImageView) findViewById(R.id.view);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.camera:
Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
CameraActivity.this.startActivityForResult(camera, PICTURE_RESULT);
return true;
default:
return super.onOptionsItemSelected(item);
}
}

public void onDestroy() {
super.onDestroy();
if (mPicture != null) {
mPicture.recycle();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);

// if results comes from the camera activity
if (requestCode == PICTURE_RESULT) {

// if a picture was taken
if (resultCode == Activity.RESULT_OK) {
// Free the data of the last picture
if(mPicture != null)
mPicture.recycle();

// Get the picture taken by the user
mPicture = (Bitmap) data.getExtras().get("data");

// Avoid IllegalStateException with Immutable bitmap
Bitmap pic = mPicture.copy(mPicture.getConfig(), true);
mPicture.recycle();
mPicture = pic;

// Show the picture
mView.setImageBitmap(mPicture);

// if user canceled from the camera activity
} else if (resultCode == Activity.RESULT_CANCELED) {

}
}
}
}

Ainsi, à cette étape, notre application permet à l'utilisateur de prendre une photo et de l'afficher dans l'interface.

Comme dans la suite nous allons dessiner sur l'image, celui-ci devra être dans l'état mutable. Or l'image renvoyée par la caméra se trouve être dans un état immutable, ce qui provoquera une exception lorsqu'on tentera de dessiner par-dessus. D'où la copie de l'image dans une autre variable afin de la rendre mutable.

VI - Appliquer l'algorithme SIFT

En jetant un coup d'œil dans la documentation de la librairie SIFT, on constate que la méthode qui nous intéresse est celle-ci :

static java.util.Vector<Feature> SIFT.getFeatures(int w, int h, int[] pixels);

Ainsi, la méthode prend en paramètres la largeur et la hauteur de l'image, ainsi qu'un tableau d'entiers représentant la couleur pour chaque pixel. Or nous possédons une instance de la classe Bitmap pour représenter l'image, nous devons ainsi procéder à l'écriture d'une fonction permettant la conversion d'un type Bitmap vers un tableau de pixels :

Conversion de Bitmap vers tableau de pixels

private int[] toPixelsTab(Bitmap picture) {
int width = picture.getWidth();
int height = picture.getHeight();

int[] pixels = new int[width * height];
// copy pixels of picture into the tab
picture.getPixels(pixels, 0, picture.getWidth(), 0, 0, width, height);

// On Android, Color are coded in 4 bytes (argb),
// whereas SIFT needs color coded in 3 bytes (rgb)

for (int i = 0; i < (width * height); i++)
pixels[i] &= 0x00ffffff;

return pixels;
}

Maintenant, les choses vont se compliquer un peu plus. En effet, l'algorithme SIFT étant assez lent, il est indispensable d'afficher durant le traitement un dialogue demandant à l'utilisateur de patienter, et ceci pour diverses raisons :

- parce que c'est une règle dans la création d'IHM, c'est-à-dire d'afficher à l'utilisateur qu'un traitement est en cours et ainsi le rassurer dans le fait que l'application est bien en train d'agir et de faire quelque chose en fond ;

- sans ça, cela provoquerait un blocage de l'interface durant le traitement, qui pourrait avoir comme conséquence l'affichage d'une alerte provenant du système informant que le programme ne répond plus.

Pour répondre à ce besoin, on utilisera la classe ProgressDialog afin d'alerter qu'un traitement est en cours. Elle sera affichée lors de la réception de la photo, puis nous instancierons un nouveau thread afin de nous occuper du traitement de l'image en tâche de fond. Pour finir, afin de gérer la communication entre le thread UI, et le thread de traitement, on utilisera la classe Handler, cela afin que le thread UI puisse être alerté du résultat du traitement (erreur ou succès) et d'arrêter l'affichage du ProgressDialog.

Cet article n'ayant pas pour but d'expliquer le fonctionnement et l'utilité de la classe Handler, référez-vous à la documentation Android pour plus d'information.

Nous allons donc créer plusieurs constantes qui serviront à l'envoi de messages au Handler.

À la fin du thread de traitement, trois choses peuvent avoir pu se passer :

- l'algorithme s'est bien déroulé ;

- il y a eu un dépassement de mémoire ;

- il y a eu une erreur quelconque.

Pour chacun de ces évènements, on attribuera une constante :

Constantes

private static final int OK = 0;
private static final int MEMORY_ERROR = 1;
private static final int ERROR = 2

On n'oublie pas de définir notre ProgressDialog en attribut :

Attribut

private ProgressDialog mProgressDialog;

Et enfin la définition de notre Handler qui exécutera une certaine action selon le code reçu :

Handler

private final Handler mHandler = new Handler() {

@Override
public void handleMessage(Message msg) {
AlertDialog.Builder builder;

switch (msg.what) {
case OK:
// set the picture
mView.setImageBitmap(mPicture);
break;
case MEMORY_ERROR:
builder = new AlertDialog.Builder(CameraActivity.this);
builder.setMessage("Out of memory.\nPicture too big.");
builder.setPositiveButton("Ok", null);
builder.show();
break;
case ERROR:
builder = new AlertDialog.Builder(CameraActivity.this);
builder.setMessage("Error during the process.");
builder.setPositiveButton("Ok", null);
builder.show();
break;
}
mProgressDialog.dismiss();
}
};

En cas de succès, on rafraîchit l'image dans notre ImageView, car on verra par la suite qu'on dessinera dessus les différents points-clés que l'algorithme en aura extraits. En cas d'erreur, on prévient l'utilisateur. Puis dans tous les cas, on élimine le ProgressDialog.

Maintenant, nous allons attaquer la partie la plus intéressante : nous allons créer la méthode qui va se charger d'appliquer l'algorithme SIFT sur notre image. Elle va tout d'abord instancier notre ProgressDialog, puis créer le thread qui s'occupera d'appeler la méthode nécessaire, et d'avertir via un message le Handler en lui indiquant le résultat.

Traitement avec l'algorithme SIFT

private void processSIFT() {
// show the dialog
mProgressDialog = ProgressDialog.show(this, "Please wait",
"Processing of SIFT Algorithm...");

new Thread(new Runnable() {

@Override
public void run() {
Message msg = null;

try {
// convert bitmap to pixels table
int pixels[] = toPixelsTab(mPicture);

// get the features detected into a vector
Vector<Feature> features = SIFT.getFeatures(
mPicture.getWidth(), mPicture.getHeight(), pixels);

msg = mHandler.obtainMessage(OK);
} catch (Exception e) {
e.printStackTrace();
msg = mHandler.obtainMessage(ERROR);
} catch (OutOfMemoryError e) {
msg = mHandler.obtainMessage(MEMORY_ERROR);
} finally {
// send the message
mHandler.sendMessage(msg);
}
}

}).start();
}

Maintenant, on peut ajouter l'appel à cette méthode dans la méthode onActivityResult :

Appel de la méthode

if (resultCode == Activity.RESULT_OK) {

// Free the data of the last picture
if(mPicture != null)
mPicture.recycle();

// Get the picture taken by the user
mPicture = (Bitmap) data.getExtras().get("data");

// Avoid IllegalStateException with Immutable bitmap
Bitmap pic = mPicture.copy(mPicture.getConfig(), true);
mPicture.recycle();
mPicture = pic;

// Show the picture
mView.setImageBitmap(mPicture);

// process SIFT algorithm on the picture
processSIFT();

// if user canceled from the camera activity
} else if (resultCode == Activity.RESULT_CANCELED) {
// ...
}

Jusqu'ici, on a donc un programme qui permet à l'utilisateur de prendre une photo puis d'appliquer la recherche de points-clés dessus grâce à l'algorithme SIFT.

Cependant, on n'en voit pas le résultat. On va donc, pour finir, créer une méthode dessinant sur la photo un point-clé :

Représentation des points-clés

public void drawFeature(Canvas c, float x, float y, double scale,
double orientation) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

// line too small...
scale *= 6.;

double sin = Math.sin(orientation);
double cos = Math.cos(orientation);

paint.setStrokeWidth(2f);
paint.setColor(Color.GREEN);
c.drawLine((float) x, (float) y, (float) (x - (sin - cos) * scale),
(float) (y + (sin + cos) * scale), paint);

paint.setStrokeWidth(4f);
paint.setColor(Color.YELLOW);
c.drawPoint(x, y, paint);
}

Et d'appeler cette méthode pour chaque point-clé, après leur récupération dans le vecteur :

Appel de la méthode

// convert bitmap to pixels table
int pixels[] = toPixelsTab(mPicture);
// get the features detected into a vector
Vector<Feature> features = SIFT.getFeatures(
mPicture.getWidth(), mPicture.getHeight(), pixels);

// draw features on bitmap
Canvas c = new Canvas(mPicture);
for (Feature f : features) {
drawFeature(c, f.location[0], f.location[1], f.scale, f.orientation);

L'application est maintenant terminée. Ci-dessous vous trouverez des captures d'écran du programme en fonctionnement :

Code complet de l'Activity :

CameraActivity.java

package com.jidul.android.sift_camera;

import java.util.Vector;

import mpi.cbg.fly.Feature;
import mpi.cbg.fly.SIFT;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ImageView;

public class CameraActivity extends Activity {

private static final int PICTURE_RESULT = 9;

private static final int OK = 0;
private static final int MEMORY_ERROR = 1;
private static final int ERROR = 2;

private Bitmap mPicture;
private ImageView mView;

private ProgressDialog mProgressDialog;

public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);

mView = (ImageView) findViewById(R.id.view);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.camera:
Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
CameraActivity.this.startActivityForResult(camera, PICTURE_RESULT);
return true;
default:
return super.onOptionsItemSelected(item);
}
}

public void onDestroy() {
super.onDestroy();
if (mPicture != null) {
mPicture.recycle();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);

// if results comes from the camera activity
if (requestCode == PICTURE_RESULT) {

// if a picture was taken
if (resultCode == Activity.RESULT_OK) {

// Free the data of the last picture
if(mPicture != null)
mPicture.recycle();

// Get the picture taken by the user
mPicture = (Bitmap) data.getExtras().get("data");

// Avoid IllegalStateException with Immutable bitmap
Bitmap pic = mPicture.copy(mPicture.getConfig(), true);
mPicture.recycle();
mPicture = pic;

// Show the picture
mView.setImageBitmap(mPicture);

// process SIFT algorithm on the picture
processSIFT();

// if user canceled from the camera activity
} else if (resultCode == Activity.RESULT_CANCELED) {
}
}
}

private void processSIFT() {
// show the dialog
mProgressDialog = ProgressDialog.show(this, "Please wait",
"Processing of SIFT Algorithm...");

new Thread(new Runnable() {

@Override
public void run() {
Message msg = null;

try {
// convert bitmap to pixels table
int pixels[] = toPixelsTab(mPicture);

// get the features detected into a vector
Vector<Feature> features = SIFT.getFeatures(
mPicture.getWidth(), mPicture.getHeight(), pixels);

// draw features on bitmap
Canvas c = new Canvas(mPicture);
for (Feature f : features) {
drawFeature(c, f.location[0], f.location[1], f.scale,
f.orientation);
}

msg = mHandler.obtainMessage(OK);
} catch (Exception e) {
e.printStackTrace();
msg = mHandler.obtainMessage(ERROR);
} catch (OutOfMemoryError e) {
msg = mHandler.obtainMessage(MEMORY_ERROR);
} finally {
// send the message
mHandler.sendMessage(msg);
}
}

}).start();
}

private int[] toPixelsTab(Bitmap picture) {
int width = picture.getWidth();
int height = picture.getHeight();

int[] pixels = new int[width * height];
// copy pixels of picture into the tab
picture.getPixels(pixels, 0, picture.getWidth(), 0, 0, width, height);

// On Android, Color are coded in 4 bytes (argb),
// whereas SIFT needs color coded in 3 bytes (rgb)

for (int i = 0; i < (width * height); i++)
pixels[i] &= 0x00ffffff;

return pixels;
}

public void drawFeature(Canvas c, float x, float y, double scale,
double orientation) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

// line too small...
scale *= 6.;

double sin = Math.sin(orientation);
double cos = Math.cos(orientation);

paint.setStrokeWidth(2f);
paint.setColor(Color.GREEN);
c.drawLine((float) x, (float) y, (float) (x - (sin - cos) * scale),
(float) (y + (sin + cos) * scale), paint);

paint.setStrokeWidth(4f);
paint.setColor(Color.YELLOW);
c.drawPoint(x, y, paint);
}

private final Handler mHandler = new Handler() {

@Override
public void handleMessage(Message msg) {
AlertDialog.Builder builder;

switch (msg.what) {
case OK:
// set the picture with features drawed
mView.setImageBitmap(mPicture);
break;
case MEMORY_ERROR:
builder = new AlertDialog.Builder(CameraActivity.this);
builder.setMessage("Out of memory.\nPicture too big.");
builder.setPositiveButton("Ok", null);
builder.show();
break;
case ERROR:
builder = new AlertDialog.Builder(CameraActivity.this);
builder.setMessage("Error during the process.");
builder.setPositiveButton("Ok", null);
builder.show();
break;
}
mProgressDialog.dismiss();
}
};
}

Sources du projet :
android_sift_camera.zip (Miroir)

VII - Performance

Voici le détail des performances de l'algorithme selon différents modèles de téléphone :

- Samung Teos, Photo 3M pixels : 4 secondes ;

- Nexus S, Photo 3M pixels : 2 secondes.

VIII - Pour continuer…

La question que tout le monde se pose maintenant est : mais que faire de ces points-clés ?

Intéressons-nous à la javadoc contenue dans la librairie JAR, et notez cette méthode :

static java.util.Vector<PointMatch>    SIFT.createMatches(java.util.List<Feature> fs1, java.util.List<Feature> fs2, float max_sd, Model model, float max_id)

Cette méthode permet, à partir de deux vecteurs de points-clés calculés chacun sur deux images différentes par exemple, d'obtenir tous les points qui se correspondent entre eux. Pour comprendre à quoi les paramètres de cette méthode correspondent, reportez-vous à la documentation.

Exemple de de points concordants par SIFT

Avec tout cela, à vous de trouver les bonnes idées pour exploiter au mieux cet algorithme dans une application !

IX - Remerciements

Je tiens particulièrement à remercier ClaudeLELOUP pour sa relecture attentive, ainsi que yan et Feanorin pour leur relecture technique.