Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Fibertube Tracking #971

Open
wants to merge 64 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
2486166
FT Core: collision filtering + tracking
VincentBeaud Apr 16, 2024
94e474a
FT Analysis: fibers and reconstruction metrics
VincentBeaud Apr 16, 2024
790fddb
FT visual: visualize collisions and reconstructions
VincentBeaud Apr 16, 2024
749eacd
numba in requirements + pep8 fix
VincentBeaud Apr 16, 2024
ec87a0c
Test for each new script + Fix accordingly
VincentBeaud Apr 16, 2024
84a60c9
Style for pep8
VincentBeaud Apr 16, 2024
c7f6076
centroid -> centerline
VincentBeaud Apr 17, 2024
7f7c2fb
Review changes for collision filtering script
VincentBeaud Apr 17, 2024
4688bc2
English
VincentBeaud Apr 17, 2024
156c369
Review changes for collision visualization
VincentBeaud Apr 17, 2024
495ca8c
Update scilpy/tractograms/intersection_finder.py
VincentBeaud Apr 19, 2024
7b698b1
Update doc + terminology
VincentBeaud Sep 3, 2024
102a2b9
Add demo to project (Will be removed later)
VincentBeaud Sep 3, 2024
65eb980
Merge branch 'dev' of https://github.com/VincentBeaud/scilpy into dev
VincentBeaud Sep 3, 2024
283e51d
Add lines between docstring and imports
VincentBeaud Sep 3, 2024
bb494f5
Update doc + terminology (Lab PC version)
VincentBeaud Sep 3, 2024
342fd02
Merge branch 'dev' of https://github.com/VincentBeaud/scilpy into dev
VincentBeaud Sep 3, 2024
314cdcc
Merge branch 'master' of https://github.com/VincentBeaud/scilpy into dev
VincentBeaud Sep 3, 2024
0ae9c28
useless import
VincentBeaud Sep 3, 2024
7d5283a
Full recap of scripts after coming back from vacation.
VincentBeaud Sep 4, 2024
7800eda
Make tractogram_filter_collision more generic.
VincentBeaud Sep 4, 2024
1005e1d
First pass of PR fixes since comeback. Not much left. :)
VincentBeaud Sep 4, 2024
97dca72
Add notImplementedError to unimplemented function in FibertubeDataVolume
VincentBeaud Sep 5, 2024
855fdfc
Remove all masks arguments + clean FibertubeSeeder
VincentBeaud Sep 9, 2024
c0c5a9f
Update demo
VincentBeaud Sep 9, 2024
0b8d327
Fix for pep8
VincentBeaud Sep 10, 2024
e052ee1
Update scilpy/image/volume_space_management.py
VincentBeaud Sep 10, 2024
8941535
Few code fixes
VincentBeaud Sep 10, 2024
553a2f6
Save diameters locally for fibertube testing
VincentBeaud Sep 10, 2024
ef967c0
Fix the previous fixes
VincentBeaud Sep 10, 2024
297c4aa
Finalize pep8
VincentBeaud Sep 10, 2024
8c26115
Add doc in ft tracking
VincentBeaud Sep 10, 2024
de0e295
Move demo to /docs and rename fibertube scripts
VincentBeaud Sep 10, 2024
bd48a24
update .python-version for tests
VincentBeaud Sep 10, 2024
e3ac953
update test for new script names
VincentBeaud Sep 11, 2024
3f3cfef
Update /tracking tests documentation
VincentBeaud Sep 11, 2024
afe7e5d
Fix bug when no config output given
VincentBeaud Sep 12, 2024
77050ab
Begin tests and small fixes
VincentBeaud Sep 12, 2024
d90183d
filter collisions test + fixes
VincentBeaud Sep 12, 2024
30a9aa1
Finalizing tests (from home, so may not run)
Sep 17, 2024
d14ec51
Merge branch 'master' into dev
VincentBeaud Sep 23, 2024
0d43ed2
Finalize tests
VincentBeaud Sep 23, 2024
ecc71fd
fix for pep8
VincentBeaud Sep 23, 2024
4adc5eb
Update scil_viz_tractogram_collisions.py
VincentBeaud Sep 24, 2024
43fcb8c
First pass, Maxime comments
VincentBeaud Sep 30, 2024
db99c84
Merge branch 'dev' of https://github.com/VincentBeaud/scilpy into dev
VincentBeaud Sep 30, 2024
a7c9e94
Begin work on DEMO
VincentBeaud Oct 2, 2024
731ee80
More demo work + fix for tests
VincentBeaud Oct 7, 2024
c13a844
pep8 fix
VincentBeaud Oct 15, 2024
68f6491
hotfix json
VincentBeaud Oct 15, 2024
82b7bef
Put json back in
VincentBeaud Oct 22, 2024
0baa724
Update DEMO.md
VincentBeaud Oct 24, 2024
b2cba05
Update DEMO.md
VincentBeaud Oct 25, 2024
ba51e60
Update DEMO.md
VincentBeaud Oct 25, 2024
2fa2574
Update DEMO.md
VincentBeaud Oct 25, 2024
f0031c6
update doc
VincentBeaud Oct 25, 2024
0b0ba38
Merge branch 'dev' of github.com:VincentBeaud/scilpy into dev
VincentBeaud Oct 25, 2024
5b37dc2
Add possibility of being a IC if we reach the START segment of anothe…
VincentBeaud Oct 25, 2024
2b17eb2
pep8 fix
VincentBeaud Oct 25, 2024
155f5df
Update DEMO.md
VincentBeaud Oct 25, 2024
10c2574
Update DEMO.md
VincentBeaud Oct 25, 2024
59b218b
Update DEMO.md
VincentBeaud Oct 25, 2024
c8d6825
fix pep8 again + doc
VincentBeaud Oct 25, 2024
c846a8c
Merge branch 'dev' of github.com:VincentBeaud/scilpy into dev
VincentBeaud Oct 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ kiwisolver==1.4.*
matplotlib==3.6.*
nibabel==5.2.*
nilearn==0.9.*
numba==0.59.1
numba-kdtree==0.4.0
numpy==1.23.*
openpyxl==3.0.*
packaging == 23.2.*
Expand Down
188 changes: 184 additions & 4 deletions scilpy/image/volume_space_management.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
import numpy as np

from numba_kdtree import KDTree
from numba import njit
from scilpy.tracking.fibertube import (segment_tractogram,
point_in_cylinder,
sphere_cylinder_intersection)
from scilpy.tractograms.streamline_operations import \
get_streamlines_as_fixed_array
from dipy.core.interpolation import trilinear_interpolate4d, \
nearestneighbor_interpolate
from dipy.io.stateful_tractogram import Origin, Space
from dipy.segment.mask import bounding_box


from scilpy.utils.util import voxel_to_world


class DataVolume(object):
Expand Down Expand Up @@ -361,3 +364,180 @@ def _is_voxmm_in_bound(self, x, y, z, origin):
True if position is in dataset range and false otherwise.
"""
return self.is_idx_in_bound(*self.voxmm_to_idx(x, y, z, origin))


class FibertubeDataVolume(DataVolume):
"""
Adaptation of the scilpy.image.volume_space_management.AbstractDataVolume
interface for fibertube tracking. Instead of a spherical function,
provides direction and intersection volume of close-by fiber segments.
"""
def __init__(self, fibers, diameters, mask, voxres, sampling_radius,
origin, random_generator):
"""
Parameters
----------
fibers: list
Tractogram containing the fibertube centroids
diameters: list
Diameters of each fibertube
mask: any
VincentBeaud marked this conversation as resolved.
Show resolved Hide resolved
voxres: np.array(3,)
The pixel resolution, ex, using img.header.get_zooms()[:3].
sampling_radius: float
Radius of the sampling sphere to be used for degrading resolution.
origin: dipy Origin
random_generator: any
"""
self._prepare_and_store_data(fibers)
self.diameters = diameters
self.data = mask
self.dim = mask.shape[:3]
self.voxres = voxres
self.sampling_radius = sampling_radius
self.origin = origin
self.random_generator = random_generator

def _prepare_and_store_data(self, fibers):
if fibers is None:
self.tree = None
self.segments_indices = None
self.max_seg_length = None
self.fibers = None
return

# Flatten all segments of tractogram
segments_centers, segments_indices, max_seg_length = (
segment_tractogram(fibers, False))

self.tree = KDTree(segments_centers)
self.segments_indices = segments_indices
self.max_seg_length = max_seg_length
self.fibers, _ = get_streamlines_as_fixed_array(fibers)

def _validate_origin(self, origin):
if self.origin is not origin:
raise ValueError('The provided origin is not the same as'
'the origin used for instantiation.')

def get_value_at_idx(self, i, j, k):
i, j, k = self._clip_idx_to_bound(i, j, k)
return self._voxmm_to_value(i, j, k)

def get_value_at_coordinate(self, x, y, z, space, origin):
self._validate_origin(origin)

if space == Space.VOX:
return self._voxmm_to_value(*self.vox_to_voxmm(x, y, z), origin)
elif space == Space.VOXMM:
return self._voxmm_to_value(x, y, z, origin)
else:
raise NotImplementedError("We have not prepared the DataVolume "
"to work in RASMM space yet.")

def is_idx_in_bound(self, i, j, k):
return super().is_idx_in_bound(i, j, k)

def is_coordinate_in_bound(self, x, y, z, space, origin):
self._validate_origin(origin)
return super().is_coordinate_in_bound(x, y, z, space, origin)

# TODO: Figure out how to validate origin within vox_to_idx
VincentBeaud marked this conversation as resolved.
Show resolved Hide resolved

def voxmm_to_idx(self, x, y, z, origin):
self._validate_origin(origin)
return super().voxmm_to_idx(x, y, z, origin)

def vox_to_voxmm(self, x, y, z):
"""
Get mm space coordinates at position x, y, z (vox).

Parameters
----------
x, y, z: floats
Position coordinate (vox) along x, y, z axis.

Return
------
x, y, z: floats
mm space coordinates for position x, y, z.
"""

# Does not depend on origin!
# In each dimension:
# In corner: 0 to 1 will become 0 to voxres.
# In center: -0.5 to 0.5 will become -0.5*voxres to 0.5*voxres.
return [x * self.voxres[0],
y * self.voxres[1],
z * self.voxres[2]]

def _clip_voxmm_to_bound(self, x, y, z, origin):
return self.vox_to_voxmm(*self._clip_vox_to_bound(
*self.voxmm_to_vox(x, y, z), origin))

def _vox_to_value(self, x, y, z, origin):
return self._voxmm_to_value(*self.vox_to_voxmm(x, y, z), origin)

def _voxmm_to_value(self, x, y, z, origin):
x, y, z = self._clip_voxmm_to_bound(x, y, z, origin)

pos = np.array([x, y, z], dtype=np.float64)

neighbors = self.tree.query_radius(
pos, self.sampling_radius + self.max_seg_length / 2)[0]

return self.extract_directions(pos, neighbors, self.sampling_radius,
self.segments_indices, self.fibers,
self.diameters, self.random_generator)

def get_absolute_direction(self, x, y, z):
pos = np.array([x, y, z], np.float64)

neighbors = self.tree.query_radius(
pos, self.sampling_radius + self.max_seg_length / 2)[0]

for segi in neighbors:
fi, pi = self.segments_indices[segi]
fiber = self.fibers[fi]
radius = self.diameters[fi] / 2

if point_in_cylinder(fiber[pi], fiber[pi+1], radius, pos):
return fiber[pi+1] - fiber[pi]

return None

@staticmethod
VincentBeaud marked this conversation as resolved.
Show resolved Hide resolved
@njit
def extract_directions(pos, neighbors, sampling_radius, segments_indices,
VincentBeaud marked this conversation as resolved.
Show resolved Hide resolved
fibers, diameters, random_generator):
directions = []
volumes = []

for segi in neighbors:
fi, pi = segments_indices[segi]
fiber = fibers[fi]
fib_pt1 = fiber[pi]
fib_pt2 = fiber[pi+1]
dir = fib_pt2 - fib_pt1
radius = diameters[fi] / 2

volume, is_estimated = sphere_cylinder_intersection(
pos, sampling_radius, fib_pt1,
fib_pt2, radius, 1000, random_generator)
VincentBeaud marked this conversation as resolved.
Show resolved Hide resolved

# Catch estimation error when using very small sampling_radius.
if volume == 0 and is_estimated:
volume, _ = sphere_cylinder_intersection(
pos, sampling_radius, fib_pt1,
fib_pt2, radius, 10000, random_generator)

if volume > 0:
directions.append(dir / np.linalg.norm(dir))
volumes.append(volume)

if len(volumes) > 0:
max_vol = max(volumes)
for vol in volumes:
vol /= max_vol

return (directions, volumes)
24 changes: 24 additions & 0 deletions scilpy/io/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from fury import window
from PIL import Image
from scipy.io import loadmat
from tqdm import tqdm
import six

from scilpy.gradients.bvec_bval_tools import DEFAULT_B0_THRESHOLD
Expand Down Expand Up @@ -966,3 +967,26 @@ def get_default_screenshotting_data(args):
labelmap_img, \
mask_imgs, \
masks_colors


def v_enumerate(x, verbose):
if verbose:
return enumerate(tqdm(x))
else:
return enumerate(x)
VincentBeaud marked this conversation as resolved.
Show resolved Hide resolved


def save_dictionary(dictionary: dict, filename: str, overwrite: bool):
VincentBeaud marked this conversation as resolved.
Show resolved Hide resolved
with open(filename,
'w' if overwrite else 'x') as f:
f.writelines([str(key) + ': ' + str(dictionary[key]) + '\n'
for key in dictionary.keys()])


def load_dictionary(filename: str):
dictionary = {}
with open(filename, 'r') as f:
for line in f.readlines():
[key, value] = line.split(': ')
dictionary[key] = value.removesuffix('\n')
return dictionary
Loading
Loading