From 2fcc2a1ecfe77526b241ca19d28ef5db534e6c6b Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 6 Dec 2023 16:16:43 -0500 Subject: [PATCH 1/8] add h5py as dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a9a409e..73d3608 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ classifiers = [ dependencies = [ "py4dstem >= 0.14.3", "emdfile >= 0.0.11", + "h5py", "numpy >= 1.19", "matplotlib >= 3.2.2", "PyQt5 >= 5.10", From 37b8fd9b3d7d56102f214551fb5c5faa1dc01a8c Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 10 Dec 2023 14:08:29 -0500 Subject: [PATCH 2/8] integrate with empad2 module --- pyproject.toml | 2 +- src/py4D_browser/empad2_reader.py | 70 +++++++++++++++++++++++++++++++ src/py4D_browser/main_window.py | 35 ++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/py4D_browser/empad2_reader.py diff --git a/pyproject.toml b/pyproject.toml index 73d3608..f2f330d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "py4D_browser" -version = "0.9999" +version = "0.99999" authors = [ { name="Steven Zeltmann", email="steven.zeltmann@berkeley.edu" }, ] diff --git a/src/py4D_browser/empad2_reader.py b/src/py4D_browser/empad2_reader.py new file mode 100644 index 0000000..5aba496 --- /dev/null +++ b/src/py4D_browser/empad2_reader.py @@ -0,0 +1,70 @@ +import empad2 +from PyQt5.QtWidgets import QFileDialog, QMessageBox +import numpy as np + + +def set_empad2_sensor(self, sensor_name): + self.empad2_calibrations = empad2.load_calibration_data(sensor=sensor_name) + + +def load_empad2_background(self): + if self.empad2_calibrations is not None: + filename = raw_file_dialog(self) + self.empad2_background = empad2.load_background( + filepath=filename, calibration_data=self.empad2_calibrations + ) + else: + QMessageBox.warning( + self, "No calibrations loaded!", "Please select a sensor first" + ) + + +def load_empad2_dataset(self): + if self.empad2_calibrations is not None: + dummy_data = False + if self.empad2_background is None: + continue_wo_bkg = QMessageBox.question( + self, + "Load without background?", + "Background data has not been loaded. Do you want to continue loading data?", + ) + if continue_wo_bkg == QMessageBox.No: + return + else: + self.empad2_background = { + "even": np.zeros((128, 128), dtype=np.float32), + "odd": np.zeros((128, 128), dtype=np.float32), + } + dummy_data = True + + filename = raw_file_dialog(self) + self.datacube = empad2.load_dataset( + filename, self.empad2_background, self.empad2_calibrations + ) + + if dummy_data: + self.empad2_background = None + + self.update_diffraction_space_view(reset=True) + self.update_real_space_view(reset=True) + + self.setWindowTitle(filename) + + else: + QMessageBox.warning( + self, "No calibrations loaded!", "Please select a sensor first" + ) + + +def raw_file_dialog(browser): + filename = QFileDialog.getOpenFileName( + browser, + "Open EMPAD-G2 Data", + "", + "EMPAD-G2 Data (*.raw);;Any file(*)", + ) + if filename is not None and len(filename[0]) > 0: + return filename[0] + else: + print("File was invalid, or something?") + raise ValueError("Could not read file") diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 3717388..1b72bcd 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -15,6 +15,7 @@ from functools import partial from pathlib import Path +import importlib from py4D_browser.utils import pg_point_roi @@ -44,6 +45,14 @@ class DataViewer(QMainWindow): update_annulus_radii, ) + HAS_EMPAD2 = importlib.util.find_spec("empad2") is not None + if HAS_EMPAD2: + from py4D_browser.empad2_reader import ( + set_empad2_sensor, + load_empad2_background, + load_empad2_dataset, + ) + def __init__(self, argv): super().__init__() # Define this as the QApplication object @@ -92,6 +101,32 @@ def setup_menus(self): self.load_binned_action.triggered.connect(self.load_data_bin) self.file_menu.addAction(self.load_binned_action) + # EMPAD2 menu + if self.HAS_EMPAD2: + self.empad2_calibrations = None + self.empad2_background = None + + self.empad2_menu = QMenu("EMPAD-G2", self) + self.menu_bar.addMenu(self.empad2_menu) + + sensor_menu = self.empad2_menu.addMenu("Sensor") + calibration_action_group = QActionGroup(self) + calibration_action_group.setExclusive(True) + from empad2 import SENSORS + + for name, sensor in SENSORS.items(): + menu_item = sensor_menu.addAction(sensor["display-name"]) + calibration_action_group.addAction(menu_item) + menu_item.setCheckable(True) + menu_item.triggered.connect(partial(self.set_empad2_sensor, name)) + + self.empad2_menu.addAction("Load Background...").triggered.connect( + self.load_empad2_background + ) + self.empad2_menu.addAction("Load Dataset...").triggered.connect( + self.load_empad2_dataset + ) + # Scaling Menu self.scaling_menu = QMenu("&Scaling", self) self.menu_bar.addMenu(self.scaling_menu) From af907f0e04290a7213d9dc2a8b20ec0b9a5c998e Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 10 Dec 2023 14:24:10 -0500 Subject: [PATCH 3/8] enable iCoM --- src/py4D_browser/main_window.py | 10 +++++----- src/py4D_browser/update_views.py | 10 +++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 1b72bcd..de7581d 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -240,11 +240,11 @@ def setup_menus(self): detector_mode_group.addAction(detector_CoM_angle) self.detector_menu.addAction(detector_CoM_angle) - # detector_iCoM = QAction("i&CoM", self) - # detector_iCoM.setCheckable(True) - # detector_iCoM.triggered.connect(partial(self.update_real_space_view, True)) - # detector_mode_group.addAction(detector_iCoM) - # self.detector_menu.addAction(detector_iCoM) + detector_iCoM = QAction("i&CoM", self) + detector_iCoM.setCheckable(True) + detector_iCoM.triggered.connect(partial(self.update_real_space_view, True)) + detector_mode_group.addAction(detector_iCoM) + self.detector_menu.addAction(detector_iCoM) # Detector Shape Menu self.detector_shape_menu = QMenu("Detector &Shape", self) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index c4dba23..19800fa 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -23,6 +23,7 @@ def update_real_space_view(self, reset=False): "Maximum", "CoM Magnitude", "CoM Angle", + "iCoM", ], detector_mode # If a CoM method is checked, ensure linear scaling @@ -145,7 +146,14 @@ def update_real_space_view(self, reset=False): elif detector_mode == "CoM Angle": vimg = np.arctan2(CoMy, CoMx) elif detector_mode == "iCoM": - raise NotImplementedError("Coming soon...") + dpc = py4DSTEM.process.phase.DPCReconstruction(verbose=False) + dpc.preprocess( + force_com_measured=[CoMx, CoMy], + plot_rotation=False, + plot_center_of_mass="", + ) + dpc.reconstruct(max_iter=1, step_size=1) + vimg = dpc.object_phase else: raise ValueError("Mode logic gone haywire!") From 105dc996ae17121f7909e198c94c298a6e89f645 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 11 Dec 2023 12:01:50 -0500 Subject: [PATCH 4/8] minor tweaks --- pyproject.toml | 2 +- src/py4D_browser/main_window.py | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f2f330d..64c32a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "py4D_browser" version = "0.99999" authors = [ - { name="Steven Zeltmann", email="steven.zeltmann@berkeley.edu" }, + { name="Steven Zeltmann", email="steven.zeltmann@lbl.gov" }, ] description = "A 4D-STEM data browser built on py4DSTEM." readme = "README.md" diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index de7581d..2a97f98 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -106,10 +106,10 @@ def setup_menus(self): self.empad2_calibrations = None self.empad2_background = None - self.empad2_menu = QMenu("EMPAD-G2", self) + self.empad2_menu = QMenu("&EMPAD-G2", self) self.menu_bar.addMenu(self.empad2_menu) - sensor_menu = self.empad2_menu.addMenu("Sensor") + sensor_menu = self.empad2_menu.addMenu("&Sensor") calibration_action_group = QActionGroup(self) calibration_action_group.setExclusive(True) from empad2 import SENSORS @@ -120,10 +120,10 @@ def setup_menus(self): menu_item.setCheckable(True) menu_item.triggered.connect(partial(self.set_empad2_sensor, name)) - self.empad2_menu.addAction("Load Background...").triggered.connect( + self.empad2_menu.addAction("Load &Background...").triggered.connect( self.load_empad2_background ) - self.empad2_menu.addAction("Load Dataset...").triggered.connect( + self.empad2_menu.addAction("Load &Dataset...").triggered.connect( self.load_empad2_dataset ) @@ -296,11 +296,6 @@ def setup_views(self): self.virtual_detector_point.sigRegionChanged.connect( self.update_real_space_view ) - # self.virtual_detector_roi = pg.RectROI([5, 5], [20, 20], pen=(3, 9)) - # self.diffraction_space_widget.getView().addItem(self.virtual_detector_roi) - # self.virtual_detector_roi.sigRegionChangeFinished.connect( - # partial(self.update_real_space_view, False) - # ) # Name and return self.diffraction_space_widget.setWindowTitle("Diffraction Space") From 6d0d26f31d555f12aeec50bbcd1c0ea677df705a Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 11 Dec 2023 12:31:50 -0500 Subject: [PATCH 5/8] add rectangular detector in real space --- src/py4D_browser/main_window.py | 29 ++++++++++ src/py4D_browser/update_views.py | 92 ++++++++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 2a97f98..26f3e70 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -40,6 +40,7 @@ class DataViewer(QMainWindow): from py4D_browser.update_views import ( update_diffraction_space_view, update_real_space_view, + update_realspace_detector, update_diffraction_detector, update_annulus_pos, update_annulus_radii, @@ -254,6 +255,10 @@ def setup_menus(self): detector_shape_group.setExclusive(True) self.detector_shape_group = detector_shape_group + diffraction_detector_separator = QAction("Diffraction", self) + diffraction_detector_separator.setDisabled(True) + self.detector_shape_menu.addAction(diffraction_detector_separator) + detector_point_action = QAction("&Point", self) detector_point_action.setCheckable(True) detector_point_action.setChecked(True) # Default @@ -280,6 +285,30 @@ def setup_menus(self): detector_shape_group.addAction(detector_annulus_action) self.detector_shape_menu.addAction(detector_annulus_action) + self.detector_shape_menu.addSeparator() + + diffraction_detector_separator = QAction("Real Space", self) + diffraction_detector_separator.setDisabled(True) + self.detector_shape_menu.addAction(diffraction_detector_separator) + + rs_detector_shape_group = QActionGroup(self) + rs_detector_shape_group.setExclusive(True) + self.rs_detector_shape_group = rs_detector_shape_group + + rs_detector_point_action = QAction("Poin&t", self) + rs_detector_point_action.setCheckable(True) + rs_detector_point_action.setChecked(True) # Default + rs_detector_point_action.triggered.connect(self.update_realspace_detector) + rs_detector_shape_group.addAction(rs_detector_point_action) + self.detector_shape_menu.addAction(rs_detector_point_action) + + detector_rectangle_action = QAction("Rectan&gular", self) + detector_rectangle_action.setCheckable(True) + detector_rectangle_action.triggered.connect(self.update_realspace_detector) + rs_detector_shape_group.addAction(detector_rectangle_action) + self.detector_shape_menu.addAction(detector_rectangle_action) + + def setup_views(self): # Set up the diffraction space window. self.diffraction_space_widget = pg.ImageView() diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 19800fa..d3ee6ce 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -183,17 +183,48 @@ def update_diffraction_space_view(self, reset=False): if self.datacube is None: return - roi_state = self.real_space_point_selector.saveState() - y0, x0 = roi_state["pos"] - xc, yc = int(x0 + 1), int(y0 + 1) + detector_shape = self.rs_detector_shape_group.checkedAction().text().replace("&", "") + assert detector_shape in [ + "Point", + "Rectangular", + ], detector_shape - # Set the diffraction space image - # Normalize coordinates - xc = np.clip(xc, 0, self.datacube.R_Nx - 1) - yc = np.clip(yc, 0, self.datacube.R_Ny - 1) - DP = self.datacube.data[xc, yc] + if detector_shape == "Point": + roi_state = self.real_space_point_selector.saveState() + y0, x0 = roi_state["pos"] + xc, yc = int(x0 + 1), int(y0 + 1) - self.real_space_view_text.setText(f"[{xc},{yc}]") + # Set the diffraction space image + # Normalize coordinates + xc = np.clip(xc, 0, self.datacube.R_Nx - 1) + yc = np.clip(yc, 0, self.datacube.R_Ny - 1) + + self.real_space_view_text.setText(f"[{xc},{yc}]") + + DP = self.datacube.data[xc, yc] + elif detector_shape == "Rectangular": + # Get slices corresponding to ROI + slices, _ = self.real_space_rect_selector.getArraySlice( + np.zeros((self.datacube.Rshape)), self.real_space_widget.getImageItem() + ) + slice_y, slice_x = slices + + # update the label: + self.real_space_view_text.setText( + f"[{slice_x.start}:{slice_x.stop},{slice_y.start}:{slice_y.stop}]" + ) + + DP = np.sum(self.datacube.data[slice_x, slice_y], axis=(0,1)) + + # if detector_mode == "Integrating": + # vimg = np.sum(self.datacube.data[:, :, slice_x, slice_y], axis=(2, 3)) + # elif detector_mode == "Maximum": + # vimg = np.max(self.datacube.data[:, :, slice_x, slice_y], axis=(2, 3)) + # else: + # mask = np.zeros((self.datacube.Q_Nx, self.datacube.Q_Ny), dtype=np.bool_) + # mask[slice_x, slice_y] = True + else: + raise ValueError("Detector shape not recognized") if scaling_mode == "Linear": new_view = DP @@ -208,6 +239,49 @@ def update_diffraction_space_view(self, reset=False): new_view.T, autoLevels=reset, autoRange=reset ) +def update_realspace_detector(self): + # change the shape of the detector, then update the view + + detector_shape = self.rs_detector_shape_group.checkedAction().text().replace("&","") + assert detector_shape in ["Point", "Rectangular"], detector_shape + + if self.datacube is None: + return + + x, y = self.datacube.shape[2:] + x0, y0 = x / 2, y / 2 + xr, yr = x / 10, y / 10 + + # Remove existing detector + if hasattr(self, "real_space_point_selector"): + self.real_space_widget.view.scene().removeItem( + self.real_space_point_selector + ) + if hasattr(self, "real_space_rect_selector"): + self.real_space_widget.view.scene().removeItem(self.real_space_rect_selector) + + # Rectangular detector + if detector_shape == "Point": + self.real_space_point_selector = pg_point_roi( + self.real_space_widget.getView() + ) + self.real_space_point_selector.sigRegionChanged.connect( + self.update_diffraction_space_view + ) + + elif detector_shape == "Rectangular": + self.real_space_rect_selector = pg.RectROI( + [int(x0 - xr / 2), int(y0 - yr / 2)], [int(xr), int(yr)], pen=(3, 9) + ) + self.real_space_widget.getView().addItem(self.real_space_rect_selector) + self.real_space_rect_selector.sigRegionChangeFinished.connect( + self.update_diffraction_space_view + ) + + else: + raise ValueError("Unknown detector shape! Got: {}".format(detector_shape)) + + self.update_diffraction_space_view() def update_diffraction_detector(self): # change the shape of the detector, then update the view From 069b95f289734474bc4e84a64f98e9ea05088874 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 11 Dec 2023 12:33:32 -0500 Subject: [PATCH 6/8] format with black --- src/py4D_browser/main_window.py | 1 - src/py4D_browser/update_views.py | 20 +++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 26f3e70..ec83c32 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -308,7 +308,6 @@ def setup_menus(self): rs_detector_shape_group.addAction(detector_rectangle_action) self.detector_shape_menu.addAction(detector_rectangle_action) - def setup_views(self): # Set up the diffraction space window. self.diffraction_space_widget = pg.ImageView() diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index d3ee6ce..8b7fd52 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -183,7 +183,9 @@ def update_diffraction_space_view(self, reset=False): if self.datacube is None: return - detector_shape = self.rs_detector_shape_group.checkedAction().text().replace("&", "") + detector_shape = ( + self.rs_detector_shape_group.checkedAction().text().replace("&", "") + ) assert detector_shape in [ "Point", "Rectangular", @@ -214,7 +216,7 @@ def update_diffraction_space_view(self, reset=False): f"[{slice_x.start}:{slice_x.stop},{slice_y.start}:{slice_y.stop}]" ) - DP = np.sum(self.datacube.data[slice_x, slice_y], axis=(0,1)) + DP = np.sum(self.datacube.data[slice_x, slice_y], axis=(0, 1)) # if detector_mode == "Integrating": # vimg = np.sum(self.datacube.data[:, :, slice_x, slice_y], axis=(2, 3)) @@ -239,10 +241,13 @@ def update_diffraction_space_view(self, reset=False): new_view.T, autoLevels=reset, autoRange=reset ) + def update_realspace_detector(self): # change the shape of the detector, then update the view - detector_shape = self.rs_detector_shape_group.checkedAction().text().replace("&","") + detector_shape = ( + self.rs_detector_shape_group.checkedAction().text().replace("&", "") + ) assert detector_shape in ["Point", "Rectangular"], detector_shape if self.datacube is None: @@ -254,17 +259,13 @@ def update_realspace_detector(self): # Remove existing detector if hasattr(self, "real_space_point_selector"): - self.real_space_widget.view.scene().removeItem( - self.real_space_point_selector - ) + self.real_space_widget.view.scene().removeItem(self.real_space_point_selector) if hasattr(self, "real_space_rect_selector"): self.real_space_widget.view.scene().removeItem(self.real_space_rect_selector) # Rectangular detector if detector_shape == "Point": - self.real_space_point_selector = pg_point_roi( - self.real_space_widget.getView() - ) + self.real_space_point_selector = pg_point_roi(self.real_space_widget.getView()) self.real_space_point_selector.sigRegionChanged.connect( self.update_diffraction_space_view ) @@ -283,6 +284,7 @@ def update_realspace_detector(self): self.update_diffraction_space_view() + def update_diffraction_detector(self): # change the shape of the detector, then update the view From e9a7422aa410ffe2553c355f7a1f55eb1002c355 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 15 Dec 2023 16:48:59 -0500 Subject: [PATCH 7/8] add virtual image export functions --- src/py4D_browser/main_window.py | 38 ++++++++++ src/py4D_browser/menu_actions.py | 117 +++++++++++++++++++++++++++++-- 2 files changed, 150 insertions(+), 5 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index ec83c32..4277c3a 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -35,6 +35,9 @@ class DataViewer(QMainWindow): load_data_bin, load_data_mmap, show_file_dialog, + get_savefile_name, + export_datacube, + export_virtual_image, ) from py4D_browser.update_views import ( @@ -90,6 +93,10 @@ def setup_menus(self): self.file_menu = QMenu("&File", self) self.menu_bar.addMenu(self.file_menu) + import_label = QAction("Import", self) + import_label.setDisabled(True) + self.file_menu.addAction(import_label) + self.load_auto_action = QAction("&Load Data...", self) self.load_auto_action.triggered.connect(self.load_data_auto) self.file_menu.addAction(self.load_auto_action) @@ -102,6 +109,37 @@ def setup_menus(self): self.load_binned_action.triggered.connect(self.load_data_bin) self.file_menu.addAction(self.load_binned_action) + self.file_menu.addSeparator() + + export_label = QAction("Export", self) + export_label.setDisabled(True) + self.file_menu.addAction(export_label) + + # Submenu to export datacube + datacube_export_menu = QMenu("Export Datacube", self) + self.file_menu.addMenu(datacube_export_menu) + for method in ["Raw float32", "py4DSTEM HDF5", "Plain HDF5"]: + menu_item = datacube_export_menu.addAction(method) + menu_item.triggered.connect(partial(self.export_datacube, method)) + + # Submenu to export virtual image + vimg_export_menu = QMenu("Export Virtual Image", self) + self.file_menu.addMenu(vimg_export_menu) + for method in ["PNG", "TIFF", "TIFF (raw)"]: + menu_item = vimg_export_menu.addAction(method) + menu_item.triggered.connect( + partial(self.export_virtual_image, method, "image") + ) + + # Submenu to export diffraction + vdiff_export_menu = QMenu("Export Diffraction Pattern", self) + self.file_menu.addMenu(vdiff_export_menu) + for method in ["PNG", "TIFF", "TIFF (raw)"]: + menu_item = vdiff_export_menu.addAction(method) + menu_item.triggered.connect( + partial(self.export_virtual_image, method, "diffraction") + ) + # EMPAD2 menu if self.HAS_EMPAD2: self.empad2_calibrations = None diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index e55a668..b3754a1 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -1,7 +1,9 @@ import py4DSTEM -from PyQt5.QtWidgets import QFileDialog +from PyQt5.QtWidgets import QFileDialog, QMessageBox import h5py import os +import numpy as np +import matplotlib.pyplot as plt def load_data_auto(self): @@ -22,8 +24,9 @@ def load_data_bin(self): def load_file(self, filepath, mmap=False, binning=1): print(f"Loading file {filepath}") - print(f"Type: {os.path.splitext(filepath)[-1].lower()}") - if os.path.splitext(filepath)[-1].lower() in (".h5", ".hdf5", ".py4dstem", ".emd"): + extension = os.path.splitext(filepath)[-1].lower() + print(f"Type: {extension}") + if extension in (".h5", ".hdf5", ".py4dstem", ".emd"): datacubes = get_4D(h5py.File(filepath, "r")) print(f"Found {len(datacubes)} 4D datasets inside the HDF5 file...") if len(datacubes) >= 1: @@ -32,6 +35,8 @@ def load_file(self, filepath, mmap=False, binning=1): self.datacube = py4DSTEM.DataCube( datacubes[0] if mmap else datacubes[0][()] ) + elif extension in [".npy", ".npz"]: + self.datacube = py4DSTEM.DataCube(np.load(filepath)) else: self.datacube = py4DSTEM.import_file( filepath, @@ -45,12 +50,72 @@ def load_file(self, filepath, mmap=False, binning=1): self.setWindowTitle(filepath) -def show_file_dialog(self): +def export_datacube(self, save_format: str): + assert save_format in [ + "Raw float32", + "py4DSTEM HDF5", + "Plain HDF5", + ], f"unrecognized format {format}" + assert self.datacube is not None, "No datacube!" + + # Display RAW format disclaimer + if save_format == "Raw float32": + response = QMessageBox.question( + self, + "Save RAW file?", + ( + "Saving raw binary files is not recommended as such files" + " encode no information about the shape, endianness, or " + "ordering of the data. Saving to HDF5 is recommended. " + "Do you wish to continue saving RAW data?" + ), + QMessageBox.Cancel, + QMessageBox.Save, + ) + + if response == QMessageBox.Cancel: + print("Cancelling due to user guilt") + return + + filename = self.get_savefile_name(save_format) + + if save_format == "Raw float32": + self.datacube.data.astype(np.float32).tofile(filename) + + elif save_format == "py4DSTEM HDF5": + py4DSTEM.save(filename, self.datacube, mode="o") + + elif save_format == "Plain HDF5": + with h5py.File(filename, "o") as f: + f["array"] = self.datacube.data + + +def export_virtual_image(self, im_format: str, im_type: str): + assert im_type in ['image', 'diffraction'], f"bad image type: {im_type}" + + filename = self.get_savefile_name(im_format) + + view = self.real_space_widget if im_type == "image" else self.diffraction_space_widget + + vimg = view.image + vmin, vmax = view.getLevels() + + if im_format == "PNG": + plt.imsave(fname=filename,arr=vimg, vmin=vmin, vmax=vmax, format="png", cmap='gray') + elif im_format == "TIFF": + plt.imsave(fname=filename,arr=vimg, vmin=vmin, vmax=vmax, format="tiff", cmap='gray') + elif im_format == "TIFF (raw)": + from tifffile import TiffWriter + with TiffWriter(filename) as tw: + tw.write(vimg) + + +def show_file_dialog(self) -> str: filename = QFileDialog.getOpenFileName( self, "Open 4D-STEM Data", "", - "4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg *.h5 *.hdf5 *.emd *.py4dstem);;Any file (*)", + "4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg *.h5 *.hdf5 *.emd *.py4dstem *.npy *.npz);;Any file (*)", ) if filename is not None and len(filename[0]) > 0: return filename[0] @@ -59,6 +124,48 @@ def show_file_dialog(self): raise ValueError("Could not read file") +def get_savefile_name(self, file_format) -> str: + filters = { + "Raw float32": "RAW File (*.raw *.f32);;Any file (*)", + "py4DSTEM HDF5": "HDF5 File (*.hdf5 *.h5 *.emd *.py4dstem);;Any file (*)", + "Plain HDF5": "HDF5 File (*.hdf5 *.h5;;Any file (*)", + "PNG": "PNG File (*.png);;Any file (*)", + "TIFF": "TIFF File (*.tiff *.tif *.tff);;Any File (*)", + "TIFF (raw)": "TIFF File (*.tiff *.tif *.tff);;Any File (*)", + } + + defaults = { + "Raw float32": ".raw", + "py4DSTEM HDF5": ".h5", + "Plain HDF5": ".h5", + "PNG": ".png", + "TIFF": ".tiff", + "TIFF (raw)": ".tiff", + } + + file_filter = filters.get(file_format, "Any file (*)") + + filename = QFileDialog.getSaveFileName( + parent=self, + caption="Select save file", + directory="", + filter=file_filter, + ) + + if filename is not None and len(filename[0]) > 0: + fname = filename[0] + print(f"Save file picked at {filename}") + + if os.path.splitext(fname)[1] == '': + fname = fname + defaults.get(file_format,"") + print(f"Added default extension to get: {fname}") + return fname + else: + print("File was invalid, or something?") + print(f"QFileDialog returned {filename}") + raise ValueError("Could get save file") + + def get_4D(f, datacubes=None): if datacubes is None: datacubes = [] From bc46d56ac4d0a77ba75bded6e41cca834bb4171c Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 15 Dec 2023 16:49:26 -0500 Subject: [PATCH 8/8] format with black --- src/py4D_browser/menu_actions.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index b3754a1..2e61631 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -91,21 +91,28 @@ def export_datacube(self, save_format: str): def export_virtual_image(self, im_format: str, im_type: str): - assert im_type in ['image', 'diffraction'], f"bad image type: {im_type}" + assert im_type in ["image", "diffraction"], f"bad image type: {im_type}" filename = self.get_savefile_name(im_format) - view = self.real_space_widget if im_type == "image" else self.diffraction_space_widget + view = ( + self.real_space_widget if im_type == "image" else self.diffraction_space_widget + ) vimg = view.image vmin, vmax = view.getLevels() if im_format == "PNG": - plt.imsave(fname=filename,arr=vimg, vmin=vmin, vmax=vmax, format="png", cmap='gray') + plt.imsave( + fname=filename, arr=vimg, vmin=vmin, vmax=vmax, format="png", cmap="gray" + ) elif im_format == "TIFF": - plt.imsave(fname=filename,arr=vimg, vmin=vmin, vmax=vmax, format="tiff", cmap='gray') + plt.imsave( + fname=filename, arr=vimg, vmin=vmin, vmax=vmax, format="tiff", cmap="gray" + ) elif im_format == "TIFF (raw)": from tifffile import TiffWriter + with TiffWriter(filename) as tw: tw.write(vimg) @@ -156,8 +163,8 @@ def get_savefile_name(self, file_format) -> str: fname = filename[0] print(f"Save file picked at {filename}") - if os.path.splitext(fname)[1] == '': - fname = fname + defaults.get(file_format,"") + if os.path.splitext(fname)[1] == "": + fname = fname + defaults.get(file_format, "") print(f"Added default extension to get: {fname}") return fname else: