-
-
Notifications
You must be signed in to change notification settings - Fork 715
Description
Description
ITK’s NRRD reader parses floating-point metadata (such as spacing, direction vectors, and other numeric header fields) using locale-dependent number parsing (strtod).
In numeric locales where the decimal separator is a comma (for example de_DE.UTF-8, common in many European countries), values containing a dot such as 0.878906 are parsed incorrectly. In such locales, strtod("0.878906") yields 0.0.
This problem leads to two kinds of failures:
-
Silent metadata corruption (no error raised)
Values with fractional parts greater than 1 (for example3.5,2.2) may be misparsed (fractional part ignored) without causing an error. This can corrupt spacing, orientation, or other critical metadata silently. The image loads and all downstream computations use incorrect metadata. -
Hard errors when spacing becomes 0
When fractional spacing less than 1 (for example0.878906or0.8) is parsed as0.0, ITK sometimes throws
Zero-valued spacing is not supported.
This error exposes the bug, but only for particular values. For many other metadata fields and values the corruption is completely silent.
The same issue was already reported here, but was never resolved:
#3375
A similar issue was previously identified and fixed for VTK files:
#2297
Impact
This issue can silently corrupt metadata when reading NRRD files on systems with non-English numeric locales. This includes:
space directionsspace originspacing- measurements encoded in metadata
- values in DICOM-derived metadata fields stored in NRRD
- any numeric field parsed through locale-dependent routines
This is particularly problematic in medical imaging, where spacing, orientation, and geometric metadata directly affect:
- registration
- segmentation
- dose calculation
- physical measurement interpretation
- reconstruction algorithms
The most serious aspect is that metadata can be corrupted without any warning or error message. The bug was only discovered because in some cases spacing becomes exactly zero, triggering ITK’s Zero-valued spacing is not supported check. In many other cases (for example when only the fractional part is lost, or when values are truncated but remain positive) the corruption is completely silent and can remain undetected.
The issue is typically triggered only when the host application explicitly applies the system locale, which is common in GUI frameworks such as Qt. This is why the bug appears in some environments (for example napari or other Qt-based tools) while plain C++ programs often appear unaffected.
Root Cause
Many GUI frameworks, such as Qt, call:
setlocale(LC_ALL, "");to apply the system locale. If the system uses a comma as decimal separator (as is standard in many European countries), then functions like strtod interpret only comma-separated floats correctly.
Example:
- In
Clocale:strtod("0.878906")→0.878906 - In
de_DE.UTF-8locale:strtod("0.878906")→0.0
Thus, a valid NRRD header field such as:
space directions: (0.878906,0,0) (0,3,0) (0,0,3)
may be parsed by ITK as something like:
(0.0, 0, 0)
(0, 3, 0)
(0, 0, 3)
If the corrupted value results in spacing zero, ITK throws an error.
If the corrupted value remains positive (for example if only the fractional part is dropped), the metadata is accepted but wrong, and no error is raised.
Minimal Reproducible Examples (Python, self-contained)
The following examples assume that the de_DE.UTF-8 locale is installed on the system.
They demonstrate both the silent corruption case and the zero-spacing error case, using only Python and ITK. Each example:
- Creates a random 3D numpy array.
- Writes it to NRRD using ITK under a safe numeric locale (
C). - Switches the numeric locale to
de_DE.UTF-8. - Reads the same NRRD with ITK and inspects the spacing.
Depending on the actual parsing behavior, the spacing may be silently wrong or may cause an exception.
Note: depending on how the ITK Python wrappers format spacing, component order in printing may appear as (z, y, x) or (x, y, z), but the key observation is the incorrect handling of fractional parts under de_DE numeric locale.
1. Silent metadata corruption example (spacing > 1)
import os
import locale
import pathlib
import numpy as np
import itk
# Ensure we start in a safe numeric locale for writing
locale.setlocale(locale.LC_NUMERIC, "C")
# Create a small test image and write it to NRRD with fractional spacing > 1
tmpdir = pathlib.Path("itk_locale_test")
tmpdir.mkdir(exist_ok=True)
filename = tmpdir / "spacing_silent.nrrd"
array = np.random.rand(16, 16, 16).astype("float32")
ImageType = itk.Image[itk.F, 3]
img = itk.image_from_array(array)
# Set non-integer spacing; fractional parts are important
img.SetSpacing((3.5, 2.2, 2.2))
itk.imwrite(img, str(filename))
print("Written spacing (intended):", img.GetSpacing())
# Now switch to a locale with comma decimal separator (if available)
os.environ["LC_NUMERIC"] = "de_DE.UTF-8"
locale.setlocale(locale.LC_NUMERIC, "")
# Read back using ITK under the problematic locale
img_read = itk.imread(str(filename))
print("Read spacing under de_DE:", img_read.GetSpacing())In this scenario, the image is saved with spacing (3.5, 2.2, 2.2).
When reloaded under de_DE.UTF-8 numeric locale, the spacing may be parsed incorrectly (for example the fractional part may be dropped or misinterpreted), without any error being raised. This is silent metadata corruption.
2. Error example (spacing with fractional part < 1 leading to zero)
import os
import locale
import pathlib
import numpy as np
import itk
# Ensure we start in a safe numeric locale for writing
locale.setlocale(locale.LC_NUMERIC, "C")
# Create a small test image and write it to NRRD with fractional spacing < 1
tmpdir = pathlib.Path("itk_locale_test")
tmpdir.mkdir(exist_ok=True)
filename = tmpdir / "spacing_zero.nrrd"
array = np.random.rand(16, 16, 16).astype("float32")
ImageType = itk.Image[itk.F, 3]
img = itk.image_from_array(array)
# One component < 1; when misparsed as 0 it may trigger ITK's zero spacing check
img.SetSpacing((3.5, 0.8, 0.8))
itk.imwrite(img, str(filename))
print("Written spacing (intended):", img.GetSpacing())
# Switch to locale with comma decimal separator (if available)
os.environ["LC_NUMERIC"] = "de_DE.UTF-8"
locale.setlocale(locale.LC_NUMERIC, "")
# Read back using ITK under the problematic locale
try:
img_read = itk.imread(str(filename))
print("Read spacing under de_DE:", img_read.GetSpacing())
except Exception as e:
print("Exception while reading under de_DE:")
print(e)In this example, the spacing components 0.8 may be parsed as 0.0 under de_DE.UTF-8.
When ITK attempts to set spacing with a zero component, it can trigger an error similar to:
Zero-valued spacing is not supported and may result in undefined behavior.
This is the explicit error case that reveals the underlying locale-dependent parsing problem.
However, as demonstrated in the first example, many other values will not trigger such an error and will simply be wrong.
Reproducibility
100%
Versions
5.4.5
Environment
itk==5.4.5
python==3.12.12
Notes
- The issue often does not appear in standalone C++ programs because they typically remain in the default
"C"locale unlesssetlocaleis called explicitly. - The issue reliably appears in Python applications that use Qt (such as napari), because Qt commonly calls
setlocale(LC_ALL, "")to apply the system locale. - As a result, the exact same NRRD can be read correctly in one environment and incorrectly in another, depending solely on whether
LC_NUMERIChas been set to a locale with a comma decimal separator.