{ "cells": [ { "cell_type": "markdown", "source": [ "# Working with Image Data\n", "\n", "This notebook shows how to transfer image data between Python and a running MITK Workbench.\n", "\n", "You will learn how to:\n", "- Create an `Image` from a numpy array with spatial properties (spacing, origin)\n", "- Upload it to the Workbench via `node.set_data()`\n", "- Download it back via `node.get_data()`, with and without data-scope properties\n", "- Change display properties (color, opacity)\n", "- Save raw data to disk via `node.save_data()` (works for any data type)\n", "- Convert to/from SimpleITK (optional)\n", "\n", "**Prerequisites:** A running MITK Workbench instance with the REST API enabled." ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## 1. Connect to a running Workbench\n", "\n", "`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." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "import numpy as np\n", "import mitk_workbench_remote as mw\n", "\n", "instances = mw.discover(timeout=2) #longer timeouts to make the example more robust for latencies.\n", "if not instances:\n", " raise SystemExit(\"No running MITK Workbench found. Start one and try again.\")\n", "\n", "wb = instances[0]\n", "print(f\"Connected to: {wb.info.name} (MITK {wb.info.mitk_version}) at {wb.url}\")" ], "metadata": { "collapsed": false } }, { "cell_type": "markdown", "source": [ "## 2. Create an Image from a numpy array\n", "\n", "`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).\n", "\n", "All data transfer in `mitk-workbench-remote` goes through `Image`. Conversion to other types (SimpleITK, mlarray) is explicit and opt-in." ], "metadata": { "collapsed": false } }, { "cell_type": "code", "id": "4pdqs7yrsvs", "source": [ "size = 64\n", "arr = np.sum(np.indices((size, size, size), dtype=np.float32), axis=0)\n", "arr /= arr.max()\n", "\n", "image = mw.Image(arr, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0))\n", "print(f\"Image: shape={image.shape}, spacing={image.spacing}, dtype={image.dtype}\")\n", "print(f\"Value range: [{arr.min():.3f}, {arr.max():.3f}]\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "lkwu71pgobs", "source": [ "## 3. Upload to the Workbench\n", "\n", "To display data in the Workbench you need two steps:\n", "1. **Create a node** in the DataStorage via `wb.storage.create(name)` -- this is an empty container.\n", "2. **Upload data** via `node.set_data(image)` -- this transfers the pixel data and spatial metadata.\n", "\n", "`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.\n", "\n", "After uploading, call `wb.reinit([node])` to fit the render windows to the new data." ], "metadata": {} }, { "cell_type": "code", "id": "zkd9v2uh3s", "source": [ "node = wb.storage.create(\"Gradient Ramp\")\n", "node.set_data(image)\n", "wb.reinit([node])\n", "print(f\"Uploaded '{node.name}' to Workbench (uid={node.uid})\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "code", "id": "e79m5t1q0o", "source": [ "node" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "zlb5jkecuvc", "source": [ "## 4. Download and verify roundtrip\n", "\n", "`node.get_data()` downloads the node's data and returns a typed Python object based on the node's `data_type`:\n", "- `\"Image\"` -> `mw.Image`\n", "- `\"MultiLabelSegmentation\"` -> `mw.MultiLabelSegmentation`\n", "\n", "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.\n", "\n", "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." ], "metadata": {} }, { "cell_type": "code", "id": "lldg8rwo9ol", "source": [ "downloaded_image = node.get_data()\n", "print(f\"Downloaded: shape={downloaded_image.shape}, spacing={downloaded_image.spacing}\")\n", "\n", "match = np.allclose(image.array, downloaded_image.array)\n", "print(f\"Pixel values match original: {match}\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "code", "execution_count": null, "outputs": [], "source": [ "downloaded_image" ], "metadata": { "collapsed": false } }, { "cell_type": "code", "id": "87565de6", "source": [ "from mitk_workbench_remote import SpatialImage\n", "\n", "# mw.Image satisfies the SpatialImage protocol\n", "print(f\"Is SpatialImage? {isinstance(downloaded_image, SpatialImage)}\")\n", "\n", "# Write generic functions that accept any SpatialImage\n", "def print_geometry(img: SpatialImage) -> None:\n", " print(f\" ndim={img.ndim}, shape={img.shape}\")\n", " print(f\" spacing={img.spacing}\")\n", " print(f\" origin={img.origin}\")\n", "\n", "print_geometry(downloaded_image)" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "4gardrgeegw", "source": [ "### Fetching data-scope properties alongside the image\n", "\n", "Passing `include_properties=True` makes `get_data()` perform an additional request to retrieve\n", "all data-scope properties that MITK has attached to the data of the node (e.g. reader metadata,\n", "custom keys, etc.) and store them in the returned `Image`'s `properties` dict.\n", "\n", "Three things to keep in mind:\n", "\n", "- **Snapshot, not a live view.** The properties are fetched once at call time. If they change\n", " on the Workbench afterwards the local `Image` object will not reflect that — you would need\n", " to call `get_data(include_properties=True)` again.\n", "- **Data-scope only.** This fetches *data* properties, not the node's display properties\n", " (color, opacity, visibility). Use `node.get_properties(scope=PropertyScope.NODE)` for those.\n", "- **There may be more.** The Workbench can hold additional properties that are not shown here,\n", " for example properties set by render plugins or by other clients after your download." ], "metadata": {} }, { "cell_type": "code", "id": "62j8vi0d2e7", "source": [ "img_with_props = node.get_data(include_properties=True)\n", "\n", "# Show the difference: without vs. with fetched properties\n", "print(f\"Without include_properties: {len(downloaded_image.properties)} property entries\")\n", "print(f\"With include_properties: {len(img_with_props.properties)} property entries\")\n", "\n", "# Property access API (aligned with mitk.Image)\n", "print(f\"\\nProperty keys: {img_with_props.property_keys[:5]}\") # show first 5\n", "\n", "# Read a single property by key\n", "modality = img_with_props.get_property(\"dicom.series.Modality\")\n", "print(f\"DICOM modality: {modality}\")\n", "\n", "# Set a custom property\n", "img_with_props.set_property(\"my_annotation\", \"reviewed\")\n", "print(f\"Custom property: {img_with_props.get_property('my_annotation')}\")\n", "\n", "# The rich display now shows the fetched properties table\n", "img_with_props" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "33tqt4pfmab", "source": [ "## 5. Change display properties\n", "\n", "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.\n", "\n", "For bulk changes, use `node.update_properties(visible=True, opacity=0.5)` to batch them into a single request.\n", "\n", "After changing properties, call `wb.update()` to trigger a render window redraw." ], "metadata": {} }, { "cell_type": "code", "id": "we0ncrycthl", "source": [ "node.color = (1.0, 0.0, 0.0) # red tint\n", "node.opacity = 0.7\n", "wb.update()\n", "print(f\"Color: {node.color}\")\n", "print(f\"Opacity: {node.opacity}\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "ez1g6b1z9kb", "source": [ "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:" ], "metadata": {} }, { "cell_type": "code", "id": "6hfrqxok9h3", "source": [ "from mitk_workbench_remote.node import PropertyScope\n", "\n", "# Batch-update multiple node properties in one request\n", "node.update_properties(scope=PropertyScope.NODE, visible=True, opacity=1.0)\n", "\n", "# Read all data-scope properties (MITK fills these after data upload)\n", "data_props = node.get_properties(scope=PropertyScope.DATA)\n", "print(\"Data-scope properties:\")\n", "for key, value in list(data_props.items())[:8]: # show first 8\n", " print(f\" {key}: {value}\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "b7pfyxvb1nj", "source": [ "## 6. Save raw data to disk\n", "\n", "`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.\n", "\n", "Use this to:\n", "- Archive data from the Workbench to disk\n", "- Hand off data to external tools that read MITK data serialized in files\n", "- Download data types that are not yet supported by this library" ], "metadata": {} }, { "cell_type": "code", "id": "b3dso3nxyl", "source": [ "from pathlib import Path\n", "\n", "output_path = Path(\"gradient_ramp.nrrd\")\n", "saved = node.save_data(output_path)\n", "print(f\"Saved to: {saved}\")\n", "print(f\"File size: {saved.stat().st_size} bytes\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "g0yvl6qqytv", "source": [ "Contrast with `get_data()`, which raises `UnsupportedDataTypeError` upfront for types it cannot represent in Python — without even starting a download:" ], "metadata": {} }, { "cell_type": "code", "id": "mqo8lvackv8", "source": [ "# Example: what happens when get_data() encounters an unsupported type.\n", "# (This code is illustrative — the node above is an Image, so it would succeed.)\n", "from mitk_workbench_remote import UnsupportedDataTypeError\n", "\n", "surface_nodes = wb.storage.list(data_type=\"Surface\")\n", "if surface_nodes:\n", " surface = surface_nodes[0]\n", " try:\n", " surface.get_data() # raises before any HTTP download\n", " except UnsupportedDataTypeError as e:\n", " print(f\"Cannot load in memory: {e}\")\n", " # Fall back to saving to disk\n", " surface.save_data(Path(f\"{surface.name}.nrrd\"))\n", " print(\"Saved to disk instead.\")\n", "else:\n", " print(\"No Surface nodes in this Workbench — skipping example.\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "061mmpm88qud", "source": [ "## 7. SimpleITK conversion (optional)\n", "\n", "`mw.Image` integrates with SimpleITK when it is installed. Conversion is always explicit:\n", "- `image.to_simpleitk()` — convert a downloaded_image `Image` to `SimpleITK.Image`, including spatial metadata\n", "- `mw.Image(sitk_image)` — wrap a `SimpleITK.Image` for upload; spacing, origin, and direction are extracted automatically\n", "\n", "This lets you use the full SimpleITK filter ecosystem and upload the result straight back to the Workbench." ], "metadata": {} }, { "cell_type": "code", "id": "cx7pdvvvjr", "source": [ "try:\n", " import SimpleITK\n", "except ImportError:\n", " SimpleITK = None\n", " print(\"SimpleITK not installed — skipping this section.\")\n", " print(\"Install with: pip install SimpleITK\")\n", "\n", "if SimpleITK is not None:\n", " # Convert the downloaded_image Image to SimpleITK and apply a binary threshold\n", " sitk_image = downloaded_image.to_simpleitk()\n", " print(f\"SimpleITK image: size={sitk_image.GetSize()}, spacing={sitk_image.GetSpacing()}\")\n", "\n", " thresholded = SimpleITK.BinaryThreshold(\n", " sitk_image, lowerThreshold=0.3, upperThreshold=0.7\n", " )\n", "\n", " # Wrap result as mw.Image and upload — spacing/origin/direction are preserved\n", " threshold_image = mw.Image(thresholded)\n", " node.set_data(threshold_image)\n", " node.color = (0.0, 1.0, 0.0) # green to distinguish from original\n", " wb.update()\n", " print(f\"Uploaded thresholded image: shape={threshold_image.shape}, dtype={threshold_image.dtype}\")" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "mitk-interop-intro", "source": [ "## 8. Interop with the `mitk` package\n", "\n", "> **Note:** This section requires the `mitk` package, which is only available\n", "> inside MITK's Python environment. Run these cells inside that environment.\n", "> All cells below are non-executing in the docs build.\n", "\n", "`DataNode.get_data()` accepts an `as_type` keyword of type `mw.DataRepresentation`\n", "that controls whether you get back an `mw.Image` or a native `mitk.Image`:\n", "\n", "| `as_type` | `mitk` installed? | Return type |\n", "|---|---|---|\n", "| `AUTO` (default) | yes | `mitk.Image` |\n", "| `AUTO` (default) | no | `mw.Image` |\n", "| `REMOTE` | either | `mw.Image` |\n", "| `MITK` | yes | `mitk.Image` |\n", "| `MITK` | no | `ImportError` |" ], "metadata": {} }, { "cell_type": "code", "id": "mitk-interop-auto", "source": [ "# AUTO: returns mitk.Image when mitk is available, mw.Image otherwise\n", "# doctest: +SKIP\n", "data = node.get_data() # auto-detects mitk availability\n", "print(type(data)) # mitk.Image or mw.Image" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "code", "id": "mitk-interop-explicit", "source": [ "# Explicit opt-in: always request mitk.Image (ImportError if mitk absent)\n", "# doctest: +SKIP\n", "mitk_img = node.get_data(as_type=mw.DataRepresentation.MITK)\n", "\n", "print(mitk_img.get_spacing())\n", "print(mitk_img.property_keys[:5])\n", "\n", "import numpy as np\n", "arr = np.asarray(mitk_img)\n", "print(f'Pixel array shape: {arr.shape}, dtype: {arr.dtype}')" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "code", "id": "mitk-interop-upload", "source": [ "# Upload a mitk.Image back -- the MitkImageConverter handles serialisation automatically\n", "# doctest: +SKIP\n", "local = mw.Image(np.zeros((64, 64, 64), dtype=np.uint8))\n", "mitk_native = local.to_mitk() # mw.Image -> mitk.Image\n", "\n", "node.set_data(mitk_native) # converter picks up mitk.Image transparently\n", "print('Upload via mitk.Image succeeded')" ], "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "id": "evb6t9ttwqh", "source": [ "## 9. Clean up\n", "\n", "Remove the node from the DataStorage when done. Use `recursive=True` to also remove child nodes." ], "metadata": {} }, { "cell_type": "code", "id": "2eauy1onbcm", "source": [ "node.remove()\n", "\n", "# Also clean up the saved file from section 6\n", "output_path.unlink(missing_ok=True)\n", "\n", "print(\"Done.\")" ], "metadata": {}, "execution_count": null, "outputs": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 5 }