View  Edit  Attributes  History  Attach  Print  Search

PYQTLAB

PyQt Lab' : Transposition en PyQt de l'exemple Processing Math Graphing2DEquation et optimisation des calculs avec module numexpr

Par X. HINAULT - Juin 2013

Ce que l'on va faire ici

  • Dans ce code PyQt, je transpose un exemple Processing parlant qui montre comment il est assez simple de transposer en PyQt un code Processing. L'exemple de code Processing que je transpose ici est fournit avec le logiciel Processing (Examples > Math > Graphing2DEquation )
  • Les raisons qui me poussent à tester un tel exemple sont multiples :
    • j'ai utilisé Processing pendant un bon moment, et c'est tout naturellement que la comparaison s'impose pour moi...
    • évaluer la transposition d'un calcul complexe basé sur une double boucle intriquée en Processing vers l'utilisation de tableau Numpy
    • évaluer la vitesse comparative entre un code Processing et le code équivalent en PyQt
  • Ici le calcul utilisé appliqué à tous les pixels en temps réel lors du mouvement de la souris est basé sur : sin(n*cos(r) + 5*theta)
  • En terme de codage, le code Processing et le code Python ont des tailles comparables.

Résultat obtenu
Pour une image 320 x 240 pixels en niveau de gris :

  • 34 ms / image (31 ms/image en mode compilé) avec Processing
  • 27 ms / image avec PyQt + Numpy
  • 14 ms / image avec PyQt + Numpy + Numexpr.. soit une vitesse x 2.5 par rapport à Processing. Donc Python fait très bien le job !

Pré-requis

  • python 2.7
  • pyqt4.x
  • pyqtgraph
  • modules :
    • python-numpy : calculs et manipulation de tableaux numériques
    • python-numexpr : accélération des calculs réalisés avec numpy

Pour info : le code Processing de départ

L'exemple de code Processing que je transpose ici est fournit avec le logiciel Processing (Examples > Math > Graphing2DEquation ) :

/**
 * Graphing 2D Equations
 * by Daniel Shiffman.
 *
 * Graphics the following equation:
 * sin(n*cos(r) + 5*theta)
 * where n is a function of horizontal mouse location.  
 */


void setup() {
  size(200,200);
  frameRate(30);
}

void draw() {
  loadPixels();
  float n = (mouseX * 10.0) / width;
  float w = 16.0;         // 2D space width
  float h = 16.0;         // 2D space height
  float dx = w / width;    // Increment x this amount per pixel
  float dy = h / height;   // Increment y this amount per pixel
  float x = -w/2;          // Start x at -1 * width / 2
  for (int i = 0; i < width; i++) {
    float y = -h/2;        // Start y at -1 * height / 2
    for (int j = 0; j < height; j++) {
      float r = sqrt((x*x) + (y*y));    // Convert cartesian to polar
      float theta = atan2(y,x);         // Convert cartesian to polar
      // Compute 2D polar coordinate function
      float val = sin(n*cos(r) + 5 * theta);           // Results in a value between -1 and 1
      //float val = cos(r);                            // Another simple function
      //float val = sin(theta);                        // Another simple function
      // Map resulting vale to grayscale value
      pixels[i+j*width] = color((val + 1.0) * 255.0/2.0);     // Scale to between 0 and 255
      y += dy;                // Increment y
    }
    x += dx;                  // Increment x
  }
  updatePixels();
}
 

Le fichier d'interface *.py

  • Fichier obtenu automatiquement avec l'utilitaire pyuic4 à partir du fichier *.ui créé avec QtDesigner :
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file
#
# Created: Fri Jun 14 22:11:51 2013
#      by: PyQt4 UI code generator 4.9.1
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    _fromUtf8 = lambda s: s

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName(_fromUtf8("Form"))
        Form.resize(333, 258)
        Form.setMouseTracking(True)
        self.labelImage = QtGui.QLabel(Form)
        self.labelImage.setGeometry(QtCore.QRect(5, 10, 320, 240))
        self.labelImage.setStyleSheet(_fromUtf8("background-color: rgb(255, 255, 255);"))
        self.labelImage.setText(_fromUtf8(""))
        self.labelImage.setObjectName(_fromUtf8("labelImage"))

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        Form.setWindowTitle(QtGui.QApplication.translate("Form", "Pyqt : Image modifiée par MouseMove", None, QtGui.QApplication.UnicodeUTF8))


if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    Form = QtGui.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

Le fichier d'application *Main.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

# par X. HINAULT - Mai 2013 - Tous droits réservés
# GPLv3 - www.mon-club-elec.fr

# adaptation exemple Processing Math : Graphing 2D Equation

"""
 * Version originale Processing :
 *
 * Graphing 2D Equations
 * by Daniel Shiffman.
 *
 * Graphics the following equation:
 * sin(n*cos(r) + 5*theta)
 * where n is a function of horizontal mouse location.  
 """


# modules a importer
from PyQt4.QtGui import *
from PyQt4.QtCore import *  # inclut QTimer..
import os,sys

import numpy as np
import numexpr as ne # accélère numpy : https://code.google.com/p/numexpr/ !

import time # temps

from  tuto_pyqt_dessin_qpixmap_qimage_mouse_move_2D_equation import * # fichier obtenu à partir QtDesigner et pyuic4

# +/- variables et objets globaux

class myApp(QWidget, Ui_Form): # la classe reçoit le Qwidget principal ET la classe définie dans test.py obtenu avec pyuic4
        def __init__(self, parent=None):
                QWidget.__init__(self) # initialise le Qwidget principal
                self.setupUi(parent) # Obligatoire

                # --- Variables de classe

                self.width=self.labelImage.width()
                self.height=self.labelImage.height()

                self.n = (0.0* 10.0) / self.width #(mouseX * 10.0) / self.width;
                self.w = 16.0 # 2D space width
                self.h = 16.0 # 2D space height
                self.dx = self.w / self.width # Increment x this amount per pixel
                self.dy = self.h / self.height # Increment y this amount per pixel

                # --- Paramétrage des widgets de l'interface GUI si nécessaire ---

                # --- Connexions entre signaux des widgets et fonctions
                # connecte chaque signal utilisé des objets à l'appel de la fonction voulue

                # --- Code actif initial  ---


                # Initialisation QImage

                # tableau numpy définissant l'image
                self.pixels=np.fromfunction(self.f,(self.labelImage.height(),self.labelImage.width())) # tableau 320x240 x 1 canal et 8U - pixels calculés par fonction                

                #QImage(self,str data, int width, int height, Format format)
                self.image=QImage(self.pixels.tostring(), self.labelImage.width() , self.labelImage.height() ,QImage.Format_Indexed8) # crée image RGB 8 bits même taille que label


                #-- Affichage initial --
                self.pixmap=QPixmap.fromImage(self.image) # initialise  le QPixmap...

                self.updatePixmap(10,10)

                #----- initialisation capture évènement souris du Qlabel
                self.labelImage.setMouseTracking(True) # active le suivi position souris
                self.labelImage.installEventFilter(self) # défini un objet dans cette classe pour "filtrer" les évènements en provenance de l'objet label - méthode classe Object

        # --- les fonctions appelées, utilisées par les signaux des widgets ---

        # --- les fonctions appelées, utilisées par les signaux hors widgets ---

        # fonction pour gérer filtrage évènements
        #def eventFilter(self, _, event): # fonction pour gérer filtrage évènements
        def eventFilter(self, srcEvent, event): # fonction pour gérer filtrage évènements - reçoit l'objet source de l'évènement
                print ("event ")
                #print srcEvent

                if srcEvent==self.labelImage: # teste si la source de l'évènement est bien le label - utile si plusieurs sources d'évènements activées
                        if event.type() == QEvent.MouseMove: # si évènement "mouvement de la souris"
                                x=event.pos().x() # coordonnée souris au moment évènement dans le widget source de l'évènement =
                                y=event.pos().y()
                                print ("MouseMove : x="+ str(x) + " y= " + str(y)) # affiche coordonnées

                                self.micros0=self.micros() # temps reference

                                self.updateImage(x,y) # met à jour le qimage

                                self.updatePixmap(x,y) # met à jour le pixmap

                                # affiche delai
                                deltaMicros=self.micros()-self.micros0 # calcul délai          
                                print str(deltaMicros/1000.0)+" ms"

                                # note : temps comparatif avec même nombre pixels : Processing = 34ms (31ms en mode compilé)soit 30 fps, PyQt = 27ms soit 37 fps
                                # encore mieux... en utilisant numexpr (sudo apt-get install numexpr) on fait encore mieux que numpy : 14ms soit 70 fps environ... !
                                # soit près de 2 fois mieux que numpy...
                                # et près de 2,5 fois mieux que Processing... tout en étant beaucoup plus light à lancer...

                                # de plus ici : usage CPU seulement si mouvement souris - tombe à 0% sinon... Avec Processing : usage CPU permanent car Framerate obligé

                        # pour la liste des QEvent : voir : http://pyqt.sourceforge.net/Docs/PyQt4/qevent.html

                return False # obligatoire...

        # --- fonctions de classes autres---    
        def micros(self):
                return(int(round(time.time() * 1000000))) # microsecondes de l'horloge système

        def updateImage(self, xIn, yIn):
                #print ("updateImage")

                self.n = (xIn* 10.0) / self.width #(mouseX * 10.0) / self.width;

                # tableau numpy définissant l'image
                self.pixels=np.fromfunction(self.f,(self.labelImage.height(),self.labelImage.width())) # tableau 320x240 x 1 canal et 8U - pixels calculés par fonction                

                #QImage(self,str data, int width, int height, Format format)
                self.image=QImage(self.pixels.tostring(), self.labelImage.width() , self.labelImage.height() ,QImage.Format_Indexed8) # crée image RGB 8 bits même taille que label
                #self.image.loadFromData(self.pixels.tostring()) # recharge à partir données # ne "marche pas"..

        def updatePixmap(self, xIn, yIn):

                # chargement du QImage dans le QPixmap
                self.pixmap.convertFromImage(self.image) # recharge le QImage dans le QPixmap existant - met à jour le QPixmap

                #-- affichage du QPixmap dans QLabel
                self.labelImage.setPixmap(self.pixmap) # met à jour le qpixmap affiché dans le qlabel  

        #-- fonctions math utiliser pour calculer la valeur des pixels
        def f(self,  j, i): # Attention - inversion xIn et yIn : pour calcul pixel (x,y) la fonction reçoit (y,x) !
                # ici i et j sont les indices des pixels - i : indice en abscisse de 0 à width-1  et j : indice en orodnnée de 0 à heigth-1

                # ici x et y sont 2 grandeurs calculées à partir de i et j, les index des pixels d'abscisse i et d'ordonnée j

                # au lieu de l'incrémentation dans une boucle, on calcule x et y à partir indice i,j...
                x=-self.w/2+(i*self.dx)
                y=-self.h/2+(j*self.dy)

                n=self.n # pour usage local possible avec ne.evaluate()        

                # calcul à réaliser en utilisant les valeurs x,y calculées à partir de i,j

                #r=np.sqrt((x*x) + (y*y)) # avec numpy
                r = ne.evaluate("sqrt((x*x) + (y*y))") # avec numpexpr

                #theta=np.arctan2(y,x)  # avec numpy
                theta=ne.evaluate("arctan2(y,x)")

                #valeur=np.sin(self.n*np.cos(r)+(5*theta))  # avec numpy
                valeur=ne.evaluate("sin(n*cos(r)+(5*theta))") # avec numpexpr

                # valeur=(valeur+1)*255.0/2.0  # avec numpy
                valeur=ne.evaluate("(valeur+1)*255.0/2.0")

                return np.uint8(valeur) # renvoie la valeur au format np.uint8

# -- Autres Classes utiles --

# -- Classe principale (lancement)  --
def main(args):
        a=QApplication(args) # crée l'objet application
        f=QWidget() # crée le QWidget racine
        c=myApp(f) # appelle la classe contenant le code de l'application
        f.show() # affiche la fenêtre QWidget
        r=a.exec_() # lance l'exécution de l'application
        return r

if __name__=="__main__": # pour rendre le code exécutable
        main(sys.argv) # appelle la fonction main
 

Utilisation

  • Les 2 fichiers suivants sont à enregistrer dans un même répertoire, l'un en nom.py et l'autre en nomMain.py.
  • Puis lancer l'application depuis Geany ou équivalent, en exécutant le fichier nomMain.py
  • Lorsque l'on déplace le pointeur de souris sur le graphique, celui-ci se modifie en conséquence...