Add support for MRtrix MIF file format#1489
Conversation
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
|
@mattcieslak any chance you have some very small MIF files (< 50K) that we can use for testing here? |
effigies
left a comment
There was a problem hiding this comment.
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.
| _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') |
There was a problem hiding this comment.
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.
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.