diff --git a/scilpy/image/datasets.py b/scilpy/image/datasets.py index 8bdffa93d..ceef9a3ee 100644 --- a/scilpy/image/datasets.py +++ b/scilpy/image/datasets.py @@ -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. @@ -95,28 +97,18 @@ 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). @@ -124,15 +116,29 @@ def voxmm_to_vox(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 ------ 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. @@ -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 ------ @@ -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 @@ -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. @@ -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)) diff --git a/scilpy/tracking/propagator.py b/scilpy/tracking/propagator.py index 768734087..7bff9a106 100644 --- a/scilpy/tracking/propagator.py +++ b/scilpy/tracking/propagator.py @@ -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 ---------- @@ -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. diff --git a/scilpy/tracking/seed.py b/scilpy/tracking/seed.py index 3a51a77aa..549061590 100644 --- a/scilpy/tracking/seed.py +++ b/scilpy/tracking/seed.py @@ -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), diff --git a/scilpy/tracking/tracker.py b/scilpy/tracking/tracker.py index 9587c53e7..495216d71 100644 --- a/scilpy/tracking/tracker.py +++ b/scilpy/tracking/tracker.py @@ -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.") @@ -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]) @@ -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: @@ -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 diff --git a/scilpy/tracking/tracking_field.py b/scilpy/tracking/tracking_field.py index 97dd6b7d8..28e1027e5 100644 --- a/scilpy/tracking/tracking_field.py +++ b/scilpy/tracking/tracking_field.py @@ -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 ' @@ -175,7 +179,7 @@ def _get_sf(self, pos): Parameters ---------- pos: ndarray (3,) - Position in mm in the trackable dataset. + Position in voxmm in the trackable dataset. Return ------ @@ -183,7 +187,7 @@ def _get_sf(self, pos): 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) diff --git a/scripts/scil_compute_local_tracking_dev.py b/scripts/scil_compute_local_tracking_dev.py index 440953087..2404c84de 100644 --- a/scripts/scil_compute_local_tracking_dev.py +++ b/scripts/scil_compute_local_tracking_dev.py @@ -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