From 1a8d41517f46dcdc227dece08f1ad9c5c9985a45 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Mon, 15 Jan 2024 14:03:37 +0000 Subject: [PATCH 01/32] Fixed contents margins --- brainglobe_registration/napari.yaml | 2 +- .../registration_widget.py | 38 ++++++++----------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/brainglobe_registration/napari.yaml b/brainglobe_registration/napari.yaml index a5c3aaa..818695e 100644 --- a/brainglobe_registration/napari.yaml +++ b/brainglobe_registration/napari.yaml @@ -8,7 +8,7 @@ contributions: sample_data: - key: example display_name: Sample Brain Slice - uri: src/brainglobe_registration/resources/sample_hipp.tif + uri: brainglobe_registration/resources/sample_hipp.tif widgets: - command: brainglobe-registration.make_registration_widget display_name: BrainGlobe Registration diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index ddecb4a..f98e0da 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -13,14 +13,11 @@ import numpy as np from bg_atlasapi import BrainGlobeAtlas from bg_atlasapi.list_atlases import get_downloaded_atlases +from brainglobe_utils.qtpy.collapsible_widget import CollapsibleWidgetContainer from napari.viewer import Viewer -from qtpy.QtCore import Qt from qtpy.QtWidgets import ( - QGroupBox, QPushButton, QTabWidget, - QVBoxLayout, - QWidget, ) from skimage.segmentation import find_boundaries @@ -44,9 +41,10 @@ ) -class RegistrationWidget(QWidget): +class RegistrationWidget(CollapsibleWidgetContainer): def __init__(self, napari_viewer: Viewer): super().__init__() + self.setContentsMargins(10, 10, 10, 10) self._viewer = napari_viewer self._atlas: BrainGlobeAtlas = None @@ -84,16 +82,6 @@ def __init__(self, napari_viewer: Viewer): else: self._moving_image = None - self.setLayout(QVBoxLayout()) - self.layout().addWidget(header_widget()) - - self.main_tabs = QTabWidget(parent=self) - self.main_tabs.setTabPosition(QTabWidget.West) - - self.settings_tab = QGroupBox() - self.settings_tab.setLayout(QVBoxLayout()) - self.parameters_tab = QTabWidget() - self.get_atlas_widget = SelectImagesView( available_atlases=self._available_atlases, sample_image_names=self._sample_images, @@ -133,13 +121,17 @@ def __init__(self, napari_viewer: Viewer): self.run_button.clicked.connect(self._on_run_button_click) self.run_button.setEnabled(False) - self.settings_tab.layout().addWidget(self.get_atlas_widget) - self.settings_tab.layout().addWidget(self.adjust_moving_image_widget) - self.settings_tab.layout().addWidget(self.transform_select_view) - self.settings_tab.layout().addWidget(self.run_button) - self.settings_tab.layout().setAlignment(Qt.AlignTop) + self.add_widget(header_widget(), collapsible=False) + self.add_widget(self.get_atlas_widget, widget_title="Select Images") + self.add_widget( + self.adjust_moving_image_widget, widget_title="Adjust Sample Image" + ) + self.add_widget( + self.transform_select_view, widget_title="Select Transformations" + ) self.parameter_setting_tabs_lists = [] + self.parameters_tab = QTabWidget() for transform_type in self.transform_params: new_tab = RegistrationParameterListView( @@ -150,10 +142,10 @@ def __init__(self, napari_viewer: Viewer): self.parameters_tab.addTab(new_tab, transform_type) self.parameter_setting_tabs_lists.append(new_tab) - self.main_tabs.addTab(self.settings_tab, "Settings") - self.main_tabs.addTab(self.parameters_tab, "Parameters") + self.add_widget(self.parameters_tab, widget_title="Advanced Settings") + self.add_widget(self.run_button, collapsible=False) - self.layout().addWidget(self.main_tabs) + self.layout().itemAt(1).widget().collapse(animate=False) def _on_atlas_dropdown_index_changed(self, index): # Hacky way of having an empty first dropdown From e71bbfb9c3c4860788621a41cc67d4009f463ed4 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Mon, 15 Jan 2024 15:32:04 +0000 Subject: [PATCH 02/32] Scaling of sample image added to adjust moving image dropdown --- .../registration_widget.py | 18 +++++++- .../widgets/adjust_moving_image_view.py | 43 ++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index f98e0da..7434568 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -20,6 +20,7 @@ QTabWidget, ) from skimage.segmentation import find_boundaries +from skimage.transform import rescale from brainglobe_registration.elastix.register import run_registration from brainglobe_registration.utils.brainglobe_logo import header_widget @@ -101,10 +102,12 @@ def __init__(self, napari_viewer: Viewer): self.adjust_moving_image_widget.adjust_image_signal.connect( self._on_adjust_moving_image ) - self.adjust_moving_image_widget.reset_image_signal.connect( self._on_adjust_moving_image_reset_button_click ) + self.adjust_moving_image_widget.scale_image_signal.connect( + self._on_scale_moving_image + ) self.transform_select_view = TransformSelectView() self.transform_select_view.transform_type_added_signal.connect( @@ -299,3 +302,16 @@ def _on_default_file_selection_change( def _on_sample_popup_about_to_show(self): self._sample_images = get_image_layer_names(self._viewer) self.get_atlas_widget.update_sample_image_names(self._sample_images) + + def _on_scale_moving_image(self, x: float, y: float): + if self._moving_image and self._atlas: + x_factor = x / self._atlas.resolution[0] + y_factor = y / self._atlas.resolution[1] + + self._moving_image.data = rescale( + self._moving_image.data, + (y_factor, x_factor), + mode="constant", + preserve_range=True, + anti_aliasing=True, + ) diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index 4740e25..7e829fe 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -14,8 +14,8 @@ class AdjustMovingImageView(QWidget): A QWidget subclass that provides controls for adjusting the moving image. This widget provides controls for adjusting the x and y offsets and - rotation of the moving image. It emits signals when the image is adjusted - or reset. + rotation of the moving image. It emits signals when the image is adjusted, + scaled, or reset. Attributes ---------- @@ -33,9 +33,12 @@ class AdjustMovingImageView(QWidget): _on_reset_image_button_click(): Resets the x and y offsets and rotation to 0 and emits the reset_image_signal. + _on_scale_image_button_click(): + Emits the scale_image_signal with the entered pixel sizes. """ adjust_image_signal = Signal(int, int, float) + scale_image_signal = Signal(float, float) reset_image_signal = Signal() def __init__(self, parent=None): @@ -54,15 +57,23 @@ def __init__(self, parent=None): offset_range = 2000 rotation_range = 360 - self.adjust_moving_image_x = QSpinBox() + self.adjust_moving_image_voxel_size_x = QDoubleSpinBox(parent=self) + self.adjust_moving_image_voxel_size_y = QDoubleSpinBox(parent=self) + self.scale_moving_image_button = QPushButton() + self.scale_moving_image_button.setText("Scale Image") + self.scale_moving_image_button.clicked.connect( + self._on_scale_image_button_click + ) + + self.adjust_moving_image_x = QSpinBox(parent=self) self.adjust_moving_image_x.setRange(-offset_range, offset_range) self.adjust_moving_image_x.valueChanged.connect(self._on_adjust_image) - self.adjust_moving_image_y = QSpinBox() + self.adjust_moving_image_y = QSpinBox(parent=self) self.adjust_moving_image_y.setRange(-offset_range, offset_range) self.adjust_moving_image_y.valueChanged.connect(self._on_adjust_image) - self.adjust_moving_image_rotate = QDoubleSpinBox() + self.adjust_moving_image_rotate = QDoubleSpinBox(parent=self) self.adjust_moving_image_rotate.setRange( -rotation_range, rotation_range ) @@ -71,13 +82,22 @@ def __init__(self, parent=None): self._on_adjust_image ) - self.adjust_moving_image_reset_button = QPushButton(parent=self) + self.adjust_moving_image_reset_button = QPushButton() self.adjust_moving_image_reset_button.setText("Reset Image") self.adjust_moving_image_reset_button.clicked.connect( self._on_reset_image_button_click ) - self.layout().addRow(QLabel("Adjust the moving image: ")) + self.layout().addRow(QLabel("Adjust the moving image scale: ")) + self.layout().addRow( + "Sample image X pixel size:", self.adjust_moving_image_voxel_size_x + ) + self.layout().addRow( + "Sample image Y pixel size:", self.adjust_moving_image_voxel_size_y + ) + self.layout().addRow(self.scale_moving_image_button) + + self.layout().addRow(QLabel("Adjust the moving image position: ")) self.layout().addRow("X offset:", self.adjust_moving_image_x) self.layout().addRow("Y offset:", self.adjust_moving_image_y) self.layout().addRow( @@ -106,3 +126,12 @@ def _on_reset_image_button_click(self): self.adjust_moving_image_rotate.setValue(0) self.reset_image_signal.emit() + + def _on_scale_image_button_click(self): + """ + Emit the scale_image_signal with the entered pixel sizes. + """ + self.scale_image_signal.emit( + self.adjust_moving_image_voxel_size_x.value(), + self.adjust_moving_image_voxel_size_y.value(), + ) From 4bdcdf8ef004f3de1e479a7f03192cb80f44639d Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Mon, 15 Jan 2024 15:33:25 +0000 Subject: [PATCH 03/32] Added brainglobe-utils as dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index aeb282b..27bc776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ dependencies = [ "napari>=0.4.18", "bg-atlasapi", + "brainglobe-utils", "numpy", "qtpy", "itk-elastix", From 35b24bd1655b9c99454274df79071fdbe5b4180f Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 16 Jan 2024 10:53:32 +0000 Subject: [PATCH 04/32] Added error message when no images selected --- brainglobe_registration/registration_widget.py | 6 ++++++ brainglobe_registration/widgets/adjust_moving_image_view.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 7434568..e50e74b 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -14,6 +14,7 @@ from bg_atlasapi import BrainGlobeAtlas from bg_atlasapi.list_atlases import get_downloaded_atlases from brainglobe_utils.qtpy.collapsible_widget import CollapsibleWidgetContainer +from napari.utils.notifications import show_error from napari.viewer import Viewer from qtpy.QtWidgets import ( QPushButton, @@ -315,3 +316,8 @@ def _on_scale_moving_image(self, x: float, y: float): preserve_range=True, anti_aliasing=True, ) + else: + show_error( + "No sample image or atlas selected. " + "Please select a sample image and atlas before scaling", + ) diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index 7e829fe..76abf3a 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -58,7 +58,9 @@ def __init__(self, parent=None): rotation_range = 360 self.adjust_moving_image_voxel_size_x = QDoubleSpinBox(parent=self) + self.adjust_moving_image_voxel_size_x.setDecimals(5) self.adjust_moving_image_voxel_size_y = QDoubleSpinBox(parent=self) + self.adjust_moving_image_voxel_size_y.setDecimals(5) self.scale_moving_image_button = QPushButton() self.scale_moving_image_button.setText("Scale Image") self.scale_moving_image_button.clicked.connect( From 7d34970d68e3e84a56e1a31a930deae573407b0b Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 16 Jan 2024 13:46:19 +0000 Subject: [PATCH 05/32] Atlas rotation working using scipy --- .../registration_widget.py | 26 +++++++++++++ .../widgets/adjust_moving_image_view.py | 38 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index e50e74b..f09e62d 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -20,6 +20,7 @@ QPushButton, QTabWidget, ) +from scipy.ndimage import rotate from skimage.segmentation import find_boundaries from skimage.transform import rescale @@ -109,6 +110,9 @@ def __init__(self, napari_viewer: Viewer): self.adjust_moving_image_widget.scale_image_signal.connect( self._on_scale_moving_image ) + self.adjust_moving_image_widget.atlas_rotation_signal.connect( + self._on_adjust_atlas_rotation + ) self.transform_select_view = TransformSelectView() self.transform_select_view.transform_type_added_signal.connect( @@ -321,3 +325,25 @@ def _on_scale_moving_image(self, x: float, y: float): "No sample image or atlas selected. " "Please select a sample image and atlas before scaling", ) + + def _on_adjust_atlas_rotation(self, pitch: float, yaw: float): + if self._atlas: + curr_atlas_layer_index = find_layer_index( + self._viewer, self._atlas.atlas_name + ) + + curr_atlas_data = rotate( + self._viewer.layers[curr_atlas_layer_index].data, + yaw, + axes=(0, 2), + order=0, + ) + rotated_atlas = rotate( + curr_atlas_data, pitch, axes=(0, 1), order=0 + ) + self._viewer.layers[curr_atlas_layer_index].data = rotated_atlas + + else: + show_error( + "No atlas selected. Please select an atlas before rotating" + ) diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index 76abf3a..692ccda 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -35,10 +35,13 @@ class AdjustMovingImageView(QWidget): reset_image_signal. _on_scale_image_button_click(): Emits the scale_image_signal with the entered pixel sizes. + _on_adjust_atlas_rotation(): + Emits the atlas_rotation_signal with the entered pitch and yaw. """ adjust_image_signal = Signal(int, int, float) scale_image_signal = Signal(float, float) + atlas_rotation_signal = Signal(float, float) reset_image_signal = Signal() def __init__(self, parent=None): @@ -56,17 +59,34 @@ def __init__(self, parent=None): offset_range = 2000 rotation_range = 360 + voxel_decimal_places = 3 self.adjust_moving_image_voxel_size_x = QDoubleSpinBox(parent=self) - self.adjust_moving_image_voxel_size_x.setDecimals(5) + self.adjust_moving_image_voxel_size_x.setDecimals(voxel_decimal_places) + self.adjust_moving_image_voxel_size_y = QDoubleSpinBox(parent=self) - self.adjust_moving_image_voxel_size_y.setDecimals(5) + self.adjust_moving_image_voxel_size_y.setDecimals(voxel_decimal_places) + self.scale_moving_image_button = QPushButton() self.scale_moving_image_button.setText("Scale Image") self.scale_moving_image_button.clicked.connect( self._on_scale_image_button_click ) + self.adjust_atlas_pitch = QDoubleSpinBox(parent=self) + self.adjust_atlas_pitch.setSingleStep(0.1) + self.adjust_atlas_pitch.setRange(-rotation_range, rotation_range) + + self.adjust_atlas_yaw = QDoubleSpinBox(parent=self) + self.adjust_atlas_yaw.setSingleStep(0.1) + self.adjust_atlas_yaw.setRange(-rotation_range, rotation_range) + + self.adjust_rotation_button = QPushButton() + self.adjust_rotation_button.setText("Rotate Atlas") + self.adjust_rotation_button.clicked.connect( + self._on_adjust_atlas_rotation + ) + self.adjust_moving_image_x = QSpinBox(parent=self) self.adjust_moving_image_x.setRange(-offset_range, offset_range) self.adjust_moving_image_x.valueChanged.connect(self._on_adjust_image) @@ -99,6 +119,11 @@ def __init__(self, parent=None): ) self.layout().addRow(self.scale_moving_image_button) + self.layout().addRow(QLabel("Adjust the atlas pitch and yaw: ")) + self.layout().addRow("Pitch:", self.adjust_atlas_pitch) + self.layout().addRow("Yaw:", self.adjust_atlas_yaw) + self.layout().addRow(self.adjust_rotation_button) + self.layout().addRow(QLabel("Adjust the moving image position: ")) self.layout().addRow("X offset:", self.adjust_moving_image_x) self.layout().addRow("Y offset:", self.adjust_moving_image_y) @@ -137,3 +162,12 @@ def _on_scale_image_button_click(self): self.adjust_moving_image_voxel_size_x.value(), self.adjust_moving_image_voxel_size_y.value(), ) + + def _on_adjust_atlas_rotation(self): + """ + Emit the atlas_rotation_signal with the entered pitch and yaw. + """ + self.atlas_rotation_signal.emit( + self.adjust_atlas_pitch.value(), + self.adjust_atlas_yaw.value(), + ) From 2b50fce7f8a49ae5bb582fc79cdfc175b8a4712e Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 16 Jan 2024 16:36:02 +0000 Subject: [PATCH 06/32] Pitch, yaw, roll implemented using one affine transform --- .../registration_widget.py | 48 +++++++++++++++---- brainglobe_registration/utils/utils.py | 47 ++++++++++++++++++ .../widgets/adjust_moving_image_view.py | 10 +++- 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index f09e62d..ce598be 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -16,11 +16,12 @@ from brainglobe_utils.qtpy.collapsible_widget import CollapsibleWidgetContainer from napari.utils.notifications import show_error from napari.viewer import Viewer +from pytransform3d.rotations import active_matrix_from_angle from qtpy.QtWidgets import ( QPushButton, QTabWidget, ) -from scipy.ndimage import rotate +from scipy.ndimage import affine_transform from skimage.segmentation import find_boundaries from skimage.transform import rescale @@ -28,6 +29,7 @@ from brainglobe_registration.utils.brainglobe_logo import header_widget from brainglobe_registration.utils.utils import ( adjust_napari_image_layer, + calculate_rotated_bounding_box, find_layer_index, get_image_layer_names, open_parameter_file, @@ -51,6 +53,8 @@ def __init__(self, napari_viewer: Viewer): self._viewer = napari_viewer self._atlas: BrainGlobeAtlas = None + self.atlas_pitch = 0 + self.atlas_yaw = 0 self._moving_image = None self.transform_params: dict[str, dict] = { @@ -326,22 +330,46 @@ def _on_scale_moving_image(self, x: float, y: float): "Please select a sample image and atlas before scaling", ) - def _on_adjust_atlas_rotation(self, pitch: float, yaw: float): + def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): if self._atlas: curr_atlas_layer_index = find_layer_index( self._viewer, self._atlas.atlas_name ) - curr_atlas_data = rotate( - self._viewer.layers[curr_atlas_layer_index].data, - yaw, - axes=(0, 2), - order=0, + roll_matrix = active_matrix_from_angle(0, np.deg2rad(roll)) + pitch_matrix = active_matrix_from_angle(2, np.deg2rad(pitch)) + yaw_matrix = active_matrix_from_angle(1, np.deg2rad(yaw)) + + rot_matrix = roll_matrix @ pitch_matrix @ yaw_matrix + + full_matrix = np.eye(4) + full_matrix[:-1, :-1] = rot_matrix + + origin = np.asarray(self._atlas.reference.shape) // 2 + translate_matrix = np.eye(4) + translate_matrix[:-1, -1] = origin + + bounding_box = calculate_rotated_bounding_box( + self._atlas.reference.shape, full_matrix + ) + new_translation = np.asarray(bounding_box) // 2 + post_rotate_translation = np.eye(4) + post_rotate_translation[:3, -1] = new_translation + + transform_matrix = ( + translate_matrix + @ full_matrix + @ np.linalg.inv(post_rotate_translation) ) - rotated_atlas = rotate( - curr_atlas_data, pitch, axes=(0, 1), order=0 + + self._viewer.layers[ + curr_atlas_layer_index + ].data = affine_transform( + self._atlas.reference, + transform_matrix, + order=1, + output_shape=bounding_box, ) - self._viewer.layers[curr_atlas_layer_index].data = rotated_atlas else: show_error( diff --git a/brainglobe_registration/utils/utils.py b/brainglobe_registration/utils/utils.py index 48d0933..a66fe90 100644 --- a/brainglobe_registration/utils/utils.py +++ b/brainglobe_registration/utils/utils.py @@ -96,3 +96,50 @@ def get_image_layer_names(viewer: napari.Viewer) -> List[str]: Returns a list of the names of the napari image layers in the viewer. """ return [layer.name for layer in viewer.layers] + + +def calculate_rotated_bounding_box( + image_shape: tuple[int, int, int], rotation_matrix: np.array +) -> tuple[int, int, int]: + """ + Calculates the bounding box of the rotated image. + + This function calculates the bounding box of the rotated image given the + image shape and rotation matrix. The bounding box is calculated by + transforming the corners of the image and finding the minimum and maximum + values of the transformed corners. + + Parameters + ---------- + image_shape : tuple + The shape of the image. + rotation_matrix : np.array + The rotation matrix. + + Returns + ------- + tuple + The bounding box of the rotated image. + """ + corners = np.array( + [ + [0, 0, 0, 1], + [image_shape[0], 0, 0, 1], + [0, image_shape[1], 0, 1], + [0, 0, image_shape[2], 1], + [image_shape[0], image_shape[1], 0, 1], + [image_shape[0], 0, image_shape[2], 1], + [0, image_shape[1], image_shape[2], 1], + [image_shape[0], image_shape[1], image_shape[2], 1], + ] + ) + + transformed_corners = rotation_matrix @ corners.T + min_corner = np.min(transformed_corners, axis=1) + max_corner = np.max(transformed_corners, axis=1) + + return ( + int(np.round(max_corner[0] - min_corner[0])), + int(np.round(max_corner[1] - min_corner[1])), + int(np.round(max_corner[2] - min_corner[2])), + ) diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index 692ccda..d7e1e66 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -36,12 +36,12 @@ class AdjustMovingImageView(QWidget): _on_scale_image_button_click(): Emits the scale_image_signal with the entered pixel sizes. _on_adjust_atlas_rotation(): - Emits the atlas_rotation_signal with the entered pitch and yaw. + Emits the atlas_rotation_signal with the entered pitch, yaw, and roll. """ adjust_image_signal = Signal(int, int, float) scale_image_signal = Signal(float, float) - atlas_rotation_signal = Signal(float, float) + atlas_rotation_signal = Signal(float, float, float) reset_image_signal = Signal() def __init__(self, parent=None): @@ -81,6 +81,10 @@ def __init__(self, parent=None): self.adjust_atlas_yaw.setSingleStep(0.1) self.adjust_atlas_yaw.setRange(-rotation_range, rotation_range) + self.adjust_atlas_roll = QDoubleSpinBox(parent=self) + self.adjust_atlas_roll.setSingleStep(0.1) + self.adjust_atlas_roll.setRange(-rotation_range, rotation_range) + self.adjust_rotation_button = QPushButton() self.adjust_rotation_button.setText("Rotate Atlas") self.adjust_rotation_button.clicked.connect( @@ -122,6 +126,7 @@ def __init__(self, parent=None): self.layout().addRow(QLabel("Adjust the atlas pitch and yaw: ")) self.layout().addRow("Pitch:", self.adjust_atlas_pitch) self.layout().addRow("Yaw:", self.adjust_atlas_yaw) + self.layout().addRow("Roll:", self.adjust_atlas_roll) self.layout().addRow(self.adjust_rotation_button) self.layout().addRow(QLabel("Adjust the moving image position: ")) @@ -170,4 +175,5 @@ def _on_adjust_atlas_rotation(self): self.atlas_rotation_signal.emit( self.adjust_atlas_pitch.value(), self.adjust_atlas_yaw.value(), + self.adjust_atlas_roll.value(), ) From 8f42d3301911af91362cccb2fa4fe77bd6d162cf Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 16 Jan 2024 17:53:38 +0000 Subject: [PATCH 07/32] Dask loading for both the reference atlas and the rotation --- .../registration_widget.py | 26 ++++++++++++++++--- pyproject.toml | 6 +++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index ce598be..9b9851b 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -10,10 +10,13 @@ from pathlib import Path +import dask.array as da import numpy as np from bg_atlasapi import BrainGlobeAtlas from bg_atlasapi.list_atlases import get_downloaded_atlases from brainglobe_utils.qtpy.collapsible_widget import CollapsibleWidgetContainer +from dask_image.ndinterp import affine_transform as affine_transform +from napari.qt.threading import thread_worker from napari.utils.notifications import show_error from napari.viewer import Viewer from pytransform3d.rotations import active_matrix_from_angle @@ -21,7 +24,6 @@ QPushButton, QTabWidget, ) -from scipy.ndimage import affine_transform from skimage.segmentation import find_boundaries from skimage.transform import rescale @@ -186,11 +188,18 @@ def _on_atlas_dropdown_index_changed(self, index): else: self.run_button.setEnabled(True) - self._viewer.add_image( + dask_atlas_reference = da.from_array( atlas.reference, + chunks=(1, atlas.reference.shape[1], atlas.reference.shape[2]), + ) + + self._viewer.add_image( + dask_atlas_reference, name=atlas_name, colormap="gray", blending="translucent", + contrast_limits=[0, 512], + multiscale=False, ) self._atlas = BrainGlobeAtlas(atlas_name=atlas_name) @@ -367,11 +376,22 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): ].data = affine_transform( self._atlas.reference, transform_matrix, - order=1, + order=3, output_shape=bounding_box, + output_chunks=(1, bounding_box[1], bounding_box[2]), ) + worker = self.compute_dask_array( + self._viewer.layers[curr_atlas_layer_index].data, + curr_atlas_layer_index, + ) + worker.start() else: show_error( "No atlas selected. Please select an atlas before rotating" ) + + @thread_worker + def compute_dask_array(self, dask_array: da, viewer_index: int): + dask_array.compute() + self._viewer.layers[viewer_index].data = dask_array diff --git a/pyproject.toml b/pyproject.toml index 27bc776..0eef1eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,10 +26,12 @@ dependencies = [ "napari>=0.4.18", "bg-atlasapi", "brainglobe-utils", + "dask", + "dask-image", + "itk-elastix", "numpy", + "pytransform3d", "qtpy", - "itk-elastix", - "pytransform3d" ] [project.urls] From 94b38b60100f58f78a21a3ca38cc1d13b9762690 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Wed, 17 Jan 2024 12:59:36 +0000 Subject: [PATCH 08/32] Added a 'Reset Atlas' button, blocked while dask computes the rotation to avoid race conditions --- .../registration_widget.py | 30 +++++++++++++-- .../widgets/adjust_moving_image_view.py | 38 ++++++++++++++----- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 9b9851b..f2386c7 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -55,8 +55,6 @@ def __init__(self, napari_viewer: Viewer): self._viewer = napari_viewer self._atlas: BrainGlobeAtlas = None - self.atlas_pitch = 0 - self.atlas_yaw = 0 self._moving_image = None self.transform_params: dict[str, dict] = { @@ -119,6 +117,9 @@ def __init__(self, napari_viewer: Viewer): self.adjust_moving_image_widget.atlas_rotation_signal.connect( self._on_adjust_atlas_rotation ) + self.adjust_moving_image_widget.reset_atlas_signal.connect( + self._on_atlas_reset + ) self.transform_select_view = TransformSelectView() self.transform_select_view.transform_type_added_signal.connect( @@ -138,7 +139,7 @@ def __init__(self, napari_viewer: Viewer): self.add_widget(header_widget(), collapsible=False) self.add_widget(self.get_atlas_widget, widget_title="Select Images") self.add_widget( - self.adjust_moving_image_widget, widget_title="Adjust Sample Image" + self.adjust_moving_image_widget, widget_title="Prepare Images" ) self.add_widget( self.transform_select_view, widget_title="Select Transformations" @@ -198,7 +199,7 @@ def _on_atlas_dropdown_index_changed(self, index): name=atlas_name, colormap="gray", blending="translucent", - contrast_limits=[0, 512], + contrast_limits=[0, 350], multiscale=False, ) @@ -386,6 +387,9 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): curr_atlas_layer_index, ) worker.start() + + self._viewer.grid.enabled = False + self._viewer.grid.enabled = True else: show_error( "No atlas selected. Please select an atlas before rotating" @@ -393,5 +397,23 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): @thread_worker def compute_dask_array(self, dask_array: da, viewer_index: int): + self.adjust_moving_image_widget.reset_atlas_button.setEnabled(False) dask_array.compute() self._viewer.layers[viewer_index].data = dask_array + self.adjust_moving_image_widget.reset_atlas_button.setEnabled(True) + + def _on_atlas_reset(self): + if self._atlas: + curr_atlas_layer_index = find_layer_index( + self._viewer, self._atlas.atlas_name + ) + + self._viewer.layers[ + curr_atlas_layer_index + ].data = self._atlas.reference + self._viewer.grid.enabled = False + self._viewer.grid.enabled = True + else: + show_error( + "No atlas selected. Please select an atlas before resetting" + ) diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index d7e1e66..f6eb7a8 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -1,4 +1,4 @@ -from qtpy.QtCore import Signal +from qtpy.QtCore import Qt, Signal from qtpy.QtWidgets import ( QDoubleSpinBox, QFormLayout, @@ -37,11 +37,14 @@ class AdjustMovingImageView(QWidget): Emits the scale_image_signal with the entered pixel sizes. _on_adjust_atlas_rotation(): Emits the atlas_rotation_signal with the entered pitch, yaw, and roll. + _on_atlas_reset(): + Resets the pitch, yaw, and roll to 0 and emits the atlas_reset_signal. """ adjust_image_signal = Signal(int, int, float) scale_image_signal = Signal(float, float) atlas_rotation_signal = Signal(float, float, float) + reset_atlas_signal = Signal() reset_image_signal = Signal() def __init__(self, parent=None): @@ -56,16 +59,17 @@ def __init__(self, parent=None): super().__init__(parent=parent) self.setLayout(QFormLayout()) + self.layout().setLabelAlignment(Qt.AlignLeft) offset_range = 2000 rotation_range = 360 - voxel_decimal_places = 3 + pixel_demical_places = 3 - self.adjust_moving_image_voxel_size_x = QDoubleSpinBox(parent=self) - self.adjust_moving_image_voxel_size_x.setDecimals(voxel_decimal_places) + self.adjust_moving_image_pixel_size_x = QDoubleSpinBox(parent=self) + self.adjust_moving_image_pixel_size_x.setDecimals(pixel_demical_places) - self.adjust_moving_image_voxel_size_y = QDoubleSpinBox(parent=self) - self.adjust_moving_image_voxel_size_y.setDecimals(voxel_decimal_places) + self.adjust_moving_image_pixel_size_y = QDoubleSpinBox(parent=self) + self.adjust_moving_image_pixel_size_y.setDecimals(pixel_demical_places) self.scale_moving_image_button = QPushButton() self.scale_moving_image_button.setText("Scale Image") @@ -90,6 +94,9 @@ def __init__(self, parent=None): self.adjust_rotation_button.clicked.connect( self._on_adjust_atlas_rotation ) + self.reset_atlas_button = QPushButton() + self.reset_atlas_button.setText("Reset Atlas") + self.reset_atlas_button.clicked.connect(self._on_atlas_reset) self.adjust_moving_image_x = QSpinBox(parent=self) self.adjust_moving_image_x.setRange(-offset_range, offset_range) @@ -116,10 +123,10 @@ def __init__(self, parent=None): self.layout().addRow(QLabel("Adjust the moving image scale: ")) self.layout().addRow( - "Sample image X pixel size:", self.adjust_moving_image_voxel_size_x + "Sample image X pixel size:", self.adjust_moving_image_pixel_size_x ) self.layout().addRow( - "Sample image Y pixel size:", self.adjust_moving_image_voxel_size_y + "Sample image Y pixel size:", self.adjust_moving_image_pixel_size_y ) self.layout().addRow(self.scale_moving_image_button) @@ -128,6 +135,7 @@ def __init__(self, parent=None): self.layout().addRow("Yaw:", self.adjust_atlas_yaw) self.layout().addRow("Roll:", self.adjust_atlas_roll) self.layout().addRow(self.adjust_rotation_button) + self.layout().addRow(self.reset_atlas_button) self.layout().addRow(QLabel("Adjust the moving image position: ")) self.layout().addRow("X offset:", self.adjust_moving_image_x) @@ -164,8 +172,8 @@ def _on_scale_image_button_click(self): Emit the scale_image_signal with the entered pixel sizes. """ self.scale_image_signal.emit( - self.adjust_moving_image_voxel_size_x.value(), - self.adjust_moving_image_voxel_size_y.value(), + self.adjust_moving_image_pixel_size_x.value(), + self.adjust_moving_image_pixel_size_y.value(), ) def _on_adjust_atlas_rotation(self): @@ -177,3 +185,13 @@ def _on_adjust_atlas_rotation(self): self.adjust_atlas_yaw.value(), self.adjust_atlas_roll.value(), ) + + def _on_atlas_reset(self): + """ + Reset the pitch, yaw, and roll to 0 and emit the atlas_reset_signal. + """ + self.adjust_atlas_yaw.setValue(0) + self.adjust_atlas_pitch.setValue(0) + self.adjust_atlas_roll.setValue(0) + + self.reset_atlas_signal.emit() From fe109bf29beba896171e172b7457a646b0de9725 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Thu, 18 Jan 2024 11:23:08 +0000 Subject: [PATCH 09/32] Rotate atlas button also greyed out while daks processes the rotation --- brainglobe_registration/registration_widget.py | 7 +++++-- .../widgets/adjust_moving_image_view.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index f2386c7..1a683e4 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -398,9 +398,12 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): @thread_worker def compute_dask_array(self, dask_array: da, viewer_index: int): self.adjust_moving_image_widget.reset_atlas_button.setEnabled(False) - dask_array.compute() - self._viewer.layers[viewer_index].data = dask_array + self.adjust_moving_image_widget.adjust_rotation_button.setEnabled( + False + ) + self._viewer.layers[viewer_index].data = np.array(dask_array) self.adjust_moving_image_widget.reset_atlas_button.setEnabled(True) + self.adjust_moving_image_widget.adjust_rotation_button.setEnabled(True) def _on_atlas_reset(self): if self._atlas: diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index f6eb7a8..b94db29 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -121,7 +121,7 @@ def __init__(self, parent=None): self._on_reset_image_button_click ) - self.layout().addRow(QLabel("Adjust the moving image scale: ")) + self.layout().addRow(QLabel("Adjust the sample image scale: ")) self.layout().addRow( "Sample image X pixel size:", self.adjust_moving_image_pixel_size_x ) From 7d17da109af1b527bf13d44ffa5a2625313f8975 Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov <61896994+IgorTatarnikov@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:59:33 +0000 Subject: [PATCH 10/32] Use the update upload_pypi action (#29) See https://github.com/neuroinformatics-unit/movement/pull/108 --- .github/workflows/test_and_deploy.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index c00e554..5501042 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -66,11 +66,6 @@ jobs: needs: [build_sdist_wheels] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v3 + - uses: neuroinformatics-unit/actions/upload_pypi@v2 with: - name: artifact - path: dist - - uses: pypa/gh-action-pypi-publish@v1.5.0 - with: - user: __token__ - password: ${{ secrets.TWINE_API_KEY }} + secret-pypi-key: ${{ secrets.TWINE_API_KEY }} From d2bec0de59e23f9605bd97dca797c8dc7a2a694d Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 6 Feb 2024 09:47:40 +0000 Subject: [PATCH 11/32] Added elastix Logs directory to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 73d56d3..08e97e3 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ venv/ # written by setuptools_scm **/_version.py + +# Elastix related files +/Logs/ From 9bd9ca44dd056428be4371a0a27aaaef29999493 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 6 Feb 2024 10:17:10 +0000 Subject: [PATCH 12/32] Fixed tests --- .../widgets/adjust_moving_image_view.py | 16 ++++++++-------- tests/test_adjust_moving_image_view.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index 53e6e76..1e50855 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -64,12 +64,12 @@ def __init__(self, parent=None): offset_range = 2000 rotation_range = 360 - self.adjust_moving_image_voxel_size_x = QDoubleSpinBox(parent=self) - self.adjust_moving_image_voxel_size_x.setDecimals(2) - self.adjust_moving_image_voxel_size_x.setRange(0.01, 100.00) - self.adjust_moving_image_voxel_size_y = QDoubleSpinBox(parent=self) - self.adjust_moving_image_voxel_size_y.setDecimals(2) - self.adjust_moving_image_voxel_size_y.setRange(0.01, 100.00) + self.adjust_moving_image_pixel_size_x = QDoubleSpinBox(parent=self) + self.adjust_moving_image_pixel_size_x.setDecimals(2) + self.adjust_moving_image_pixel_size_x.setRange(0.01, 100.00) + self.adjust_moving_image_pixel_size_y = QDoubleSpinBox(parent=self) + self.adjust_moving_image_pixel_size_y.setDecimals(2) + self.adjust_moving_image_pixel_size_y.setRange(0.01, 100.00) self.scale_moving_image_button = QPushButton() self.scale_moving_image_button.setText("Scale Image") self.scale_moving_image_button.clicked.connect( @@ -123,11 +123,11 @@ def __init__(self, parent=None): self.layout().addRow(QLabel("Adjust the moving image scale:")) self.layout().addRow( "Sample image X pixel size (\u03BCm / pixel):", - self.adjust_moving_image_voxel_size_x, + self.adjust_moving_image_pixel_size_x, ) self.layout().addRow( "Sample image Y pixel size (\u03BCm / pixel):", - self.adjust_moving_image_voxel_size_y, + self.adjust_moving_image_pixel_size_y, ) self.layout().addRow(self.scale_moving_image_button) diff --git a/tests/test_adjust_moving_image_view.py b/tests/test_adjust_moving_image_view.py index fb2dc41..0821c7b 100644 --- a/tests/test_adjust_moving_image_view.py +++ b/tests/test_adjust_moving_image_view.py @@ -17,7 +17,7 @@ def adjust_moving_image_view() -> AdjustMovingImageView: def test_init(qtbot, adjust_moving_image_view): qtbot.addWidget(adjust_moving_image_view) - assert adjust_moving_image_view.layout().rowCount() == 9 + assert adjust_moving_image_view.layout().rowCount() == 15 @pytest.mark.parametrize( @@ -114,10 +114,10 @@ def test_scale_image_button_click( with qtbot.waitSignal( adjust_moving_image_view.scale_image_signal, timeout=1000 ) as blocker: - adjust_moving_image_view.adjust_moving_image_voxel_size_x.setValue( + adjust_moving_image_view.adjust_moving_image_pixel_size_x.setValue( x_scale ) - adjust_moving_image_view.adjust_moving_image_voxel_size_y.setValue( + adjust_moving_image_view.adjust_moving_image_pixel_size_y.setValue( y_scale ) adjust_moving_image_view.scale_moving_image_button.click() From 8d4635b9de75e09675320467d8754c236258bdd2 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 6 Feb 2024 10:31:46 +0000 Subject: [PATCH 13/32] Moved run_registration to be imported just as run button is clicked to avoid long boot times --- brainglobe_registration/registration_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 60304c9..c11f4b9 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -27,7 +27,6 @@ from skimage.segmentation import find_boundaries from skimage.transform import rescale -from brainglobe_registration.elastix.register import run_registration from brainglobe_registration.utils.brainglobe_logo import header_widget from brainglobe_registration.utils.utils import ( adjust_napari_image_layer, @@ -221,6 +220,8 @@ def _on_adjust_moving_image_reset_button_click(self): adjust_napari_image_layer(self._moving_image, 0, 0, 0) def _on_run_button_click(self): + from brainglobe_registration.elastix.register import run_registration + current_atlas_slice = self._viewer.dims.current_step[0] result, parameters, registered_annotation_image = run_registration( From 8480a9c0a663ecd72271fd991a7391766fa3ca9d Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 6 Feb 2024 12:13:15 +0000 Subject: [PATCH 14/32] Atlas rotation works again (might relate to napari 0.4.19 --- .../registration_widget.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index c11f4b9..e26f833 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -395,20 +395,20 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): @ np.linalg.inv(post_rotate_translation) ) - self._viewer.layers[ - curr_atlas_layer_index - ].data = affine_transform( + transformed_atlas = affine_transform( self._atlas.reference, transform_matrix, order=3, output_shape=bounding_box, - output_chunks=(1, bounding_box[1], bounding_box[2]), + output_chunks=(2, bounding_box[1], bounding_box[2]), ) - worker = self.compute_dask_array( - self._viewer.layers[curr_atlas_layer_index].data, - curr_atlas_layer_index, - ) + self._viewer.layers[ + curr_atlas_layer_index + ].data = transformed_atlas + + worker = self.compute_atlas_rotation(transformed_atlas) + worker.returned.connect(self.set_atlas_layer_data) worker.start() self._viewer.grid.enabled = False @@ -419,15 +419,27 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): ) @thread_worker - def compute_dask_array(self, dask_array: da, viewer_index: int): + def compute_atlas_rotation(self, dask_array: da.Array): self.adjust_moving_image_widget.reset_atlas_button.setEnabled(False) self.adjust_moving_image_widget.adjust_rotation_button.setEnabled( False ) - self._viewer.layers[viewer_index].data = np.array(dask_array) + + computed_array = dask_array.compute() + self.adjust_moving_image_widget.reset_atlas_button.setEnabled(True) self.adjust_moving_image_widget.adjust_rotation_button.setEnabled(True) + return computed_array + + def set_atlas_layer_data(self, new_data): + atlas_layer_index = find_layer_index( + self._viewer, self._atlas.atlas_name + ) + self._viewer.layers[atlas_layer_index].data = new_data + self._viewer.grid.enabled = False + self._viewer.grid.enabled = True + def _on_atlas_reset(self): if self._atlas: curr_atlas_layer_index = find_layer_index( From ae1110a999538038f125ef2319f1f748db3f1fee Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 6 Feb 2024 12:23:07 +0000 Subject: [PATCH 15/32] Added test for atlas_rotation_signal from AdjustMovingImage view --- brainglobe_registration/registration_widget.py | 6 ++---- .../widgets/adjust_moving_image_view.py | 8 ++++---- tests/test_adjust_moving_image_view.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index e26f833..0c2ae1e 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -421,14 +421,12 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): @thread_worker def compute_atlas_rotation(self, dask_array: da.Array): self.adjust_moving_image_widget.reset_atlas_button.setEnabled(False) - self.adjust_moving_image_widget.adjust_rotation_button.setEnabled( - False - ) + self.adjust_moving_image_widget.adjust_atlas_rotation.setEnabled(False) computed_array = dask_array.compute() self.adjust_moving_image_widget.reset_atlas_button.setEnabled(True) - self.adjust_moving_image_widget.adjust_rotation_button.setEnabled(True) + self.adjust_moving_image_widget.adjust_atlas_rotation.setEnabled(True) return computed_array diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index 1e50855..82d51bb 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -88,9 +88,9 @@ def __init__(self, parent=None): self.adjust_atlas_roll.setSingleStep(0.1) self.adjust_atlas_roll.setRange(-rotation_range, rotation_range) - self.adjust_rotation_button = QPushButton() - self.adjust_rotation_button.setText("Rotate Atlas") - self.adjust_rotation_button.clicked.connect( + self.adjust_atlas_rotation = QPushButton() + self.adjust_atlas_rotation.setText("Rotate Atlas") + self.adjust_atlas_rotation.clicked.connect( self._on_adjust_atlas_rotation ) self.reset_atlas_button = QPushButton() @@ -135,7 +135,7 @@ def __init__(self, parent=None): self.layout().addRow("Pitch:", self.adjust_atlas_pitch) self.layout().addRow("Yaw:", self.adjust_atlas_yaw) self.layout().addRow("Roll:", self.adjust_atlas_roll) - self.layout().addRow(self.adjust_rotation_button) + self.layout().addRow(self.adjust_atlas_rotation) self.layout().addRow(self.reset_atlas_button) self.layout().addRow(QLabel("Adjust the moving image position: ")) diff --git a/tests/test_adjust_moving_image_view.py b/tests/test_adjust_moving_image_view.py index 0821c7b..8c4a2d8 100644 --- a/tests/test_adjust_moving_image_view.py +++ b/tests/test_adjust_moving_image_view.py @@ -123,3 +123,21 @@ def test_scale_image_button_click( adjust_moving_image_view.scale_moving_image_button.click() assert blocker.args == [round(x_scale, 2), round(y_scale, 2)] + + +def test_atlas_rotation_changed( + qtbot, + adjust_moving_image_view, +): + qtbot.addWidget(adjust_moving_image_view) + + with qtbot.waitSignal( + adjust_moving_image_view.atlas_rotation_signal, timeout=1000 + ) as blocker: + adjust_moving_image_view.adjust_atlas_pitch.setValue(10) + adjust_moving_image_view.adjust_atlas_yaw.setValue(20) + adjust_moving_image_view.adjust_atlas_roll.setValue(30) + + adjust_moving_image_view.adjust_atlas_rotation.click() + + assert blocker.args == [10, 20, 30] From 36403631c8646bc409476c791189e9a62def6c33 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:12:04 +0000 Subject: [PATCH 16/32] [pre-commit.ci] pre-commit autoupdate (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.9...v0.2.0) - [github.com/psf/black: 23.12.1 → 24.1.1](https://github.com/psf/black/compare/23.12.1...24.1.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ffb785..7577ee0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,11 +17,11 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.2.0 hooks: - id: ruff - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy From 9d54161f3d737906fa1248570301ab3a9f3d2a7f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:18:30 +0100 Subject: [PATCH 17/32] [pre-commit.ci] pre-commit autoupdate (#31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.3.5) - [github.com/psf/black: 24.1.1 → 24.3.0](https://github.com/psf/black/compare/24.1.1...24.3.0) - [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .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 7577ee0..f719a01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,15 +17,15 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.3.5 hooks: - id: ruff - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.3.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy additional_dependencies: From ef2174d4ccdc8f695a8d50c83d5879f03ef4dc69 Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <24316371+K-Meech@users.noreply.github.com> Date: Fri, 12 Apr 2024 18:05:54 +0100 Subject: [PATCH 18/32] Import header from brainglobe-utils (#33) * import header from brainglobe-utils * split package name over two lines * remove brainglobe png from manifest * add brainglobe-utils dependency --- MANIFEST.in | 1 - .../registration_widget.py | 10 +++- .../resources/brainglobe.png | Bin 21216 -> 0 bytes .../utils/brainglobe_logo.py | 46 ------------------ pyproject.toml | 1 + 5 files changed, 9 insertions(+), 49 deletions(-) delete mode 100644 brainglobe_registration/resources/brainglobe.png delete mode 100644 brainglobe_registration/utils/brainglobe_logo.py diff --git a/MANIFEST.in b/MANIFEST.in index b0e8711..902dcc4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,6 @@ include README.md include .napari-hub/DESCRIPTION.md include .napari-hub/config.yml include brainglobe_registration/napari.yaml -include brainglobe_registration/resources/brainglobe.png exclude .pre-commit-config.yaml recursive-include brainglobe_registration/parameters *.txt diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index ddecb4a..7db58d3 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -13,6 +13,7 @@ import numpy as np from bg_atlasapi import BrainGlobeAtlas from bg_atlasapi.list_atlases import get_downloaded_atlases +from brainglobe_utils.qtpy.logo import header_widget from napari.viewer import Viewer from qtpy.QtCore import Qt from qtpy.QtWidgets import ( @@ -25,7 +26,6 @@ from skimage.segmentation import find_boundaries from brainglobe_registration.elastix.register import run_registration -from brainglobe_registration.utils.brainglobe_logo import header_widget from brainglobe_registration.utils.utils import ( adjust_napari_image_layer, find_layer_index, @@ -85,7 +85,13 @@ def __init__(self, napari_viewer: Viewer): self._moving_image = None self.setLayout(QVBoxLayout()) - self.layout().addWidget(header_widget()) + self.layout().addWidget( + header_widget( + "brainglobe-
registration", # line break at
+ "Registration with Elastix", + github_repo_name="brainglobe-registration", + ) + ) self.main_tabs = QTabWidget(parent=self) self.main_tabs.setTabPosition(QTabWidget.West) diff --git a/brainglobe_registration/resources/brainglobe.png b/brainglobe_registration/resources/brainglobe.png deleted file mode 100644 index 427bdaba9b89f967c183f4a998e7e642df50600d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21216 zcmbqb1zS~Hv;{#z8YH9<6qJ_ku8V?ncS;>vx>Hg>q>+%2?mRRcK)M@|?(TZ)_`UZF z9^ZH6Uhm#(uf5h>W6n9|m^=KVf(+I((q{+=2v~BmlFA4Oh_Ud0&``kN__cBmARtg9 z$VrN+x~1(ce)f94K81QT^G1$a$KtbVyiM(Zf~z@?X=Ya4&r1bS@la~$_mS|iudm|} z#_(d7@No$lcP6b5pH5geOgo;>3AinZaJGac*i=?o{J!=PZuLF5O&NHR!AD3$fK7-VElp4?n^ENAkMV7f8?P4VTQL40Ac=ejr3O2%ci*0$90@knqvj@=c- z+CBkhRWsDSL>z>h{mTRv{Y~G>Eg>I_tkpAvH8bzdMH+WhqqGj~77p+8MBb*3%skmI_y?Y5XnqO~NSoA6T5#BH|-qz^U z*_D*n39zyrHlCfGC4T0Nvi+{*ujLV1g6& z3^O)3=@uA@>gMHb6}ZF~%`EIWF|o|uBC&MN=$#9)?eVH;*k)($wVTo}h`#%oLFw zwEa&(VTee|LE|}QXseV`LK#&5&?t3~TRmsga&iGD2p50y%a<>udd+SQS_Klh*3+df z6El?Ge}99K+&=UCsQZ`p7X+#g!Sj}sLZczJ9M6j~o3FLC5Pv}p-rc)n#on@`21~ny zTOuwO7_|GIk%Q5l&!dV$zLU{^|9uhGt-%+U)$JP|h7j#Yop(nNAELt!Yc-joUYI~k za3U~h<;GTtB7U+iK9@zKA!M_jDpvUU^QTDYJ4y3_q|w&ORvd&=JBY>ZW4g#qA)CCewF#&v(L;rp$7VPWCHT#a>R8znhq z%x2i6l`UnTw;UP`y2WU=l(jg;OK)J{9>>Eg?k$8Sd#$~ltDjN_rzlKB;FEM{(B&!7 ztT1*8y9~W6l8b+RaFS_MHQV!U0ityux_MpE*ViXK@NYQS;pH?FGqX2OSHw;ka$vg^ zWen=M1KpQw@~m54G#X^3%Y#KEFnI%2XTQDqI-HyH-O%eVvoTCW@??AW(g1wkyeTX% zw>OsxfkjgY`~VNLw4kVDT8@U60VveX&COlQV8n_MIBX_J;kNzqju*hzn~3gg!$Vqt zmERrCwnm9rxC9ef4WeD9jzz?9pKWQQ(gaC17_u=WCMNp$t{iEW8zO?Kh2h{xDP`1Y zGTByKq64dZwzJ&oJASgFq^cU^xUq72ePMiv3da2QQB$+t87$AoR-b#>Oc$@T?emk|K}!;M{= zyqmKbL?o@;dMle7%?DG=_Zq>3RsFk-8(zg(P-2JX;5M0qnW}Z2dbMOTWXc%ZGkiH| zn6#o0jiCc?M)Ex)V~Gn~xj$c*57EXGmn|OOJ$E57S(Y-kul_WSQVUteh94FhA)&=^ zJ%Hu0|Ggu@q?A$`lCpBMS!%u>4t>X>-+a{$wo3!{@3qBq$G4U5eI8H0l2Gz2dvTES z+W%3G2{wp;Qh|31`cY7@r5yuvV#4ptQOQ>*ciEl#4Ncn7_3;sgMknHm8%w#mUi8ai z#<1u$w$<9smtJWvR905TXxCUtef|Nkyn5B$$BHgQW`C|`K-StN#>@8pA2h>ZJ}h=|YQ-r#@Z|9mcW{qhOY8c)t>QnP7a z9CsFGEP(mm;o;U7o>aj9H2D2qZwzO@sGsZXJiKg8j*H7?eS|Wq7uPd((VW;Vw|MUw z9Wa7bC61-rE=5ao_p+#(y(50m&maH951hbi7UowlV}qw+<;MH)>wN8-7HKCaDFGmI z6isSBDz}PoaBwtLSD)>F4Cw{$h%Ms*5ErdM)ZEiX+@m*J zDuPJH>faS%%ZDN*7f+qO|5nRFrA*UKdnHDE0DFGPy!ySsmEKBBaJ~MCz8VnL^GZiMoSNA?T46UK; zu7Ubx!&CWrQ|x&^*LR%xb@cU@T(b71#L?tLBP%|uQBs$wxz1Zn{xV}_X1?c3paHJ6 z68ci4Nji#@s6C@1Np6-3)fA5f^E6z9P0MR0{#!#N&x?&?^6Bn%$3@jyeqi>c_T(DC z*bY?O-Mk>&sn!~10-@Lx{Ol9EGyobiy&5v}$G4lKF?ZEBPMgO+9(Sg=cdk58)ZHh? z&)Ws-73HQ4abw0Ztb?BmN<0-K@=r zqQ>+%y2X08>*C8JyFpA1@@-Thf8t5gVOPG@SvFD z3G+e=#{yUXyFNa@FL$0ZEJ$BvZVho9`Z|Pzekn{HgsTdCci<#BrEun9ai8hmiJ1!! zDtu-D+s2eY^UiB#q|%S6Bg=(D?FA+AW}!q~=N`NF4|gP#_e77WLk<#=(k7Lpw}HTW z_ZYI}vi0a|IQiw~=NH{fwRLoK%!0FXY$7ctO0*I@Hi`FwdfolD-KE@g&JeBlR*)&A@g6U%qx3x7-x~F0KV^HIaxb@kC}DG) z`3UY^sJovE!;U>6*B8OCJD9*sV!b6Uqu+Hcsr3l?XXzdN#ud&g*Nrf;ufKlkHM#QW z?c@~eYz_w3G6&cndsBcVSGpI-!o+pkL`xHKBB^CbMR0MXL%Zs_bc^17Ug!;MLoE&t z$IFMl`h$qs)4Nr)TH`;&0_|GN)A>il5B?1+bti2IsG{72*8VzMjUqg$L?egQbS=aL;hqpr{!;sG8kw$P z?Z%rv#GZ66$Fi9&G{@!dWPz<#Z?w$*nHQ`>EfYnIuppS~(XxeWTdxlVcpZ`Bv@C+f zZUwaPXOR`(CrK&e=!C+xQbA0P*83My1v^78GvVE&RjmCqOQvRL&iTelXu-(Kt&MeKSWUL^ z{3E$`%x5bOMz68AvB451Bs@pD5Pls*M-8GFR$R2Z+@7PO+xy|4d3ihK)@Sv5I!`J! zf12Ol+;@*OBL=?d^vK4EK1uU-Lt-=Fb|X(tEx%-^p=(EI<9h4k{M4?_6W29L4*Vcy zUYR%LxfCswe=bRR0v>dZA)SL0bJBb*iVhaA`1Ls2-u-(tIcn@slTz(YT3oc|Fg=3? zKc)-=L6?u3q!$Yg*BZ@uc(m~t9-^ZCK40{I%Ap zR3EtY@y^8hN|>8p?Flf!&QpQ5hB`(cgK%y4<0}f>SSfo`Z4Ki?h`4N;yqp|6ThGJK zDTGlF4^&t)SyYga@sy;JUF3OtoeBc`v(e*5^!B^EnG&tzI;_b$)XIQp$2c zlt815QOh(lHj>vMk;;L`K2dOx*t{iK3;J`E-=@l3F!}G<;313oE$L~u zpPGGEy|#F`y76&a^~dLJ@D?sk5x|!|JAC$mY^q`H$FZB>qho`v047T3AW9(3)7#|~ z-0YX}eG?ibRWZfI#X9A?P0xYDEV@9rJ_**dcgi;(kv;CDSA=MsfA+pwBbuqUd3j~Z zy5?eq{`q-Sz@dKO0C3ElA{#&z4Uk(&T=jlWSCrT2abZNGHxYAN`BScdJu_iq?F3o1>u3 zRf+|`1d-QM^V@QT(>Z5>h5)h0XWOTtQ^V0;%5V*mGM9J}SkvnXrkKf5i#DTzQk!a9 zjw09HBf8%$TXIIPxMyT6O35;fTGf#D%s{L#aYH-ViJMw}qxZg)H1-3AZ@Ah2=2x@G z+KMNUgqRP#y7~LC zZsXa~NvnCOT`H=eSP`}!RnNb6@IYtgTu*@xynz$)+|yn0MQwbqqpQ5PkHwkGf(R7a zTqt6=f1-6{vTy?JH*)CfI(iZHeh(`Q<>%)^rnjB9XLf^e9r3>Zb|FyNwgqt%kJSNx zYl19J?4=?kps|LIVQUfF$LCb^r|;0XjJoJ%?T0L#Tl1sdjP5NggAzdlSJXW%0VI#l zpPmL#dwr^#$4Pmg_jP7B1os3rf1FRDqzY$muJko^#3n7=kRE+AZvy&|-kvX)v5r7dIlQBv|+l^~fr$n^OD|?p1S9 zdleyjq3!OrS%|0YjF+W_N4}Rk-rXZVZE^W)?VhkwmRLETuZdH5& zm;#P)`+un1Umv-~(KLzub?X>JArj_wxuegJ!in2F`*LRg;`YO96|y}pDcKHt zUUGTWo2wnOy}7p?Hr$+8Q@=~b&cpgQCWfv7X3aFO^)L5(TxE2FW1GN460uaC!vztk zrZkV-#>n3CkBp2=|Mcn8VEHr=jRQ0I`Jj{#Gf6`I=FJ<*y_>gB5K4h533^+)uaPa! zcCt16Kn1A7;;DXoBosjdqi^?jsTxUjW?F;7xH}T?pklbl_6@obywOGB&>pmKd6dAc zc+!7x9;K(lkhFGPVK2#=5OSI6YZJUg5jaw2{k!#kxpqMq-K&xqyEHRuDanKx=Rg}d z(4z)b3|F5c`MpZTGzpD^Ok8daFz@fbxt=%Genh4lh9YW7TDz_vV)JRfTOEG1g&e=f zMMhNE*9}{WG~tW6b3oZVb`w?r6iJW1bx_yS=+#P)w*`B;F??|xGGs$8-^B=y+U$91 zir3<0M68|j`*4hWXU%OTKEJNBh<-X?I`u;*?fdJU?!|JODYgQG(DW)zCIS@|6_*+7 z=-8y5(>vX7EE!ZirOc}QQLjN9VShOFvikL9x6@|{Q6eJ{c(a+jH%Q|DYkt;4265kt zVWl-SHy0L-k)g?rdvLS4r3J53G|7+K)bbI;uCvLCcSaXm!n}j~dq!7l+xu+Iim1Sk zF!+M4JP-JvnM~_Y=K&ZvRt@33p@Gb+oqGPJ7Evo8G}=|3Pzs zq_i}fE`8siE=<56sYW6Gb>^=#nO4^V3`HJ@fK5CT3+9?zd~I0=D3*yyr~3TKXOZOd zar|>W4~FEBZMIcJIsMt{;Tx&d(%YJ;MD%*tTf7?Nj&{0O!5Yceu?9Tm4XRMV*Vt#u z&({-3k%Bg_r)Fx@p2l~7Rv%#cSYl+W{SKQqIxxiL&K}&NLH_PM31=hvFk;D+fTm7? z6fJ<-Z_u195Q>5x?g3)IA3X>a^ZS$K)&f~j{wfv#j=;7K#5y6Gd z_c@qNwZF!!IR`<~1FIXr&~f6g_94<7FyC4#mFs2oc$+Lj3g)RIK>UMeR%0VZ_dk$_^3Y;2i zccnumx!k*>8{yu=B@|tj=_+vIu*#=tX_Ta*s&LAtJ)bm{+OsFWKTl@8{caImT;NQ= z@4JEfo@5S-bh6LiBBrF+QH5+OU#Qwfm&`|HTW=?!b)!k+vLmT|Q4BXo5m2ERdt7p| zs%&5kaG+VfTNAq4uB`R-^Ui_DjRU6yr{VhUZUb>^s+OIhruhhjbVi>nsYGh9kTr;y zQrVwF8(#nCBj8EmTAcPId+U1CxZ-`Aetggmrv}|$f{lw-HQcaL=ClEGiE&^cdJ*ED zUN<;p`+?ESWTH9zd`J7gJv-(`^ci%p?D8ox1lP6~X?UGt0QKDgDz4ee5 zfeozCn=d%}eD3)>QG2Q14*=JcDk_%gdbG&6a-S6GxZeDDWlkcHiwekpA>Y*~9*>G? zdK$bOhJJJ|6f{>oD}ucjry%BraSt`vm{gn))q75^g>hBV<1isbul_mm@Q|mGMmaw~ z0RAj{(u$)Tes@;S5gPK7GNWlibV?a>2LLD+Ny10QvsQj0Eef~G7Kb5rwq%`M0S9;x zV;SxNl+02Z0MEY}Q7h0Ezz9t8NqSWNG%y%XB}cfa%pB_b?p6yPKbj9}SMhmeGVm zG91N*C`0r1bpe*R<0{P>GJ@-^pgoK285qe}q(W^^u$Y(L2?(HDBPo`z`Tjh1L zCl+$XV+76((X|KQ$-_u(IFmad5?O@h|e;HZAH$feZIrYPO<^1g#-t=ux{< z0ZtGq@#RFxK@sc{1TkD{T3Qp73({9~Og7x9@=-BmZW);YdSrl1O3-=7VY7v``xmp{ z8p*qE>3NVP~y-)tY*y02=`OL77qxN(@vTy$~OOyVg&{m&Vf}Qh@nSe%DOz zQPs$MC|HTO!%hWe$_Ejur-TPGu1!LA8Ok5<6Bxdns*+HbnM?=wS#kQ@D+g5m>ehma^ZOn~7m&;9vx-8@%j z8KyyOF1KRXTWJxr?{JdPzg`8*M!g-yux_5pIC9Ys>WYpY4!Aryr347L+lG7otr7-cqu!nT8^0>l2{QRwaiL%#M+xsJp zT1dcgHDY-R5jaYL+5`3_0!T2>n~u&s?)&B~dX|KUs01rsXmhI@O14J-n`_yU1|~3D zKq31>^UxcZdS-sFP*MY9>@39y1i4w#`#eY{Un4#gQSEivVjYpDf~%x~msEonwdj*hn_;IJ;Vnh4+2evEGG zc|{Y&0QsQGm9tvm-%tE819ip4g_`m^6X&l+1U6v zj!79r{6RVx@#KZ;2Tk5+0k0pXC?1{YGAB*7ek``6S@3H?9z+t=<1k~L7AtZr$Qt75 zhy27}ree1v78u?t@4@t{&PEnbs{NWlj4iT-`fD4g_sf4XD%8UOE{LjHyCf1ig)RqD zzW*v`ZSH}Q!ONz5jcgMIN;yJcyDwJGw+3J9x^MJ8JMu_7+5$m7e8UNzoq&*>3Z}ms zLh{ngADL|Wib6y zI{#Ul+MTyvNEjdmblaO_OB&qxd|8|vvSc{XFVRH&1m^IumQT2a4)^!<-E33)VLT}8 z{xmN0jQjEab-YRxqA{>$021w1>no?52htE0EAzuS@q$;Lf8u@ZWB@P?$8A3aL7xtS zJ=Vn>J-TfE_@su3pJmNl$V`otHasP3M`_sl@CqO9>^OhLBOMBP5Z~0&IX+x=uz;W# z98H|Pb4C2vcEhcL9t3vMD7(G6mmhLQ%l+MLM^gWzJ2@VoZ`*YcTj|vY7_+6t=E4SE zZaD(_AooOhS4x>Xm3bv&HfMU*g6Qm1%+kk~-=+_AFlwyj>>cMz;CCF~<~o^I@r8L~ z#vK~Eb(Ksf;Zg80Hg6dw&M_GpC{n{uHB z7n*FC`{`aci94V))X@B9%d5AycP7T0JQi6TiB1rdyMc$jA5R@2Lzh-K)O$h>YX^ZU z00yPgwxM9Kri!Fd(h=T*vB+ZJa=XUu*TgUfhfB=jkB;_>?~ml4G&|p6gFT;!e6c{` zmSOM(eyI`IuTg};##Fn;LS~r578bGqIA~(t-KZ8+OMc*ml2&yfPsMPBe^%k*+wIX7bk34YjAdvev9}KXo7IpD@O)Gg zDq^~!QK_?|5lhb73?Tq$4w5=cf@)7FLGYOL4i|o@ip&BgD4LR7IEek#m6k^Fj^e=t zQ1dX$AuvKF!@1GD`q^)papXJ}&3t_U59|i&4D4yRlQXT68#qJxfA);P1WL5nJxM08 zdYye`gDSn(pW`C02*L*>2SxxwTZj=)l;scz^Mf9QwA+6)PXTj8n}z1#B8$n^uf|G} zfXjeI5$xsR?f+ZG_m`oPbUKNOPsDJCjQ+g@0y<87-x&m4f#L3UDgFmoR=p>H)-ER^ zLhPqt`JVmX^T0s1;4;Q!%@GQCC@_yV{K8;_09kA%%3C*IopX*j>Yom#^G?;eHxqWm$0Pmy{$blc}Qfe?{;Y^Xz*@{-#R5-)wkgGKeA z7UtuG?+V3;N@rd%-tS|P+Z=QgN#S1P^+Z!L7wMpIBOAHLfffhWfH_CAk1soTP5G$> ze$qYHTcLmE2ZS$L|3XEW@idVf;M|v|=IMX%#AyEB9A3l%QIB61PhFrB`W-jSetjWX zVgxGTG_vh1Q4xb4M-6C2K#|WVQUB*7%yVMgJUThF?gEQOk^Sfhx zXIXjq-|fJL13o@q_aQ`)755HiV`jGfu$xyiN+w&p0V-Mkbd83v{=Thw{NKqUX1E}{ zw(St6lzjqHQmw)cgxOF%4LXoHe;AO>_ayrjN1zkYwqD*@SGFg%oTxbf$RqJuhKH^f znCTx-I_cuYcoc_R_m9jlV6WO6+D%6WZ$al z-BYY&%odxK@6JWn850w!_&e4Yl2V|)RQZf}jH;uQG47XIQDUD1IakZu4S@6^&Z&gz zAT(}au1HV{Z1k70m$*zk&ob2(PMZ}#{c}9&+ZThw9NJ`lhN|TpIB(QSb(=)v5CE!= z&ydzcW?ZC{;erMLY1Wq|Qjpgb8N~hSfd=@0dP8Qxn(e#a^!^9WEpJ?&br6uf?qrQA zzMB72&r7c!9dd(+zMz#{RUSizPY)u_wo@+@SG})Y4&*jE!g$caE{R$uG8`~+sG^PZ za-!wHch-g=p zE1JcTRC&R*e4dNWgcYGI?^BPXbNQyxI(pKRRkGVG=Tkuvu5z*o4M#)eN9>~>p(+VV z%5^G>$-?!gGUhidd#fQ`h&?l8Z`1q-I>F+_#mB=4;qJ%s@0(dldZtV@zZQy_S&erD zUX{pz*{f5>TIuYpQ?s(yO=7M>4Pc$W|4ApuV@xGL*K_UF;tj$uAYBm#@0mMlS>MdYc#ngeDrCy<1J1! zhDu!`@(XS!@%tSJMhjO4ekNXV5p@Dnsdf$cyt_{@@TwEGPcjnAf;yTfP0>zT-}7v@nvvR;@^1 zP_!nV?nm|d0)m73J<`upBMSXCxo6$cYuz$disuNFr7kKYjFYrk6G2JsL%DTCZgq9F zLolr;NH*Ml6A$krN3Z^{pBO~2z{!CqN}ZghqWamhwV zb>oO&yKWPs3@4kv9<8;nTgC`qpO*w4T?Fo~v{0NHb^bkOGn0gD&9R`(&i>5|%x;N8 z>h!1%(mB!gXZT`&cA->&w5q(lR7q*7If#C~hK6s>@_qL;&?phMgnx?x;HXxl&K$AI z%P=oU3|;Bb;jb>h8NaCuHlWf&-_5%WZ5(*~-nt`1iP%^RL8*eB2S&-$w*@v4wWsD! zwNI;&Y=HV0v3|>?!c2wL+xIV)$h4pCKCXXGq@QMOAE9JkDNRQDQ=?b)r*6FHVJvs} zAQM%;jQBuLh{)SeH?2nH)-Pe-hw^rYhFw7^!35Y{HbW__lyM9cB{Ez<-HT_zO+M!0 zgORaMQ7cy)t0IfPuT9#M`AsbUdFcy3hwJ@^ftkBnwgPLXt{HA|30{__UGIqtG-<1rmdd0d;COf$o0{NlQg~}h62A6WV!hrFinWzG~p)z zg5=A^G!l^duw;I$GpoZEdfp=8kG9(lmm5!VrC^DOk6i$7w= zuEvkntwQ*q&+hd7QP2wc^T%IY$fN+_4-LSEJfP0ui@se4T;^6359P_BuMMD%|4hqh z{N-8rFV9{Us9IQWTy@{NI97i0i~Z%qOP)G$MIsMka=?a-0Y&AQLttYz&$3%@)_E0R zCC;REzC{i~a2oi8&-Uw8t;P{(Rse2<7IBK=?}T&!T~;ng)yxGUVvv^OSRMKcBq^d1 zGF!E=d=uOh$rUbKH!m(^)35M}rYl&enD~J@G~Y~(5gQ36&g-#w)5-tDGB3)#bQR$b z0=1|g2Ado39rE#T7sdoL>JKV z8JlE=(;;Fxqt5|tZUC((gVMm1Gx)2?fhAdRD@sACzx4%L)B7nV8j3yF7i1k?^Vt`* zpTjYMj{&MP9{&n)6+}(kn4s39xXF94OIQ_}#j~#2-|oVZ75?&Z|DJR-!Ixt>)_uYRkY=|%iITC* zP2rtdns|Faf>gN`@}A7V&)X)Jch#O(yTF0yrQDb+AdLX!M0gH#DDaSdgM9z`U{dHK zk4)qaD#OEF@3WbzBO;QSnx|mNNyc&r7h27|RDI@ml;7wH;{&%5lJFeoqC)I%EN~fL zTcgx~%kTha9(v{$Uu5pEV#u&H_Z-@%&qA*r6-?d=pVu6B397}#pu}?C$0~c(&#^)s z^6<<-#i#W#2-Fr<>!ZPHv6Eo#_TX%PS^AN~M!! zgRP#9-x~lQ;DqVnQuo$}i%l8zwhg|(EJ(gNKv8!C zN3|0Z6SZI&LFgt9nHo*kufQ z82P%HZ8DvOP#rwS-a)XF*?6;O;@{N(O6Rk0y~(UaTy2xyza9^*f{m?yU>VvR-VeH| zsa%mEq`I&0a1;hRT#!Ez5aRM=q)|?fWaKB`0Ob_cftEc+nywJG{xU_=H22#$-997` zDq!!e`gQS@s23SU-e>pV%5`ZhCXQQjVn?rzkhxZy4YaZR^1Zb(evEDv$=qq5A0^|H zUJ_tCyre%O#q=(ZL5f3=R9b-7fo5&cy^#oD4rPiYa}>}|GV(*2e3;;{aZSJw19HiD8y`r zehy%)jmJG?RlCThR}YiWEi7^lI5qfgyyTZK%9;p_$S*Dx$vW>`3e4ffViVz2Ww1fT zw2&N|c8R)qKaWRAl)_{Z2n-5NT$;Rd0TVcjZ`m`AXk)kr7SUnxdQ4d90(4k*V)+Xbv{XrO| zBdk1$((ia*ZmJ78laBr-=RF_5)JVh3TtWmNqj$xA;N!OOO&uVpT)j2HFlXd2Xua7i z@|k(`*B3dEki_%5qrrQ5V14snLc}T5F_!8rusvvDy(YV_#qV{|$4aKeW_Hu*7=Y~m zEf%gN;0MJs)#>T!V_XIHk2<pu%7Z=si~XK%MgYuP!A z3Bpo0HG@<7?&ks&pDz_D*k~Ujv~h1>n;w znOY0RaNP}ZM(NHtDxLWkLrQH)+d;OJ!*grDxsN)H2y-~8+3Xo z$N<1>biUT*J*9SB?G-C^X)$9B?j5tLc@bHG14J2cLE)s8*&ZqT&)i(hbF-!uVEm)0 zKJ_P{3S2M6lZHA1bu*Hj*xT`tL^K&Ocswrb(;Rur;926f2t3A^qYcI(S}b| zCiyYG(rqj3-LS#8-i3ehK3$tz6mqxnUufcayLnZ6oCEYJ+P>ss;#NSE+viRoyff(#vD+z%5Ch7M)i_Y=$mYglPIF}*K&VR81 zW5qp8iN-XI6Sd3EK)&!aW1btTjCI=et74>3O2Sf=2O*{;Gjge|!s#sLR@k|)@VtDQ znT9a&+cz$Qk}BMunMVL%>LuDWiR3V`*^P!Ri63v@Y>iXi)IhE*I_IH=B)xISV1lM{ z0rT`{{svUg8`=*oExvCscV|hP^;xSS>QvwyhAlPc&|L46`46wog+lew(dB??$aNz= zQ7@{Yb&X0w_>%%MO@Wk&-@)*b8x``mzSR8xMDg|D!RX`I89a@=U?IhpXskolit}5) zF?yfr%2wekQ*UBrz+3N32O<}ghq=cL!yXUBy(Zt>)s;}ci&x62`*R8=5ZnVS4+!}iV zDrc`~-J#{R*ZXaOAp;NPfSqh^$WqQt!F`t(hwXEsjAmpqx<*KBn^XgzYGV0W$_gur9`k*yom z_$;?)0tC-fxOh^>UnPBB_o$07b1jYC88?Uo+(Gi*)XN=EpT z(;A(~!K~k-beis+%gWA#W!I~9)0~7$=NQd!Ne6<^qJr`>kzlX+9SNS=*^ zEiDjqkE*CTFZ(NQ%SAQ>pP1W<=H}+z^69_pNrUE~E9rgD5;ndBCJK_uDO0Y`{mof) zpa?)}%ynLD*OH6?VK$Zlpddh6G7hP`7?6h49w%l}X(0;fOrvT&Z)wx_)qo>tYuE8D z1`vJr%m@R)*KhNx8Xzn;2W@srJ=@OFN={*bD8%Y6W!Humnc6@e+UTW@c=LyWin2i% zZ=>j}vOsdE4u>gFOfMxMf!V8cGLN%M-&5o7vD*RmF{y>PNMo385c@@CiUbg;kb>8Z z0Qs3<^s0}i{j~TQVx?WXB{*?xqcddZnOxXcVh7DJis?lHaK82oT8C8liT zsGzbbke*W5hR52Nsb+!~+D-T+>Zw@_{W{KUs0y{JUWe)-h2sZM+kkcKT0MSl zGEMTqnLoYDVL=ATqc)_>V2)yR<#-*V>4(#92%E`AfirV5$ZV@U3E0!hcO{JF(=(H9 z(4})VzC6&a=)6ZNn*i1d z0un_xd`FPRP1@pFehX?aBKgJH`KS-o^WjtPUmk*tUrxAyx7pK{P55{^7#ztCbPR{H z`xjO-F4ANSq?9)DYI`8E<74Z1KUUV zB4OvBXU=08+DV$kloB*1$X^2A95=2L3g#b>bZ zF_M6_NYJYTYG&s^nC&t?hV}gBsSw$a$s90`?$U5cbcFg4_KVbhDl@FkQVvoeZEoa? z7T9#1TRF8j#NIt}JH9$(4d<^sBl1Au^&&W_so3aivASX9t#u!;rM`0 zEKWg^R7x;7_wd%+5dXRx5(%e!8`VVe_E~lTbv#nppa5*3g zh`IC~A1c(?AezKS?hBjm+?`Z?)#`NB)e}=Ly}W+`3ZlZAPN@y_g3zILh}DG3g2oqw zrYTDx8)|Jk?H4wHunS?Bqu?T0k-y=*Qo!G&)3$io3O z#07#K$KbVFng6+6pONq@(55D^Uwlh}RgZ6zN9)mts;Es#E**{ey<_3Wv#i zK(PZBX9pp$-9BNpo)yUl59PuJ>#6sg1Rxvi<(IM9Dt+neU$u5-KI*g{l(cQS1a=#T zD10Ei`ZH3p^?^tKq%2Tsq(n;SDEz=FO@H}`r?;vkXf9O%yf8_a2d)hh2 zH+zHy{qx4g-A9LpEfdYB{k(D32FpAe3&0kvm6#_)IWmjiK}%6|8z zW5crw>nQry>80P88jWDh?eK{z*Lpp#E-=8*Mow+Z4#rTFy7i`n3)TgPr@>(QH8BPN zWC)wo@w;~>3Qr5>B1Wug#;k=qB~R)z50)#~+dg?vx%0lO`@sQD!p+$ZI;co|1YNYC z3C|P#cKODFW(qQ@y1ti|Ugn3UJ-lVbgv6V|{SK{GS6<@3X3k)5fc zNvjfKe-lE`2FtUjEr(gCRy4JQmWJF_37YC|j zfwF1t0HjuDYT`)La-^~J;lfn?T+k&p3&bK6uF#60)wmB}h)+m!IXr8c{j}W#KQvNN z{`_`*j5Df=9RB^AOnY=iDWju9d_pD_q{$nn*Gn;N^vDL*HFKB)+H#Q`B$yPm$KSC{ z_Bg;hMBz?<(g&D0NVsB1anE0@VM{WHIr8rkxw~6U?0Ve5tPHY+`C$85vvI!s6F$mkTzp}2DD_u!0LZOK`sQTQ;^{v5 z(h-%?$z2*vlgjIAy_h!}#d@r_e&xC@TUqkJY=)soRXsW}py@Czo4yqd3h9-AOOcJH$uZz0UjxO3FL&vF z;AX9Y1Y~aJ`mXCGZ>2NxS{($$?ZSrx)fB^HR`-On-?7NJZ8WsCZ{!#xOlK?2b|)Tq zrmSizvl$MhE!`x$zaz_%R|0V+ot*NyT0Jo!@+x+t2!YsVa=q70ooKP#!UxfV0M^SI zgT7$Zi-3S11OF~SvETXltyoh}|DgqW^Z<HNAIxyt#65Mt(|&o2CW-g_w~!Q8KY#>0}&6*mZscKj}mIxe<++9%3l zN`p|RB7-$P4+WJS=$vdm`T+Yn`n3JT&13xe%sWqn_dC&>qj|B5=mQPI`pq_I98B+MGb>6CG3S zKL^$|t{Br=QSN!mHz}U#PtW~8h$cowM7()j68teg@p70Wyr_G>w;5*4$sOMHpi^U| zcKpqkVgjei z#1uSll(d{BOuA9)un(WnYee~9Vn5HqjH05_#=a~i$3o)g2KN5A=4wjLVO0}VJ(nO& z6rTkNnS4hBwj)ocgw~cw{&R`?E)LA#;#&o1+M!wEVMR6q-D8X9)Y zAwWMIu8S8mG$5!>lzo_7J9fJmclkP!{Qk8?$YApTed99oW5h!uS8Xg1F z$?-q$GwEG-)H@0JPy8d`@aJjW5da0G7MDv_u^X-S8rq`MIN2=aEYRL0ZSmDrDs3MY~)E7ie|F+ zs%q8-Rw^*H4uBi>0@M3F%A#QVSA8anreXb^8>G{4;Li0(#+wKSD5hShE9_x`0@7pL zWV}b`-%%mMy}=UD^Y-|->=`uu0o*C^-xohUh#4F#Np{X-L5Hs1@herpq`3-B1M)G7f`o)MU!(>O$To9&r*;^<^7b7Ps<;r+thlW=d-Q;y9;xv znX}VsX~&fhyLa~B)@!ySM|ku+SJ7E$`!^9KNZ1wovYR*=$7^ZFt+r9*wD3P(7*IFvf@#Chga$Ox&~MW0m=so*|bwPL5!DPub=3bea3QK zq8;(7?YsGMf2)&(EYGCCwF&p zd!Gls_%M(mbo*773d1+eeWb2LcF&QQJeUMEdVE_~dU@=nRkEPFj^$O@X9GjtSd$ZpF=@I322%FEy!QI3<;(*12W+h(Aux}Lg4|Ss=3rfW0 z2@9sx0Y^*A`?;r9#8x&=?z~NCcN9%N49aybg8JQhNw(>4Z?Lbmm@BAQSXeN_<(cg1 zu!o>Rj7n{6RP)xz)!%W27qI()u>V`G3flYSrqI__zLMQA-pV>Bx_z>>LGsH!km8jI zT+FsURrnD&(uyq~kkKHAcBJ7|Qx$L#&>*V4w7|n(0)?u^3tMK^{Rb^@&q~zm0+j}3 zW@J2b4GxRnPY14Z*QDt*Nd4n?MpO2?qPQtb4x%utj7K#xQKkRgG{rI@3bkYEQS1(O(kNpL0YM$|e~+yF`0SEsrsGw$+!=e-POh~;W}ujEy1fV%HC z^*$ZK)x69wZcMvXz2wHp8Qr8eLkh7E^ZqfCMutHm&mCO0*4~-c9(YC*X|3= zA)?9q)UqpcRBRjr4DP3ZIMxwNtTV|WuLt*ZckI+o>1}e!>(ft9r%!N1K*Y1vg6LJVX z2OHqhvHFEaZgc`KBaArAO_5`vfh(|l)->Z(WE0<|L^BjXfhyY40d6D@;6{`_zz_h3 z;+00Q1;hz(_6UAixB7Lt;<+}UNbd)$nEpK?S}hxK+?lTdc2v1JFM&4$E4CM;26!9B zz}p;HCjOiV#T>b%(1UAoLucy=Zhj(>_+x1|Z>4O%vssvGa&s!7mFyDRRpmyOE;$Pg zh14cgIoG0wXa&Sd;S^f2+8q%Lwn@lV2pg#nle1SwiMl30=#ysS_neCx5=~iRWtjf6 zo<**DsaiB*-ptk`WCoWzG0RWBxw56ffiv+rrWpEpzb0&~gXo`~TRaky5L4n@$*!p| zcOajQjwtJq>e2z{HxmHP5j81t{)*~4K`ndz5$p>9(bReN*xIhI?ycjav^WoXzDCpa z1`dfOS{;dU@waiV99Biq@8`v|Q>oOc8m@2em(QO+KX^qIfA&|gle$X6lO=8i?nl2J z4LiW15r$|#MeONp=7a6DS1sovd@~Z?OmES!#4*~>^*eX&R6KudyPAM@^)X%J6fg=e6s zVk-Ie-S+genSHYzUK-L`c8Jg_qywtIdO0fYVmlMyp(Z_Q-H8|HQM_wEwu~V67e0T5 zzSQ8Xb#--V5wyd+@;MZaUzW4rxsy6DsGn9CR(w}u&9U68X*P&Den49FDTABM?2MA3 zZ^zxkeH9JE0tNtM;SBF_FPqKIzpghv^kPZ^>QgCWKea7Ow?=Gp?!nF7(Jig5JxA~; zvPo=Y)h9l@siC2v^$3qEG>nknFM?&oYZndpyPG`ht?E59hFPfusFx|$L(cM-cd0qX zi%FY-SFXI3nNr;~avJ^?hnUrh(|-Oyl6rIgQx&>E2{U(kl*KBnay_BN^gPOZ^nPX} zh_bIs=do=WD$9C?5@EdcWM;~ngM4>+Idu2#-5HU1ye7xREiG(qX&MyE5o*Z?^2y}6 z;?&zk!@*4wTU%RKzkfa6{AlhieuBv3@tQWH{a)GI+q2ykw=$QPmrql*B{*pEhU*fNOn>mF8y6#r#K)h948u$9xUqys$AvX}|z$8%0gDcTE>{l4D@;qPLi&5ZS7wg-ETxT+u8 zSPK?Z#{sp2z}R%0KeD#A77s@C`GQV9dwgPo5ET*e&n3icr<)|5cYukP(arCF2>a3z zLl&94m18@54WHX9!ML$^5c2i)ohT?*uva-?bw4SosMJZWpm04-;Mv!REJKecq3D>4 zgpR^X0RhiSy=whD9ipF_5(Vofu`B;n`{~|RJB6a2%AqUP&39&tuVS|59FU9vYDmH3xY(U>Y zoM&OXUgEQ7;8(css%3-6=Fh>wK}igYd`o`*R1Xd*llm^Qju9*KG1H;q80bD2oRod+ z@At8-p#RCZv8z7CD4qIF?<0Vcq!*S)ck6xB1*_r)g%j^@yY8IYS^B#Oqt8$D=4hk1 z*TKl94L?aUdeF7%(y5;+T`G{(F$&Pg9U!2C%caR>S*s}QPqJ!lX+f5wy=>?&A7M7r z6YPE^?}t2&UCCZC7lCSUheV;SgJQza=RfxBf|e3?2|-b2cOd9&@i_=0J1IkuqdE+_ zef6@MGy`s}ZCX)-$1!Ov%?gFa1n&Ob# zj*Pl}(4b1O(~DtcKC9VyBGlC)m294HNful|L^&MIiZYf?{a%%!3e})@7BrScn+TNcHiEw` z`7u4!D+-s!`w~@T#U~X+h2)s>nFN=jq%0@CwPt?kJFajjEjwJt=wQkw-+Dx&8C6v>;>#g9a46@ zBMo-Bp|zDi38d$)gip}9{Bz3B63vPO7dz-{r9YA2OB}71m?SAAr1$ z%8KXO)8jmOFu%a{)Jw9fxB=m=f`gzW((Hl%dcYth$*0^JsyU3sVp#-j4<;(YcdC5j z>F#sqos_}U1Cs!z089j!#*Qt3Ndva=Ul+i)|6rWQn6_hRC_w diff --git a/brainglobe_registration/utils/brainglobe_logo.py b/brainglobe_registration/utils/brainglobe_logo.py deleted file mode 100644 index e387adb..0000000 --- a/brainglobe_registration/utils/brainglobe_logo.py +++ /dev/null @@ -1,46 +0,0 @@ -from importlib.resources import files - -from qtpy.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout, QWidget - -brainglobe_logo = files("brainglobe_registration").joinpath( - "resources/brainglobe.png" -) - -_logo_html = f""" -

- -<\h1> -""" - - -def _docs_links_widget(parent: QWidget = None): - _docs_links_html = """ -

-

Website

-

Source

-

- """ # noqa: E501 - docs_links_widget = QLabel(_docs_links_html, parent=parent) - docs_links_widget.setOpenExternalLinks(True) - return docs_links_widget - - -def _logo_widget(parent: QWidget = None): - return QLabel(_logo_html, parent=None) - - -def header_widget(parent: QWidget = None): - box = QGroupBox(parent) - box.setFlat(True) - box.setLayout(QVBoxLayout()) - box.layout().addWidget(QLabel("

brainglobe-registration

")) - subbox = QGroupBox(parent) - subbox.setFlat(True) - subbox.setLayout(QHBoxLayout()) - subbox.layout().setSpacing(0) - subbox.layout().setContentsMargins(0, 0, 0, 0) - subbox.setStyleSheet("border: 0;") - subbox.layout().addWidget(_logo_widget(parent=box)) - subbox.layout().addWidget(_docs_links_widget(parent=box)) - box.layout().addWidget(subbox) - return box diff --git a/pyproject.toml b/pyproject.toml index aeb282b..4eacaf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ dependencies = [ "napari>=0.4.18", "bg-atlasapi", + "brainglobe-utils>=0.4.3", "numpy", "qtpy", "itk-elastix", From b02d396ae5d8e99d3afe9b749807e023b04b5e3c Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov <61896994+IgorTatarnikov@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:29:14 +0100 Subject: [PATCH 19/32] Update requirements (#34) * import header from brainglobe-utils * split package name over two lines * remove brainglobe png from manifest * add brainglobe-utils dependency * Switched from bg-atlasapi to brainglobe-atlasapi * Added lxml_html_clean explicitly to requirements * Pinned itk to 5.4rc2 for now as 5.4rc3 cased a seg fault * Undo itk pin --------- Co-authored-by: Kimberly Meechan <24316371+K-Meech@users.noreply.github.com> --- brainglobe_registration/elastix/register.py | 2 +- brainglobe_registration/registration_widget.py | 4 ++-- pyproject.toml | 7 ++++--- tests/conftest.py | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/brainglobe_registration/elastix/register.py b/brainglobe_registration/elastix/register.py index 955ed44..d04d918 100644 --- a/brainglobe_registration/elastix/register.py +++ b/brainglobe_registration/elastix/register.py @@ -2,7 +2,7 @@ import itk import numpy as np -from bg_atlasapi import BrainGlobeAtlas +from brainglobe_atlasapi import BrainGlobeAtlas def get_atlas_by_name(atlas_name: str) -> BrainGlobeAtlas: diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 7db58d3..d389d0c 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -11,8 +11,8 @@ from pathlib import Path import numpy as np -from bg_atlasapi import BrainGlobeAtlas -from bg_atlasapi.list_atlases import get_downloaded_atlases +from brainglobe_atlasapi import BrainGlobeAtlas +from brainglobe_atlasapi.list_atlases import get_downloaded_atlases from brainglobe_utils.qtpy.logo import header_widget from napari.viewer import Viewer from qtpy.QtCore import Qt diff --git a/pyproject.toml b/pyproject.toml index 4eacaf8..5927e6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,12 +24,13 @@ classifiers = [ dependencies = [ "napari>=0.4.18", - "bg-atlasapi", + "brainglobe-atlasapi", "brainglobe-utils>=0.4.3", + "itk-elastix", + "lxml_html_clean", "numpy", + "pytransform3d", "qtpy", - "itk-elastix", - "pytransform3d" ] [project.urls] diff --git a/tests/conftest.py b/tests/conftest.py index a7708ef..df32c05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,8 @@ import numpy as np import pytest -from bg_atlasapi import BrainGlobeAtlas -from bg_atlasapi import config as bg_config +from brainglobe_atlasapi import BrainGlobeAtlas +from brainglobe_atlasapi import config as bg_config from PIL import Image From 6436db4d3e3ea234e21369bcc5a877c539a646ae Mon Sep 17 00:00:00 2001 From: Kimberly Meechan <24316371+K-Meech@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:33:09 +0100 Subject: [PATCH 20/32] add codecov token (#35) --- .github/workflows/test_and_deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 5501042..c26cbd4 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -51,6 +51,7 @@ jobs: - uses: neuroinformatics-unit/actions/test@v2 with: python-version: ${{ matrix.python-version }} + secret-codecov-token: ${{ secrets.CODECOV_TOKEN }} build_sdist_wheels: name: Build source distribution From 77673486ea023a906856864b4d52a22873001872 Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Tue, 23 Apr 2024 15:09:31 +0100 Subject: [PATCH 21/32] Added tests for calculate_rotated_bounding_box --- .../registration_widget.py | 12 +++++--- brainglobe_registration/utils/utils.py | 15 +++++----- tests/test_utils.py | 28 +++++++++++++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 0c2ae1e..681c067 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -369,6 +369,7 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): self._viewer, self._atlas.atlas_name ) + # Create the rotation matrix roll_matrix = active_matrix_from_angle(0, np.deg2rad(roll)) pitch_matrix = active_matrix_from_angle(2, np.deg2rad(pitch)) yaw_matrix = active_matrix_from_angle(1, np.deg2rad(yaw)) @@ -378,6 +379,7 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): full_matrix = np.eye(4) full_matrix[:-1, :-1] = rot_matrix + # Translate the origin to the center of the image origin = np.asarray(self._atlas.reference.shape) // 2 translate_matrix = np.eye(4) translate_matrix[:-1, -1] = origin @@ -389,6 +391,10 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): post_rotate_translation = np.eye(4) post_rotate_translation[:3, -1] = new_translation + # Combine the matrices. The order of operations is: + # 1. Translate the origin to the center of the image + # 2. Rotate the image + # 3. Translate the origin back to the top left corner transform_matrix = ( translate_matrix @ full_matrix @@ -398,7 +404,7 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): transformed_atlas = affine_transform( self._atlas.reference, transform_matrix, - order=3, + order=2, output_shape=bounding_box, output_chunks=(2, bounding_box[1], bounding_box[2]), ) @@ -410,9 +416,6 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): worker = self.compute_atlas_rotation(transformed_atlas) worker.returned.connect(self.set_atlas_layer_data) worker.start() - - self._viewer.grid.enabled = False - self._viewer.grid.enabled = True else: show_error( "No atlas selected. Please select an atlas before rotating" @@ -435,6 +438,7 @@ def set_atlas_layer_data(self, new_data): self._viewer, self._atlas.atlas_name ) self._viewer.layers[atlas_layer_index].data = new_data + # Resets the viewer grid to update the grid to the new atlas self._viewer.grid.enabled = False self._viewer.grid.enabled = True diff --git a/brainglobe_registration/utils/utils.py b/brainglobe_registration/utils/utils.py index a66fe90..c2ab262 100644 --- a/brainglobe_registration/utils/utils.py +++ b/brainglobe_registration/utils/utils.py @@ -1,8 +1,9 @@ from pathlib import Path -from typing import List +from typing import List, Tuple, Dict import napari import numpy as np +import numpy.typing as npt from pytransform3d.rotations import active_matrix_from_angle @@ -46,7 +47,7 @@ def adjust_napari_image_layer( image_layer.affine = transform_matrix -def open_parameter_file(file_path: Path) -> dict: +def open_parameter_file(file_path: Path) -> Dict: """ Opens the parameter file and returns the parameter dictionary. @@ -99,8 +100,8 @@ def get_image_layer_names(viewer: napari.Viewer) -> List[str]: def calculate_rotated_bounding_box( - image_shape: tuple[int, int, int], rotation_matrix: np.array -) -> tuple[int, int, int]: + image_shape: Tuple[int, int, int], rotation_matrix: npt.NDArray +) -> Tuple[int, int, int]: """ Calculates the bounding box of the rotated image. @@ -111,14 +112,14 @@ def calculate_rotated_bounding_box( Parameters ---------- - image_shape : tuple + image_shape : Tuple[int, int, int] The shape of the image. - rotation_matrix : np.array + rotation_matrix : npt.NDArray The rotation matrix. Returns ------- - tuple + Tuple[int, int, int] The bounding box of the rotated image. """ corners = np.array( diff --git a/tests/test_utils.py b/tests/test_utils.py index d857e09..35dab9e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,6 +10,7 @@ find_layer_index, get_image_layer_names, open_parameter_file, + calculate_rotated_bounding_box, ) @@ -93,3 +94,30 @@ def test_get_image_layer_names(make_napari_viewer_with_images): assert len(layer_names) == 2 assert layer_names == ["moving_image", "atlas_image"] + + +@pytest.mark.parametrize( + "basis, rotation, expected_bounds", + [ + (0, 0, (50, 100, 200)), + (0, 90, (50, 200, 100)), + (0, 180, (50, 100, 200)), + (0, 45, (50, 212, 212)), + (1, 0, (50, 100, 200)), + (1, 90, (200, 100, 50)), + (1, 180, (50, 100, 200)), + (1, 45, (177, 100, 177)), + (2, 0, (50, 100, 200)), + (2, 90, (100, 50, 200)), + (2, 180, (50, 100, 200)), + (2, 45, (106, 106, 200)), + ] +) +def test_calculate_rotated_bounding_box(basis, rotation, expected_bounds): + image_shape = (50, 100, 200) + rotation_matrix = np.eye(4) + rotation_matrix[:-1, :-1] = active_matrix_from_angle(basis, np.deg2rad(rotation)) + + result_shape = calculate_rotated_bounding_box(image_shape, rotation_matrix) + + assert result_shape == expected_bounds From 9de80145cc83088c63c118220d35c2066e5a6222 Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Tue, 23 Apr 2024 15:11:04 +0100 Subject: [PATCH 22/32] Fixed docstrings for functions in utils.py --- brainglobe_registration/utils/utils.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/brainglobe_registration/utils/utils.py b/brainglobe_registration/utils/utils.py index c2ab262..7c6db59 100644 --- a/brainglobe_registration/utils/utils.py +++ b/brainglobe_registration/utils/utils.py @@ -21,7 +21,7 @@ def adjust_napari_image_layer( https://forum.image.sc/t/napari-3d-rotation-center-change-and-scaling/66347/5 Parameters - ---------- + ------------ image_layer : napari.layers.Layer The napari image layer to be adjusted. x : int @@ -32,7 +32,7 @@ def adjust_napari_image_layer( The angle of rotation in degrees. Returns - ------- + -------- None """ image_layer.translate = (y, x) @@ -57,13 +57,13 @@ def open_parameter_file(file_path: Path) -> Dict: The values are stripped of any trailing ")" and leading or trailing quotes. Parameters - ---------- + ------------ file_path : Path The path to the parameter file. Returns - ------- - dict + -------- + Dict A dictionary containing the parameters from the file. """ with open(file_path, "r") as f: @@ -95,6 +95,16 @@ def find_layer_index(viewer: napari.Viewer, layer_name: str) -> int: def get_image_layer_names(viewer: napari.Viewer) -> List[str]: """ Returns a list of the names of the napari image layers in the viewer. + + Parameters + ------------ + viewer : napari.Viewer + The napari viewer containing the image layers. + + Returns + -------- + List[str] + A list of the names of the image layers in the viewer. """ return [layer.name for layer in viewer.layers] @@ -111,14 +121,14 @@ def calculate_rotated_bounding_box( values of the transformed corners. Parameters - ---------- + ------------ image_shape : Tuple[int, int, int] The shape of the image. rotation_matrix : npt.NDArray The rotation matrix. Returns - ------- + -------- Tuple[int, int, int] The bounding box of the rotated image. """ From 78fbd78f96879dc7b0a20e305601ae00f39ebb69 Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Tue, 23 Apr 2024 16:13:33 +0100 Subject: [PATCH 23/32] Keep track of napari layers for the atlas and atlas annotations in the napari widget class --- .../registration_widget.py | 193 ++++++++++-------- 1 file changed, 109 insertions(+), 84 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 681c067..dca125f 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -15,7 +15,8 @@ from bg_atlasapi import BrainGlobeAtlas from bg_atlasapi.list_atlases import get_downloaded_atlases from brainglobe_utils.qtpy.collapsible_widget import CollapsibleWidgetContainer -from dask_image.ndinterp import affine_transform as affine_transform +from dask_image.ndinterp import affine_transform as dask_affine_transform +from scipy.ndimage.interpolation import affine_transform from napari.qt.threading import thread_worker from napari.utils.notifications import show_error from napari.viewer import Viewer @@ -54,6 +55,8 @@ def __init__(self, napari_viewer: Viewer): self._viewer = napari_viewer self._atlas: BrainGlobeAtlas = None + self._atlas_data_layer = None + self._atlas_annotations_layer = None self._moving_image = None self._moving_image_data_backup = None @@ -172,13 +175,14 @@ def _on_atlas_dropdown_index_changed(self, index): self._viewer.layers.pop(current_atlas_layer_index) self._atlas = None + self._atlas_data_layer = None + self._atlas_annotations_layer = None self.run_button.setEnabled(False) self._viewer.grid.enabled = False return atlas_name = self._available_atlases[index] - atlas = BrainGlobeAtlas(atlas_name) if self._atlas: current_atlas_layer_index = find_layer_index( @@ -189,21 +193,30 @@ def _on_atlas_dropdown_index_changed(self, index): else: self.run_button.setEnabled(True) - dask_atlas_reference = da.from_array( - atlas.reference, - chunks=(1, atlas.reference.shape[1], atlas.reference.shape[2]), + self._atlas = BrainGlobeAtlas(atlas_name) + atlas_dask_array = da.from_array( + self._atlas.reference, + chunks=(1, self._atlas.reference.shape[1], self._atlas.reference.shape[2]), + ) + dask_annotations = da.from_array( + self._atlas.annotation, + chunks=(1, self._atlas.annotation.shape[1], self._atlas.annotation.shape[2]), ) - self._viewer.add_image( - dask_atlas_reference, + self._atlas_data_layer = self._viewer.add_image( + atlas_dask_array, name=atlas_name, colormap="gray", blending="translucent", contrast_limits=[0, 350], multiscale=False, ) + self._atlas_annotations_layer = self._viewer.add_labels( + dask_annotations, + name="Annotations", + visible=False, + ) - self._atlas = BrainGlobeAtlas(atlas_name=atlas_name) self._viewer.grid.enabled = True def _on_sample_dropdown_index_changed(self, index): @@ -214,9 +227,19 @@ def _on_sample_dropdown_index_changed(self, index): self._moving_image_data_backup = self._moving_image.data.copy() def _on_adjust_moving_image(self, x: int, y: int, rotate: float): + if not self._moving_image: + show_error( + "No moving image selected. Please select a moving image before adjusting" + ) + return adjust_napari_image_layer(self._moving_image, x, y, rotate) def _on_adjust_moving_image_reset_button_click(self): + if not self._moving_image: + show_error( + "No moving image selected. Please select a moving image before adjusting" + ) + return adjust_napari_image_layer(self._moving_image, 0, 0, 0) def _on_run_button_click(self): @@ -225,9 +248,9 @@ def _on_run_button_click(self): current_atlas_slice = self._viewer.dims.current_step[0] result, parameters, registered_annotation_image = run_registration( - self._atlas.reference[current_atlas_slice, :, :], + self._atlas_data_layer.data[current_atlas_slice, :, :], self._moving_image.data, - self._atlas.annotation[current_atlas_slice, :, :], + self._atlas_annotations_layer.data[current_atlas_slice, :, :], self.transform_selections, ) @@ -330,7 +353,7 @@ def _on_scale_moving_image(self, x: float, y: float): Scale the moving image to have resolution equal to the atlas. Parameters - ---------- + ------------ x : float Moving image x pixel size (> 0.0). y : float @@ -343,83 +366,89 @@ def _on_scale_moving_image(self, x: float, y: float): show_error("Pixel sizes must be greater than 0") return - if self._moving_image and self._atlas: - if self._moving_image_data_backup is None: - self._moving_image_data_backup = self._moving_image.data.copy() - - x_factor = x / self._atlas.resolution[0] - y_factor = y / self._atlas.resolution[1] - - self._moving_image.data = rescale( - self._moving_image_data_backup, - (y_factor, x_factor), - mode="constant", - preserve_range=True, - anti_aliasing=True, - ) - else: + if not (self._moving_image and self._atlas): show_error( "Sample image or atlas not selected. " "Please select a sample image and atlas before scaling", ) + return + + if self._moving_image_data_backup is None: + self._moving_image_data_backup = self._moving_image.data.copy() + + x_factor = x / self._atlas.resolution[0] + y_factor = y / self._atlas.resolution[1] + + self._moving_image.data = rescale( + self._moving_image_data_backup, + (y_factor, x_factor), + mode="constant", + preserve_range=True, + anti_aliasing=True, + ) def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): - if self._atlas: - curr_atlas_layer_index = find_layer_index( - self._viewer, self._atlas.atlas_name + if not self._atlas: + show_error( + "No atlas selected. Please select an atlas before rotating" ) + return - # Create the rotation matrix - roll_matrix = active_matrix_from_angle(0, np.deg2rad(roll)) - pitch_matrix = active_matrix_from_angle(2, np.deg2rad(pitch)) - yaw_matrix = active_matrix_from_angle(1, np.deg2rad(yaw)) + curr_atlas_layer_index = find_layer_index( + self._viewer, self._atlas.atlas_name + ) - rot_matrix = roll_matrix @ pitch_matrix @ yaw_matrix + # Create the rotation matrix + roll_matrix = active_matrix_from_angle(0, np.deg2rad(roll)) + pitch_matrix = active_matrix_from_angle(2, np.deg2rad(pitch)) + yaw_matrix = active_matrix_from_angle(1, np.deg2rad(yaw)) - full_matrix = np.eye(4) - full_matrix[:-1, :-1] = rot_matrix + rot_matrix = roll_matrix @ pitch_matrix @ yaw_matrix - # Translate the origin to the center of the image - origin = np.asarray(self._atlas.reference.shape) // 2 - translate_matrix = np.eye(4) - translate_matrix[:-1, -1] = origin + full_matrix = np.eye(4) + full_matrix[:-1, :-1] = rot_matrix - bounding_box = calculate_rotated_bounding_box( - self._atlas.reference.shape, full_matrix - ) - new_translation = np.asarray(bounding_box) // 2 - post_rotate_translation = np.eye(4) - post_rotate_translation[:3, -1] = new_translation - - # Combine the matrices. The order of operations is: - # 1. Translate the origin to the center of the image - # 2. Rotate the image - # 3. Translate the origin back to the top left corner - transform_matrix = ( - translate_matrix - @ full_matrix - @ np.linalg.inv(post_rotate_translation) - ) + # Translate the origin to the center of the image + origin = np.asarray(self._atlas.reference.shape) // 2 + translate_matrix = np.eye(4) + translate_matrix[:-1, -1] = origin - transformed_atlas = affine_transform( - self._atlas.reference, - transform_matrix, - order=2, - output_shape=bounding_box, - output_chunks=(2, bounding_box[1], bounding_box[2]), - ) + bounding_box = calculate_rotated_bounding_box( + self._atlas.reference.shape, full_matrix + ) + new_translation = np.asarray(bounding_box) // 2 + post_rotate_translation = np.eye(4) + post_rotate_translation[:3, -1] = new_translation + + # Combine the matrices. The order of operations is: + # 1. Translate the origin to the center of the image + # 2. Rotate the image + # 3. Translate the origin back to the top left corner + transform_matrix = ( + translate_matrix + @ full_matrix + @ np.linalg.inv(post_rotate_translation) + ) - self._viewer.layers[ - curr_atlas_layer_index - ].data = transformed_atlas + self._atlas_data_layer.data = dask_affine_transform( + self._atlas.reference, + transform_matrix, + order=2, + output_shape=bounding_box, + output_chunks=(2, bounding_box[1], bounding_box[2]), + ) - worker = self.compute_atlas_rotation(transformed_atlas) - worker.returned.connect(self.set_atlas_layer_data) - worker.start() - else: - show_error( - "No atlas selected. Please select an atlas before rotating" - ) + self._atlas_annotations_layer.data = dask_affine_transform( + self._atlas.annotation, + transform_matrix, + order=0, + output_shape=bounding_box, + output_chunks=(2, bounding_box[1], bounding_box[2]), + ) + + worker = self.compute_atlas_rotation(self._atlas_data_layer.data) + worker.returned.connect(self.set_atlas_layer_data) + worker.start() @thread_worker def compute_atlas_rotation(self, dask_array: da.Array): @@ -443,17 +472,13 @@ def set_atlas_layer_data(self, new_data): self._viewer.grid.enabled = True def _on_atlas_reset(self): - if self._atlas: - curr_atlas_layer_index = find_layer_index( - self._viewer, self._atlas.atlas_name - ) - - self._viewer.layers[ - curr_atlas_layer_index - ].data = self._atlas.reference - self._viewer.grid.enabled = False - self._viewer.grid.enabled = True - else: + if not self._atlas: show_error( "No atlas selected. Please select an atlas before resetting" ) + return + + self._atlas_data_layer.data = self._atlas.reference + self._atlas_annotations_layer.data = self._atlas.annotation + self._viewer.grid.enabled = False + self._viewer.grid.enabled = True From 4cbd6bee4f778c3659d86761597424628ec8540b Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Tue, 23 Apr 2024 17:32:00 +0100 Subject: [PATCH 24/32] Added tests for atlas rotation --- .../registration_widget.py | 9 +- tests/test_registration_widget.py | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index dca125f..a98f9cb 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -394,10 +394,6 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): ) return - curr_atlas_layer_index = find_layer_index( - self._viewer, self._atlas.atlas_name - ) - # Create the rotation matrix roll_matrix = active_matrix_from_angle(0, np.deg2rad(roll)) pitch_matrix = active_matrix_from_angle(2, np.deg2rad(pitch)) @@ -463,10 +459,7 @@ def compute_atlas_rotation(self, dask_array: da.Array): return computed_array def set_atlas_layer_data(self, new_data): - atlas_layer_index = find_layer_index( - self._viewer, self._atlas.atlas_name - ) - self._viewer.layers[atlas_layer_index].data = new_data + self._atlas_data_layer.data = new_data # Resets the viewer grid to update the grid to the new atlas self._viewer.grid.enabled = False self._viewer.grid.enabled = True diff --git a/tests/test_registration_widget.py b/tests/test_registration_widget.py index 2b74c98..d46a0bb 100644 --- a/tests/test_registration_widget.py +++ b/tests/test_registration_widget.py @@ -1,7 +1,11 @@ import pytest +from pytransform3d.rotations import active_matrix_from_angle +import numpy as np from brainglobe_registration.registration_widget import RegistrationWidget +from brainglobe_registration.utils.utils import calculate_rotated_bounding_box + @pytest.fixture() def registration_widget(make_napari_viewer_with_images): @@ -12,6 +16,17 @@ def registration_widget(make_napari_viewer_with_images): return widget +@pytest.fixture() +def registration_widget_with_example_atlas(make_napari_viewer_with_images): + viewer = make_napari_viewer_with_images + + widget = RegistrationWidget(viewer) + + widget._on_atlas_dropdown_index_changed(2) + + return widget + + def test_registration_widget(make_napari_viewer_with_images): widget = RegistrationWidget(make_napari_viewer_with_images) @@ -132,3 +147,71 @@ def test_scale_moving_image( current_size[0] * y_scale_factor, current_size[1] * x_scale_factor, ) + + +@pytest.mark.parametrize( + "pitch, yaw, roll, expected_shape", + [ + (0, 0, 0, (132, 80, 114)), + (45, 0, 0, (150, 150, 114)), + (0, 45, 0, (174, 80, 174)), + (0, 0, 45, (132, 137, 137)), + ], + +) +def test_on_adjust_atlas_rotation( + registration_widget_with_example_atlas, + pitch, + yaw, + roll, + expected_shape, +): + reg_widget = registration_widget_with_example_atlas + atlas_shape = reg_widget._atlas.reference.shape + + reg_widget._on_adjust_atlas_rotation( + pitch, yaw, roll + ) + + assert reg_widget._atlas_data_layer.data.shape == expected_shape + assert reg_widget._atlas_annotations_layer.data.shape == expected_shape + assert reg_widget._atlas.reference.shape == atlas_shape + + +def test_on_adjust_atlas_rotation_no_atlas( + registration_widget, mocker +): + mocked_show_error = mocker.patch( + "brainglobe_registration.registration_widget.show_error" + ) + registration_widget._on_adjust_atlas_rotation(10, 10, 10) + mocked_show_error.assert_called_once_with( + "No atlas selected. Please select an atlas before rotating" + ) + + +def test_on_atlas_reset( + registration_widget_with_example_atlas +): + reg_widget = registration_widget_with_example_atlas + atlas_shape = reg_widget._atlas.reference.shape + reg_widget._on_adjust_atlas_rotation(10, 10, 10) + + reg_widget._on_atlas_reset() + + assert reg_widget._atlas_data_layer.data.shape == atlas_shape + assert reg_widget._atlas.reference.shape == atlas_shape + assert reg_widget._atlas_annotations_layer.data.shape == atlas_shape + + +def test_on_atlas_reset_no_atlas( + registration_widget, mocker +): + mocked_show_error = mocker.patch( + "brainglobe_registration.registration_widget.show_error" + ) + + registration_widget._on_atlas_reset() + mocked_show_error.assert_called_once_with( + "No atlas selected. Please select an atlas before resetting" + ) From 503f57ee8eec926df21cbd057cba8c17a101ad0c Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Tue, 23 Apr 2024 17:38:12 +0100 Subject: [PATCH 25/32] Fixed pre-commit --- .../registration_widget.py | 38 +++++++++++++------ brainglobe_registration/utils/utils.py | 2 +- tests/test_registration_widget.py | 23 +++-------- tests/test_utils.py | 10 +++-- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index a98f9cb..cc46da2 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -9,14 +9,16 @@ """ from pathlib import Path +from typing import Optional import dask.array as da +import napari.layers import numpy as np +import numpy.typing as npt from bg_atlasapi import BrainGlobeAtlas from bg_atlasapi.list_atlases import get_downloaded_atlases from brainglobe_utils.qtpy.collapsible_widget import CollapsibleWidgetContainer from dask_image.ndinterp import affine_transform as dask_affine_transform -from scipy.ndimage.interpolation import affine_transform from napari.qt.threading import thread_worker from napari.utils.notifications import show_error from napari.viewer import Viewer @@ -54,11 +56,11 @@ def __init__(self, napari_viewer: Viewer): self.setContentsMargins(10, 10, 10, 10) self._viewer = napari_viewer - self._atlas: BrainGlobeAtlas = None - self._atlas_data_layer = None - self._atlas_annotations_layer = None - self._moving_image = None - self._moving_image_data_backup = None + self._atlas: Optional[BrainGlobeAtlas] = None + self._atlas_data_layer: Optional[napari.layers.Image] = None + self._atlas_annotations_layer: Optional[napari.layers.Labels] = None + self._moving_image: Optional[napari.layers.Image] = None + self._moving_image_data_backup: Optional[npt.NDArray] = None self.transform_params: dict[str, dict] = { "rigid": {}, @@ -196,11 +198,19 @@ def _on_atlas_dropdown_index_changed(self, index): self._atlas = BrainGlobeAtlas(atlas_name) atlas_dask_array = da.from_array( self._atlas.reference, - chunks=(1, self._atlas.reference.shape[1], self._atlas.reference.shape[2]), + chunks=( + 1, + self._atlas.reference.shape[1], + self._atlas.reference.shape[2], + ), ) dask_annotations = da.from_array( self._atlas.annotation, - chunks=(1, self._atlas.annotation.shape[1], self._atlas.annotation.shape[2]), + chunks=( + 1, + self._atlas.annotation.shape[1], + self._atlas.annotation.shape[2], + ), ) self._atlas_data_layer = self._viewer.add_image( @@ -229,7 +239,8 @@ def _on_sample_dropdown_index_changed(self, index): def _on_adjust_moving_image(self, x: int, y: int, rotate: float): if not self._moving_image: show_error( - "No moving image selected. Please select a moving image before adjusting" + "No moving image selected. " + "Please select a moving image before adjusting" ) return adjust_napari_image_layer(self._moving_image, x, y, rotate) @@ -237,7 +248,8 @@ def _on_adjust_moving_image(self, x: int, y: int, rotate: float): def _on_adjust_moving_image_reset_button_click(self): if not self._moving_image: show_error( - "No moving image selected. Please select a moving image before adjusting" + "No moving image selected. " + "Please select a moving image before adjusting" ) return adjust_napari_image_layer(self._moving_image, 0, 0, 0) @@ -388,7 +400,11 @@ def _on_scale_moving_image(self, x: float, y: float): ) def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): - if not self._atlas: + if not ( + self._atlas + and self._atlas_data_layer + and self._atlas_annotations_layer + ): show_error( "No atlas selected. Please select an atlas before rotating" ) diff --git a/brainglobe_registration/utils/utils.py b/brainglobe_registration/utils/utils.py index 7c6db59..2bb9891 100644 --- a/brainglobe_registration/utils/utils.py +++ b/brainglobe_registration/utils/utils.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Tuple, Dict +from typing import Dict, List, Tuple import napari import numpy as np diff --git a/tests/test_registration_widget.py b/tests/test_registration_widget.py index d46a0bb..e713305 100644 --- a/tests/test_registration_widget.py +++ b/tests/test_registration_widget.py @@ -1,11 +1,7 @@ import pytest -from pytransform3d.rotations import active_matrix_from_angle -import numpy as np from brainglobe_registration.registration_widget import RegistrationWidget -from brainglobe_registration.utils.utils import calculate_rotated_bounding_box - @pytest.fixture() def registration_widget(make_napari_viewer_with_images): @@ -153,11 +149,10 @@ def test_scale_moving_image( "pitch, yaw, roll, expected_shape", [ (0, 0, 0, (132, 80, 114)), - (45, 0, 0, (150, 150, 114)), + (45, 0, 0, (150, 150, 114)), (0, 45, 0, (174, 80, 174)), (0, 0, 45, (132, 137, 137)), ], - ) def test_on_adjust_atlas_rotation( registration_widget_with_example_atlas, @@ -169,18 +164,14 @@ def test_on_adjust_atlas_rotation( reg_widget = registration_widget_with_example_atlas atlas_shape = reg_widget._atlas.reference.shape - reg_widget._on_adjust_atlas_rotation( - pitch, yaw, roll - ) + reg_widget._on_adjust_atlas_rotation(pitch, yaw, roll) assert reg_widget._atlas_data_layer.data.shape == expected_shape assert reg_widget._atlas_annotations_layer.data.shape == expected_shape assert reg_widget._atlas.reference.shape == atlas_shape -def test_on_adjust_atlas_rotation_no_atlas( - registration_widget, mocker -): +def test_on_adjust_atlas_rotation_no_atlas(registration_widget, mocker): mocked_show_error = mocker.patch( "brainglobe_registration.registration_widget.show_error" ) @@ -190,9 +181,7 @@ def test_on_adjust_atlas_rotation_no_atlas( ) -def test_on_atlas_reset( - registration_widget_with_example_atlas -): +def test_on_atlas_reset(registration_widget_with_example_atlas): reg_widget = registration_widget_with_example_atlas atlas_shape = reg_widget._atlas.reference.shape reg_widget._on_adjust_atlas_rotation(10, 10, 10) @@ -204,9 +193,7 @@ def test_on_atlas_reset( assert reg_widget._atlas_annotations_layer.data.shape == atlas_shape -def test_on_atlas_reset_no_atlas( - registration_widget, mocker -): +def test_on_atlas_reset_no_atlas(registration_widget, mocker): mocked_show_error = mocker.patch( "brainglobe_registration.registration_widget.show_error" ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 35dab9e..89068e7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,10 +7,10 @@ from brainglobe_registration.utils.utils import ( adjust_napari_image_layer, + calculate_rotated_bounding_box, find_layer_index, get_image_layer_names, open_parameter_file, - calculate_rotated_bounding_box, ) @@ -101,7 +101,7 @@ def test_get_image_layer_names(make_napari_viewer_with_images): [ (0, 0, (50, 100, 200)), (0, 90, (50, 200, 100)), - (0, 180, (50, 100, 200)), + (0, 180, (50, 100, 200)), (0, 45, (50, 212, 212)), (1, 0, (50, 100, 200)), (1, 90, (200, 100, 50)), @@ -111,12 +111,14 @@ def test_get_image_layer_names(make_napari_viewer_with_images): (2, 90, (100, 50, 200)), (2, 180, (50, 100, 200)), (2, 45, (106, 106, 200)), - ] + ], ) def test_calculate_rotated_bounding_box(basis, rotation, expected_bounds): image_shape = (50, 100, 200) rotation_matrix = np.eye(4) - rotation_matrix[:-1, :-1] = active_matrix_from_angle(basis, np.deg2rad(rotation)) + rotation_matrix[:-1, :-1] = active_matrix_from_angle( + basis, np.deg2rad(rotation) + ) result_shape = calculate_rotated_bounding_box(image_shape, rotation_matrix) From 01db43b93557c3d9343d1f414de015d54132df17 Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Thu, 27 Jun 2024 16:21:30 +0100 Subject: [PATCH 26/32] Add test for reset_atlas in adjust_moving_image_view --- tests/test_adjust_moving_image_view.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_adjust_moving_image_view.py b/tests/test_adjust_moving_image_view.py index 8c4a2d8..399b876 100644 --- a/tests/test_adjust_moving_image_view.py +++ b/tests/test_adjust_moving_image_view.py @@ -141,3 +141,21 @@ def test_atlas_rotation_changed( adjust_moving_image_view.adjust_atlas_rotation.click() assert blocker.args == [10, 20, 30] + + +def test_atlas_reset_button_click( + qtbot, + adjust_moving_image_view, +): + qtbot.addWidget(adjust_moving_image_view) + + with qtbot.waitSignal( + adjust_moving_image_view.atlas_reset_signal, timeout=1000 + ): + adjust_moving_image_view.atlas_reset_button.click() + + assert ( + adjust_moving_image_view.adjust_atlas_pitch.value() == 0 + and adjust_moving_image_view.adjust_atlas_yaw.value() == 0 + and adjust_moving_image_view.adjust_atlas_roll.value() == 0 + ) From e675ca01336ae694ae2e131f4aa6d713128d76eb Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Thu, 27 Jun 2024 16:48:51 +0100 Subject: [PATCH 27/32] Add caching --- .github/workflows/test_and_deploy.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index c26cbd4..de1d2d5 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -47,6 +47,23 @@ jobs: with: qt: true + # cache atlases needed by the tests + - name: Cache Atlases + id: atlas-cache + uses: actions/cache@v3 + with: + path: | # ensure we don't cache any interrupted atlas download and extraction! + ~/.brainglobe/* + !~/.brainglobe/atlas.tar.gz + key: ${{ runner.os }}-cached-atlases + enableCrossOsArchive: false # ~ and $HOME evaluate to different places across OSs! + + - if: ${{ steps.atlas-cache.outputs.cache-hit == 'true' }} + name: List files in brainglobe data folder # good to be able to sanity check that user data is as expected + run: | + ls -af ~/.brainglobe/ + + # Run tests - uses: neuroinformatics-unit/actions/test@v2 with: From a44410a670bd1857ca132d1d5f6fecd3ab0a21ff Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Thu, 27 Jun 2024 16:49:18 +0100 Subject: [PATCH 28/32] Update the function calls in test_adjust_moving_image_view --- tests/test_adjust_moving_image_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_adjust_moving_image_view.py b/tests/test_adjust_moving_image_view.py index 399b876..60f3a74 100644 --- a/tests/test_adjust_moving_image_view.py +++ b/tests/test_adjust_moving_image_view.py @@ -150,9 +150,9 @@ def test_atlas_reset_button_click( qtbot.addWidget(adjust_moving_image_view) with qtbot.waitSignal( - adjust_moving_image_view.atlas_reset_signal, timeout=1000 + adjust_moving_image_view.reset_atlas_signal, timeout=1000 ): - adjust_moving_image_view.atlas_reset_button.click() + adjust_moving_image_view.reset_atlas_button.click() assert ( adjust_moving_image_view.adjust_atlas_pitch.value() == 0 From 816310120019a33c819ab9923d5eacae9a56c5ec Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov <61896994+IgorTatarnikov@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:52:50 +0100 Subject: [PATCH 29/32] Apply suggestions from code review Co-authored-by: Alessandro Felder --- brainglobe_registration/registration_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 3afda7f..212a3c6 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -437,7 +437,7 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): ) new_translation = np.asarray(bounding_box) // 2 post_rotate_translation = np.eye(4) - post_rotate_translation[:3, -1] = new_translation + post_rotate_translation[:3, -1] = -new_translation # Combine the matrices. The order of operations is: # 1. Translate the origin to the center of the image @@ -446,7 +446,7 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): transform_matrix = ( translate_matrix @ full_matrix - @ np.linalg.inv(post_rotate_translation) + @ post_rotate_translation ) self._atlas_data_layer.data = dask_affine_transform( From de3a2812c0907dc7f80494a626b436977294fb29 Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Fri, 26 Jul 2024 12:53:33 +0100 Subject: [PATCH 30/32] Applied suggestions from code review --- .../registration_widget.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 3afda7f..35016fd 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -204,7 +204,7 @@ def _on_atlas_dropdown_index_changed(self, index): self.run_button.setEnabled(True) self._atlas = BrainGlobeAtlas(atlas_name) - atlas_dask_array = da.from_array( + dask_reference = da.from_array( self._atlas.reference, chunks=( 1, @@ -221,12 +221,15 @@ def _on_atlas_dropdown_index_changed(self, index): ), ) + contrast_max = np.max( + dask_reference[dask_reference.shape[0] // 2] + ).compute() self._atlas_data_layer = self._viewer.add_image( - atlas_dask_array, + dask_reference, name=atlas_name, colormap="gray", blending="translucent", - contrast_limits=[0, 350], + contrast_limits=[0, contrast_max], multiscale=False, ) self._atlas_annotations_layer = self._viewer.add_labels( @@ -437,16 +440,14 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): ) new_translation = np.asarray(bounding_box) // 2 post_rotate_translation = np.eye(4) - post_rotate_translation[:3, -1] = new_translation + post_rotate_translation[:3, -1] = -new_translation # Combine the matrices. The order of operations is: # 1. Translate the origin to the center of the image # 2. Rotate the image # 3. Translate the origin back to the top left corner transform_matrix = ( - translate_matrix - @ full_matrix - @ np.linalg.inv(post_rotate_translation) + translate_matrix @ full_matrix @ post_rotate_translation ) self._atlas_data_layer.data = dask_affine_transform( @@ -465,6 +466,9 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): output_chunks=(2, bounding_box[1], bounding_box[2]), ) + # Resets the viewer grid to update the grid to the new atlas + self._viewer.reset_view() + worker = self.compute_atlas_rotation(self._atlas_data_layer.data) worker.returned.connect(self.set_atlas_layer_data) worker.start() @@ -483,9 +487,6 @@ def compute_atlas_rotation(self, dask_array: da.Array): def set_atlas_layer_data(self, new_data): self._atlas_data_layer.data = new_data - # Resets the viewer grid to update the grid to the new atlas - self._viewer.grid.enabled = False - self._viewer.grid.enabled = True def _on_atlas_reset(self): if not self._atlas: From 52a640f9837f8853ed27b908df6e795ca3f5179d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:54:29 +0000 Subject: [PATCH 31/32] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- brainglobe_registration/registration_widget.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/brainglobe_registration/registration_widget.py b/brainglobe_registration/registration_widget.py index 212a3c6..29d8541 100644 --- a/brainglobe_registration/registration_widget.py +++ b/brainglobe_registration/registration_widget.py @@ -444,9 +444,7 @@ def _on_adjust_atlas_rotation(self, pitch: float, yaw: float, roll: float): # 2. Rotate the image # 3. Translate the origin back to the top left corner transform_matrix = ( - translate_matrix - @ full_matrix - @ post_rotate_translation + translate_matrix @ full_matrix @ post_rotate_translation ) self._atlas_data_layer.data = dask_affine_transform( From 680848b382d29ca4f7877924096830c264ecf1f5 Mon Sep 17 00:00:00 2001 From: Igor Tatarnikov Date: Fri, 26 Jul 2024 12:55:16 +0100 Subject: [PATCH 32/32] Applied suggestions from code review --- .../widgets/adjust_moving_image_view.py | 8 ++++---- tests/test_adjust_moving_image_view.py | 4 ++-- tests/test_registration_widget.py | 14 +++++++++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/brainglobe_registration/widgets/adjust_moving_image_view.py b/brainglobe_registration/widgets/adjust_moving_image_view.py index 82d51bb..111951d 100644 --- a/brainglobe_registration/widgets/adjust_moving_image_view.py +++ b/brainglobe_registration/widgets/adjust_moving_image_view.py @@ -65,11 +65,11 @@ def __init__(self, parent=None): rotation_range = 360 self.adjust_moving_image_pixel_size_x = QDoubleSpinBox(parent=self) - self.adjust_moving_image_pixel_size_x.setDecimals(2) - self.adjust_moving_image_pixel_size_x.setRange(0.01, 100.00) + self.adjust_moving_image_pixel_size_x.setDecimals(3) + self.adjust_moving_image_pixel_size_x.setRange(0.001, 100.00) self.adjust_moving_image_pixel_size_y = QDoubleSpinBox(parent=self) - self.adjust_moving_image_pixel_size_y.setDecimals(2) - self.adjust_moving_image_pixel_size_y.setRange(0.01, 100.00) + self.adjust_moving_image_pixel_size_y.setDecimals(3) + self.adjust_moving_image_pixel_size_y.setRange(0.001, 100.00) self.scale_moving_image_button = QPushButton() self.scale_moving_image_button.setText("Scale Image") self.scale_moving_image_button.clicked.connect( diff --git a/tests/test_adjust_moving_image_view.py b/tests/test_adjust_moving_image_view.py index 60f3a74..8eae56c 100644 --- a/tests/test_adjust_moving_image_view.py +++ b/tests/test_adjust_moving_image_view.py @@ -104,7 +104,7 @@ def test_reset_image_button_click(qtbot, adjust_moving_image_view): @pytest.mark.parametrize( "x_scale, y_scale", - [(2.5, 2.5), (10, 20), (10.221, 10.228)], + [(2.5, 2.5), (10, 20), (10.2212, 10.2289)], ) def test_scale_image_button_click( qtbot, adjust_moving_image_view, x_scale, y_scale @@ -122,7 +122,7 @@ def test_scale_image_button_click( ) adjust_moving_image_view.scale_moving_image_button.click() - assert blocker.args == [round(x_scale, 2), round(y_scale, 2)] + assert blocker.args == [round(x_scale, 3), round(y_scale, 3)] def test_atlas_rotation_changed( diff --git a/tests/test_registration_widget.py b/tests/test_registration_widget.py index e713305..828c4aa 100644 --- a/tests/test_registration_widget.py +++ b/tests/test_registration_widget.py @@ -14,11 +14,23 @@ def registration_widget(make_napari_viewer_with_images): @pytest.fixture() def registration_widget_with_example_atlas(make_napari_viewer_with_images): + """ + Create an initialised RegistrationWidget with the "example_mouse_100um" + loaded. + + Parameters + ------------ + make_napari_viewer_with_images for testing + Fixture that creates a napari viewer + """ viewer = make_napari_viewer_with_images widget = RegistrationWidget(viewer) - widget._on_atlas_dropdown_index_changed(2) + # Based on the downloaded atlases by the fixture in conftest.py, + # the example atlas will be in the third position. + example_atlas_index = 2 + widget._on_atlas_dropdown_index_changed(example_atlas_index) return widget