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
6 changes: 3 additions & 3 deletions packages/core/python/itkwasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ We are glad you are here and appreciate your contribution. Please keep in mind o
```
git clone https://github.com/InsightSoftwareConsortium/ITK-Wasm
cd itk-wasm/packages/core/python/itkwasm
pip install hatch
hatch run download-pyodide
hatch run test
pixi global install hatch
pixi run download-pyodide
pixi run test
```
2 changes: 1 addition & 1 deletion packages/core/python/itkwasm/itkwasm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""itkwasm: Python interface to itk-wasm WebAssembly modules."""

__version__ = "1.0b195"
__version__ = "1.0b199"

from .interface_types import InterfaceTypes
from .image import Image, ImageType, ImageRegion
Expand Down
65 changes: 47 additions & 18 deletions packages/core/python/itkwasm/itkwasm/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ class ImageRegion:
size: Sequence[int] = field(default_factory=list)


def _buffered_region_size(data, dimension: int) -> Optional[Sequence[int]]:
"""The buffered region size implied by the shape of a pixel data buffer.

None when the buffer does not describe the region, so the current region is
kept. This is the case for a raveled buffer, and for the data: URI that
transiently occupies the field while pipeline output JSON is deserialized,
before the buffer it addresses is read into an array.
"""
shape = getattr(data, "shape", None)
if shape is None or len(shape) < dimension:
return None
return list(shape[:dimension][::-1])
Comment thread
thewtex marked this conversation as resolved.


@dataclass
class Image:
imageType: Union[ImageType, Dict] = field(default_factory=ImageType)
Expand All @@ -48,9 +62,6 @@ class Image:
bufferedRegion: Optional[ImageRegion] = None

def __post_init__(self):
if isinstance(self.imageType, dict):
self.imageType = ImageType(**self.imageType)

dimension = self.imageType.dimension
if len(self.origin) == 0:
self.origin += [
Expand All @@ -71,18 +82,36 @@ def __post_init__(self):
] * dimension

if self.bufferedRegion is None:
if self.data is not None and hasattr(self.data, 'shape'):
self.bufferedRegion = ImageRegion(
index=(0,) * dimension,
size=self.data.shape[:dimension][::-1],
)
else:
self.bufferedRegion = ImageRegion(
index=(
0,
)
* dimension,
size=self.size,
)
elif isinstance(self.bufferedRegion, dict):
self.bufferedRegion = ImageRegion(**self.bufferedRegion)
size = _buffered_region_size(self.data, dimension)
if size is None:
# A copy, so the buffered region does not track subsequent
# changes to the largest possible region
size = list(self.size)
self.bufferedRegion = ImageRegion(
index=[
0,
]
* dimension,
size=size,
)

def __setattr__(self, name, value):
# Dicts, e.g. from JSON, are converted however they are assigned
if name == "imageType" and isinstance(value, dict):
value = ImageType(**value)
elif name == "bufferedRegion" and isinstance(value, dict):
value = ImageRegion(**value)

super().__setattr__(name, value)

# The data buffer holds the buffered region -- keep the region
# consistent when data is assigned after construction.
if name == "data" and value is not None:
buffered_region = getattr(self, "bufferedRegion", None)
if buffered_region is None:
return
size = _buffered_region_size(value, self.imageType.dimension)
if size is not None and list(buffered_region.size) != size:
# A new region, so a shallow copy of the image does not modify
# the region of the image it was copied from
self.bufferedRegion = ImageRegion(index=list(buffered_region.index), size=size)
2 changes: 1 addition & 1 deletion packages/core/python/itkwasm/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ path = "itkwasm/__init__.py"
[tool.black]
line-length = 120

[tool.pixi.project]
[tool.pixi.workspace]
channels = ["conda-forge"]
platforms = ["win-64", "linux-64", "linux-aarch64", "osx-arm64"]

Expand Down
119 changes: 117 additions & 2 deletions packages/core/python/itkwasm/test/test_image.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import copy
from pathlib import Path

import itk

from itkwasm import Image, ImageType
from itkwasm import Image, ImageRegion, ImageType
from dataclasses import asdict
import numpy as np

Expand Down Expand Up @@ -43,6 +44,7 @@ def test_image_defaults():
assert isinstance(image.metadata, dict)
assert image.data == None


def test_image_with_data():
"""Test creating an Image with data."""
image_type = ImageType(
Expand Down Expand Up @@ -78,4 +80,117 @@ def test_image_with_data():
assert image.bufferedRegion.index[0] == 0
assert image.bufferedRegion.index[1] == 0
assert image.bufferedRegion.size[0] == 6
assert image.bufferedRegion.size[1] == 5
assert image.bufferedRegion.size[1] == 5


def test_image_data_set_after_construction():
"""The buffered region follows data assigned after construction."""
image = Image()
image.size = [4, 4]
image.data = np.arange(16, dtype=np.uint8).reshape((4, 4))

assert image.size == [4, 4]
assert list(image.bufferedRegion.index) == [0, 0]
assert list(image.bufferedRegion.size) == [4, 4]

# The largest possible region is not the buffered region
image.size = [8, 8]
assert list(image.bufferedRegion.size) == [4, 4]


def test_image_information_only():
"""An explicit buffered region is preserved."""
image = Image(
size=[4, 4],
bufferedRegion=ImageRegion(index=[0, 0], size=[0, 0]),
data=np.empty((0, 0), dtype=np.uint8),
)

assert image.size == [4, 4]
assert list(image.bufferedRegion.size) == [0, 0]


def test_image_buffered_region_does_not_alias_size():
"""The default buffered region is not the largest possible region."""
image = Image(size=[4, 4])
assert list(image.bufferedRegion.size) == [4, 4]

image.size[0] = 8
assert list(image.bufferedRegion.size) == [4, 4]


def test_image_data_preserves_buffered_region_index():
"""A buffered region index survives data assignment."""
image = Image(size=[8, 8], bufferedRegion=ImageRegion(index=[2, 2], size=[1, 1]))
image.data = np.zeros((4, 4), dtype=np.uint8)

assert list(image.bufferedRegion.index) == [2, 2]
assert list(image.bufferedRegion.size) == [4, 4]


def test_image_data_on_a_shallow_copy():
"""Data assigned to a shallow copy does not change the original region."""
image = Image(size=[4, 4], data=np.zeros((4, 4), dtype=np.uint8))
copied = copy.copy(image)
copied.data = np.zeros((2, 2), dtype=np.uint8)

assert list(image.bufferedRegion.size) == [4, 4]
assert list(copied.bufferedRegion.size) == [2, 2]


def test_image_vector_data():
"""The buffered region excludes the components axis."""
image_type = ImageType(
dimension=2,
componentType="uint8",
pixelType="VariableLengthVector",
components=3,
)
image = Image(imageType=image_type)
image.size = [5, 4]
image.data = np.zeros((4, 5, 3), dtype=np.uint8)

assert list(image.bufferedRegion.size) == [5, 4]


def test_image_type_assigned_a_dict():
"""A dict imageType is converted however it is assigned."""
image = Image()
image.imageType = {
"dimension": 3,
"componentType": "uint16",
"pixelType": "Scalar",
"components": 1,
}

assert isinstance(image.imageType, ImageType)
assert image.imageType.dimension == 3

image.size = [4, 3, 2]
image.data = np.zeros((2, 3, 4), dtype=np.uint16)
assert list(image.bufferedRegion.size) == [4, 3, 2]


def test_buffered_region_assigned_a_dict():
"""A dict bufferedRegion is converted however it is assigned."""
image = Image(size=[4, 4])
image.bufferedRegion = {"index": [0, 0], "size": [2, 2]}

assert isinstance(image.bufferedRegion, ImageRegion)
assert list(image.bufferedRegion.size) == [2, 2]

image.data = np.zeros((4, 4), dtype=np.uint8)
assert list(image.bufferedRegion.size) == [4, 4]


def test_image_data_without_a_region_shape():
"""Data that does not describe the buffered region leaves it alone."""
image = Image(size=[4, 4], bufferedRegion=ImageRegion(index=[0, 0], size=[4, 4]))

# A raveled buffer
image.data = np.zeros(16, dtype=np.uint8)
assert list(image.bufferedRegion.size) == [4, 4]

# A data: URI, as found in pipeline output JSON
image.data = "data:application/vnd.itk.path,data/data.raw"
assert list(image.bufferedRegion.size) == [4, 4]
3 changes: 2 additions & 1 deletion packages/core/python/itkwasm/test/test_image_from_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def test_image_from_array_explicit():
assert image.imageType.pixelType == PixelTypes.VariableLengthVector
assert image.imageType.components == 3


def test_image_from_array_buffered_region():
arr = np.random.rand(9, 9, 3)
image = image_from_array(arr, is_vector=True)
Expand All @@ -62,4 +63,4 @@ def test_image_from_array_buffered_region():
assert image.bufferedRegion.index[0] == 0
assert image.bufferedRegion.index[1] == 0
assert image.bufferedRegion.size[0] == 9
assert image.bufferedRegion.size[1] == 9
assert image.bufferedRegion.size[1] == 9
25 changes: 20 additions & 5 deletions packages/core/python/itkwasm/test/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ def test_pipeline_write_read_polydata():
assert out_mesh.GetNumberOfPoints() == 2903
assert out_mesh.GetNumberOfCells() == 3263


def test_pipeline_write_read_transform():
pipeline = Pipeline(test_input_dir / "transform-read-write-test.wasi.wasm")

Expand Down Expand Up @@ -384,8 +385,22 @@ def test_pipeline_write_read_transform():
assert transform.numberOfParameters == 12
assert transform.numberOfFixedParameters == 3
np.testing.assert_allclose(transform.fixedParameters, np.array([0.0, 0.0, 0.0]))
np.testing.assert_allclose(transform.parameters, np.array([
0.65631490118447, 0.5806583745824385, -0.4817536741017158,
-0.7407986817430222, 0.37486398378429736, -0.5573995934598175,
-0.14306664045479867, 0.7227121458012518, 0.676179776908723,
-65.99999999999997, 69.00000000000004, 32.000000000000036]))
np.testing.assert_allclose(
transform.parameters,
np.array(
[
0.65631490118447,
0.5806583745824385,
-0.4817536741017158,
-0.7407986817430222,
0.37486398378429736,
-0.5573995934598175,
-0.14306664045479867,
0.7227121458012518,
0.676179776908723,
-65.99999999999997,
69.00000000000004,
32.000000000000036,
]
),
)
Loading
Loading