Skip to content

Add support for MRtrix MIF file format#1489

Draft
tsalo wants to merge 4 commits into
nipy:masterfrom
tsalo:mif
Draft

Add support for MRtrix MIF file format#1489
tsalo wants to merge 4 commits into
nipy:masterfrom
tsalo:mif

Conversation

@tsalo

@tsalo tsalo commented Mar 28, 2026

Copy link
Copy Markdown
Member

Related to #927 (does not close because this doesn't include MIH support). I used Claude to write this for ModelArrayIO, but I don't have much experience with the file format so I'm happy to make any changes.

I don't have any small MIF files that would be appropriate for nibabel's tests on hand, but I can hunt some down.

@codecov

codecov Bot commented Mar 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.59843% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.38%. Comparing base (0b5bbaf) to head (c0e5966).
⚠️ Report is 26 commits behind head on master.

Files with missing lines Patch % Lines
nibabel/mif.py 88.34% 20 Missing and 18 partials ⚠️
nibabel/tests/test_mif.py 97.06% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1489      +/-   ##
==========================================
- Coverage   95.44%   95.38%   -0.07%     
==========================================
  Files         209      211       +2     
  Lines       29981    30616     +635     
  Branches     4483     4567      +84     
==========================================
+ Hits        28616    29202     +586     
- Misses        930      960      +30     
- Partials      435      454      +19     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tsalo

tsalo commented Apr 13, 2026

Copy link
Copy Markdown
Member Author

@mattcieslak any chance you have some very small MIF files (< 50K) that we can use for testing here?

@effigies effigies left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get very far in reviewing this, but I had written a comment. I can try to look at this again on Wednesday, but hitting post in case this is helpful to you now.

Comment thread nibabel/mif.py
Comment on lines +44 to +109
_MIF_DTYPE_MAP: dict[str, str] = {
'Int8': 'i1',
'UInt8': 'u1',
'Int16': 'i2',
'UInt16': 'u2',
'Int32': 'i4',
'UInt32': 'u4',
'Int64': 'i8',
'UInt64': 'u8',
'Float32': 'f4',
'Float64': 'f8',
'CFloat32': 'c8',
'CFloat64': 'c16',
}

_NUMPY_TO_MIF_BASE: dict[tuple[str, int], str] = {
('i', 1): 'Int8',
('u', 1): 'UInt8',
('i', 2): 'Int16',
('u', 2): 'UInt16',
('i', 4): 'Int32',
('u', 4): 'UInt32',
('i', 8): 'Int64',
('u', 8): 'UInt64',
('f', 4): 'Float32',
('f', 8): 'Float64',
('c', 8): 'CFloat32',
('c', 16): 'CFloat64',
}


def _mif_parse_dtype(dtype_str: str) -> np.dtype:
"""Convert a MIF datatype string (e.g. ``'Float32LE'``) to a numpy dtype."""
dtype_str = dtype_str.strip()
if dtype_str.endswith('LE'):
endian, base = '<', dtype_str[:-2]
elif dtype_str.endswith('BE'):
endian, base = '>', dtype_str[:-2]
else:
endian = '<' if sys.byteorder == 'little' else '>'
base = dtype_str

if base not in _MIF_DTYPE_MAP:
raise ValueError(f'Unknown MIF datatype: {dtype_str!r}')

type_char = _MIF_DTYPE_MAP[base]
if type_char in ('i1', 'u1'): # single-byte types have no endianness
return np.dtype(type_char)
return np.dtype(endian + type_char)


def _mif_dtype_to_str(dtype: np.dtype) -> str:
"""Convert a numpy dtype to a MIF datatype string."""
dtype = np.dtype(dtype)
base_name = _NUMPY_TO_MIF_BASE.get((dtype.kind, dtype.itemsize))
if base_name is None:
raise ValueError(f'Cannot represent numpy dtype {dtype!r} in MIF format')
if dtype.itemsize == 1:
return base_name

byte_order = dtype.byteorder
if byte_order == '=':
byte_order = '<' if sys.byteorder == 'little' else '>'
elif byte_order == '|':
return base_name
return base_name + ('LE' if byte_order == '<' else 'BE')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this work is already done in nibabel.volumeutils. Building off your first structure:

_dtdefs = [
  (f'{mift}{end}', (label := f'{end and recode[end]}{dt}'), np.dtype(label).type)
  for mift, dt in _MIF_DTYPE_MAP.items()
  for end in ('', 'BE', 'LE')
]

data_type_codes = nb.volumeutils.make_dt_codes(_dtdefs)

This allows you to replace your functions:

_mif_dtype_to_str = lambda dtype: data_type_codes[dtype]
_mif_parse_dtype = lambda dtype_str: data_type_codes.dtype[dtype_str]

See, for example:

>>> data_type_codes.dtype['Float32']
dtype('float32')

>>> data_type_codes.dtype['Float32BE']
dtype('>f4')

>>> data_type_codes.dtype['Float32LE']
dtype('float32')

>>> data_type_codes[np.dtype('f8')]
'Float64LE'

This doesn't do the same validation. Bad names will show up as KeyErrors, instead of custom-raised ValueErrors, and you could theoretically have a file with a bad name that matches a numpy dtype string, and that would be accepted.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants