PyQt, Matplotlib und py2exe - Graph wird nicht angezeigt

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
sebastian06
User
Beiträge: 19
Registriert: Mittwoch 16. Dezember 2009, 11:05

Servus,

ich habe eine kleine Anwendung geschrieben, die PyQt * Matplotlib für die GUI benutzt. Wenn man das Programm "normal" laufen lässt, erscheint nach dem Drücken von OK eine Matplotlib Figure mit einem Graph und Interpereter einige Textausgaben.
Num habe ich das Programm mithilfe von py2exe verpackt. Wenn ich jetzt die EXE-Datei starte erscheint wie erhofft der Eingabe GUI. Aber nach Drücken von OK bekomme ich nur die Testausgaben in der Konsole. Der Graph wird nicht oder nur so kurt angezeigt, das er nicht sichtbar ist. Fehlermeldungen gibt es keine. Der Plan war aber, das dieses Graph-Fenster offen bleibt, bis der User es manuell schließt (so wie bei der "normalen" Version).

Wer hat eine Idee, was ich da falsch mache?

Grüße,

Sebi

Code:

Code: Alles auswählen

import numpy as np
import gauss1d as g1d
import matplotlib.pyplot as plt
from xlwt import Workbook
from TIFFfile import *
import os

from PyQt4.QtCore import *
from PyQt4.QtGui import *
# Qt4 bindings for core Qt functionalities (non-GUI)
from PyQt4 import QtCore
# Python Qt4 bindings for GUI objects
from PyQt4 import QtGui

# import the MainWindow widget from the converted .ui files
import ui_SIPChart_Basic_Dialog


class SIPChart_Basic_Dialog(QDialog, ui_SIPChart_Basic_Dialog.Ui_SIPChart_Basic_Dialog):

    def __init__(self, parent=None):
    #def __init__(self, text, parent=None):
        super(SIPChart_Basic_Dialog, self).__init__(parent)
        self.setupUi(self)
        
        # connect the signals with the slots
        QtCore.QObject.connect(self.OpenFile, QtCore.SIGNAL('clicked()'), self.OnOpenFile)
        QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL('accepted()'), self.OnAccepted)
    
    def find_stackmax(self):
        # find the zplane with contains the overall maximum position within the stack
        overall_max = self.imagestack.max()
        position = (self.imagestack == overall_max).nonzero()
        zpos = position[0][0]
        
        return zpos


    def OnOpenFile(self):

        self.filepath = QtGui.QFileDialog.getOpenFileName(self, 'Open file',
                                'c:/Dokumente und Einstellungen/sebastian.rhode/Eigene Dateien/Sebi','TIF Image Files (*.tif)')
       
        self.text_filename.setText(self.filepath)               


    def OnAccepted(self):

         # read image data
        imagefilepath = str(self.text_filename.text()) 
        imagefile = os.path.basename(imagefilepath)
        imagedir = os.path.dirname(imagefilepath)
        
        tifdata = TIFFfile(imagefilepath)
        self.imagestack = tifdata.asarray()
        tifdata.close()
        # find brightest xy-plane
        zpos = self.find_stackmax()
        #brightest_plane = self.imagestack[zpos]
        # check dimensions --> switched !!!
        xdim = self.imagestack.shape[2] # x-dimension
        ydim = self.imagestack.shape[1] # y-dimension
        zdim = self.imagestack.shape[0] # z-dimension

        print xdim,ydim,zdim
        
        area_start_x = xdim/2 - self.SpinBox_width.value()/2
        area_end_x   = xdim/2 + self.SpinBox_width.value()/2
        area_start_y = ydim/2 - self.SpinBox_width.value()/2
        area_end_y   = ydim/2 + self.SpinBox_width.value()/2 
        
        z_values = np.zeros([zdim])
        mean_values = np.zeros([zdim])
        
        # fit PSY in xy-plane
        for i in range(0,zdim,1):
            
            current_plane = self.imagestack[i]
            area = current_plane[area_start_x:area_end_x,area_start_y:area_end_y]
            mean_area = np.sum(area) / (area.shape[0] * area.shape[1])
            z_values[i] = self.SpinBox_zspacing.value() * i
            mean_values[i] = mean_area
            #print i, z_values[i],mean_values[i]              
        
        [bgrd, height, center, fwhm_z, cov, xdat_new, yfit] = g1d.fitgauss1d(z_values,mean_values,
                                self.SpinBox_guess_fwhmz.value())
                                
        
        print ('Background [cts]         : '),bgrd
        print ('Height [cts]             : '),height
        print ('Center of Peak [micron]  : '),center
        print ('Brightest Plane          : '),zpos
        print ('FWHM-Z [micron]          : '),fwhm_z
        
        ydatn = mean_values - bgrd + np.sqrt(bgrd) * 1.3
        ydatn_max = ydatn.max()
        ydatn = ydatn / ydatn_max * 100
    
        yfitn = yfit - bgrd
        yfit_max = yfitn.max()
        yfitn = yfitn / yfit_max * 100    
                
        # display original and fitted data
        fig1 = plt.figure(figsize=(8,6))
        ax1 = fig1.add_subplot(1,1,1)
        ax1.plot(z_values,ydatn,'bo', lw=2, markersize=5, label = 'Original Data')
        ax1.plot(xdat_new,yfitn,'r-',lw=2,  label = 'Fitted Data')             
        ax1.set_ylim(-5,105)
        ax1.set_title(imagefile)
        ax1.text(10,80,'FWHM-Z = '+str(round(fwhm_z,3)) +' micron')    
        ax1.grid(True)   
        plt.subplots_adjust(left=0.05, bottom=0.06, right=0.94, top=0.94, wspace=0.1, hspace=0.2)    
        
        paramlist = ['Image Directory',
                     'Image Filepath',
                     'Objective Name',
                     'Objective Magnification',
                     'Objective NA',
                     'FWHM-Z [micron]',
                     'Height Peak [cts]',
                     'Center Peak [micron]',
                     'Brightest Plane',
                     'Z-Spacing [micron]',
                     'Guess FWHM-Z [micron]',
                     'Width Analysis Area [pixel]']
        
        if (self.writexls.isChecked() == True):

            book = Workbook()
            # ---------------------------------
            sheet1 = book.add_sheet('SIP Basic FWHM-Z')
            # ------------------------------------
            sheet1.col(0).width = 6500
            sheet1.col(1).width = 4000
            sheet1.write(0,0,paramlist[0])
            sheet1.write(1,0,paramlist[1])
            sheet1.write(2,0,paramlist[2])
            sheet1.write(3,0,paramlist[3])
            sheet1.write(4,0,paramlist[4])
            sheet1.write(5,0,paramlist[5])
            sheet1.write(6,0,paramlist[6])
            sheet1.write(7,0,paramlist[7])
            sheet1.write(8,0,paramlist[8])
            sheet1.write(9,0,paramlist[9])
            sheet1.write(10,0,paramlist[10])
            sheet1.write(11,0,paramlist[11])
            
            sheet1.write(0,1,imagedir)
            sheet1.write(1,1,imagefile)
            sheet1.write(2,1,str(self.lineEdit_Objective_Name.text()))
            sheet1.write(3,1,self.SpinBox_Magnification.value())
            sheet1.write(4,1,self.SpinBox_NA.value())
            sheet1.write(5,1,np.round(fwhm_z,3))
            sheet1.write(6,1,round(height,0))
            sheet1.write(7,1,np.round(center,3))
            sheet1.write(8,1,zpos)
            sheet1.write(9,1,self.SpinBox_zspacing.value())
            sheet1.write(10,1,self.SpinBox_guess_fwhmz.value())
            sheet1.write(11,1,self.SpinBox_width.value())
            
                
            os.chdir(imagedir)
            book.save(imagefile+'_SIP_FWHM_Z.xls')

            plt.show()

if __name__ == "__main__":
    import sys

    app = QApplication(sys.argv)
    form = SIPChart_Basic_Dialog()
    form.show()
    app.exec_()
sebastian06
User
Beiträge: 19
Registriert: Mittwoch 16. Dezember 2009, 11:05

ich galube ich habe den Fehler selber gefunden ....

plt.show() war zuweit eingerückt - keine Ahnung wie dass passiert ist. Aber jetzt habe ich ein anderes Problem. In der Console wird folgendes angezeigt.

C:/Dokumente und Einstellungen/sebastian.rhode/Eigene Dateien/Sebi/Python_Files/SIPChart_Basic_Dialog_Build/dist/tcl/tk8.5/tk.tcl: version conflict for package "Tk": have 8.5.9, ne
ed exactly 8.5.2
version conflict for package "Tk": have 8.5.9, need exactly 8.5.2
while executing
"package require -exact Tk 8.5.2"
(file "C:/Dokumente und Einstellungen/sebastian.rhode/Eigene Dateien/Sebi/Python_Files/SIPChart_Basic_Dialog_Build/dist/tcl/tk8.5/tk.tcl" line 20)
invoked from within
"source {C:/Dokumente und Einstellungen/sebastian.rhode/Eigene Dateien/Sebi/Python_Files/SIPChart_Basic_Dialog_Build/dist/tcl/tk8.5/tk.tcl}"
("uplevel" body line 1)
invoked from within
"uplevel #0 [list source $file]"


This probably means that tk wasn't installed properly.


Da bedeutet, ich muss Tk "downgraden" ?
BlackJack

@sebastian06: Das sieht nach einer Tk-Fehlermeldung aus!? Ich sehe jetzt gerade nicht wo die bei Deinem Qt-Programm herkommen soll!? Funktioniert das Programm trotzdem?

Sonstige Anmerkungen zum Quelltext:

Sternchenimporte sollte man vermeiden. Bei den Qt-Modulen ist das vielleicht noch okay, weil da alle Namen mit einem `Q` anfangen und man deshalb weiss wo sie herkommen, aber bei `TIFFfile` würde ich das expliziter machen. Wenn man bei den Qt-Modulen einen Sternchenimport macht, sollte man sie aber nicht noch *zusätzlich* als Module importieren, sondern sich auf einen Weg beschränken.

Die Kommentare über den Importen von `QtCore` und `QtGui` sind überflüssig, weil sie das Offensichtliche was darunter im Quelltext steht noch einmal sagen. Das ist kein Mehrwert für den Leser. Gleiches gilt zum Beispiel auch für den Kommentar in der `__init__()` vor dem Verbinden der Signale/Slots. Oder das `xdim` für 'x-dimensionen' steht. Wenn das unklar sein sollte, könnte man das auch einfach `x_dimensions` nennen.

Für das Verbinden von Signalen und Slots gibt es schon seit geraumer Zeit eine neue API die kürzer und typsicherer ist:

Code: Alles auswählen

        QtCore.QObject.connect(self.OpenFile, QtCore.SIGNAL('clicked()'), self.OnOpenFile)
        QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL('accepted()'), self.OnAccepted)
# 
# =>
# 
        self.OpenFile.clicked.connect(self.OnOpenFile)
        self.buttonBox.accepted.connect(self.OnAccepted)
Nach dem eine `__init__()`-Methode ausgeführt wurde, sollten in der Regel alle Attribute auf dem Objekt existieren, die von Methoden verwendet werden. Oder anders gesagt sollte ein normaler Methodenaufruf dem Objekt keine neuen Attribute hinzufügen. Das macht das Programm schwerer verständlich und fehleranfälliger.

Zum aufteilen des Dateipfad in Pfad und Dateiname hätte man auch `os.path.split()` verwenden können.

`find_stackmax()` scheint mir keine richtige Methode zu sein. Das könnte man genau so gut als Funktion implementieren.

Die Werte von `self.imagestack.shape` könnte man mit "tuple unpacking" kürzer an die drei Namen binden.

Wenn man wiederholt einen Wert benötigt und dafür jedes mal ``self.SpinBox_width.value() / 2`` tippen oder kopieren muss, sollte man ihn vorher an einen Namen binden. Wobei in dem Quelltext eine unschöne Vermischung zwischen GUI und Logik stattfindet. Mitten in Berechnungen werden Werte aus GUI-Elementen ausgelesen.

In Python 2.x sollte man `xrange()` verwenden, wenn man die Liste, die `range()` erzeugt eigentlich gar nicht benötigt.

An der Stelle hätte ich vielleicht auch eher versucht das in `numpy` zu lösen, statt die Schleife in Python schreiben. `z_values` sieht mir nach einem Fall für `numpy.arange()` aus. Für `mean_area` kann man `numpy.mean()` beziehungsweise die `mean()`-Methode auf dem Array verwenden. Und wenn man den Ausschnitt statt aus der "Scheibe" `current_plane` gleich aus dem "Block" `imagestack` per „slicing“ ausschneidet, käme man vielleicht komplett ohne Schleife in Python aus. Weitestgehend ungetestet:

Code: Alles auswählen

        area_width = self.SpinBox_width.value() / 2
        zspacing = self.SpinBox_zspacing.value()
        
        zdim, xdim, ydim = self.imagestack.shape
        print xdim,ydim,zdim
        
        area_start_x = xdim / 2 - area_width
        area_end_x = xdim / 2 + area_width
        area_start_y = ydim / 2 - area_width
        area_end_y = ydim / 2 + area_width
        
        z_values = np.arange(0, zdim * zspacing, zspacing)
        mean_values = self.imagestack[
            ..., area_start_x:area_end_x, area_start_y:area_end_y
        ].reshape([zdim, -1]).mean(1)
Insgesamt ist die `OnAccepted()` zu lang. Eine Funktion/Methode sollte nur eine Sache tun. Berechnen, Plotten, und Speichern als Exceltabelle ist zu viel für eine Methode.

Das schreiben von der Exceltabelle enthält viel unschönes kopieren und einfügen. Das liesse sich mit Schleifen und der `enumerate()`-Funktion deutlich kürzer schreiben. Wenn man in `paramlist` nicht nur die Daten für die erste Spalte ablegt, sondern eine verschachtelte Struktur daraus macht, dann hätte man auch Beschriftung und Wert nahe beieinander stehen.
Antworten