Skip to content

Commit 909e0b6

Browse files
committed
Added Tractogram.to_world and LazyTractogram.to_world methods
1 parent 7ba65c2 commit 909e0b6

File tree

2 files changed

+142
-35
lines changed

2 files changed

+142
-35
lines changed

nibabel/streamlines/tests/test_tractogram.py

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -389,34 +389,73 @@ def test_tractogram_apply_affine(self):
389389
# Apply the affine to the streamline in a lazy manner.
390390
transformed_tractogram = tractogram.apply_affine(affine, lazy=True)
391391
assert_true(type(transformed_tractogram) is LazyTractogram)
392-
assert_true(check_iteration(transformed_tractogram))
393-
assert_equal(len(transformed_tractogram), len(DATA['streamlines']))
394-
for s1, s2 in zip(transformed_tractogram.streamlines,
395-
DATA['streamlines']):
396-
assert_array_almost_equal(s1, s2*scaling)
397-
398-
for s1, s2 in zip(transformed_tractogram.streamlines,
399-
tractogram.streamlines):
400-
assert_array_almost_equal(s1, s2*scaling)
401-
392+
check_tractogram(transformed_tractogram,
393+
streamlines=[s*scaling for s in DATA['streamlines']],
394+
data_per_streamline=DATA['data_per_streamline'],
395+
data_per_point=DATA['data_per_point'])
402396
assert_array_equal(transformed_tractogram.get_affine_to_rasmm(),
403397
np.dot(np.eye(4), np.linalg.inv(affine)))
398+
# Make sure streamlines of the original tractogram have not been modified.
399+
assert_arrays_equal(tractogram.streamlines, DATA['streamlines'])
404400

405401
# Apply the affine to the streamlines in-place.
406402
transformed_tractogram = tractogram.apply_affine(affine)
407403
assert_true(transformed_tractogram is tractogram)
408-
assert_true(check_iteration(transformed_tractogram))
409-
assert_equal(len(transformed_tractogram), len(DATA['streamlines']))
410-
for s1, s2 in zip(transformed_tractogram.streamlines,
411-
DATA['streamlines']):
412-
assert_array_almost_equal(s1, s2*scaling)
404+
check_tractogram(tractogram,
405+
streamlines=[s*scaling for s in DATA['streamlines']],
406+
data_per_streamline=DATA['data_per_streamline'],
407+
data_per_point=DATA['data_per_point'])
413408

414409
# Apply affine again and check the affine_to_rasmm.
415410
transformed_tractogram = tractogram.apply_affine(affine)
416411
assert_array_equal(transformed_tractogram.get_affine_to_rasmm(),
417412
np.dot(np.eye(4), np.dot(np.linalg.inv(affine),
418413
np.linalg.inv(affine))))
419414

415+
# Check that applying an affine and its inverse give us back the
416+
# original streamlines.
417+
tractogram = DATA['tractogram'].copy()
418+
affine = np.random.RandomState(1234).randn(4, 4)
419+
affine[-1] = [0, 0, 0, 1] # Remove perspective projection.
420+
421+
tractogram.apply_affine(affine)
422+
tractogram.apply_affine(np.linalg.inv(affine))
423+
assert_array_almost_equal(tractogram.get_affine_to_rasmm(), np.eye(4))
424+
for s1, s2 in zip(tractogram.streamlines, DATA['streamlines']):
425+
assert_array_almost_equal(s1, s2)
426+
427+
def test_tractogram_to_world(self):
428+
tractogram = DATA['tractogram'].copy()
429+
affine = np.random.RandomState(1234).randn(4, 4)
430+
affine[-1] = [0, 0, 0, 1] # Remove perspective projection.
431+
432+
# Apply the affine to the streamlines, then bring them back
433+
# to world space in a lazy manner.
434+
transformed_tractogram = tractogram.apply_affine(affine)
435+
assert_array_equal(transformed_tractogram.get_affine_to_rasmm(),
436+
np.linalg.inv(affine))
437+
438+
tractogram_world = transformed_tractogram.to_world(lazy=True)
439+
assert_true(type(tractogram_world) is LazyTractogram)
440+
assert_array_almost_equal(tractogram_world.get_affine_to_rasmm(),
441+
np.eye(4))
442+
for s1, s2 in zip(tractogram_world.streamlines, DATA['streamlines']):
443+
assert_array_almost_equal(s1, s2)
444+
445+
# Bring them back streamlines to world space in a in-place manner.
446+
tractogram_world = transformed_tractogram.to_world()
447+
assert_true(tractogram_world is tractogram)
448+
assert_array_almost_equal(tractogram.get_affine_to_rasmm(), np.eye(4))
449+
for s1, s2 in zip(tractogram.streamlines, DATA['streamlines']):
450+
assert_array_almost_equal(s1, s2)
451+
452+
# Calling to_world twice should do nothing.
453+
tractogram_world2 = transformed_tractogram.to_world()
454+
assert_true(tractogram_world2 is tractogram)
455+
assert_array_almost_equal(tractogram.get_affine_to_rasmm(), np.eye(4))
456+
for s1, s2 in zip(tractogram.streamlines, DATA['streamlines']):
457+
assert_array_almost_equal(s1, s2)
458+
420459

421460
class TestLazyTractogram(unittest.TestCase):
422461

@@ -531,18 +570,47 @@ def test_lazy_tractogram_apply_affine(self):
531570

532571
tractogram = DATA['lazy_tractogram'].copy()
533572

534-
tractogram.apply_affine(affine)
535-
assert_true(check_iteration(tractogram))
536-
assert_equal(len(tractogram), len(DATA['streamlines']))
537-
for s1, s2 in zip(tractogram.streamlines, DATA['streamlines']):
538-
assert_array_almost_equal(s1, s2*scaling)
573+
transformed_tractogram = tractogram.apply_affine(affine)
574+
assert_true(transformed_tractogram is tractogram)
575+
assert_array_equal(tractogram._affine_to_apply, affine)
576+
assert_array_equal(tractogram.get_affine_to_rasmm(),
577+
np.dot(np.eye(4), np.linalg.inv(affine)))
578+
check_tractogram(tractogram,
579+
streamlines=[s*scaling for s in DATA['streamlines']],
580+
data_per_streamline=DATA['data_per_streamline'],
581+
data_per_point=DATA['data_per_point'])
539582

540583
# Apply affine again and check the affine_to_rasmm.
541584
transformed_tractogram = tractogram.apply_affine(affine)
585+
assert_array_equal(tractogram._affine_to_apply, np.dot(affine, affine))
542586
assert_array_equal(transformed_tractogram.get_affine_to_rasmm(),
543587
np.dot(np.eye(4), np.dot(np.linalg.inv(affine),
544588
np.linalg.inv(affine))))
545589

590+
def test_tractogram_to_world(self):
591+
tractogram = DATA['lazy_tractogram'].copy()
592+
affine = np.random.RandomState(1234).randn(4, 4)
593+
affine[-1] = [0, 0, 0, 1] # Remove perspective projection.
594+
595+
# Apply the affine to the streamlines, then bring them back
596+
# to world space in a lazy manner.
597+
tractogram.apply_affine(affine)
598+
assert_array_equal(tractogram.get_affine_to_rasmm(),
599+
np.linalg.inv(affine))
600+
601+
tractogram_world = tractogram.to_world()
602+
assert_true(tractogram_world is tractogram)
603+
assert_array_almost_equal(tractogram.get_affine_to_rasmm(),
604+
np.eye(4))
605+
for s1, s2 in zip(tractogram.streamlines, DATA['streamlines']):
606+
assert_array_almost_equal(s1, s2)
607+
608+
# Calling to_world twice should do nothing.
609+
tractogram.to_world()
610+
assert_array_almost_equal(tractogram.get_affine_to_rasmm(), np.eye(4))
611+
for s1, s2 in zip(tractogram.streamlines, DATA['streamlines']):
612+
assert_array_almost_equal(s1, s2)
613+
546614
def test_lazy_tractogram_copy(self):
547615
# Create a copy of the lazy tractogram.
548616
tractogram = DATA['lazy_tractogram'].copy()

nibabel/streamlines/tractogram.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ class Tractogram(object):
198198
Tractogram objects have three main properties: `streamlines`,
199199
`data_per_streamline` and `data_per_point`.
200200
201+
Streamlines of a tractogram can be in any coordinate system of your
202+
choice as long as you provide the correct `affine_to_rasmm` matrix, at
203+
construction time, that brings the streamlines back to *RAS+*, *mm* space,
204+
where the coordinates (0,0,0) corresponds to the center of the voxel
205+
(opposed to a corner).
206+
201207
"""
202208
def __init__(self, streamlines=None,
203209
data_per_streamline=None,
@@ -243,9 +249,6 @@ def data_per_streamline(self):
243249

244250
@data_per_streamline.setter
245251
def data_per_streamline(self, value):
246-
# if is_lazy_dict(value):
247-
# self._data_per_streamline = DataPerStreamlineDict(self, **value.items())
248-
# else:
249252
self._data_per_streamline = DataPerStreamlineDict(self, value)
250253

251254
@property
@@ -254,9 +257,6 @@ def data_per_point(self):
254257

255258
@data_per_point.setter
256259
def data_per_point(self, value):
257-
# if is_lazy_dict(value):
258-
# self._data_per_point = DataPerPointDict(self, **value.items())
259-
# else:
260260
self._data_per_point = DataPerPointDict(self, value)
261261

262262
def get_affine_to_rasmm(self):
@@ -333,6 +333,28 @@ def apply_affine(self, affine, lazy=False):
333333

334334
return self
335335

336+
def to_world(self, lazy=False):
337+
""" Brings the streamlines to world space (i.e. RAS+ and mm).
338+
339+
If `lazy` is not specified, this is performed *in-place*.
340+
341+
Parameters
342+
----------
343+
lazy_load : {False, True}, optional
344+
If True, streamlines are *not* transformed in-place and a
345+
:class:`LazyTractogram` object is returned. Otherwise, streamlines
346+
are modified in-place.
347+
348+
Returns
349+
-------
350+
tractogram : :class:`Tractogram` or :class:`LazyTractogram` object
351+
Tractogram where the streamlines have been sent to world space.
352+
If the `lazy` option is true, it returns a :class:`LazyTractogram`
353+
object, otherwise it returns a reference to this
354+
:class:`Tractogram` object with updated streamlines.
355+
"""
356+
return self.apply_affine(self._affine_to_rasmm, lazy=lazy)
357+
336358

337359
class LazyTractogram(Tractogram):
338360
""" Class containing information about streamlines.
@@ -394,17 +416,21 @@ def from_tractogram(cls, tractogram):
394416
lazy_tractogram : :class:`LazyTractogram` object
395417
New lazy tractogram.
396418
"""
397-
data_per_streamline = {}
398-
for key, value in tractogram.data_per_streamline.items():
399-
data_per_streamline[key] = lambda: value
419+
lazy_tractogram = cls(lambda: tractogram.streamlines.copy())
400420

401-
data_per_point = {}
402-
for key, value in tractogram.data_per_point.items():
403-
data_per_point[key] = lambda: value
421+
# Set data_per_streamline using data_func
422+
def _gen(key):
423+
return lambda: iter(tractogram.data_per_streamline[key])
424+
425+
for k in tractogram.data_per_streamline:
426+
lazy_tractogram._data_per_streamline[k] = _gen(k)
404427

405-
lazy_tractogram = cls(lambda: tractogram.streamlines.copy(),
406-
data_per_streamline,
407-
data_per_point)
428+
# Set data_per_point using data_func
429+
def _gen(key):
430+
return lambda: iter(tractogram.data_per_point[key])
431+
432+
for k in tractogram.data_per_point:
433+
lazy_tractogram._data_per_point[k] = _gen(k)
408434

409435
lazy_tractogram._nb_streamlines = len(tractogram)
410436
lazy_tractogram._affine_to_rasmm = tractogram.get_affine_to_rasmm()
@@ -584,3 +610,16 @@ def apply_affine(self, affine):
584610
self._affine_to_rasmm = np.dot(self._affine_to_rasmm,
585611
np.linalg.inv(affine))
586612
return self
613+
614+
def to_world(self):
615+
""" Brings the streamlines to world space (i.e. RAS+ and mm).
616+
617+
The transformation will be applied just before returning the
618+
streamlines.
619+
620+
Returns
621+
-------
622+
lazy_tractogram : :class:`LazyTractogram` object
623+
Reference to this instance of :class:`LazyTractogram`.
624+
"""
625+
return self.apply_affine(self._affine_to_rasmm)

0 commit comments

Comments
 (0)