Club robotique de Sophia-Antipolis

Accueil > POBOTpedia > Lego Mindstorms > Découverte des Lego Mindstorms EV3 > Outil de génération d’images pour l’EV3

Outil de génération d’images pour l’EV3

dimanche 3 novembre 2013, par Eric P.

Dans un précédent article nous avons affiché une image personnalisée sur le LCD de l’EV3.

Comme on peut s’y attendre, l’EV3 ne fournit rien à la base pour interagir facilement avec l’afficheur et il faut donc passer par les fichiers virtuels /dev/lms_xxx, le frame buffer et toutes ces sortes de choses pour cela.

Heureusement, les auteurs de leJOS ont fait le travail difficile pour nous, et comme vous avez pu le constater en analysant le source de la démo incluse dans l’article précédent, afficher une image sur le LCD est très simple.

Oui mais...

...le format des données graphiques est assez spécial.

Pour faire court :

  • il s’agit d’images monochromes uniquement, le LCD de l’EV3 ne sachant pas faire autre chose
  • les données binaires contiennent les différents pixels ligne par ligne
  • comme ça aurait été trop simple sinon, chaque byte est inversé. Autrement dit, le bit de poids faible est celui qui correspond au pixel le plus à gauche du groupe de 8
  • les données d’une ligne sont paddées sur une frontière d’octet

Pour tout savoir dans le détail, un petit tour sur le forum leJOS s’impose.

C’est pas gagné :/

Heureusement, il y a Python...

... qui va nous permettre d’écrire un petit script fort pratique, capable de charger l’image depuis n’importe quel fichier graphique supporté par la librairie Python Imaging Library (aka PIL) [1] et de générer les données au format voulu.

Il est prévu deux options en termes de format de sortie :

  • un source Java qui définit une classe qui contient les données de l’image
  • un fichier ressource binaire que l’application peut charger à la demande

Le format du fichier ressource binaire est le suivant :

  • entête
    • largeur de l’image en pixels (2 bytes)
    • hauteur de l’image en pixels (2 bytes)
    • taille des données en bytes (4 bytes)
  • données de l’image

Les entiers sont au format big endian de manière à pouvoir être lus directement depuis un stream Java.

Et voilà le travail

#!/usr/bin/python
# -*- coding: utf8 -*-

import sys
import os
import argparse
import logging
import datetime
import math
from struct import pack
  
import Image
   
JAVA_CODE_PACKAGE = 'package %s;\n'
JAVA_CODE_PROLOG = '''import javax.microedition.lcdui.Image;
  
// Generated on %(date)s from file %(infile)s
  
public class %(classname)s {
    static public final Image image = new Image(%(width)d, %(height)d, new byte[]{
'''
JAVA_CODE_EPILOG = '''    });
}
'''
JAVA_CODE_BYTE = '(byte)0x%02x'
  
logging.basicConfig(
    stream=sys.stdout,
    level=logging.INFO,
    format='[%(levelname).1s] %(message)s'
)
  
class Converter(object):
    def run(self, run_args):
        try:
            method = getattr(self, 'export_as_' + run_args.output_format)
        except AttributeError:
            raise ConvertError('format not yet implemented : %s' % run_args.output_format)
        else:
            try:
                logging.info('Loading image from %s...', run_args.infile)
                img = Image.open(run_args.infile)
            except Exception as e:
                raise ConvertError(e)
            else:
                method(img, run_args)
                logging.info('Done.')
        
    def execute(self, img, emit_byte):
        img_w, img_h = img.size
        line_padcnt = img_w % 8 
  
        byte = 0
        for y in xrange(img_h):
            for x in xrange(img_w):
                byte = (byte >> 1) | (0x80 if img.getpixel((x, y)) > 0 else 0)
                if x % 8 == 7:
                    emit_byte(byte)
                    byte = 0
            if line_padcnt:
                emit_byte(byte >> line_padcnt)
                byte = 0
          
    def export_as_java(self, img, run_args):
        img_w, img_h = img.size
        line_padcnt = img_w % 8
        
        outfile = run_args.class_name + '.java'
        logging.info('Generating file %s...', outfile)
        out = open(os.path.join(run_args.output_dir, outfile), 'wt')
        
        try:
            if run_args.package_name:
                out.write(JAVA_CODE_PACKAGE % run_args.package_name)
            out.write(JAVA_CODE_PROLOG % {
                'date':datetime.datetime.now().isoformat(' ')[:-7],
                'infile':run_args.infile,
                'classname':run_args.class_name,
                'width':img_w,
                'height':img_h
                }
            )
        
            src = []
            self.execute(img, lambda b : src.append(JAVA_CODE_BYTE % b))
        
            lines = []
            for offs in xrange(0, len(src), 8):
                lines.append('        ' + ','.join(src[offs:offs+8]))
            out.write(',\n'.join(lines) + '\n')
            out.write(JAVA_CODE_EPILOG)
        
        finally:
            out.close()
             
    def export_as_resource(self, img, run_args):
        img_w, img_h = img.size
        line_padcnt = img_w % 8
        
        outfile = run_args.res_fname
        logging.info('Generating file %s...', outfile)
        out = open(os.path.join(run_args.output_dir, outfile), 'wb')
        
        try:
            lg = int(math.ceil(img_w * img_h / 8.))
            out.write(pack('>HHI', img_w, img_h, lg))
            self.execute(img, lambda b : out.write(chr(b)))
        
        finally:
            out.close()
    
class ConvertError(Exception):
    pass
  
if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Converts a graphic file into an EV3 image.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser.add_argument(
        '--class-name',
        help='the name of the generated class (if --output-format is "java")',
        dest='class_name',
        default='EV3Image'
    )
    parser.add_argument(
        '--resource-filename',
        help='the name of the generated resource file (if --output-format is "resource")',
        dest='res_fname',
        default='image.bin'
    )
    parser.add_argument(
        '--package',
        help='name of the parent package',
        dest='package_name'
    )
    parser.add_argument(
        '--output-format',
        help='output format selector',
        dest='output_format',
        choices=['java','resource'],
        default='java'
    )
    parser.add_argument(
        '--output-dir',
        help='output directory path',
        dest='output_dir',
        default='.'
    )
    
    parser.add_argument(
        help='input graphic file',
        dest='infile'
    )
    
    args = parser.parse_args()
    try:
        Converter().run(args)
    except ConvertError as e:
        logging.error(e)
        sys.exit(2)

Vous pourrez noter l’utilisation des lambda fonctions pour factoriser l’algorithme global dans la méthode execute tout en lui fournissant les fonctions d’émission des données produites correspondant au format généré.

Et le mode d’emploi ?

Pour le savoir, il suffit de demander ./make_ev3_image.py —help pour obtenir les informations attendues :

usage: make_ev3_image.py [-h] [--class-name CLASS_NAME]
                         [--resource-filename RES_FNAME]
                         [--package PACKAGE_NAME]
                         [--output-format {java,resource}]
                         [--output-dir OUTPUT_DIR]
                         infile

Converts a graphic file into an EV3 image.

positional arguments:
  infile                input graphic file

optional arguments:
  -h, --help            show this help message and exit
  --class-name CLASS_NAME
                        the name of the generated class (if --output-format is
                        "java") (default: EV3Image)
  --resource-filename RES_FNAME
                        the name of the generated resource file (if --output-
                        format is "resource") (default: image.bin)
  --package PACKAGE_NAME
                        name of the parent package (default: None)
  --output-format {java,resource}
                        output format selector (default: java)
  --output-dir OUTPUT_DIR
                        output directory path (default: .)

Difficile de faire plus clair non ?

Pour illustrer les choses, voici le fichier graphique qui a servi à générer la classe utilisée dans le programme de démonstration de l’article précédent :

Le fichier graphique de départ

Vous pouvez noter que l’affichage sur le LCD de l’EV3 reproduit fidèlement cette image.

Il n’y a plus qu’à placer le fichier généré dans le projet qui utilise l’image. Et pour savoir comment l’utiliser dans l’application hôte, reportez-vous à l’article précédent.

A noter que l’inclusion des données de l’image sous forme de ressource binaire diminue grandement la taille du jar résultat. Dans notre application exemple, le jar de la version avec l’image sous forme de classe Java pèse 9.6kb alors que celle avec l’image sous forme de ressource binaire n’en pèse que 3.7kb. Je soupçonne l’initialisateur statique du tableau de bytes d’être le coupable.

Et c’est là qu’on mesure l’intérêt du script Ant qui y est présenté : pour voir le résultat d’une modification de l’image il suffit juste de relancer la commande Run d’Eclipse ou de taper ant run en ligne de commande. Je vous avais bien dit que ça vous simplifierait la vie.

Have fun ;)


[1incluse dans la plupart des distributions Python

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.