Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 40 additions & 5 deletions itk_napari_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,34 @@ def image_from_image_layer(image_layer):
return image


def points_layer_from_point_set(point_set):
"""Convert an itk.PointSet to a napari.layers.Points."""
def points_layer_from_point_set(point_set, reverse_coords=True):
"""Convert an itk.PointSet to a napari.layers.Points.

Parameters
----------
point_set : itk.PointSet
The ITK PointSet to convert.
reverse_coords : bool, optional
If True (default), reverse the coordinate order from ITK's x,y,z
to napari's z,y,x order.

Returns
-------
napari.layers.Points
The converted napari Points layer.
"""
# Get points as numpy array
number_of_points = point_set.GetNumberOfPoints()

if number_of_points == 0:
data = np.array([]).reshape(0, 3) # Default to 3D empty array
else:
points_array = itk.array_from_vector_container(point_set.GetPoints())
data = points_array
if reverse_coords:
# Reverse coordinate order from ITK (x,y,z) to napari (z,y,x)
data = points_array[:, ::-1]
else:
data = points_array
Comment on lines -74 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main part of the fix, right? Cool! I can't wait to try it out, with SuperElastix/elastix-napari#65 😃


# Get point data (features) if available
point_data = point_set.GetPointData()
Expand All @@ -92,8 +110,22 @@ def points_layer_from_point_set(point_set):
return points_layer


def point_set_from_points_layer(points_layer):
"""Convert a napari.layers.Points to an itk.PointSet."""
def point_set_from_points_layer(points_layer, reverse_coords=True):
"""Convert a napari.layers.Points to an itk.PointSet.

Parameters
----------
points_layer : napari.layers.Points
The napari Points layer to convert.
reverse_coords : bool, optional
If True (default), reverse the coordinate order from napari's z,y,x
to ITK's x,y,z order.

Returns
-------
itk.PointSet
The converted ITK PointSet.
"""
# Apply transformations (rotate, scale, translate) to points
data = points_layer.data.copy() # Make a copy to avoid modifying original

Expand Down Expand Up @@ -122,6 +154,9 @@ def point_set_from_points_layer(points_layer):

# Set points
if len(data) > 0:
if reverse_coords:
# Reverse coordinate order from napari (z,y,x) to ITK (x,y,z)
data = data[:, ::-1]
Comment on lines +157 to +159
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, the other "main" part of the fix 😃

points = itk.vector_container_from_array(data.astype(np.float32).flatten())
point_set.SetPoints(points)

Expand Down
160 changes: 132 additions & 28 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,24 +175,25 @@ def test_points_layer_from_point_set():
PointSetType = itk.PointSet[itk.F, 3]
point_set = PointSetType.New()

# Add some points
# Add some points (ITK order: x,y,z)
points_data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32)
points = itk.vector_container_from_array(points_data.flatten())
point_set.SetPoints(points)

# Convert to napari points layer
# Convert to napari points layer with default reverse_coords=True
points_layer = itk_napari_conversion.points_layer_from_point_set(point_set)

# Check that data matches
assert np.allclose(points_data, points_layer.data)
# Check that data is reversed (napari order: z,y,x)
expected = points_data[:, ::-1]
assert np.allclose(expected, points_layer.data)


def test_points_layer_from_point_set_with_features():
# Create a 3D point set with point data
PointSetType = itk.PointSet[itk.F, 3]
point_set = PointSetType.New()

# Add points
# Add points (ITK order: x,y,z)
points_data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32)
points = itk.vector_container_from_array(points_data.flatten())
point_set.SetPoints(points)
Expand All @@ -202,41 +203,44 @@ def test_points_layer_from_point_set_with_features():
point_data = itk.vector_container_from_array(feature_data)
point_set.SetPointData(point_data)

# Convert to napari points layer
# Convert to napari points layer with default reverse_coords=True
points_layer = itk_napari_conversion.points_layer_from_point_set(point_set)

# Check that data matches
assert np.allclose(points_data, points_layer.data)
# Check that data is reversed (napari order: z,y,x)
expected = points_data[:, ::-1]
assert np.allclose(expected, points_layer.data)
assert points_layer.features is not None
assert 'feature' in points_layer.features
assert np.allclose(feature_data, points_layer.features['feature'])


def test_point_set_from_points_layer():
# Create napari points layer
# Create napari points layer (napari order: z,y,x)
data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
points_layer = napari.layers.Points(data)

# Convert to ITK point set
# Convert to ITK point set with default reverse_coords=True
point_set = itk_napari_conversion.point_set_from_points_layer(points_layer)

# Check points
# Check points are reversed (ITK order: x,y,z)
points_array = itk.array_from_vector_container(point_set.GetPoints())
assert np.allclose(data, points_array)
expected = data[:, ::-1]
assert np.allclose(expected, points_array)


def test_point_set_from_points_layer_with_features():
# Create napari points layer with features
# Create napari points layer with features (napari order: z,y,x)
data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
features = {'feature': np.array([10.0, 20.0, 30.0])}
points_layer = napari.layers.Points(data, features=features)

# Convert to ITK point set
# Convert to ITK point set with default reverse_coords=True
point_set = itk_napari_conversion.point_set_from_points_layer(points_layer)

# Check points
# Check points are reversed (ITK order: x,y,z)
points_array = itk.array_from_vector_container(point_set.GetPoints())
assert np.allclose(data, points_array)
expected = data[:, ::-1]
assert np.allclose(expected, points_array)

# Check point data - verify it was set
point_data = point_set.GetPointData()
Expand All @@ -246,37 +250,37 @@ def test_point_set_from_points_layer_with_features():


def test_point_set_from_points_layer_with_scale():
# Create napari points layer with scale
# Create napari points layer with scale (napari order: z,y,x)
data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
scale = np.array([2.0, 3.0, 4.0])
points_layer = napari.layers.Points(data, scale=scale)

# Convert to ITK point set
# Convert to ITK point set with default reverse_coords=True
point_set = itk_napari_conversion.point_set_from_points_layer(points_layer)

# Check that points are scaled
# Check that points are scaled and reversed (ITK order: x,y,z)
points_array = itk.array_from_vector_container(point_set.GetPoints())
expected = data * scale
expected = (data * scale)[:, ::-1]
assert np.allclose(expected, points_array)


def test_point_set_from_points_layer_with_translate():
# Create napari points layer with translation
# Create napari points layer with translation (napari order: z,y,x)
data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
translate = np.array([10.0, 20.0, 30.0])
points_layer = napari.layers.Points(data, translate=translate)

# Convert to ITK point set
# Convert to ITK point set with default reverse_coords=True
point_set = itk_napari_conversion.point_set_from_points_layer(points_layer)

# Check that points are translated
# Check that points are translated and reversed (ITK order: x,y,z)
points_array = itk.array_from_vector_container(point_set.GetPoints())
expected = data + translate
expected = (data + translate)[:, ::-1]
assert np.allclose(expected, points_array)


def test_point_set_from_points_layer_with_rotate():
# Create napari points layer with rotation
# Create napari points layer with rotation (napari order: z,y,x)
data = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])

# 90 degree rotation around z-axis
Expand All @@ -289,11 +293,111 @@ def test_point_set_from_points_layer_with_rotate():

points_layer = napari.layers.Points(data, rotate=rotate)

# Convert to ITK point set
# Convert to ITK point set with default reverse_coords=True
point_set = itk_napari_conversion.point_set_from_points_layer(points_layer)

# Check that points are rotated
# Check that points are rotated and reversed (ITK order: x,y,z)
points_array = itk.array_from_vector_container(point_set.GetPoints())
expected = data @ rotate.T
expected = (data @ rotate.T)[:, ::-1]
assert np.allclose(expected, points_array, atol=1e-10)


def test_points_layer_from_point_set_no_reverse():
"""Test points_layer_from_point_set with reverse_coords=False."""
# Create a simple 3D point set
PointSetType = itk.PointSet[itk.F, 3]
point_set = PointSetType.New()

# Add some points (ITK order: x,y,z)
points_data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32)
points = itk.vector_container_from_array(points_data.flatten())
point_set.SetPoints(points)

# Convert to napari points layer with reverse_coords=False
points_layer = itk_napari_conversion.points_layer_from_point_set(point_set, reverse_coords=False)

# Check that data is NOT reversed
assert np.allclose(points_data, points_layer.data)


def test_point_set_from_points_layer_no_reverse():
"""Test point_set_from_points_layer with reverse_coords=False."""
# Create napari points layer
data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
points_layer = napari.layers.Points(data)

# Convert to ITK point set with reverse_coords=False
point_set = itk_napari_conversion.point_set_from_points_layer(points_layer, reverse_coords=False)

# Check points are NOT reversed
points_array = itk.array_from_vector_container(point_set.GetPoints())
assert np.allclose(data, points_array)


def test_roundtrip_itk_to_napari_to_itk():
"""Test roundtrip conversion from ITK to napari and back to ITK."""
# Create a simple 3D point set with ITK coordinates (x,y,z)
PointSetType = itk.PointSet[itk.F, 3]
original_point_set = PointSetType.New()

# Add some points with distinct x,y,z values for testing
original_data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32)
points = itk.vector_container_from_array(original_data.flatten())
original_point_set.SetPoints(points)

# Convert ITK -> napari (reverses coords) -> ITK (reverses back)
points_layer = itk_napari_conversion.points_layer_from_point_set(original_point_set)
roundtrip_point_set = itk_napari_conversion.point_set_from_points_layer(points_layer)

# Check that the roundtrip produces the original data
roundtrip_array = itk.array_from_vector_container(roundtrip_point_set.GetPoints())
assert np.allclose(original_data, roundtrip_array)


def test_roundtrip_napari_to_itk_to_napari():
"""Test roundtrip conversion from napari to ITK and back to napari."""
# Create napari points layer with napari coordinates (z,y,x)
original_data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
original_layer = napari.layers.Points(original_data)

# Convert napari -> ITK (reverses coords) -> napari (reverses back)
point_set = itk_napari_conversion.point_set_from_points_layer(original_layer)
roundtrip_layer = itk_napari_conversion.points_layer_from_point_set(point_set)

# Check that the roundtrip produces the original data
assert np.allclose(original_data, roundtrip_layer.data)


def test_points_layer_from_point_set_2d():
"""Test points_layer_from_point_set with 2D points."""
# Create a simple 2D point set
PointSetType = itk.PointSet[itk.F, 2]
point_set = PointSetType.New()

# Add some points (ITK order: x,y)
points_data = np.array([[1.0, 2.0], [4.0, 5.0], [7.0, 8.0]], dtype=np.float32)
points = itk.vector_container_from_array(points_data.flatten())
point_set.SetPoints(points)

# Convert to napari points layer with default reverse_coords=True
points_layer = itk_napari_conversion.points_layer_from_point_set(point_set)

# Check that data is reversed (napari order: y,x)
expected = points_data[:, ::-1]
assert np.allclose(expected, points_layer.data)


def test_point_set_from_points_layer_2d():
"""Test point_set_from_points_layer with 2D points."""
# Create napari points layer (napari order: y,x)
data = np.array([[1.0, 2.0], [4.0, 5.0], [7.0, 8.0]])
points_layer = napari.layers.Points(data)

# Convert to ITK point set with default reverse_coords=True
point_set = itk_napari_conversion.point_set_from_points_layer(points_layer)

# Check points are reversed (ITK order: x,y)
points_array = itk.array_from_vector_container(point_set.GetPoints())
expected = data[:, ::-1]
assert np.allclose(expected, points_array)