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.

[1]:
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}")
Connected to: MITK Workbench REST API (MITK 2026.06.00) at http://localhost:8080

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.

[2]:
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}]")
Image: shape=(64, 64, 64), spacing=(1.0, 1.0, 1.0), dtype=float32
Value range: [0.000, 1.000]

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.

[3]:
node = wb.storage.create("Gradient Ramp")
node.set_data(image)
wb.reinit([node])
print(f"Uploaded '{node.name}' to Workbench (uid={node.uid})")
Uploaded 'Gradient Ramp' to Workbench (uid=node_7)
[4]:
node
[4]:
uidnode_7
nameGradient Ramp
data_typeImage
path/Gradient Ramp
parent_uid

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.

[5]:
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: shape=(64, 64, 64), spacing=(1.0, 1.0, 1.0)
Pixel values match original: True
[6]:
downloaded_image
[6]:
<mitk.Image at 0x1cff53c3730>
[7]:
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)
Is SpatialImage? True
  ndim=3, shape=(64, 64, 64)
  spacing=(1.0, 1.0, 1.0)
  origin=(0.0, 0.0, 0.0)

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.

A few 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.

  • Different source than a plain download. A plain get_data() already surfaces any properties MITK embedded in the NRRD header, while include_properties=True fetches the node’s live data-scope set from the Workbench. The two need not match in size, and for a freshly uploaded synthetic image they may even be equal – so the next cell compares the set of keys, not just the counts.

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

# A plain get_data() only recovers whatever properties were embedded in the NRRD
# header; include_properties=True instead fetches the node's live data-scope
# property set from the Workbench. These are different sources -- neither is
# guaranteed to be a superset of the other -- so compare the *keys*, not counts.
without_keys = set(downloaded_image.property_keys)
with_keys = set(img_with_props.property_keys)
print(f"Without include_properties: {len(without_keys)} keys")
print(f"With    include_properties: {len(with_keys)} keys")
print(f"Only present after fetching from the Workbench: {sorted(with_keys - without_keys)}")
print(f"Only present in the plain (NRRD-header) download: {sorted(without_keys - with_keys)}")

# 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
Without include_properties: 12 keys
With    include_properties: 12 keys
Only present after fetching from the Workbench: []
Only present in the plain (NRRD-header) download: []

Property keys: ['ITK.InputFilterName', 'MITK.IO.reader.description', 'MITK.IO.reader.inputlocation', 'MITK.IO.reader.mime.category', 'MITK.IO.reader.mime.name']
DICOM modality: None
Custom property: reviewed
[8]:
<mitk.Image at 0x1cff56f91b0>

5. Change display properties

Every DataNode exposes common display properties as Python attributes: name, visible, opacity, color. These are live, so 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.

[9]:
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}")
Color: (1.0, 0.0, 0.0)
Opacity: 0.699999988079071

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:

[10]:
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}")
Data-scope properties:
  ITK.InputFilterName: NrrdImageIO
  MITK.IO.reader.description: ITK NrrdImageIO
  MITK.IO.reader.inputlocation: C:\Users\<user>\AppData\Local\Temp\tmp8kaabb_9.nrrd
  MITK.IO.reader.mime.category: Images
  MITK.IO.reader.mime.name: application/vnd.mitk.image.nrrd
  MITK.IO.reader.version: 2026.06.00
  NRRD.space: left-posterior-superior
  path: C:\Users\<user>\AppData\Local\Temp\tmp8kaabb_9.nrrd

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

[11]:
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")
Saved to: gradient_ramp.nrrd
File size: 19446 bytes

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

[13]:
# 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.")
Cannot load in memory: Data type 'Surface' is not supported for in-memory representation. Use save_data() to download the raw bytes to a file.
Saved to disk instead.

7. SimpleITK conversion (optional)

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

  • mw.Image.to_simpleitk() – convert an mw.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.

Heads-up: to_simpleitk() lives only on mw.Image. With the default as_type=AUTO, node.get_data() returns a native mitk.Image whenever the mitk package is installed (see Interop with the mitk package), and that type has no to_simpleitk(). Request the remote wrapper explicitly with node.get_data(as_type=mw.DataRepresentation.REMOTE) before converting, as the next cell does.

[14]:
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:
    # get_data() with as_type=AUTO may return a native mitk.Image, which has no
    # to_simpleitk(); request the remote wrapper explicitly for SimpleITK interop.
    remote_image = node.get_data(as_type=mw.DataRepresentation.REMOTE)
    sitk_image = remote_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}")
SimpleITK image: size=(64, 64, 64), spacing=(1.0, 1.0, 1.0)
Uploaded thresholded image: shape=(64, 64, 64), dtype=uint8

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

[15]:
# 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
<class 'mitk.Image'>
[16]:
# 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}')
(1.0, 1.0, 1.0)
['ITK.InputFilterName', 'MITK.IO.reader.description', 'MITK.IO.reader.inputlocation', 'MITK.IO.reader.mime.category', 'MITK.IO.reader.mime.name']
Pixel array shape: (64, 64, 64), dtype: uint8
[17]:
# 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')
Upload via mitk.Image succeeded

9. Clean up

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

[19]:
node.remove()

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

print("Done.")
Done.