Working with Image Data

This notebook shows how to transfer image data between Python and a running MITK Workbench.

You will learn how to:

  • Create an Image from a numpy array with spatial properties (spacing, origin)

  • Upload it to the Workbench via node.set_data()

  • Download it back via node.get_data(), with and without data-scope properties

  • Change display properties (color, opacity)

  • Save raw data to disk via node.save_data() (works for any data type)

  • Convert to/from SimpleITK (optional)

Prerequisites: A running MITK Workbench instance with the REST API enabled.

1. Connect to a running Workbench

mw.discover() probes localhost ports 8080-8099 for running MITK Workbench instances and returns a list of Workbench handles. Each handle is an independent connection – no global state is shared.

[ ]:
import numpy as np
import mitk_workbench_remote as mw

instances = mw.discover(timeout=2) #longer timeouts to make the example more robust for latencies.
if not instances:
    raise SystemExit("No running MITK Workbench found. Start one and try again.")

wb = instances[0]
print(f"Connected to: {wb.info.name} (MITK {wb.info.mitk_version}) at {wb.url}")

2. Create an Image from a numpy array

mw.Image is the universal spatial image type. It wraps a numpy array together with spatial metadata: spacing (voxel size), origin (world-space position), and direction (orientation cosine matrix).

All data transfer in mitk-workbench-remote goes through Image. Conversion to other types (SimpleITK, mlarray) is explicit and opt-in.

[ ]:
size = 64
arr = np.sum(np.indices((size, size, size), dtype=np.float32), axis=0)
arr /= arr.max()

image = mw.Image(arr, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0))
print(f"Image: shape={image.shape}, spacing={image.spacing}, dtype={image.dtype}")
print(f"Value range: [{arr.min():.3f}, {arr.max():.3f}]")

3. Upload to the Workbench

To display data in the Workbench you need two steps:

  1. Create a node in the DataStorage via wb.storage.create(name) – this is an empty container.

  2. Upload data via node.set_data(image) – this transfers the pixel data and spatial metadata.

set_data() accepts Image, numpy arrays, or any type registered in the converter registry (e.g. SimpleITK.Image). The transfer mode (direct HTTP body or file-reference for large data on localhost) is negotiated automatically.

After uploading, call wb.reinit([node]) to fit the render windows to the new data.

[ ]:
node = wb.storage.create("Gradient Ramp")
node.set_data(image)
wb.reinit([node])
print(f"Uploaded '{node.name}' to Workbench (uid={node.uid})")
[ ]:
node

4. Download and verify roundtrip

node.get_data() downloads the node’s data and returns a typed Python object based on the node’s data_type:

  • "Image" -> mw.Image

  • "MultiLabelSegmentation" -> mw.MultiLabelSegmentation

For unsupported data types (e.g. Surface, PointSet), get_data() raises UnsupportedDataTypeError before any download happens. Use node.save_data(path) instead to save the raw bytes to disk.

By default only pixel data and spatial metadata (spacing, origin, direction) are transferred. Pass include_properties=True to also fetch data-scope properties from the Workbench — see the subsection below.

[ ]:
downloaded_image = node.get_data()
print(f"Downloaded: shape={downloaded_image.shape}, spacing={downloaded_image.spacing}")

match = np.allclose(image.array, downloaded_image.array)
print(f"Pixel values match original: {match}")
[ ]:
downloaded_image
[ ]:
from mitk_workbench_remote import SpatialImage

# mw.Image satisfies the SpatialImage protocol
print(f"Is SpatialImage? {isinstance(downloaded_image, SpatialImage)}")

# Write generic functions that accept any SpatialImage
def print_geometry(img: SpatialImage) -> None:
    print(f"  ndim={img.ndim}, shape={img.shape}")
    print(f"  spacing={img.spacing}")
    print(f"  origin={img.origin}")

print_geometry(downloaded_image)

Fetching data-scope properties alongside the image

Passing include_properties=True makes get_data() perform an additional request to retrieve all data-scope properties that MITK has attached to the data of the node (e.g. reader metadata, custom keys, etc.) and store them in the returned Image’s properties dict.

Three things to keep in mind:

  • Snapshot, not a live view. The properties are fetched once at call time. If they change on the Workbench afterwards the local Image object will not reflect that — you would need to call get_data(include_properties=True) again.

  • Data-scope only. This fetches data properties, not the node’s display properties (color, opacity, visibility). Use node.get_properties(scope=PropertyScope.NODE) for those.

  • There may be more. The Workbench can hold additional properties that are not shown here, for example properties set by render plugins or by other clients after your download.

[ ]:
img_with_props = node.get_data(include_properties=True)

# Show the difference: without vs. with fetched properties
print(f"Without include_properties: {len(downloaded_image.properties)} property entries")
print(f"With    include_properties: {len(img_with_props.properties)} property entries")

# Property access API (aligned with mitk.Image)
print(f"\nProperty keys: {img_with_props.property_keys[:5]}")  # show first 5

# Read a single property by key
modality = img_with_props.get_property("dicom.series.Modality")
print(f"DICOM modality: {modality}")

# Set a custom property
img_with_props.set_property("my_annotation", "reviewed")
print(f"Custom property: {img_with_props.get_property('my_annotation')}")

# The rich display now shows the fetched properties table
img_with_props

5. Change display properties

Every DataNode exposes common display properties as Python attributes: name, visible, opacity, color. These are live — each read or write makes a REST call to the Workbench. No state is cached.

For bulk changes, use node.update_properties(visible=True, opacity=0.5) to batch them into a single request.

After changing properties, call wb.update() to trigger a render window redraw.

[ ]:
node.color = (1.0, 0.0, 0.0)   # red tint
node.opacity = 0.7
wb.update()
print(f"Color: {node.color}")
print(f"Opacity: {node.opacity}")

You can also read and write arbitrary properties by key. MITK stores many data-derived properties (scalar range, active rendering mode, etc.) that you can inspect or override:

[ ]:
from mitk_workbench_remote.node import PropertyScope

# Batch-update multiple node properties in one request
node.update_properties(scope=PropertyScope.NODE, visible=True, opacity=1.0)

# Read all data-scope properties (MITK fills these after data upload)
data_props = node.get_properties(scope=PropertyScope.DATA)
print("Data-scope properties:")
for key, value in list(data_props.items())[:8]:   # show first 8
    print(f"  {key}: {value}")

6. Save raw data to disk

node.save_data(path) downloads the raw serialized bytes of the data object and writes them directly to a file — no parsing, no in-memory representation. This works for any data type, including types that mitk-workbench-remote cannot handle. In comparison to get_data() this function will always work as long as the MITK Workbench instance you are communicating with knows how to serialize the data.

Use this to:

  • Archive data from the Workbench to disk

  • Hand off data to external tools that read MITK data serialized in files

  • Download data types that are not yet supported by this library

[ ]:
from pathlib import Path

output_path = Path("gradient_ramp.nrrd")
saved = node.save_data(output_path)
print(f"Saved to: {saved}")
print(f"File size: {saved.stat().st_size} bytes")

Contrast with get_data(), which raises UnsupportedDataTypeError upfront for types it cannot represent in Python — without even starting a download:

[ ]:
# Example: what happens when get_data() encounters an unsupported type.
# (This code is illustrative — the node above is an Image, so it would succeed.)
from mitk_workbench_remote import UnsupportedDataTypeError

surface_nodes = wb.storage.list(data_type="Surface")
if surface_nodes:
    surface = surface_nodes[0]
    try:
        surface.get_data()   # raises before any HTTP download
    except UnsupportedDataTypeError as e:
        print(f"Cannot load in memory: {e}")
        # Fall back to saving to disk
        surface.save_data(Path(f"{surface.name}.nrrd"))
        print("Saved to disk instead.")
else:
    print("No Surface nodes in this Workbench — skipping example.")

7. SimpleITK conversion (optional)

mw.Image integrates with SimpleITK when it is installed. Conversion is always explicit:

  • image.to_simpleitk() — convert a downloaded_image Image to SimpleITK.Image, including spatial metadata

  • mw.Image(sitk_image) — wrap a SimpleITK.Image for upload; spacing, origin, and direction are extracted automatically

This lets you use the full SimpleITK filter ecosystem and upload the result straight back to the Workbench.

[ ]:
try:
    import SimpleITK
except ImportError:
    SimpleITK = None
    print("SimpleITK not installed — skipping this section.")
    print("Install with: pip install SimpleITK")

if SimpleITK is not None:
    # Convert the downloaded_image Image to SimpleITK and apply a binary threshold
    sitk_image = downloaded_image.to_simpleitk()
    print(f"SimpleITK image: size={sitk_image.GetSize()}, spacing={sitk_image.GetSpacing()}")

    thresholded = SimpleITK.BinaryThreshold(
        sitk_image, lowerThreshold=0.3, upperThreshold=0.7
    )

    # Wrap result as mw.Image and upload — spacing/origin/direction are preserved
    threshold_image = mw.Image(thresholded)
    node.set_data(threshold_image)
    node.color = (0.0, 1.0, 0.0)   # green to distinguish from original
    wb.update()
    print(f"Uploaded thresholded image: shape={threshold_image.shape}, dtype={threshold_image.dtype}")

8. Interop with the mitk package

Note: This section requires the mitk package, which is only available inside MITK’s Python environment. Run these cells inside that environment. All cells below are non-executing in the docs build.

DataNode.get_data() accepts an as_type keyword of type mw.DataRepresentation that controls whether you get back an mw.Image or a native mitk.Image:

as_type

mitk installed?

Return type

AUTO (default)

yes

mitk.Image

AUTO (default)

no

mw.Image

REMOTE

either

mw.Image

MITK

yes

mitk.Image

MITK

no

ImportError

[ ]:
# AUTO: returns mitk.Image when mitk is available, mw.Image otherwise
# doctest: +SKIP
data = node.get_data()  # auto-detects mitk availability
print(type(data))       # mitk.Image  or  mw.Image
[ ]:
# Explicit opt-in: always request mitk.Image (ImportError if mitk absent)
# doctest: +SKIP
mitk_img = node.get_data(as_type=mw.DataRepresentation.MITK)

print(mitk_img.get_spacing())
print(mitk_img.property_keys[:5])

import numpy as np
arr = np.asarray(mitk_img)
print(f'Pixel array shape: {arr.shape}, dtype: {arr.dtype}')
[ ]:
# Upload a mitk.Image back -- the MitkImageConverter handles serialisation automatically
# doctest: +SKIP
local = mw.Image(np.zeros((64, 64, 64), dtype=np.uint8))
mitk_native = local.to_mitk()   # mw.Image -> mitk.Image

node.set_data(mitk_native)      # converter picks up mitk.Image transparently
print('Upload via mitk.Image succeeded')

9. Clean up

Remove the node from the DataStorage when done. Use recursive=True to also remove child nodes.

[ ]:
node.remove()

# Also clean up the saved file from section 6
output_path.unlink(missing_ok=True)

print("Done.")