Skip to content

Commit

Permalink
Add option to scale sample image (#26)
Browse files Browse the repository at this point in the history
* Fixed contents margins

* Scaling of sample image added to adjust moving image dropdown

* Added brainglobe-utils as dependency

* Added error message when no images selected

* Added tests for scaling

* Added tests to check multiple scale_factors

* Cache the moving image data to allow scaling image multiple times

* Fixed tests by removing (0,0) case add guard in _on_scale_image in registration_widget.py

* Fixed tests

* Add docstring to _on_scale_moving_image

* Replaced all curr_ variable names with current_

* Added units to pixel size selection, set max range to be 100
  • Loading branch information
IgorTatarnikov authored Feb 5, 2024
1 parent fdec546 commit a09d4e6
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 25 deletions.
54 changes: 49 additions & 5 deletions brainglobe_registration/registration_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
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,
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
Expand Down Expand Up @@ -49,6 +51,7 @@ def __init__(self, napari_viewer: Viewer):
self._viewer = napari_viewer
self._atlas: BrainGlobeAtlas = None
self._moving_image = None
self._moving_image_data_backup = None

self.transform_params: dict[str, dict] = {
"rigid": {},
Expand Down Expand Up @@ -101,10 +104,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(
Expand Down Expand Up @@ -151,11 +156,11 @@ def _on_atlas_dropdown_index_changed(self, index):
# Hacky way of having an empty first dropdown
if index == 0:
if self._atlas:
curr_atlas_layer_index = find_layer_index(
current_atlas_layer_index = find_layer_index(
self._viewer, self._atlas.atlas_name
)

self._viewer.layers.pop(curr_atlas_layer_index)
self._viewer.layers.pop(current_atlas_layer_index)
self._atlas = None
self.run_button.setEnabled(False)
self._viewer.grid.enabled = False
Expand All @@ -166,11 +171,11 @@ def _on_atlas_dropdown_index_changed(self, index):
atlas = BrainGlobeAtlas(atlas_name)

if self._atlas:
curr_atlas_layer_index = find_layer_index(
current_atlas_layer_index = find_layer_index(
self._viewer, self._atlas.atlas_name
)

self._viewer.layers.pop(curr_atlas_layer_index)
self._viewer.layers.pop(current_atlas_layer_index)
else:
self.run_button.setEnabled(True)

Expand All @@ -189,6 +194,7 @@ def _on_sample_dropdown_index_changed(self, index):
self._viewer, self._sample_images[index]
)
self._moving_image = self._viewer.layers[viewer_index]
self._moving_image_data_backup = self._moving_image.data.copy()

def _on_adjust_moving_image(self, x: int, y: int, rotate: float):
adjust_napari_image_layer(self._moving_image, x, y, rotate)
Expand Down Expand Up @@ -299,3 +305,41 @@ 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):
"""
Scale the moving image to have resolution equal to the atlas.
Parameters
----------
x : float
Moving image x pixel size (> 0.0).
y : float
Moving image y pixel size (> 0.0).
Will show an error if the pixel sizes are less than or equal to 0.
Will show an error if the moving image or atlas is not selected.
"""
if x <= 0 or y <= 0:
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:
show_error(
"Sample image or atlas not selected. "
"Please select a sample image and atlas before scaling",
)
53 changes: 44 additions & 9 deletions brainglobe_registration/widgets/adjust_moving_image_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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):
Expand All @@ -54,15 +57,27 @@ 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_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.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
)
Expand All @@ -71,15 +86,26 @@ 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("X offset:", self.adjust_moving_image_x)
self.layout().addRow("Y offset:", self.adjust_moving_image_y)
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.layout().addRow(
"Sample image Y pixel size (\u03BCm / pixel):",
self.adjust_moving_image_voxel_size_y,
)
self.layout().addRow(self.scale_moving_image_button)

self.layout().addRow(QLabel("Adjust the sample image position:"))
self.layout().addRow("X offset (pixels):", self.adjust_moving_image_x)
self.layout().addRow("Y offset (pixels):", self.adjust_moving_image_y)
self.layout().addRow(
"Rotation (degrees):", self.adjust_moving_image_rotate
)
Expand All @@ -106,3 +132,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(),
)
12 changes: 7 additions & 5 deletions brainglobe_registration/widgets/transform_select_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def _on_transform_type_change(self, index):
self.file_selections[index].setCurrentIndex(0)

if index >= len(self.transform_type_selections) - 1:
curr_length = self.rowCount()
current_length = self.rowCount()
self.setRowCount(self.rowCount() + 1)

self.transform_type_selections.append(QComboBox())
Expand All @@ -180,7 +180,7 @@ def _on_transform_type_change(self, index):
self.transform_type_signaller.map
)
self.transform_type_signaller.setMapping(
self.transform_type_selections[-1], curr_length
self.transform_type_selections[-1], current_length
)

self.file_selections.append(QComboBox())
Expand All @@ -191,14 +191,16 @@ def _on_transform_type_change(self, index):
self.file_signaller.map
)
self.file_signaller.setMapping(
self.file_selections[-1], curr_length
self.file_selections[-1], current_length
)

self.setCellWidget(
curr_length, 0, self.transform_type_selections[curr_length]
current_length,
0,
self.transform_type_selections[current_length],
)
self.setCellWidget(
curr_length, 1, self.file_selections[curr_length]
current_length, 1, self.file_selections[current_length]
)

else:
Expand Down
29 changes: 29 additions & 0 deletions tests/test_adjust_moving_image_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ def adjust_moving_image_view() -> AdjustMovingImageView:
return adjust_moving_image_view


def test_init(qtbot, adjust_moving_image_view):
qtbot.addWidget(adjust_moving_image_view)

assert adjust_moving_image_view.layout().rowCount() == 9


@pytest.mark.parametrize(
"x_value, expected",
[
Expand Down Expand Up @@ -94,3 +100,26 @@ def test_reset_image_button_click(qtbot, adjust_moving_image_view):
assert adjust_moving_image_view.adjust_moving_image_x.value() == 0
assert adjust_moving_image_view.adjust_moving_image_y.value() == 0
assert adjust_moving_image_view.adjust_moving_image_rotate.value() == 0


@pytest.mark.parametrize(
"x_scale, y_scale",
[(2.5, 2.5), (10, 20), (10.221, 10.228)],
)
def test_scale_image_button_click(
qtbot, adjust_moving_image_view, x_scale, y_scale
):
qtbot.addWidget(adjust_moving_image_view)

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(
x_scale
)
adjust_moving_image_view.adjust_moving_image_voxel_size_y.setValue(
y_scale
)
adjust_moving_image_view.scale_moving_image_button.click()

assert blocker.args == [round(x_scale, 2), round(y_scale, 2)]
12 changes: 6 additions & 6 deletions tests/test_parameter_list_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_parameter_list_view_cell_change(parameter_list_view, qtbot):
def test_parameter_list_view_cell_change_last_row(parameter_list_view, qtbot):
qtbot.addWidget(parameter_list_view)

curr_row_count = parameter_list_view.rowCount()
current_row_count = parameter_list_view.rowCount()
last_row_index = len(param_dict)

parameter_list_view.setItem(
Expand All @@ -65,32 +65,32 @@ def test_parameter_list_view_cell_change_last_row(parameter_list_view, qtbot):
parameter_list_view.setItem(last_row_index, 1, QTableWidgetItem("true"))

assert parameter_list_view.param_dict["TestParameter"] == ["true"]
assert parameter_list_view.rowCount() == curr_row_count + 1
assert parameter_list_view.rowCount() == current_row_count + 1


def test_parameter_list_view_cell_change_last_row_no_param(
parameter_list_view, qtbot
):
qtbot.addWidget(parameter_list_view)

curr_row_count = parameter_list_view.rowCount()
current_row_count = parameter_list_view.rowCount()
last_row_index = len(param_dict)

parameter_list_view.setItem(last_row_index, 1, QTableWidgetItem("true"))

assert parameter_list_view.rowCount() == curr_row_count
assert parameter_list_view.rowCount() == current_row_count


def test_parameter_list_view_cell_change_last_row_no_value(
parameter_list_view, qtbot
):
qtbot.addWidget(parameter_list_view)

curr_row_count = parameter_list_view.rowCount()
current_row_count = parameter_list_view.rowCount()
last_row_index = len(param_dict)

parameter_list_view.setItem(
last_row_index, 0, QTableWidgetItem("TestParameter")
)

assert parameter_list_view.rowCount() == curr_row_count
assert parameter_list_view.rowCount() == current_row_count
Loading

0 comments on commit a09d4e6

Please sign in to comment.