source: branches/GUIdev/OutputGUI/ApplicationWindow.py @ 1441

Last change on this file since 1441 was 1344, checked in by KelvinHsu, 10 years ago

Initial Commit

File size: 19.6 KB
Line 
1#-------------------------------------------------------------------------------
2# Name:        ApplicationWindow
3# Purpose:
4#
5# Author:      Kelvin
6#
7# Created:     02/01/2014
8#-------------------------------------------------------------------------------
9
10from PyQt4 import QtGui, QtCore
11from Skymap import *
12from SpectralGraph import *
13from ProgressBar import *
14from DuchampResults import *
15
16import pyfits
17import os
18
19"""Application Window Class Definition"""
20# This handles the main window.
21class ApplicationWindow(QtGui.QMainWindow):
22
23    # Main Window Initialisation
24    def __init__(self):
25
26        # QMainWindow Initialisation
27        QtGui.QMainWindow.__init__(self)
28        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
29        self.setWindowTitle("Duchamp")
30
31        # File Menu
32        self.file_menu = QtGui.QMenu('&File', self)
33        self.file_menu.addAction('&Open File', self.openFiles, QtCore.Qt.CTRL + QtCore.Qt.Key_O)
34        self.file_menu.addAction('&Save as parameters file', self.saveParameters, QtCore.Qt.CTRL + QtCore.Qt.Key_P)
35        self.file_menu.addSeparator()
36        self.file_menu.addAction('&Quit', self.close, QtCore.Qt.CTRL + QtCore.Qt.Key_Q)
37        self.menuBar().addMenu(self.file_menu)
38
39        # Options Menu
40        self.options_menu = QtGui.QMenu('&Options', self)
41        self.menuBar().addSeparator()
42        self.menuBar().addMenu(self.options_menu)
43        self.options_menu.addAction('&Full Screen Mode', self.showFullScreen, QtCore.Qt.CTRL + QtCore.Qt.Key_F)
44        self.options_menu.addAction('&Maximized Mode', self.showMaximized, QtCore.Qt.CTRL + QtCore.Qt.Key_M)
45        self.options_menu.addAction('&Normal Mode', self.showNormal, QtCore.Qt.CTRL + QtCore.Qt.Key_N)
46
47        # Help Menu
48        self.help_menu = QtGui.QMenu('&Help', self)
49        self.menuBar().addSeparator()
50        self.menuBar().addMenu(self.help_menu)
51        self.help_menu.addAction('&About', self.about, QtCore.Qt.CTRL + QtCore.Qt.Key_A)
52
53        # Main Widget
54        self.main_widget = QtGui.QWidget(self)
55        self.main_widget.setFocus()
56        self.setCentralWidget(self.main_widget)
57        self.layout = QtGui.QGridLayout(self.main_widget)
58
59        # Main Window Status Bar
60        self.statusBar().showMessage("Duchamp Output Window", 2000)
61
62        # Window location and size
63        self.center = QtGui.QDesktopWidget().availableGeometry().center()
64        self.move(0, 0)
65        self.xsize = 2*self.center.x()*0.9
66        self.ysize = 2*self.center.y()*0.9
67        self.resize(self.xsize, self.ysize)
68
69    # About Message
70    def about(self):
71
72        QtGui.QMessageBox.about(self, "About Duchamp",
73"""
74<i>Duchamp</i> is an object-finder for use
75on spectral-line data cubes. The basic execution of <i>Duchamp</i> is to read in a
76FITS data cube, find sources in the cube, and produce a text file of positions,
77velocities and fluxes of the detections, as well as a postscript file of the spectra
78of each detection.
79
80<i>Duchamp</i> has been designed to search for objects in particular sorts of data:
81those with relatively small, isolated objects in a large amount of background or
82noise. Examples of such data are extragalactic HI surveys, or maser surveys.
83<i>Duchamp</i> searches for groups of connected voxels (or pixels) that are all above
84some flux threshold. No assumption is made as to the shape of detections, and
85the only size constraints applied are those specified by the user.
86
87<i>Duchamp</i> has been written as a three-dimensional source finder, but it is possible
88to run it on a two-dimensional image (i.e. one with no frequency or velocity
89information), or indeed a one-dimensional array, and many of the features of
90the program will work fine. The focus, however, is on object detection in three
91dimensions, one of which is a spectral dimension. Note, in particular, that it
92does not do any fitting of source profiles, a feature common (and desirable)
93for many two-dimensional source finders. This is beyond the current scope of
94<i>Duchamp</i>, whose aim is reliable detection of spectral-line objects.
95
96<i>Duchamp</i> provides the ability to pre-process the data prior to searching.
97Spectral baselines can be removed, and either smoothing or multi-resolution
98wavelet reconstruction can be performed to enhance the completeness and
99reliability of the resulting catalogue.
100""")
101
102    # This prompts the user to open required files.
103    def openFiles(self):
104
105        # Prompt for results file name.
106        resultsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open results File', '.', "text files (*.txt);;All files (*)")
107
108        # If the name is empty, then we did not load any files.
109        if not resultsFileName:
110            QtGui.QMessageBox.information(self, 'Message', 'No file loaded.', QtGui.QMessageBox.Ok)
111            return
112
113        # Open up the results file.
114        resultsFile = open(resultsFileName)
115
116        # Get the first line of the results file.
117        for line in resultsFile:
118            firstline = line
119            break
120
121        # Determine if the content of the results file is correct.
122        if "# Results of the Duchamp source finder" not in firstline:
123            QtGui.QMessageBox.information(self, 'Message', 'Incorrect content for results file. No file loaded.', QtGui.QMessageBox.Ok)
124            return
125
126        # Find the maskfits and fits file name.
127        maskfitsFileName = ''
128        fitsFileName = ''
129        for line in resultsFile:
130
131            if 'imageFile' in line and ' = ' in line:
132
133                fitsFileName = line.split(' = ')[-1].split()[0].strip()
134
135            if 'flagOutputMask' in line and '>' in line:
136
137                maskfitsFileName = line.split('>')[-1].split()[0].strip()
138
139        # If we cannot find the fits file name, prompt the user to find it.
140        if not fitsFileName:
141            QtGui.QMessageBox.information(self, 'Message', 'Results file do not contain input "imageFile" name. Please specify the original input fits file used in source finding.', QtGui.QMessageBox.Ok)
142           
143            # Prompt for the input cube fits file name.
144            fitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open input File', '.', "fits files (*.fits);;All files (*)")
145
146            # If the name is empty, then we did not load any files.
147            if not fitsFileName:
148                QtGui.QMessageBox.information(self, 'Message', 'No file loaded.', QtGui.QMessageBox.Ok)
149                return
150
151        # If the file found does not exist, then let the user know and give the user a final chance.
152        while not os.path.isfile(fitsFileName):
153            QtGui.QMessageBox.information(self, 'Message', 'Input "imageFile" <%s> specified in results file cannot be found in current directory. Please specify the original input fits file used in source finding.'%fitsFileName, QtGui.QMessageBox.Ok)
154           
155            # Prompt for the input cube fits file name.
156            fitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open input File', '.', "fits files (*.fits);;All files (*)")
157
158            # If the user cancelled the loading, then do not prompt the user anymore.
159            if not fitsFileName:
160                return
161
162        # If we cannot find the maskfits file name, prompt the user to find it.
163        if not maskfitsFileName:
164            QtGui.QMessageBox.information(self, 'Message', 'Results file do not contain MASK fits file name. Please specify MASK fits file.', QtGui.QMessageBox.Ok)
165
166            # Prompt for the mask cube fits file name.
167            maskfitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open mask File', '.', "MASK fits files (*.MASK.fits);;fits files (*.fits);;All files (*)")
168       
169            # If the name is empty, then we did not load any files.
170            if not maskfitsFileName:
171                QtGui.QMessageBox.information(self, 'Message', 'No file loaded.', QtGui.QMessageBox.Ok)
172                return
173
174        # If the file found does not exist, then let the user know and give the user a final chance.
175        while not os.path.isfile(maskfitsFileName):
176            QtGui.QMessageBox.information(self, 'Message', 'MASK fits file <%s> specified in results file cannot be found in current directory. Please specify the MASK fits file.'%maskfitsFileName, QtGui.QMessageBox.Ok)
177           
178            # Prompt for the mask cube fits file name.
179            maskfitsFileName = QtGui.QFileDialog.getOpenFileName(self, 'Open mask File', '.', "MASK fits files (*.MASK.fits);;fits files (*.fits);;All files (*)")
180       
181            # If the user cancelled the loading, then do not prompt the user anymore.
182            if not maskfitsFileName:
183                return
184
185        # Store the results from the results file as a class
186        self.Results = DuchampResults(resultsFileName)
187
188        # Set the names of the files
189        self.Results.maskfitsFileName = maskfitsFileName
190        self.Results.fitsFileName = fitsFileName
191
192        # If the file names are okay, then check and load the files using those names.
193        self.checkAndLoadFiles(maskfitsFileName, fitsFileName)
194
195    # This makes sure that the files are correct and in the correct format.
196    # If everything is good, it will start the software.
197    def checkAndLoadFiles(self, maskfitsFileName, fitsFileName):
198       
199        # Try reading the files.
200        try:
201            # Open the mask cube fits file.
202            maskFits = pyfits.open(maskfitsFileName)
203       
204            # If the 'BUNIT' option in the mask cube fits header is not 'Object ID', then the program will not start.
205            if maskFits[0].header.get('BUNIT') != 'Object ID':
206                QtGui.QMessageBox.warning(self, 'Incorrect specification in  .MASK.fits file', 'The "BUNIT" option in "%s" is not set to "Object ID". Files not loaded.'%maskfitsFileName, QtGui.QMessageBox.Ok)
207                return(0)
208
209            # Open the input cube fits file.
210            realFits = pyfits.open(fitsFileName)
211
212            # If the two cubes do not have matching shapes, then they are obviously not meant for each other. The program will not start.
213            if maskFits[0].data.shape != realFits[0].data.shape:
214                QtGui.QMessageBox.warning(self, 'Cube sizes mismatch', 'Fits cube sizes differ in shape in files "%s" and "%s". Files not loaded.'%(maskfitsFileName, fitsFileName), QtGui.QMessageBox.Ok)
215                return(0)
216
217            # If everything is fine, we will confirm the choice of files with the user.
218            SuccessMsg = QtGui.QMessageBox()
219            SuccessMsg.setWindowTitle("Files Loaded")
220            SuccessMsg.setText('Files\n"%s" and \n"%s"\n are loaded successfully!'%(maskfitsFileName, fitsFileName))
221            SuccessMsg.setInformativeText('Please confirm that those are the correct files.')
222            SuccessMsg.setStandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.Discard)
223            SuccessMsg.setDefaultButton(QtGui.QMessageBox.Yes)
224
225            # If the user is fine with the choice, then start the program with those files.
226            if SuccessMsg.exec_() == QtGui.QMessageBox.Yes:
227
228                # This is the name of the input file
229                self.fitsName = fitsFileName.split('/')[-1]
230
231                # This is the name of the maskfits file
232                self.maskfitsName = maskfitsFileName.split('/')[-1]
233
234                # Those are the fits data
235                self.maskFits = maskFits
236                self.realFits = realFits
237
238                # Now we can start processing the data
239                self.processData()
240                return(1)
241
242            # Otherwise, discard the files.
243            QtGui.QMessageBox.information(self, 'Message', 'Files discarded.', QtGui.QMessageBox.Ok)
244            return(0)
245     
246        # If anything went wrong in the above process, then file reading has failed.
247        except Exception:
248            QtGui.QMessageBox.warning(self, 'Error', 'Reading of fits files failed. Files not loaded.', QtGui.QMessageBox.Ok)
249
250        return(0)
251
252    # This loads the required data from the files that contains the data.
253    def processData(self):
254
255        # Use a progress bar to indicate the loading process
256        self.pbar = ProgressBar(title = "Processing data", total = 100)
257        self.pbar.show()
258
259        # Obtain the two cubes
260        self.maskCube = self.maskFits[0].data
261        self.realCube = self.realFits[0].data
262
263        # Those are the size of our data cubes
264        [self.lenZ, self.lenY, self.lenX] = self.maskCube.shape
265
266        # This is the cube used to produce the final image
267        self.finalCube = zeros([self.lenZ, self.lenY, self.lenX])
268
269        # Those are our mask image, real image, and final image respectively
270        self.maskImage = zeros([self.lenY, self.lenX])
271        self.realImage = zeros([self.lenY, self.lenX])
272        self.finalImage = zeros([self.lenY, self.lenX])
273
274        # This will contain all the moment maps with their index matching its object ID
275        self.momentMaps = ['Moment Maps']
276
277        # Here we compute the final cube from the mask cube and real cube
278
279        # Go through each object we found
280        for ObjID in range(1, self.Results.totalDetections + 1):
281
282            # Obtain information regarding that source
283            ObjStats = self.Results.SkyObjects[ObjID].stats
284
285            # Obtain the spatial and spectral bound of that source
286            x1 = ObjStats['X1']
287            x2 = ObjStats['X2']
288            y1 = ObjStats['Y1']
289            y2 = ObjStats['Y2']
290            z1 = ObjStats['Z1']
291            z2 = ObjStats['Z2']
292
293            # Calculate the moment map of the source as well as add the source to the final cube
294            momentMap = zeros([self.lenY, self.lenX])
295
296            for z in range(z1, z2 + 1):
297
298                momentMap += self.realCube[z]
299
300                for y in range(y1, y2 + 1):
301                    for x in range(x1, x2 + 1):
302
303                        if self.maskCube[z][y][x] == ObjID:
304                            self.finalCube[z][y][x] = self.realCube[z][y][x]
305
306            # Add this moment map to the list of moment maps
307            self.momentMaps += [momentMap]
308
309            # And also compute the mask image from the mask cube
310            for z in range(self.lenZ):
311                self.maskImage[self.maskCube[z] == ObjID] = ObjID
312
313            # Update the progress bar accordingly
314            self.pbar.update_progressbar(100*ObjID/self.Results.totalDetections + 1)
315
316        # Compute the real image from the real cube
317        for z in range(self.lenZ):
318            self.realImage += self.realCube[z]
319
320        # Compute the final image from the final cube
321        for z in range(self.lenZ):
322            self.finalImage += self.finalCube[z]
323       
324        # The data processing has finished
325        self.pbar.close()
326
327        # The program can now start
328        self.start()
329
330    # This defines what happens when the software starts.
331    def start(self):
332
333        # Use a progress bar to indicate the start up progress
334        self.pbar = ProgressBar(title = "Loading Skymap", total = 100)
335        self.pbar.show()
336
337        # Start the skymap.
338        self.startSkymap()
339        self.pbar.update_progressbar(26)
340
341        # Start the spectral graph.
342        self.startSpectralGraph()
343        self.pbar.update_progressbar(51)
344
345        # Start the zoom graph
346        self.startZoomGraph()
347        self.pbar.update_progressbar(76)
348
349        # Start the moment map
350        self.startMomentMap()
351        self.pbar.update_progressbar(100)
352
353        self.pbar.close()
354
355    # This starts the skymap.
356    def startSkymap(self):
357
358        # Initialise the skymap.
359        self.skymap = Skymap(self.Results, maskImage = self.maskImage, finalImage = self.finalImage)
360        self.skymap.drawImage()
361        self.skymap.enableInteractivity()
362        self.skymap.fig.suptitle(self.fitsName)
363
364        # Determine its size and location.
365        self.skymap.setMinimumHeight(0.5*self.ysize)
366        self.skymap.setMinimumWidth(0.3*self.xsize)
367        self.layout.addWidget(self.skymap, 0, 0, 4, 4)
368
369        # Connect the signal from skymap to update the spectral graph.
370        self.skymap.graphSignal.connect(self.updateSpectralGraph)
371        self.skymap.graphSignal.connect(self.updateZoomGraph)
372        self.skymap.graphSignal.connect(self.updateMomentMap)
373
374    # This starts the spectral graph.
375    def startSpectralGraph(self):
376
377        # Initialise the spectral graph.
378        self.spectralGraph = SpectralGraph(self.Results, self.maskFits, self.realFits, self.skymap.maskImage)
379
380        # Determine its size and location.
381        self.spectralGraph.setMinimumHeight(0.3*self.ysize)
382        self.spectralGraph.setMinimumWidth(self.xsize)
383
384        self.layout.addWidget(self.spectralGraph, 4, 0, 2, 8)
385
386        # Initialise the radio buttons for display options.
387        self.displayMaxButton = QtGui.QRadioButton("Peak Detected Intensity")
388        self.displaySumButton = QtGui.QRadioButton("Total Detected Intensity")
389
390        # Determine its location.
391        self.layout.addWidget(self.displayMaxButton, 0, 4, 1, 1)
392        self.layout.addWidget(self.displaySumButton, 0, 5, 1, 1)
393       
394        # Determine its functionality.
395        self.displayMaxButton.toggled.connect(self.spectralGraph.plotMax)
396        self.displaySumButton.toggled.connect(self.spectralGraph.plotSum)
397        self.displayMaxButton.setChecked(True)
398
399
400    # This updates the spectral graph.
401    def updateSpectralGraph(self):
402
403        # Set the current object to plot the spectral graph with as the highlighted object in the skymap, and plot the spectral graph.
404        self.spectralGraph.setCurrentObjID(self.skymap.highlightedObj)
405        self.spectralGraph.plotSource()
406
407    # This starts the zoom graph.
408    def startZoomGraph(self):
409
410        # Initialise the zoom graph
411        self.zoomGraph = ZoomGraph(self.Results, self.maskFits, self.realFits, self.skymap.maskImage)
412
413        # Determine its location
414        self.layout.addWidget(self.zoomGraph, 1, 4, 3, 2)
415       
416        # Link the radio button functionalities to the zoom graph
417        self.displayMaxButton.toggled.connect(self.zoomGraph.plotMax)
418        self.displaySumButton.toggled.connect(self.zoomGraph.plotSum)
419
420    # This updates the zoom graph.
421    def updateZoomGraph(self):
422
423        # Set the current object to plot the zoom graph with as the highlighted object in the skymap, and plot the zoom graph.
424        self.zoomGraph.setCurrentObjID(self.skymap.highlightedObj)
425        self.zoomGraph.plotSource()
426
427    # This starts the moment map.
428    def startMomentMap(self):
429
430        # Initialise the moment map
431        self.momentMap = MomentMap(self.Results, maskImage = self.maskImage, finalImage = self.finalImage, momentMaps = self.momentMaps)
432
433        # Determine its location
434        self.layout.addWidget(self.momentMap, 1, 6, 3, 2)
435
436    # This updates the moment map.
437    def updateMomentMap(self):
438
439        # Set the current object to draw the moment map with as the highlighted object in the skymap, and draw the moment map
440        self.momentMap.setCurrentObjID(self.skymap.highlightedObj)
441        self.momentMap.drawImage()
442
443    # This saves the parameters used to generate this output into a parameter file.
444    def saveParameters(self):
445
446        # Prompt for the name of the file.
447        parameterFilePath = QtGui.QFileDialog.getSaveFileName(self, 'Save parameters as', '.', "text files (*.txt);;All files (*)")
448
449        # Write the parameter.
450        try:
451            writeParamFile(parameterFilePath, self.Results.parameters, self.Results.parameterName)
452
453        except Exceptions:
454            pass
Note: See TracBrowser for help on using the repository browser.