Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

843 questions

1,292 réponses

3,064 commentaires

5,968 utilisateurs

Swap efficient

+7 votes

Voici le code que je souhaiterai rendre plus rapide :

# Replace pixels values: BGR to RGB
buffer_len = width * height * 3
for idx in range(0, buffer_len - 2, 3):
    pixels[idx + 2], pixels[idx] = pixels[idx], pixels[idx + 2]

Il s'agit d'une partie du module MSS qui s'occupe de récupérer les pixels sous Windows.
pixels contient les données brutes et buffer_len vaut plusieurs millions suivant la configuration.

Y a-til un module ou une autre façon de réorganiser les pixels qui serait plus véloce ? La consommation mémoire importe peu tant que le temps d'exécution est raccourci.


EDIT du 20/04/2015
Je mets à disposition des fichiers de test afin que vous puissiez manipuler les données en situation réelle :
test-raw-windows.py, que vous pouvez exécuter depuis n'importe quel OS, le nom est trompeur.
data-windows.raw, les données brutes.

demandé 14-Avr-2015 par Tiger-222 (1,200 points)
reclassée 2-Mai-2015 par Tiger-222

3 Réponses

+8 votes
 
Meilleure réponse

On peut imaginer de faire des slices plutôt qu'une boucle explicite :

pixels[2:buffer_len:3], pixels[0:buffer_len:3] = pixels[0:buffer_len:3], pixels[2:buffer_len:3]

Le problème de cette solution est de créer des listes temporaires mais puisque tu dis que la mémoire n'est pas forcément un problème.

Je ne sais pas si il y a un gain avec cette solution, à toi de tester.

[EDIT]
Je rajoute ici le complément apporté en commentaire par un généreux anonyme :

Passer le buffer en bytearray améliore encore grandement les perfs
(facteur 10) sous CPython 2 & 3 (win32).

data = bytearray(data)
data[2:buffer_len:3], data[0:buffer_len:3] = data[0:buffer_len:3], data[2:buffer_len:3]
data = bytes(data)
# ensuite seulement (mais inutile il me semble, on peut travailler sur data)
pixels = cast(data, POINTER(c_char * buffer_len)).contents
répondu 15-Avr-2015 par bubulle (2,256 points)
edité 26-Avr-2015 par bubulle

À la vache ! Je suis bluffé, c'est 62x plus rapide que ma solution et 8x plus rapide que la solution apportée par bubulle. Châpeau à vous, merci :)

Faudrait la passer en réponse ou la rajouter à la réponse sélectionnée :)

J'ai fais un petit tweet parce que wow.

A mettre en parallèle avec la super prez du très drole Brandon Rhodes lors de la pycon 2015 "Oh, Come On Who Needs Bytearrays" (https://www.youtube.com/watch?v=z9Hmys8ojno)

Pour info, la dernière ligne n'est pas nécessaire :

pixels = cast(data, POINTER(c_char * buffer_len)).contents
+1 vote

si je comprends le problème :
pixels[0] contient une valeur de bleu
pixels1 contient une valeur de green
pixels[2] contient une valeur de red

et toi tu veux que :
pixels[0] contient une valeur de red
pixels1 contient une valeur de green
pixels[2] contient une valeur de blue

et ainsi de suite ^^

pourquoi ne pas utiliser un tableau 3 dimensions?

pixels[0]=[pixel_bleu,pixel_green,pixel_red]

ensuite :

taille = len(pixels)
for i in range(0,taille):
    pixels[i].reverse()

Petite info :

Le temps d'accès à un élément par son index est constant, quel que
soit l'élément désiré. Cela s'explique par le fait que les éléments
d'un tableau sont contigus dans l'espace mémoire. Ainsi, il est
possible de calculer l'adresse mémoire de l'élément auquel on veut
accéder, à partir de l'adresse de base du tableau et de l'index de
l'élément. L'accès est immédiat, comme il le serait pour une variable
simple.

Les limites d'une telle structure viennent de son avantage. Un tableau
étant représenté en mémoire sous la forme de cellules contiguës, les
opérations d'insertion et de suppression d'élément sont impossibles,
sauf si on crée un nouveau tableau, de taille plus grande ou plus
petite (selon l'opération). Il est alors nécessaire de copier tous les
éléments du tableau original dans le nouveau tableau, puis de libérer
l'espace mémoire alloué à l'ancien tableau. Cela fait donc beaucoup
d'opérations et oblige certains langages fournissant de telles
possibilités à implémenter leurs tableaux, non pas sous la forme
traditionnelle (cellules adjacentes), mais en utilisant une liste
chaînée, ou une combinaison des deux structures pour améliorer les
performances.

répondu 14-Avr-2015 par boblinux (3,092 points)
edité 14-Avr-2015 par boblinux

pourquoi ne pas utiliser un tableau 3 dimensions?

Je ne choisis pas la manière dont sont stockés les pixels, j'utilise ctypes pour accéder aux fonctions de la GDI (BitBlt et GetDIBits). Réorganiser comme tu le demandes prendrait autant voir plus de temps. J'ai profilé les différentes étapes, et 95% du temps est passé dans ce bout de code.

hum... Autant pour moi, quand tu parlais de

réorganiser les pixels

je pensais que tu avais plus de liberté que ça =D.

apparement la méthode avec range + len sauvegardé est plus optimal qu'utiliser un compteur inverse, un simple compteur, un énumerate ou encore un range (sans sauvegardé le len mais en le mettant dans le for).

+6 votes

A mon avis, les gains que tu puisses choper en pur Python sont minimes comparés au coût de manipuler des objets Python pour chaque entier.

En Python 2.7, tu peux forcer l'usage xrange() plutôt que range(), ce qui évite la création d'une liste pour rien:

xrange = getattr(__builtins__, 'xrange', range) # xrange pour toute version

Ensuite tu peux tenter de remplacer ta liste par un array typé pour grappiller encore un peu. Mais comme je sais pas où tu récupères tes données ni comment, je peux pas vraiment faire une démo. En plus, le gain n'est pas certain d'être significatif.

Puisque l'utilisation mémoire ne compte pas, tu peux tenter jouer sur la création d'une nouvelle collection plutôt que l'utilisation de l'ancienne:

from array import array
buffer_len = width * height

def to_rgb(pixels, buffer_len):
    for i in xrange(0, buffer_len - 2, 3):
        yield pixels[i + 2]
        yield pixels[i + 1]
        yield pixels[i])

pixels = array('H', to_rgb(pixels, buffer_len))

Je sais pas ce qui va gagner : l'overhead du générateur et de la création de liste à celui de la double assignation dans une boucle. Il faut mesurer.

Dans tous les cas, je ne pense pas que tu gagnes beaucoup. Là tu vas être limité par le typage dynamique du langage qui impacte sérieusement les grosses boucles.

Vu que je suppose que tu veux garder le côté pur Python, il va falloir que tu trouves une lib C de chaque OS visé qui fait cette opération et te mapper dessus. Ils ont tous forcément ça puisqu'il font de l'affichage, mais après pour trouver le bon appel...

Une solution alternative serait de te mapper directement sur opengl, qui est installé sur tous les OS modernes et est spécialisé dans le traitement d'images et a probablement tout ce qu'il faut embarqué pour ça. PyOpenGL est un binding pur Python existant, ça peut peut être t'aider : charge l'image avec opengl et manipule là directement depuis la lib, ça utilisera la carte graphique et ça sera super rapide.

répondu 14-Avr-2015 par Sam (5,000 points)
edité 14-Avr-2015 par Sam

En effet, avec xrange je grapille quelques millisecondes. La solution apportée par bubulle est bluffante par contre, le temps d'éxecution est divisé par 5 voire plus :)

Arf, je viens d'essayer sans le array, et c'est aussi rapide que le slicing. Cte blaz'.

def to_rgb(pixels, buffer_len):
    for i in xrange(0, buffer_len - 2, 3):
        yield pixels[i + 2]
        yield pixels[i + 1]
        yield pixels[i])

pixels = list(to_rgb(pixels, buffer_len))

Ca mérite un article tout ça.

Je viens d'essayer et la différence reste énorme, le slicing est plus rapide de 88%.
Je viens de lire ton article, peut-être que sur les données reçues par GetDIBits() sont d'un type "plus lent" à traiter ? Il s'agit de c_char_Array_$buffer_len, crée tel que :

pixels = ctypes.create_string_buffer(buffer_len)

Ensuite, GetDIBits() rempli pixels avec les données brutes.

C'est vraiment intéressant. Y a peut être encore l'unboxing des valeurs C qui jouent ici car ta méthode la saute sûrement. Meilleure question de index error pour le coup :)

Je viens de capter. C'est rapide parce qu'il s'agit d'un generator. Mais du coup, pour récupérer les données sous la forme voulue, je dois faire :

pixels = b''.join(to_rgb(pixels, pixels_len))

Et là, c'est le traitement qui alourdi tout.

Pas pixels = bytes(torgb(pixels, pixelslen)) ?

J'ai mis à disposition un fichier de test afin que tu puisses voir ce que ça donne en situation réelle. Je copie les liens dans le post de départ.

...