~8x slower registration with itk-elastix Python API vs elastix CLI — minimal reproducible example

NicoKiaru
Sun, 22 Mar 2026 13:48:26 +0000
Engineering

Disclaimer: this is a [crosspost from image.sc]( ~8x slower registration with itk-elastix Python API vs elastix CLI — minimal reproducible example - Usage & Issues - Image.sc Forum )

Hi all,

I’m integrating itk-elastix as a drop-in replacement for the elastix CLI in a Java/Fiji plugin, and I’m hitting a consistent ~8.5x wall-clock slowdown when using the Python API compared to spawning the elastix executable directly. This is not a warm-up effect — the ratio holds across all runs.

Environment

Windows 11, Python 3.11 (conda-forge), itk-elastix==0.21.0 (pinned to match elastix 5.2.0)

14 physical cores, same thread count passed to both paths

Test images: blobs.tif / blobs-rot15deg.tif (256×256, standard ImageJ sample images)

Transform: BSpline, 6 resolutions, 100 iterations/level, 4096 random samples, FinalGridSpacingInVoxels=20

Exactly the same parameter file is used for both paths

The two execution paths

  # Path 1 — CLI (fast)
  subprocess.run([
      "elastix.exe",
      "-f", fixed, "-m", moving,
      "-p", params_bspline.txt,
      "-out", out_dir,
      "-threads", "14"
  ])

Path 2 — itk-elastix Python API (slow)

  erm = itk.ElastixRegistrationMethod[ImageType, ImageType].New()
  erm.SetFixedImage(fixed_img)
  erm.SetMovingImage(moving_img)
  erm.SetParameterObject(param_obj)   # same param file, NumberOfThreads patched to 14
  erm.SetOutputDirectory(out_dir)
  erm.SetLogToConsole(False)
  erm.SetLogToFile(True)
  erm.UpdateLargestPossibleRegion()

Results (3 runs, no warm-up effect observed)

Run CLI (s) itk-API (s) ratio

1 2.50 22.08 8.82x
2 2.73 22.24 8.16x
3 2.41 21.70 8.99x

Steady-state avg CLI: 2.57 s
Steady-state avg itk-API: 21.97 s
Steady-state ratio: 8.55x

What I’ve already checked / ruled out

Thread count is identical (NumberOfThreads is explicitly patched in the ParameterMap before calling UpdateLargestPossibleRegion, confirmed via the parameter dump)

The parameter file is bit-for-bit identical between the two paths

The slowdown is not a warm-up / import cost — Run 1 and Run 3 are essentially the same

The ITK pipeline overhead (filter setup, New(), etc.) is negligible compared to 20 s

WriteResultImage is false in both paths, so resampling is not the culprit

Both paths produce a valid TransformParameters.0.txt

Questions

1. Is there a known performance difference between the CLI and the Python API for BSpline registration specifically? The CLI likely uses elastix’s own thread pool management —
does the Python API use a different ITK thread scheduler?

2. Could there be a memory allocation or cache-thrashing issue when ITK is loaded inside a Python process vs. running standalone?

3. Is there a way to configure the ITK thread pool (e.g. itk.MultiThreaderBase.SetGlobalDefaultNumberOfThreads()) that would close the gap?

4. Any known difference in how the BSpline Jacobian accumulation is handled between the two paths?

Reproducible example

I’ve attached a zip containing bench.py, params_bspline.txt, and the two test images. You only need itk-elastix==0.21.0 (plus psutil for physical-core detection) and an elastix
5.2.0 binary in your PATH or passed via --elastix.

pip install itk-elastix==0.21.0 psutil

  python bench.py \
      --elastix /path/to/elastix \
      --fixed  blobs.tif \
      --moving blobs-rot15deg.tif \
      --runs 3

Any pointers to what’s going on under the hood would be very much appreciated!

PS:

This post has been redacted with claude’s help. I’ve been trying to debug and find the reason for the discrepancy with the help of claude for a while now, but I’ve hit a wall, so I ask for community help.

I’ve been trying to replace my CLI integration with Appose and itk-elastix, but a 8x difference in performance won’t cut it… The problem is not due to Appose since I can reproduce it in a pure python pixi env.

I tried to look at the itk discourse forum to find an answer but did not find anything relevant.

Pinging @dzenanz, @Niels_Dekker

Thanks!

Here’s the zip file of the minimal self-contained benchmark:

benchmark-itk.zip (72.2 KB)

30 posts - 4 participants