Par facilité j'ai choisi de faire le traitement en déporté et en Java.
Le client se connecte au serveur de streaming mjpeg integré au robot qui lui fournit les images puis fait le traitement avant de lui renvoyer les ordres.


Je voici quelques points du programmes qui sont intéressant à regarder :

Capture du flux Mjpeg :


Cette classe analyse un flux mjpeg (conforme au protocole mjpeg AXIS) et en extrait les images jpeg.
Toutes les classes sont disponible sur le repository SVN dans le projet "JArobotTracking"
package psykokwak.jarobottracking.mjpeg;

import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class MjpegParser {
        private InputStream in;
        private boolean canceled = false;
        private MjpegParserListener listener;
        private Pattern segmentSizePattern;

        public boolean isCanceled() {
                return canceled;
        }

        public void setCanceled(boolean canceled) {
                this.canceled = canceled;
                if (canceled) {
                        try {
                                // TODO make this thread-safe
                                in.close();
                        } catch (IOException e) {
                        }
                }
        }

        public MjpegParser(InputStream in) {
                this.in = in;
        }

        private int readBlockBoundary() throws IOException{
                byte[] b = new byte[128];
                int v;
                int i = 0;
                while ((v = in.read()) != -1) {
                        b[i] = (byte)v;
                        if (i > 3 && b[i - 3] == '\r' && b[i - 2] == '\n' && b[i - 1] == '\r' && b[i] == '\n')
                                break;
                        if (i == 128) return 0;
                        i++;
                }
                String s = new String(b, 0, i);

                Matcher matcher = segmentSizePattern.matcher(s);
                if (!matcher.find(1)) return 0;

                String r = matcher.group(1);
                if (r == null) return 0;

                return Integer.parseInt(r);
        }


        public void parse() throws IOException, MjpegParserIncompleteSegmentException {
                byte[] b = new byte[1024 * 100];
                int n, s;

                segmentSizePattern = Pattern.compile("Content-Length: (\\d+)");

                while (!canceled) {
                        s = readBlockBoundary();

                        if (s == 0) throw new MjpegParserIncompleteSegmentException("Bad Boundary Header Found.");

                        n = 0;
                        do {
                                n += in.read(b, n, s - n);
                        } while(n < s);

                        if (n < 0 || n != s)
                                throw new MjpegParserIncompleteSegmentException("Bad Segment size.");

                        if (listener != null)
                                listener.segmentReady(b);

                        in.read(b, 0, 2);
                }              
        }

        public void setListener(MjpegParserListener listener) {
                this.listener = listener;
        }
}

package psykokwak.jarobottracking.mjpeg;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class MjpegStreaming implements Runnable{

        private String mjpgURL;
        private HttpURLConnection huc;
        private boolean connected = false;;
        private MjpegParser parser;
        private MjpegParserListener listener;

        public MjpegStreaming(MjpegParserListener listener) {
                this.listener = listener;
        }

        public void start(String address, int videoport) throws Exception {
                mjpgURL = "http://"+address+":"+videoport+"/video.mjpeg";
                connect();
                new Thread(this).start();
        }

        public void connect() throws IOException {
                URL u = new URL(mjpgURL);
                huc = (HttpURLConnection) u.openConnection();
                InputStream is = huc.getInputStream();
                connected = true;
                parser = new MjpegParser(is);
                parser.setListener(listener);
        }

        public void disconnect() {
                try {
                        if (connected) {
                                parser.setCanceled(true);
                                huc.disconnect();
                                connected = false;
                        }
                } catch (Exception e) {}
        }
       
        public  HttpURLConnection getSocket() {
                return huc;
        }

        @Override
        public void run() {     
                try {
                        parser.parse();
                } catch (Exception e) {
                        disconnect();
                }
        }
}

Il faut creer une interface qui sera instancié par la class qui souhaitera utiliser le parseur mjpeg :
package psykokwak.jarobottracking.mjpeg;

public interface MjpegParserListener {

        public void segmentReady(byte segment[]);
}

Pour utiliser le parseur mjpeg il suffit de creer une classe implémentant l'interface MjpegParserListener et d'instancier MjpegStreaming avec this en listener.
A chaque image reçu, la méthode segmentReady sera appelé.
Voici un exemple simple d'utilisation :
public class TestMjpeg implements MjpegParserListener {

        public static void main(String[] args) {
                TestMjpeg app = new TestMjpeg ();
                try {
                        mjpeg = getMjpegStreaming();
                        mjpeg.start("192.168.0.90", 8888);
                } catch (Exception e) {
                        System.out.println(e.getMessage());
                }
        }

        public MjpegStreaming getMjpegStreaming() {
                if (mjpeg == null)
                        mjpeg = new MjpegStreaming(this);
                return mjpeg;
        }
        @Override
        public void segmentReady(byte[] segment){
                BufferedImage image = null;
                try {
                        image = ImageIO.read(new ByteArrayInputStream(segment));
                        // Do some stuff...
                } catch (IOException e) {
                        System.out.println(e.getMessage());
                }
        }
}


Algorithmes de traitement d'image


Il y a en faite, plusieurs algorithmes utilisés successivement pour obtenir un résultat satisfaisant.
Avant ca, en JAVA, il faut travailler sur des BufferedImage mais les methodes getRGB() et setRGB() qui permettent respectivement d'obtenir la valeur RGB d'un pixel et de la modifier sont très lente. Il faut donc directement passer par la mémoire de la carte vidéo. Voici un moyen simple et environ 10 fois plus rapide qu'en utilisant getRGB() d'obtenir un tableau (d'int) correspondant à l'image :
// Si l'image n'est pas de type TYPE_INT_*, Il faut la dupliquer pour obtenir un databuffer d'int : chaque pixel est codé sous un int (4 octets) ou chaque octet correspond a une composante de couleur (alpha, rouge, vert, bleu).
BufferedImage newImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), this);
g.dispose();

// On récupère notre image sous forme d'un tableau. Ce tableau est une référence vers le buffer de l'image, toutes modification sur ce tableau modifiera l'image.
int[] binaryImage = ((DataBufferInt) newImage.getRaster().getDataBuffer()).getData();

// l'équivalent de getPixel(x, y)
int pixel = binaryImage[(y * height) + x];

// l'équivalent de setPixel(x, y, pixel);
binaryImage[(y * height) + x] = pixel;


La première chose a faire quand on a une image, c'est de la nettoyer.
Pour cela je fais la moyenne de chaque composante RGB pour chaque bloc de x pixel. L'idée est de passer de ceci à cela :

Toujours sur le tableau correspondant à l'image :
        public void segmentationImage(int[] image, int w, int h, int size) {       
                if (w * h != image.length) return;

                if (size < 2) return;

                for(int y = 0; y < h; y += size) {
                        for(int x = 0; x < w; x += size){
                                int c = getColorBloc(image, size, x, y, w, h);

                                for(int yy = y; yy < y + size; yy++) {
                                        for(int xx = x; xx < x + size; xx++) {
                                                image[(yy * w) + xx] = c;
                                        }
                                }                            
                        }
                }
        }
        public int getColorBloc(int[] image, int size, int x, int y, int w, int h) {
                int r = 0, g = 0, b = 0;
                for (int dx = x; dx < x + size; dx++)
                        for (int dy = y; dy < y + size; dy++) {
                                int pxl = image[(y * w) + x];
                                r += getRed(pxl);
                                g += getGreen(pxl);
                                b += getBlue(pxl);
                        }
                r /= size * size;
                g /= size * size;
                b /= size * size;

                return setPixel(0xFF, r, g, b);
        }

Ensuite on seuille l'image afin d'obtenir une image binaire, c'est à dire une image en noir et ... blanc.
Le blanc étant ce qu'on ne souhaite pas garder et le noir ce qu'on souhaite garder.

Ensuite on érode l'image afin de retirer tout ce qui est trop petit pour être un objet nous intéressant (le petit point noir en bas à gauche).

        public int[] erosionImage(int[] image, int w, int h, int size) {
                int[] dist = new int[image.length];
                Arrays.fill(dist, 0xFFFFFFFF);

                if (w * h != image.length)
                        return null;

                for(int y = size; y < h - size; y += size) {
                        for(int x = size; x < w - size; x += size) {

                                if(image[(y * w) + x] == 0xFF000000) {

                                        int gauche = image[(y * w) + (x - size)];
                                        int haut =   image[((y - size) * w) + x];
                                        int droite = image[(y * w) + (x + size)];
                                        int bas =    image[((y + size) * w) + x];

                                        if(!(
                                                        gauche == 0xFFFFFFFF || haut == 0xFFFFFFFF ||
                                                        bas == 0xFFFFFFFF || droite == 0xFFFFFFFF
                                        )){


                                                for(int yy = y; yy < y + size; yy++) {
                                                        for(int xx = x; xx < x + size; xx++) {
                                                                dist[(yy * w) + xx] = (gauche < haut) ? gauche : haut;
                                                        }
                                                }
                                        }
                                }
                        }       
                }
                return dist;
        }

Pour effectuer plusieurs érosions, il suffit de boucler sur cette fonction autant de fois que nécessaire.

Une fois qu'on a une image binaire qui nous convient, il faut repérer et compter les zones qui nous intéressent (ici il n'y en a qu'une).
On parcourt l'image à la recherche d'un pixel noir. Une fois trouvé on va le peindre ainsi que tous les pixels de la même zone qui se touchent d'une autre couleur. On va répéter cette opération pour chaque groupement de pixels avec à chaque fois une couleur différente.
        public ImageLabel floodFillRegionImage(int[] image, int x, int y, int w, int h, int size, int fill, int old, ImageLabel label) {
                int fillL, fillR, i;
                int in_line = 1;

                fillL = fillR = x;

                if (y < label.startY)
                        label.startY = y;
                if (y > label.endY)
                        label.endY = y;

                while(in_line != 0)
                {
                        for(int yy = y; yy < y + size; yy++)
                                for(int xx = fillL; xx < fillL + size; xx++)
                                        image[(yy * w) + xx] = fill;

                        if (fillL < label.startX)
                                label.startX = fillL;

                        fillL -= size;
                        in_line = (fillL < size) ? 0 : checkPixel(image[(y * w) + fillL], old);
                }
                fillL += size;

                in_line = 1;
                while (in_line != 0)
                {
                        for(int yy = y; yy < y + size; yy++)
                                for(int xx = fillR; xx < fillR + size; xx++)
                                        image[(yy * w) + xx] = fill;

                        if (fillR > label.endX)
                                label.endX = fillR;

                        fillR += size;
                        in_line = (fillR > w - size) ? 0 : checkPixel(image[(y * w) + fillR], old);
                }
                fillR -= size;

                for (i = fillL; i <= fillR; i += size)
                {
                        if ( y > size && checkPixel(image[((y - size) * w) + i], old) !=0 )
                                label = floodFillRegionImage(image, i, y - size, w, h, size, fill, old, label);

                        if ( y < h - size && checkPixel(image[((y + size) * w) + i], old) !=0 )
                                label = floodFillRegionImage(image, i, y + size, w, h, size, fill, old, label);

                }
                return label;
        }
        public Vector<ImageLabel> floodFillImage(int[] image, int w, int h, int size) {
                Vector<ImageLabel> labels = new Vector<ImageLabel>();
                int i = 0;

                for(int y = size; y < h - size; y += size) {
                        for(int x = size; x < w - size; x += size) {
                                if(image[(y * w) + x] == 0xFF000000) {
                                        labels.add(floodFillRegionImage(image, x, y, w, h, size, 0xFF000001 + i++, 0xFF000000, new ImageLabel()));
                                }
                        }
                }
                return labels;
        }

La methode floodFillImage() retourne un vecteur d'ImageLabels qui corresponds aux rectangles de chaque zones de l'image qui nous interresse.


Au final on obtient ce résultat :


Maintenant qu'on connait les objets qui nous intéressent et qu'on sait ou ils sont situé sur l'image, il suffit de pondérer les données puis de les renvoyer au robot qui recentrera la camera sur l'objet ciblé :)




Le code source complet du programme se trouve sur le repository SVN


Edit : Indice de performance pour 15fps
Testé sur un Intel Core 2 Duo 6600 : 25/30% CPU (sachant que la moitié est imputé à l'écriture de l'image vers l'affichage).