Skip to content

Commit

Permalink
Making sure origin is coherent everywhere
Browse files Browse the repository at this point in the history
  • Loading branch information
EmmaRenauld committed Nov 23, 2021
1 parent a2c1223 commit dd8c66b
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 40 deletions.
96 changes: 64 additions & 32 deletions scilpy/image/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,20 @@ def is_voxel_in_bound(self, i, j, k):
Parameters
----------
i, j, k: ints
Voxel indice along each axis.
i, j, k: ints or floats
Voxel indice along each axis (as ints) or voxel coordinates in
voxel world (as floats).
Return
------
out: bool
True if voxel is in dataset range, False otherwise.
"""
return (0 <= i < self.dim[0] and 0 <= j < self.dim[1] and
0 <= k < self.dim[2])
return (0 <= i <= (self.dim[0] - 1) and
0 <= j <= (self.dim[1] - 1) and
0 <= k <= (self.dim[2] - 1))

def voxmm_to_idx(self, x, y, z, origin='center'):
def voxmm_to_idx(self, x, y, z, origin):
"""
Get the 3D indice of the closest voxel at position x, y, z expressed
in mm.
Expand All @@ -95,44 +97,48 @@ def voxmm_to_idx(self, x, y, z, origin='center'):
x, y, z: floats
Position coordinate (mm) along x, y, z axis.
origin: str
'Center': Voxel 0,0,0 goes from [-resx/2, -resy/2, -resz/2] to
'center': Voxel 0,0,0 goes from [-resx/2, -resy/2, -resz/2] to
[resx/2, resy/2, resz/2].
'Corner': Voxel 0,0,0 goes from [0,0,0] to [resx, resy, resz].
'corner': Voxel 0,0,0 goes from [0,0,0] to [resx, resy, resz].
Return
------
out: list
3D indice of voxel at position x, y, z.
"""
if origin == 'center':
return np.asarray([(x + self.voxres[0] / 2) // self.voxres[0],
(y + self.voxres[1] / 2) // self.voxres[1],
(z + self.voxres[2] / 2) // self.voxres[2]],
dtype=int)
elif origin == 'corner':
return np.asarray([x // self.voxres[0],
y // self.voxres[1],
z // self.voxres[2]], dtype=int)
else:
raise ValueError("Origin must be one of 'center' or 'corner'.")
return np.floor(self.voxmm_to_vox(x, y, z, origin))

def voxmm_to_vox(self, x, y, z):
def voxmm_to_vox(self, x, y, z, origin):
"""
Get voxel space coordinates at position x, y, z (mm).
Parameters
----------
x, y, z: floats
Position coordinate (mm) along x, y, z axis.
origin: str
'center': Voxel 0,0,0 goes from [-resx/2, -resy/2, -resz/2] to
[resx/2, resy/2, resz/2].
'corner': Voxel 0,0,0 goes from [0,0,0] to [resx, resy, resz].
Return
------
out: list
Voxel space coordinates for position x, y, z.
"""
return [x / self.voxres[0], y / self.voxres[1], z / self.voxres[2]]
if origin == 'center':
half_res = self.voxres / 2.
return [(x + half_res[0]) / self.voxres[0],
(y + half_res[1]) / self.voxres[1],
(z + half_res[2]) / self.voxres[2]]
elif origin == 'corner':
return [x / self.voxres[0],
y / self.voxres[1],
z / self.voxres[2]]
else:
raise ValueError("Origin should be 'center' or 'corner'.")

def voxmm_to_value(self, x, y, z):
def voxmm_to_value(self, x, y, z, origin):
"""
Get the voxel value at voxel position x, y, z (mm) in the dataset.
If the coordinates are out of bound, the nearest voxel value is taken.
Expand All @@ -142,6 +148,10 @@ def voxmm_to_value(self, x, y, z):
----------
x, y, z: floats
Position coordinate (mm) along x, y, z axis.
origin: str
'center': Voxel 0,0,0 goes from [-resx/2, -resy/2, -resz/2] to
[resx/2, resy/2, resz/2].
'corner': Voxel 0,0,0 goes from [0,0,0] to [resx, resy, resz].
Return
------
Expand All @@ -150,20 +160,42 @@ def voxmm_to_value(self, x, y, z):
is of length 1, return a scalar value.
"""
if self.interpolation is not None:
if not self.is_voxmm_in_bound(x, y, z):
if not self.is_voxmm_in_bound(x, y, z, origin):
eps = float(1e-8) # Epsilon to exclude upper borders
x = max(-self.voxres[0] / 2,
min(self.voxres[0] * (self.dim[0] - 0.5 - eps), x))
y = max(-self.voxres[1] / 2,
min(self.voxres[1] * (self.dim[1] - 0.5 - eps), y))
z = max(-self.voxres[2] / 2,
min(self.voxres[2] * (self.dim[2] - 0.5 - eps), z))
coord = np.array(self.voxmm_to_vox(x, y, z), dtype=np.float64)

if origin == 'corner':
x = max(0,
min(self.voxres[0] * (self.dim[0] - eps), x))
y = max(0,
min(self.voxres[1] * (self.dim[1] - eps), y))
z = max(0,
min(self.voxres[2] * (self.dim[2] - eps), z))
elif origin == 'center':
x = max(-self.voxres[0] / 2,
min(self.voxres[0] * (self.dim[0] - 0.5 - eps), x))
y = max(-self.voxres[1] / 2,
min(self.voxres[1] * (self.dim[1] - 0.5 - eps), y))
z = max(-self.voxres[2] / 2,
min(self.voxres[2] * (self.dim[2] - 0.5 - eps), z))
else:
raise ValueError("Origin should be 'center' or 'corner'.")

coord = np.array(self.voxmm_to_vox(x, y, z, origin),
dtype=np.float64)

# Interpolation: Using dipy's pyx methods. The doc can be found in
# the file dipy.core.interpolation.pxd. Dipy works with origin
# center.
if origin == 'corner':
coord -= 0.5
if self.interpolation == 'nearest':
# They use round(point), not floor. This is the equivalent of
# origin = 'center'.
result = nearestneighbor_interpolate(self.data, coord)
else:
# Trilinear
# They do not say it explicitely but they verify if
# point[i] < -.5 or point[i] >= (data.shape[i] - .5),
# meaning that they work with origin='center'.
result = trilinear_interpolate4d(self.data, coord)

# Squeezing returns only value instead of array of length 1 if 3D
Expand All @@ -173,7 +205,7 @@ def voxmm_to_value(self, x, y, z):
raise Exception("No interpolation method was given, cannot run "
"this method..")

def is_voxmm_in_bound(self, x, y, z, origin='center'):
def is_voxmm_in_bound(self, x, y, z, origin):
"""
Test if the position x, y, z mm is in the dataset range.
Expand All @@ -191,4 +223,4 @@ def is_voxmm_in_bound(self, x, y, z, origin='center'):
value: bool
True if position is in dataset range and false otherwise.
"""
return self.is_voxel_in_bound(*self.voxmm_to_idx(x, y, z, origin))
return self.is_voxel_in_bound(*self.voxmm_to_vox(x, y, z, origin))
6 changes: 4 additions & 2 deletions scilpy/tracking/propagator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

class AbstractPropagator(object):
"""
Abstract class for tracker object.
Abstract class for propagator object. Responsible for sampling the final
direction out of the possible directions offered by the tracking field, and
for the propagation through Runge-Kutta integration.
Parameters
----------
Expand Down Expand Up @@ -138,7 +140,7 @@ def propagate(self, pos, v_in):

return new_pos, new_dir, is_direction_valid

def is_voxmm_in_bound(self, pos, origin='center'):
def is_voxmm_in_bound(self, pos, origin):
"""
Test if the streamline point is inside the boundary of the image.
Expand Down
4 changes: 4 additions & 0 deletions scilpy/tracking/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def __init__(self, data, voxres):
self.data = data
self.voxres = voxres

# Everything scilpy.tracking is in 'corner', 'voxmm'
self.origin = 'corner'
self.space = 'voxmm'

# self.seed are all the voxels where a seed could be placed
# (voxel space, int numbers).
self.seeds = np.array(np.where(np.squeeze(data) > 0),
Expand Down
14 changes: 10 additions & 4 deletions scilpy/tracking/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def __init__(self, propagator: AbstractPropagator, mask: DataVolume,
self.track_forward_only = track_forward_only
self.skip = skip

# Everything scilpy.tracking is in 'corner', 'voxmm'
self.origin = 'corner'
self.space = 'voxmm'

if self.min_nbr_pts <= 0:
logging.warning("Minimum number of points cannot be 0. Changed to "
"1.")
Expand Down Expand Up @@ -164,7 +168,6 @@ def _get_streamlines_sub(self, chunk_id):
"""
global data_file_info

# args[0] is the Tracker.
self.propagator.tracking_field.dataset.data = np.load(
data_file_info[0], mmap_mode=data_file_info[1])

Expand Down Expand Up @@ -315,8 +318,10 @@ def _propagate_line(self, is_forward):
# Bound can be checked with mask or tracking field
# (through self.propagator.is_voxmm_in_bound)
propagation_can_continue = (
self.mask.voxmm_to_value(*line[-1]) > 0 and
self.mask.is_voxmm_in_bound(*line[-1], origin='corner'))
self.mask.voxmm_to_value(*line[-1],
origin=self.origin) > 0 and
self.mask.is_voxmm_in_bound(*line[-1],
origin=self.origin))
last_dir = new_dir

if propagation_can_continue:
Expand All @@ -328,7 +333,8 @@ def _propagate_line(self, is_forward):
# Last cleaning of the streamline
# First position is the seed: necessarily in bound.
while (len(line) > 1 and
not self.propagator.is_voxmm_in_bound(line[-1], 'corner')):
not self.propagator.is_voxmm_in_bound(line[-1],
origin=self.origin)):
line.pop()

return line
8 changes: 6 additions & 2 deletions scilpy/tracking/tracking_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def __init__(self, dataset, theta, dipy_sphere=None):
self.theta = theta
self.dataset = dataset

# Everything scilpy.tracking is in 'corner', 'voxmm'
self.origin = 'corner'
self.space = 'voxmm'

if dipy_sphere:
if 'symmetric' not in dipy_sphere:
raise ValueError('Sphere must be symmetric. Call to '
Expand Down Expand Up @@ -175,15 +179,15 @@ def _get_sf(self, pos):
Parameters
----------
pos: ndarray (3,)
Position in mm in the trackable dataset.
Position in voxmm in the trackable dataset.
Return
------
sf: ndarray (len(self.sphere.vertices),)
Spherical function evaluated at pos, normalized by
its maximum amplitude.
"""
sh = self.dataset.voxmm_to_value(*pos)
sh = self.dataset.voxmm_to_value(*pos, self.origin)
sf = np.dot(self.B.T, sh).reshape((-1, 1))

sf_max = np.max(sf)
Expand Down
2 changes: 2 additions & 0 deletions scripts/scil_compute_local_tracking_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ def main():
seed_res = seed_img.header.get_zooms()[:3]
seed_generator = SeedGenerator(seed_data, seed_res)
if args.npv:
# toDo. This will not really produce n seeds per voxel, only true
# in average.
nbr_seeds = len(seed_generator.seeds) * args.npv
elif args.nt:
nbr_seeds = args.nt
Expand Down

0 comments on commit dd8c66b

Please sign in to comment.