diff --git a/requirements.txt b/requirements.txt index b02edbd..cc31bfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ napari-plugin-engine>=0.1.9 napari>=0.4.3 numpy -scikit-image +scikit-image>=0.19.2 magicgui>=0.2.5,!=0.2.7 -napari toolz +zarr \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index fb10c31..5f3d71c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ install_requires = napari>=0.4.17 npe2>=0.1.2 numpy - scikit-image + scikit-image>=0.19.2 magicgui>=0.3.7 toolz python_requires = >=3.9 diff --git a/src/affinder/_test_data.py b/src/affinder/_test_data.py new file mode 100644 index 0000000..26b40b5 --- /dev/null +++ b/src/affinder/_test_data.py @@ -0,0 +1,72 @@ +import numpy as np +from scipy import ndimage as ndi +from skimage import data, feature, filters, util, segmentation, morphology, transform +import toolz as tz + +median_filter = tz.curry(ndi.median_filter) +remove_holes = tz.curry(morphology.remove_small_holes) +remove_objects = tz.curry(morphology.remove_small_objects) + + +@tz.curry +def threshold_with(image, method=filters.threshold_li): + return image > method(image) + + +to_origin = np.array([0, -127.5, -127.5]) +c = np.cos(np.radians(60)) +s = np.sin(np.radians(60)) +rot60 = np.array([ + [1, 0, 0], + [0, c, -s], + [0, s, c], + ]) +from_origin = -to_origin +trans = np.array([0, 5, 10]) + +nuclei = data.cells3d()[:, 1, ...] +nuclei_rotated = ndi.rotate(nuclei, 60, axes=(1, 2), reshape=False) +nuclei_rotated_translated = ndi.shift(nuclei_rotated, trans) +nuclei_points = feature.peak_local_max(filters.gaussian(nuclei, 15)) + +nuclei_points_rotated_translated = ((nuclei_points+to_origin) @ rot60.T + + from_origin + trans) + +nuclei_binary = tz.pipe( + nuclei, + median_filter(size=3), + threshold_with(method=filters.threshold_li), + remove_holes(area_threshold=20**3), + remove_objects(min_size=20**3), + ) +nuclei_labels = segmentation.watershed( + filters.farid(nuclei), + markers=util.label_points(nuclei_points, nuclei.shape), + mask=nuclei_binary, + ) +nuclei_labels_rotated = ndi.rotate( + nuclei_labels, 60, axes=(1, 2), reshape=False, order=0 + ) +nuclei_labels_rotated_translated = ndi.shift(nuclei_labels, trans, order=0) + +nuclei2d = nuclei[30] +nuclei2d_points = nuclei_points[:, 1:] # remove z = project onto yx +nuclei2d_rotated = nuclei_rotated[30] +nuclei2d_rotated_translated = nuclei_rotated_translated[30] +nuclei2d_labels = nuclei_labels[30] +nuclei2d_labels_rotated_translated = nuclei_labels_rotated_translated[30] +nuclei2d_points_rotated_translated = nuclei_points_rotated_translated[:, 1:] + +if __name__ == '__main__': + import napari + viewer = napari.Viewer(ndisplay=3) + viewer.add_image(nuclei, blending='additive') + viewer.add_points(nuclei_points) + viewer.add_labels(nuclei_labels, blending='translucent_no_depth') + viewer.add_image(nuclei_rotated_translated, blending='additive') + viewer.add_points(nuclei_points_rotated_translated, face_color='red') + viewer.add_labels(nuclei_labels_rotated, blending='translucent_no_depth') + + viewer.grid.enabled = True + viewer.grid.stride = 3 + napari.run() diff --git a/src/affinder/_tests/labels0.zarr/.zarray b/src/affinder/_tests/labels0.zarr/.zarray deleted file mode 100644 index 31d4d6a..0000000 --- a/src/affinder/_tests/labels0.zarr/.zarray +++ /dev/null @@ -1,22 +0,0 @@ -{ - "chunks": [ - 512, - 512 - ], - "compressor": { - "blocksize": 0, - "clevel": 5, - "cname": "lz4", - "id": "blosc", - "shuffle": 1 - }, - "dtype": " ndim: - mat = calculate_transform(pts0, pts1, model_class=model_class) - moving_image_layer.affine = ( - reference_image_layer.affine.affine_matrix @ mat.params + mat = calculate_transform( + pts0, pts1, ndim, model_class=model_class ) - moving_points_layer.affine = ( - reference_image_layer.affine.affine_matrix @ mat.params + ref_mat = reference_image_layer.affine.affine_matrix + # must shrink ndims of affine matrix if dims of image layer is bigger than moving layer ##### + if reference_image_layer.ndim > moving_image_layer.ndim: + ref_mat = convert_affine_to_ndims( + ref_mat, moving_image_layer.ndim + ) + # must pad affine matrix with identity matrix if dims of moving layer smaller ##### + moving_image_layer.affine = convert_affine_to_ndims( + (ref_mat @ mat.params), moving_image_layer.ndim ) if output is not None: np.savetxt(output, np.asarray(mat.params), delimiter=',') @@ -91,6 +105,22 @@ def remove_pts_layers(viewer, layers): viewer.layers.remove(layer) +def convert_affine_to_ndims(affine, target_ndim): + """Either embed or slice an affine matrix to match the target ndims.""" + affine_matrix = np.asarray(affine) + diff = np.shape(affine_matrix)[0] - 1 - target_ndim + if diff == 0: + out = affine_matrix + elif diff < 0: + # target is larger, so embed + out = np.identity(target_ndim + 1) + out[-diff:, -diff:] = affine_matrix + else: # diff > 0 + out = affine_matrix[diff:, diff:] + + return out + + def _update_unique_choices(widget, choice_name): """Update the selected choice in a ComboBox widget to be unique. @@ -129,7 +159,10 @@ def _on_affinder_main_init(widget): widget_init=_on_affinder_main_init, call_button='Start', layout='vertical', - output={'mode': 'w', 'label': 'Save transformation as', 'filter': '*.txt'}, + output={ + 'mode': 'w', 'label': 'Save transformation as', 'filter': + '*.txt' + }, viewer={'visible': False, 'label': ' '}, delete_pts={ 'label': @@ -154,6 +187,7 @@ def start_affinder( mode = start_affinder._call_button.text # can be "Start" or "Finish" if mode == 'Start': + # focus on the reference layer reset_view(viewer, reference) # set points layer for each image @@ -161,20 +195,23 @@ def start_affinder( # Use C0 and C1 from matplotlib color cycle points_layers_to_add = [(reference, (0.122, 0.467, 0.706, 1.0)), (moving, (1.0, 0.498, 0.055, 1.0))] + # make points layer if it was not specified + estimation_ndim = min(reference.ndim, moving.ndim) for i in range(len(points_layers)): if points_layers[i] is None: layer, color = points_layers_to_add[i] new_layer = viewer.add_points( - ndim=layer.ndim, + ndim=estimation_ndim, # ndims of all points layers same lowest ndim of reference or moving name=layer.name + '_pts', - affine=layer.affine, + affine=convert_affine_to_ndims( + layer.affine, estimation_ndim + ), face_color=[color], ) points_layers[i] = new_layer pts_layer0 = points_layers[0] pts_layer1 = points_layers[1] - # make a callback for points added callback = next_layer_callback( viewer=viewer, @@ -183,7 +220,7 @@ def start_affinder( moving_image_layer=moving, moving_points_layer=pts_layer1, model_class=model.value, - output=output, + output=output ) pts_layer0.events.data.connect(callback) pts_layer1.events.data.connect(callback) @@ -210,7 +247,7 @@ def start_affinder( start_affinder._call_button.text = 'Start' -def calculate_transform(src, dst, model_class=AffineTransform): +def calculate_transform(src, dst, ndim, model_class=AffineTransform): """Calculate transformation matrix from matched coordinate pairs. Parameters @@ -227,6 +264,11 @@ def calculate_transform(src, dst, model_class=AffineTransform): transform scikit-image Transformation object """ - model = model_class() - model.estimate(dst, src) # we want the inverse + # convert points to correct dimension (from right bottom corner) + # pos_val = lambda x: x if x > 0 else 0 + + # do transform + model = model_class(dimensionality=ndim) + model.estimate(dst, src) # we want + # the inverse return model