From f34712e29b080861977b437a88ae5b231e7b584d Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Mon, 12 Feb 2024 16:17:17 -0500 Subject: [PATCH 01/34] EMPAD2 load progress in status bar --- src/py4D_browser/empad2_reader.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/py4D_browser/empad2_reader.py b/src/py4D_browser/empad2_reader.py index 3fedfe8..81fb961 100644 --- a/src/py4D_browser/empad2_reader.py +++ b/src/py4D_browser/empad2_reader.py @@ -1,8 +1,21 @@ import empad2 -from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication import numpy as np +class StatusBarWriter: + def __init__(self, statusBar): + self.statusBar = statusBar + self.app = app = QApplication.instance() + + def write(self, message): + self.statusBar.showMessage(message, 1_000) + self.app.processEvents() + + def flush(self): + pass + + def set_empad2_sensor(self, sensor_name): self.empad2_calibrations = empad2.load_calibration_data(sensor=sensor_name) self.statusBar().showMessage(f"{sensor_name} calibrations loaded", 5_000) @@ -41,7 +54,14 @@ def load_empad2_dataset(self): filename = raw_file_dialog(self) self.datacube = empad2.load_dataset( - filename, self.empad2_background, self.empad2_calibrations + filename, + self.empad2_background, + self.empad2_calibrations, + _tqdm_args={ + "desc": "Loading", + "file": StatusBarWriter(self.statusBar()), + "mininterval": 1.0, + }, ) if dummy_data: From 91603d8481bd7e6807eb8f3508b6e175655b5ec2 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sat, 17 Feb 2024 11:57:57 -0500 Subject: [PATCH 02/34] add arina reader --- src/py4D_browser/main_window.py | 5 +++++ src/py4D_browser/menu_actions.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 0b4ef66..fa76c77 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -34,6 +34,7 @@ class DataViewer(QMainWindow): from py4D_browser.menu_actions import ( load_file, + load_data_arina, load_data_auto, load_data_bin, load_data_mmap, @@ -115,6 +116,10 @@ def setup_menus(self): self.load_binned_action.triggered.connect(self.load_data_bin) self.file_menu.addAction(self.load_binned_action) + self.load_arina_action = QAction("Load &Arina Data...", self) + self.load_arina_action.triggered.connect(self.load_data_arina) + self.file_menu.addAction(self.load_arina_action) + self.file_menu.addSeparator() export_label = QAction("Export", self) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index 94ff7c1..ef8efac 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -5,6 +5,7 @@ import numpy as np import matplotlib.pyplot as plt from py4D_browser.help_menu import KeyboardMapMenu +from py4DSTEM.io.filereaders import read_arina def load_data_auto(self): @@ -22,6 +23,35 @@ def load_data_bin(self): filename = self.show_file_dialog() self.load_file(filename, mmap=False, binning=4) +def load_data_arina(self): + filename = self.show_file_dialog() + dataset = read_arina(filename) + + # Try to reshape the data to be square + N_patterns = dataset.data.shape[1] + Nxy = np.sqrt(N_patterns) + if np.abs(Nxy - np.round(Nxy)) <= 1e-10: + Nxy = int(Nxy) + dataset.data = dataset.data.reshape(Nxy, Nxy, dataset.data.shape[2], dataset.data.shape[3]) + else: + self.statusBar().showMessage(f"The scan appears to not be square! Found {N_patterns} patterns", 5_000) + + self.datacube = dataset + self.diffraction_scale_bar.pixel_size = self.datacube.calibration.get_Q_pixel_size() + self.diffraction_scale_bar.units = self.datacube.calibration.get_Q_pixel_units() + + self.real_space_scale_bar.pixel_size = self.datacube.calibration.get_R_pixel_size() + self.real_space_scale_bar.units = self.datacube.calibration.get_R_pixel_units() + + self.fft_scale_bar.pixel_size = ( + 1.0 / self.datacube.calibration.get_R_pixel_size() / self.datacube.R_Ny + ) + self.fft_scale_bar.units = f"1/{self.datacube.calibration.get_R_pixel_units()}" + + self.update_diffraction_space_view(reset=True) + self.update_real_space_view(reset=True) + + self.setWindowTitle(filename) def load_file(self, filepath, mmap=False, binning=1): print(f"Loading file {filepath}") From 244bdc18c3429b8aa8be7a20bd1f28a82a260f45 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sat, 17 Feb 2024 12:04:16 -0500 Subject: [PATCH 03/34] fix incorrect positioning of realspace detector --- src/py4D_browser/update_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 7b7c332..36c47db 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -279,7 +279,7 @@ def update_realspace_detector(self): if self.datacube is None: return - x, y = self.datacube.shape[2:] + x, y = self.datacube.data.shape[:2] x0, y0 = x / 2, y / 2 xr, yr = x / 10, y / 10 @@ -322,7 +322,7 @@ def update_diffraction_detector(self): if self.datacube is None: return - x, y = self.datacube.shape[2:] + x, y = self.datacube.data.shape[2:] x0, y0 = x / 2, y / 2 xr, yr = x / 10, y / 10 From d165ed46658157040d099d26215d49044e29ac47 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sat, 17 Feb 2024 12:15:33 -0500 Subject: [PATCH 04/34] use clipped autoscaling --- src/py4D_browser/menu_actions.py | 10 ++++++++-- src/py4D_browser/update_views.py | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index ef8efac..fbe3e9e 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -23,6 +23,7 @@ def load_data_bin(self): filename = self.show_file_dialog() self.load_file(filename, mmap=False, binning=4) + def load_data_arina(self): filename = self.show_file_dialog() dataset = read_arina(filename) @@ -32,9 +33,13 @@ def load_data_arina(self): Nxy = np.sqrt(N_patterns) if np.abs(Nxy - np.round(Nxy)) <= 1e-10: Nxy = int(Nxy) - dataset.data = dataset.data.reshape(Nxy, Nxy, dataset.data.shape[2], dataset.data.shape[3]) + dataset.data = dataset.data.reshape( + Nxy, Nxy, dataset.data.shape[2], dataset.data.shape[3] + ) else: - self.statusBar().showMessage(f"The scan appears to not be square! Found {N_patterns} patterns", 5_000) + self.statusBar().showMessage( + f"The scan appears to not be square! Found {N_patterns} patterns", 5_000 + ) self.datacube = dataset self.diffraction_scale_bar.pixel_size = self.datacube.calibration.get_Q_pixel_size() @@ -53,6 +58,7 @@ def load_data_arina(self): self.setWindowTitle(filename) + def load_file(self, filepath, mmap=False, binning=1): print(f"Loading file {filepath}") extension = os.path.splitext(filepath)[-1].lower() diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 36c47db..ecb7b90 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -174,9 +174,16 @@ def update_real_space_view(self, reset=False): self.unscaled_realspace_image = vimg + auto_level = reset or self.realspace_rescale_button.latched + self.real_space_widget.setImage( new_view.T, - autoLevels=reset or self.realspace_rescale_button.latched, + autoLevels=False, + levels=( + (np.percentile(new_view, 2), np.percentile(new_view, 98)) + if auto_level + else None + ), autoRange=reset, ) @@ -251,9 +258,16 @@ def update_diffraction_space_view(self, reset=False): else: raise ValueError("Mode not recognized") + auto_level = reset or self.diffraction_rescale_button.latched + self.diffraction_space_widget.setImage( new_view.T, - autoLevels=reset or self.diffraction_rescale_button.latched, + autoLevels=False, + levels=( + (np.percentile(new_view, 2), np.percentile(new_view, 98)) + if auto_level + else None + ), autoRange=reset, ) From 17650994aec67009eefea6a5b6cfcfb3cf2dcb9f Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 6 Mar 2024 09:19:47 -0500 Subject: [PATCH 05/34] add complex FFT view --- src/py4D_browser/main_window.py | 9 ++++++++- src/py4D_browser/update_views.py | 22 +++++++++++++++++++++- src/py4D_browser/utils.py | 25 +++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index fa76c77..875ee07 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -365,13 +365,20 @@ def setup_menus(self): img_fft_action = QAction("Virtual Image FFT", self) img_fft_action.setCheckable(True) img_fft_action.setChecked(True) + img_fft_action.triggered.connect(partial(self.update_real_space_view, False)) self.fft_menu.addAction(img_fft_action) self.fft_source_action_group.addAction(img_fft_action) + + img_complex_fft_action = QAction("Virtual Image FFT (complex)", self) + img_complex_fft_action.setCheckable(True) + self.fft_menu.addAction(img_complex_fft_action) + self.fft_source_action_group.addAction(img_complex_fft_action) + img_complex_fft_action.triggered.connect(partial(self.update_real_space_view, False)) + img_ewpc_action = QAction("EWPC", self) img_ewpc_action.setCheckable(True) self.fft_menu.addAction(img_ewpc_action) self.fft_source_action_group.addAction(img_ewpc_action) - img_fft_action.triggered.connect(partial(self.update_real_space_view, False)) img_ewpc_action.triggered.connect( partial(self.update_diffraction_space_view, False) ) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index ecb7b90..e2853ba 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -3,7 +3,7 @@ import py4DSTEM from functools import partial -from py4D_browser.utils import pg_point_roi, make_detector +from py4D_browser.utils import pg_point_roi, make_detector, complex_to_Lab def update_real_space_view(self, reset=False): @@ -200,6 +200,26 @@ def update_real_space_view(self, reset=False): if mode_switch: # Need to autorange after setRect self.fft_widget.autoRange() + elif self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT (complex)": + fft = np.fft.fftshift(np.fft.fft2(new_view)) + levels = (np.min(np.abs(fft)), np.percentile(np.abs(fft), 99.9)) + mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" + self.fft_widget_text.setText("Virtual Image FFT") + fft_img = complex_to_Lab( + fft.T, + amin=levels[0], + amax=levels[1], + ab_scale=128, + gamma=0.5, + ) + self.fft_widget.setImage( + fft_img, autoLevels=False, autoRange=mode_switch, levels=(0,1), + ) + + self.fft_widget.getImageItem().setRect(0, 0, fft.shape[1], fft.shape[1]) + if mode_switch: + # Need to autorange after setRect + self.fft_widget.autoRange() def update_diffraction_space_view(self, reset=False): diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index fb7be24..e773096 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -108,3 +108,28 @@ def make_detector(shape: tuple, mode: str, geometry) -> np.ndarray: raise ValueError(f"mode and geometry not understood: {unknown}") return mask + +def complex_to_Lab(im,amin=None,amax=None,gamma=1,L_scale=100,ab_scale=64,uniform_L=None): + from skimage.color import lab2rgb + from matplotlib.colors import Normalize + import warnings + + Lab = np.zeros(im.shape + (3,),dtype=np.float64) + angle = np.angle(im) + + L = Normalize(vmin=amin, vmax=amax, clip=True)(np.abs(im)) ** gamma + L = Normalize()(L) + + # attempt at polynomial saturation + # ab_prescale = 4*L - 4*L*L + ab_prescale = 0.5 + + Lab[...,0] = uniform_L or L * L_scale + Lab[...,1] = np.cos(angle) * ab_scale * ab_prescale + Lab[...,2] = np.sin(angle) * ab_scale * ab_prescale + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + rgb = lab2rgb(Lab) + + return rgb \ No newline at end of file From a29f6a9c0fca16e2aaddf78093fe6f0f476f19bd Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 6 Mar 2024 09:23:10 -0500 Subject: [PATCH 06/34] format with black --- src/py4D_browser/main_window.py | 4 +++- src/py4D_browser/update_views.py | 10 ++++++++-- src/py4D_browser/utils.py | 27 +++++++++++++++------------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 875ee07..ccb32bd 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -373,7 +373,9 @@ def setup_menus(self): img_complex_fft_action.setCheckable(True) self.fft_menu.addAction(img_complex_fft_action) self.fft_source_action_group.addAction(img_complex_fft_action) - img_complex_fft_action.triggered.connect(partial(self.update_real_space_view, False)) + img_complex_fft_action.triggered.connect( + partial(self.update_real_space_view, False) + ) img_ewpc_action = QAction("EWPC", self) img_ewpc_action.setCheckable(True) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index e2853ba..acb5e2d 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -200,7 +200,10 @@ def update_real_space_view(self, reset=False): if mode_switch: # Need to autorange after setRect self.fft_widget.autoRange() - elif self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT (complex)": + elif ( + self.fft_source_action_group.checkedAction().text() + == "Virtual Image FFT (complex)" + ): fft = np.fft.fftshift(np.fft.fft2(new_view)) levels = (np.min(np.abs(fft)), np.percentile(np.abs(fft), 99.9)) mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" @@ -213,7 +216,10 @@ def update_real_space_view(self, reset=False): gamma=0.5, ) self.fft_widget.setImage( - fft_img, autoLevels=False, autoRange=mode_switch, levels=(0,1), + fft_img, + autoLevels=False, + autoRange=mode_switch, + levels=(0, 1), ) self.fft_widget.getImageItem().setRect(0, 0, fft.shape[1], fft.shape[1]) diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index e773096..e1ec718 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -109,27 +109,30 @@ def make_detector(shape: tuple, mode: str, geometry) -> np.ndarray: return mask -def complex_to_Lab(im,amin=None,amax=None,gamma=1,L_scale=100,ab_scale=64,uniform_L=None): + +def complex_to_Lab( + im, amin=None, amax=None, gamma=1, L_scale=100, ab_scale=64, uniform_L=None +): from skimage.color import lab2rgb from matplotlib.colors import Normalize import warnings - - Lab = np.zeros(im.shape + (3,),dtype=np.float64) + + Lab = np.zeros(im.shape + (3,), dtype=np.float64) angle = np.angle(im) - + L = Normalize(vmin=amin, vmax=amax, clip=True)(np.abs(im)) ** gamma L = Normalize()(L) - + # attempt at polynomial saturation # ab_prescale = 4*L - 4*L*L ab_prescale = 0.5 - - Lab[...,0] = uniform_L or L * L_scale - Lab[...,1] = np.cos(angle) * ab_scale * ab_prescale - Lab[...,2] = np.sin(angle) * ab_scale * ab_prescale - + + Lab[..., 0] = uniform_L or L * L_scale + Lab[..., 1] = np.cos(angle) * ab_scale * ab_prescale + Lab[..., 2] = np.sin(angle) * ab_scale * ab_prescale + with warnings.catch_warnings(): warnings.simplefilter("ignore") rgb = lab2rgb(Lab) - - return rgb \ No newline at end of file + + return rgb From 36e4591a7cf7ee7911096e53c4b2d0f59fedb554 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 6 Mar 2024 13:36:48 -0500 Subject: [PATCH 07/34] add statistics for displays --- src/py4D_browser/main_window.py | 6 ++++++ src/py4D_browser/update_views.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index ccb32bd..4a1d33b 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -480,6 +480,12 @@ def setup_views(self): self.fft_widget.getView().setMenuEnabled(False) # Setup Status Bar + self.realspace_statistics_text = QLabel("Image Stats") + self.diffraction_statistics_text = QLabel("Diffraction Stats") + self.statusBar().addPermanentWidget(VLine()) + self.statusBar().addPermanentWidget(self.realspace_statistics_text) + self.statusBar().addPermanentWidget(VLine()) + self.statusBar().addPermanentWidget(self.diffraction_statistics_text) self.statusBar().addPermanentWidget(VLine()) self.statusBar().addPermanentWidget(self.diffraction_space_view_text) self.statusBar().addPermanentWidget(VLine()) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index acb5e2d..48948f4 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -174,6 +174,10 @@ def update_real_space_view(self, reset=False): self.unscaled_realspace_image = vimg + self.realspace_statistics_text.setToolTip( + f"min\t{vimg.min():.5g}\nmax\t{vimg.max():.5g}\nmean\t{vimg.mean():.5g}\nsum\t{vimg.sum():.5g}\nstd\t{np.std(vimg):.5g}" + ) + auto_level = reset or self.realspace_rescale_button.latched self.real_space_widget.setImage( @@ -284,6 +288,10 @@ def update_diffraction_space_view(self, reset=False): else: raise ValueError("Mode not recognized") + self.diffraction_statistics_text.setToolTip( + f"min\t{DP.min():.5g}\nmax\t{DP.max():.5g}\nmean\t{DP.mean():.5g}\nsum\t{DP.sum():.5g}\nstd\t{np.std(DP):.5g}" + ) + auto_level = reset or self.diffraction_rescale_button.latched self.diffraction_space_widget.setImage( From 4548c9885989ef3e816a6c086f7242dccdb68a1a Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 6 Mar 2024 13:47:07 -0500 Subject: [PATCH 08/34] add pre-commit config --- .github/workflows/black.yml | 2 +- .pre-commit-config.yaml | 14 ++++++++++++++ CITATION.cff | 2 +- LICENSE.txt | 1 - README.md | 16 ++++++++-------- py4DGUI-keymap.html | 16 ++++++++-------- pyproject.toml | 2 +- 7 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 09b2a0f..f7afd36 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -11,4 +11,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: psf/black@stable \ No newline at end of file + - uses: psf/black@stable diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..503f4f9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/psf/black + rev: 24.2.0 + hooks: + - id: black diff --git a/CITATION.cff b/CITATION.cff index 89d614c..1892afa 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,7 +1,7 @@ cff-version: 1.1.0 message: "If you use this software, please cite the accompanying paper." abstract: "Scanning transmission electron microscopy (STEM) allows for imaging, diffraction, and spectroscopy of materials on length scales ranging from microns to atoms. By using a high-speed, direct electron detector, it is now possible to record a full two-dimensional (2D) image of the diffracted electron beam at each probe position, typically a 2D grid of probe positions. These 4D-STEM datasets are rich in information, including signatures of the local structure, orientation, deformation, electromagnetic fields, and other sample-dependent properties. However, extracting this information requires complex analysis pipelines that include data wrangling, calibration, analysis, and visualization, all while maintaining robustness against imaging distortions and artifacts. In this paper, we present py4DSTEM, an analysis toolkit for measuring material properties from 4D-STEM datasets, written in the Python language and released with an open-source license. We describe the algorithmic steps for dataset calibration and various 4D-STEM property measurements in detail and present results from several experimental datasets. We also implement a simple and universal file format appropriate for electron microscopy data in py4DSTEM, which uses the open-source HDF5 standard. We hope this tool will benefit the research community and help improve the standards for data and computational methods in electron microscopy, and we invite the community to contribute to this ongoing project." -authors: +authors: - affiliation: "National Center for Electron Microscopy, Molecular Foundry, Lawrence Berkeley National Laboratory, 1 Cyclotron Road, Berkeley, CA 94720, USA" family-names: Savitzky diff --git a/LICENSE.txt b/LICENSE.txt index 72f3b9d..77f1435 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -672,4 +672,3 @@ may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . - diff --git a/README.md b/README.md index e68e9d3..c7132b6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # The `py4DSTEM` GUI -This repository hosts the `pyqt` based graphical 4D--STEM data browser that was originally part of **py4DSTEM** until version 0.13.11. +This repository hosts the `pyqt` based graphical 4D--STEM data browser that was originally part of **py4DSTEM** until version 0.13.11. -## Installation -The GUI is available on PyPI and conda-forge: +## Installation +The GUI is available on PyPI and conda-forge: `pip install py4D-browser` @@ -15,19 +15,19 @@ The GUI is available on PyPI and conda-forge: Run `py4DGUI` in your terminal to open the GUI. Then just drag and drop a 4D-STEM dataset into the window! ### Controls -* Move the virtual detector and the real-space selector using the mouse or using the keyboard shortcuts: WASD moves the detector and IJKL moves the selector, and holding down shift moves 5 pixels at a time. -* Auto scaling of both views is on by default. Press the "Autoscale" buttons in the bottom right to disable. Press either button to apply automatic scaling once, or Shift + click to lock autoscaling back on. +* Move the virtual detector and the real-space selector using the mouse or using the keyboard shortcuts: WASD moves the detector and IJKL moves the selector, and holding down shift moves 5 pixels at a time. +* Auto scaling of both views is on by default. Press the "Autoscale" buttons in the bottom right to disable. Press either button to apply automatic scaling once, or Shift + click to lock autoscaling back on. * Different shapes of virtual detector are available in the "Detector Shape" menu, and different detector responses are available in the "Detector Response" menu. -* The information in the bottom bar contains the details of the virtual detector used to generate the images, and can be entered into py4DSTEM to generate the same image. +* The information in the bottom bar contains the details of the virtual detector used to generate the images, and can be entered into py4DSTEM to generate the same image. * The FFT pane can be switched between displaying the FFT of the virtual image and displaying the [exit wave power cepstrum](https://doi.org/10.1016/j.ultramic.2020.112994). * Virtual images can be exported either as the scaled and clipped displays shown in the GUI or as raw data. The exact datatype stored in the raw TIFF image depends on both the datatype of the dataset and the type of virtual image being displayed (in particular, integer datatypes are converted internally to floating point to prevent overflows when generating any synthesized virtual images). -* If the [EMPAD-G2 Raw Reader](https://github.com/sezelt/empad2) is installed in the same environment, an extra menu will appear that allows the concatenated binary format data to be background subtracted and calibrated in the GUI. You can also save the calibrated data as an HDF5 file for later analysis. +* If the [EMPAD-G2 Raw Reader](https://github.com/sezelt/empad2) is installed in the same environment, an extra menu will appear that allows the concatenated binary format data to be background subtracted and calibrated in the GUI. You can also save the calibrated data as an HDF5 file for later analysis. ![Demonstration](/images/demo.gif) The keyboard map in the Help menu was made using [this tool](https://archie-adams.github.io/keyboard-shortcut-map-maker/) and the map file is in the top level of this repo. -## About +## About ![py4DSTEM logo](/images/py4DSTEM_logo.png) diff --git a/py4DGUI-keymap.html b/py4DGUI-keymap.html index 95996c2..b701bf8 100644 --- a/py4DGUI-keymap.html +++ b/py4DGUI-keymap.html @@ -21,8 +21,8 @@ .ulButtons ul { margin: 0; - list-style-type: none; - text-align: center; + list-style-type: none; + text-align: center; background-color: #b4b3bd; } .ulButtons ul li { @@ -51,7 +51,7 @@ } .bodyStyle { - width:1109px; + width:1109px; margin:0 auto; } @@ -68,8 +68,8 @@ /* Footer styling. */ footer { - background-color: #b4b3bd; - margin-top: 20px; + background-color: #b4b3bd; + margin-top: 20px; margin-bottom: 0; } @@ -79,12 +79,12 @@ .footer-div { width:960px; - margin:0 auto; + margin:0 auto; background-color: #b4b3bd; } #to-top-button{ - margin-left:426px; + margin-left:426px; padding-top: 14px; } #to-top-input { @@ -689,4 +689,4 @@

Keyboard Shortcuts

- \ No newline at end of file + diff --git a/pyproject.toml b/pyproject.toml index 3426704..229a8ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,4 +43,4 @@ include-package-data = true where = ["src"] [tool.setuptools.package-data] -py4D_browser = ["*.png"] \ No newline at end of file +py4D_browser = ["*.png"] From 664fb20ece7e4a0094dc7530f90a9242c5c8fbac Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 6 Mar 2024 13:47:46 -0500 Subject: [PATCH 09/34] disable some pre-commit hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 503f4f9..196c01b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,9 +4,9 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml + # - id: trailing-whitespace + # - id: end-of-file-fixer + # - id: check-yaml - id: check-added-large-files - repo: https://github.com/psf/black rev: 24.2.0 From 604f3e5d5d3e3083a3838421d474cef4ea1c73b1 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 6 Mar 2024 14:08:01 -0500 Subject: [PATCH 10/34] VERSION ONE POINT OH --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 229a8ea..a715a7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "py4D_browser" -version = "0.999999" +version = "1.0.0" authors = [ { name="Steven Zeltmann", email="steven.zeltmann@lbl.gov" }, ] From 80ae86562aacf95d81b323d050cab4c2151c1dcf Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 17 Mar 2024 10:48:35 -0400 Subject: [PATCH 11/34] add ResizeDialog for picking appropriate data shapes (not yet used) --- src/py4D_browser/menu_actions.py | 7 ++- src/py4D_browser/utils.py | 101 ++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index fbe3e9e..27f95aa 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -64,7 +64,7 @@ def load_file(self, filepath, mmap=False, binning=1): 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")) + datacubes = get_ND(h5py.File(filepath, "r")) print(f"Found {len(datacubes)} 4D datasets inside the HDF5 file...") if len(datacubes) >= 1: # Read the first datacube in the HDF5 file into RAM @@ -243,7 +243,8 @@ def get_savefile_name(self, file_format) -> str: raise ValueError("Could get save file") -def get_4D(f, datacubes=None): +def get_ND(f, datacubes=None, N=4): + # Traverse an h5py.File and look for Datasets with N dimensions if datacubes is None: datacubes = [] for k in f.keys(): @@ -252,7 +253,7 @@ def get_4D(f, datacubes=None): if len(f[k].shape) == 4: datacubes.append(f[k]) elif isinstance(f[k], h5py.Group): - get_4D(f[k], datacubes) + get_ND(f[k], datacubes) return datacubes diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index e1ec718..94a979c 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -1,8 +1,9 @@ import pyqtgraph as pg import numpy as np -from PyQt5.QtWidgets import QFrame, QPushButton, QApplication +from PyQt5.QtWidgets import QFrame, QPushButton, QApplication, QLabel from PyQt5.QtCore import pyqtSignal from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout, QSpinBox class VLine(QFrame): @@ -52,6 +53,104 @@ def on_click(self, *args): self.status_bar.showMessage("Shift+click to keep on", 5_000) +class ResizeDialog(QDialog): + def __init__(self, size, parent=None): + super().__init__(parent=parent) + + self.new_size = size + Nmax = size[0] * size[1] + + layout = QVBoxLayout(self) + + layout.addWidget(QLabel("Dataset size unknown. Please enter the shape:")) + + box_layout = QHBoxLayout() + box_layout.addWidget(QLabel("X:")) + + xbox = QSpinBox() + xbox.setRange(1, Nmax) + xbox.setSingleStep(1) + xbox.setKeyboardTracking(False) + xbox.valueChanged.connect(self.x_box_changed) + box_layout.addWidget(xbox) + + box_layout.addStretch() + box_layout.addWidget(QLabel("Y:")) + + ybox = QSpinBox() + ybox.setRange(1, Nmax) + ybox.setSingleStep(1) + ybox.setValue(Nmax) + ybox.setKeyboardTracking(False) + ybox.valueChanged.connect(self.y_box_changed) + box_layout.addWidget(ybox) + + layout.addLayout(box_layout) + + button_layout = QHBoxLayout() + button_layout.addStretch() + done_button = QPushButton("Done") + done_button.pressed.connect(self.close) + button_layout.addWidget(done_button) + layout.addLayout(button_layout) + + self.x_box = xbox + self.y_box = ybox + self.x_box_last = xbox.value() + self.y_box_last = ybox.value() + self.N = Nmax + + self.resize(600, 400) + + @classmethod + def get_new_size(cls, size, parent=None): + dialog = cls(size=size, parent=parent) + dialog.exec_() + return dialog.new_size + + def x_box_changed(self, new_value): + if new_value == self.x_box_last: + return + x_new, y_new = self.get_next_rect( + new_value, "down" if new_value < self.x_box_last else "up" + ) + + self.x_box_last = x_new + self.y_box_last = y_new + + self.x_box.setValue(x_new) + self.y_box.setValue(y_new) + + self.new_size = [x_new, y_new] + + def y_box_changed(self, new_value): + if new_value == self.y_box_last: + return + y_new, x_new = self.get_next_rect( + new_value, "down" if new_value < self.y_box_last else "up" + ) + + self.x_box_last = x_new + self.y_box_last = y_new + + self.x_box.setValue(x_new) + self.y_box.setValue(y_new) + + self.new_size = [x_new, y_new] + + def get_next_rect(self, current, direction): + # get the next perfect rectangle + iterator = ( + range(current, 0, -1) if direction == "down" else range(current, self.N + 1) + ) + + for i in iterator: + if self.N % i == 0: + return i, self.N // i + + raise ValueError("Factor finding failed, frustratingly.") + + def pg_point_roi(view_box): """ Point selection. Based in pyqtgraph, and returns a pyqtgraph CircleROI object. From 1465a76e4b049fff25fe2257464d50b24847107d Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 17 Mar 2024 11:04:52 -0400 Subject: [PATCH 12/34] show reshape dialog when a H5 file has 3D data --- src/py4D_browser/menu_actions.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index 27f95aa..b3bc7fc 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -1,3 +1,4 @@ +from numbers import Real import py4DSTEM from PyQt5.QtWidgets import QFileDialog, QMessageBox import h5py @@ -5,6 +6,7 @@ import numpy as np import matplotlib.pyplot as plt from py4D_browser.help_menu import KeyboardMapMenu +from py4D_browser.utils import ResizeDialog from py4DSTEM.io.filereaders import read_arina @@ -63,8 +65,9 @@ def load_file(self, filepath, mmap=False, binning=1): print(f"Loading file {filepath}") extension = os.path.splitext(filepath)[-1].lower() print(f"Type: {extension}") - if extension in (".h5", ".hdf5", ".py4dstem", ".emd"): - datacubes = get_ND(h5py.File(filepath, "r")) + if extension in (".h5", ".hdf5", ".py4dstem", ".emd", ".mat"): + file = h5py.File(filepath, "r") + datacubes = get_ND(file) print(f"Found {len(datacubes)} 4D datasets inside the HDF5 file...") if len(datacubes) >= 1: # Read the first datacube in the HDF5 file into RAM @@ -81,7 +84,17 @@ def load_file(self, filepath, mmap=False, binning=1): self.datacube.calibration.set_Q_pixel_units(Q_units) else: - raise ValueError("No 4D data detected in the H5 file!") + # if no 4D data was found, look for 3D data + datacubes = get_ND(file, N=3) + print(f"Found {len(datacubes)} 3D datasets inside the HDF5 file...") + if len(datacubes) >= 1: + array = datacubes[0] if mmap else datacubes[0][()] + new_shape = ResizeDialog.get_new_size([1, array.shape[0]], parent=self) + self.datacube = py4DSTEM.DataCube( + array.reshape(*new_shape, *array.shape[1:]) + ) + else: + raise ValueError("No 4D (or even 3D) data detected in the H5 file!") elif extension in [".npy"]: self.datacube = py4DSTEM.DataCube(np.load(filepath)) else: @@ -192,7 +205,7 @@ def show_file_dialog(self) -> str: self, "Open 4D-STEM Data", "", - "4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg *.h5 *.hdf5 *.emd *.py4dstem *.npy *.npz);;Any file (*)", + "4D-STEM Data (*.dm3 *.dm4 *.raw *.mib *.gtg *.h5 *.hdf5 *.emd *.py4dstem *.npy *.npz *.mat);;Any file (*)", ) if filename is not None and len(filename[0]) > 0: return filename[0] @@ -250,7 +263,7 @@ def get_ND(f, datacubes=None, N=4): for k in f.keys(): if isinstance(f[k], h5py.Dataset): # we found data - if len(f[k].shape) == 4: + if len(f[k].shape) == N: datacubes.append(f[k]) elif isinstance(f[k], h5py.Group): get_ND(f[k], datacubes) From 97b84a872ad036eb92b4ed6e86a2d5e981a2c2ad Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 17 Mar 2024 11:09:22 -0400 Subject: [PATCH 13/34] add reshape menu to adjust shape any time --- src/py4D_browser/main_window.py | 5 +++++ src/py4D_browser/menu_actions.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 4a1d33b..81f7391 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -43,6 +43,7 @@ class DataViewer(QMainWindow): export_datacube, export_virtual_image, show_keyboard_map, + reshape_data, ) from py4D_browser.update_views import ( @@ -120,6 +121,10 @@ def setup_menus(self): self.load_arina_action.triggered.connect(self.load_data_arina) self.file_menu.addAction(self.load_arina_action) + self.reshape_data_action = QAction("&Reshape Data...", self) + self.reshape_data_action.triggered.connect(self.reshape_data) + self.file_menu.addAction(self.reshape_data_action) + self.file_menu.addSeparator() export_label = QAction("Export", self) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index b3bc7fc..1b09ad3 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -121,6 +121,18 @@ def load_file(self, filepath, mmap=False, binning=1): self.setWindowTitle(filepath) +def reshape_data(self): + new_shape = ResizeDialog.get_new_size(self.datacube.shape[:2], parent=self) + self.datacube.data = self.datacube.data.reshape( + *new_shape, *self.datacube.data.shape[2:] + ) + + print(f"Reshaping data to {new_shape}") + + self.update_diffraction_space_view(reset=True) + self.update_real_space_view(reset=True) + + def export_datacube(self, save_format: str): assert save_format in [ "Raw float32", From 58c9a74e722959a3ad21e09b02b590bbe704f204 Mon Sep 17 00:00:00 2001 From: Steve Zeltmann Date: Mon, 15 Apr 2024 10:13:10 -0700 Subject: [PATCH 14/34] add pyrightconfig to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8817b70..c69b3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .pydevproject *.swp *.ipynb_checkpoints* +pyrightconfig.json # Folders # .idea/ From 13d1cb9cbeacc99722bd6cce78bc0a3cd44171c7 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 19 May 2024 13:18:48 -0400 Subject: [PATCH 15/34] starting to add tooltip --- src/py4D_browser/main_window.py | 43 ++++++++++++++++++++++++++++++++ src/py4D_browser/update_views.py | 22 ++++++++++++++++ src/py4D_browser/utils.py | 2 +- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 81f7391..6396e05 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -12,12 +12,14 @@ QPushButton, ) +from matplotlib.backend_bases import tools import pyqtgraph as pg import numpy as np from functools import partial from pathlib import Path import importlib +import os from py4D_browser.utils import pg_point_roi, VLine, LatchingButton from py4D_browser.scalebar import ScaleBar @@ -46,6 +48,11 @@ class DataViewer(QMainWindow): reshape_data, ) + from py4D_browser.reconstruct_actions import ( + reconstruct_tcBF_manual, + reconstruct_tcBF_auto, + ) + from py4D_browser.update_views import ( update_diffraction_space_view, update_real_space_view, @@ -55,6 +62,7 @@ class DataViewer(QMainWindow): nudge_diffraction_selector, update_annulus_pos, update_annulus_radii, + update_tooltip, ) HAS_EMPAD2 = importlib.util.find_spec("empad2") is not None @@ -94,6 +102,10 @@ def __init__(self, argv): if len(argv) > 1: self.load_file(argv[1]) + # launch pyqtgraph's debug console if environment variable exists + if os.environ.get("PY4DGUI_DEBUG"): + pg.dbg() + def setup_menus(self): self.menu_bar = self.menuBar() @@ -390,6 +402,19 @@ def setup_menus(self): partial(self.update_diffraction_space_view, False) ) + # Reconstructions menu + self.reconstruction_menu = QMenu("Reconstructions", self) + self.menu_bar.addMenu(self.reconstruction_menu) + + tcBF_action_manual = QAction("tcBF (Manual)...", self) + tcBF_action_manual.triggered.connect(self.reconstruct_tcBF_manual) + self.reconstruction_menu.addAction(tcBF_action_manual) + + tcBF_action_auto = QAction("tcBF (Auto)", self) + tcBF_action_auto.triggered.connect(self.reconstruct_tcBF_auto) + self.reconstruction_menu.addAction(tcBF_action_auto) + + # Help menu self.help_menu = QMenu("&Help", self) self.menu_bar.addMenu(self.help_menu) @@ -403,6 +428,24 @@ def setup_views(self): self.diffraction_space_widget.setImage(np.zeros((512, 512))) self.diffraction_space_view_text = QLabel("Slice") + self.diffraction_space_widget.setMouseTracking(True) + + # def f(x): + # import inspect + # curframe = inspect.currentframe() + # calframe = inspect.getouterframes(curframe, 2) + # print('caller name:', calframe[1][3]) + + # self.tooltip_proxy = pg.SignalProxy( + # signal=self.diffraction_space_widget.scene.sigMouseMoved, + # slot=f, + # rateLimit=5, + # ) + self.tooltip_timer = pg.ThreadsafeTimer() + self.tooltip_timer.timeout.connect(self.update_tooltip) + self.tooltip_timer.start(1000 // 30) # run at 30 Hz + # self.diffraction_space_widget.scene.sigMouseMoved.connect(f) + # Create virtual detector ROI selector self.virtual_detector_point = pg_point_roi( self.diffraction_space_widget.getView() diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 48948f4..b94274e 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -2,6 +2,9 @@ import numpy as np import py4DSTEM from functools import partial +from PyQt5.QtWidgets import QApplication +from PyQt5 import QtCore +from PyQt5.QtGui import QCursor from py4D_browser.utils import pg_point_roi, make_detector, complex_to_Lab @@ -510,6 +513,25 @@ def nudge_diffraction_selector(self, dx, dy): selector.setPos(position) +def update_tooltip(self): + modifier_keys = QApplication.queryKeyboardModifiers() + + if QtCore.Qt.ControlModifier == modifier_keys: + pos = self.mapFromGlobal(QCursor.pos()) + print(f"global: {QCursor.pos()}\tapplication: {pos}") + + for scene in [ + self.diffraction_space_widget, + self.real_space_widget, + self.fft_widget, + ]: + pos_in_scene = scene.mapFromGlobal(QCursor.pos()) + print(f"In view: {pos_in_scene}") + if scene.getView().rect().contains(pos_in_scene): + pos_in_data = scene.view.mapSceneToView(pos_in_scene) + print(f"Inside: {scene} at {pos_in_data}") + + def update_annulus_pos(self): """ Function to keep inner and outer rings of annulus aligned. diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index 94a979c..8196261 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -2,7 +2,7 @@ import numpy as np from PyQt5.QtWidgets import QFrame, QPushButton, QApplication, QLabel from PyQt5.QtCore import pyqtSignal -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QObject from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout, QSpinBox From 1d39a78ae9ba87409c4e5bc662e40fd66770ada5 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 19 May 2024 13:21:04 -0400 Subject: [PATCH 16/34] move tooltip code around --- src/py4D_browser/main_window.py | 21 +++++---------------- src/py4D_browser/update_views.py | 4 ++-- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 6396e05..fcff2c2 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -94,6 +94,11 @@ def __init__(self, argv): self.setup_menus() self.setup_views() + # setup listener for tooltip + self.tooltip_timer = pg.ThreadsafeTimer() + self.tooltip_timer.timeout.connect(self.update_tooltip) + self.tooltip_timer.start(1000 // 30) # run at 30 Hz + self.resize(1000, 800) self.show() @@ -430,22 +435,6 @@ def setup_views(self): self.diffraction_space_widget.setMouseTracking(True) - # def f(x): - # import inspect - # curframe = inspect.currentframe() - # calframe = inspect.getouterframes(curframe, 2) - # print('caller name:', calframe[1][3]) - - # self.tooltip_proxy = pg.SignalProxy( - # signal=self.diffraction_space_widget.scene.sigMouseMoved, - # slot=f, - # rateLimit=5, - # ) - self.tooltip_timer = pg.ThreadsafeTimer() - self.tooltip_timer.timeout.connect(self.update_tooltip) - self.tooltip_timer.start(1000 // 30) # run at 30 Hz - # self.diffraction_space_widget.scene.sigMouseMoved.connect(f) - # Create virtual detector ROI selector self.virtual_detector_point = pg_point_roi( self.diffraction_space_widget.getView() diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index b94274e..34211e8 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -518,7 +518,7 @@ def update_tooltip(self): if QtCore.Qt.ControlModifier == modifier_keys: pos = self.mapFromGlobal(QCursor.pos()) - print(f"global: {QCursor.pos()}\tapplication: {pos}") + # print(f"global: {QCursor.pos()}\tapplication: {pos}") for scene in [ self.diffraction_space_widget, @@ -526,7 +526,7 @@ def update_tooltip(self): self.fft_widget, ]: pos_in_scene = scene.mapFromGlobal(QCursor.pos()) - print(f"In view: {pos_in_scene}") + # print(f"In view: {pos_in_scene}") if scene.getView().rect().contains(pos_in_scene): pos_in_data = scene.view.mapSceneToView(pos_in_scene) print(f"Inside: {scene} at {pos_in_data}") From ee995ce3d47ac4e59fcef8904569895bb7235aa4 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Thu, 23 May 2024 12:21:26 -0400 Subject: [PATCH 17/34] tooltip mostly working --- src/py4D_browser/main_window.py | 7 +++++-- src/py4D_browser/update_views.py | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index fcff2c2..c99f1ec 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -9,7 +9,7 @@ QSplitter, QActionGroup, QLabel, - QPushButton, + QToolTip, ) from matplotlib.backend_bases import tools @@ -97,7 +97,10 @@ def __init__(self, argv): # setup listener for tooltip self.tooltip_timer = pg.ThreadsafeTimer() self.tooltip_timer.timeout.connect(self.update_tooltip) - self.tooltip_timer.start(1000 // 30) # run at 30 Hz + self.tooltip_timer.start(1000 // 30) # run tooltip at 30 Hz + font = QtGui.QFont(self.font()) + font.setPointSize(10) + QToolTip.setFont(font) self.resize(1000, 800) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 34211e8..e793323 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -2,7 +2,7 @@ import numpy as np import py4DSTEM from functools import partial -from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QApplication, QToolTip from PyQt5 import QtCore from PyQt5.QtGui import QCursor @@ -233,6 +233,7 @@ def update_real_space_view(self, reset=False): if mode_switch: # Need to autorange after setRect self.fft_widget.autoRange() + self.unscaled_fft_image = fft def update_diffraction_space_view(self, reset=False): @@ -517,19 +518,26 @@ def update_tooltip(self): modifier_keys = QApplication.queryKeyboardModifiers() if QtCore.Qt.ControlModifier == modifier_keys: - pos = self.mapFromGlobal(QCursor.pos()) + global_pos = QCursor.pos() + # pos = self.mapFromGlobal(global_pos) # print(f"global: {QCursor.pos()}\tapplication: {pos}") - for scene in [ - self.diffraction_space_widget, - self.real_space_widget, - self.fft_widget, + for scene, data in [ + (self.diffraction_space_widget, self.unscaled_diffraction_image), + (self.real_space_widget, self.unscaled_realspace_image), + (self.fft_widget, self.unscaled_fft_image), ]: pos_in_scene = scene.mapFromGlobal(QCursor.pos()) # print(f"In view: {pos_in_scene}") if scene.getView().rect().contains(pos_in_scene): pos_in_data = scene.view.mapSceneToView(pos_in_scene) - print(f"Inside: {scene} at {pos_in_data}") + # print(f"Inside: {scene} at {pos_in_data}") + + y = int(np.clip(np.floor(pos_in_data.x()), 0, data.shape[0] - 1)) + x = int(np.clip(np.floor(pos_in_data.y()), 0, data.shape[1] - 1)) + display_text = f"[{x},{y}]: {data[x,y]:.5g}" + + QToolTip.showText(global_pos, display_text) def update_annulus_pos(self): From 498ee05850c20ac2a985d9359f7a790a6c91e182 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 24 May 2024 09:54:26 -0400 Subject: [PATCH 18/34] add autoscale range options --- src/py4D_browser/main_window.py | 52 ++++++++++++++++++++++++++++++-- src/py4D_browser/update_views.py | 30 ++++++++++++++---- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index c99f1ec..d2c0d44 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -58,6 +58,8 @@ class DataViewer(QMainWindow): update_real_space_view, update_realspace_detector, update_diffraction_detector, + set_diffraction_autoscale_range, + set_real_space_autoscale_range, nudge_real_space_selector, nudge_diffraction_selector, update_annulus_pos, @@ -241,6 +243,28 @@ def setup_menus(self): self.scaling_menu.addSeparator() + diff_autoscale_separator = QAction("Diffraction Autoscale", self) + diff_autoscale_separator.setDisabled(True) + self.scaling_menu.addAction(diff_autoscale_separator) + + diff_range_group = QActionGroup(self) + diff_range_group.setExclusive(True) + + for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]: + action = QAction(f"{scale_range[0]}% - {scale_range[1]}%", self) + diff_range_group.addAction(action) + self.scaling_menu.addAction(action) + action.setCheckable(True) + action.triggered.connect( + partial(self.set_diffraction_autoscale_range, scale_range) + ) + # set default + if scale_range[0] == 2 and scale_range[1] == 98: + action.setChecked(True) + self.set_diffraction_autoscale_range(scale_range, redraw=False) + + self.scaling_menu.addSeparator() + # Real space scaling vimg_scaling_group = QActionGroup(self) vimg_scaling_group.setExclusive(True) @@ -276,6 +300,28 @@ def setup_menus(self): vimg_scaling_group.addAction(vimg_scale_sqrt_action) self.scaling_menu.addAction(vimg_scale_sqrt_action) + self.scaling_menu.addSeparator() + + vimg_autoscale_separator = QAction("Virtual Image Autoscale", self) + vimg_autoscale_separator.setDisabled(True) + self.scaling_menu.addAction(vimg_autoscale_separator) + + vimg_range_group = QActionGroup(self) + vimg_range_group.setExclusive(True) + + for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]: + action = QAction(f"{scale_range[0]}% - {scale_range[1]}%", self) + vimg_range_group.addAction(action) + self.scaling_menu.addAction(action) + action.setCheckable(True) + action.triggered.connect( + partial(self.set_real_space_autoscale_range, scale_range) + ) + # set default + if scale_range[0] == 2 and scale_range[1] == 98: + action.setChecked(True) + self.set_real_space_autoscale_range(scale_range, redraw=False) + # Detector Response Menu self.detector_menu = QMenu("&Detector Response", self) self.menu_bar.addMenu(self.detector_menu) @@ -361,7 +407,7 @@ def setup_menus(self): self.detector_shape_menu.addSeparator() - diffraction_detector_separator = QAction("Real Space", self) + diffraction_detector_separator = QAction("Virtual Image", self) diffraction_detector_separator.setDisabled(True) self.detector_shape_menu.addAction(diffraction_detector_separator) @@ -473,7 +519,7 @@ def setup_views(self): self.real_space_scale_bar.anchor((1, 1), (1, 1), offset=(-40, -40)) # Name and return - self.real_space_widget.setWindowTitle("Real Space") + self.real_space_widget.setWindowTitle("Virtual Image") self.diffraction_space_widget.setAcceptDrops(True) self.real_space_widget.setAcceptDrops(True) @@ -541,7 +587,7 @@ def setup_views(self): ) self.statusBar().addPermanentWidget(self.diffraction_rescale_button) self.realspace_rescale_button = LatchingButton( - "Autoscale Real Space", + "Autoscale Virtual Image", status_bar=self.statusBar(), latched=True, ) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index e793323..c6304f2 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -187,7 +187,10 @@ def update_real_space_view(self, reset=False): new_view.T, autoLevels=False, levels=( - (np.percentile(new_view, 2), np.percentile(new_view, 98)) + ( + np.percentile(new_view, self.real_space_autoscale_percentiles[0]), + np.percentile(new_view, self.real_space_autoscale_percentiles[1]), + ) if auto_level else None ), @@ -302,7 +305,10 @@ def update_diffraction_space_view(self, reset=False): new_view.T, autoLevels=False, levels=( - (np.percentile(new_view, 2), np.percentile(new_view, 98)) + ( + np.percentile(new_view, self.diffraction_autoscale_percentiles[0]), + np.percentile(new_view, self.diffraction_autoscale_percentiles[1]), + ) if auto_level else None ), @@ -468,6 +474,20 @@ def update_diffraction_detector(self): self.update_real_space_view(reset=True) +def set_diffraction_autoscale_range(self, percentiles, redraw=True): + self.diffraction_autoscale_percentiles = percentiles + + if redraw: + self.update_diffraction_space_view(reset=False) + + +def set_real_space_autoscale_range(self, percentiles, redraw=True): + self.real_space_autoscale_percentiles = percentiles + + if redraw: + self.update_real_space_view(reset=False) + + def nudge_real_space_selector(self, dx, dy): if ( hasattr(self, "real_space_point_selector") @@ -519,8 +539,6 @@ def update_tooltip(self): if QtCore.Qt.ControlModifier == modifier_keys: global_pos = QCursor.pos() - # pos = self.mapFromGlobal(global_pos) - # print(f"global: {QCursor.pos()}\tapplication: {pos}") for scene, data in [ (self.diffraction_space_widget, self.unscaled_diffraction_image), @@ -528,15 +546,15 @@ def update_tooltip(self): (self.fft_widget, self.unscaled_fft_image), ]: pos_in_scene = scene.mapFromGlobal(QCursor.pos()) - # print(f"In view: {pos_in_scene}") if scene.getView().rect().contains(pos_in_scene): pos_in_data = scene.view.mapSceneToView(pos_in_scene) - # print(f"Inside: {scene} at {pos_in_data}") y = int(np.clip(np.floor(pos_in_data.x()), 0, data.shape[0] - 1)) x = int(np.clip(np.floor(pos_in_data.y()), 0, data.shape[1] - 1)) display_text = f"[{x},{y}]: {data[x,y]:.5g}" + # Clearing the tooltip forces it to move every tick, but it flickers + # QToolTip.showText(global_pos, "") QToolTip.showText(global_pos, display_text) From 2947c2d75bec2314bf0192240ed46e3b79fd14a3 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 24 May 2024 11:03:51 -0400 Subject: [PATCH 19/34] move autorange to a new menu --- src/py4D_browser/main_window.py | 58 +++++++++++++++++---------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index d2c0d44..aa26c7c 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -243,28 +243,6 @@ def setup_menus(self): self.scaling_menu.addSeparator() - diff_autoscale_separator = QAction("Diffraction Autoscale", self) - diff_autoscale_separator.setDisabled(True) - self.scaling_menu.addAction(diff_autoscale_separator) - - diff_range_group = QActionGroup(self) - diff_range_group.setExclusive(True) - - for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]: - action = QAction(f"{scale_range[0]}% - {scale_range[1]}%", self) - diff_range_group.addAction(action) - self.scaling_menu.addAction(action) - action.setCheckable(True) - action.triggered.connect( - partial(self.set_diffraction_autoscale_range, scale_range) - ) - # set default - if scale_range[0] == 2 and scale_range[1] == 98: - action.setChecked(True) - self.set_diffraction_autoscale_range(scale_range, redraw=False) - - self.scaling_menu.addSeparator() - # Real space scaling vimg_scaling_group = QActionGroup(self) vimg_scaling_group.setExclusive(True) @@ -300,11 +278,35 @@ def setup_menus(self): vimg_scaling_group.addAction(vimg_scale_sqrt_action) self.scaling_menu.addAction(vimg_scale_sqrt_action) - self.scaling_menu.addSeparator() + # Autorange menu + self.autorange_menu = QMenu("&Autorange", self) + self.menu_bar.addMenu(self.autorange_menu) + + diff_autoscale_separator = QAction("Diffraction", self) + diff_autoscale_separator.setDisabled(True) + self.autorange_menu.addAction(diff_autoscale_separator) + + diff_range_group = QActionGroup(self) + diff_range_group.setExclusive(True) + + for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]: + action = QAction(f"{scale_range[0]}% - {scale_range[1]}%", self) + diff_range_group.addAction(action) + self.autorange_menu.addAction(action) + action.setCheckable(True) + action.triggered.connect( + partial(self.set_diffraction_autoscale_range, scale_range) + ) + # set default + if scale_range[0] == 2 and scale_range[1] == 98: + action.setChecked(True) + self.set_diffraction_autoscale_range(scale_range, redraw=False) + + self.autorange_menu.addSeparator() - vimg_autoscale_separator = QAction("Virtual Image Autoscale", self) + vimg_autoscale_separator = QAction("Virtual Image", self) vimg_autoscale_separator.setDisabled(True) - self.scaling_menu.addAction(vimg_autoscale_separator) + self.autorange_menu.addAction(vimg_autoscale_separator) vimg_range_group = QActionGroup(self) vimg_range_group.setExclusive(True) @@ -312,7 +314,7 @@ def setup_menus(self): for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]: action = QAction(f"{scale_range[0]}% - {scale_range[1]}%", self) vimg_range_group.addAction(action) - self.scaling_menu.addAction(action) + self.autorange_menu.addAction(action) action.setCheckable(True) action.triggered.connect( partial(self.set_real_space_autoscale_range, scale_range) @@ -578,7 +580,7 @@ def setup_views(self): self.statusBar().addPermanentWidget(self.real_space_view_text) self.statusBar().addPermanentWidget(VLine()) self.diffraction_rescale_button = LatchingButton( - "Autoscale Diffraction", + "Autorange Diffraction", status_bar=self.statusBar(), latched=True, ) @@ -587,7 +589,7 @@ def setup_views(self): ) self.statusBar().addPermanentWidget(self.diffraction_rescale_button) self.realspace_rescale_button = LatchingButton( - "Autoscale Virtual Image", + "Autorange Virtual Image", status_bar=self.statusBar(), latched=True, ) From f0db42da4d907a322ff7a79638dfa868490850e2 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 24 May 2024 12:54:03 -0400 Subject: [PATCH 20/34] remove reconstruction menu --- src/py4D_browser/main_window.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index aa26c7c..71cca4c 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -48,11 +48,6 @@ class DataViewer(QMainWindow): reshape_data, ) - from py4D_browser.reconstruct_actions import ( - reconstruct_tcBF_manual, - reconstruct_tcBF_auto, - ) - from py4D_browser.update_views import ( update_diffraction_space_view, update_real_space_view, @@ -458,18 +453,6 @@ def setup_menus(self): partial(self.update_diffraction_space_view, False) ) - # Reconstructions menu - self.reconstruction_menu = QMenu("Reconstructions", self) - self.menu_bar.addMenu(self.reconstruction_menu) - - tcBF_action_manual = QAction("tcBF (Manual)...", self) - tcBF_action_manual.triggered.connect(self.reconstruct_tcBF_manual) - self.reconstruction_menu.addAction(tcBF_action_manual) - - tcBF_action_auto = QAction("tcBF (Auto)", self) - tcBF_action_auto.triggered.connect(self.reconstruct_tcBF_auto) - self.reconstruction_menu.addAction(tcBF_action_auto) - # Help menu self.help_menu = QMenu("&Help", self) self.menu_bar.addMenu(self.help_menu) From cd4e224e9c92833c268d8155d250c33d5c091346 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 24 May 2024 12:54:25 -0400 Subject: [PATCH 21/34] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a715a7e..b1b145f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "py4D_browser" -version = "1.0.0" +version = "1.1.0" authors = [ { name="Steven Zeltmann", email="steven.zeltmann@lbl.gov" }, ] From 2fa91c3f4d0ead48f9b63510419fbc37cbdc637e Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Sun, 26 May 2024 10:24:55 -0400 Subject: [PATCH 22/34] change hyphen to en-dash --- src/py4D_browser/main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 71cca4c..6b438f3 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -285,7 +285,7 @@ def setup_menus(self): diff_range_group.setExclusive(True) for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]: - action = QAction(f"{scale_range[0]}% - {scale_range[1]}%", self) + action = QAction(f"{scale_range[0]}% – {scale_range[1]}%", self) diff_range_group.addAction(action) self.autorange_menu.addAction(action) action.setCheckable(True) @@ -307,7 +307,7 @@ def setup_menus(self): vimg_range_group.setExclusive(True) for scale_range in [(0, 100), (0.1, 99.9), (1, 99), (2, 98), (5, 95)]: - action = QAction(f"{scale_range[0]}% - {scale_range[1]}%", self) + action = QAction(f"{scale_range[0]}% – {scale_range[1]}%", self) vimg_range_group.addAction(action) self.autorange_menu.addAction(action) action.setCheckable(True) From a637d4650ccc8d91622bfb823f41b07f74236feb Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Wed, 10 Jul 2024 13:12:18 -0400 Subject: [PATCH 23/34] prevent tooltip before loading data --- src/py4D_browser/update_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index c6304f2..78d54cf 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -536,8 +536,9 @@ def nudge_diffraction_selector(self, dx, dy): def update_tooltip(self): modifier_keys = QApplication.queryKeyboardModifiers() + print(self.isHidden()) - if QtCore.Qt.ControlModifier == modifier_keys: + if QtCore.Qt.ControlModifier == modifier_keys and self.datacube is not None: global_pos = QCursor.pos() for scene, data in [ From 72e3e980a6593687186e7b10d6d92f9da1dd862d Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Thu, 11 Jul 2024 17:35:42 -0400 Subject: [PATCH 24/34] add calibration menu --- src/py4D_browser/dialogs.py | 279 +++++++++++++++++++++++++++++++ src/py4D_browser/main_window.py | 18 ++ src/py4D_browser/menu_actions.py | 47 +++++- src/py4D_browser/update_views.py | 2 +- src/py4D_browser/utils.py | 98 ----------- 5 files changed, 337 insertions(+), 107 deletions(-) create mode 100644 src/py4D_browser/dialogs.py diff --git a/src/py4D_browser/dialogs.py b/src/py4D_browser/dialogs.py new file mode 100644 index 0000000..e492d2e --- /dev/null +++ b/src/py4D_browser/dialogs.py @@ -0,0 +1,279 @@ +from cupy import real +from py4DSTEM import DataCube, data +import pyqtgraph as pg +import numpy as np +from PyQt5.QtWidgets import QFrame, QPushButton, QApplication, QLabel +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import Qt, QObject +from PyQt5.QtGui import QDoubleValidator +from PyQt5.QtWidgets import ( + QDialog, + QHBoxLayout, + QVBoxLayout, + QSpinBox, + QLineEdit, + QComboBox, + QGroupBox, + QGridLayout, +) + + +class ResizeDialog(QDialog): + def __init__(self, size, parent=None): + super().__init__(parent=parent) + + self.new_size = size + Nmax = size[0] * size[1] + + layout = QVBoxLayout(self) + + layout.addWidget(QLabel("Dataset size unknown. Please enter the shape:")) + + box_layout = QHBoxLayout() + box_layout.addWidget(QLabel("X:")) + + xbox = QSpinBox() + xbox.setRange(1, Nmax) + xbox.setSingleStep(1) + xbox.setKeyboardTracking(False) + xbox.valueChanged.connect(self.x_box_changed) + box_layout.addWidget(xbox) + + box_layout.addStretch() + box_layout.addWidget(QLabel("Y:")) + + ybox = QSpinBox() + ybox.setRange(1, Nmax) + ybox.setSingleStep(1) + ybox.setValue(Nmax) + ybox.setKeyboardTracking(False) + ybox.valueChanged.connect(self.y_box_changed) + box_layout.addWidget(ybox) + + layout.addLayout(box_layout) + + button_layout = QHBoxLayout() + button_layout.addStretch() + done_button = QPushButton("Done") + done_button.pressed.connect(self.close) + button_layout.addWidget(done_button) + layout.addLayout(button_layout) + + self.x_box = xbox + self.y_box = ybox + self.x_box_last = xbox.value() + self.y_box_last = ybox.value() + self.N = Nmax + + self.resize(600, 400) + + @classmethod + def get_new_size(cls, size, parent=None): + dialog = cls(size=size, parent=parent) + dialog.exec_() + return dialog.new_size + + def x_box_changed(self, new_value): + if new_value == self.x_box_last: + return + x_new, y_new = self.get_next_rect( + new_value, "down" if new_value < self.x_box_last else "up" + ) + + self.x_box_last = x_new + self.y_box_last = y_new + + self.x_box.setValue(x_new) + self.y_box.setValue(y_new) + + self.new_size = [x_new, y_new] + + def y_box_changed(self, new_value): + if new_value == self.y_box_last: + return + y_new, x_new = self.get_next_rect( + new_value, "down" if new_value < self.y_box_last else "up" + ) + + self.x_box_last = x_new + self.y_box_last = y_new + + self.x_box.setValue(x_new) + self.y_box.setValue(y_new) + + self.new_size = [x_new, y_new] + + def get_next_rect(self, current, direction): + # get the next perfect rectangle + iterator = ( + range(current, 0, -1) if direction == "down" else range(current, self.N + 1) + ) + + for i in iterator: + if self.N % i == 0: + return i, self.N // i + + raise ValueError("Factor finding failed, frustratingly.") + + +class CalibrateDialog(QDialog): + def __init__(self, datacube, parent, diffraction_selector_size=None): + super().__init__(parent=parent) + + self.datacube = datacube + self.parent = parent + self.diffraction_selector_size = diffraction_selector_size + + layout = QVBoxLayout(self) + + ####### LAYOUT ######## + + realspace_box = QGroupBox("Real Space") + layout.addWidget(realspace_box) + realspace_layout = QHBoxLayout() + realspace_box.setLayout(realspace_layout) + + realspace_left_layout = QGridLayout() + realspace_layout.addLayout(realspace_left_layout) + + realspace_left_layout.addWidget(QLabel("Pixel Size"), 0, 0, Qt.AlignRight) + self.realspace_pix_box = QLineEdit() + self.realspace_pix_box.setValidator(QDoubleValidator()) + realspace_left_layout.addWidget(self.realspace_pix_box, 0, 1) + + realspace_left_layout.addWidget(QLabel("Full Width"), 1, 0, Qt.AlignRight) + self.realspace_fov_box = QLineEdit() + realspace_left_layout.addWidget(self.realspace_fov_box, 1, 1) + + realspace_right_layout = QHBoxLayout() + realspace_layout.addLayout(realspace_right_layout) + self.realspace_unit_box = QComboBox() + self.realspace_unit_box.addItems(["Å", "nm"]) + self.realspace_unit_box.setMinimumContentsLength(5) + realspace_right_layout.addWidget(self.realspace_unit_box) + + diff_box = QGroupBox("Diffraction") + layout.addWidget(diff_box) + diff_layout = QHBoxLayout() + diff_box.setLayout(diff_layout) + + diff_left_layout = QGridLayout() + diff_layout.addLayout(diff_left_layout) + + diff_left_layout.addWidget(QLabel("Pixel Size"), 0, 0, Qt.AlignRight) + self.diff_pix_box = QLineEdit() + diff_left_layout.addWidget(self.diff_pix_box, 0, 1) + + diff_left_layout.addWidget(QLabel("Full Width"), 1, 0, Qt.AlignRight) + self.diff_fov_box = QLineEdit() + diff_left_layout.addWidget(self.diff_fov_box, 1, 1) + + diff_left_layout.addWidget(QLabel("Selection Radius"), 2, 0, Qt.AlignRight) + self.diff_selection_box = QLineEdit() + diff_left_layout.addWidget(self.diff_selection_box, 2, 1) + self.diff_selection_box.setEnabled(self.diffraction_selector_size is not None) + + diff_right_layout = QHBoxLayout() + diff_layout.addLayout(diff_right_layout) + self.diff_unit_box = QComboBox() + self.diff_unit_box.setMinimumContentsLength(5) + self.diff_unit_box.addItems( + [ + "mrad", + "Å⁻¹", + # "nm⁻¹", + ] + ) + diff_right_layout.addWidget(self.diff_unit_box) + + button_layout = QHBoxLayout() + button_layout.addStretch() + cancel_button = QPushButton("Cancel") + cancel_button.pressed.connect(self.close) + button_layout.addWidget(cancel_button) + done_button = QPushButton("Done") + done_button.pressed.connect(self.set_and_close) + button_layout.addWidget(done_button) + layout.addLayout(button_layout) + + ######### CALLBACKS ######## + self.realspace_pix_box.textEdited.connect(self.realspace_pix_box_changed) + self.realspace_fov_box.textEdited.connect(self.realspace_fov_box_changed) + self.diff_pix_box.textEdited.connect(self.diffraction_pix_box_changed) + self.diff_fov_box.textEdited.connect(self.diffraction_fov_box_changed) + self.diff_selection_box.textEdited.connect( + self.diffraction_selection_box_changed + ) + + def realspace_pix_box_changed(self, new_text): + pix_size = float(new_text) + + fov = pix_size * self.datacube.R_Ny + self.realspace_fov_box.setText(f"{fov:g}") + + def realspace_fov_box_changed(self, new_text): + fov = float(new_text) + + pix_size = fov / self.datacube.R_Ny + self.realspace_pix_box.setText(f"{pix_size:g}") + + def diffraction_pix_box_changed(self, new_text): + pix_size = float(new_text) + + fov = pix_size * self.datacube.Q_Ny + self.diff_fov_box.setText(f"{fov:g}") + + if self.diffraction_selector_size: + sel_size = pix_size * self.diffraction_selector_size + self.diff_selection_box.setText(f"{sel_size:.g}") + + def diffraction_fov_box_changed(self, new_text): + fov = float(new_text) + + pix_size = fov / self.datacube.Q_Ny + self.diff_pix_box.setText(f"{pix_size:g}") + + if self.diffraction_selector_size: + sel_size = pix_size * self.diffraction_selector_size + self.diff_selection_box.setText(f"{sel_size:.g}") + + def diffraction_selection_box_changed(self, new_text): + if self.diffraction_selector_size: + sel_size = float(new_text) + + pix_size = sel_size / self.diffraction_selector_size + fov = pix_size * self.Q_Nx + self.diff_pix_box.setText(f"{pix_size:g}") + self.diff_fov_box.setText(f"{fov:g}") + + sel_size = pix_size * self.diffraction_selector_size + self.diff_selection_box.setText(f"{sel_size:.g}") + + def set_and_close(self): + + realspace_text = self.realspace_pix_box.text() + if realspace_text != "": + realspace_pix = float(realspace_text) + self.datacube.calibration.set_R_pixel_size(realspace_pix) + self.datacube.calibration.set_R_pixel_units( + self.realspace_unit_box.currentText().replace("Å", "A") + ) + + diff_text = self.diff_pix_box.text() + if diff_text != "": + diff_pix = float(diff_text) + self.datacube.calibration.set_Q_pixel_size(diff_pix) + translation = { + "mrad": "mrad", + "Å⁻¹": "A^-1", + "nm⁻¹": "1/nm", + } + self.datacube.calibration.set_Q_pixel_units( + translation[self.diff_unit_box.currentText()] + ) + + self.parent.update_scalebars() + + print(self.datacube.calibration) + + self.close() diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 6b438f3..cae750d 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -45,7 +45,9 @@ class DataViewer(QMainWindow): export_datacube, export_virtual_image, show_keyboard_map, + show_calibration_dialog, reshape_data, + update_scalebars, ) from py4D_browser.update_views import ( @@ -453,6 +455,22 @@ def setup_menus(self): partial(self.update_diffraction_space_view, False) ) + # Processing menu + self.processing_menu = QMenu("&Processing", self) + self.menu_bar.addMenu(self.processing_menu) + + calibrate_action = QAction("&Calibrate...", self) + calibrate_action.triggered.connect(self.show_calibration_dialog) + self.processing_menu.addAction(calibrate_action) + + # tcBF_action_manual = QAction("tcBF (Manual)...", self) + # tcBF_action_manual.triggered.connect(self.reconstruct_tcBF_manual) + # self.processing_menu.addAction(tcBF_action_manual) + + # tcBF_action_auto = QAction("tcBF (Auto)", self) + # tcBF_action_auto.triggered.connect(self.reconstruct_tcBF_auto) + # self.processing_menu.addAction(tcBF_action_auto) + # Help menu self.help_menu = QMenu("&Help", self) self.menu_bar.addMenu(self.help_menu) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index 1b09ad3..168fcda 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -6,7 +6,7 @@ import numpy as np import matplotlib.pyplot as plt from py4D_browser.help_menu import KeyboardMapMenu -from py4D_browser.utils import ResizeDialog +from py4D_browser.dialogs import CalibrateDialog, ResizeDialog from py4DSTEM.io.filereaders import read_arina @@ -104,21 +104,47 @@ def load_file(self, filepath, mmap=False, binning=1): binfactor=binning, ) + self.update_scalebars() + + self.update_diffraction_space_view(reset=True) + self.update_real_space_view(reset=True) + + self.setWindowTitle(filepath) + + +def update_scalebars(self): + + realspace_translation = { + "A": "Å", + } + reciprocal_translation = { + "A^-1": "Å⁻¹", + } + self.diffraction_scale_bar.pixel_size = self.datacube.calibration.get_Q_pixel_size() - self.diffraction_scale_bar.units = self.datacube.calibration.get_Q_pixel_units() + q_units = self.datacube.calibration.get_Q_pixel_units() + self.diffraction_scale_bar.units = ( + reciprocal_translation[q_units] + if q_units in reciprocal_translation.keys() + else q_units + ) self.real_space_scale_bar.pixel_size = self.datacube.calibration.get_R_pixel_size() - self.real_space_scale_bar.units = self.datacube.calibration.get_R_pixel_units() + r_units = self.datacube.calibration.get_R_pixel_units() + self.real_space_scale_bar.units = ( + realspace_translation[r_units] + if r_units in realspace_translation.keys() + else r_units + ) self.fft_scale_bar.pixel_size = ( 1.0 / self.datacube.calibration.get_R_pixel_size() / self.datacube.R_Ny ) - self.fft_scale_bar.units = f"1/{self.datacube.calibration.get_R_pixel_units()}" + self.fft_scale_bar.units = f"{self.datacube.calibration.get_R_pixel_units()}⁻¹" - self.update_diffraction_space_view(reset=True) - self.update_real_space_view(reset=True) - - self.setWindowTitle(filepath) + self.diffraction_scale_bar.updateBar() + self.real_space_scale_bar.updateBar() + self.fft_scale_bar.updateBar() def reshape_data(self): @@ -212,6 +238,11 @@ def show_keyboard_map(self): keymap.open() +def show_calibration_dialog(self): + dialog = CalibrateDialog(self.datacube, parent=self) + dialog.open() + + def show_file_dialog(self) -> str: filename = QFileDialog.getOpenFileName( self, diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 78d54cf..35c4885 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -536,7 +536,7 @@ def nudge_diffraction_selector(self, dx, dy): def update_tooltip(self): modifier_keys = QApplication.queryKeyboardModifiers() - print(self.isHidden()) + # print(self.isHidden()) if QtCore.Qt.ControlModifier == modifier_keys and self.datacube is not None: global_pos = QCursor.pos() diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index 8196261..78c7c9d 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -53,104 +53,6 @@ def on_click(self, *args): self.status_bar.showMessage("Shift+click to keep on", 5_000) -class ResizeDialog(QDialog): - def __init__(self, size, parent=None): - super().__init__(parent=parent) - - self.new_size = size - Nmax = size[0] * size[1] - - layout = QVBoxLayout(self) - - layout.addWidget(QLabel("Dataset size unknown. Please enter the shape:")) - - box_layout = QHBoxLayout() - box_layout.addWidget(QLabel("X:")) - - xbox = QSpinBox() - xbox.setRange(1, Nmax) - xbox.setSingleStep(1) - xbox.setKeyboardTracking(False) - xbox.valueChanged.connect(self.x_box_changed) - box_layout.addWidget(xbox) - - box_layout.addStretch() - box_layout.addWidget(QLabel("Y:")) - - ybox = QSpinBox() - ybox.setRange(1, Nmax) - ybox.setSingleStep(1) - ybox.setValue(Nmax) - ybox.setKeyboardTracking(False) - ybox.valueChanged.connect(self.y_box_changed) - box_layout.addWidget(ybox) - - layout.addLayout(box_layout) - - button_layout = QHBoxLayout() - button_layout.addStretch() - done_button = QPushButton("Done") - done_button.pressed.connect(self.close) - button_layout.addWidget(done_button) - layout.addLayout(button_layout) - - self.x_box = xbox - self.y_box = ybox - self.x_box_last = xbox.value() - self.y_box_last = ybox.value() - self.N = Nmax - - self.resize(600, 400) - - @classmethod - def get_new_size(cls, size, parent=None): - dialog = cls(size=size, parent=parent) - dialog.exec_() - return dialog.new_size - - def x_box_changed(self, new_value): - if new_value == self.x_box_last: - return - x_new, y_new = self.get_next_rect( - new_value, "down" if new_value < self.x_box_last else "up" - ) - - self.x_box_last = x_new - self.y_box_last = y_new - - self.x_box.setValue(x_new) - self.y_box.setValue(y_new) - - self.new_size = [x_new, y_new] - - def y_box_changed(self, new_value): - if new_value == self.y_box_last: - return - y_new, x_new = self.get_next_rect( - new_value, "down" if new_value < self.y_box_last else "up" - ) - - self.x_box_last = x_new - self.y_box_last = y_new - - self.x_box.setValue(x_new) - self.y_box.setValue(y_new) - - self.new_size = [x_new, y_new] - - def get_next_rect(self, current, direction): - # get the next perfect rectangle - iterator = ( - range(current, 0, -1) if direction == "down" else range(current, self.N + 1) - ) - - for i in iterator: - if self.N % i == 0: - return i, self.N // i - - raise ValueError("Factor finding failed, frustratingly.") - - def pg_point_roi(view_box): """ Point selection. Based in pyqtgraph, and returns a pyqtgraph CircleROI object. From d51aa566d1dc3010ec0bdcc6c3952bc8d8785a5b Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 12 Jul 2024 12:49:48 -0400 Subject: [PATCH 25/34] window FFTs --- src/py4D_browser/update_views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 35c4885..c005122 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -198,8 +198,11 @@ def update_real_space_view(self, reset=False): ) # Update FFT view + fft_window = ( + np.hanning(new_view.shape[0])[:, None] * np.hanning(new_view.shape[1])[None, :] + ) if self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT": - fft = np.abs(np.fft.fftshift(np.fft.fft2(new_view))) ** 0.5 + fft = np.abs(np.fft.fftshift(np.fft.fft2(new_view * fft_window))) ** 0.5 levels = (np.min(fft), np.percentile(fft, 99.9)) mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" self.fft_widget_text.setText("Virtual Image FFT") @@ -214,7 +217,7 @@ def update_real_space_view(self, reset=False): self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT (complex)" ): - fft = np.fft.fftshift(np.fft.fft2(new_view)) + fft = np.fft.fftshift(np.fft.fft2(new_view * fft_window)) levels = (np.min(np.abs(fft)), np.percentile(np.abs(fft), 99.9)) mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" self.fft_widget_text.setText("Virtual Image FFT") From 9079cd3c80dd33d7107365ee25c3347babf9c2c3 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 12 Jul 2024 12:51:22 -0400 Subject: [PATCH 26/34] avoid unbound variable --- src/py4D_browser/update_views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index c005122..13e777f 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -198,6 +198,7 @@ def update_real_space_view(self, reset=False): ) # Update FFT view + self.unscaled_fft_image = None fft_window = ( np.hanning(new_view.shape[0])[:, None] * np.hanning(new_view.shape[1])[None, :] ) @@ -213,6 +214,7 @@ def update_real_space_view(self, reset=False): if mode_switch: # Need to autorange after setRect self.fft_widget.autoRange() + self.unscaled_fft_image = fft elif ( self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT (complex)" @@ -239,7 +241,7 @@ def update_real_space_view(self, reset=False): if mode_switch: # Need to autorange after setRect self.fft_widget.autoRange() - self.unscaled_fft_image = fft + self.unscaled_fft_image = fft def update_diffraction_space_view(self, reset=False): From 6e21f54f6affcc22339d4b7c14c5739b296243d9 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 12 Jul 2024 12:58:55 -0400 Subject: [PATCH 27/34] enable calibration using virtual detector for reference --- src/py4D_browser/dialogs.py | 12 ++++++++---- src/py4D_browser/menu_actions.py | 10 +++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/py4D_browser/dialogs.py b/src/py4D_browser/dialogs.py index e492d2e..9de2943 100644 --- a/src/py4D_browser/dialogs.py +++ b/src/py4D_browser/dialogs.py @@ -225,7 +225,7 @@ def diffraction_pix_box_changed(self, new_text): if self.diffraction_selector_size: sel_size = pix_size * self.diffraction_selector_size - self.diff_selection_box.setText(f"{sel_size:.g}") + self.diff_selection_box.setText(f"{sel_size:g}") def diffraction_fov_box_changed(self, new_text): fov = float(new_text) @@ -235,22 +235,25 @@ def diffraction_fov_box_changed(self, new_text): if self.diffraction_selector_size: sel_size = pix_size * self.diffraction_selector_size - self.diff_selection_box.setText(f"{sel_size:.g}") + self.diff_selection_box.setText(f"{sel_size:g}") def diffraction_selection_box_changed(self, new_text): if self.diffraction_selector_size: sel_size = float(new_text) pix_size = sel_size / self.diffraction_selector_size - fov = pix_size * self.Q_Nx + fov = pix_size * self.datacube.Q_Nx self.diff_pix_box.setText(f"{pix_size:g}") self.diff_fov_box.setText(f"{fov:g}") sel_size = pix_size * self.diffraction_selector_size - self.diff_selection_box.setText(f"{sel_size:.g}") + self.diff_selection_box.setText(f"{sel_size:g}") def set_and_close(self): + print("Old calibration") + print(self.datacube.calibration) + realspace_text = self.realspace_pix_box.text() if realspace_text != "": realspace_pix = float(realspace_text) @@ -274,6 +277,7 @@ def set_and_close(self): self.parent.update_scalebars() + print("New calibration") print(self.datacube.calibration) self.close() diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index 168fcda..edcc22a 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -239,7 +239,15 @@ def show_keyboard_map(self): def show_calibration_dialog(self): - dialog = CalibrateDialog(self.datacube, parent=self) + # If the selector has a size, figure that out + if hasattr(self, "virtual_detector_roi") and self.virtual_detector_roi is not None: + selector_size = self.virtual_detector_roi.size()[0] / 2.0 + else: + selector_size = None + + dialog = CalibrateDialog( + self.datacube, parent=self, diffraction_selector_size=selector_size + ) dialog.open() From ebb7b673a032bf7ecf02261abd9f301ea1bd7351 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 12 Jul 2024 14:30:35 -0400 Subject: [PATCH 28/34] make image display in separate function from detector calculation --- src/py4D_browser/main_window.py | 2 ++ src/py4D_browser/update_views.py | 20 ++++++++++++++------ src/py4D_browser/utils.py | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index cae750d..ea895b6 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -51,6 +51,8 @@ class DataViewer(QMainWindow): ) from py4D_browser.update_views import ( + set_virtual_image, + set_diffraction_image, update_diffraction_space_view, update_real_space_view, update_realspace_detector, diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 13e777f..692a9b0 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -10,9 +10,6 @@ def update_real_space_view(self, reset=False): - scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "") - assert scaling_mode in ["Linear", "Log", "Square Root"], scaling_mode - detector_shape = self.detector_shape_group.checkedAction().text().replace("&", "") assert detector_shape in [ "Point", @@ -166,6 +163,13 @@ def update_real_space_view(self, reset=False): else: raise ValueError("Oopsie") + self.set_virtual_image(vimg, reset=reset) + + +def set_virtual_image(self, vimg, reset=False): + scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "") + assert scaling_mode in ["Linear", "Log", "Square Root"], scaling_mode + if scaling_mode == "Linear": new_view = vimg elif scaling_mode == "Log": @@ -245,9 +249,6 @@ def update_real_space_view(self, reset=False): def update_diffraction_space_view(self, reset=False): - scaling_mode = self.diff_scaling_group.checkedAction().text().replace("&", "") - assert scaling_mode in ["Linear", "Log", "Square Root"] - if self.datacube is None: return @@ -289,6 +290,13 @@ def update_diffraction_space_view(self, reset=False): else: raise ValueError("Detector shape not recognized") + self.set_diffraction_image(DP, reset=reset) + + +def set_diffraction_image(self, DP, reset=False): + scaling_mode = self.diff_scaling_group.checkedAction().text().replace("&", "") + assert scaling_mode in ["Linear", "Log", "Square Root"] + self.unscaled_diffraction_image = DP if scaling_mode == "Linear": diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index 78c7c9d..9b11a01 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -112,7 +112,7 @@ def make_detector(shape: tuple, mode: str, geometry) -> np.ndarray: def complex_to_Lab( - im, amin=None, amax=None, gamma=1, L_scale=100, ab_scale=64, uniform_L=None + im, amin=None, amax=None, gamma=1.0, L_scale=100, ab_scale=64, uniform_L=None ): from skimage.color import lab2rgb from matplotlib.colors import Normalize From 783fe5362bff414e807db98edc8f897889957ad0 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 12 Jul 2024 15:00:07 -0400 Subject: [PATCH 29/34] initial commit adding tcBF reconstruction --- src/py4D_browser/main_window.py | 14 ++-- src/py4D_browser/menu_actions.py | 107 ++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index ea895b6..dd61ba2 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -48,6 +48,8 @@ class DataViewer(QMainWindow): show_calibration_dialog, reshape_data, update_scalebars, + reconstruct_tcBF_auto, + reconstruct_tcBF_manual, ) from py4D_browser.update_views import ( @@ -465,13 +467,13 @@ def setup_menus(self): calibrate_action.triggered.connect(self.show_calibration_dialog) self.processing_menu.addAction(calibrate_action) - # tcBF_action_manual = QAction("tcBF (Manual)...", self) - # tcBF_action_manual.triggered.connect(self.reconstruct_tcBF_manual) - # self.processing_menu.addAction(tcBF_action_manual) + tcBF_action_manual = QAction("tcBF (Manual)...", self) + tcBF_action_manual.triggered.connect(self.reconstruct_tcBF_manual) + self.processing_menu.addAction(tcBF_action_manual) - # tcBF_action_auto = QAction("tcBF (Auto)", self) - # tcBF_action_auto.triggered.connect(self.reconstruct_tcBF_auto) - # self.processing_menu.addAction(tcBF_action_auto) + tcBF_action_auto = QAction("tcBF", self) + tcBF_action_auto.triggered.connect(self.reconstruct_tcBF_auto) + self.processing_menu.addAction(tcBF_action_auto) # Help menu self.help_menu = QMenu("&Help", self) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index edcc22a..596444c 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -7,6 +7,7 @@ import matplotlib.pyplot as plt from py4D_browser.help_menu import KeyboardMapMenu from py4D_browser.dialogs import CalibrateDialog, ResizeDialog +from py4D_browser.utils import make_detector from py4DSTEM.io.filereaders import read_arina @@ -238,6 +239,110 @@ def show_keyboard_map(self): keymap.open() +def reconstruct_tcBF_auto(self): + # tcBF requires an area detector for generating the mask + detector_shape = self.detector_shape_group.checkedAction().text().replace("&", "") + if detector_shape not in [ + "Rectangular", + "Circle", + ]: + self.statusBar().showMessage("tcBF requires a selection of the BF disk", 5_000) + return + + if ( + self.datacube.calibration.get_R_pixel_units == "pixels" + or self.datacube.calibration.get_Q_pixel_units == "pixels" + ): + self.statusBar().showMessage("tcBF requires caibrated data", 5_000) + return + + if detector_shape == "Rectangular": + # Get slices corresponding to ROI + slices, _ = self.virtual_detector_roi.getArraySlice( + self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem() + ) + slice_y, slice_x = slices + + mask = np.zeros((self.datacube.Q_Nx, self.datacube.Q_Ny), dtype=np.bool_) + mask[slice_x, slice_y] = True + + elif detector_shape == "Circle": + R = self.virtual_detector_roi.size()[0] / 2.0 + + x0 = self.virtual_detector_roi.pos()[0] + R + y0 = self.virtual_detector_roi.pos()[1] + R + + mask = make_detector( + (self.datacube.Q_Nx, self.datacube.Q_Ny), "circle", ((x0, y0), R) + ) + else: + raise ValueError("idk how we got here...") + + # do tcBF! + self.statusBar().showMessage("Reconstructing... (This may take a while)") + + tcBF = py4DSTEM.process.phase.Parallax( + energy=300e3, + datacube=self.datacube, + ) + tcBF.preprocess( + dp_mask=mask, + plot_average_bf=False, + vectorized_com_calculation=False, + store_initial_arrays=False, + ) + tcBF.reconstruct( + plot_aligned_bf=False, + plot_convergence=False, + ) + + self.set_virtual_image(tcBF.recon_BF, reset=True) + + +# def reconstruct_tcBF_manual(self): +# # tcBF requires an area detector for generating the mask +# detector_shape = self.detector_shape_group.checkedAction().text().replace("&", "") +# if detector_shape not in [ +# "Rectangular", +# "Circle", +# ]: +# self.statusBar().showMessage("tcBF requires a selection of the BF disk", 5_000) +# return + +# if self.datacube.calibration.get_R_pixel_units == "pixels" or self.datacube.calibration.get_Q_pixel_units == "pixels": +# self.statusBar().showMessage("tcBF requires caibrated data", 5_000) +# return + +# if detector_shape == "Rectangular": +# # Get slices corresponding to ROI +# slices, _ = self.virtual_detector_roi.getArraySlice( +# self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem() +# ) +# slice_y, slice_x = slices + +# mask = np.zeros((self.datacube.Q_Nx, self.datacube.Q_Ny), dtype=np.bool_) +# mask[slice_x, slice_y] = True + +# elif detector_shape == "Circle": +# R = self.virtual_detector_roi.size()[0] / 2.0 + +# x0 = self.virtual_detector_roi.pos()[0] + R +# y0 = self.virtual_detector_roi.pos()[1] + R + +# mask = make_detector( +# (self.datacube.Q_Nx, self.datacube.Q_Ny), "circle", ((x0, y0), R) +# ) +# else: +# raise ValueError("idk how we got here...") + +# # do tcBF! +# self.statusBar().showMessage("Reconstructing... (This may take a while)") + +# recon = None + +# self.set_virtual_image(recon, reset=True) + + def show_calibration_dialog(self): # If the selector has a size, figure that out if hasattr(self, "virtual_detector_roi") and self.virtual_detector_roi is not None: @@ -359,7 +464,7 @@ def find_calibrations(dset: h5py.Dataset): try: if "sampling" in dset.parent and "units" in dset.parent: R_size = dset.parent["sampling"][0] - R_units = dset.parent["units"][0].decode() + R_units = dset.parent["units"][0].decode().replace("Å", "A") Q_size = dset.parent["sampling"][3] Q_units = dset.parent["units"][3].decode() From 1a2f8115b689ccb884165f3a5593ad4f98583af0 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 12 Jul 2024 15:14:34 -0400 Subject: [PATCH 30/34] separate rendering from image computation so rescaling is instantaneous --- src/py4D_browser/main_window.py | 14 ++--- src/py4D_browser/menu_actions.py | 87 +++++++++++++++++--------------- src/py4D_browser/update_views.py | 34 ++++++++----- 3 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index dd61ba2..a1ffb90 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -55,6 +55,8 @@ class DataViewer(QMainWindow): from py4D_browser.update_views import ( set_virtual_image, set_diffraction_image, + _render_virtual_image, + _render_diffraction_image, update_diffraction_space_view, update_real_space_view, update_realspace_detector, @@ -220,7 +222,7 @@ def setup_menus(self): diff_scale_linear_action = QAction("Linear", self) diff_scale_linear_action.setCheckable(True) diff_scale_linear_action.triggered.connect( - partial(self.update_diffraction_space_view, True) + partial(self._render_diffraction_image, True) ) diff_scaling_group.addAction(diff_scale_linear_action) self.scaling_menu.addAction(diff_scale_linear_action) @@ -228,7 +230,7 @@ def setup_menus(self): diff_scale_log_action = QAction("Log", self) diff_scale_log_action.setCheckable(True) diff_scale_log_action.triggered.connect( - partial(self.update_diffraction_space_view, True) + partial(self._render_diffraction_image, True) ) diff_scaling_group.addAction(diff_scale_log_action) self.scaling_menu.addAction(diff_scale_log_action) @@ -236,7 +238,7 @@ def setup_menus(self): diff_scale_sqrt_action = QAction("Square Root", self) diff_scale_sqrt_action.setCheckable(True) diff_scale_sqrt_action.triggered.connect( - partial(self.update_diffraction_space_view, True) + partial(self._render_diffraction_image, True) ) diff_scaling_group.addAction(diff_scale_sqrt_action) diff_scale_sqrt_action.setChecked(True) @@ -258,7 +260,7 @@ def setup_menus(self): vimg_scale_linear_action.setCheckable(True) vimg_scale_linear_action.setChecked(True) vimg_scale_linear_action.triggered.connect( - partial(self.update_real_space_view, True) + partial(self._render_virtual_image, True) ) vimg_scaling_group.addAction(vimg_scale_linear_action) self.scaling_menu.addAction(vimg_scale_linear_action) @@ -266,7 +268,7 @@ def setup_menus(self): vimg_scale_log_action = QAction("Log", self) vimg_scale_log_action.setCheckable(True) vimg_scale_log_action.triggered.connect( - partial(self.update_real_space_view, True) + partial(self._render_virtual_image, True) ) vimg_scaling_group.addAction(vimg_scale_log_action) self.scaling_menu.addAction(vimg_scale_log_action) @@ -274,7 +276,7 @@ def setup_menus(self): vimg_scale_sqrt_action = QAction("Square Root", self) vimg_scale_sqrt_action.setCheckable(True) vimg_scale_sqrt_action.triggered.connect( - partial(self.update_real_space_view, True) + partial(self._render_virtual_image, True) ) vimg_scaling_group.addAction(vimg_scale_sqrt_action) self.scaling_menu.addAction(vimg_scale_sqrt_action) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index 596444c..ed15dad 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -299,48 +299,51 @@ def reconstruct_tcBF_auto(self): self.set_virtual_image(tcBF.recon_BF, reset=True) -# def reconstruct_tcBF_manual(self): -# # tcBF requires an area detector for generating the mask -# detector_shape = self.detector_shape_group.checkedAction().text().replace("&", "") -# if detector_shape not in [ -# "Rectangular", -# "Circle", -# ]: -# self.statusBar().showMessage("tcBF requires a selection of the BF disk", 5_000) -# return - -# if self.datacube.calibration.get_R_pixel_units == "pixels" or self.datacube.calibration.get_Q_pixel_units == "pixels": -# self.statusBar().showMessage("tcBF requires caibrated data", 5_000) -# return - -# if detector_shape == "Rectangular": -# # Get slices corresponding to ROI -# slices, _ = self.virtual_detector_roi.getArraySlice( -# self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem() -# ) -# slice_y, slice_x = slices - -# mask = np.zeros((self.datacube.Q_Nx, self.datacube.Q_Ny), dtype=np.bool_) -# mask[slice_x, slice_y] = True - -# elif detector_shape == "Circle": -# R = self.virtual_detector_roi.size()[0] / 2.0 - -# x0 = self.virtual_detector_roi.pos()[0] + R -# y0 = self.virtual_detector_roi.pos()[1] + R - -# mask = make_detector( -# (self.datacube.Q_Nx, self.datacube.Q_Ny), "circle", ((x0, y0), R) -# ) -# else: -# raise ValueError("idk how we got here...") - -# # do tcBF! -# self.statusBar().showMessage("Reconstructing... (This may take a while)") - -# recon = None - -# self.set_virtual_image(recon, reset=True) +def reconstruct_tcBF_manual(self): + # tcBF requires an area detector for generating the mask + detector_shape = self.detector_shape_group.checkedAction().text().replace("&", "") + if detector_shape not in [ + "Rectangular", + "Circle", + ]: + self.statusBar().showMessage("tcBF requires a selection of the BF disk", 5_000) + return + + if ( + self.datacube.calibration.get_R_pixel_units == "pixels" + or self.datacube.calibration.get_Q_pixel_units == "pixels" + ): + self.statusBar().showMessage("tcBF requires caibrated data", 5_000) + return + + if detector_shape == "Rectangular": + # Get slices corresponding to ROI + slices, _ = self.virtual_detector_roi.getArraySlice( + self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem() + ) + slice_y, slice_x = slices + + mask = np.zeros((self.datacube.Q_Nx, self.datacube.Q_Ny), dtype=np.bool_) + mask[slice_x, slice_y] = True + + elif detector_shape == "Circle": + R = self.virtual_detector_roi.size()[0] / 2.0 + + x0 = self.virtual_detector_roi.pos()[0] + R + y0 = self.virtual_detector_roi.pos()[1] + R + + mask = make_detector( + (self.datacube.Q_Nx, self.datacube.Q_Ny), "circle", ((x0, y0), R) + ) + else: + raise ValueError("idk how we got here...") + + # do tcBF! + self.statusBar().showMessage("Reconstructing... (This may take a while)") + + recon = None + + self.set_virtual_image(recon, reset=True) def show_calibration_dialog(self): diff --git a/src/py4D_browser/update_views.py b/src/py4D_browser/update_views.py index 692a9b0..8125baa 100644 --- a/src/py4D_browser/update_views.py +++ b/src/py4D_browser/update_views.py @@ -167,11 +167,18 @@ def update_real_space_view(self, reset=False): def set_virtual_image(self, vimg, reset=False): + self.unscaled_realspace_image = vimg + self._render_virtual_image(reset=reset) + + +def _render_virtual_image(self, reset=False): + vimg = self.unscaled_realspace_image + scaling_mode = self.vimg_scaling_group.checkedAction().text().replace("&", "") assert scaling_mode in ["Linear", "Log", "Square Root"], scaling_mode if scaling_mode == "Linear": - new_view = vimg + new_view = vimg.copy() elif scaling_mode == "Log": new_view = np.log2(np.maximum(vimg, self.LOG_SCALE_MIN_VALUE)) elif scaling_mode == "Square Root": @@ -179,8 +186,6 @@ def set_virtual_image(self, vimg, reset=False): else: raise ValueError("Mode not recognized") - self.unscaled_realspace_image = vimg - self.realspace_statistics_text.setToolTip( f"min\t{vimg.min():.5g}\nmax\t{vimg.max():.5g}\nmean\t{vimg.mean():.5g}\nsum\t{vimg.sum():.5g}\nstd\t{np.std(vimg):.5g}" ) @@ -203,11 +208,9 @@ def set_virtual_image(self, vimg, reset=False): # Update FFT view self.unscaled_fft_image = None - fft_window = ( - np.hanning(new_view.shape[0])[:, None] * np.hanning(new_view.shape[1])[None, :] - ) + fft_window = np.hanning(vimg.shape[0])[:, None] * np.hanning(vimg.shape[1])[None, :] if self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT": - fft = np.abs(np.fft.fftshift(np.fft.fft2(new_view * fft_window))) ** 0.5 + fft = np.abs(np.fft.fftshift(np.fft.fft2(vimg * fft_window))) ** 0.5 levels = (np.min(fft), np.percentile(fft, 99.9)) mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" self.fft_widget_text.setText("Virtual Image FFT") @@ -223,7 +226,7 @@ def set_virtual_image(self, vimg, reset=False): self.fft_source_action_group.checkedAction().text() == "Virtual Image FFT (complex)" ): - fft = np.fft.fftshift(np.fft.fft2(new_view * fft_window)) + fft = np.fft.fftshift(np.fft.fft2(vimg * fft_window)) levels = (np.min(np.abs(fft)), np.percentile(np.abs(fft), 99.9)) mode_switch = self.fft_widget_text.textItem.toPlainText() != "Virtual Image FFT" self.fft_widget_text.setText("Virtual Image FFT") @@ -294,13 +297,18 @@ def update_diffraction_space_view(self, reset=False): def set_diffraction_image(self, DP, reset=False): + self.unscaled_diffraction_image = DP + self._render_diffraction_image(reset=reset) + + +def _render_diffraction_image(self, reset=False): + DP = self.unscaled_diffraction_image + scaling_mode = self.diff_scaling_group.checkedAction().text().replace("&", "") assert scaling_mode in ["Linear", "Log", "Square Root"] - self.unscaled_diffraction_image = DP - if scaling_mode == "Linear": - new_view = DP + new_view = DP.copy() elif scaling_mode == "Log": new_view = np.log2(np.maximum(DP, self.LOG_SCALE_MIN_VALUE)) elif scaling_mode == "Square Root": @@ -491,14 +499,14 @@ def set_diffraction_autoscale_range(self, percentiles, redraw=True): self.diffraction_autoscale_percentiles = percentiles if redraw: - self.update_diffraction_space_view(reset=False) + self._render_diffraction_image(reset=False) def set_real_space_autoscale_range(self, percentiles, redraw=True): self.real_space_autoscale_percentiles = percentiles if redraw: - self.update_real_space_view(reset=False) + self._render_virtual_image(reset=False) def nudge_real_space_selector(self, dx, dy): From 881500cda71d109618fa23f72a4d70e8cff19a3f Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Fri, 12 Jul 2024 18:05:46 -0400 Subject: [PATCH 31/34] starting to add manual tcBF dialog --- src/py4D_browser/dialogs.py | 92 ++++++++++++++++++++++++++++++++ src/py4D_browser/main_window.py | 2 +- src/py4D_browser/menu_actions.py | 48 ++--------------- 3 files changed, 96 insertions(+), 46 deletions(-) diff --git a/src/py4D_browser/dialogs.py b/src/py4D_browser/dialogs.py index 9de2943..59f6e51 100644 --- a/src/py4D_browser/dialogs.py +++ b/src/py4D_browser/dialogs.py @@ -15,7 +15,9 @@ QComboBox, QGroupBox, QGridLayout, + QCheckBox, ) +from py4D_browser.utils import make_detector class ResizeDialog(QDialog): @@ -281,3 +283,93 @@ def set_and_close(self): print(self.datacube.calibration) self.close() + + +class ManualTCBFDialog(QDialog): + def __init__(self, parent): + super().__init__(parent=parent) + + self.parent = parent + + layout = QVBoxLayout(self) + + ####### LAYOUT ######## + + params_box = QGroupBox("Parameters") + layout.addWidget(params_box) + + params_layout = QGridLayout() + params_box.setLayout(params_layout) + + params_layout.addWidget(QLabel("Rotation [deg]"), 0, 0, Qt.AlignRight) + rotation_box = QLineEdit() + rotation_box.setValidator(QDoubleValidator()) + params_layout.addWidget(rotation_box, 0, 1) + + params_layout.addWidget(QLabel("Transpose x/y"), 1, 0, Qt.AlignRight) + transpose_box = QCheckBox() + params_layout.addWidget(transpose_box, 1, 1) + + params_layout.addWidget(QLabel("Max Shift [px]"), 2, 0, Qt.AlignRight) + max_shift_box = QLineEdit() + max_shift_box.setValidator(QDoubleValidator()) + params_layout.addWidget(max_shift_box, 2, 1) + + button_layout = QHBoxLayout() + button_layout.addStretch() + cancel_button = QPushButton("Cancel") + cancel_button.pressed.connect(self.close) + button_layout.addWidget(cancel_button) + done_button = QPushButton("Reconstruct") + done_button.pressed.connect(self.reconstruct) + button_layout.addWidget(done_button) + layout.addLayout(button_layout) + + def reconstruct(self): + # tcBF requires an area detector for generating the mask + detector_shape = ( + self.parent.detector_shape_group.checkedAction().text().replace("&", "") + ) + if detector_shape not in [ + "Rectangular", + "Circle", + ]: + self.parent.statusBar().showMessage( + "tcBF requires a selection of the BF disk" + ) + return + + if detector_shape == "Rectangular": + # Get slices corresponding to ROI + slices, _ = self.parent.virtual_detector_roi.getArraySlice( + self.parent.datacube.data[0, 0, :, :], + self.parent.diffraction_space_widget.getImageItem(), + ) + slice_y, slice_x = slices + + mask = np.zeros( + (self.parent.datacube.Q_Nx, self.parent.datacube.Q_Ny), dtype=np.bool_ + ) + mask[slice_x, slice_y] = True + + elif detector_shape == "Circle": + R = self.parent.virtual_detector_roi.size()[0] / 2.0 + + x0 = self.parent.virtual_detector_roi.pos()[0] + R + y0 = self.parent.virtual_detector_roi.pos()[1] + R + + mask = make_detector( + (self.parent.datacube.Q_Nx, self.parent.datacube.Q_Ny), + "circle", + ((x0, y0), R), + ) + else: + raise ValueError("idk how we got here...") + + if self.max_shift_box.text() == "": + self.parent.statusBar().showMessage("Max Shift must be specified") + return + + rotation = float(self.rotation_box.text() or 0.0) + transpose = self.transpose_box.checkState() + max_shift = float(self.max_shift_box.text()) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index a1ffb90..31cb7ff 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -473,7 +473,7 @@ def setup_menus(self): tcBF_action_manual.triggered.connect(self.reconstruct_tcBF_manual) self.processing_menu.addAction(tcBF_action_manual) - tcBF_action_auto = QAction("tcBF", self) + tcBF_action_auto = QAction("tcBF (Automatic)", self) tcBF_action_auto.triggered.connect(self.reconstruct_tcBF_auto) self.processing_menu.addAction(tcBF_action_auto) diff --git a/src/py4D_browser/menu_actions.py b/src/py4D_browser/menu_actions.py index ed15dad..ce76600 100644 --- a/src/py4D_browser/menu_actions.py +++ b/src/py4D_browser/menu_actions.py @@ -6,7 +6,7 @@ import numpy as np import matplotlib.pyplot as plt from py4D_browser.help_menu import KeyboardMapMenu -from py4D_browser.dialogs import CalibrateDialog, ResizeDialog +from py4D_browser.dialogs import CalibrateDialog, ResizeDialog, ManualTCBFDialog from py4D_browser.utils import make_detector from py4DSTEM.io.filereaders import read_arina @@ -300,50 +300,8 @@ def reconstruct_tcBF_auto(self): def reconstruct_tcBF_manual(self): - # tcBF requires an area detector for generating the mask - detector_shape = self.detector_shape_group.checkedAction().text().replace("&", "") - if detector_shape not in [ - "Rectangular", - "Circle", - ]: - self.statusBar().showMessage("tcBF requires a selection of the BF disk", 5_000) - return - - if ( - self.datacube.calibration.get_R_pixel_units == "pixels" - or self.datacube.calibration.get_Q_pixel_units == "pixels" - ): - self.statusBar().showMessage("tcBF requires caibrated data", 5_000) - return - - if detector_shape == "Rectangular": - # Get slices corresponding to ROI - slices, _ = self.virtual_detector_roi.getArraySlice( - self.datacube.data[0, 0, :, :], self.diffraction_space_widget.getImageItem() - ) - slice_y, slice_x = slices - - mask = np.zeros((self.datacube.Q_Nx, self.datacube.Q_Ny), dtype=np.bool_) - mask[slice_x, slice_y] = True - - elif detector_shape == "Circle": - R = self.virtual_detector_roi.size()[0] / 2.0 - - x0 = self.virtual_detector_roi.pos()[0] + R - y0 = self.virtual_detector_roi.pos()[1] + R - - mask = make_detector( - (self.datacube.Q_Nx, self.datacube.Q_Ny), "circle", ((x0, y0), R) - ) - else: - raise ValueError("idk how we got here...") - - # do tcBF! - self.statusBar().showMessage("Reconstructing... (This may take a while)") - - recon = None - - self.set_virtual_image(recon, reset=True) + dialog = ManualTCBFDialog(parent=self) + dialog.show() def show_calibration_dialog(self): From 1bb79824dd6a648bb074a0b6edcb424018c71792 Mon Sep 17 00:00:00 2001 From: Steve Zeltmann Date: Mon, 15 Jul 2024 15:16:58 -0400 Subject: [PATCH 32/34] remove random cupy import --- src/py4D_browser/dialogs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/py4D_browser/dialogs.py b/src/py4D_browser/dialogs.py index 59f6e51..07c99a4 100644 --- a/src/py4D_browser/dialogs.py +++ b/src/py4D_browser/dialogs.py @@ -1,4 +1,3 @@ -from cupy import real from py4DSTEM import DataCube, data import pyqtgraph as pg import numpy as np From 6d057deb79168b14395ec384bbe7554c8a4ae851 Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Thu, 25 Jul 2024 13:36:28 -0400 Subject: [PATCH 33/34] adding more to tcBF dialog --- src/py4D_browser/dialogs.py | 35 ++++++++++++++++++++++++++++--- src/py4D_browser/empad2_reader.py | 14 +------------ src/py4D_browser/utils.py | 13 ++++++++++++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/py4D_browser/dialogs.py b/src/py4D_browser/dialogs.py index 07c99a4..d7c86c0 100644 --- a/src/py4D_browser/dialogs.py +++ b/src/py4D_browser/dialogs.py @@ -310,9 +310,9 @@ def __init__(self, parent): params_layout.addWidget(transpose_box, 1, 1) params_layout.addWidget(QLabel("Max Shift [px]"), 2, 0, Qt.AlignRight) - max_shift_box = QLineEdit() - max_shift_box.setValidator(QDoubleValidator()) - params_layout.addWidget(max_shift_box, 2, 1) + self.max_shift_box = QLineEdit() + self.max_shift_box.setValidator(QDoubleValidator()) + params_layout.addWidget(self.max_shift_box, 2, 1) button_layout = QHBoxLayout() button_layout.addStretch() @@ -325,6 +325,8 @@ def __init__(self, parent): layout.addLayout(button_layout) def reconstruct(self): + datacube = self.parent.datacube + # tcBF requires an area detector for generating the mask detector_shape = ( self.parent.detector_shape_group.checkedAction().text().replace("&", "") @@ -372,3 +374,30 @@ def reconstruct(self): rotation = float(self.rotation_box.text() or 0.0) transpose = self.transpose_box.checkState() max_shift = float(self.max_shift_box.text()) + + x, y = np.meshgrid( + np.arange(datacube.Q_Nx), np.arange(datacube.Q_Ny), indexing="ij" + ) + + mask_comx = np.sum(mask * x) / np.sum(mask) + mask_comy = np.sum(mask * y) / np.sum(mask) + + pix_coord_x = x - mask_comx + pix_coord_y = y - mask_comy + + q_pix = np.hypot(pix_coord_x, pix_coord_y) + # unrotated shifts in scan pixels + shifts_pix_x = pix_coord_x / np.max(q_pix * mask) * max_shift + shifts_pix_y = pix_coord_y / np.max(q_pix * mask) * max_shift + # shifts_pix = np. + + R = np.array( + [ + [np.cos(rotation), -np.sin(rotation)], + [np.sin(rotation), np.cos(rotation)], + ] + ) + T = np.array([[0.0, 1.0], [1.0, 0.0]]) + + if transpose: + R = T @ R diff --git a/src/py4D_browser/empad2_reader.py b/src/py4D_browser/empad2_reader.py index 81fb961..8d8a9ec 100644 --- a/src/py4D_browser/empad2_reader.py +++ b/src/py4D_browser/empad2_reader.py @@ -1,19 +1,7 @@ import empad2 from PyQt5.QtWidgets import QFileDialog, QMessageBox, QApplication import numpy as np - - -class StatusBarWriter: - def __init__(self, statusBar): - self.statusBar = statusBar - self.app = app = QApplication.instance() - - def write(self, message): - self.statusBar.showMessage(message, 1_000) - self.app.processEvents() - - def flush(self): - pass +from py4D_browser.utils import StatusBarWriter def set_empad2_sensor(self, sensor_name): diff --git a/src/py4D_browser/utils.py b/src/py4D_browser/utils.py index 9b11a01..03e1d21 100644 --- a/src/py4D_browser/utils.py +++ b/src/py4D_browser/utils.py @@ -6,6 +6,19 @@ from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout, QSpinBox +class StatusBarWriter: + def __init__(self, statusBar): + self.statusBar = statusBar + self.app = app = QApplication.instance() + + def write(self, message): + self.statusBar.showMessage(message, 1_000) + self.app.processEvents() + + def flush(self): + pass + + class VLine(QFrame): # a simple vertical divider line def __init__(self): From 862f10d09337c63f8060ea93aba612bd63861e6b Mon Sep 17 00:00:00 2001 From: Steven Zeltmann Date: Thu, 25 Jul 2024 13:38:50 -0400 Subject: [PATCH 34/34] disable manual tcBF menu while under construction --- src/py4D_browser/main_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/py4D_browser/main_window.py b/src/py4D_browser/main_window.py index 31cb7ff..2967774 100644 --- a/src/py4D_browser/main_window.py +++ b/src/py4D_browser/main_window.py @@ -472,6 +472,7 @@ def setup_menus(self): tcBF_action_manual = QAction("tcBF (Manual)...", self) tcBF_action_manual.triggered.connect(self.reconstruct_tcBF_manual) self.processing_menu.addAction(tcBF_action_manual) + tcBF_action_manual.setEnabled(False) tcBF_action_auto = QAction("tcBF (Automatic)", self) tcBF_action_auto.triggered.connect(self.reconstruct_tcBF_auto)