From 4ccb5afbb34bf918641a35ec5549072e2e04b8a1 Mon Sep 17 00:00:00 2001 From: James Ryan Date: Mon, 30 May 2022 12:28:00 +1000 Subject: [PATCH 01/26] add initial interpolation widget --- src/zarpaint/__init__.py | 2 + src/zarpaint/_interpolate_labels.py | 79 +++++++++++++++++++++++++++ src/zarpaint/_tests/test_watershed.py | 1 - src/zarpaint/napari.yaml | 5 ++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/zarpaint/_interpolate_labels.py diff --git a/src/zarpaint/__init__.py b/src/zarpaint/__init__.py index 90821a2..1262eee 100644 --- a/src/zarpaint/__init__.py +++ b/src/zarpaint/__init__.py @@ -9,6 +9,7 @@ from ._add_3d_points import add_points_3d_with_alt_click from .reader import zarr_tensorstore from ._copy_data import copy_data +from ._interpolate_labels import interpolate_between_slices __all__ = [ 'create_labels', @@ -19,4 +20,5 @@ 'add_points_3d_with_alt_click', 'zarr_tensorstore' 'copy_data', + 'interpolate_between_slices', ] diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py new file mode 100644 index 0000000..b095726 --- /dev/null +++ b/src/zarpaint/_interpolate_labels.py @@ -0,0 +1,79 @@ +from cProfile import label +from magicgui import magic_factory +import numpy as np +from scipy.interpolate import interpn +from scipy.ndimage import distance_transform_edt + + +def distance_transform(image): + """Distance transform for a boolean image. + + Returns positive values inside the object, + and negative values outside. + """ + image = image.astype(bool) + edt = distance_transform_edt(image) - distance_transform_edt(~image) + return edt + + +def point_and_values(image_1, image_2, interp_dim=0): + edt_1 = distance_transform(image_1) + edt_2 = distance_transform(image_2) + values = np.stack([edt_1, edt_2], axis=interp_dim) + points = tuple([np.arange(i) for i in values.shape]) + return points, values + + +def xi_coords(shape, percent=0.5): + slices = [slice(0, i) for i in shape] + xi = np.moveaxis(np.mgrid[slices], 0, -1).reshape(np.prod(shape), len(shape)) + xi = xi = np.c_[np.full((np.prod(shape)), percent), xi] + return xi + + +def slice_iterator(slice_index_1, slice_index_2): + intermediate_slices = np.arange(slice_index_1 + 1, slice_index_2) + n_slices = slice_index_2 - slice_index_1 + 1 # inclusive + stepsize = 1 / n_slices + intermediate_percentages = np.arange(0 + stepsize, 1, stepsize) + return zip(intermediate_slices, intermediate_percentages) + + +def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): + # Find the original image shape + img_shape = list(values.shape) + del img_shape[interp_dim] + # Calculate the interpolated slice + xi = xi_coords(img_shape, percent=percent) + interpolated_img = interpn(points, values, xi, method=method) + interpolated_img = np.reshape(interpolated_img, img_shape) > 0 + return interpolated_img + + +## Second draft, writes directly into tensorstore zarr array +@magic_factory( + call_button='Interpolate' + ) +def interpolate_between_slices(label_layer: "napari.layers.Labels", slice_index_1: int, slice_index_2: int, label_id: int =1, interp_dim: int =0): + if slice_index_1 > slice_index_2: + slice_index_1, slice_index_2 = slice_index_2, slice_index_1 + layer_data = np.asarray(label_layer.data) + slice_1 = np.take(layer_data, slice_index_1, axis=interp_dim) + slice_2 = np.take(layer_data, slice_index_2, axis=interp_dim) + # slice_1 = np.asarray(label_layer.data[slice_index_1]) + # slice_2 = np.asarray(label_layer.data[slice_index_2]) + + #TODO: possible extension, handle all label ids separately + slice_1 = slice_1.astype(bool) + slice_2 = slice_2.astype(bool) + # interp_dim should just be the slider "dimension" right? + points, values = point_and_values(slice_1, slice_2, interp_dim) + #TODO: Thread this? + for slice_number, percentage in slice_iterator(slice_index_1, slice_index_2): + interpolated_img = interpolated_slice(percentage, points, values, interp_dim=interp_dim, method='linear') + label_layer.data[slice_number, interpolated_img] = label_id + label_layer.refresh() # will update the current view + +# interpolate_between_slices(label_layer, image_1, image_2, slice_index_1, slice_index_2) +# print("Done!") +# print("Please scroll through napari to see the interpolated label slices") diff --git a/src/zarpaint/_tests/test_watershed.py b/src/zarpaint/_tests/test_watershed.py index 8119c33..29e2ed3 100644 --- a/src/zarpaint/_tests/test_watershed.py +++ b/src/zarpaint/_tests/test_watershed.py @@ -9,7 +9,6 @@ def test_watershed_split_2d(make_napari_viewer): # create 2 squares with one corner overlapping data[1:10, 1:10] = 1 data[8:17, 8:17] = 1 - print(data) # palce points in the centre of the 2 squares points = np.asarray([[5, 5], [12, 12]]) diff --git a/src/zarpaint/napari.yaml b/src/zarpaint/napari.yaml index 89ce8bc..aa06c58 100644 --- a/src/zarpaint/napari.yaml +++ b/src/zarpaint/napari.yaml @@ -23,6 +23,9 @@ contributions: - id: zarpaint.copy_data title: Copy Data… python_name: zarpaint:copy_data + - id: zarpaint.interpolate_labels + title: Interpolate Labels… + python_name: zarpaint:interpolate_between_slices readers: - command: zarpaint.get_reader @@ -42,3 +45,5 @@ contributions: display_name: Split With Watershed - command: zarpaint.copy_data display_name: Copy Data + - command: zarpaint.interpolate_labels + display_name: Interpolate Labels From b3a04afb2ed6763683bdd205a267756c1bc0fba1 Mon Sep 17 00:00:00 2001 From: James Ryan Date: Thu, 2 Jun 2022 15:07:31 +1000 Subject: [PATCH 02/26] incorporated changes to avoid interp_dim error --- src/zarpaint/_interpolate_labels.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index b095726..24ae44b 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -24,10 +24,10 @@ def point_and_values(image_1, image_2, interp_dim=0): return points, values -def xi_coords(shape, percent=0.5): +def xi_coords(shape, percent=0.5, interp_dim=0): slices = [slice(0, i) for i in shape] - xi = np.moveaxis(np.mgrid[slices], 0, -1).reshape(np.prod(shape), len(shape)) - xi = xi = np.c_[np.full((np.prod(shape)), percent), xi] + xi = np.moveaxis(np.mgrid[slices], 0, -1).reshape(np.prod(shape), len(shape)).astype('float') + xi = np.insert(xi, interp_dim, percent, axis=1) return xi @@ -44,7 +44,7 @@ def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): img_shape = list(values.shape) del img_shape[interp_dim] # Calculate the interpolated slice - xi = xi_coords(img_shape, percent=percent) + xi = xi_coords(img_shape, percent=percent, interp_dim=interp_dim) interpolated_img = interpn(points, values, xi, method=method) interpolated_img = np.reshape(interpolated_img, img_shape) > 0 return interpolated_img @@ -71,7 +71,10 @@ def interpolate_between_slices(label_layer: "napari.layers.Labels", slice_index_ #TODO: Thread this? for slice_number, percentage in slice_iterator(slice_index_1, slice_index_2): interpolated_img = interpolated_slice(percentage, points, values, interp_dim=interp_dim, method='linear') - label_layer.data[slice_number, interpolated_img] = label_id + indices = [slice(None) for _ in range(label_layer.data.ndim)] + indices[interp_dim] = slice_number + indices = tuple(indices) + label_layer.data[indices][interpolated_img] = label_id label_layer.refresh() # will update the current view # interpolate_between_slices(label_layer, image_1, image_2, slice_index_1, slice_index_2) From 46dceb1393c70e5bb744a0491f5d35ba20ab1e45 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Thu, 16 Jun 2022 11:10:09 +1000 Subject: [PATCH 03/26] basic container widget added --- src/zarpaint/_interpolate_labels.py | 56 +++++++++++++++++++++++++++-- src/zarpaint/napari.yaml | 5 +++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 24ae44b..16beeda 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -1,5 +1,8 @@ from cProfile import label +from typing import Sequence from magicgui import magic_factory +from magicgui.widgets import Container, ComboBox +import napari import numpy as np from scipy.interpolate import interpn from scipy.ndimage import distance_transform_edt @@ -48,13 +51,62 @@ def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): interpolated_img = interpn(points, values, xi, method=method) interpolated_img = np.reshape(interpolated_img, img_shape) > 0 return interpolated_img - + +# 1. Create labels and widget for each parameter to interpolate_between_slices, and add them to form layout +# 2. Add run button and connect callback that just prints something +# 3. Set initial options and boundaries for different widgets e.g. min/max for slice_index +# 4. Update button callback to actually do the thing +class InterpolateSliceWidget(Container): + + def __init__(self, viewer: "napari.viewer.Viewer"): + super().__init__() + self.viewer = viewer + self.labels_combo = ComboBox(name='Labels Layer', choices=self.get_labels_layers) + self.append(self.labels_combo) + + def get_labels_layers(self, combo): + return [layer for layer in self.viewer.layers if isinstance(layer, napari.layers.Labels)] + + + + # def __init__(self, viewer: 'napari.viewer.Viewer') -> None: + # super().__init__() + # self.layer_label = "Labels Layer: " + # self.layer_combo = QComboBox() + # viewer.layers.inserted.conecct(self.reset_layer_options) + # viewer.layers.removed.connect(self.reset_layer_options) + + # self.layer_combo.currentText.connect(self.set_default_label_id) + + + # my_layour = QFormLayout() + # my_layour.addRow(layer_label, layer_combo) + + + # self.setLayout(my_layour) + + +def on_init(interpolate_widget): + """called each time widget_factory creates a new widget.""" + + # at best, this would work just when you first start the widget + interpolate_widget.label_id = interpolate_widget.label_layer.selected_label + + @interpolate_widget.label_layer.selection_changed.connect + def set_default_label_id(event): + print('YOu just selected a new labels layer: ', event.value) + interpolate_widget.label_id.value = interpolate_widget.viewer.layers[event.value].selsected_label + + ## Second draft, writes directly into tensorstore zarr array @magic_factory( call_button='Interpolate' + # widget_init=on_init ) -def interpolate_between_slices(label_layer: "napari.layers.Labels", slice_index_1: int, slice_index_2: int, label_id: int =1, interp_dim: int =0): +def interpolate_between_slices(viewer: "napari.viewer.Viewer", label_layer: "napari.layers.Labels", slice_index_1: int, slice_index_2: int, label_id: int =1, interp_dim: int =0): + print(viewer.layers) + print() if slice_index_1 > slice_index_2: slice_index_1, slice_index_2 = slice_index_2, slice_index_1 layer_data = np.asarray(label_layer.data) diff --git a/src/zarpaint/napari.yaml b/src/zarpaint/napari.yaml index aa06c58..7eb4dc1 100644 --- a/src/zarpaint/napari.yaml +++ b/src/zarpaint/napari.yaml @@ -26,6 +26,9 @@ contributions: - id: zarpaint.interpolate_labels title: Interpolate Labels… python_name: zarpaint:interpolate_between_slices + - id: zarpaint.interpolate_widg + title: Interpolate + python_name: zarpaint._interpolate_labels:InterpolateSliceWidget readers: - command: zarpaint.get_reader @@ -47,3 +50,5 @@ contributions: display_name: Copy Data - command: zarpaint.interpolate_labels display_name: Interpolate Labels + - command: zarpaint.interpolate_widg + display_name: Interpolate Slices From d11ad4bc157ec83c1ee7ac030e7f70caedc60720 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Mon, 27 Jun 2022 10:42:32 +1000 Subject: [PATCH 04/26] rough proof of concept for not asking user for input --- src/zarpaint/_interpolate_labels.py | 99 +++++++++++++++-------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 16beeda..b7137f3 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -1,7 +1,4 @@ -from cProfile import label -from typing import Sequence -from magicgui import magic_factory -from magicgui.widgets import Container, ComboBox +from magicgui.widgets import Container, ComboBox, PushButton import napari import numpy as np from scipy.interpolate import interpn @@ -43,92 +40,98 @@ def slice_iterator(slice_index_1, slice_index_2): def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): - # Find the original image shape img_shape = list(values.shape) del img_shape[interp_dim] - # Calculate the interpolated slice + xi = xi_coords(img_shape, percent=percent, interp_dim=interp_dim) interpolated_img = interpn(points, values, xi, method=method) interpolated_img = np.reshape(interpolated_img, img_shape) > 0 return interpolated_img -# 1. Create labels and widget for each parameter to interpolate_between_slices, and add them to form layout -# 2. Add run button and connect callback that just prints something -# 3. Set initial options and boundaries for different widgets e.g. min/max for slice_index -# 4. Update button callback to actually do the thing class InterpolateSliceWidget(Container): def __init__(self, viewer: "napari.viewer.Viewer"): super().__init__() self.viewer = viewer self.labels_combo = ComboBox(name='Labels Layer', choices=self.get_labels_layers) - self.append(self.labels_combo) + self.start_interpolation = PushButton(name='Start Interpolation') + self.interpolate_btn = PushButton(name='Interpolate') + self.start_interpolation.clicked.connect(self.enter_interpolation) + self.extend([self.labels_combo, self.start_interpolation, self.interpolate_btn]) + self.interpolate_btn.hide() def get_labels_layers(self, combo): return [layer for layer in self.viewer.layers if isinstance(layer, napari.layers.Labels)] + def enter_interpolation(self, event): + # TODO: we wanna connect some callbacks that track for us the painted labels + # grey out the combo box + self.selected_layer = self.viewer.layers[self.labels_combo.current_choice] + self.start_interpolation.hide() + self.interpolate_btn.show() + self.interpolate_btn.clicked.connect(self.interpolate) + self.viewer.dims.events.current_step.connect(self.remember_slices) + self.painted_slices = [] + + # TODO: for multiple slices nut we need more logic to determine which slices actualy got pained on + # not currently used + def remember_slices(self, event): + self.painted_slices.append(event.value) - # def __init__(self, viewer: 'napari.viewer.Viewer') -> None: - # super().__init__() - # self.layer_label = "Labels Layer: " - # self.layer_combo = QComboBox() - # viewer.layers.inserted.conecct(self.reset_layer_options) - # viewer.layers.removed.connect(self.reset_layer_options) - # self.layer_combo.currentText.connect(self.set_default_label_id) - - # my_layour = QFormLayout() - # my_layour.addRow(layer_label, layer_combo) + def interpolate(self, event): + # TODO: accessing private attribute, need paint event to not do that + last_two_labels = [self.selected_layer._undo_history[-1], self.selected_layer._undo_history[-2]] + last_label_history_item = last_two_labels[0] # this is a list of tuples + last_label_coords = last_label_history_item[0][0] # first history atom is a tuple, first element of atom is coords + # here we can determine both the slice index that was painted *and* the interp dim + # it wil be the array in last_label_coords that has only one unique element in it + # the interp dim will be the index of that array in the tuple + unique_coords = list(map(np.unique, last_label_coords)) + + # TODO: make this a func + interp_dim = 0 + for i in range(len(unique_coords)): + if len(unique_coords[i]) == 1: + interp_dim = i + break + last_slice_painted = unique_coords[interp_dim][0] - # self.setLayout(my_layour) + second_last_label_history_item = last_two_labels[1] + second_last_label_coords = second_last_label_history_item[0][0] + # TODO: use interp_dim/last slice func and raise if you get a different interp dim + second_last_slice_painted = second_last_label_coords[0][0] + label_id = last_label_history_item[-1][-1] -def on_init(interpolate_widget): - """called each time widget_factory creates a new widget.""" + interpolate_between_slices(self.selected_layer, second_last_slice_painted, last_slice_painted, label_id,interp_dim) - # at best, this would work just when you first start the widget - interpolate_widget.label_id = interpolate_widget.label_layer.selected_label + # TODO: multiple slices, multiple labels, stitching history items so we don't have to pass in the whole layer + # TODO: switch the button back out - @interpolate_widget.label_layer.selection_changed.connect - def set_default_label_id(event): - print('YOu just selected a new labels layer: ', event.value) - interpolate_widget.label_id.value = interpolate_widget.viewer.layers[event.value].selsected_label +def interpolate_between_slices(label_layer: "napari.layers.Labels", slice_index_1: int, slice_index_2: int, label_id: int =1, interp_dim: int =0): -## Second draft, writes directly into tensorstore zarr array -@magic_factory( - call_button='Interpolate' - # widget_init=on_init - ) -def interpolate_between_slices(viewer: "napari.viewer.Viewer", label_layer: "napari.layers.Labels", slice_index_1: int, slice_index_2: int, label_id: int =1, interp_dim: int =0): - print(viewer.layers) - print() if slice_index_1 > slice_index_2: slice_index_1, slice_index_2 = slice_index_2, slice_index_1 layer_data = np.asarray(label_layer.data) slice_1 = np.take(layer_data, slice_index_1, axis=interp_dim) slice_2 = np.take(layer_data, slice_index_2, axis=interp_dim) - # slice_1 = np.asarray(label_layer.data[slice_index_1]) - # slice_2 = np.asarray(label_layer.data[slice_index_2]) - #TODO: possible extension, handle all label ids separately slice_1 = slice_1.astype(bool) slice_2 = slice_2.astype(bool) - # interp_dim should just be the slider "dimension" right? + points, values = point_and_values(slice_1, slice_2, interp_dim) - #TODO: Thread this? + + # TODO: Thread this for slice_number, percentage in slice_iterator(slice_index_1, slice_index_2): interpolated_img = interpolated_slice(percentage, points, values, interp_dim=interp_dim, method='linear') indices = [slice(None) for _ in range(label_layer.data.ndim)] indices[interp_dim] = slice_number indices = tuple(indices) - label_layer.data[indices][interpolated_img] = label_id + label_layer.data[indices][interpolated_img] = label_id #use labels data_setitem (from the paint event PR) label_layer.refresh() # will update the current view - -# interpolate_between_slices(label_layer, image_1, image_2, slice_index_1, slice_index_2) -# print("Done!") -# print("Please scroll through napari to see the interpolated label slices") From 8f084f2a792604f780f393eaeec481b0854bc863 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Mon, 27 Jun 2022 10:45:45 +1000 Subject: [PATCH 05/26] yapf --- src/zarpaint/_interpolate_labels.py | 77 ++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index b7137f3..2e4a380 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -26,7 +26,8 @@ def point_and_values(image_1, image_2, interp_dim=0): def xi_coords(shape, percent=0.5, interp_dim=0): slices = [slice(0, i) for i in shape] - xi = np.moveaxis(np.mgrid[slices], 0, -1).reshape(np.prod(shape), len(shape)).astype('float') + xi = np.moveaxis(np.mgrid[slices], 0, + -1).reshape(np.prod(shape), len(shape)).astype('float') xi = np.insert(xi, interp_dim, percent, axis=1) return xi @@ -42,31 +43,40 @@ def slice_iterator(slice_index_1, slice_index_2): def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): img_shape = list(values.shape) del img_shape[interp_dim] - + xi = xi_coords(img_shape, percent=percent, interp_dim=interp_dim) interpolated_img = interpn(points, values, xi, method=method) interpolated_img = np.reshape(interpolated_img, img_shape) > 0 return interpolated_img -class InterpolateSliceWidget(Container): +class InterpolateSliceWidget(Container): def __init__(self, viewer: "napari.viewer.Viewer"): super().__init__() self.viewer = viewer - self.labels_combo = ComboBox(name='Labels Layer', choices=self.get_labels_layers) + self.labels_combo = ComboBox( + name='Labels Layer', choices=self.get_labels_layers + ) self.start_interpolation = PushButton(name='Start Interpolation') self.interpolate_btn = PushButton(name='Interpolate') self.start_interpolation.clicked.connect(self.enter_interpolation) - self.extend([self.labels_combo, self.start_interpolation, self.interpolate_btn]) + self.extend([ + self.labels_combo, self.start_interpolation, + self.interpolate_btn + ]) self.interpolate_btn.hide() def get_labels_layers(self, combo): - return [layer for layer in self.viewer.layers if isinstance(layer, napari.layers.Labels)] + return [ + layer for layer in self.viewer.layers + if isinstance(layer, napari.layers.Labels) + ] def enter_interpolation(self, event): # TODO: we wanna connect some callbacks that track for us the painted labels # grey out the combo box - self.selected_layer = self.viewer.layers[self.labels_combo.current_choice] + self.selected_layer = self.viewer.layers[ + self.labels_combo.current_choice] self.start_interpolation.hide() self.interpolate_btn.show() @@ -77,21 +87,25 @@ def enter_interpolation(self, event): # TODO: for multiple slices nut we need more logic to determine which slices actualy got pained on # not currently used def remember_slices(self, event): - self.painted_slices.append(event.value) - - + self.painted_slices.append(event.value) def interpolate(self, event): # TODO: accessing private attribute, need paint event to not do that - last_two_labels = [self.selected_layer._undo_history[-1], self.selected_layer._undo_history[-2]] - - last_label_history_item = last_two_labels[0] # this is a list of tuples - last_label_coords = last_label_history_item[0][0] # first history atom is a tuple, first element of atom is coords + last_two_labels = [ + self.selected_layer._undo_history[-1], + self.selected_layer._undo_history[-2] + ] + + last_label_history_item = last_two_labels[0 + ] # this is a list of tuples + last_label_coords = last_label_history_item[0][ + 0 + ] # first history atom is a tuple, first element of atom is coords # here we can determine both the slice index that was painted *and* the interp dim # it wil be the array in last_label_coords that has only one unique element in it # the interp dim will be the index of that array in the tuple unique_coords = list(map(np.unique, last_label_coords)) - + # TODO: make this a func interp_dim = 0 for i in range(len(unique_coords)): @@ -107,14 +121,22 @@ def interpolate(self, event): label_id = last_label_history_item[-1][-1] - interpolate_between_slices(self.selected_layer, second_last_slice_painted, last_slice_painted, label_id,interp_dim) + interpolate_between_slices( + self.selected_layer, second_last_slice_painted, + last_slice_painted, label_id, interp_dim + ) # TODO: multiple slices, multiple labels, stitching history items so we don't have to pass in the whole layer # TODO: switch the button back out - -def interpolate_between_slices(label_layer: "napari.layers.Labels", slice_index_1: int, slice_index_2: int, label_id: int =1, interp_dim: int =0): +def interpolate_between_slices( + label_layer: "napari.layers.Labels", + slice_index_1: int, + slice_index_2: int, + label_id: int = 1, + interp_dim: int = 0 + ): if slice_index_1 > slice_index_2: slice_index_1, slice_index_2 = slice_index_2, slice_index_1 @@ -126,12 +148,21 @@ def interpolate_between_slices(label_layer: "napari.layers.Labels", slice_index_ slice_2 = slice_2.astype(bool) points, values = point_and_values(slice_1, slice_2, interp_dim) - - # TODO: Thread this - for slice_number, percentage in slice_iterator(slice_index_1, slice_index_2): - interpolated_img = interpolated_slice(percentage, points, values, interp_dim=interp_dim, method='linear') + + # TODO: Thread this + for slice_number, percentage in slice_iterator(slice_index_1, + slice_index_2): + interpolated_img = interpolated_slice( + percentage, + points, + values, + interp_dim=interp_dim, + method='linear' + ) indices = [slice(None) for _ in range(label_layer.data.ndim)] indices[interp_dim] = slice_number indices = tuple(indices) - label_layer.data[indices][interpolated_img] = label_id #use labels data_setitem (from the paint event PR) + label_layer.data[indices][ + interpolated_img + ] = label_id #use labels data_setitem (from the paint event PR) label_layer.refresh() # will update the current view From 3fcc3ebc918c72a2a973a22a459eabfb9dcecc96 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Tue, 2 Aug 2022 14:12:30 +1000 Subject: [PATCH 06/26] refactored to track paint event for history --- src/zarpaint/_interpolate_labels.py | 78 +++++++++++++++++------------ 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 2e4a380..bb016e1 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -65,6 +65,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.interpolate_btn ]) self.interpolate_btn.hide() + self.painted_slice_history = [] def get_labels_layers(self, combo): return [ @@ -72,41 +73,28 @@ def get_labels_layers(self, combo): if isinstance(layer, napari.layers.Labels) ] - def enter_interpolation(self, event): - # TODO: we wanna connect some callbacks that track for us the painted labels - # grey out the combo box - self.selected_layer = self.viewer.layers[ - self.labels_combo.current_choice] - self.start_interpolation.hide() - self.interpolate_btn.show() - - self.interpolate_btn.clicked.connect(self.interpolate) - self.viewer.dims.events.current_step.connect(self.remember_slices) - self.painted_slices = [] - - # TODO: for multiple slices nut we need more logic to determine which slices actualy got pained on - # not currently used - def remember_slices(self, event): - self.painted_slices.append(event.value) - - def interpolate(self, event): - # TODO: accessing private attribute, need paint event to not do that - last_two_labels = [ - self.selected_layer._undo_history[-1], - self.selected_layer._undo_history[-2] - ] - - last_label_history_item = last_two_labels[0 - ] # this is a list of tuples + def paint_callback(self, event): + last_label_history_item = event.value + real_item = [] + # filter empty history atoms from item + for atom in last_label_history_item: + all_coords = list(atom[0]) + if any([len(arr) for arr in all_coords]): + real_item.append(atom) + if not real_item: + return + + last_label_history_item = real_item last_label_coords = last_label_history_item[0][ 0 ] # first history atom is a tuple, first element of atom is coords # here we can determine both the slice index that was painted *and* the interp dim # it wil be the array in last_label_coords that has only one unique element in it # the interp dim will be the index of that array in the tuple + if not last_label_coords: + return unique_coords = list(map(np.unique, last_label_coords)) - # TODO: make this a func interp_dim = 0 for i in range(len(unique_coords)): if len(unique_coords[i]) == 1: @@ -114,20 +102,44 @@ def interpolate(self, event): break last_slice_painted = unique_coords[interp_dim][0] - second_last_label_history_item = last_two_labels[1] - second_last_label_coords = second_last_label_history_item[0][0] - # TODO: use interp_dim/last slice func and raise if you get a different interp dim - second_last_slice_painted = second_last_label_coords[0][0] - label_id = last_label_history_item[-1][-1] + history_item = (last_slice_painted, label_id, interp_dim) + if not self.painted_slice_history or history_item != self.painted_slice_history[ + -1]: + self.painted_slice_history.append(history_item) + #TODO: if the last two items only differ on interp_dim we should error + + def enter_interpolation(self, event): + # TODO: we wanna connect some callbacks that track for us the painted labels + # grey out the combo box + self.selected_layer = self.viewer.layers[ + self.labels_combo.current_choice] + + self.selected_layer.events.paint.connect(self.paint_callback) + + self.start_interpolation.hide() + self.interpolate_btn.show() + + self.interpolate_btn.clicked.connect(self.interpolate) + + def interpolate(self, event): + #TODO: here we're assuming only one label ID was painted on two slices and nothing else. We should expand to go through all + # label IDs and slices + if len(self.painted_slice_history) >= 2: + last_slice_painted, label_id, interp_dim = self.painted_slice_history[ + -1] + second_last_slice_painted, label_id, interp_dim = self.painted_slice_history[ + -2] + interpolate_between_slices( self.selected_layer, second_last_slice_painted, last_slice_painted, label_id, interp_dim ) + self.selected_layer.events.paint.disconnect(self.paint_callback) + self.painted_slice_history = [] # TODO: multiple slices, multiple labels, stitching history items so we don't have to pass in the whole layer - # TODO: switch the button back out def interpolate_between_slices( From 15cb88da1c80d571229d62bcf6d75fd9c087b324 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Mon, 8 Aug 2022 13:16:10 +1000 Subject: [PATCH 07/26] added support for multiple lable IDs and multiple slices --- src/zarpaint/_interpolate_labels.py | 64 +++++++++++++++++------------ 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index bb016e1..6b7e41d 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -1,3 +1,5 @@ +from collections import defaultdict +import warnings from magicgui.widgets import Container, ComboBox, PushButton import napari import numpy as np @@ -65,7 +67,8 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.interpolate_btn ]) self.interpolate_btn.hide() - self.painted_slice_history = [] + self.painted_slice_history = defaultdict(set) + self.interp_dim = None def get_labels_layers(self, combo): return [ @@ -93,22 +96,30 @@ def paint_callback(self, event): # the interp dim will be the index of that array in the tuple if not last_label_coords: return + unique_coords = list(map(np.unique, last_label_coords)) + if self.interp_dim is None: + self._infer_interp_dim(unique_coords) + + last_slice_painted = unique_coords[self.interp_dim][0] + + label_id = last_label_history_item[-1][-1] + + self.painted_slice_history[label_id].add(last_slice_painted) - interp_dim = 0 + def _infer_interp_dim(self, unique_coords): + interp_dim = None for i in range(len(unique_coords)): if len(unique_coords[i]) == 1: interp_dim = i break - last_slice_painted = unique_coords[interp_dim][0] - - label_id = last_label_history_item[-1][-1] - - history_item = (last_slice_painted, label_id, interp_dim) - if not self.painted_slice_history or history_item != self.painted_slice_history[ - -1]: - self.painted_slice_history.append(history_item) - #TODO: if the last two items only differ on interp_dim we should error + if interp_dim == None: + warnings.warn( + "Couldn't determine axis for interpolation. Using axis 0 by default." + ) + self.interp_dim = 0 + else: + self.interp_dim = interp_dim def enter_interpolation(self, event): # TODO: we wanna connect some callbacks that track for us the painted labels @@ -124,21 +135,21 @@ def enter_interpolation(self, event): self.interpolate_btn.clicked.connect(self.interpolate) def interpolate(self, event): - #TODO: here we're assuming only one label ID was painted on two slices and nothing else. We should expand to go through all - # label IDs and slices - if len(self.painted_slice_history) >= 2: - last_slice_painted, label_id, interp_dim = self.painted_slice_history[ - -1] - second_last_slice_painted, label_id, interp_dim = self.painted_slice_history[ - -2] - - interpolate_between_slices( - self.selected_layer, second_last_slice_painted, - last_slice_painted, label_id, interp_dim - ) + + assert self.interp_dim is not None, 'Cannot interpolate without knowing dimension' + + for label_id, slices_painted in self.painted_slice_history.items(): + slices_painted = list(sorted(slices_painted)) + if len(slices_painted) > 1: + for i in range(1, len(slices_painted)): + interpolate_between_slices( + self.selected_layer, slices_painted[i - 1], + slices_painted[i], label_id, self.interp_dim + ) self.selected_layer.events.paint.disconnect(self.paint_callback) - self.painted_slice_history = [] + self.painted_slice_history.clear() + self.interp_dim = None # TODO: multiple slices, multiple labels, stitching history items so we don't have to pass in the whole layer @@ -156,12 +167,11 @@ def interpolate_between_slices( slice_1 = np.take(layer_data, slice_index_1, axis=interp_dim) slice_2 = np.take(layer_data, slice_index_2, axis=interp_dim) - slice_1 = slice_1.astype(bool) - slice_2 = slice_2.astype(bool) + slice_1 = np.where(slice_1 == label_id, 1, 0) + slice_2 = np.where(slice_2 == label_id, 1, 0) points, values = point_and_values(slice_1, slice_2, interp_dim) - # TODO: Thread this for slice_number, percentage in slice_iterator(slice_index_1, slice_index_2): interpolated_img = interpolated_slice( From 9cf0896322d74682393e86728a654aa8dc340b20 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Mon, 8 Aug 2022 14:04:33 +1000 Subject: [PATCH 08/26] clean up yaml --- src/zarpaint/_interpolate_labels.py | 16 +++++++++++----- src/zarpaint/napari.yaml | 5 ----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 6b7e41d..6623470 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -59,11 +59,11 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.labels_combo = ComboBox( name='Labels Layer', choices=self.get_labels_layers ) - self.start_interpolation = PushButton(name='Start Interpolation') + self.start_interpolation_btn = PushButton(name='Start Interpolation') self.interpolate_btn = PushButton(name='Interpolate') - self.start_interpolation.clicked.connect(self.enter_interpolation) + self.start_interpolation_btn.clicked.connect(self.enter_interpolation) self.extend([ - self.labels_combo, self.start_interpolation, + self.labels_combo, self.start_interpolation_btn, self.interpolate_btn ]) self.interpolate_btn.hide() @@ -129,7 +129,7 @@ def enter_interpolation(self, event): self.selected_layer.events.paint.connect(self.paint_callback) - self.start_interpolation.hide() + self.start_interpolation_btn.hide() self.interpolate_btn.show() self.interpolate_btn.clicked.connect(self.interpolate) @@ -147,10 +147,16 @@ def interpolate(self, event): slices_painted[i], label_id, self.interp_dim ) + self.reset() + + def reset(self): self.selected_layer.events.paint.disconnect(self.paint_callback) self.painted_slice_history.clear() self.interp_dim = None - # TODO: multiple slices, multiple labels, stitching history items so we don't have to pass in the whole layer + self.interpolate_btn.clicked.disconnect(self.interpolate) + + self.interpolate_btn.hide() + self.start_interpolation_btn.show() def interpolate_between_slices( diff --git a/src/zarpaint/napari.yaml b/src/zarpaint/napari.yaml index 7eb4dc1..5bfb0cd 100644 --- a/src/zarpaint/napari.yaml +++ b/src/zarpaint/napari.yaml @@ -23,9 +23,6 @@ contributions: - id: zarpaint.copy_data title: Copy Data… python_name: zarpaint:copy_data - - id: zarpaint.interpolate_labels - title: Interpolate Labels… - python_name: zarpaint:interpolate_between_slices - id: zarpaint.interpolate_widg title: Interpolate python_name: zarpaint._interpolate_labels:InterpolateSliceWidget @@ -48,7 +45,5 @@ contributions: display_name: Split With Watershed - command: zarpaint.copy_data display_name: Copy Data - - command: zarpaint.interpolate_labels - display_name: Interpolate Labels - command: zarpaint.interpolate_widg display_name: Interpolate Slices From fc48307b899a36109101c87df1da762cad014d76 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Tue, 27 Sep 2022 09:32:43 +1000 Subject: [PATCH 09/26] Add docstrings --- src/zarpaint/_interpolate_labels.py | 169 ++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 12 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 6623470..a31dc08 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -8,10 +8,17 @@ def distance_transform(image): - """Distance transform for a boolean image. - - Returns positive values inside the object, - and negative values outside. + """apply distance transform and return new image + + Parameters + ---------- + image : np.array + the label layer data + + Returns + ------- + np.array + label layer data with distance transform applied """ image = image.astype(bool) edt = distance_transform_edt(image) - distance_transform_edt(~image) @@ -19,6 +26,26 @@ def distance_transform(image): def point_and_values(image_1, image_2, interp_dim=0): + """apply distance transforms to the 2 images to interpolate. + + Apply distance transforms to the 2 images that are going to be + interpolated between. Return a tuple of points and values where + points represent + + Parameters + ---------- + image_1 : np.array + The first slice to interpolate from + image_2 : _type_ + The second slice to interpolate to + interp_dim : int, optional + The dimention along which to interpolate, by default 0 + + Returns + ------- + tuple of numpy array + tuple og distance transform data and corresponding location + """ edt_1 = distance_transform(image_1) edt_2 = distance_transform(image_2) values = np.stack([edt_1, edt_2], axis=interp_dim) @@ -27,6 +54,22 @@ def point_and_values(image_1, image_2, interp_dim=0): def xi_coords(shape, percent=0.5, interp_dim=0): + """ + create array of coordinates to interpolate between + Parameters + ---------- + shape : tuple + Shape of the slice + percent : float, optional + Value to populate the xi array, by default 0.5 + interp_dim : int, optional + The axis to interpolate along , by default 0 + + Returns + ------- + numpy array + Coordinate denoting the area of the slice to interpolate along + """ slices = [slice(0, i) for i in shape] xi = np.moveaxis(np.mgrid[slices], 0, -1).reshape(np.prod(shape), len(shape)).astype('float') @@ -35,14 +78,49 @@ def xi_coords(shape, percent=0.5, interp_dim=0): def slice_iterator(slice_index_1, slice_index_2): + """create an iterable across the range of slices to be interpolated + + Parameters + ---------- + slice_index_1 : int + number of one bound of the slice range + slice_index_2 : _type_ + the opposite bound of the slice range + + Returns + ------- + zip of 2 numpy arrays + tuple of slice indicies, tuple of percentages to give xi coords + """ intermediate_slices = np.arange(slice_index_1 + 1, slice_index_2) - n_slices = slice_index_2 - slice_index_1 + 1 # inclusive + n_slices = slice_index_2 - slice_index_1 + 1 stepsize = 1 / n_slices intermediate_percentages = np.arange(0 + stepsize, 1, stepsize) return zip(intermediate_slices, intermediate_percentages) def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): + """Create the dtata for one of the interpolated slices + + Parameters + ---------- + percent : array_like + A value to populate the xi array + points : tuple of ndarray of float + The points of the slice on which to paint + values : array_like + Data to draw on the slice + interp_dim : int, optional + The axis along which to interpolate, by default 0 + method : str, optional + Interpolation method, by default 'linear' + + Returns + ------- + np array + A slice with interpolated data drawn on + """ + # TODO: check return type img_shape = list(values.shape) del img_shape[interp_dim] @@ -54,6 +132,13 @@ def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): class InterpolateSliceWidget(Container): def __init__(self, viewer: "napari.viewer.Viewer"): + """Widget for handling the interpolate slice gui and event callbacks + + Parameters + ---------- + viewer : napari.viewer.Viewer + napari viewer to add the widget to + """ super().__init__() self.viewer = viewer self.labels_combo = ComboBox( @@ -71,12 +156,31 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.interp_dim = None def get_labels_layers(self, combo): + """Returns a list of existing labels to display + + Parameters + ---------- + combo : magicgui ComboBox + A dropdown to dispaly the layers + + Returns + ------- + list[napari.layer.label] + A list of curently existing layers + """ return [ layer for layer in self.viewer.layers if isinstance(layer, napari.layers.Labels) ] def paint_callback(self, event): + """Identify slices that have been painetd on + + Parameters + ---------- + event : Event + napari paint event + """ last_label_history_item = event.value real_item = [] # filter empty history atoms from item @@ -108,6 +212,19 @@ def paint_callback(self, event): self.painted_slice_history[label_id].add(last_slice_painted) def _infer_interp_dim(self, unique_coords): + """Infer the dimension/axis on which to interpolate. + + unique_coords contains a list for each dimension. + One of the lists will contain a single element referencing the slice + being painted on. This mean that that lists is the dimension being + painted across + + Parameters + ---------- + unique_coords : List + A list of lists, containing coordinates which have been painted + and the label which is being painted + """ interp_dim = None for i in range(len(unique_coords)): if len(unique_coords[i]) == 1: @@ -122,8 +239,13 @@ def _infer_interp_dim(self, unique_coords): self.interp_dim = interp_dim def enter_interpolation(self, event): - # TODO: we wanna connect some callbacks that track for us the painted labels - # grey out the combo box + """Connect the paint callback and change button text + + Parameters + ---------- + event : Event + Event spawned by button click + """ self.selected_layer = self.viewer.layers[ self.labels_combo.current_choice] @@ -135,7 +257,14 @@ def enter_interpolation(self, event): self.interpolate_btn.clicked.connect(self.interpolate) def interpolate(self, event): - + """For each label_id, iterate over each slice that has been painted on + and perform pairwise (i, i+1) interpolation on each pair. + + Parameters + ---------- + event : Event + Object created upon clicking "interpolate" in the widget + """ assert self.interp_dim is not None, 'Cannot interpolate without knowing dimension' for label_id, slices_painted in self.painted_slice_history.items(): @@ -150,6 +279,8 @@ def interpolate(self, event): self.reset() def reset(self): + """Reset button text and clear paint event history + """ self.selected_layer.events.paint.disconnect(self.paint_callback) self.painted_slice_history.clear() self.interp_dim = None @@ -166,6 +297,22 @@ def interpolate_between_slices( label_id: int = 1, interp_dim: int = 0 ): + """Compute and draw interpolation between 2 label annotations. + + Parameters + ---------- + label_layer : napari.layers.Labels + The label layer to draw on + slice_index_1 : int + slice containing the first label annotation + slice_index_2 : int + slice containing the second label anotation + interpolation occurs between slice_index_1 slice_index_2 + label_id : int, optional + the id of the annotation that is to be painted, by default 1 + interp_dim : int, optional + the dimension/axis to interpolate across, by default 0 + """ if slice_index_1 > slice_index_2: slice_index_1, slice_index_2 = slice_index_2, slice_index_1 @@ -190,7 +337,5 @@ def interpolate_between_slices( indices = [slice(None) for _ in range(label_layer.data.ndim)] indices[interp_dim] = slice_number indices = tuple(indices) - label_layer.data[indices][ - interpolated_img - ] = label_id #use labels data_setitem (from the paint event PR) - label_layer.refresh() # will update the current view + label_layer.data[indices][interpolated_img] = label_id + label_layer.refresh() From e66aff18cce96fda628af1b3574a80d982674748 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Fri, 2 Dec 2022 14:03:29 +1100 Subject: [PATCH 10/26] add Genevieve tests, removed np.s_ and viewer --- .../_tests/test_interpolate_labels.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/zarpaint/_tests/test_interpolate_labels.py diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py new file mode 100644 index 0000000..40d8838 --- /dev/null +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -0,0 +1,124 @@ +import numpy as np +from zarpaint import _interpolate_labels +from napari.layers import Labels +from skimage.draw import ellipse, ellipsoid +from zarpaint._interpolate_labels import interpolate_between_slices + + +def test_2d_slice_ellipse(): + label_id = 2 + arraysize = 512 + slice_index_1 = 0 + slice_index_2 = 10 + # First label slice + coords_1 = ellipse( + arraysize // 2, + arraysize // 2, # row, column (center coordinates) + arraysize // 6, + arraysize // 6, # r_radius, c_radius (ellipse radii) + shape=(arraysize, arraysize) + ) + label_slice_1 = np.zeros((arraysize, arraysize)).astype(int) + label_slice_1[coords_1] = label_id + # Second label slice + coords_2 = ellipse( + arraysize // 2, + arraysize // 2, # row, column (center coordinates) + arraysize // 5, + arraysize // 3, # r_radius, c_radius (ellipse radii) + shape=(arraysize, arraysize) + ) + label_slice_2 = np.zeros((arraysize, arraysize)).astype(int) + label_slice_2[coords_2] = label_id + # Create labels for interpolation + labels = np.zeros((11, arraysize, arraysize)).astype(int) + labels[slice_index_1] = label_slice_1 + labels[slice_index_2] = label_slice_2 + labels_layer = Labels(labels) + + # Check all intermediate label slices contain only zero + for labels_slice in labels_layer.data[1:-1]: + assert np.max(labels_slice) == 0 + # Check the expected number of non-zero pixels exist now + assert np.count_nonzero(labels_layer.data[1:-1]) == 0 + + # Interpolate intermediate slices + interpolate_between_slices( + labels_layer, slice_index_1, slice_index_2, label_id, interp_dim=0 + ) + + # Check all intermediate label slices now contain non-zero pixels + for labels_slice in labels_layer.data[1:-1]: + assert np.max(labels_slice) == label_id + # Check the expected number of non-zero pixels exist now + assert np.count_nonzero(labels_layer.data[1:-1]) == 315045 + + +def test_2d_slice_square(): + space = (100, 100, 100) + + data = np.zeros(shape=space, dtype="uint8") + + data[10, 10:20, 10:20] = 2 + data[20, 10:20, 10:20] = 2 + + labels = Labels(data) + + _interpolate_labels.interpolate_between_slices(labels, 10, 20, 2, 0) + + np.testing.assert_allclose(labels.data[10:20, 10:20, 10:20], 2) + + +def test_3d_slice_ellipsoid(): + label_id = 2 + arraysize = 100 + slice_index_1 = 0 + slice_index_2 = 4 + # First label slice + ellipse_1 = ellipsoid(20, 35, 25) * label_id + padding = np.array((arraysize, arraysize, arraysize) + ) - np.array(ellipse_1.shape) + pad_width = [(i // 2, i//2 + 1) for i in padding] + label_slice_1 = np.pad(ellipse_1, pad_width) + # Second label slice + ellipse_2 = ellipsoid(28, 33, 40) * label_id + padding = np.array((arraysize, arraysize, arraysize) + ) - np.array(ellipse_2.shape) + pad_width = [(i // 2, i//2 + 1) for i in padding] + label_slice_2 = np.pad(ellipse_2, pad_width) + # Create labels for interpolation + labels = np.zeros((5, arraysize, arraysize, arraysize)).astype(int) + labels[slice_index_1] = label_slice_1 + labels[slice_index_2] = label_slice_2 + labels_layer = Labels(labels) + + # Check all intermediate label slices contain only zero + for labels_slice in labels_layer.data[1:-1]: + assert np.max(labels_slice) == 0 + # Check the expected number of non-zero pixels exist now + assert np.count_nonzero(labels_layer.data[1:-1]) == 0 + + # Interpolate intermediate slices + interpolate_between_slices( + labels_layer, slice_index_1, slice_index_2, label_id, interp_dim=0 + ) + + # Check all intermediate label slices now contain non-zero pixels + for labels_slice in labels_layer.data[1:-1]: + assert np.max(labels_slice) == label_id + # Check the expected number of non-zero pixels exist now + assert np.count_nonzero(labels_layer.data[1:-1]) == 297885 + + +def test_3d_slice_cube(): + space = (100, 100, 100, 100) + data = np.zeros(shape=space, dtype="uint8") + + data[20, 10:20, 10:20, 10:20] = 2 + data[10, 10:20, 10:20, 10:20] = 2 + + labels = Labels(data) + + _interpolate_labels.interpolate_between_slices(labels, 10, 20, 2, 0) + + np.testing.assert_allclose(labels.data[10:20, 10:20, 10:20, 10:20], 2) From 2ecf462cbc951b0fbc706a954d3ccd0bfaa2b0b3 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Wed, 11 Jan 2023 11:06:10 +1100 Subject: [PATCH 11/26] remove line that reads data into array --- src/zarpaint/_interpolate_labels.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index a31dc08..81f56a8 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -316,9 +316,8 @@ def interpolate_between_slices( if slice_index_1 > slice_index_2: slice_index_1, slice_index_2 = slice_index_2, slice_index_1 - layer_data = np.asarray(label_layer.data) - slice_1 = np.take(layer_data, slice_index_1, axis=interp_dim) - slice_2 = np.take(layer_data, slice_index_2, axis=interp_dim) + slice_1 = np.take(label_layer.data, slice_index_1, axis=interp_dim) + slice_2 = np.take(label_layer.data, slice_index_2, axis=interp_dim) slice_1 = np.where(slice_1 == label_id, 1, 0) slice_2 = np.where(slice_2 == label_id, 1, 0) From dd23e51015cc540f1ad331e28e46f43e98dfe886 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Wed, 11 Jan 2023 11:39:52 +1100 Subject: [PATCH 12/26] initial test for widget coverage --- .../_tests/test_interpolate_labels.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index 40d8838..bea6071 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -3,6 +3,7 @@ from napari.layers import Labels from skimage.draw import ellipse, ellipsoid from zarpaint._interpolate_labels import interpolate_between_slices +import napari def test_2d_slice_ellipse(): @@ -122,3 +123,28 @@ def test_3d_slice_cube(): _interpolate_labels.interpolate_between_slices(labels, 10, 20, 2, 0) np.testing.assert_allclose(labels.data[10:20, 10:20, 10:20, 10:20], 2) + + +def test_labels_layer_combo_box(): + viewer = napari.Viewer() + space = (100, 100, 100, 100) + data = np.zeros(shape=space, dtype="uint8") + + data[20, 10:20, 10:20, 10:20] = 2 + data[10, 10:20, 10:20, 10:20] = 2 + + viewer.add_labels(data, name="test data") + viewer.add_image(data) + interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) + labels_layers_list = interp_widget.get_labels_layers( + interp_widget.labels_combo + ) + + assert len(labels_layers_list) == 1 + assert labels_layers_list[0].name == "test data" + + viewer.layers.remove(labels_layers_list[0]) + labels_layers_list = interp_widget.get_labels_layers( + interp_widget.labels_combo + ) + assert len(labels_layers_list) == 0 From 766536771d1d4873da96db0bea5ac08f1f40c183 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Fri, 13 Jan 2023 15:15:22 +1100 Subject: [PATCH 13/26] add test for store_painted_sices --- src/zarpaint/_interpolate_labels.py | 6 +-- .../_tests/test_interpolate_labels.py | 39 ++++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 81f56a8..61e6341 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -173,7 +173,7 @@ def get_labels_layers(self, combo): if isinstance(layer, napari.layers.Labels) ] - def paint_callback(self, event): + def store_painted_slices(self, event): """Identify slices that have been painetd on Parameters @@ -249,7 +249,7 @@ def enter_interpolation(self, event): self.selected_layer = self.viewer.layers[ self.labels_combo.current_choice] - self.selected_layer.events.paint.connect(self.paint_callback) + self.selected_layer.events.paint.connect(self.store_painted_slices) self.start_interpolation_btn.hide() self.interpolate_btn.show() @@ -281,7 +281,7 @@ def interpolate(self, event): def reset(self): """Reset button text and clear paint event history """ - self.selected_layer.events.paint.disconnect(self.paint_callback) + self.selected_layer.events.paint.disconnect(self.store_painted_slices) self.painted_slice_history.clear() self.interp_dim = None self.interpolate_btn.clicked.disconnect(self.interpolate) diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index bea6071..d61455e 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -3,6 +3,7 @@ from napari.layers import Labels from skimage.draw import ellipse, ellipsoid from zarpaint._interpolate_labels import interpolate_between_slices +from unittest.mock import MagicMock import napari @@ -125,8 +126,8 @@ def test_3d_slice_cube(): np.testing.assert_allclose(labels.data[10:20, 10:20, 10:20, 10:20], 2) -def test_labels_layer_combo_box(): - viewer = napari.Viewer() +def test_labels_layer_combo_box(make_napari_viewer): + viewer = make_napari_viewer() space = (100, 100, 100, 100) data = np.zeros(shape=space, dtype="uint8") @@ -148,3 +149,37 @@ def test_labels_layer_combo_box(): interp_widget.labels_combo ) assert len(labels_layers_list) == 0 + + +def test_store_painted_slices(): + viewer = napari.Viewer() + space = (100, 100, 100, 100) + data = np.zeros(shape=space, dtype="uint8") + + # data[50, 50, 19:22, 9:12] = 2 + # data[45, 45, 19:22, 9:12] = 2 + + viewer.add_labels(data, name="test data") + interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) + event = MagicMock() + event.value = [(([50, 50, 50, 50, 50, 50, 50, 50, + 50], [50, 50, 50, 50, 50, 50, 50, 50, + 50], [19, 20, 21, 19, 20, 21, 19, 20, + 21], [9, 9, 9, 10, 10, 10, 11, 11, + 11]), [0, 0, 0, 0, 0, 0, 0, 0, 0], 2)] + painted_slices = interp_widget.store_painted_slices(event) + + event_2 = MagicMock() + event_2.value = [(([45, 45, 45, 45, 45, 45, 45, 45, + 45], [45, 45, 45, 45, 45, 45, 45, 45, + 45], [19, 20, 21, 19, 20, 21, 19, 20, + 21], [9, 9, 9, 10, 10, 10, 11, 11, + 11]), [0, 0, 0, 0, 0, 0, 0, 0, + 0], 2)] + + painted_slices = interp_widget.store_painted_slices(event_2) + print(interp_widget.painted_slice_history) + assert interp_widget.painted_slice_history[2] == {50, 45} + + +test_widget_interpolate() From c009b55f821277e5805734c2dca3de891537c998 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Fri, 13 Jan 2023 15:20:47 +1100 Subject: [PATCH 14/26] tidy up test_store_painted_slice --- .../_tests/test_interpolate_labels.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index d61455e..bf41026 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -4,7 +4,6 @@ from skimage.draw import ellipse, ellipsoid from zarpaint._interpolate_labels import interpolate_between_slices from unittest.mock import MagicMock -import napari def test_2d_slice_ellipse(): @@ -151,15 +150,12 @@ def test_labels_layer_combo_box(make_napari_viewer): assert len(labels_layers_list) == 0 -def test_store_painted_slices(): - viewer = napari.Viewer() +def test_store_painted_slices(make_napari_viewer): + viewer = make_napari_viewer() space = (100, 100, 100, 100) data = np.zeros(shape=space, dtype="uint8") - - # data[50, 50, 19:22, 9:12] = 2 - # data[45, 45, 19:22, 9:12] = 2 - viewer.add_labels(data, name="test data") + interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) event = MagicMock() event.value = [(([50, 50, 50, 50, 50, 50, 50, 50, @@ -167,8 +163,6 @@ def test_store_painted_slices(): 50], [19, 20, 21, 19, 20, 21, 19, 20, 21], [9, 9, 9, 10, 10, 10, 11, 11, 11]), [0, 0, 0, 0, 0, 0, 0, 0, 0], 2)] - painted_slices = interp_widget.store_painted_slices(event) - event_2 = MagicMock() event_2.value = [(([45, 45, 45, 45, 45, 45, 45, 45, 45], [45, 45, 45, 45, 45, 45, 45, 45, @@ -176,10 +170,16 @@ def test_store_painted_slices(): 21], [9, 9, 9, 10, 10, 10, 11, 11, 11]), [0, 0, 0, 0, 0, 0, 0, 0, 0], 2)] - - painted_slices = interp_widget.store_painted_slices(event_2) - print(interp_widget.painted_slice_history) + interp_widget.store_painted_slices(event) + interp_widget.store_painted_slices(event_2) assert interp_widget.painted_slice_history[2] == {50, 45} - -test_widget_interpolate() + event_3 = MagicMock() + event_3.value = [(([30, 30, 30, 30, 30, 30, 30, 30, + 30], [30, 30, 30, 30, 30, 30, 30, 30, + 30], [19, 20, 21, 19, 20, 21, 19, 20, + 21], [9, 9, 9, 10, 10, 10, 11, 11, + 11]), [0, 0, 0, 0, 0, 0, 0, 0, + 0], 5)] + interp_widget.store_painted_slices(event_3) + assert interp_widget.painted_slice_history[5] == {30} From a6dc9399a0f7547d36cd11d15f970f7e2b33a4d0 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Fri, 13 Jan 2023 15:24:33 +1100 Subject: [PATCH 15/26] removed unnecessary labels layer --- src/zarpaint/_tests/test_interpolate_labels.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index bf41026..07d723d 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -152,9 +152,6 @@ def test_labels_layer_combo_box(make_napari_viewer): def test_store_painted_slices(make_napari_viewer): viewer = make_napari_viewer() - space = (100, 100, 100, 100) - data = np.zeros(shape=space, dtype="uint8") - viewer.add_labels(data, name="test data") interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) event = MagicMock() From 4442879aa581a4e19e447af21ed7d75f8f5c6887 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Tue, 17 Jan 2023 14:03:17 +1100 Subject: [PATCH 16/26] add test for distance_transfrom --- .../_tests/test_interpolate_labels.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index 07d723d..670c64f 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -155,28 +155,40 @@ def test_store_painted_slices(make_napari_viewer): interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) event = MagicMock() + + interp_widget.store_painted_slices(event) + assert len(interp_widget.painted_slice_history) == 0 + event.value = [(([50, 50, 50, 50, 50, 50, 50, 50, - 50], [50, 50, 50, 50, 50, 50, 50, 50, - 50], [19, 20, 21, 19, 20, 21, 19, 20, - 21], [9, 9, 9, 10, 10, 10, 11, 11, - 11]), [0, 0, 0, 0, 0, 0, 0, 0, 0], 2)] + 50], [19, 20, 21, 19, 20, 21, 19, 20, + 21], [9, 9, 9, 10, 10, 10, 11, 11, + 11]), [0, 0, 0, 0, 0, 0, 0, 0, 0], 2)] event_2 = MagicMock() event_2.value = [(([45, 45, 45, 45, 45, 45, 45, 45, - 45], [45, 45, 45, 45, 45, 45, 45, 45, - 45], [19, 20, 21, 19, 20, 21, 19, 20, - 21], [9, 9, 9, 10, 10, 10, 11, 11, - 11]), [0, 0, 0, 0, 0, 0, 0, 0, - 0], 2)] + 45], [19, 20, 21, 19, 20, 21, 19, 20, + 21], [9, 9, 9, 10, 10, 10, 11, 11, + 11]), [0, 0, 0, 0, 0, 0, 0, 0, 0], 2)] interp_widget.store_painted_slices(event) interp_widget.store_painted_slices(event_2) assert interp_widget.painted_slice_history[2] == {50, 45} event_3 = MagicMock() event_3.value = [(([30, 30, 30, 30, 30, 30, 30, 30, - 30], [30, 30, 30, 30, 30, 30, 30, 30, - 30], [19, 20, 21, 19, 20, 21, 19, 20, - 21], [9, 9, 9, 10, 10, 10, 11, 11, - 11]), [0, 0, 0, 0, 0, 0, 0, 0, - 0], 5)] + 30], [19, 20, 21, 19, 20, 21, 19, 20, + 21], [9, 9, 9, 10, 10, 10, 11, 11, + 11]), [0, 0, 0, 0, 0, 0, 0, 0, 0], 5)] interp_widget.store_painted_slices(event_3) assert interp_widget.painted_slice_history[5] == {30} + + +def test_distance_transform(): + shape = (3, 3) + image = np.zeros(shape=shape, dtype="uint8") + image[1, 1] = 3 + res = _interpolate_labels.distance_transform(image) + + assert res[1, 1] == 1 + np.testing.assert_allclose(res[0, 0], -2**0.5) + + +test_distance_transform() From a7c50ffcf7f273a07d693e2370417a91934f7068 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Thu, 2 Feb 2023 11:02:07 +1100 Subject: [PATCH 17/26] added test for interpolate --- src/zarpaint/_interpolate_labels.py | 1 + .../_tests/test_interpolate_labels.py | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 61e6341..46d55b0 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -154,6 +154,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.interpolate_btn.hide() self.painted_slice_history = defaultdict(set) self.interp_dim = None + self.selected_layer = None def get_labels_layers(self, combo): """Returns a list of existing labels to display diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index 670c64f..671e081 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -191,4 +191,38 @@ def test_distance_transform(): np.testing.assert_allclose(res[0, 0], -2**0.5) -test_distance_transform() +def test_interpolate(make_napari_viewer): + viewer = make_napari_viewer() + + space = (10, 10, 10) + + data = np.zeros(shape=space, dtype="uint8") + + paint_history = [[([2, 2, 2, 2, 2, 2, 2, 2, + 2], [3, 4, 5, 3, 4, 5, 3, 4, + 5], [3, 3, 3, 4, 4, 4, 5, 5, 5]), + [2, 2, 2, 2, 2, 2, 2, 2, 2], 2], + [([6, 6, 6, 6, 6, 6, 6, 6, 6], [ + 3, 4, 5, 3, 4, 5, 3, 4, 5 + ], [3, 3, 3, 4, 4, 4, 5, 5, 5]), + [2, 2, 2, 2, 2, 2, 2, 2, 2], 2]] + + data[3, 3:6, 3:6] = 2 + data[6, 3:6, 3:6] = 2 + + labels = Labels(data) + viewer.add_layer(labels) + interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) + event = MagicMock() + event.value = paint_history + + interp_widget.painted_slice_history[2] = [6, 3] + interp_widget.interp_dim = 0 + interp_widget.selected_layer = viewer.layers[0] + interp_widget.interpolate(event) + + print(viewer.layers[0].data) + + np.testing.assert_allclose( + viewer.layers[0].data[3], viewer.layers[0].data[4] + ) From 0d66c4ef20ba72e294012e014b0ac5333dc5b7a8 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Mon, 13 Feb 2023 13:36:13 +1100 Subject: [PATCH 18/26] added dimention combo box --- src/zarpaint/_interpolate_labels.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 46d55b0..e9dde2a 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -140,22 +140,34 @@ def __init__(self, viewer: "napari.viewer.Viewer"): napari viewer to add the widget to """ super().__init__() + self.viewer = viewer + self.painted_slice_history = defaultdict(set) + self.labels_combo = ComboBox( name='Labels Layer', choices=self.get_labels_layers ) + self.interp_dim = ComboBox( + name="interpret Dimention", choices=self.update_dim_choices + ) + self.start_interpolation_btn = PushButton(name='Start Interpolation') self.interpolate_btn = PushButton(name='Interpolate') self.start_interpolation_btn.clicked.connect(self.enter_interpolation) self.extend([ - self.labels_combo, self.start_interpolation_btn, - self.interpolate_btn + self.labels_combo, self.interp_dim, + self.start_interpolation_btn, self.interpolate_btn ]) self.interpolate_btn.hide() - self.painted_slice_history = defaultdict(set) - self.interp_dim = None + self.selected_layer = None + def update_dim_choices(self, interp_dim): + layer_name = self.labels_combo.current_choice + if not layer_name: + return [] + return list(range(self.viewer.layers[layer_name].data.ndim)) + def get_labels_layers(self, combo): """Returns a list of existing labels to display From d5414cff1e327b2ae6b0d28759b7a6a834d38b59 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Mon, 13 Feb 2023 15:08:38 +1100 Subject: [PATCH 19/26] refactor interp_dim --- src/zarpaint/_interpolate_labels.py | 54 +++++-------------- .../_tests/test_interpolate_labels.py | 12 ++++- 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index e9dde2a..95f07b2 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -39,7 +39,7 @@ def point_and_values(image_1, image_2, interp_dim=0): image_2 : _type_ The second slice to interpolate to interp_dim : int, optional - The dimention along which to interpolate, by default 0 + The dimension along which to interpolate, by default 0 Returns ------- @@ -148,7 +148,8 @@ def __init__(self, viewer: "napari.viewer.Viewer"): name='Labels Layer', choices=self.get_labels_layers ) self.interp_dim = ComboBox( - name="interpret Dimention", choices=self.update_dim_choices + name="Interpolation Dimension", + choices=self.update_dim_choices ) self.start_interpolation_btn = PushButton(name='Start Interpolation') @@ -215,42 +216,13 @@ def store_painted_slices(self, event): return unique_coords = list(map(np.unique, last_label_coords)) - if self.interp_dim is None: - self._infer_interp_dim(unique_coords) - last_slice_painted = unique_coords[self.interp_dim][0] + last_slice_painted = unique_coords[self.interpolate_axis][0] label_id = last_label_history_item[-1][-1] self.painted_slice_history[label_id].add(last_slice_painted) - def _infer_interp_dim(self, unique_coords): - """Infer the dimension/axis on which to interpolate. - - unique_coords contains a list for each dimension. - One of the lists will contain a single element referencing the slice - being painted on. This mean that that lists is the dimension being - painted across - - Parameters - ---------- - unique_coords : List - A list of lists, containing coordinates which have been painted - and the label which is being painted - """ - interp_dim = None - for i in range(len(unique_coords)): - if len(unique_coords[i]) == 1: - interp_dim = i - break - if interp_dim == None: - warnings.warn( - "Couldn't determine axis for interpolation. Using axis 0 by default." - ) - self.interp_dim = 0 - else: - self.interp_dim = interp_dim - def enter_interpolation(self, event): """Connect the paint callback and change button text @@ -268,6 +240,7 @@ def enter_interpolation(self, event): self.interpolate_btn.show() self.interpolate_btn.clicked.connect(self.interpolate) + self.interpolate_axis = self.interp_dim.get_value() def interpolate(self, event): """For each label_id, iterate over each slice that has been painted on @@ -278,7 +251,6 @@ def interpolate(self, event): event : Event Object created upon clicking "interpolate" in the widget """ - assert self.interp_dim is not None, 'Cannot interpolate without knowing dimension' for label_id, slices_painted in self.painted_slice_history.items(): slices_painted = list(sorted(slices_painted)) @@ -286,7 +258,7 @@ def interpolate(self, event): for i in range(1, len(slices_painted)): interpolate_between_slices( self.selected_layer, slices_painted[i - 1], - slices_painted[i], label_id, self.interp_dim + slices_painted[i], label_id, self.interpolate_axis ) self.reset() @@ -296,7 +268,7 @@ def reset(self): """ self.selected_layer.events.paint.disconnect(self.store_painted_slices) self.painted_slice_history.clear() - self.interp_dim = None + self.interpolate_axis = None self.interpolate_btn.clicked.disconnect(self.interpolate) self.interpolate_btn.hide() @@ -308,7 +280,7 @@ def interpolate_between_slices( slice_index_1: int, slice_index_2: int, label_id: int = 1, - interp_dim: int = 0 + interpolate_axis: int = 0 ): """Compute and draw interpolation between 2 label annotations. @@ -329,13 +301,13 @@ def interpolate_between_slices( if slice_index_1 > slice_index_2: slice_index_1, slice_index_2 = slice_index_2, slice_index_1 - slice_1 = np.take(label_layer.data, slice_index_1, axis=interp_dim) - slice_2 = np.take(label_layer.data, slice_index_2, axis=interp_dim) + slice_1 = np.take(label_layer.data, slice_index_1, axis=interpolate_axis) + slice_2 = np.take(label_layer.data, slice_index_2, axis=interpolate_axis) slice_1 = np.where(slice_1 == label_id, 1, 0) slice_2 = np.where(slice_2 == label_id, 1, 0) - points, values = point_and_values(slice_1, slice_2, interp_dim) + points, values = point_and_values(slice_1, slice_2, interpolate_axis) for slice_number, percentage in slice_iterator(slice_index_1, slice_index_2): @@ -343,11 +315,11 @@ def interpolate_between_slices( percentage, points, values, - interp_dim=interp_dim, + interp_dim=interpolate_axis, method='linear' ) indices = [slice(None) for _ in range(label_layer.data.ndim)] - indices[interp_dim] = slice_number + indices[interpolate_axis] = slice_number indices = tuple(indices) label_layer.data[indices][interpolated_img] = label_id label_layer.refresh() diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index 671e081..64d52dd 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -45,7 +45,11 @@ def test_2d_slice_ellipse(): # Interpolate intermediate slices interpolate_between_slices( - labels_layer, slice_index_1, slice_index_2, label_id, interp_dim=0 + labels_layer, + slice_index_1, + slice_index_2, + label_id, + interpolate_axis=0 ) # Check all intermediate label slices now contain non-zero pixels @@ -101,7 +105,11 @@ def test_3d_slice_ellipsoid(): # Interpolate intermediate slices interpolate_between_slices( - labels_layer, slice_index_1, slice_index_2, label_id, interp_dim=0 + labels_layer, + slice_index_1, + slice_index_2, + label_id, + interpolate_axis=0 ) # Check all intermediate label slices now contain non-zero pixels From 1dcfc45c190787b35f9e48594f075a379cb07b33 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Fri, 17 Feb 2023 14:47:40 +1100 Subject: [PATCH 20/26] test enter_interpolation_mode --- src/zarpaint/_interpolate_labels.py | 59 +++++++++---------- .../_tests/test_interpolate_labels.py | 34 +++++++++-- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 95f07b2..2369f36 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -1,5 +1,4 @@ from collections import defaultdict -import warnings from magicgui.widgets import Container, ComboBox, PushButton import napari import numpy as np @@ -7,7 +6,7 @@ from scipy.ndimage import distance_transform_edt -def distance_transform(image): +def signed_distance_transform(image): """apply distance transform and return new image Parameters @@ -25,7 +24,7 @@ def distance_transform(image): return edt -def point_and_values(image_1, image_2, interp_dim=0): +def point_and_values(image_1, image_2, interp_axis=0): """apply distance transforms to the 2 images to interpolate. Apply distance transforms to the 2 images that are going to be @@ -38,7 +37,7 @@ def point_and_values(image_1, image_2, interp_dim=0): The first slice to interpolate from image_2 : _type_ The second slice to interpolate to - interp_dim : int, optional + interp_axis : int, optional The dimension along which to interpolate, by default 0 Returns @@ -46,14 +45,14 @@ def point_and_values(image_1, image_2, interp_dim=0): tuple of numpy array tuple og distance transform data and corresponding location """ - edt_1 = distance_transform(image_1) - edt_2 = distance_transform(image_2) - values = np.stack([edt_1, edt_2], axis=interp_dim) + edt_1 = signed_distance_transform(image_1) + edt_2 = signed_distance_transform(image_2) + values = np.stack([edt_1, edt_2], axis=interp_axis) points = tuple([np.arange(i) for i in values.shape]) return points, values -def xi_coords(shape, percent=0.5, interp_dim=0): +def xi_coords(shape, percent=0.5, interp_axis=0): """ create array of coordinates to interpolate between Parameters @@ -62,7 +61,7 @@ def xi_coords(shape, percent=0.5, interp_dim=0): Shape of the slice percent : float, optional Value to populate the xi array, by default 0.5 - interp_dim : int, optional + interp_axis : int, optional The axis to interpolate along , by default 0 Returns @@ -73,7 +72,7 @@ def xi_coords(shape, percent=0.5, interp_dim=0): slices = [slice(0, i) for i in shape] xi = np.moveaxis(np.mgrid[slices], 0, -1).reshape(np.prod(shape), len(shape)).astype('float') - xi = np.insert(xi, interp_dim, percent, axis=1) + xi = np.insert(xi, interp_axis, percent, axis=1) return xi @@ -99,7 +98,9 @@ def slice_iterator(slice_index_1, slice_index_2): return zip(intermediate_slices, intermediate_percentages) -def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): +def interpolated_slice( + percent, points, values, interp_axis=0, method='linear' + ): """Create the dtata for one of the interpolated slices Parameters @@ -110,7 +111,7 @@ def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): The points of the slice on which to paint values : array_like Data to draw on the slice - interp_dim : int, optional + interp_axis : int, optional The axis along which to interpolate, by default 0 method : str, optional Interpolation method, by default 'linear' @@ -122,9 +123,9 @@ def interpolated_slice(percent, points, values, interp_dim=0, method='linear'): """ # TODO: check return type img_shape = list(values.shape) - del img_shape[interp_dim] + del img_shape[interp_axis] - xi = xi_coords(img_shape, percent=percent, interp_dim=interp_dim) + xi = xi_coords(img_shape, percent=percent, interp_axis=interp_axis) interpolated_img = interpn(points, values, xi, method=method) interpolated_img = np.reshape(interpolated_img, img_shape) > 0 return interpolated_img @@ -140,30 +141,32 @@ def __init__(self, viewer: "napari.viewer.Viewer"): napari viewer to add the widget to """ super().__init__() - self.viewer = viewer self.painted_slice_history = defaultdict(set) self.labels_combo = ComboBox( name='Labels Layer', choices=self.get_labels_layers ) - self.interp_dim = ComboBox( + self.interp_dim_combo = ComboBox( name="Interpolation Dimension", choices=self.update_dim_choices ) self.start_interpolation_btn = PushButton(name='Start Interpolation') self.interpolate_btn = PushButton(name='Interpolate') - self.start_interpolation_btn.clicked.connect(self.enter_interpolation) + self.start_interpolation_btn.clicked.connect( + self.enter_interpolation_mode + ) self.extend([ - self.labels_combo, self.interp_dim, + self.labels_combo, self.interp_dim_combo, self.start_interpolation_btn, self.interpolate_btn ]) self.interpolate_btn.hide() + self.interpolate_axis = 0 self.selected_layer = None - def update_dim_choices(self, interp_dim): + def update_dim_choices(self, interp_dim_combo): layer_name = self.labels_combo.current_choice if not layer_name: return [] @@ -205,15 +208,9 @@ def store_painted_slices(self, event): if not real_item: return + # item is list of atoms. atom is (tuple of e.g. (y, x) painted coords, array of original label, new label) last_label_history_item = real_item - last_label_coords = last_label_history_item[0][ - 0 - ] # first history atom is a tuple, first element of atom is coords - # here we can determine both the slice index that was painted *and* the interp dim - # it wil be the array in last_label_coords that has only one unique element in it - # the interp dim will be the index of that array in the tuple - if not last_label_coords: - return + last_label_coords = last_label_history_item[0][0] unique_coords = list(map(np.unique, last_label_coords)) @@ -223,7 +220,7 @@ def store_painted_slices(self, event): self.painted_slice_history[label_id].add(last_slice_painted) - def enter_interpolation(self, event): + def enter_interpolation_mode(self, event): """Connect the paint callback and change button text Parameters @@ -240,7 +237,7 @@ def enter_interpolation(self, event): self.interpolate_btn.show() self.interpolate_btn.clicked.connect(self.interpolate) - self.interpolate_axis = self.interp_dim.get_value() + self.interpolate_axis = self.interp_dim_combo.get_value() def interpolate(self, event): """For each label_id, iterate over each slice that has been painted on @@ -295,7 +292,7 @@ def interpolate_between_slices( interpolation occurs between slice_index_1 slice_index_2 label_id : int, optional the id of the annotation that is to be painted, by default 1 - interp_dim : int, optional + interpolate_axis : int, optional the dimension/axis to interpolate across, by default 0 """ @@ -315,7 +312,7 @@ def interpolate_between_slices( percentage, points, values, - interp_dim=interpolate_axis, + interp_axis=interpolate_axis, method='linear' ) indices = [slice(None) for _ in range(label_layer.data.ndim)] diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index 64d52dd..5f74195 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -74,6 +74,21 @@ def test_2d_slice_square(): np.testing.assert_allclose(labels.data[10:20, 10:20, 10:20], 2) +def test_interpolate_reversed_slices(): + space = (100, 100, 100) + + data = np.zeros(shape=space, dtype="uint8") + + data[10, 10:20, 10:20] = 2 + data[20, 10:20, 10:20] = 2 + + labels = Labels(data) + + _interpolate_labels.interpolate_between_slices(labels, 20, 10, 2, 0) + + np.testing.assert_allclose(labels.data[10:20, 10:20, 10:20], 2) + + def test_3d_slice_ellipsoid(): label_id = 2 arraysize = 100 @@ -193,7 +208,7 @@ def test_distance_transform(): shape = (3, 3) image = np.zeros(shape=shape, dtype="uint8") image[1, 1] = 3 - res = _interpolate_labels.distance_transform(image) + res = _interpolate_labels.signed_distance_transform(image) assert res[1, 1] == 1 np.testing.assert_allclose(res[0, 0], -2**0.5) @@ -225,12 +240,23 @@ def test_interpolate(make_napari_viewer): event.value = paint_history interp_widget.painted_slice_history[2] = [6, 3] - interp_widget.interp_dim = 0 + interp_widget.interpolate_axis = 0 interp_widget.selected_layer = viewer.layers[0] interp_widget.interpolate(event) - print(viewer.layers[0].data) - np.testing.assert_allclose( viewer.layers[0].data[3], viewer.layers[0].data[4] ) + + +def test_enter_interpolation_mode(make_napari_viewer): + viewer = make_napari_viewer() + labels = Labels(np.zeros(shape=(10, 10), dtype=np.uint8), name="Layer 1") + labels_2 = Labels(np.zeros(shape=(10, 10), dtype=np.uint8), name="Layer 2") + + viewer.add_layer(labels) + viewer.add_layer(labels_2) + interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) + interp_widget.labels_combo.value = labels_2 + interp_widget.enter_interpolation_mode(None) + assert interp_widget.selected_layer == labels_2 From 342b9a9b05608cb7c9c8b980b694ff927f4ce0af Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Fri, 17 Feb 2023 14:53:17 +1100 Subject: [PATCH 21/26] disable comboboxed when interpolating --- src/zarpaint/_interpolate_labels.py | 4 ++++ src/zarpaint/_tests/test_interpolate_labels.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index 2369f36..d5887f0 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -238,6 +238,8 @@ def enter_interpolation_mode(self, event): self.interpolate_btn.clicked.connect(self.interpolate) self.interpolate_axis = self.interp_dim_combo.get_value() + self.labels_combo.enabled = False + self.interp_dim_combo.enabled = False def interpolate(self, event): """For each label_id, iterate over each slice that has been painted on @@ -270,6 +272,8 @@ def reset(self): self.interpolate_btn.hide() self.start_interpolation_btn.show() + self.labels_combo.enabled = True + self.interp_dim_combo.enabled = True def interpolate_between_slices( diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index 5f74195..c684a73 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -260,3 +260,5 @@ def test_enter_interpolation_mode(make_napari_viewer): interp_widget.labels_combo.value = labels_2 interp_widget.enter_interpolation_mode(None) assert interp_widget.selected_layer == labels_2 + assert interp_widget.labels_combo.enabled == False + assert interp_widget.interp_dim_combo.enabled == False From 9f65ce41f9118959f7d46087c918ef45f546f479 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Fri, 17 Feb 2023 15:01:20 +1100 Subject: [PATCH 22/26] check for layers before interpolation --- src/zarpaint/_interpolate_labels.py | 3 +++ src/zarpaint/_tests/test_interpolate_labels.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/zarpaint/_interpolate_labels.py b/src/zarpaint/_interpolate_labels.py index d5887f0..dfab5d7 100644 --- a/src/zarpaint/_interpolate_labels.py +++ b/src/zarpaint/_interpolate_labels.py @@ -228,6 +228,9 @@ def enter_interpolation_mode(self, event): event : Event Event spawned by button click """ + if not self.labels_combo.current_choice: + raise RuntimeError("No labels layer selected.") + self.selected_layer = self.viewer.layers[ self.labels_combo.current_choice] diff --git a/src/zarpaint/_tests/test_interpolate_labels.py b/src/zarpaint/_tests/test_interpolate_labels.py index c684a73..e07691e 100644 --- a/src/zarpaint/_tests/test_interpolate_labels.py +++ b/src/zarpaint/_tests/test_interpolate_labels.py @@ -4,6 +4,7 @@ from skimage.draw import ellipse, ellipsoid from zarpaint._interpolate_labels import interpolate_between_slices from unittest.mock import MagicMock +import pytest def test_2d_slice_ellipse(): @@ -262,3 +263,10 @@ def test_enter_interpolation_mode(make_napari_viewer): assert interp_widget.selected_layer == labels_2 assert interp_widget.labels_combo.enabled == False assert interp_widget.interp_dim_combo.enabled == False + + +def test_enter_interpolation_mode_no_labels(make_napari_viewer): + viewer = make_napari_viewer() + interp_widget = _interpolate_labels.InterpolateSliceWidget(viewer) + with pytest.raises(RuntimeError): + interp_widget.enter_interpolation_mode(None) From 7fd2c21345d26fe90f15530224d11e81eaa722c9 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Tue, 18 Apr 2023 12:03:32 +1000 Subject: [PATCH 23/26] Update gabrielBB action to aganders headless gui --- .github/workflows/test_and_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index b88cfd6..046fc98 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -59,7 +59,7 @@ jobs: # this runs the platform-specific tests declared in tox.ini - name: Test with tox - uses: GabrielBB/xvfb-action@v1 + uses: aganders3/headless-gui@v1 with: run: python -m tox env: From f9fc95607fd0e08a78a2cd6fa12351cc106c1322 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Tue, 18 Apr 2023 12:06:27 +1000 Subject: [PATCH 24/26] use setup-qt-libs --- .github/workflows/test_and_deploy.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 046fc98..d0cb258 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -34,12 +34,7 @@ jobs: python-version: ${{ matrix.python-version }} # these libraries enable testing on Qt on linux - - name: Install Linux libraries - if: runner.os == 'Linux' - run: | - sudo apt-get install -y libdbus-1-3 libxkbcommon-x11-0 libxcb-icccm4 \ - libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \ - libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 + - uses: tlambert03/setup-qt-libs@v1 # strategy borrowed from vispy for installing opengl libs on windows - name: Install Windows OpenGL From 5dcd6dd291c494b9399b159c1ae15a7ea6c05707 Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Tue, 18 Apr 2023 12:33:20 +1000 Subject: [PATCH 25/26] update test_and_deploy to use latest versions of tests --- .github/workflows/test_and_deploy.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index d0cb258..d1fedd3 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -23,13 +23,13 @@ jobs: strategy: matrix: platform: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.7, 3.8, 3.9] + python-version: ['3.8', '3.9', '3.10'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -61,7 +61,7 @@ jobs: PLATFORM: ${{ matrix.platform }} - name: Coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 deploy: # this will run when you have tagged a commit, starting with "v*" @@ -71,9 +71,9 @@ jobs: runs-on: ubuntu-latest if: contains(github.ref, 'tags') steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install dependencies From 0b03144cb983f624db23fa67e062733ada531ceb Mon Sep 17 00:00:00 2001 From: jamesyan-git Date: Tue, 18 Apr 2023 12:54:36 +1000 Subject: [PATCH 26/26] fix tox.ini having two passenv on one line --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index e31d94a..c957678 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,9 @@ envlist = py{37,38,39}-{linux,macos,windows} [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [gh-actions:env] PLATFORM = @@ -22,7 +22,8 @@ platform = passenv = CI GITHUB_ACTIONS - DISPLAY XAUTHORITY + DISPLAY + XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION PYVISTA_OFF_SCREEN deps =