Pour vos thumbnails, ImageMagick à la rescousse 24


ImageMagick est la solution de manipulation d’images la plus souple et versatile à disposition. Après pas mal de tests, Max en avait conclu qu’appeler la lib avec un bon subprocess était simplement plus rapide et donnait des résultats de meilleure qualité que d’utiliser PIL en Python ou GD en PHP.

Et je peux vous dire qu’il en a fait des tests pour obtenir le thumbnail des nichons parfait. C’est un perfectionniste quand il s’agit des tétons.

En général, on trouve ImageMagick dans les dépôts. Par exemple, sous debian :

sudo apt-get install imagemagick

Mais il n’est pas rare que Max compile la bête histoire d’avoir la même version partout :

wget http://mirror.checkdomain.de/imagemagick/ImageMagick-6.8.7-4.tar.gz ; tar -xvf ImageMagick-6.8.7-4.tar.gz ; cd ImageMagick-6.8.7-4
./configure
make && make install
ln -s /usr/local/bin/convert /usr/bin/convert
ln -s /usr/local/bin/identify /usr/bin/identify

Après tout ça, vous aurez accès à plusieurs commandes, qui permettent des manipulations d’images diverses.

Par exemple, identify permet de récupérer des informations sur l’image telles que le format, la profondeur des couleurs… Nous on l’utilise surtout pour récupérer la taille :

identify -format "%[fx:w],%[fx:h]" "chemin/vers/image"

On peut utiliser un wrapper tel que wand pour utiliser ça depuis Python. Mais il est très facile de se faire un wrapper vite fait à la main :

import envoy
from minibelt import normalize
 
class ImageMagicError(Exception):
    def __init__(self, result, encoding):
        msg = normalize(result.std_err.decode(encoding))
        super(ImageMagicError, self).__init__(msg)
 
def size(img, encoding=sys.stdout.encoding):
    r = envoy.run('identify -format "%[fx:w],%[fx:h]" "{}"'.format(img))
 
    if r.status_code != 0:
        raise ImageMagicError(r, encoding)
 
    w, h = r.std_out.strip().split(',')
    return int(w), int(h)

Notez que j’utilise minibelt et envoy (un wrapper pour subprocess), car je suis une grosse feignasse.

Pour faire un thumbnail, la recette est plus compliquée, car ImageMagick est à l’image, ce que Perl est à la programmation. There’s_more_than_one_way_to_do_it ©.

Au final, celui qui nous a donné le meilleur résultat en qualité est cette suite d’arcanes secrètes transmise d’oreille de druide à oreille de druide :

convert chemin/image/originale -thumbnail largeurxhauteur^ -gravity center -extent largeurxhauteur -quality 80 chemin/vers/resultat.jpg

Ceci va redimensionner l’image en conservant le ratio, et si le résultat n’est pas adaptée aux dimensions finales, va en découper une partie tout en préservant le centre de l’image. La sortie sera un jpg avec un taux de qualité de 80%.

Là encore, un petit wrapper ne fait pas de mal :

def thumb(img, width, heigth, output, crop=False, encoding=sys.stdout.encoding):
    if crop:
        if crop is True:
            crop = 'center'
        crop = "-gravity {} -extent {}x{}".format(crop, width, heigth)
    else:
        crop = ''
 
    cmd = ("convert {img} -thumbnail {width}x{heigth}^ {crop} -quality 80 {output}").format(
          img=img, width=width, heigth=heigth, crop=crop, output=output,
    )
 
    r =  envoy.run(cmd)
 
    if r.status_code != 0:
        raise ImageMagicError(r, encoding)
 
    return r

Bref, ce genre de petit bricolage est loin d’être propre, mais il s’est montré fort robuste et efficace au fil des années. Quand on fait des dizaines de milliers de screenshots de films de cul tous les jours, c’est important :-)

24 thoughts on “Pour vos thumbnails, ImageMagick à la rescousse

    • Max

      j’utilise mediainfo ou ffprobes pour avoir le temps de la video que je divise par le nb de screenshots voulu, ensuite j’extrait l’image avec l’option -ss dans ffmpeg, pas trouvé plus simple pour le moment :(

      on peut aussi le faire avec le nombre de frames et l’option -v de ffmpeg si ma mémoire est bonne.

  • Baronsed

    @ Ludovic Gasc : je suppose que tu veux des gifs :
    mplayer video.mp4 -vf scale=largeur:hauteur -vo gif89a:fps=nombreentier:output=sortie.gif

    Après il y a -ss et -endpos pour définir le début et le temps à attendre avant de couper.

  • groug

    Wow. J’ai tiqué sur le super(ImageMagicError, self) (j’ai pensé que ça ferait un stack overflow), ai lu la doc de super, et dormirais moins bête ce soir.

    C’est quelque chose que vous faites automatiquement, gérer le cas des héritages en diamant, ou c’est un cas à part, ici ?

    Je rajoute envoy à la liste des libs géniales que je dois tester.

  • Ludovic Gasc

    @baronsed: Non, c’est pour avoir des screenshots à intervalle régulier de la vidéo au format jpg, comme on peut voir sur les sites Web de vidéo.

  • Syl

    Je profite de tomber sur un pro du python qui utilise envoy! ^^
    Est-ce que tu connais un moyen d’éviter les process zombies avec envoy.connect?

    En fait, le pb semble venir de subprocess.Popen…j’arrive à dégager le process zombie en faisant un p._process.wait() (p étant mon objet envoy.ConnectedCommand) ou un os.wait(), mais du coup, je ne récupère jamais le résultat de ma commande.

    Est-ce que ça vous parle, Ô grands mages du python?

    (désolé d’être HS, mais une occasion de parler envoy.connect, je pouvais pas rater ça!)

  • Sam Post author

    J’en connais pas. En même temps, si tu as un processus zombie, tu peux pas récupérer le résultat de ta commande de toutes façon…

  • Syl

    C’est sur oui. Le but était plutôt d’éviter la zombification du process.

    En utilisant le module ‘signal’, on peut intercepter la fin du process et éviter ça…mais pas directement avec ‘envoy’, qui semble être à l’abandon.

    Ça me parait trop gros pour un langage si habituellement parfait qu’est python! On a le PID, je comprend pas qu’on puisse pas intercepter le signal de fin de process avec Popen alors qu’on l’a facilement via un wait()….je me demande si c’est pas un vieux bug qui a été résolu depuis ou si c’est lié à ma plateforme (je suis en 2.6 sous Solaris…et ouais, je suis au boulot!).

    Je ferais d’autres test chez moi et si je trouve un moyen, je ferais tourner l’info!

  • Sam Post author

    C’est peut être possible, après tout, je ne connais pas l’API par coeur. Et c’est vrai que c’est suspect que ça le soit pas.

  • Syl

    @Max: Super! Je viens de tester ça! C’est plus riche que ‘envoy’. A première vue, j’avais le même problème qu’avec envoy, étant donné qu’en arrière plan, c’est toujours subprocess.Popen qui est utilisé, mais j’ai quand même fini par trouver une solution satisfaisante.

    J’ai mis un timeout court à la lecture du retour de mon process et je boucle sur la fonction read() de pexpect:

    c=pexpect.spawn(ma_commande_qui_renvoie_16_Mo_de_texte_en_50sec)
    c.timeout=0.2
    res="nada"
    while res=="nada":
        try:
            res=c.read()
        except pexpect.TIMEOUT: 
            print "Wait...or do something else!"

    J’en profite pour dire que les infos qui accompagnent l’exception TIMEOUT du module ‘pexpect’ sont super riches et permettent de voir si votre process est freezé en affichant les 100 dernier caractères du buffer de sortie.

    Merci pour le tuyau Max!

  • Sam Post author

    Je ne sais pas, mais puisqu’il y a un native toolkit sur android, je pense qu’un bon bidouilleur pourrait le faire.

Leave a comment

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.