From 2b963e8c4cffefc1f9d270e3be4a011dea145daa Mon Sep 17 00:00:00 2001 From: Peter Ercius Date: Thu, 27 Jun 2024 13:13:08 -0700 Subject: [PATCH 1/9] version of DuSC which can read Dectris files. --- DuSC_explorer/DuSC_dectris.py | 437 ++++++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 DuSC_explorer/DuSC_dectris.py diff --git a/DuSC_explorer/DuSC_dectris.py b/DuSC_explorer/DuSC_dectris.py new file mode 100644 index 0000000..a4d3548 --- /dev/null +++ b/DuSC_explorer/DuSC_dectris.py @@ -0,0 +1,437 @@ +""" +Load TitanX data and show real and reciprocal space. + +The real space ROI can be rotated + +author: Peter Ercius +date: 2023/11/02 + +""" + +from pathlib import Path + +import pyqtgraph as pg +from pyqtgraph.graphicsItems.ROI import Handle +import numpy as np +from scipy import ndimage +import ncempy +#from tifffile import imwrite +import h5py +import hdf5plugin + +from qtpy.QtWidgets import * +from qtpy.QtCore import QRectF +from qtpy import QtGui + +class fileDECTRIS: + def __init__(self, filename, verbose=False): + self._verbose = verbose + self.raw_shape = [0, 0, 0] # shape of data on disk + self.data_shape = [0, 0, 0, 0] # the shape of the final 4D dataset + self.file_hdl = None + self.data_dtype = None + + self.bad_pixels = ((49, 75), (93,118), (95,119), (108, 57)) + + if hasattr(filename, 'read'): + try: + self.file_path = Path(filename.name) + self.file_name = self.file_path.name + except AttributeError: + self.file_path = None + self.file_name = None + else: + # check filename type, change to pathlib.Path + if isinstance(filename, str): + filename = Path(filename) + elif isinstance(filename, Path): + pass + else: + raise TypeError('Filename is supposed to be a string or pathlib.Path or file object') + self.file_path = filename + self.file_name = self.file_path.name + + # Try opening the file + try: + self.file_hdl = h5py.File(filename, 'r') + assert self.file_hdl['/entry/data'] + except: + print('Error opening file: "{}"'.format(filename)) + raise + + # if this is a HDF5 file + if self.file_hdl: + # Find the initial shape of the data set + for v in self.file_hdl['/entry/data'].values(): + self.raw_shape[0] = self.raw_shape[0] + v.shape[0] + self.raw_shape[1] = v.shape[1] + self.raw_shape[2] = v.shape[2] + self.data_dtype = v.dtype + + def __del__(self): + """Destructor for EMD file object. + + """ + # close the file + # if(not self.file_hdl.closed): + self.file_hdl.close() + + def __enter__(self): + """Implement python's with statement + + """ + return self + + def __exit__(self, exception_type, exception_value, traceback): + """Implement python's with statement + and close the file via __del__() + """ + self.__del__() + return None + + def get_dataset(self, remove_bad_pixels=False): + + # Pre allocate space + dd = np.zeros(self.raw_shape, dtype=self.data_dtype) + # Read in the data in all linked files + ii = 0 + for v in self.file_hdl['/entry/data'].values(): + dd[ii:ii+v.shape[0]] = v[:] + ii += v.shape[0] + + # Reshape assuming square + shape_square = int((dd.shape[0])**0.5) + assert dd.shape[0] == shape_square**2 + self.data_shape = (shape_square, shape_square, dd.shape[1], dd.shape[2]) + dd = dd.reshape(self.data_shape) + if remove_bad_pixels: + self.remove_bad_pixels(dd) + return dd + + def remove_bad_pixels(self, data, value=0, bad_pixels=None): + if bad_pixels: + self.bad_pixels = bad_pixels + for bad in self.bad_pixels: + data[:,:,bad[0], bad[1]] = value + + +class fourD(QWidget): + + def __init__(self, *args, **kwargs): + + self.real_space_limit = None + self.diffraction_pattern_limit = None + self.sa = None + self.current_dir = Path.home() + self.scale = 1 + self.center = (287, 287) + self.scan_dimensions = (0, 0) + self.frame_dimensions = (576, 576) + self.tt = None + self.dp = None + self.rs = None + self.log_diffraction = True + self.handle_size = 10 + + self.available_colormaps = ['thermal', 'flame', 'yellowy', 'bipolar', 'spectrum', 'cyclic', 'greyclip', 'grey', + 'viridis', 'inferno', 'plasma', 'magma'] + self.colormap = pg.colormap.getFromMatplotlib('grey') + + super(fourD, self).__init__(*args, *kwargs) + self.setWindowTitle("NCEM: TitanX 4D Data Explorer") + self.setWindowIcon(QtGui.QIcon('MF_logo_only_small.ico')) + + # Set the update strategy to the JIT version + #self.update_real = self.update_real_stempy + #self.update_diffr = self.update_diffr_stempy + + # Add a graphics/view/image + # Need to set invertY = True and row-major + self.graphics = pg.GraphicsLayoutWidget() + self.view = self.graphics.addViewBox(row=0, col=0, invertY=True) + self.view2 = self.graphics.addViewBox(row=0, col=1, invertY=True) + + self.real_space_image_item = pg.ImageItem(border=pg.mkPen('w')) + self.real_space_image_item.setImage(self.rs) + self.view.addItem(self.real_space_image_item) + self.real_space_image_item.setColorMap(self.colormap) + + self.view.setAspectLocked() + + self.diffraction_pattern_image_item = pg.ImageItem(border=pg.mkPen('w')) + self.diffraction_pattern_image_item.setImage(np.zeros((100, 100), dtype=np.uint32)) + self.view2.addItem(self.diffraction_pattern_image_item) + self.view2.setAspectLocked() + self.diffraction_pattern_image_item.setColorMap(self.colormap) + + self.diffraction_pattern_image_item.setOpts(axisOrder="row-major") + self.real_space_image_item.setOpts(axisOrder="row-major") + + self.statusBar = QStatusBar() + self.statusBar.showMessage("Starting up...") + + # Add a File menu + self.myQMenuBar = QMenuBar(self) + menu_bar_file = self.myQMenuBar.addMenu('File') + menu_bar_export = self.myQMenuBar.addMenu('Export') + menu_bar_display = self.myQMenuBar.addMenu('Display') + display_colormap = menu_bar_display.addMenu('Set colormap') + open_action = QAction('Open', self) + open_action.triggered.connect(self.open_file) + menu_bar_file.addAction(open_action) + export_diff_tif_action = QAction('Export diffraction (TIF)', self) + export_diff_tif_action.triggered.connect(self._on_export) + menu_bar_export.addAction(export_diff_tif_action) + export_diff_smv_action = QAction('Export diffraction (SMV)', self) + export_diff_smv_action.triggered.connect(self._on_export) + menu_bar_export.addAction(export_diff_smv_action) + export_real_action = QAction('Export real (TIF)', self) + export_real_action.triggered.connect(self._on_export) + menu_bar_export.addAction(export_real_action) + toggle_log_action = QAction('Toggle log(diffraction)', self) + #toggle_log_action.triggered.connect(self._on_log) + menu_bar_display.addAction(toggle_log_action) + + self.cm_actions = {} + for cm in self.available_colormaps: + self.cm_actions[cm] = QAction(cm, self) + #self.cm_actions[cm].triggered.connect(self._on_use_colormap) + display_colormap.addAction(self.cm_actions[cm]) + + self.setLayout(QVBoxLayout()) + self.layout().addWidget(self.myQMenuBar) + self.layout().addWidget(self.graphics) + self.layout().addWidget(self.statusBar) + + # Initialize the user interface objects + # Image ROI + self.real_space_roi = pg.RectROI(pos=(0, 0), size=(10, 10), + translateSnap=True, snapSize=1, scaleSnap=True, + removable=False, invertible=False, pen='g') + self.view.addItem(self.real_space_roi) + + # Diffraction ROI + self.diffraction_space_roi = pg.RectROI(pos=(0, 0), size=(10, 10), + translateSnap=True, snapSize=1, scaleSnap=True, + removable=False, invertible=False, pen='g') + for hh in self.diffraction_space_roi.getHandles() + self.real_space_roi.getHandles(): + hh.radius = self.handle_size + hh.buildPath() + hh.update() + + self.view2.addItem(self.diffraction_space_roi) + + self.open_file() + + self.real_space_roi.addRotateHandle((0, 0), (0.5, 0.5)) + + self.real_space_roi.sigRegionChanged.connect(self.update_diffr) + self.diffraction_space_roi.sigRegionChanged.connect(self.update_real) + self.real_space_roi.sigRegionChanged.connect(self._update_position_message) + self.diffraction_space_roi.sigRegionChanged.connect(self._update_position_message) + + def _update_position_message(self): + self.statusBar.showMessage( + f'Real: ({int(self.real_space_roi.pos().y())}, {int(self.real_space_roi.pos().x())}), ' + f'({int(self.real_space_roi.size().y())}, {int(self.real_space_roi.size().x())}); ' + f'Diffraction: ({int(self.diffraction_space_roi.pos().y())}, {int(self.diffraction_space_roi.pos().x())}), ' + f'({int(self.diffraction_space_roi.size().y())}, {int(self.diffraction_space_roi.size().x())})' + ) + + def open_file(self): + """ Show a dialog to choose a file to open. + """ + + fd = pg.FileDialog() + fd.setNameFilter("Sparse Stempy (*.dm3 *.dm4, *.hdf5, *.h5)") + fd.setDirectory(str(self.current_dir)) + fd.setFileMode(pg.FileDialog.ExistingFile) + + if fd.exec_(): + file_names = fd.selectedFiles() + self.current_dir = Path(file_names[0]).parent + + self.setData(Path(file_names[0])) + + def setData(self, fPath): + """ Load the data from the file. + + Parameters + ---------- + fPath : pathlib.Path + The path of to the file to load. + """ + self.statusBar.showMessage("Loading the data...") + + # Load DM data + #with ncempy.io.dm.fileDM(fPath) as f0: + # dm0 = f0.getDataset(0) + + # scanI = int(f0.allTags['.ImageList.2.ImageTags.Series.nimagesx']) + # scanJ = int(f0.allTags['.ImageList.2.ImageTags.Series.nimagesy']) + # numkI = dm0['data'].shape[2] + # numkJ = dm0['data'].shape[1] + + # self.sa = dm0['data'].reshape([scanJ,scanI,numkJ,numkI]) + + # Load Dectris data + with fileDECTRIS(fPath) as dectris: + self.sa = dectris.get_dataset(remove_bad_pixels=True) + scanI = self.sa.shape[0] + scanJ = self.sa.shape[1] + numkI = self.sa.shape[2] + numkJ = self.sa.shape[3] + + print('Data shape is {}'.format(self.sa.shape)) + + self.scan_dimensions = (scanJ, scanI) + self.frame_dimensions = (numkJ, numkI) + self.num_frames_per_scan = 1 + print('scan dimensions = {}'.format(self.scan_dimensions)) + + self.dp = np.zeros((self.frame_dimensions[0], self.frame_dimensions[1]), np.uint32) + self.rs = np.zeros((self.scan_dimensions[0], self.scan_dimensions[1]), np.uint32) + + self.diffraction_pattern_limit = QRectF(0, 0, self.frame_dimensions[0], self.frame_dimensions[1]) + self.diffraction_space_roi.maxBounds = self.diffraction_pattern_limit + + self.real_space_limit = QRectF(0, 0, self.scan_dimensions[0], self.scan_dimensions[1]) + self.real_space_roi.maxBounds = self.real_space_limit + + self.real_space_roi.setSize([ii // 4 for ii in self.scan_dimensions]) + self.diffraction_space_roi.setSize([ii // 4 for ii in self.frame_dimensions]) + + self.real_space_roi.setPos([ii // 4 + ii //8 for ii in self.scan_dimensions]) + self.diffraction_space_roi.setPos([ii // 4 + ii // 8 for ii in self.frame_dimensions]) + + self.update_real() + self.update_diffr() + + self.statusBar.showMessage('loaded {}'.format(fPath.name)) + + + def update_diffr(self): + """ Update the diffraction space image by summing in real space + """ + # Get the region of the real space ROI + out = self.real_space_roi.getArrayRegion(self.dp, self.real_space_image_item,returnMappedCoords=True,order=0) + # Setup a mask with the correct dimensions + mask = np.zeros(self.scan_dimensions, dtype=bool) + mask[np.round(out[1][0].astype(np.uint16)), np.round(out[1][1]).astype(np.uint16)] = 1 + ndimage.binary_closing(mask, output=mask) + + out2 = self.real_space_roi.getArraySlice(self.dp, self.real_space_image_item) + + temp = self.sa[out2[0][0],out2[0][1],:,:] + mask = mask[out2[0][0],out2[0][1]] + self.dp = temp.sum(axis=(0,1),where=mask[:,:,None,None], dtype=np.uint16) + + self.diffraction_pattern_image_item.setImage(np.log(self.dp + 1)) + + def update_real(self): + """ Update the real space image by summing in diffraction space + """ + self.rs = self.sa[:, :, + int(self.diffraction_space_roi.pos().y()) - 1:int(self.diffraction_space_roi.pos().y() + self.diffraction_space_roi.size().y()) + 0, + int(self.diffraction_space_roi.pos().x()) - 1:int(self.diffraction_space_roi.pos().x() + self.diffraction_space_roi.size().x()) + 0] + self.rs = self.rs.sum(axis=(2, 3)) + self.real_space_image_item.setImage(self.rs, autoRange=True) + + def _on_export(self): + """Export the shown diffraction pattern as raw data in TIF file format""" + action = self.sender() + + # Get a file path to save to in current directory + fd = pg.FileDialog() + if 'TIF' in action.text(): + fd.setNameFilter("TIF (*.tif)") + elif 'SMV' in action.text(): + fd.setNameFilter("IMG (*.IMG)") + fd.setDirectory(str(self.current_dir)) + fd.setFileMode(pg.FileDialog.AnyFile) + fd.setAcceptMode(pg.FileDialog.AcceptSave) + + if fd.exec_(): + file_name = fd.selectedFiles()[0] + out_path = Path(file_name) + else: + return + + # Get the data and change to float + if action.text() == 'Export diffraction (TIF)': + if out_path.suffix != '.tif': + out_path = out_path.with_suffix('.tif') + imwrite(out_path, self.dp.reshape(self.frame_dimensions).astype(np.float32)) + elif action.text() == 'Export diffraction (SMV)': + if out_path.suffix != '.img': + out_path = out_path.with_suffix('.img') + self._write_smv(out_path) + elif action.text() == 'Export real (TIF)': + imwrite(out_path, self.rs.reshape(self.scan_dimensions).astype(np.float32)) + else: + print('Export: unknown action {}'.format(action.text())) + + def _write_smv(self, out_path): + """Write out diffraction as SMV formatted file + Header is 512 bytes of zeros and then filled with ASCII + + camera length, wavelength, and pixel_size are hard coded. + """ + # Hard coded metadata + mag = 110 # camera length in mm + lamda = 1.9687576525122874e-12 + pixel_size = 10e-6 # micron + + im = self.dp.reshape(self.frame_dimensions) + if im.max() > 65535: + im[im > 65535] = 65535 # maximum 16 bit value allowed + im[im < 0] = 0 # just in case + print('warning. Loss of dynamic range due to conversion from 32 bit to 16 bit') + im = im.astype(np.uint16) + dtype = 'unsigned_short' + + #if self.dp.dtype == np.uint16: + # dtype = 'unsigned_short' + #elif im.dtype == np.uint32: + # dtype = 'unsigned_long' + #else: + # raise TypeError('Unsupported dtype: {}'.format(im.dtype)) + + # Write 512 bytes of zeros + with open(out_path, 'wb') as f0: + f0.write(np.zeros(512, dtype=np.uint8)) + # Write the header over the zeros as needed + with open(out_path, 'r+') as f0: + f0.write("{\nHEADER_BYTES=512;\n") + f0.write("DIM=2;\n") + f0.write("BYTE_ORDER=little_endian;\n") + f0.write(f"TYPE={dtype};\n") + f0.write(f"SIZE1={im.shape[1]};\n") # size1 is columns + f0.write(f"SIZE2={im.shape[0]};\n") # size 2 is rows + f0.write(f"PIXEL_SIZE={pixel_size};\n") # physical pixel size in micron + f0.write(f"WAVELENGTH={lamda};\n") # wavelength + if mag: + f0.write(f"DISTANCE={int(mag)};\n") + f0.write("PHI=0.0;\n") + f0.write("BEAM_CENTER_X=1.0;\n") + f0.write("BEAM_CENTER_Y=1.0;\n") + f0.write("BIN=1x1;\n") + f0.write("DATE=Thu Oct 21 23:06:09 2021;\n") + f0.write("DETECTOR_SN=unknown;\n") + f0.write("OSC_RANGE=1.0;\n") + f0.write("OSC_START=0;\n") + f0.write("IMAGE_PEDESTAL=0;\n") + f0.write("TIME=10.0;\n") + f0.write("TWOTHETA=0;\n") + f0.write("}\n") + # Append the binary image data at the end of the header + with open(out_path, 'rb+') as f0: + f0.seek(512, 0) + f0.write(im) +if __name__ == '__main__': + """Main function used to start the GUI.""" + + qapp = QApplication([]) + fourD_view = fourD() + fourD_view.show() + qapp.exec_() From 82f2437b13e5d7625d673fca0eb7063f821fe5ca Mon Sep 17 00:00:00 2001 From: Peter Ercius Date: Thu, 27 Jun 2024 13:13:40 -0700 Subject: [PATCH 2/9] rename rotatable to a TitanX version --- DuSC_explorer/{rotatable.py => DuSC_titanX.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename DuSC_explorer/{rotatable.py => DuSC_titanX.py} (100%) diff --git a/DuSC_explorer/rotatable.py b/DuSC_explorer/DuSC_titanX.py similarity index 100% rename from DuSC_explorer/rotatable.py rename to DuSC_explorer/DuSC_titanX.py From 82c40ea60d5ce4e4813c363112a08bf0ffb4b36f Mon Sep 17 00:00:00 2001 From: Peter Ercius Date: Wed, 28 Aug 2024 11:16:57 -0700 Subject: [PATCH 3/9] fix colormap issue. Remove rotating code. Simplify real space image generation. --- DuSC_explorer/DuSC_dectris.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/DuSC_explorer/DuSC_dectris.py b/DuSC_explorer/DuSC_dectris.py index a4d3548..13ce300 100644 --- a/DuSC_explorer/DuSC_dectris.py +++ b/DuSC_explorer/DuSC_dectris.py @@ -13,8 +13,8 @@ import pyqtgraph as pg from pyqtgraph.graphicsItems.ROI import Handle import numpy as np -from scipy import ndimage -import ncempy +#from scipy import ndimage +#import ncempy #from tifffile import imwrite import h5py import hdf5plugin @@ -223,7 +223,7 @@ def __init__(self, *args, **kwargs): self.open_file() - self.real_space_roi.addRotateHandle((0, 0), (0.5, 0.5)) + # self.real_space_roi.addRotateHandle((0, 0), (0.5, 0.5)) self.real_space_roi.sigRegionChanged.connect(self.update_diffr) self.diffraction_space_roi.sigRegionChanged.connect(self.update_real) @@ -243,7 +243,7 @@ def open_file(self): """ fd = pg.FileDialog() - fd.setNameFilter("Sparse Stempy (*.dm3 *.dm4, *.hdf5, *.h5)") + fd.setNameFilter("Dectis (*.hdf5, *.h5)") fd.setDirectory(str(self.current_dir)) fd.setFileMode(pg.FileDialog.ExistingFile) @@ -314,26 +314,31 @@ def update_diffr(self): """ Update the diffraction space image by summing in real space """ # Get the region of the real space ROI - out = self.real_space_roi.getArrayRegion(self.dp, self.real_space_image_item,returnMappedCoords=True,order=0) + #out = self.real_space_roi.getArrayRegion(self.dp, self.real_space_image_item,returnMappedCoords=True,order=0) + # Setup a mask with the correct dimensions - mask = np.zeros(self.scan_dimensions, dtype=bool) - mask[np.round(out[1][0].astype(np.uint16)), np.round(out[1][1]).astype(np.uint16)] = 1 - ndimage.binary_closing(mask, output=mask) + #mask = np.zeros(self.scan_dimensions, dtype=bool) + #mask[np.round(out[1][0].astype(np.uint16)), np.round(out[1][1]).astype(np.uint16)] = 1 + #ndimage.binary_closing(mask, output=mask) - out2 = self.real_space_roi.getArraySlice(self.dp, self.real_space_image_item) - - temp = self.sa[out2[0][0],out2[0][1],:,:] - mask = mask[out2[0][0],out2[0][1]] - self.dp = temp.sum(axis=(0,1),where=mask[:,:,None,None], dtype=np.uint16) + #out2 = self.real_space_roi.getArraySlice(self.dp, self.real_space_image_item) + #temp = self.sa[out2[0][0],out2[0][1],:,:] + #mask = mask[out2[0][0],out2[0][1]] + #self.dp = temp.sum(axis=(0,1), dtype=np.uint64) + + self.dp = self.sa[int(self.real_space_roi.pos().y()) - 0:int(self.real_space_roi.pos().y() + self.real_space_roi.size().y()) + 1, + int(self.real_space_roi.pos().x()) - 0:int(self.real_space_roi.pos().x() + self.real_space_roi.size().x()) + 1, + :, :] + self.dp = self.dp.sum(axis=(0,1)) self.diffraction_pattern_image_item.setImage(np.log(self.dp + 1)) def update_real(self): """ Update the real space image by summing in diffraction space """ self.rs = self.sa[:, :, - int(self.diffraction_space_roi.pos().y()) - 1:int(self.diffraction_space_roi.pos().y() + self.diffraction_space_roi.size().y()) + 0, - int(self.diffraction_space_roi.pos().x()) - 1:int(self.diffraction_space_roi.pos().x() + self.diffraction_space_roi.size().x()) + 0] + int(self.diffraction_space_roi.pos().y()) - 0:int(self.diffraction_space_roi.pos().y() + self.diffraction_space_roi.size().y()) + 1, + int(self.diffraction_space_roi.pos().x()) - 0:int(self.diffraction_space_roi.pos().x() + self.diffraction_space_roi.size().x()) + 1] self.rs = self.rs.sum(axis=(2, 3)) self.real_space_image_item.setImage(self.rs, autoRange=True) From bc16ff2a9930cca300a7a8e79eec5ea95db09244 Mon Sep 17 00:00:00 2001 From: Peter Ercius Date: Wed, 28 Aug 2024 11:19:11 -0700 Subject: [PATCH 4/9] improve file loading --- DuSC_explorer/DuSC_dectris.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuSC_explorer/DuSC_dectris.py b/DuSC_explorer/DuSC_dectris.py index 13ce300..b9accf4 100644 --- a/DuSC_explorer/DuSC_dectris.py +++ b/DuSC_explorer/DuSC_dectris.py @@ -243,7 +243,7 @@ def open_file(self): """ fd = pg.FileDialog() - fd.setNameFilter("Dectis (*.hdf5, *.h5)") + fd.setNameFilter("Dectris (*_master.h5)") fd.setDirectory(str(self.current_dir)) fd.setFileMode(pg.FileDialog.ExistingFile) From c1bb8b12f0293a464f1072c9ed921cee90b7ce60 Mon Sep 17 00:00:00 2001 From: Peter Ercius Date: Thu, 29 Aug 2024 17:39:47 -0700 Subject: [PATCH 5/9] Change bad pixels to mean. Change window title --- DuSC_explorer/DuSC_dectris.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DuSC_explorer/DuSC_dectris.py b/DuSC_explorer/DuSC_dectris.py index b9accf4..16afb0b 100644 --- a/DuSC_explorer/DuSC_dectris.py +++ b/DuSC_explorer/DuSC_dectris.py @@ -105,7 +105,8 @@ def get_dataset(self, remove_bad_pixels=False): self.data_shape = (shape_square, shape_square, dd.shape[1], dd.shape[2]) dd = dd.reshape(self.data_shape) if remove_bad_pixels: - self.remove_bad_pixels(dd) + mm = dd.mean() + self.remove_bad_pixels(dd, value=mm) return dd def remove_bad_pixels(self, data, value=0, bad_pixels=None): @@ -138,7 +139,7 @@ def __init__(self, *args, **kwargs): self.colormap = pg.colormap.getFromMatplotlib('grey') super(fourD, self).__init__(*args, *kwargs) - self.setWindowTitle("NCEM: TitanX 4D Data Explorer") + self.setWindowTitle("NCEM: Dectris Arina 4D Data Explorer") self.setWindowIcon(QtGui.QIcon('MF_logo_only_small.ico')) # Set the update strategy to the JIT version From 807d041e6f82a97cc7a9e7717e20ffff7c381d04 Mon Sep 17 00:00:00 2001 From: Peter Ercius ncem-gauss jupyter Date: Thu, 29 Aug 2024 17:45:55 -0700 Subject: [PATCH 6/9] fix colormap issue --- DuSC_explorer/DuSC_dectris.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DuSC_explorer/DuSC_dectris.py b/DuSC_explorer/DuSC_dectris.py index 16afb0b..5864116 100644 --- a/DuSC_explorer/DuSC_dectris.py +++ b/DuSC_explorer/DuSC_dectris.py @@ -134,9 +134,8 @@ def __init__(self, *args, **kwargs): self.log_diffraction = True self.handle_size = 10 - self.available_colormaps = ['thermal', 'flame', 'yellowy', 'bipolar', 'spectrum', 'cyclic', 'greyclip', 'grey', - 'viridis', 'inferno', 'plasma', 'magma'] - self.colormap = pg.colormap.getFromMatplotlib('grey') + self.available_colormaps = ['viridis', 'inferno', 'plasma', 'magma','cividis','CET-C5','CET-C5s'] + self.colormap = 'cividis' # default colormap super(fourD, self).__init__(*args, *kwargs) self.setWindowTitle("NCEM: Dectris Arina 4D Data Explorer") From 7bf2f733d829bcf88e120e998570c2e0851462ba Mon Sep 17 00:00:00 2001 From: Peter Ercius ncem-gauss jupyter Date: Tue, 28 Oct 2025 09:07:06 -0700 Subject: [PATCH 7/9] remove titanX version. This will be added to its own feature branch. --- DuSC_explorer/DuSC_titanX.py | 335 ----------------------------------- 1 file changed, 335 deletions(-) delete mode 100644 DuSC_explorer/DuSC_titanX.py diff --git a/DuSC_explorer/DuSC_titanX.py b/DuSC_explorer/DuSC_titanX.py deleted file mode 100644 index fe2d9b6..0000000 --- a/DuSC_explorer/DuSC_titanX.py +++ /dev/null @@ -1,335 +0,0 @@ -""" -Load TitanX data and show real and reciprocal space. - -The real space ROI can be rotated - -author: Peter Ercius -date: 2023/11/02 - -""" - -from pathlib import Path - -import pyqtgraph as pg -from pyqtgraph.graphicsItems.ROI import Handle -import numpy as np -from scipy import ndimage -import ncempy -from tifffile import imwrite - -from qtpy.QtWidgets import * -from qtpy.QtCore import QRectF -from qtpy import QtGui - -class fourD(QWidget): - - def __init__(self, *args, **kwargs): - - self.real_space_limit = None - self.diffraction_pattern_limit = None - self.sa = None - self.current_dir = Path.home() - self.scale = 1 - self.center = (287, 287) - self.scan_dimensions = (0, 0) - self.frame_dimensions = (576, 576) - self.tt = None - self.dp = None - self.rs = None - self.log_diffraction = True - self.handle_size = 10 - - self.available_colormaps = ['thermal', 'flame', 'yellowy', 'bipolar', 'spectrum', 'cyclic', 'greyclip', 'grey', - 'viridis', 'inferno', 'plasma', 'magma'] - self.colormap = pg.colormap.getFromMatplotlib('grey') - - super(fourD, self).__init__(*args, *kwargs) - self.setWindowTitle("NCEM: TitanX 4D Data Explorer") - self.setWindowIcon(QtGui.QIcon('MF_logo_only_small.ico')) - - # Set the update strategy to the JIT version - #self.update_real = self.update_real_stempy - #self.update_diffr = self.update_diffr_stempy - - # Add a graphics/view/image - # Need to set invertY = True and row-major - self.graphics = pg.GraphicsLayoutWidget() - self.view = self.graphics.addViewBox(row=0, col=0, invertY=True) - self.view2 = self.graphics.addViewBox(row=0, col=1, invertY=True) - - self.real_space_image_item = pg.ImageItem(border=pg.mkPen('w')) - self.real_space_image_item.setImage(self.rs) - self.view.addItem(self.real_space_image_item) - self.real_space_image_item.setColorMap(self.colormap) - - self.view.setAspectLocked() - - self.diffraction_pattern_image_item = pg.ImageItem(border=pg.mkPen('w')) - self.diffraction_pattern_image_item.setImage(np.zeros((100, 100), dtype=np.uint32)) - self.view2.addItem(self.diffraction_pattern_image_item) - self.view2.setAspectLocked() - self.diffraction_pattern_image_item.setColorMap(self.colormap) - - self.diffraction_pattern_image_item.setOpts(axisOrder="row-major") - self.real_space_image_item.setOpts(axisOrder="row-major") - - self.statusBar = QStatusBar() - self.statusBar.showMessage("Starting up...") - - # Add a File menu - self.myQMenuBar = QMenuBar(self) - menu_bar_file = self.myQMenuBar.addMenu('File') - menu_bar_export = self.myQMenuBar.addMenu('Export') - menu_bar_display = self.myQMenuBar.addMenu('Display') - display_colormap = menu_bar_display.addMenu('Set colormap') - open_action = QAction('Open', self) - open_action.triggered.connect(self.open_file) - menu_bar_file.addAction(open_action) - export_diff_tif_action = QAction('Export diffraction (TIF)', self) - export_diff_tif_action.triggered.connect(self._on_export) - menu_bar_export.addAction(export_diff_tif_action) - export_diff_smv_action = QAction('Export diffraction (SMV)', self) - export_diff_smv_action.triggered.connect(self._on_export) - menu_bar_export.addAction(export_diff_smv_action) - export_real_action = QAction('Export real (TIF)', self) - export_real_action.triggered.connect(self._on_export) - menu_bar_export.addAction(export_real_action) - toggle_log_action = QAction('Toggle log(diffraction)', self) - #toggle_log_action.triggered.connect(self._on_log) - menu_bar_display.addAction(toggle_log_action) - - self.cm_actions = {} - for cm in self.available_colormaps: - self.cm_actions[cm] = QAction(cm, self) - #self.cm_actions[cm].triggered.connect(self._on_use_colormap) - display_colormap.addAction(self.cm_actions[cm]) - - self.setLayout(QVBoxLayout()) - self.layout().addWidget(self.myQMenuBar) - self.layout().addWidget(self.graphics) - self.layout().addWidget(self.statusBar) - - # Initialize the user interface objects - # Image ROI - self.real_space_roi = pg.RectROI(pos=(0, 0), size=(10, 10), - translateSnap=True, snapSize=1, scaleSnap=True, - removable=False, invertible=False, pen='g') - self.view.addItem(self.real_space_roi) - - # Diffraction ROI - self.diffraction_space_roi = pg.RectROI(pos=(0, 0), size=(10, 10), - translateSnap=True, snapSize=1, scaleSnap=True, - removable=False, invertible=False, pen='g') - for hh in self.diffraction_space_roi.getHandles() + self.real_space_roi.getHandles(): - hh.radius = self.handle_size - hh.buildPath() - hh.update() - - self.view2.addItem(self.diffraction_space_roi) - - self.open_file() - - self.real_space_roi.addRotateHandle((0, 0), (0.5, 0.5)) - - self.real_space_roi.sigRegionChanged.connect(self.update_diffr) - self.diffraction_space_roi.sigRegionChanged.connect(self.update_real) - self.real_space_roi.sigRegionChanged.connect(self._update_position_message) - self.diffraction_space_roi.sigRegionChanged.connect(self._update_position_message) - - def _update_position_message(self): - self.statusBar.showMessage( - f'Real: ({int(self.real_space_roi.pos().y())}, {int(self.real_space_roi.pos().x())}), ' - f'({int(self.real_space_roi.size().y())}, {int(self.real_space_roi.size().x())}); ' - f'Diffraction: ({int(self.diffraction_space_roi.pos().y())}, {int(self.diffraction_space_roi.pos().x())}), ' - f'({int(self.diffraction_space_roi.size().y())}, {int(self.diffraction_space_roi.size().x())})' - ) - - def open_file(self): - """ Show a dialog to choose a file to open. - """ - - fd = pg.FileDialog() - fd.setNameFilter("Sparse Stempy (*.dm3 *.dm4)") - fd.setDirectory(str(self.current_dir)) - fd.setFileMode(pg.FileDialog.ExistingFile) - - if fd.exec_(): - file_names = fd.selectedFiles() - self.current_dir = Path(file_names[0]).parent - - self.setData(Path(file_names[0])) - - def setData(self, fPath): - """ Load the data from the DM file. - - Parameters - ---------- - fPath : pathlib.Path - The path of to the file to load. - """ - self.statusBar.showMessage("Loading the data...") - - # Load data as a SparseArray class - with ncempy.io.dm.fileDM(fPath) as f0: - dm0 = f0.getDataset(0) - - scanI = int(f0.allTags['.ImageList.2.ImageTags.Series.nimagesx']) - scanJ = int(f0.allTags['.ImageList.2.ImageTags.Series.nimagesy']) - numkI = dm0['data'].shape[2] - numkJ = dm0['data'].shape[1] - - self.sa = dm0['data'].reshape([scanJ,scanI,numkJ,numkI]) - - print('Data shape is {}'.format(self.sa.shape)) - - self.scan_dimensions = (scanJ, scanI) - self.frame_dimensions = (numkJ, numkI) - self.num_frames_per_scan = 1 - print('scan dimensions = {}'.format(self.scan_dimensions)) - - self.dp = np.zeros((self.frame_dimensions[0], self.frame_dimensions[1]), np.uint32) - self.rs = np.zeros((self.scan_dimensions[0], self.scan_dimensions[1]), np.uint32) - - self.diffraction_pattern_limit = QRectF(0, 0, self.frame_dimensions[0], self.frame_dimensions[1]) - self.diffraction_space_roi.maxBounds = self.diffraction_pattern_limit - - self.real_space_limit = QRectF(0, 0, self.scan_dimensions[0], self.scan_dimensions[1]) - self.real_space_roi.maxBounds = self.real_space_limit - - self.real_space_roi.setSize([ii // 4 for ii in self.scan_dimensions]) - self.diffraction_space_roi.setSize([ii // 4 for ii in self.frame_dimensions]) - - self.real_space_roi.setPos([ii // 4 + ii //8 for ii in self.scan_dimensions]) - self.diffraction_space_roi.setPos([ii // 4 + ii // 8 for ii in self.frame_dimensions]) - - self.update_real() - self.update_diffr() - - self.statusBar.showMessage('loaded {}'.format(fPath.name)) - - - def update_diffr(self): - """ Update the diffraction space image by summing in real space - """ - # Get the region of the real space ROI - out = self.real_space_roi.getArrayRegion(self.dp, self.real_space_image_item,returnMappedCoords=True,order=0) - # Setup a mask with the correct dimensions - mask = np.zeros(self.scan_dimensions, dtype=bool) - mask[np.round(out[1][0].astype(np.uint16)), np.round(out[1][1]).astype(np.uint16)] = 1 - ndimage.binary_closing(mask, output=mask) - - out2 = self.real_space_roi.getArraySlice(self.dp, self.real_space_image_item) - - temp = self.sa[out2[0][0],out2[0][1],:,:] - mask = mask[out2[0][0],out2[0][1]] - self.dp = temp.sum(axis=(0,1),where=mask[:,:,None,None], dtype=np.uint16) - - self.diffraction_pattern_image_item.setImage(np.log(self.dp + 1)) - - def update_real(self): - """ Update the real space image by summing in diffraction space - """ - self.rs = self.sa[:, :, - int(self.diffraction_space_roi.pos().y()) - 1:int(self.diffraction_space_roi.pos().y() + self.diffraction_space_roi.size().y()) + 0, - int(self.diffraction_space_roi.pos().x()) - 1:int(self.diffraction_space_roi.pos().x() + self.diffraction_space_roi.size().x()) + 0] - self.rs = self.rs.sum(axis=(2, 3)) - self.real_space_image_item.setImage(self.rs, autoRange=True) - - def _on_export(self): - """Export the shown diffraction pattern as raw data in TIF file format""" - action = self.sender() - - # Get a file path to save to in current directory - fd = pg.FileDialog() - if 'TIF' in action.text(): - fd.setNameFilter("TIF (*.tif)") - elif 'SMV' in action.text(): - fd.setNameFilter("IMG (*.IMG)") - fd.setDirectory(str(self.current_dir)) - fd.setFileMode(pg.FileDialog.AnyFile) - fd.setAcceptMode(pg.FileDialog.AcceptSave) - - if fd.exec_(): - file_name = fd.selectedFiles()[0] - out_path = Path(file_name) - else: - return - - # Get the data and change to float - if action.text() == 'Export diffraction (TIF)': - if out_path.suffix != '.tif': - out_path = out_path.with_suffix('.tif') - imwrite(out_path, self.dp.reshape(self.frame_dimensions).astype(np.float32)) - elif action.text() == 'Export diffraction (SMV)': - if out_path.suffix != '.img': - out_path = out_path.with_suffix('.img') - self._write_smv(out_path) - elif action.text() == 'Export real (TIF)': - imwrite(out_path, self.rs.reshape(self.scan_dimensions).astype(np.float32)) - else: - print('Export: unknown action {}'.format(action.text())) - - def _write_smv(self, out_path): - """Write out diffraction as SMV formatted file - Header is 512 bytes of zeros and then filled with ASCII - - camera length, wavelength, and pixel_size are hard coded. - """ - # Hard coded metadata - mag = 110 # camera length in mm - lamda = 1.9687576525122874e-12 - pixel_size = 10e-6 # micron - - im = self.dp.reshape(self.frame_dimensions) - if im.max() > 65535: - im[im > 65535] = 65535 # maximum 16 bit value allowed - im[im < 0] = 0 # just in case - print('warning. Loss of dynamic range due to conversion from 32 bit to 16 bit') - im = im.astype(np.uint16) - dtype = 'unsigned_short' - - #if self.dp.dtype == np.uint16: - # dtype = 'unsigned_short' - #elif im.dtype == np.uint32: - # dtype = 'unsigned_long' - #else: - # raise TypeError('Unsupported dtype: {}'.format(im.dtype)) - - # Write 512 bytes of zeros - with open(out_path, 'wb') as f0: - f0.write(np.zeros(512, dtype=np.uint8)) - # Write the header over the zeros as needed - with open(out_path, 'r+') as f0: - f0.write("{\nHEADER_BYTES=512;\n") - f0.write("DIM=2;\n") - f0.write("BYTE_ORDER=little_endian;\n") - f0.write(f"TYPE={dtype};\n") - f0.write(f"SIZE1={im.shape[1]};\n") # size1 is columns - f0.write(f"SIZE2={im.shape[0]};\n") # size 2 is rows - f0.write(f"PIXEL_SIZE={pixel_size};\n") # physical pixel size in micron - f0.write(f"WAVELENGTH={lamda};\n") # wavelength - if mag: - f0.write(f"DISTANCE={int(mag)};\n") - f0.write("PHI=0.0;\n") - f0.write("BEAM_CENTER_X=1.0;\n") - f0.write("BEAM_CENTER_Y=1.0;\n") - f0.write("BIN=1x1;\n") - f0.write("DATE=Thu Oct 21 23:06:09 2021;\n") - f0.write("DETECTOR_SN=unknown;\n") - f0.write("OSC_RANGE=1.0;\n") - f0.write("OSC_START=0;\n") - f0.write("IMAGE_PEDESTAL=0;\n") - f0.write("TIME=10.0;\n") - f0.write("TWOTHETA=0;\n") - f0.write("}\n") - # Append the binary image data at the end of the header - with open(out_path, 'rb+') as f0: - f0.seek(512, 0) - f0.write(im) -if __name__ == '__main__': - """Main function used to start the GUI.""" - - qapp = QApplication([]) - fourD_view = fourD() - fourD_view.show() - qapp.exec_() From a6f809b861e804db41a3fa579ea28e7cf41c0f8e Mon Sep 17 00:00:00 2001 From: Peter Ercius ncem-gauss jupyter Date: Tue, 28 Oct 2025 09:11:40 -0700 Subject: [PATCH 8/9] remove fileDECTRIS class and use ncempy version instead. --- DuSC_explorer/DuSC_dectris.py | 97 +---------------------------------- 1 file changed, 2 insertions(+), 95 deletions(-) diff --git a/DuSC_explorer/DuSC_dectris.py b/DuSC_explorer/DuSC_dectris.py index 5864116..ca3d4df 100644 --- a/DuSC_explorer/DuSC_dectris.py +++ b/DuSC_explorer/DuSC_dectris.py @@ -14,7 +14,7 @@ from pyqtgraph.graphicsItems.ROI import Handle import numpy as np #from scipy import ndimage -#import ncempy +import ncempy #from tifffile import imwrite import h5py import hdf5plugin @@ -23,99 +23,6 @@ from qtpy.QtCore import QRectF from qtpy import QtGui -class fileDECTRIS: - def __init__(self, filename, verbose=False): - self._verbose = verbose - self.raw_shape = [0, 0, 0] # shape of data on disk - self.data_shape = [0, 0, 0, 0] # the shape of the final 4D dataset - self.file_hdl = None - self.data_dtype = None - - self.bad_pixels = ((49, 75), (93,118), (95,119), (108, 57)) - - if hasattr(filename, 'read'): - try: - self.file_path = Path(filename.name) - self.file_name = self.file_path.name - except AttributeError: - self.file_path = None - self.file_name = None - else: - # check filename type, change to pathlib.Path - if isinstance(filename, str): - filename = Path(filename) - elif isinstance(filename, Path): - pass - else: - raise TypeError('Filename is supposed to be a string or pathlib.Path or file object') - self.file_path = filename - self.file_name = self.file_path.name - - # Try opening the file - try: - self.file_hdl = h5py.File(filename, 'r') - assert self.file_hdl['/entry/data'] - except: - print('Error opening file: "{}"'.format(filename)) - raise - - # if this is a HDF5 file - if self.file_hdl: - # Find the initial shape of the data set - for v in self.file_hdl['/entry/data'].values(): - self.raw_shape[0] = self.raw_shape[0] + v.shape[0] - self.raw_shape[1] = v.shape[1] - self.raw_shape[2] = v.shape[2] - self.data_dtype = v.dtype - - def __del__(self): - """Destructor for EMD file object. - - """ - # close the file - # if(not self.file_hdl.closed): - self.file_hdl.close() - - def __enter__(self): - """Implement python's with statement - - """ - return self - - def __exit__(self, exception_type, exception_value, traceback): - """Implement python's with statement - and close the file via __del__() - """ - self.__del__() - return None - - def get_dataset(self, remove_bad_pixels=False): - - # Pre allocate space - dd = np.zeros(self.raw_shape, dtype=self.data_dtype) - # Read in the data in all linked files - ii = 0 - for v in self.file_hdl['/entry/data'].values(): - dd[ii:ii+v.shape[0]] = v[:] - ii += v.shape[0] - - # Reshape assuming square - shape_square = int((dd.shape[0])**0.5) - assert dd.shape[0] == shape_square**2 - self.data_shape = (shape_square, shape_square, dd.shape[1], dd.shape[2]) - dd = dd.reshape(self.data_shape) - if remove_bad_pixels: - mm = dd.mean() - self.remove_bad_pixels(dd, value=mm) - return dd - - def remove_bad_pixels(self, data, value=0, bad_pixels=None): - if bad_pixels: - self.bad_pixels = bad_pixels - for bad in self.bad_pixels: - data[:,:,bad[0], bad[1]] = value - - class fourD(QWidget): def __init__(self, *args, **kwargs): @@ -275,7 +182,7 @@ def setData(self, fPath): # self.sa = dm0['data'].reshape([scanJ,scanI,numkJ,numkI]) # Load Dectris data - with fileDECTRIS(fPath) as dectris: + with ncempy.io.dectris.fileDECTRIS(fPath) as dectris: self.sa = dectris.get_dataset(remove_bad_pixels=True) scanI = self.sa.shape[0] scanJ = self.sa.shape[1] From a92834d54801d6af25c0fc27a89472bd53a6f79f Mon Sep 17 00:00:00 2001 From: Peter Ercius ncem-gauss jupyter Date: Tue, 28 Oct 2025 09:18:29 -0700 Subject: [PATCH 9/9] remove old code. Refactor class name. Update how the GUI can be started. --- DuSC_explorer/DuSC_dectris.py | 57 ++++++++++------------------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/DuSC_explorer/DuSC_dectris.py b/DuSC_explorer/DuSC_dectris.py index ca3d4df..05ca8c0 100644 --- a/DuSC_explorer/DuSC_dectris.py +++ b/DuSC_explorer/DuSC_dectris.py @@ -1,10 +1,9 @@ """ -Load TitanX data and show real and reciprocal space. +Load Dectris Arina data and show real and reciprocal space. -The real space ROI can be rotated +Based on DuSC_explorer, but uses dense operations. author: Peter Ercius -date: 2023/11/02 """ @@ -13,9 +12,8 @@ import pyqtgraph as pg from pyqtgraph.graphicsItems.ROI import Handle import numpy as np -#from scipy import ndimage import ncempy -#from tifffile import imwrite +from tifffile import imwrite import h5py import hdf5plugin @@ -23,7 +21,7 @@ from qtpy.QtCore import QRectF from qtpy import QtGui -class fourD(QWidget): +class DuSC(QWidget): def __init__(self, *args, **kwargs): @@ -44,14 +42,10 @@ def __init__(self, *args, **kwargs): self.available_colormaps = ['viridis', 'inferno', 'plasma', 'magma','cividis','CET-C5','CET-C5s'] self.colormap = 'cividis' # default colormap - super(fourD, self).__init__(*args, *kwargs) + super(DuSC, self).__init__(*args, *kwargs) self.setWindowTitle("NCEM: Dectris Arina 4D Data Explorer") self.setWindowIcon(QtGui.QIcon('MF_logo_only_small.ico')) - # Set the update strategy to the JIT version - #self.update_real = self.update_real_stempy - #self.update_diffr = self.update_diffr_stempy - # Add a graphics/view/image # Need to set invertY = True and row-major self.graphics = pg.GraphicsLayoutWidget() @@ -130,8 +124,6 @@ def __init__(self, *args, **kwargs): self.open_file() - # self.real_space_roi.addRotateHandle((0, 0), (0.5, 0.5)) - self.real_space_roi.sigRegionChanged.connect(self.update_diffr) self.diffraction_space_roi.sigRegionChanged.connect(self.update_real) self.real_space_roi.sigRegionChanged.connect(self._update_position_message) @@ -169,17 +161,6 @@ def setData(self, fPath): The path of to the file to load. """ self.statusBar.showMessage("Loading the data...") - - # Load DM data - #with ncempy.io.dm.fileDM(fPath) as f0: - # dm0 = f0.getDataset(0) - - # scanI = int(f0.allTags['.ImageList.2.ImageTags.Series.nimagesx']) - # scanJ = int(f0.allTags['.ImageList.2.ImageTags.Series.nimagesy']) - # numkI = dm0['data'].shape[2] - # numkJ = dm0['data'].shape[1] - - # self.sa = dm0['data'].reshape([scanJ,scanI,numkJ,numkI]) # Load Dectris data with ncempy.io.dectris.fileDECTRIS(fPath) as dectris: @@ -216,23 +197,9 @@ def setData(self, fPath): self.statusBar.showMessage('loaded {}'.format(fPath.name)) - def update_diffr(self): """ Update the diffraction space image by summing in real space """ - # Get the region of the real space ROI - #out = self.real_space_roi.getArrayRegion(self.dp, self.real_space_image_item,returnMappedCoords=True,order=0) - - # Setup a mask with the correct dimensions - #mask = np.zeros(self.scan_dimensions, dtype=bool) - #mask[np.round(out[1][0].astype(np.uint16)), np.round(out[1][1]).astype(np.uint16)] = 1 - #ndimage.binary_closing(mask, output=mask) - - #out2 = self.real_space_roi.getArraySlice(self.dp, self.real_space_image_item) - - #temp = self.sa[out2[0][0],out2[0][1],:,:] - #mask = mask[out2[0][0],out2[0][1]] - #self.dp = temp.sum(axis=(0,1), dtype=np.uint64) self.dp = self.sa[int(self.real_space_roi.pos().y()) - 0:int(self.real_space_roi.pos().y() + self.real_space_roi.size().y()) + 1, int(self.real_space_roi.pos().x()) - 0:int(self.real_space_roi.pos().x() + self.real_space_roi.size().x()) + 1, @@ -340,10 +307,18 @@ def _write_smv(self, out_path): with open(out_path, 'rb+') as f0: f0.seek(512, 0) f0.write(im) -if __name__ == '__main__': + +def open_file(): + """Start the graphical user interface by opening a file. This is used from a python interpreter.""" + main() + +def main(): """Main function used to start the GUI.""" qapp = QApplication([]) - fourD_view = fourD() - fourD_view.show() + DuSC_view = DuSC() + DuSC_view.show() qapp.exec_() + +if __name__ == '__main__': + main()