diff --git a/recOrder/acq/acquisition_workers.py b/recOrder/acq/acquisition_workers.py index f80e7c3e..4b19ab5e 100644 --- a/recOrder/acq/acquisition_workers.py +++ b/recOrder/acq/acquisition_workers.py @@ -33,6 +33,20 @@ from recOrder.plugin.main_widget import MainWidget +def _check_scale_mismatch( + recon_scale: np.array, + ngff_scale: tuple[float, float, float, float, float], +) -> None: + if not np.allclose(np.array(ngff_scale[2:]), recon_scale, rtol=1e-2): + show_warning( + f"Requested reconstruction scale = {recon_scale} " + f"and OME-Zarr metadata scale = {ngff_scale[2:]} are not equal. " + "recOrder's reconstruction uses the GUI's " + "Z-step, pixel size, and magnification, " + "while napari's viewer uses the input array's metadata." + ) + + def _generate_reconstruction_config_from_gui( reconstruction_config_path, mode, @@ -117,8 +131,8 @@ class PolarizationAcquisitionSignals(WorkerBaseSignals): Custom Signals class that includes napari native signals """ - phase_image_emitter = Signal(object) - bire_image_emitter = Signal(object) + phase_image_emitter = Signal(tuple) + bire_image_emitter = Signal(tuple) phase_reconstructor_emitter = Signal(object) aborted = Signal() @@ -128,7 +142,7 @@ class BFAcquisitionSignals(WorkerBaseSignals): Custom Signals class that includes napari native signals """ - phase_image_emitter = Signal(object) + phase_image_emitter = Signal(tuple) phase_reconstructor_emitter = Signal(object) aborted = Signal() @@ -258,7 +272,7 @@ def work(self): # Reconstruct snapped images self.n_slices = stack.shape[2] - phase = self._reconstruct() + phase, scale = self._reconstruct() self._check_abort() # Warn the user about axial @@ -267,11 +281,18 @@ def work(self): "Inverting the phase contrast. This affects the visualization and saved reconstruction." ) + # Warn user about mismatched scales + recon_scale = np.array( + (self.calib_window.z_step,) + + 2 * (self.calib_window.ps / self.calib_window.mag,) + ) + _check_scale_mismatch(recon_scale, scale) + logging.info("Finished Acquisition") logging.debug("Finished Acquisition") # Emit the images and let thread know function is finished - self.phase_image_emitter.emit(phase) + self.phase_image_emitter.emit((phase, scale)) def _reconstruct(self): """ @@ -301,8 +322,9 @@ def _reconstruct(self): # Read reconstruction to pass to emitters with open_ome_zarr(reconstruction_path, mode="r") as dataset: phase = dataset["0/0/0/0"][0] + scale = dataset["0/0/0"].scale - return phase + return phase, scale def _cleanup_acq(self): # Get display windows @@ -479,7 +501,7 @@ def work(self): # Reconstruct snapped images self._check_abort() self.n_slices = stack.shape[2] - birefringence, phase = self._reconstruct() + birefringence, phase, scale = self._reconstruct() self._check_abort() # Warn the user about rotations and flips @@ -492,12 +514,19 @@ def work(self): "Applying a flip to orientation channel. This affects the visualization and saved reconstruction." ) + # Warn user about mismatched scales + recon_scale = np.array( + (self.calib_window.z_step,) + + 2 * (self.calib_window.ps / self.calib_window.mag,) + ) + _check_scale_mismatch(recon_scale, scale) + logging.info("Finished Acquisition") logging.debug("Finished Acquisition") # Emit the images and let thread know function is finished - self.bire_image_emitter.emit(birefringence) - self.phase_image_emitter.emit(phase) + self.bire_image_emitter.emit((birefringence, scale)) + self.phase_image_emitter.emit((phase, scale)) def _check_exposure(self) -> None: """ @@ -579,8 +608,9 @@ def _reconstruct(self): phase = czyx_data[4] except: phase = None + scale = dataset["0/0/0"].scale - return birefringence, phase + return birefringence, phase, scale def _cleanup_acq(self): # Get display windows diff --git a/recOrder/calib/calibration_workers.py b/recOrder/calib/calibration_workers.py index 6a780d69..1a5efd20 100644 --- a/recOrder/calib/calibration_workers.py +++ b/recOrder/calib/calibration_workers.py @@ -53,8 +53,8 @@ class BackgroundSignals(WorkerBaseSignals): Custom Signals class that includes napari native signals """ - bg_image_emitter = Signal(object) - bire_image_emitter = Signal(object) + bg_image_emitter = Signal(tuple) + bire_image_emitter = Signal(tuple) bg_path_update_emitter = Signal(Path) aborted = Signal() @@ -358,6 +358,7 @@ def work(self): with open_ome_zarr(reconstruction_path, mode="r") as dataset: self.retardance = dataset["0/0/0/0"][0, 0, 0] self.birefringence = dataset["0/0/0/0"][0, :, 0] + scale = dataset["0/0/0"].scale # Save metadata file and emit imgs meta_file = bg_path / "polarization_calibration.txt" @@ -381,8 +382,10 @@ def work(self): self._check_abort() # Emit background images + background birefringence - self.bg_image_emitter.emit(imgs) - self.bire_image_emitter.emit((self.retardance, self.birefringence[1])) + self.bg_image_emitter.emit((imgs, scale)) + self.bire_image_emitter.emit( + ((self.retardance, self.birefringence[1]), scale) + ) # Emit bg path self.bg_path_update_emitter.emit(bg_path) diff --git a/recOrder/io/utils.py b/recOrder/io/utils.py index 6f9e9f9a..8ae2d8cc 100644 --- a/recOrder/io/utils.py +++ b/recOrder/io/utils.py @@ -1,7 +1,7 @@ import os import textwrap from pathlib import Path -from typing import Literal +from typing import Literal, Union import numpy as np import psutil @@ -130,7 +130,10 @@ def generic_hsv_overlay( def ret_ori_overlay( - retardance, orientation, ret_max=10, cmap: Literal["JCh", "HSV"] = "JCh" + retardance, + orientation, + ret_max: Union[float, Literal["auto"]] = 10, + cmap: Literal["JCh", "HSV"] = "JCh", ): """ This function will create an overlay of retardance and orientation with two different colormap options. @@ -154,6 +157,9 @@ def ret_ori_overlay( f"{retardance.shape} vs. {orientation.shape}" ) + if ret_max == "auto": + ret_max = np.percentile(np.ravel(retardance), 99.99) + # Prepare input and output arrays ret_ = np.clip(retardance, 0, ret_max) # clip and copy # Convert 180 degree range into 360 to match periodicity of hue. diff --git a/recOrder/plugin/gui.py b/recOrder/plugin/gui.py index 007402ed..84cf37a7 100644 --- a/recOrder/plugin/gui.py +++ b/recOrder/plugin/gui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'gui.ui' # -# Created by: PyQt5 UI code generator 5.15.9 +# Created by: PyQt5 UI code generator 5.15.4 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -25,6 +25,11 @@ def setupUi(self, Form): Form.setLayoutDirection(QtCore.Qt.LeftToRight) self.gridLayout_7 = QtWidgets.QGridLayout(Form) self.gridLayout_7.setObjectName("gridLayout_7") + self.label_logo = QtWidgets.QLabel(Form) + self.label_logo.setText("") + self.label_logo.setAlignment(QtCore.Qt.AlignCenter) + self.label_logo.setObjectName("label_logo") + self.gridLayout_7.addWidget(self.label_logo, 0, 0, 1, 1) self.recon_status = QtWidgets.QGroupBox(Form) font = QtGui.QFont() font.setBold(True) @@ -936,6 +941,16 @@ def setupUi(self, Form): self.scrollAreaWidgetContents_5.setObjectName("scrollAreaWidgetContents_5") self.gridLayout_4 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_5) self.gridLayout_4.setObjectName("gridLayout_4") + self.label_orientation_image = QtWidgets.QLabel(self.scrollAreaWidgetContents_5) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_orientation_image.sizePolicy().hasHeightForWidth()) + self.label_orientation_image.setSizePolicy(sizePolicy) + self.label_orientation_image.setText("") + self.label_orientation_image.setAlignment(QtCore.Qt.AlignCenter) + self.label_orientation_image.setObjectName("label_orientation_image") + self.gridLayout_4.addWidget(self.label_orientation_image, 6, 0, 1, 1) self.DisplayOptions = QtWidgets.QGroupBox(self.scrollAreaWidgetContents_5) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -951,22 +966,39 @@ def setupUi(self, Form): self.gridLayout_17.setContentsMargins(-1, 12, -1, -1) self.gridLayout_17.setVerticalSpacing(6) self.gridLayout_17.setObjectName("gridLayout_17") - self.le_sat_max = QtWidgets.QLineEdit(self.DisplayOptions) + self.cb_hue = QtWidgets.QComboBox(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.le_sat_max.setFont(font) - self.le_sat_max.setFrame(False) - self.le_sat_max.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.le_sat_max.setObjectName("le_sat_max") - self.gridLayout_17.addWidget(self.le_sat_max, 8, 3, 1, 1) - self.qbutton_create_overlay = QtWidgets.QPushButton(self.DisplayOptions) + self.cb_hue.setFont(font) + self.cb_hue.setFocusPolicy(QtCore.Qt.StrongFocus) + self.cb_hue.setObjectName("cb_hue") + self.gridLayout_17.addWidget(self.cb_hue, 8, 1, 1, 1) + self.slider_saturation = QtWidgets.QSlider(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.qbutton_create_overlay.setFont(font) - self.qbutton_create_overlay.setObjectName("qbutton_create_overlay") - self.gridLayout_17.addWidget(self.qbutton_create_overlay, 15, 0, 1, 4) + self.slider_saturation.setFont(font) + self.slider_saturation.setOrientation(QtCore.Qt.Horizontal) + self.slider_saturation.setObjectName("slider_saturation") + self.gridLayout_17.addWidget(self.slider_saturation, 10, 2, 1, 2) + self.line = QtWidgets.QFrame(self.DisplayOptions) + font = QtGui.QFont() + font.setBold(False) + font.setItalic(False) + self.line.setFont(font) + self.line.setLineWidth(3) + self.line.setFrameShape(QtWidgets.QFrame.HLine) + self.line.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line.setObjectName("line") + self.gridLayout_17.addWidget(self.line, 6, 0, 1, 4) + self.label_saturation = QtWidgets.QLabel(self.DisplayOptions) + font = QtGui.QFont() + font.setBold(False) + font.setItalic(False) + self.label_saturation.setFont(font) + self.label_saturation.setObjectName("label_saturation") + self.gridLayout_17.addWidget(self.label_saturation, 10, 0, 1, 1) self.le_overlay_slice = QtWidgets.QLineEdit(self.DisplayOptions) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -978,15 +1010,16 @@ def setupUi(self, Form): font.setItalic(False) self.le_overlay_slice.setFont(font) self.le_overlay_slice.setObjectName("le_overlay_slice") - self.gridLayout_17.addWidget(self.le_overlay_slice, 14, 0, 1, 1) - self.slider_saturation = QtWidgets.QSlider(self.DisplayOptions) + self.gridLayout_17.addWidget(self.le_overlay_slice, 15, 0, 1, 1) + self.le_sat_min = QtWidgets.QLineEdit(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.slider_saturation.setFont(font) - self.slider_saturation.setOrientation(QtCore.Qt.Horizontal) - self.slider_saturation.setObjectName("slider_saturation") - self.gridLayout_17.addWidget(self.slider_saturation, 9, 2, 1, 2) + self.le_sat_min.setFont(font) + self.le_sat_min.setInputMask("") + self.le_sat_min.setFrame(False) + self.le_sat_min.setObjectName("le_sat_min") + self.gridLayout_17.addWidget(self.le_sat_min, 9, 2, 1, 1) self.cb_colormap = QtWidgets.QComboBox(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) @@ -1004,22 +1037,15 @@ def setupUi(self, Form): self.cb_value.setFont(font) self.cb_value.setFocusPolicy(QtCore.Qt.StrongFocus) self.cb_value.setObjectName("cb_value") - self.gridLayout_17.addWidget(self.cb_value, 11, 1, 1, 1) - self.cb_hue = QtWidgets.QComboBox(self.DisplayOptions) - font = QtGui.QFont() - font.setBold(False) - font.setItalic(False) - self.cb_hue.setFont(font) - self.cb_hue.setFocusPolicy(QtCore.Qt.StrongFocus) - self.cb_hue.setObjectName("cb_hue") - self.gridLayout_17.addWidget(self.cb_hue, 7, 1, 1, 1) - self.label_value = QtWidgets.QLabel(self.DisplayOptions) + self.gridLayout_17.addWidget(self.cb_value, 12, 1, 1, 1) + self.le_val_min = QtWidgets.QLineEdit(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.label_value.setFont(font) - self.label_value.setObjectName("label_value") - self.gridLayout_17.addWidget(self.label_value, 11, 0, 1, 1) + self.le_val_min.setFont(font) + self.le_val_min.setFrame(False) + self.le_val_min.setObjectName("le_val_min") + self.gridLayout_17.addWidget(self.le_val_min, 11, 2, 1, 1) self.cb_saturation = QtWidgets.QComboBox(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) @@ -1027,14 +1053,16 @@ def setupUi(self, Form): self.cb_saturation.setFont(font) self.cb_saturation.setFocusPolicy(QtCore.Qt.StrongFocus) self.cb_saturation.setObjectName("cb_saturation") - self.gridLayout_17.addWidget(self.cb_saturation, 9, 1, 1, 1) - self.chb_display_volume = QtWidgets.QCheckBox(self.DisplayOptions) + self.gridLayout_17.addWidget(self.cb_saturation, 10, 1, 1, 1) + self.le_sat_max = QtWidgets.QLineEdit(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.chb_display_volume.setFont(font) - self.chb_display_volume.setObjectName("chb_display_volume") - self.gridLayout_17.addWidget(self.chb_display_volume, 14, 1, 1, 1) + self.le_sat_max.setFont(font) + self.le_sat_max.setFrame(False) + self.le_sat_max.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.le_sat_max.setObjectName("le_sat_max") + self.gridLayout_17.addWidget(self.le_sat_max, 9, 3, 1, 1) self.slider_value = QtWidgets.QSlider(self.DisplayOptions) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -1047,17 +1075,21 @@ def setupUi(self, Form): self.slider_value.setFont(font) self.slider_value.setOrientation(QtCore.Qt.Horizontal) self.slider_value.setObjectName("slider_value") - self.gridLayout_17.addWidget(self.slider_value, 11, 2, 1, 2) - self.line = QtWidgets.QFrame(self.DisplayOptions) + self.gridLayout_17.addWidget(self.slider_value, 12, 2, 1, 2) + self.label_value = QtWidgets.QLabel(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.line.setFont(font) - self.line.setLineWidth(3) - self.line.setFrameShape(QtWidgets.QFrame.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line.setObjectName("line") - self.gridLayout_17.addWidget(self.line, 6, 0, 1, 4) + self.label_value.setFont(font) + self.label_value.setObjectName("label_value") + self.gridLayout_17.addWidget(self.label_value, 12, 0, 1, 1) + self.chb_display_volume = QtWidgets.QCheckBox(self.DisplayOptions) + font = QtGui.QFont() + font.setBold(False) + font.setItalic(False) + self.chb_display_volume.setFont(font) + self.chb_display_volume.setObjectName("chb_display_volume") + self.gridLayout_17.addWidget(self.chb_display_volume, 15, 1, 1, 1) self.label_colormap = QtWidgets.QLabel(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) @@ -1065,47 +1097,44 @@ def setupUi(self, Form): self.label_colormap.setFont(font) self.label_colormap.setObjectName("label_colormap") self.gridLayout_17.addWidget(self.label_colormap, 3, 0, 1, 1) - self.label_hue = QtWidgets.QLabel(self.DisplayOptions) - font = QtGui.QFont() - font.setBold(False) - font.setItalic(False) - self.label_hue.setFont(font) - self.label_hue.setObjectName("label_hue") - self.gridLayout_17.addWidget(self.label_hue, 7, 0, 1, 1) - self.label_saturation = QtWidgets.QLabel(self.DisplayOptions) - font = QtGui.QFont() - font.setBold(False) - font.setItalic(False) - self.label_saturation.setFont(font) - self.label_saturation.setObjectName("label_saturation") - self.gridLayout_17.addWidget(self.label_saturation, 9, 0, 1, 1) - self.le_sat_min = QtWidgets.QLineEdit(self.DisplayOptions) + self.le_val_max = QtWidgets.QLineEdit(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.le_sat_min.setFont(font) - self.le_sat_min.setInputMask("") - self.le_sat_min.setFrame(False) - self.le_sat_min.setObjectName("le_sat_min") - self.gridLayout_17.addWidget(self.le_sat_min, 8, 2, 1, 1) - self.le_val_min = QtWidgets.QLineEdit(self.DisplayOptions) + self.le_val_max.setFont(font) + self.le_val_max.setFrame(False) + self.le_val_max.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.le_val_max.setObjectName("le_val_max") + self.gridLayout_17.addWidget(self.le_val_max, 11, 3, 1, 1) + self.label_hue = QtWidgets.QLabel(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.le_val_min.setFont(font) - self.le_val_min.setFrame(False) - self.le_val_min.setObjectName("le_val_min") - self.gridLayout_17.addWidget(self.le_val_min, 10, 2, 1, 1) - self.le_val_max = QtWidgets.QLineEdit(self.DisplayOptions) + self.label_hue.setFont(font) + self.label_hue.setObjectName("label_hue") + self.gridLayout_17.addWidget(self.label_hue, 8, 0, 1, 1) + self.qbutton_create_overlay = QtWidgets.QPushButton(self.DisplayOptions) font = QtGui.QFont() font.setBold(False) font.setItalic(False) - self.le_val_max.setFont(font) - self.le_val_max.setFrame(False) - self.le_val_max.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.le_val_max.setObjectName("le_val_max") - self.gridLayout_17.addWidget(self.le_val_max, 10, 3, 1, 1) - self.gridLayout_4.addWidget(self.DisplayOptions, 2, 0, 1, 1, QtCore.Qt.AlignTop) + self.qbutton_create_overlay.setFont(font) + self.qbutton_create_overlay.setObjectName("qbutton_create_overlay") + self.gridLayout_17.addWidget(self.qbutton_create_overlay, 16, 0, 1, 4) + self.gridLayout_4.addWidget(self.DisplayOptions, 8, 0, 1, 1) + self.label = QtWidgets.QLabel(self.scrollAreaWidgetContents_5) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) + self.label.setSizePolicy(sizePolicy) + self.label.setObjectName("label") + self.gridLayout_4.addWidget(self.label, 1, 0, 1, 1) + self.retMaxSlider = QtWidgets.QSlider(self.scrollAreaWidgetContents_5) + self.retMaxSlider.setMaximum(50) + self.retMaxSlider.setSliderPosition(25) + self.retMaxSlider.setOrientation(QtCore.Qt.Horizontal) + self.retMaxSlider.setObjectName("retMaxSlider") + self.gridLayout_4.addWidget(self.retMaxSlider, 2, 0, 1, 1) self.label_orientation_legend = QtWidgets.QLabel(self.scrollAreaWidgetContents_5) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -1114,29 +1143,16 @@ def setupUi(self, Form): self.label_orientation_legend.setSizePolicy(sizePolicy) self.label_orientation_legend.setAlignment(QtCore.Qt.AlignCenter) self.label_orientation_legend.setObjectName("label_orientation_legend") - self.gridLayout_4.addWidget(self.label_orientation_legend, 0, 0, 1, 1) - self.label_orientation_image = QtWidgets.QLabel(self.scrollAreaWidgetContents_5) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_orientation_image.sizePolicy().hasHeightForWidth()) - self.label_orientation_image.setSizePolicy(sizePolicy) - self.label_orientation_image.setText("") - self.label_orientation_image.setAlignment(QtCore.Qt.AlignCenter) - self.label_orientation_image.setObjectName("label_orientation_image") - self.gridLayout_4.addWidget(self.label_orientation_image, 1, 0, 1, 1) + self.gridLayout_4.addWidget(self.label_orientation_legend, 3, 0, 1, 1) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_4.addItem(spacerItem, 7, 0, 1, 1) self.scrollArea_5.setWidget(self.scrollAreaWidgetContents_5) self.gridLayout_18.addWidget(self.scrollArea_5, 0, 0, 2, 2) self.tabWidget.addTab(self.Display, "") self.gridLayout_7.addWidget(self.tabWidget, 2, 0, 1, 1) - self.label_logo = QtWidgets.QLabel(Form) - self.label_logo.setText("") - self.label_logo.setAlignment(QtCore.Qt.AlignCenter) - self.label_logo.setObjectName("label_logo") - self.gridLayout_7.addWidget(self.label_logo, 0, 0, 1, 1) self.retranslateUi(Form) - self.tabWidget.setCurrentIndex(0) + self.tabWidget.setCurrentIndex(2) self.tabWidget_2.setCurrentIndex(2) self.cb_loglevel.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(Form) @@ -1313,19 +1329,20 @@ def retranslateUi(self, Form): self.qbutton_stop_acq.setText(_translate("Form", "STOP")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.Acquisition), _translate("Form", "Acquisition / Reconstruction")) self.DisplayOptions.setTitle(_translate("Form", "Display Options")) - self.le_sat_max.setText(_translate("Form", "80")) - self.qbutton_create_overlay.setText(_translate("Form", "Create Overlay")) + self.label_saturation.setText(_translate("Form", "Saturation")) self.le_overlay_slice.setPlaceholderText(_translate("Form", "Slice")) + self.le_sat_min.setText(_translate("Form", "20")) self.cb_colormap.setItemText(0, _translate("Form", "HSV")) self.cb_colormap.setItemText(1, _translate("Form", "JCh (Perceptually Uniform)")) + self.le_val_min.setText(_translate("Form", "20")) + self.le_sat_max.setText(_translate("Form", "80")) self.label_value.setText(_translate("Form", "Value")) self.chb_display_volume.setText(_translate("Form", "Use Full Volume")) self.label_colormap.setText(_translate("Form", "BirefringenceOverlay Colormap")) - self.label_hue.setText(_translate("Form", "Hue")) - self.label_saturation.setText(_translate("Form", "Saturation")) - self.le_sat_min.setText(_translate("Form", "20")) - self.le_val_min.setText(_translate("Form", "20")) self.le_val_max.setText(_translate("Form", "80")) + self.label_hue.setText(_translate("Form", "Hue")) + self.qbutton_create_overlay.setText(_translate("Form", "Create Overlay")) + self.label.setText(_translate("Form", "Overlay Retardance Maximum ")) self.label_orientation_legend.setText(_translate("Form", "Retardance Orientation Overlay Legend")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.Display), _translate("Form", "Display")) from pyqtgraph import PlotWidget diff --git a/recOrder/plugin/gui.ui b/recOrder/plugin/gui.ui index 73c94475..dde9cae4 100644 --- a/recOrder/plugin/gui.ui +++ b/recOrder/plugin/gui.ui @@ -32,6 +32,16 @@ Qt::LeftToRight + + + + + + + Qt::AlignCenter + + + @@ -124,7 +134,7 @@ QTabWidget::North - 0 + 2 Qt::ElideMiddle @@ -1752,7 +1762,23 @@ - + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + + @@ -1776,27 +1802,50 @@ 6 - - + + false false - - 80 + + Qt::StrongFocus - - false + + + + + + + false + false + - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + Qt::Horizontal - - + + + + + false + false + + + + 3 + + + Qt::Horizontal + + + + + false @@ -1804,11 +1853,11 @@ - Create Overlay + Saturation - + @@ -1827,16 +1876,22 @@ - - + + false false - - Qt::Horizontal + + + + + 20 + + + false @@ -1863,7 +1918,7 @@ - + @@ -1876,21 +1931,8 @@ - - - - - false - false - - - - Qt::StrongFocus - - - - - + + false @@ -1898,11 +1940,14 @@ - Value + 20 + + + false - + @@ -1915,8 +1960,8 @@ - - + + false @@ -1924,11 +1969,17 @@ - Use Full Volume + 80 + + + false + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + @@ -1947,24 +1998,8 @@ - - - - - false - false - - - - 3 - - - Qt::Horizontal - - - - - + + false @@ -1972,12 +2007,12 @@ - BirefringenceOverlay Colormap + Value - - + + false @@ -1985,12 +2020,12 @@ - Hue + Use Full Volume - - + + false @@ -1998,31 +2033,31 @@ - Saturation + BirefringenceOverlay Colormap - - + + false false - - - - 20 + 80 false + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + - - + + false @@ -2030,15 +2065,12 @@ - 20 - - - false + Hue - - + + false @@ -2046,21 +2078,15 @@ - 80 - - - false - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Create Overlay - - + + 0 @@ -2068,15 +2094,25 @@ - Retardance Orientation Overlay Legend + Overlay Retardance Maximum - - Qt::AlignCenter + + + + + + 50 + + + 25 + + + Qt::Horizontal - - + + 0 @@ -2084,13 +2120,26 @@ - + Retardance Orientation Overlay Legend Qt::AlignCenter + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -2099,16 +2148,6 @@ - - - - - - - Qt::AlignCenter - - - diff --git a/recOrder/plugin/main_widget.py b/recOrder/plugin/main_widget.py index afac8f15..f8bff315 100644 --- a/recOrder/plugin/main_widget.py +++ b/recOrder/plugin/main_widget.py @@ -236,6 +236,11 @@ def __init__(self, napari_viewer: Viewer): self.viewer.layers.events.moved.connect(self.handle_layers_updated) self.viewer.layers.events.inserted.connect(self.handle_layers_updated) + # Birefringence overlay controls + self.ui.retMaxSlider.sliderMoved[int].connect( + self.handle_ret_max_slider_move + ) + # Commenting for 0.3.0. Consider debugging or deleting for 1.0.0. # self.ui.cb_colormap.currentIndexChanged[int].connect( # self.enter_colormap @@ -341,6 +346,7 @@ def __init__(self, napari_viewer: Viewer): self.reconstruction_data_path = None self.reconstruction_data = None self.calib_assessment_level = None + self.ret_max = 25 recorder_dir = dirname(dirname(dirname(os.path.abspath(__file__)))) self.worker = None @@ -1133,6 +1139,7 @@ def _add_or_update_image_layer( name: str, cmap: str = "gray", move_to_top: bool = True, + scale: tuple = 5 * (1,), ): """Add image layer of the given name if it does not exist, update existing layer otherwise. @@ -1147,6 +1154,8 @@ def _add_or_update_image_layer( move_to_top : bool, optional whether to move the updated layer to the top of layers list, by default True """ + scale = scale[-image.ndim :] # match shapes + if name in self.viewer.layers: self.viewer.layers[name].data = image if move_to_top: @@ -1155,19 +1164,33 @@ def _add_or_update_image_layer( self.viewer.layers.move(src_index, dest_index=-1) else: if cmap == "rgb": - self.viewer.add_image(image, name=name, rgb=True) + self.viewer.add_image( + image, + name=name, + rgb=True, + scale=scale, + ) else: - self.viewer.add_image(image, name=name, colormap=cmap) + self.viewer.add_image( + image, + name=name, + colormap=cmap, + scale=scale, + ) - @Slot(object) + @Slot(tuple) def handle_bg_image_update(self, value): - self._add_or_update_image_layer(value, "Background Images") + data, scale = value + self._add_or_update_image_layer(data, "Background Images", scale=scale) - @Slot(object) + @Slot(tuple) def handle_bg_bire_image_update(self, value): - self._add_or_update_image_layer(value[0], "Background Retardance") + data, scale = value + self._add_or_update_image_layer( + data[0], "Background Retardance", scale=scale + ) self._add_or_update_image_layer( - value[1], "Background Orientation", cmap="hsv" + data[1], "Background Orientation", cmap="hsv", scale=scale ) def handle_layers_updated(self, event: Event): @@ -1194,7 +1217,10 @@ def handle_layers_updated(self, event: Event): f"'{retardance_name}', '{orientation_name}'" ) self._draw_bire_overlay( - retardance_name, orientation_name, overlay_name + retardance_name, + orientation_name, + overlay_name, + scale=layers[-1].scale, ) # always display layers that start with "Orientation" in hsv @@ -1205,7 +1231,11 @@ def handle_layers_updated(self, event: Event): self.viewer.layers[orientation_name].colormap = "hsv" def _draw_bire_overlay( - self, retardance_name: str, orientation_name: str, overlay_name: str + self, + retardance_name: str, + orientation_name: str, + overlay_name: str, + scale: tuple, ): def _layer_data(name: str): data = self.viewer.layers[name].data @@ -1215,40 +1245,58 @@ def _layer_data(name: str): # this object will remain a dask `Array` after calling `compute()` if any([("get_" in k) for k in data.dask.keys()]): data: da.Array = data.compute() + else: + chunks = (data.ndim - 2) * (1,) + data.shape[ + -2: + ] # needs to match + data = da.from_array(data, chunks=chunks) return data - def _draw(overlay): - self._add_or_update_image_layer(overlay, overlay_name, cmap="rgb") - - retardance = _layer_data(retardance_name) - orientation = _layer_data(orientation_name) - worker = create_worker( + self.overlay_scale = scale + self.overlay_name = overlay_name + self.overlay_retardance = _layer_data(retardance_name) + self.overlay_orientation = _layer_data(orientation_name) + self.update_overlay_dask_array() + + def update_overlay_dask_array(self): + self.rgb_chunks = ( + (self.overlay_retardance.ndim - 2) * (1,) + + self.overlay_retardance.shape[-2:] + + (3,) + ) + overlay = da.map_blocks( ret_ori_overlay, - retardance=retardance, - orientation=orientation, - ret_max=np.percentile(np.ravel(retardance), 99.99), + self.overlay_retardance, + self.overlay_orientation, + chunks=self.rgb_chunks, + new_axis=-1, + ret_max=self.ret_max, cmap=self.colormap, ) - worker.start() - logging.info(f"Updating the birefringence overlay layer.") - if retardance.size >= 2 * 2048 * 2048: - show_info("Generating large overlay. This might take a moment...") - worker.returned.connect(_draw) - @Slot(object) - def handle_bire_image_update(self, value: NDArray): + self._add_or_update_image_layer( + overlay, self.overlay_name, cmap="rgb", scale=self.overlay_scale + ) + + @Slot(tuple) + def handle_bire_image_update(self, value): + data, scale = value + # generate overlay in a separate thread for i, channel in enumerate(("Retardance", "Orientation")): name = channel cmap = "gray" if channel != "Orientation" else "hsv" - self._add_or_update_image_layer(value[i], name, cmap=cmap) + self._add_or_update_image_layer( + data[i], name, cmap=cmap, scale=scale + ) - @Slot(object) + @Slot(tuple) def handle_phase_image_update(self, value): + phase, scale = value name = "Phase2D" if self.acq_mode == "2D" else "Phase3D" # Add new layer if none exists, otherwise update layer data - self._add_or_update_image_layer(value, name) + self._add_or_update_image_layer(phase, name, scale=scale) if "Phase" not in [ self.ui.cb_saturation.itemText(i) @@ -2203,6 +2251,11 @@ def save_config(self): self.config_reader.save_yaml(dir_=dir_, name=name) + @Slot(int) + def handle_ret_max_slider_move(self, value): + self.ret_max = value + self.update_overlay_dask_array() + # Commenting for 0.3.0. Consider debugging or deleting for 1.0.0. # @Slot(int) # def update_sat_scale(self): diff --git a/recOrder/tests/acq_tests/test_acq.py b/recOrder/tests/acq_tests/test_acq.py new file mode 100644 index 00000000..0152e230 --- /dev/null +++ b/recOrder/tests/acq_tests/test_acq.py @@ -0,0 +1,16 @@ +from unittest.mock import patch + +import numpy as np +from recOrder.acq.acquisition_workers import _check_scale_mismatch + + +def test_check_scale_mismatch(): + warn_fn_path = "recOrder.acq.acquisition_workers.show_warning" + identity = np.array((1.0, 1.0, 1.0)) + with patch(warn_fn_path) as mock: + _check_scale_mismatch(identity, (1, 1, 1, 1, 1)) + mock.assert_not_called() + _check_scale_mismatch(identity, (1, 1, 1, 1, 1.001)) + mock.assert_not_called() + _check_scale_mismatch(identity, (1, 1, 1, 1, 1.1)) + mock.assert_called_once()