Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2d onto 3d with euclidean, similarity transforms #58

Merged
merged 32 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e29507a
2d onto 3d with euclidean, similarity transforms
thanushipeiris Mar 5, 2022
069f965
reference dimensionality from reference layer
thanushipeiris Mar 12, 2022
8988159
tests pass but failed attempt at 2D 'tiling' onto 3D
thanushipeiris Mar 19, 2022
e799865
fixed 2D-3D bug in reset_view (but badly - might still cause issues f…
thanushipeiris Mar 19, 2022
49d4acc
2D_2D, 3D_2D tests work for moving Points, Vectors layers
thanushipeiris Mar 22, 2022
1c62f84
Merge branch 'main' of https://github.com/jni/affinder into mixed-dim…
thanushipeiris Mar 23, 2022
9eb6a24
2D_3D tests work
thanushipeiris Apr 3, 2022
f17ce04
keep original moving image option
thanushipeiris Apr 5, 2022
5fbbb6c
yapf formatting
thanushipeiris Apr 5, 2022
dd15f8a
changed test assert sensitivity
thanushipeiris Apr 20, 2022
32a45c2
don't assume 2D layer in add_zeros_at_start_of_last_axis
thanushipeiris Apr 20, 2022
8343a21
cleaning comments, formatting
thanushipeiris Apr 20, 2022
be5a2fc
minor edits to affine model error message
thanushipeiris Apr 20, 2022
6f1c9cc
Merge branch 'main' of https://github.com/jni/affinder into mixed-dim…
thanushipeiris Apr 29, 2022
ac8ff81
added required libraries
thanushipeiris Jul 27, 2023
e1b4417
merge from main
thanushipeiris Jul 27, 2023
58987df
merging from upstream main
thanushipeiris Nov 24, 2023
d4565c2
preserve dimensions of moving layer when transforming it onto lower d…
thanushipeiris Nov 26, 2023
9f1b5cc
updated 3D affine test
thanushipeiris Nov 26, 2023
308c005
affine matrix dimensions will forcefully match minimum dimensions of …
Dec 11, 2023
984c6df
Merge branch 'main' into mixed-dimensions
jni Dec 11, 2023
cd85f71
modified tests to include all affine transform types
Dec 12, 2023
389de3f
Merge branch 'main' into mixed-dimensions
jni Dec 12, 2023
c0a2b93
Use matrices directly, not napari Affine objects
jni Dec 12, 2023
9bff5c0
fix logic in matrix-only version of convert-ndims
jni Dec 12, 2023
b3fdcaa
Remove now-redundant matrix-specific function
jni Dec 12, 2023
03471e7
Fix ndim computation wtf
jni Dec 12, 2023
535f71d
Start cleaning up tests
jni Feb 13, 2024
27455f8
Update variable names and simplify assignments
jni Feb 15, 2024
0953f82
Use generated test data instead of .zarr
jni Feb 15, 2024
7e7a4d9
Add 3d-3d test
jni Feb 15, 2024
0d4e310
Merge branch 'main' into mixed-dimensions
jni Feb 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions src/affinder/_test_data.py
Original file line number Diff line number Diff line change
@@ -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()
22 changes: 0 additions & 22 deletions src/affinder/_tests/labels0.zarr/.zarray

This file was deleted.

Binary file removed src/affinder/_tests/labels0.zarr/0.0
Binary file not shown.
22 changes: 0 additions & 22 deletions src/affinder/_tests/labels1.zarr/.zarray

This file was deleted.

Binary file removed src/affinder/_tests/labels1.zarr/0.0
Binary file not shown.
190 changes: 144 additions & 46 deletions src/affinder/_tests/test_affinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,11 @@
import zarr
import napari
import pytest
from scipy import ndimage as ndi
from pathlib import Path
from copy import copy
from scipy import ndimage as ndi

layer0_pts = np.array([[140.38371886,
322.5390704], [181.91866481, 319.65803368],
[176.15659138, 259.1562627],
[140.14363246, 254.59462124]])
layer1_pts = np.array([[70.94741072,
117.37477536], [95.80911919, 152.00358359],
[143.16475439, 118.55866623],
[131.32584559, 83.33791256]])

# get reference and moving layer types
im0 = data.camera()
im1 = transform.rotate(im0[100:, 32:496], 60)
this_dir = Path(__file__).parent.absolute()
labels0 = zarr.open(this_dir / 'labels0.zarr', mode='r')
labels1 = zarr.open(this_dir / 'labels1.zarr', mode='r')
from affinder import _test_data as dat


def make_vector_border(layer_pts):
Expand All @@ -35,54 +22,165 @@ def make_vector_border(layer_pts):
return vectors


vectors0 = make_vector_border(layer0_pts)
vectors1 = make_vector_border(layer1_pts)
def generate_all_layer_types(image, pts, labels):
layers = [
napari.layers.Image(image),
#napari.layers.Shapes(pts, shape_type='polygon'),
napari.layers.Points(pts),
napari.layers.Labels(labels),
napari.layers.Vectors(make_vector_border(pts)),
]

return layers


layers2d = generate_all_layer_types(
dat.nuclei2d, dat.nuclei2d_points, dat.nuclei2d_labels
)
layers2d_transformed = generate_all_layer_types(
dat.nuclei2d_rotated_translated,
dat.nuclei2d_points_rotated_translated,
dat.nuclei2d_labels_rotated_translated,
)
layers3d = generate_all_layer_types(
dat.nuclei, dat.nuclei_points, dat.nuclei_labels
)
layers3d_transformed = generate_all_layer_types(
dat.nuclei_rotated_translated,
dat.nuclei_points_rotated_translated,
dat.nuclei_labels_rotated_translated,
)


# 2D as reference, 2D as moving
@pytest.mark.parametrize(
"reference,moving,model_class",
product(layers2d, layers2d_transformed, AffineTransformChoices)
)
def test_2d_2d(make_napari_viewer, tmp_path, reference, moving, model_class):
"""Test a 2D reference layer with a 2D moving layer."""

viewer = make_napari_viewer()

l0 = viewer.add_layer(reference)
l0.name = "layer0"

l1 = viewer.add_layer(moving)
l1.name = 'layer1'

affinder_widget = start_affinder()
affinder_widget(
viewer=viewer,
reference=l0,
moving=l1,
model=model_class,
output=tmp_path / 'my_affine.txt'
)

viewer.layers['layer0_pts'].data = dat.nuclei2d_points
viewer.layers['layer1_pts'].data = dat.nuclei2d_points_rotated_translated

actual_affine = np.asarray(l1.affine)

ref = [
napari.layers.Image(im0),
napari.layers.Shapes(layer0_pts),
napari.layers.Points(layer0_pts),
napari.layers.Labels(labels0),
napari.layers.Vectors(vectors0),
]
mov = [
napari.layers.Image(im1),
napari.layers.Shapes(layer1_pts),
napari.layers.Points(layer1_pts),
napari.layers.Labels(labels1),
napari.layers.Vectors(vectors1),
]
# TODO add tracks layer types, after multidim affine support added
model = model_class.value(dimensionality=2)
model.estimate(
viewer.layers['layer1_pts'].data, viewer.layers['layer0_pts'].data
)
expected_affine = model.params

np.testing.assert_allclose(
actual_affine, expected_affine, rtol=10, atol=1e-10
)


# 3D as reference, 2D as moving
@pytest.mark.parametrize(
"reference,moving,model_class",
product(layers3d, layers2d_transformed, AffineTransformChoices)
)
def test_3d_2d(make_napari_viewer, tmp_path, reference, moving, model_class):
"""Test a 3D reference layer with a 2D moving layer.

@pytest.mark.parametrize("reference,moving", [p for p in product(ref, mov)])
def test_layer_types(make_napari_viewer, tmp_path, reference, moving):
The estimation dimension is always the minimum of the two, so this test
uses 2D points to estimate the transform.
"""

viewer = make_napari_viewer()

l0 = viewer.add_layer(reference)
l0.name = 'layer0'
l0.name = "layer0"

l1 = viewer.add_layer(moving)
l1.name = 'layer1'
l1.name = "layer1"

my_widget_factory = start_affinder()
my_widget_factory(
affinder_widget = start_affinder()
affinder_widget(
viewer=viewer,
reference=l0,
moving=l1,
model=AffineTransformChoices.affine,
model=model_class,
output=tmp_path / 'my_affine.txt'
)

viewer.layers['layer0_pts'].data = layer0_pts
viewer.layers['layer1_pts'].data = layer1_pts
viewer.layers['layer0_pts'].data = dat.nuclei2d_points
viewer.layers['layer1_pts'].data = dat.nuclei2d_points_rotated_translated

actual_affine = np.asarray(l1.affine)
expected_affine = np.array([[0.48155037, 0.85804854, 5.43577937],
[-0.88088632, 0.49188026, 328.20642821],
[0., 0., 1.]])

np.testing.assert_allclose(actual_affine, expected_affine)
model = model_class.value(dimensionality=2)
model.estimate(
viewer.layers['layer1_pts'].data, viewer.layers['layer0_pts'].data
)
expected_affine = model.params

np.testing.assert_allclose(
actual_affine, expected_affine, rtol=10, atol=1e-10
)


@pytest.mark.parametrize(
"reference,moving,model_class",
product(layers3d, layers3d_transformed, AffineTransformChoices)
)
def test_3d_3d(make_napari_viewer, tmp_path, reference, moving, model_class):
"""Test a 3D reference layer with a 3D moving layer.

Point clicking in 3D is hard but this should still work, for example if
you combine it with a plugin such as napari-threedee to click on points
in 3D space.
"""

viewer = make_napari_viewer()

l0 = viewer.add_layer(reference)
l0.name = "layer0"

l1 = viewer.add_layer(moving)
l1.name = "layer1"

affinder_widget = start_affinder()
affinder_widget(
viewer=viewer,
reference=l0,
moving=l1,
model=model_class,
output=tmp_path / 'my_affine.txt'
)

viewer.layers['layer0_pts'].data = dat.nuclei_points
viewer.layers['layer1_pts'].data = dat.nuclei_points_rotated_translated

actual_affine = np.asarray(l1.affine)

model = model_class.value(dimensionality=3)
model.estimate(
viewer.layers['layer1_pts'].data, viewer.layers['layer0_pts'].data
)
expected_affine = model.params

np.testing.assert_allclose(
actual_affine, expected_affine, rtol=10, atol=1e-10
)


def test_ensure_different_layers(make_napari_viewer):
Expand Down
Loading
Loading