|
Medical Imaging Interaction Toolkit
2026.06.00
Medical Imaging Interaction Toolkit
|
There are at least three closely intertwined aspects to consider when working with Python in MITK:
Each of these aspects comes with its own challenges, and their integration imposes certain restrictions and pitfalls that can affect one another. As such, even minor changes to one of these components require careful consideration of the others.
The foundation for making all of this work is to ensure that a consistent set of Python binaries is used across all aspects. These binaries and their environment must be isolated from any system Python or unrelated Python installations that may exist on the target platform.
Finally, the solution must function reliably across Windows, Linux, and macOS.
At first glance, solutions like Conda, Micromamba, or the officially provided embedded Python builds seem like silver bullets. Their advertised features and documentation suggest that robust, ready-to-use options already exist — even multiple ones to choose from.
However, it's a classic "too good to be true" scenario. All of these solutions share a fundamental flaw: they are not truly isolated. Instead, they are dynamically linked to various system libraries and other dependencies.
Sooner or later, you'll run into issues like failed builds or runtime crashes due to binary-incompatible versions of shared dependencies between MITK and the libraries of a Python package. These libraries are loaded dynamically at runtime and may export the same symbols, leading to symbol collisions, undefined behavior, or crashes.
The Standalone Python Builds project provides fully statically linked Python binaries and patches the CPython build system to use relative paths instead of absolute ones.
With these changes, Python is built from source across a wide matrix of versions, platforms, and build configurations, and distributed via GitHub releases.
These builds are fully standalone: just download, unzip, and run them on any machine, with no additional dependencies. They do not interfere with your project’s libraries or depend on system-level Python components, making them ideal for embedding or integration into complex applications like MITK.
The downside of this approach is that these Python builds are significantly larger in file size compared to their dynamically linked counterparts. However, this trade-off is acceptable given the improved isolation, reliability, and portability they provide.
We integrated the Standalone Python Builds as an external project in the MITK superbuild. When MITK_USE_Python3 is enabled (which is the default in our standard WorkbenchRelease configuration), this external project:
ep/src/Python3,pip, andnumpy package, which is required by MITK when Python is enabled.The project is then installed by copying ep/src/Python3/ into MITK-build/python.
Since our build system relies on CMake’s find_package() to locate external dependencies, we enforce consistency for calls to find_package(Python3) by passing both Python3_ROOT_DIR (set to MITK-build/python) and Python3_EXECUTABLE (the interpreter inside that directory) from the superbuild down to the MITK build itself. Python3_ROOT_DIR alone is only a search hint; find_package(Python3) still consults PATH and the Windows registry and prefers the newest version it finds there, so on a machine with a newer system Python the hint would be overruled. Pinning Python3_EXECUTABLE makes the MITK build skip that search and use the staged interpreter unconditionally.
MITK modules can then depend on Python either via the classic MITK syntax PACKAGE_DEPENDS Python3|Python or by using the native CMake target like TARGET_DEPENDS Python3::Python.
Historically, debug builds with Python have been problematic, as Python packages are rarely distributed in debug variants. As a result, debug builds are disabled in MITK when MITK_USE_Python3 is enabled. Developers should instead use the RelWithDebInfo configuration for debugging.
Leaving Python wrapping aside for now, the Python integration in MITK is split into three modules:
mitk::PythonContext class, which acts as the primary bridge between MITK and Python.In most cases, you won't need to interact with the MitkPreloadPython module directly. To run the Python interpreter as a separate process, use MitkPythonHelper. To exchange data (e.g., images) between MITK and Python, use MitkPython.
The MitkPython module ships two complementary test binaries, both compiled into MitkPythonTestDriver:
mitkPythonContextTest**: a CppUnit suite exercising the mitk::PythonContext class directly from C++ — interpreter initialization, variable exchange, Execute(), ExecuteFile(), image binding, and context isolation.mitkPythonBindingsTest**: a thin C++ host that spins up a dedicated mitk_pytest virtual environment, installs pytest on first run, then hands control to a pytest suite under Modules/Python/test/pytest/. The suite covers the mitk Python module's binding surface (image construction, NumPy interop, I/O, geometry, pixel types, points/vectors, auto-loaded modules) as seen from idiomatic Python.Splitting the two lets each side use its native testing idiom: CppUnit for C++ API coverage, pytest for Python-side behavior. The mitk_pytest venv is reused across test runs, so the pytest install cost is paid only once per machine.
The Python wrapping of MITK is handled by pybind11. The bindings are defined in Wrapping/Python/mitk/ and compiled into a native extension module (mitk.cpXYZ-<platform>.pyd / .so).
Currently, the following types and functions are exposed:
| Category | Types / Functions |
|---|---|
| Image | Image with constructor overloads (empty / numpy / file path), initialize(), classmethods from_numpy() and load(), save(), as_numpy(), __array__, geometry properties (spacing, origin, direction, direction_cosines, ndim, shape, dtype, array, time_steps, time_geometry), per-time-step accessors (get_spacing(), set_spacing(), get_origin(), set_origin(), get_direction(), set_direction(), get_geometry()) |
| IO | IOUtil.load(), IOUtil.save() |
| Geometry | BaseGeometry, Geometry3D, PlaneGeometry, SlicedGeometry3D, TimeGeometry (with count_time_steps(), get_min_time_point(), get_max_time_point(), get_time_bounds(), time_step_to_time_point(), time_point_to_time_step(), is_valid_time_step(), is_valid_time_point(), get_geometry_for_time_step(), get_geometry_for_time_point()), ArbitraryTimeGeometry, ProportionalTimeGeometry |
| Pixel types | PixelType, make_pixel_type() |
| Points / Vectors | Point2D, Point3D, Vector2D, Vector3D |
| Exceptions | Exception |
| CppMicroServices | get_loaded_modules() |
mitk.Image is the bound C++ class. Its constructor is overloaded by argument type so the same name handles empty construction, loading from a file, and wrapping a numpy array. isinstance(img, mitk.Image), type hints, IDE autocomplete, and subclassing all work normally. Named factories mitk.Image.from_numpy() and mitk.Image.load() remain available for callers who prefer to be explicit.
Basic usage:
By default, as_numpy() returns a direct numpy view that pins the underlying mitk.Image via a smart-pointer capsule but does not acquire any read/write lock. This is the preferred mode for in-process work and matches the behavior expected by numpy.asarray() and the __array__ protocol. For workflows that need lock-based concurrency control (e.g. multi-threaded access from C++ and Python at the same time), pass use_accessor=True to safeguard image access through a ImageReadAccessor/ImageWriteAccessor-backed view, which holds the MITK accessor lock until the numpy array is garbage-collected:
The bindings are available both within MITK applications (via the embedded Python in MITK-build/python) and as a standalone installable wheel (see below).
When you instantiate the mitk::PythonContext class, you can optionally specify the name of a virtual environment to use. If the environment doesn’t exist yet, it will be created automatically. This allows different MITK components — such as segmentation tools — to use isolated virtual environments, enabling them to install dependencies at runtime via pip, for example.
These virtual environments are stored in the mitk_venvs folder within a dedicated user-writable location:
%LocalAppData% on Windows$XDG_DATA_HOME or $HOME/.local/share on Linux$HOME/Library/Application Support on macOSTo avoid interference between multiple MITK versions built or installed on the same machine, we use a hash of the application path of the currently running MITK application as the top-level folder name inside mitk_venvs.
Virtual environments created by mitk::PythonContext (or the corresponding functions in the MitkPythonHelper module) can be listed and managed through the Python Environments plugin in MITK.
The mitkPythonBindingsTest described above relies on this mechanism and creates a dedicated mitk_pytest virtual environment the first time it runs.
The mitk Python module can be packaged as a standalone, redistributable wheel (mitk_python-*.whl). This allows users to pip install the MITK bindings into any compatible Python environment without building MITK from source.
The wheel bundles:
mitk.cpXYZ-<platform>.pyd / .so)On import, mitk/__init__.py sets up the environment so that CppMicroServices auto-loading works transparently — the same IO file formats are available as in a full MITK application.
Use the PythonWheel build configuration, which is a headless configuration (no Qt, BlueBerry, or plugins) locked to Release builds:
The SuperBuild build chains into the inner MITK build, which builds all MITK modules and then produces the wheel in ../MITK-superbuild/MITK-build/ via the mitk_python_wheel target (included in the default build for this configuration).
The target:
wheel + platform delocator) into the build Pythoncmake --install --component wheelThe platform delocators are:
mitk_python.libs/mitk_python.libs/ and patches RPATHmitk/.dylibs/ and rewrites load commandsA self-contained smoke test script is provided:
This automatically creates a temporary virtual environment, installs the wheel, runs the tests, and cleans up. The test scope is deliberately narrow: it covers wheel-specific concerns (import, __version__, CppMicroServices auto-load bundling) plus a single functional sanity check. Comprehensive binding coverage lives in the mitkPythonBindingsTest pytest suite described above — running those against the wheel would be redundant.
The build_wheel.py script can also be invoked manually outside of the CMake build:
The wheel is written to the build directory by default. Use --output-dir to write it elsewhere, or --skip-repair to skip the delocator step for debugging.
Doxygen does not handle Python well: it does not understand Google-style docstrings, dataclasses, or typing.Literal/union hints, and its native Python rendering undersells a typed binding surface. For that reason, the mitk Python package has its own Sphinx-based documentation site, built and published independently of this C++ Doxygen site.
The Python documentation lives at https://mitk-python.readthedocs.io/en/2026.06/. It is also reachable from the "Python API" tab in the top navigation bar of this Doxygen site.
The Sphinx project sits next to the bindings, in Wrapping/Python/docs/:
conf.py: Sphinx configuration (autodoc + napoleon + autosummary + autodoc-typehints + myst-parser + sphinx-copybutton + sphinx_book_theme).requirements.txt: pinned toolchain.index.md, installation.md, getting_started.md, user_guide/*.md, api/index.md: narrative pages and the autosummary-driven API reference.The auto-generated API reference is populated by importing the freshly-built mitk package and reading docstrings off the compiled pybind11 extension. Google-style docstrings (with Args: / Returns: / Raises: / Examples: sections) on each binding are the source of truth; keep them in sync when the bindings change.
There is a dedicated CMake target:
The target depends on mitk_python_bindings. On first invocation it creates a dedicated venv at <build-dir>/mitk_python_docs_venv (with --system-site-packages so the just-built mitk package is importable) and pip-installs the Sphinx toolchain from requirements.txt into that venv. The venv is reused on subsequent runs. This deliberately keeps the toolchain out of the embedded build Python, because on Windows the standalone Python's user-site directory is shared with the system Python and pollutes both.
The HTML lands in <build-dir>/Documentation/Python/html/.
The -W flag (warnings-as-errors) is passed to sphinx-build, so docstring syntax errors or duplicated definitions fail the build. Missing-target cross-references (e.g. a stale :py:class: pointing at a name that no longer exists) only fail the build when nitpicky = True is set in conf.py; the default is off, so silently-broken cross-refs need to be caught at review time.
The published site is hosted on Read the Docs at https://mitk-python.readthedocs.io/en/2026.06/. A separate repository, MITK/mitk-python-docs, drives the build: it fetches Wrapping/Python/docs from here and runs sphinx-build against the mitk-python wheel installed from PyPI, so the site tracks the bindings without keeping a second copy of the sources.
pip install mitk-python, NumPy interop, file I/O, geometry, properties) lives on the Sphinx site.PythonInMITK) is the developer-facing reference: how the wheel is built, how the C++ side embeds Python, why Standalone Python Builds, platform quirks, and so on.The two are intentionally complementary, not duplicates.
As mentioned at the beginning, Python integration is a complex and sometimes fragile feature that can easily break in certain scenarios. In particular, differences between our supported platforms—Windows, Linux, and macOS—can be challenging. This section collects a few non-obvious quirks.
While we explictly unset the PYTHONHOME environment variable on Windows and Linux before preloading the Python library, on macOS we must explicitly set it.
While Python Standalone Builds have proven to be the most reliable and isolated solution for us, we have encountered conflicts between the OpenMP libraries used by MITK and certain Python packages on macOS. To avoid these issues, OpenMP is automatically disabled on macOS whenever Python is enabled in the build system.
On macOS, the test driver for the MitkPython module attempts to load the Python library before the MitkPreloadPython autoload-module has a chance to load it as intended. This premature loading fails because, in Standalone Python Builds, library paths on macOS are prefixed with /install/lib/. To work around this, we use macOS’s DYLD_INSERT_LIBRARIES feature to manually preload the Python library by setting the ENVIRONMENT test property for all Python tests. As a result, the tests run correctly via CTest, but still abort when executed directly.
To sign an application bundle on macOS with codesign, the bundle must follow certain rules regarding the location and format of its binaries. Therefore, we convert the python directory from MITK-build into Python.framework for packaging. This conversion is handled in the MITK-build/FixMacOSInstaller.cmake script, which CPack executes as a post-build step.
We are using CMake's BundleUtilities to create application bundles on macOS. Unfortunately, it rewrites all library dependency paths to start with @executable_path/../MacOS, which works fine for executables in the usual Contents/MacOS folder of an app bundle. However, this breaks when the Python interpreter in Contents/Frameworks/Python.framework/Versions/A/bin tries to load the dependencies of the mitk package.
To fix this, we adjust the runtime dependency paths of the mitk package to use an @loader_path approach in the FixMacOSInstaller.cmake script, which runs automatically after fixup_bundle() has finished modifying all paths. This fix currently does not cover autoload-modules, which is why they cannot be loaded in this scenario. Running the Python interpreter as subprocess of an MITK application, however, will load autoload-modules correctly.