Le module Python MSS est en route vers la version 2.0.0 et se veut incroyablement rapide. Que ça soit sous MacOS X ou Microsoft Windows, le temps de capture est aussi rapide que du C pur, soit quelques centièmes de secondes tout au plus. Par contre, sous GNU/Linux, c'est une autre histoire...

Le soucis est qu'il n'existe pas de fonction native pour récupérer tous les pixels d'une zone ; il n'y a pas d'équivalent à GetDIBits() de Windows et CGWindowListCreateImage() de MacOS X. L'unique méthode est d'appeler XGetImage() pour stocker les informations brutes dans une variable. Cela est du à la complexité du serveur X qui doit gérer toute sorte de configuration.

Il faut donc parcourir les données brutes et convertir chaque segment pour en extraire les couleurs RGB. En C, voici ce que ça donne (cf. test-linux.c) :

/* Voir le fichier test-linux.c pour les détails */
ximage = XGetImage(display, root, left, top, width, height, allplanes, ZPixmap);
pixels = malloc(sizeof(unsigned char) * width * height * 3);

for ( x = 0; x < width; ++x ) {
    for ( y = 0; y < height; ++y ) {
        pixel = XGetPixel(ximage, x, y);
        offset = x * 3 + width * y * 3;
        pixels[offset]     = (pixel & ximage->red_mask) >> 16;
        pixels[offset + 1] = (pixel & ximage->green_mask) >> 8;
        pixels[offset + 2] =  pixel & ximage->blue_mask;
    }
}

En Python, le pseudo code suivant est utilisé (cf. get_pixels_slow()) :

def pix(pixel, _resultats={}, _b=pack):
    # Apply shifts to a pixel to get the RGB values.
    # This method uses of memoization.
    if pixel not in _resultats:
        _resultats[pixel] = _b('<B', (pixel & rmask) >> 16) + \
            _b('<B', (pixel & gmask) >> 8) + _b('<B', pixel & bmask)
    return _resultats[pixel]

pixels = [pix(xlib.XGetPixel(ximage, x, y))
          for y in range(height) for x in range(width)]

Les résultats sont effarants (pour un écran de 2360x1920 pixels) :

  • C : 0,093 sec
  • Python : 3,794 sec → soit 40 fois plus lent...

À la limite, 4 secondes pour une si grande zone, ça pourrait aller, mais le module MSS vise l'intégration dans toute sorte de logiciel, notamment le domaine du jeu-vidéo. Il se doit donc d'être plus véloce.


La solution : ctypes

Le module MSS tire parti du module ctypes pour utiliser les fonctions du système afin d'être diablement efficace. La solution, pour pallier ce problème de vitesse d'exécution, est d'abuser de ctypes, il est là pour ça. Ça tombe sous le sens, finalement.

J'ai donc crée une bibliothèque partagée et attaché ctypes par-dessus. Le code est simple, voyez mss.c :

/* gcc -shared -rdynamic -fPIC -Wall -pedantic -lX11 mss.c -o libmss.so */

#include <X11/Xlib.h>
#include <X11/Xutil.h>  /* Pour le prototype de XGetPixel */

int GetXImagePixels(XImage *ximage, unsigned char *pixels) {
    unsigned int x, y, offset;
    unsigned long pixel;

    if ( !ximage ) {
        return -1;
    }
    if ( !pixels ) {
        return 0;
    }

    for ( x = 0; x < ximage->width; ++x ) {
        for ( y = 0; y < ximage->height; ++y ) {
            offset =  x * 3 + ximage->width * y * 3;
            pixel = XGetPixel(ximage, x, y);
            pixels[offset]     = (pixel & ximage->red_mask) >> 16;
            pixels[offset + 1] = (pixel & ximage->green_mask) >> 8;
            pixels[offset + 2] =  pixel & ximage->blue_mask;
        }
    }
    return 1;
}

La fonction requiert les données brutes fournies par XGetImage() ainsi que le tableau alloué pour l'occasion.
Elle aurait très bien pu être LA fonction de la Xlib, imaginez un simple appel à XGetImagePixels()...

En Python, on l'appelle tel que (cf. get_pixels()) :

# Chargement de la bibliothèque
mss = cdll.LoadLibrary(find_library('mss'))

# Définition des arguments et du type retourné
mss.GetXImagePixels.argtypes = [POINTER(XImage), c_void_p]
mss.GetXImagePixels.restype = c_int

# Allocation du tableau qui contiendra les données RGB
buffer_len = height * width * 3
pixels = create_string_buffer(buffer_len)

# ximage aura été crée par xlib.XGetImage()
# Appel de la fonction C GetXImagePixels() qui traitera les données brutes
mss.GetXImagePixels(ximage, pixels)

C'est tout ! On peut vérifier que GetXImagePixels() a bien fonctionné si elle retourne 1.
Et le résultat est bluffant (pour un écran de 2360x1920 pixels) : 0,117 sec. Wicked!

Graphique XGetPixel
Graphique par ChartGo

Pouvez-vous faire mieux ?

La solution apportée ajoute une dépendance alors que l'objectif premier était de n'avoir qu'à copier mss.py dans son projet pour pouvoir l'utiliser. L'idéal serait d'optimiser get_pixels_slow(). Trouver pourquoi XGetPixel() est si gourmant en Python, alors que toutes les autres fonctions sont quasiment équivalentes au C en terme de rapidité.
Pour expérimenter, tout ce qu'il vous faut se trouve dans le dépôt GitHub, dans la branche dev.