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

Future Work

A first interesting subclass would be the DynamicStreamlineFile offering a way to append streamlines to an existing file when format permits it.