Skip to content

Commit

Permalink
feat(Python): Support pipeline stream inputs/outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
thewtex committed Oct 12, 2022
1 parent bfe7c62 commit 837876a
Show file tree
Hide file tree
Showing 22 changed files with 397 additions and 45 deletions.
2 changes: 1 addition & 1 deletion src/core/Mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type TypedArray from './TypedArray.js'
class Mesh {
meshType: MeshType

name: string = 'Mesh'
name: string = 'mesh'

numberOfPoints: number
points: null | TypedArray
Expand Down
31 changes: 27 additions & 4 deletions src/python/itkwasm/itkwasm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
"""itkwasm: Python interface to itk-wasm WebAssembly modules."""

__version__ = "1.0b2"
__version__ = "1.0b3"

from .interface_types import InterfaceTypes
from .image import Image, ImageType
from .mesh import Mesh, MeshType
from .pointset import PointSet, PointSetType
from .mesh import Mesh, MeshType
from .polydata import PolyData, PolyDataType
from .binary_file import BinaryFile
from .binary_stream import BinaryStream
from .text_file import TextFile
from .text_stream import TextStream
from .pipeline import Pipeline
from .pipeline_input import PipelineInput
from .pipeline_output import PipelineOutput
from .float_types import FloatTypes
from .int_types import IntTypes
from .pixel_types import PixelTypes

__all__ = [
"InterfaceTypes",
"Pipeline",
"PipelineInput",
"PipelineOutput",
"Image",
"ImageType",
"Mesh",
"MeshType",
"PointSet",
"PointSetType",
"Mesh",
"MeshType",
"PolyData",
"PolyDataType",
"BinaryFile",
"BinaryStream",
"TextFile",
"TextStream",
"FloatTypes",
"IntTypes",
"PixelTypes",
]
6 changes: 6 additions & 0 deletions src/python/itkwasm/itkwasm/binary_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from dataclasses import dataclass

@dataclass
class BinaryFile:
data: bytes
path: str
5 changes: 5 additions & 0 deletions src/python/itkwasm/itkwasm/binary_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from dataclasses import dataclass

@dataclass
class BinaryStream:
data: bytes
12 changes: 12 additions & 0 deletions src/python/itkwasm/itkwasm/float_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum

class FloatTypes(str, Enum):
Float32 = 'float32'
Float64 = 'float64'
SpacePrecisionType = 'float64'

def __str__(self):
return str(self.value)

def __repr__(self):
return f'"{str(self.value)}"'
50 changes: 37 additions & 13 deletions src/python/itkwasm/itkwasm/image.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
from dataclasses import dataclass
from dataclasses import dataclass, field

from typing import Sequence
from typing import Sequence, Union, Dict, Optional

try:
from numpy.typing import ArrayLike
except ImportError:
from numpy import ndarray as ArrayLike
import numpy as np

from .int_types import IntTypes
from .float_types import FloatTypes
from .pixel_types import PixelTypes

@dataclass
class ImageType:
dimension: int
componentType: str
pixelType: str
components: int
dimension: int = 2
componentType: Union[IntTypes, FloatTypes] = IntTypes.UInt8
pixelType: PixelTypes = PixelTypes.Scalar
components: int = 1

@dataclass
class Image:
imageType: ImageType
data: ArrayLike
size: Sequence[int]
origin: Sequence[float]
spacing: Sequence[float]
direction: ArrayLike
name: str
imageType: Union[ImageType, Dict] = ImageType()
name: str = 'image'
origin: Sequence[float] = field(default_factory=list)
spacing: Sequence[float] = field(default_factory=list)
direction: ArrayLike = np.empty((0,), np.float32)
size: Sequence[int] = field(default_factory=list)
metadata: Dict = field(default_factory=dict)
data: Optional[ArrayLike] = 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 += [0.0,] * dimension

if len(self.spacing) == 0:
self.spacing += [1.0,] * dimension

if len(self.direction) == 0:
self.direction = np.eye(dimension).astype(np.float32).ravel()

if len(self.size) == 0:
self.size += [1,] * dimension

22 changes: 22 additions & 0 deletions src/python/itkwasm/itkwasm/int_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from enum import Enum

class IntTypes(str, Enum):
Int8 = 'int8'
UInt8 = 'uint8'
Int16 = 'int16'
UInt16 = 'uint16'
Int32 = 'int32'
UInt32 = 'uint32'
Int64 = 'int64'
UInt64 = 'uint64'

SizeValueType = 'uint64'
IdentifierType = 'uint64'
IndexValueType = 'int64'
OffsetValueType = 'int64'

def __str__(self):
return str(self.value)

def __repr__(self):
return f'"{str(self.value)}"'
10 changes: 10 additions & 0 deletions src/python/itkwasm/itkwasm/interface_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from enum import Enum

class InterfaceTypes(Enum):
TextFile = 'InterfaceTextFile'
BinaryFile = 'InterfaceBinaryFile'
TextStream = 'InterfaceTextStream'
BinaryStream = 'InterfaceBinaryStream'
Image = 'InterfaceImage'
Mesh = 'InterfaceMesh'
PolyData = 'InterfacePolyData'
50 changes: 29 additions & 21 deletions src/python/itkwasm/itkwasm/mesh.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,50 @@
from dataclasses import dataclass

from typing import Optional
from typing import Optional, Union, Dict

try:
from numpy.typing import ArrayLike
except ImportError:
from numpy import ndarray as ArrayLike

from .float_types import FloatTypes
from .int_types import IntTypes
from .pixel_types import PixelTypes

@dataclass
class MeshType:
dimension: int
dimension: int = 2

pointComponentType: str
pointPixelComponentType: str
pointPixelType: str
pointPixelComponents: int
pointComponentType: Union[IntTypes, FloatTypes] = FloatTypes.Float32
pointPixelComponentType: Union[IntTypes, FloatTypes] = FloatTypes.Float32
pointPixelType: PixelTypes = PixelTypes.Scalar
pointPixelComponents: int = 1

cellComponentType: str
cellPixelComponentType: str
cellPixelType: str
cellPixelComponents: int
cellComponentType: Union[IntTypes, FloatTypes] = IntTypes.Int32
cellPixelComponentType: Union[IntTypes, FloatTypes] = FloatTypes.Float32
cellPixelType: PixelTypes = PixelTypes.Scalar
cellPixelComponents: int = 1


@dataclass
class Mesh:
meshType: MeshType
meshType: Union[MeshType, Dict] = MeshType()

name: str = 'mesh'

name: str
numberOfPoints: int = 0
points: Optional[ArrayLike] = None

numberOfPoints: int
points: Optional[ArrayLike]
numberOfPointPixels: int = 0
pointData: Optional[ArrayLike] = None

numberOfPointPixels: int
pointData: Optional[ArrayLike]
numberOfCells: int = 0
cells: Optional[ArrayLike] = None
cellBufferSize: int = 0

numberOfCells: int
cells: Optional[ArrayLike]
cellBufferSize: int
numberOfCellPixels: int = 0
cellData: Optional[ArrayLike] = None

numberOfCellPixels: int
cellData: Optional[ArrayLike]
def __post_init__(self):
if isinstance(self.meshType, dict):
self.meshType = MeshType(**self.meshType)
64 changes: 62 additions & 2 deletions src/python/itkwasm/itkwasm/pipeline.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import json
from pathlib import Path
from typing import List, Union
from typing import List, Union, Dict
from .interface_types import InterfaceTypes
from .pipeline_input import PipelineInput
from .pipeline_output import PipelineOutput
from .text_stream import TextStream
from .binary_stream import BinaryStream

from wasmer import engine, wasi, Store, Module, ImportObject, Instance
from wasmer_compiler_cranelift import Compiler
Expand All @@ -20,7 +26,7 @@ def __init__(self, pipeline: Union[str, Path, bytes]):
self.module = Module(self.store, wasm_bytes)
self.wasi_version = wasi.get_version(self.module, strict=True)

def run(self, args: List[str], outputs=[], inputs=[], preopen_directories=[], map_directories={}, environments={}):
def run(self, args: List[str], outputs: List[PipelineOutput]=[], inputs: List[PipelineInput]=[], preopen_directories=[], map_directories={}, environments={}) -> List[PipelineOutput]:
"""Run the itk-wasm pipeline."""

wasi_state = wasi.StateBuilder('itk-wasm-pipeline')
Expand All @@ -32,12 +38,66 @@ def run(self, args: List[str], outputs=[], inputs=[], preopen_directories=[], ma
import_object = wasi_env.generate_import_object(self.store, self.wasi_version)

instance = Instance(self.module, import_object)
self.memory = instance.exports.memory
self.input_array_alloc = instance.exports.itk_wasm_input_array_alloc
self.input_json_alloc = instance.exports.itk_wasm_input_json_alloc
self.output_array_address = instance.exports.itk_wasm_output_array_address
self.output_array_size = instance.exports.itk_wasm_output_array_size
self.output_json_address = instance.exports.itk_wasm_output_json_address
self.output_json_size = instance.exports.itk_wasm_output_json_size

_initialize = instance.exports._initialize
_initialize()

for index, input_ in enumerate(inputs):
if input_.type == InterfaceTypes.TextStream:
data_array = input_.data.data.encode()
array_ptr = self._set_input_array(data_array, index, 0)
data_json = { "size": len(data_array), "data": f"data:application/vnd.itk.address,0:{array_ptr}" }
self._set_input_json(data_json, index)
elif input_.type == InterfaceTypes.BinaryStream:
data_array = input_.data.data
array_ptr = self._set_input_array(data_array, index, 0)
data_json = { "size": len(data_array), "data": f"data:application/vnd.itk.address,0:{array_ptr}" }
self._set_input_json(data_json, index)
else:
raise ValueError(f'Unexpected/not yet supported input.type {input_.type}')

delayed_start = instance.exports.itk_wasm_delayed_start
return_code = delayed_start()

populated_outputs: List[PipelineOutput] = []
if len(outputs) and return_code == 0:
for index, output in enumerate(outputs):
output_data = None
if output.type == InterfaceTypes.TextStream:
data_ptr = self.output_array_address(0, index, 0)
data_size = self.output_array_size(0, index, 0)
data_array_view = bytearray(memoryview(self.memory.buffer)[data_ptr:data_ptr+data_size])
output_data = PipelineOutput(InterfaceTypes.TextStream, TextStream(data_array_view.decode()))
elif output.type == InterfaceTypes.BinaryStream:
data_ptr = self.output_array_address(0, index, 0)
data_size = self.output_array_size(0, index, 0)
data_array = bytes(memoryview(self.memory.buffer)[data_ptr:data_ptr+data_size])
output_data = PipelineOutput(InterfaceTypes.BinaryStream, BinaryStream(data_array))
populated_outputs.append(output_data)

delayed_exit = instance.exports.itk_wasm_delayed_exit
delayed_exit(return_code)

# Should we be returning the return_code?
return populated_outputs

def _set_input_array(self, data_array: bytes, input_index: int, sub_index: int) -> int:
data_ptr = 0
if data_array != None:
data_ptr = self.input_array_alloc(0, input_index, sub_index, len(data_array))
buf = memoryview(self.memory.buffer)
buf[data_ptr:data_ptr+len(data_array)] = data_array
return data_ptr

def _set_input_json(self, data_object: Dict, input_index: int) -> None:
data_json = json.dumps(data_object).encode()
json_ptr = self.input_json_alloc(0, input_index, len(data_json))
buf = memoryview(self.memory.buffer)
buf[json_ptr:json_ptr+len(data_json)] = data_json
17 changes: 17 additions & 0 deletions src/python/itkwasm/itkwasm/pipeline_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from dataclasses import dataclass
from typing import Optional, Union

from .interface_types import InterfaceTypes
from .text_file import TextFile
from .text_stream import TextStream
from .binary_file import BinaryFile
from .binary_stream import BinaryStream
from .image import Image
from .mesh import Mesh
from .polydata import PolyData

@dataclass
class PipelineInput:
type: InterfaceTypes
data: Union[str, bytes, TextStream, BinaryStream, TextFile, BinaryFile, Image, Mesh, PolyData]
path: Optional[str] = None
17 changes: 17 additions & 0 deletions src/python/itkwasm/itkwasm/pipeline_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from dataclasses import dataclass
from typing import Optional, Union

from .interface_types import InterfaceTypes
from .text_file import TextFile
from .text_stream import TextStream
from .binary_file import BinaryFile
from .binary_stream import BinaryStream
from .image import Image
from .mesh import Mesh
from .polydata import PolyData

@dataclass
class PipelineOutput:
type: InterfaceTypes
data: Optional[Union[str, bytes, TextStream, BinaryStream, TextFile, BinaryFile, Image, Mesh, PolyData]] = None
path: Optional[str] = None
Loading

0 comments on commit 837876a

Please sign in to comment.