BIAP5 - A streamlines converter¶
- Author:
Marc-Alexandre Côté
- Status:
Draft
- Type:
Standards
- Created:
2013-09-03
The first objective of this proposal is to add support to other streamlines format. The second objective is to be able to easily convert from one file format to another.
Motivation¶
There are a couple of different formats for saving streamlines to a file. Currently, NiBabel only support one of them: TRK from Trackvis. NiBabel could greatly benefit from supporting other formats: TCK (MRtrix), VTK (Camino, MITK) and more. Moreover, being able to move from one format to another would be convenient. To ease the conversion process, a generic format from which to inherit and some common header fields would be necessary. This is similar to what NiBabel already has for neuroimages.
After implementing this proposal, users could load and use streamlines file like this:
>>> import nibabel as nib
>>> f = nib.streamlines.load('my_trk.trk', lazy_load=False)
>>> type(f)
nibabel.streamlines.base_format.Streamlines
>>> f.points
[array([ [1, 1, 1],
[2, 2, 2],
[3, 3, 3] ]),
array([ [4, 4, 4],
[5, 5, 5] ])]
>>> nib.streamlines.convert('my_trk.trk', 'my_tck.tck')
>>> f2 = nib.streamlines.load('my_trk.tck', lazy_load=False)
>>> type(f2)
nibabel.streamlines.base_format.Streamlines
>>> f2.points
[array([ [1, 1, 1],
[2, 2, 2],
[3, 3, 3] ]),
array([ [4, 4, 4],
[5, 5, 5] ])]
Of course, similar functions will be available for ‘scalars’ (per point) and ‘properties’ (per streamline) as defined in the TrackVis format. A simple example to save three streamlines with no scalars nor properties would look like this:
>>> import nibabel as nib
>>> points = [np.arange(1*3).reshape((1,3)),
np.arange(2*3).reshape((2,3)),
np.arange(5*3).reshape((5,3))]
>>> streamlines = nib.streamlines.Streamlines(points)
>>> nib.streamlines.save(streamlines, 'data1.trk') # Default TRK header is used but updated with streamlines information.
>>> FA = nib.load('FA.nii')
>>> streamlines.header = nib.streamlines.header.from_nifti(FA) # Uses information of the FA to create an header.
>>> nib.streamlines.save(streamlines, 'data2.trk') # Streamlines' header is used but also updated with streamlines information.
>>> from nib.streamlines.header import VOXEL_ORDER, VOXEL_SIZES
>>> hdr = nib.streamlines.TrkFile.get_empty_header() # Default TRK header
>>> hdr[VOXEL_ORDER] = "LAS"
>>> hdr[VOXEL_SIZES] = (2, 2, 2)
>>> streamlines.header = hdr
>>> nib.streamlines.save(streamlines, 'data3.trk') # Uses hdr to create a TRK header.
Overview¶
All code related to managing streamlines should be kept in a separate folder:
nibabel.streamlines
. A first file, base_format.py
, would contain base
classes acting as general interfaces from which new streamlines file format
will inherit.
Streamlines would be represented by its own class Streamlines
which will
have three main properties: points
, scalars
and properties
.
Streamlines objects can be iterate over producing tuple of points
,
scalars
and properties
for each streamline.
The generic class StreamlinesFile
would look like this:
class StreamlinesFile:
@classmethod
def get_magic_number(cls):
raise NotImplementedError()
@classmethod
def is_correct_format(cls, fileobj):
raise NotImplementedError()
@classmethod
def get_empty_header(cls):
raise NotImplementedError()
@classmethod
def load(cls, fileobj, lazy_load=True):
raise NotImplementedError()
@classmethod
def save(cls, streamlines, fileobj):
raise NotImplementedError()
@staticmethod
def pretty_print(streamlines):
raise NotImplementedError()
When inheriting from a base class, a specific streamline format class should know how to do its i/o, in particular how to iterate through the streamlines without loading the whole file into memory.
Once, the right interface is in place, the conversion part should be quite easy. Moreover, the conversion could be done without loading the input file entirely into memory thanks to generators. Actually, the convert function should looks like this:
def convert(in_fileobj, out_filename):
# Loading part
streamlines_file = detect_format(in_fileobj)
streamlines = streamlines_file.load(in_fileobj, lazy_load=True)
# Saving part
streamlines_file = detect_format(out_filename)
streamlines_file.save(streamlines, out_filename)
Of course, this implies some sort of general header compatibility between every format.
Issues¶
Header¶
Like it is done in NiBabel, headers should be defined using the
numpy.dtype
. This consists of a list of tuples, each one containing
information (name, datatype and shape) about one field of the header. Once
loaded, the header will acted as a dictionary using the name of each field as
the key. Ideally, header of different formats would be the same, but it is
not. To avoid manually writing each possible conversion between header
formats, a general architecture should be put in place.
One solution is to define some sort of CommonHeader
containing an enum of the most common field (i.e. NB_FIBERS, VOXEL_SIZES, DIMENSIONS, etc). Like that, instead of specifying a field’s name in the header definition, the suited enum constant should be used if there is one, otherwise the name is hard coded to a string representing the field. It should be make clear, in the documentation of CommonHeader
, what is the expected value of a common field.
Future Work¶
A first interesting subclass would be the DynamicStreamlineFile
offering
a way to append streamlines to an existing file when format permits it.