Skip to content

Commit

Permalink
Add widget to apply transform and produce new layer (#78)
Browse files Browse the repository at this point in the history
Work in progress to apply affine transformation to another layer.
Still need to add tests but it seems to be working.

---------

Co-authored-by: Juan Nunez-Iglesias <[email protected]>
  • Loading branch information
andreasmarnold and jni authored Oct 30, 2023
1 parent e373c44 commit 40370a1
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/affinder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

from .affinder import start_affinder
from .copy_tf import copy_affine
from .apply_tf import apply_affine
30 changes: 29 additions & 1 deletion src/affinder/_tests/test_affinder.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from affinder import start_affinder, copy_affine
from affinder import start_affinder, copy_affine, apply_affine
from affinder.affinder import AffineTransformChoices
from skimage import data, transform
import numpy as np
from itertools import product
import zarr
import napari
import pytest
from scipy import ndimage as ndi


layer0_pts = np.array([[140.38371886,
Expand Down Expand Up @@ -109,3 +110,30 @@ def test_copy_affine():
widget = copy_affine()
widget(layer0, layer1)
np.testing.assert_allclose(layer0.affine, layer1.affine)


def test_apply_affine():
ref_im = np.random.random((5, 5))
mov_im = ndi.zoom(ref_im, 2, order=0)

ref_layer = napari.layers.Image(ref_im)
mov_layer = napari.layers.Image(mov_im)
mov_layer.affine = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]])

widget = apply_affine()
res_layer = widget(ref_layer, mov_layer)

np.testing.assert_allclose(res_layer[0], ref_im)


def test_apply_affine_nonimage():
ref_im = np.random.random((5, 5))
mov_pts = np.random.random((5, 2))

ref_layer = napari.layers.Image(ref_im)
mov_layer = napari.layers.Points(mov_pts)
mov_layer.affine = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]])

widget = apply_affine()
with pytest.raises(NotImplementedError):
widget(ref_layer, mov_layer)
107 changes: 107 additions & 0 deletions src/affinder/apply_tf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from magicgui import magic_factory
from skimage import transform
import numpy as np


def _apply_affine_image(image, affine, order, reference_shape):
"""Apply affine transformation to image.
Parameters
----------
image : numpy.ndarray
Image to be transformed.
affine : numpy.ndarray
Affine transformation matrix.
order : int
The order of the interpolation.
reference_shape : tuple
Shape of the output image.
Returns
-------
numpy.ndarray
Transformed image.
"""
if image.ndim == 2:
affine = tform_matrix_rc2xy(affine)
return transform.warp(
image,
np.linalg.inv(affine),
order=order,
output_shape=reference_shape,
preserve_range=True
)


@magic_factory
def apply_affine(
reference_layer: 'napari.layers.Layer',
moving_layer: 'napari.layers.Layer',
order: int = 0,
) -> 'napari.types.LayerDataTuple':
"""Apply current affine transformation to selected layer.
The input layer data will be transformed to match the reference layer.
Parameters
----------
reference_layer : napari.layers.Layer
Layer to use as reference for affine transformation.
moving_layer : napari.layers.Layer
Layer to apply affine to.
order : int in {0, 1, 2, 3, 4, 5}
The order of the interpolation.
Returns
-------
LayerDataTuple
Layer data tuple with transformed data.
"""
if 'Image' not in str(type(moving_layer)):
raise NotImplementedError(
'Only image transforms supported at this point.'
)

# Find the transformation relative to the reference image
affine = np.linalg.inv(reference_layer.affine) @ moving_layer.affine

# Apply the transformation
transformed = _apply_affine_image(
moving_layer.data, affine, order, reference_layer.data.shape
)

# Set the metadata
layertype = 'image'
ref_metadata = {
n: getattr(reference_layer, n)
for n in ['scale', 'translate', 'rotate', 'shear']
}
mov_metadata = moving_layer.as_layer_data_tuple()[1]
name = {'name': moving_layer.name + '_transformed'}

metadata = {**mov_metadata, **ref_metadata, **name}

return (transformed, metadata, layertype)


def tform_matrix_rc2xy(affine_matrix: np.ndarray):
"""Transpose the first and second indices of an affine matrix.
This makes the matrix match the (soon to be deprecated) skimage convention
in which the first row matches the second axis of a NumPy array and
vice-versa.
Parameters
----------
affine_matrix : numpy.ndarray (D+1, D+1)
An affine transformation matrix.
Returns:
numpy.ndarray :
The 'transposed' affine matrix.
"""
# swap columns
swapped_cols = affine_matrix[:, [1, 0, 2]]
# swap rows
swapped_both = swapped_cols[[1, 0, 2], :]
return swapped_both
7 changes: 6 additions & 1 deletion src/affinder/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ contributions:
title: Start affinder...
- id: affinder.copy_affine
python_name: affinder:copy_affine
title: Copy affine
title: Copy affine...
- id: affinder.apply_affine
python_name: affinder:apply_affine
title: Apply affine...
widgets:
- command: affinder.start_affinder
display_name: Start affinder
- command: affinder.copy_affine
display_name: Copy affine
- command: affinder.apply_affine
display_name: Apply affine

0 comments on commit 40370a1

Please sign in to comment.