def test_resizing_op_mixed_uni_nonuni(): """Check if resizing along uniform axes in mixed discretizations works.""" nonuni_part = odl.nonuniform_partition([0, 1, 4]) uni_part = odl.uniform_partition(-1, 1, 4) part = uni_part.append(nonuni_part, uni_part, nonuni_part) fspace = odl.FunctionSpace(odl.IntervalProd(part.min_pt, part.max_pt)) tspace = odl.rn(part.shape) space = odl.DiscreteLp(fspace, part, tspace) # Keep non-uniform axes fixed res_op = odl.ResizingOperator(space, ran_shp=(6, 3, 6, 3)) assert res_op.axes == (0, 2) assert res_op.offset == (1, 0, 1, 0) # Evaluation test with a simpler case part = uni_part.append(nonuni_part) fspace = odl.FunctionSpace(odl.IntervalProd(part.min_pt, part.max_pt)) tspace = odl.rn(part.shape) space = odl.DiscreteLp(fspace, part, tspace) res_op = odl.ResizingOperator(space, ran_shp=(6, 3)) result = res_op(space.one()) true_result = [[0, 0, 0], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [0, 0, 0]] assert np.array_equal(result, true_result) # Test adjoint elem = noise_element(space) res_elem = noise_element(res_op.range) inner1 = res_op(elem).inner(res_elem) inner2 = elem.inner(res_op.adjoint(res_elem)) assert almost_equal(inner1, inner2)
def test_norm_nonuniform(): """Check if norms are correct in non-uniform discretizations.""" part = odl.nonuniform_partition([0, 2, 3, 5], min_pt=0, max_pt=5) weights = part.cell_sizes_vecs[0] tspace = odl.rn(part.size, weighting=weights) discr = odl.DiscretizedSpace(part, tspace) sqrt = discr.element(lambda x: np.sqrt(x)) # Exact norm is the square root of the integral from 0 to 5 of x, # which is sqrt(5**2 / 2) exact_norm = np.sqrt(5**2 / 2.0) norm = sqrt.norm() assert norm == pytest.approx(exact_norm)
def test_inner_nonuniform(): """Check if inner products are correct in non-uniform discretizations.""" part = odl.nonuniform_partition([0, 2, 3, 5], min_pt=0, max_pt=5) weights = part.cell_sizes_vecs[0] tspace = odl.rn(part.size, weighting=weights) discr = odl.DiscretizedSpace(part, tspace) one = discr.one() linear = discr.element(lambda x: x) # Exact inner product is the integral from 0 to 5 of x, which is 5**2 / 2 exact_inner = 5**2 / 2.0 inner = one.inner(linear) assert inner == pytest.approx(exact_inner)
def ExpOp_builder(bin_param, filter_space, interp): # Create binning scheme if interp == 'Full': spf_space = filter_space Exp_op = odl.IdentityOperator(filter_space) elif interp == 'uniform': # Create binning scheme dpix = np.size(filter_space) dsize = filter_space.max_pt filt_bin_space = odl.uniform_discr(-dsize, dsize, dpix // (bin_param)) spf_space = odl.uniform_discr(0, dsize, dpix // (2 * bin_param)) resamp = odl.Resampling(filt_bin_space, filter_space) sym = SymOp(spf_space, filt_bin_space) Exp_op = resamp * sym else: if interp == 'constant': interp = 'nearest' elif interp == 'linear': pass else: raise ValueError('unknown `expansion operator type` ({})' ''.format(interp)) B = ExpBin(bin_param, np.size(filter_space)) * \ filter_space.weighting.const B[-1] -= 1 / 2 * filter_space.weighting.const # Create sparse filter space spf_part = odl.nonuniform_partition(B, min_pt=0, max_pt=B[-1]) spf_weight = np.ravel( np.multiply.reduce(np.meshgrid(*spf_part.cell_sizes_vecs))) spf_fspace = odl.FunctionSpace(spf_part.set) spf_space = odl.DiscreteLp(spf_fspace, spf_part, odl.rn(spf_part.size, weighting=spf_weight), interp=interp) filt_pos_part = odl.uniform_partition(0, B[-1], int(np.size(filter_space) / 2)) filt_pos_space = odl.uniform_discr_frompartition(filt_pos_part, dtype='float64') lin_interp = odl.Resampling(spf_space, filt_pos_space) # Create symmetry operator sym = SymOp(filt_pos_space, filter_space) # Create sparse filter operator Exp_op = sym * lin_interp return spf_space, Exp_op
def elekta_icon_geometry(sad=780.0, sdd=1000.0, piercing_point=(390.0, 0.0), angles=None, num_angles=None, detector_shape=(780, 720)): sad = float(sad) assert sad > 0 sdd = float(sdd) assert sdd > sad piercing_point = np.array(piercing_point, dtype=float) assert piercing_point.shape == (2, ) if angles is not None and num_angles is not None: raise ValueError('cannot provide both `angles` and `num_angles`') elif angles is not None: angles = odl.nonuniform_partition(angles) assert angles.ndim == 1 elif num_angles is not None: angles = odl.uniform_partition(1.2, 5.0, num_angles) else: angles = odl.uniform_partition(1.2, 5.0, 332) detector_shape = np.array(detector_shape, dtype=int) # Constant system parameters pixel_size = 0.368 det_extent_mm = np.array([287.04, 264.96]) # Compute the detector partition piercing_point_mm = pixel_size * piercing_point det_min_pt = -piercing_point_mm det_max_pt = det_min_pt + det_extent_mm detector_partition = odl.uniform_partition(min_pt=det_min_pt, max_pt=det_max_pt, shape=detector_shape) # Create the geometry geometry = odl.tomo.ConeFlatGeometry(angles, detector_partition, src_radius=sad, det_radius=sdd - sad) return geometry
def __init__(self, dataset, angle_indices, impl=None): """ Parameters ---------- dataset : `Dataset` Basis CT dataset. Requirements: - sample elements are ``(observation, ground_truth)`` - :meth:`get_ray_trafo` gives corresponding ray transform. angle_indices : array-like or slice Indices of the angles to use from the observations. impl : {``'skimage'``, ``'astra_cpu'``, ``'astra_cuda'``},\ optional Implementation passed to :class:`odl.tomo.RayTransform` to construct :attr:`ray_trafo`. """ self.dataset = dataset self.angle_indices = (angle_indices if isinstance(angle_indices, slice) else np.asarray(angle_indices)) self.train_len = self.dataset.get_len('train') self.validation_len = self.dataset.get_len('validation') self.test_len = self.dataset.get_len('test') self.random_access = self.dataset.supports_random_access() self.num_elements_per_sample = ( self.dataset.get_num_elements_per_sample()) orig_geometry = self.dataset.get_ray_trafo(impl=impl).geometry apart = nonuniform_partition( orig_geometry.angles[self.angle_indices]) self.geometry = Parallel2dGeometry( apart=apart, dpart=orig_geometry.det_partition) orig_shape = self.dataset.get_shape() self.shape = ((apart.shape[0], orig_shape[0][1]), orig_shape[1]) self.space = (None, self.dataset.space[1]) # preliminary, needed for # call to get_ray_trafo self.ray_trafo = self.get_ray_trafo(impl=impl) super().__init__(space=(self.ray_trafo.range, self.dataset.space[1]))
def projector(request, dtype): n_angles = 200 geom, impl, angle = request.param.split() if angle == 'uniform': apart = odl.uniform_partition(0, 2 * np.pi, n_angles) elif angle == 'random': # Linearly spaced with random noise min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt, max_pt, n_angles) points += np.random.rand(n_angles) * (max_pt - min_pt) / (5 * n_angles) apart = odl.nonuniform_partition(points) elif angle == 'nonuniform': # Angles spaced quadratically min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt ** 0.5, max_pt ** 0.5, n_angles) ** 2 apart = odl.nonuniform_partition(points) else: raise ValueError('angle not valid') if geom == 'par2d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20], [20, 20], [100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition(-30, 30, 200) geom = tomo.Parallel2dGeometry(apart, dpart) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'par3d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20, -20], [20, 20, 20], [100, 100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition([-30, -30], [30, 30], [200, 200]) geom = tomo.Parallel3dAxisGeometry(apart, dpart, axis=[1, 0, 0]) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'cone2d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20], [20, 20], [100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition(-30, 30, 200) geom = tomo.FanFlatGeometry(apart, dpart, src_radius=200, det_radius=100) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'cone3d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20, -20], [20, 20, 20], [100, 100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition([-30, -30], [30, 30], [200, 200]) geom = tomo.CircularConeFlatGeometry( apart, dpart, src_radius=200, det_radius=100, axis=[1, 0, 0]) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'helical': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20, 0], [20, 20, 40], [100, 100, 100], dtype=dtype) # Geometry # TODO: angles n_angle = 700 apart = odl.uniform_partition(0, 8 * 2 * np.pi, n_angle) dpart = odl.uniform_partition([-30, -3], [30, 3], [200, 20]) geom = tomo.HelicalConeFlatGeometry(apart, dpart, pitch=5.0, src_radius=200, det_radius=100) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) else: raise ValueError('param not valid')
def test_shifted_volume(geometry_type): """Check that geometry shifts are handled correctly. We forward project a square/cube of all ones and check that the correct portion of the detector gets nonzero values. In the default setup, at angle 0, the source (if existing) is at (0, -s[, 0]), and the detector at (0, +d[, 0]) with the positive x axis as (first) detector axis. Thus, when shifting enough in the negative x direction, the object should be visible at the left half of the detector only. A shift in y should not influence the result (much). At +90 degrees, a shift in the negative y direction should have the same effect. """ apart = odl.nonuniform_partition([0, np.pi / 2, np.pi, 3 * np.pi / 2]) if geometry_type == 'par2d' and odl.tomo.ASTRA_AVAILABLE: ndim = 2 dpart = odl.uniform_partition(-30, 30, 30) geometry = odl.tomo.Parallel2dGeometry(apart, dpart) elif geometry_type == 'par3d' and odl.tomo.ASTRA_CUDA_AVAILABLE: ndim = 3 dpart = odl.uniform_partition([-30, -30], [30, 30], (30, 30)) geometry = odl.tomo.Parallel3dAxisGeometry(apart, dpart) if geometry_type == 'cone2d' and odl.tomo.ASTRA_AVAILABLE: ndim = 2 dpart = odl.uniform_partition(-30, 30, 30) geometry = odl.tomo.FanFlatGeometry(apart, dpart, src_radius=200, det_radius=100) elif geometry_type == 'cone3d' and odl.tomo.ASTRA_CUDA_AVAILABLE: ndim = 3 dpart = odl.uniform_partition([-30, -30], [30, 30], (30, 30)) geometry = odl.tomo.ConeFlatGeometry(apart, dpart, src_radius=200, det_radius=100) else: pytest.skip('no projector available for geometry type') min_pt = np.array([-5.0] * ndim) max_pt = np.array([5.0] * ndim) shift_len = 6 # enough to move the projection to one side of the detector # Shift along axis 0 shift = np.zeros(ndim) shift[0] = -shift_len # Generate 4 projections with 90 degrees increment space = odl.uniform_discr(min_pt + shift, max_pt + shift, [10] * ndim) ray_trafo = odl.tomo.RayTransform(space, geometry) proj = ray_trafo(space.one()) # Check that the object is projected to the correct place. With the # chosen setup, at least one ray should go through a substantial # part of the volume, yielding a value around 10 (=side length). # 0 degrees: All on the left assert np.max(proj[0, :15]) > 5 assert np.max(proj[0, 15:]) == 0 # 90 degrees: Left and right assert np.max(proj[1, :15]) > 5 assert np.max(proj[1, 15:]) > 5 # 180 degrees: All on the right assert np.max(proj[2, :15]) == 0 assert np.max(proj[2, 15:]) > 5 # 270 degrees: Left and right assert np.max(proj[3, :15]) > 5 assert np.max(proj[3, 15:]) > 5 # Do the same for axis 1 shift = np.zeros(ndim) shift[1] = -shift_len space = odl.uniform_discr(min_pt + shift, max_pt + shift, [10] * ndim) ray_trafo = odl.tomo.RayTransform(space, geometry) proj = ray_trafo(space.one()) # 0 degrees: Left and right assert np.max(proj[0, :15]) > 5 assert np.max(proj[0, 15:]) > 5 # 90 degrees: All on the left assert np.max(proj[1, :15]) > 5 assert np.max(proj[1, 15:]) == 0 # 180 degrees: Left and right assert np.max(proj[2, :15]) > 5 assert np.max(proj[2, 15:]) > 5 # 270 degrees: All on the right assert np.max(proj[3, :15]) == 0 assert np.max(proj[3, 15:]) > 5
def projector(request): n = 100 m = 100 n_angles = 100 dtype = 'float32' geom, impl, angle = request.param.split() if angle == 'uniform': apart = odl.uniform_partition(0, 2 * np.pi, n_angles) elif angle == 'half_uniform': apart = odl.uniform_partition(0, np.pi, n_angles) elif angle == 'random': # Linearly spaced with random noise min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt, max_pt, n_angles) points += np.random.rand(n_angles) * (max_pt - min_pt) / (5 * n_angles) apart = odl.nonuniform_partition(points) elif angle == 'nonuniform': # Angles spaced quadratically min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt ** 0.5, max_pt ** 0.5, n_angles) ** 2 apart = odl.nonuniform_partition(points) else: raise ValueError('angle not valid') if geom == 'par2d': # Reconstruction space reco_space = odl.uniform_discr([-20] * 2, [20] * 2, [n] * 2, dtype=dtype) # Geometry dpart = odl.uniform_partition(-30, 30, m) geom = odl.tomo.Parallel2dGeometry(apart, dpart) # Ray transform return odl.tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'par3d': # Reconstruction space reco_space = odl.uniform_discr([-20] * 3, [20] * 3, [n] * 3, dtype=dtype) # Geometry dpart = odl.uniform_partition([-30] * 2, [30] * 2, [m] * 2) geom = odl.tomo.Parallel3dAxisGeometry(apart, dpart) # Ray transform return odl.tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'cone2d': # Reconstruction space reco_space = odl.uniform_discr([-20] * 2, [20] * 2, [n] * 2, dtype=dtype) # Geometry dpart = odl.uniform_partition(-30, 30, m) geom = odl.tomo.FanFlatGeometry(apart, dpart, src_radius=200, det_radius=100) # Ray transform return odl.tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'cone3d': # Reconstruction space reco_space = odl.uniform_discr([-20] * 3, [20] * 3, [n] * 3, dtype=dtype) # Geometry dpart = odl.uniform_partition([-60] * 2, [60] * 2, [m] * 2) geom = odl.tomo.ConeFlatGeometry(apart, dpart, src_radius=200, det_radius=100) # Ray transform return odl.tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'helical': # Reconstruction space reco_space = odl.uniform_discr([-20, -20, 0], [20, 20, 40], [n] * 3, dtype=dtype) # Geometry, overwriting angle partition apart = odl.uniform_partition(0, 8 * 2 * np.pi, n_angles) dpart = odl.uniform_partition([-30, -3], [30, 3], [m] * 2) geom = odl.tomo.ConeFlatGeometry(apart, dpart, pitch=5.0, src_radius=200, det_radius=100) # Ray transform return odl.tomo.RayTransform(reco_space, geom, impl=impl) else: raise ValueError('geom not valid')
det_part = odl.uniform_partition(-10, 10, 1024) # Ray transform implementation (`None` means "take fastest available") impl = None # %% Define X1, Y1 and R11 # The size of the spatial region is such that it would fit onto the detector # in parallel beam geometry (14 x 14 [cm^2]). X1 = odl.uniform_discr([-7, -7], [7, 7], shape=(128, 128)) print('X1_px_size / Y1_px_size:', X1.cell_sides[0] / det_part.cell_sides[0]) # Full scan, 1 degree increment # TODO: this can probably be made significantly coarser Y1_angles = np.linspace(0, 2 * np.pi, 360, endpoint=False) Y1_angle_part = odl.nonuniform_partition(Y1_angles, min_pt=0, max_pt=2 * np.pi) # Put the detector close to the object to keep the magnification low. # Here it is roughly 1.2 ((50 + 10) / 50). Y1_geom = odl.tomo.FanFlatGeometry(Y1_angle_part, det_part, src_radius=50, det_radius=10) print('Y1 magnification:', (Y1_geom.src_radius + Y1_geom.det_radius) / Y1_geom.src_radius) R11 = odl.tomo.RayTransform(X1, Y1_geom, impl=impl) # Show footprint of a 10 x 10 [cm^2] square, this should not go outside # the detector region. R11(odl.phantom.cuboid(X1, [-5, -5], [5, 5])).show('Detector footprint of a 10x10 square')
def elekta_xvi_geometry(sad=1000.0, sdd=1500.0, piercing_point=(512.0, 512.0), angles=None, num_angles=None, detector_shape=(1024, 1024)): """Tomographic geometry of the Elekta XVI system. All measurments are given in millimeters unless otherwise stated. Parameters ---------- sad : float, optional Source to Axis distance. sdd : float, optional Source to Detector distance. piercing_point : sequence of foat, optional Position in the detector (in pixel coordinates) that a beam from the source, passing through the axis of rotation perpendicularly, hits. angles : array-like, optional List of angles given in radians that the projection images were taken at. Exclusive with num_angles. Default: np.linspace(0, 2 * np.pi, 650, endpoint=False) num_angles : int, optional Number of angles. Exclusive with angles. Default: 332 detector_shape : sequence of int, optional Shape of the detector (in pixels). Useful if a sub-sampled system should be studied. Returns ------- elekta_xvi_geometry : `ConeBeamGeometry` Examples -------- Create default geometry: >>> from odl.contrib import tomo >>> geometry = tomo.elekta_xvi_geometry() Use a smaller detector (improves efficiency): >>> small_geometry = tomo.elekta_xvi_geometry(detector_shape=[100, 100]) See Also -------- elekta_xvi_space : Default reconstruction space for the Elekta XVI CBCT. elekta_xvi_fbp: Default reconstruction method for the Elekta XVI CBCT. """ sad = float(sad) assert sad > 0 sdd = float(sdd) assert sdd > sad piercing_point = np.array(piercing_point, dtype=float) assert piercing_point.shape == (2, ) if angles is not None and num_angles is not None: raise ValueError('cannot provide both `angles` and `num_angles`') elif angles is not None: angles = odl.nonuniform_partition(angles) assert angles.ndim == 1 elif num_angles is not None: angles = odl.uniform_partition(0, 2 * np.pi, num_angles) else: angles = odl.uniform_partition(0, 2 * np.pi, 650) detector_shape = np.array(detector_shape, dtype=int) # Constant system parameters pixel_size = 0.4 det_extent_mm = np.array([409.6, 409.6]) # Compute the detector partition piercing_point_mm = pixel_size * piercing_point det_min_pt = -piercing_point_mm det_max_pt = det_min_pt + det_extent_mm detector_partition = odl.uniform_partition(min_pt=det_min_pt, max_pt=det_max_pt, shape=detector_shape) # Create the geometry geometry = odl.tomo.ConeBeamGeometry(angles, detector_partition, src_radius=sad, det_radius=sdd - sad) return geometry
def elekta_icon_geometry(sad=780.0, sdd=1000.0, piercing_point=(390.0, 0.0), angles=None, num_angles=None, detector_shape=(780, 720)): """Tomographic geometry of the Elekta Icon CBCT system. See the [whitepaper]_ for specific descriptions of each parameter. All measurments are given in millimeters unless otherwise stated. Parameters ---------- sad : float, optional Source to Axis distance. sdd : float, optional Source to Detector distance. piercing_point : sequence of foat, optional Position in the detector (in pixel coordinates) that a beam from the source, passing through the axis of rotation perpendicularly, hits. angles : array-like, optional List of angles given in radians that the projection images were taken at. Exclusive with num_angles. Default: np.linspace(1.2, 5.0, 332) num_angles : int, optional Number of angles. Exclusive with angles. Default: 332 detector_shape : sequence of int, optional Shape of the detector (in pixels). Useful if a sub-sampled system should be studied. Returns ------- elekta_icon_geometry : `ConeBeamGeometry` Examples -------- Create default geometry: >>> from odl.contrib import tomo >>> geometry = tomo.elekta_icon_geometry() Use a smaller detector (improves efficiency): >>> small_geometry = tomo.elekta_icon_geometry(detector_shape=[100, 100]) See Also -------- elekta_icon_space : Default reconstruction space for the Elekta Icon CBCT. elekta_icon_fbp: Default reconstruction method for the Elekta Icon CBCT. References ---------- .. [whitepaper] *Design and performance characteristics of a Cone Beam CT system for Leksell Gamma Knife Icon* """ sad = float(sad) assert sad > 0 sdd = float(sdd) assert sdd > sad piercing_point = np.array(piercing_point, dtype=float) assert piercing_point.shape == (2, ) if angles is not None and num_angles is not None: raise ValueError('cannot provide both `angles` and `num_angles`') elif angles is not None: angles = odl.nonuniform_partition(angles) assert angles.ndim == 1 elif num_angles is not None: angles = odl.uniform_partition(1.2, 5.0, num_angles) else: angles = odl.uniform_partition(1.2, 5.0, 332) detector_shape = np.array(detector_shape, dtype=int) # Constant system parameters pixel_size = 0.368 det_extent_mm = np.array([287.04, 264.96]) # Compute the detector partition piercing_point_mm = pixel_size * piercing_point det_min_pt = -piercing_point_mm det_max_pt = det_min_pt + det_extent_mm detector_partition = odl.uniform_partition(min_pt=det_min_pt, max_pt=det_max_pt, shape=detector_shape) # Create the geometry geometry = odl.tomo.ConeBeamGeometry(angles, detector_partition, src_radius=sad, det_radius=sdd - sad) return geometry
def projector(request): n_angles = 500 dtype = 'float32' geom, impl, angle = request.param.split() if angle == 'uniform': apart = odl.uniform_partition(0, 2 * np.pi, n_angles) elif angle == 'random': # Linearly spaced with random noise min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt, max_pt, n_angles) points += np.random.rand(n_angles) * (max_pt - min_pt) / (5 * n_angles) apart = odl.nonuniform_partition(points) elif angle == 'nonuniform': # Angles spaced quadratically min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt ** 0.5, max_pt ** 0.5, n_angles) ** 2 apart = odl.nonuniform_partition(points) else: raise ValueError('angle not valid') if geom == 'par2d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20], [20, 20], [100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition(-30, 30, 500) geom = tomo.Parallel2dGeometry(apart, dpart) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'par3d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20, -20], [20, 20, 20], [100, 100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition([-30, -30], [30, 30], [200, 200]) geom = tomo.Parallel3dAxisGeometry(apart, dpart, axis=[1, 1, 0]) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'cone2d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20], [20, 20], [100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition(-40, 40, 200) geom = tomo.FanFlatGeometry(apart, dpart, src_radius=100, det_radius=100) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'cone3d': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20, -20], [20, 20, 20], [100, 100, 100], dtype=dtype) # Geometry dpart = odl.uniform_partition([-50, -50], [50, 50], [200, 200]) geom = tomo.CircularConeFlatGeometry( apart, dpart, src_radius=100, det_radius=100, axis=[1, 0, 0]) # Ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) elif geom == 'helical': # Discrete reconstruction space discr_reco_space = odl.uniform_discr([-20, -20, 0], [20, 20, 40], [100, 100, 100], dtype=dtype) # Geometry # TODO: angles n_angle = 2000 apart = odl.uniform_partition(0, 8 * 2 * np.pi, n_angle) dpart = odl.uniform_partition([-50, -4], [50, 4], [200, 20]) geom = tomo.HelicalConeFlatGeometry(apart, dpart, pitch=5.0, src_radius=100, det_radius=100) # Windowed ray transform return tomo.RayTransform(discr_reco_space, geom, impl=impl) else: raise ValueError('param not valid')
def load_projections(folder, indices=None): """Load geometry and data stored in Mayo format from folder. Parameters ---------- folder : str Path to the folder where the Mayo DICOM files are stored. indices : optional Indices of the projections to load. Accepts advanced indexing such as slice or list of indices. Returns ------- geometry : ConeFlatGeometry Geometry corresponding to the Mayo projector. proj_data : `numpy.ndarray` Projection data, given as the line integral of the linear attenuation coefficient (g/cm^3). Its unit is thus g/cm^2. """ datasets, data_array = _read_projections(folder, indices) # Get the angles angles = [d.DetectorFocalCenterAngularPosition for d in datasets] angles = -np.unwrap(angles) - np.pi # different defintion of angles # Set minimum and maximum corners shape = np.array([ datasets[0].NumberofDetectorColumns, datasets[0].NumberofDetectorRows ]) pixel_size = np.array([ datasets[0].DetectorElementTransverseSpacing, datasets[0].DetectorElementAxialSpacing ]) # Correct from center of pixel to corner of pixel minp = -(np.array(datasets[0].DetectorCentralElement) - 0.5) * pixel_size maxp = minp + shape * pixel_size # Select geometry parameters src_radius = datasets[0].DetectorFocalCenterRadialDistance det_radius = (datasets[0].ConstantRadialDistance - datasets[0].DetectorFocalCenterRadialDistance) # For unknown reasons, mayo does not include the tag # "TableFeedPerRotation", which is what we want. # Instead we manually compute the pitch pitch = ((datasets[-1].DetectorFocalCenterAxialPosition - datasets[0].DetectorFocalCenterAxialPosition) / ((np.max(angles) - np.min(angles)) / (2 * np.pi))) # Get flying focal spot data offset_axial = np.array([d.SourceAxialPositionShift for d in datasets]) offset_angular = np.array([d.SourceAngularPositionShift for d in datasets]) offset_radial = np.array([d.SourceRadialDistanceShift for d in datasets]) # TODO(adler-j): Implement proper handling of flying focal spot. # Currently we do not fully account for it, merely making some "first # order corrections" to the detector position and radial offset. # Update angles with flying focal spot (in plane direction). # This increases the resolution of the reconstructions. angles = angles - offset_angular # We correct for the mean offset due to the rotated angles, we need to # shift the detector. offset_detector_by_angles = det_radius * np.mean(offset_angular) minp[0] -= offset_detector_by_angles maxp[0] -= offset_detector_by_angles # We currently apply only the mean of the offsets src_radius = src_radius + np.mean(offset_radial) # Partially compensate for a movement of the source by moving the object # instead. We need to rescale by the magnification to get the correct # change in the detector. This approximation is only exactly valid on the # axis of rotation. mean_offset_along_axis_for_ffz = np.mean(offset_axial) * ( src_radius / (src_radius + det_radius)) # Create partition for detector detector_partition = odl.uniform_partition(minp, maxp, shape) # Convert offset to odl defintions offset_along_axis = (mean_offset_along_axis_for_ffz + datasets[0].DetectorFocalCenterAxialPosition - angles[0] / (2 * np.pi) * pitch) # Assemble geometry angle_partition = odl.nonuniform_partition(angles) geometry = odl.tomo.ConeFlatGeometry(angle_partition, detector_partition, src_radius=src_radius, det_radius=det_radius, pitch=pitch, offset_along_axis=offset_along_axis) # Create a *temporary* ray transform (we need its range) spc = odl.uniform_discr([-1] * 3, [1] * 3, [32] * 3) ray_trafo = odl.tomo.RayTransform(spc, geometry, interp='linear') # convert coordinates theta, up, vp = ray_trafo.range.grid.meshgrid d = src_radius + det_radius u = d * np.arctan(up / d) v = d / np.sqrt(d**2 + up**2) * vp # Calculate projection data in rectangular coordinates since we have no # backend that supports cylindrical proj_data_cylinder = ray_trafo.range.element(data_array) interpolated_values = proj_data_cylinder.interpolation((theta, u, v)) proj_data = ray_trafo.range.element(interpolated_values) return geometry, proj_data.asarray()
def projector(request): n = 100 m = 100 n_angles = 100 dtype = 'float32' geom, impl, angle = request.param.split() if angle == 'uniform': apart = odl.uniform_partition(0, 2 * np.pi, n_angles) elif angle == 'half_uniform': apart = odl.uniform_partition(0, np.pi, n_angles) elif angle == 'random': # Linearly spaced with random noise min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt, max_pt, n_angles) points += np.random.rand(n_angles) * (max_pt - min_pt) / (5 * n_angles) apart = odl.nonuniform_partition(points) elif angle == 'nonuniform': # Angles spaced quadratically min_pt = 2 * (2.0 * np.pi) / n_angles max_pt = (2.0 * np.pi) - 2 * (2.0 * np.pi) / n_angles points = np.linspace(min_pt ** 0.5, max_pt ** 0.5, n_angles) ** 2 apart = odl.nonuniform_partition(points) else: raise ValueError('angle not valid') if geom == 'par2d': # Discrete reconstruction space reco_space = odl.uniform_discr([-20] * 2, [20] * 2, [n] * 2, dtype=dtype) # Geometry dpart = odl.uniform_partition(-30, 30, m) geom = tomo.Parallel2dGeometry(apart, dpart) return tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'par3d': # Discrete reconstruction space reco_space = odl.uniform_discr([-20] * 3, [20] * 3, [n] * 3, dtype=dtype) # Geometry dpart = odl.uniform_partition([-30] * 2, [30] * 2, [n] * 2) geom = tomo.Parallel3dAxisGeometry(apart, dpart) # Ray transform return tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'cone2d': # Discrete reconstruction space reco_space = odl.uniform_discr([-20] * 2, [20] * 2, [n] * 2, dtype=dtype) # Geometry dpart = odl.uniform_partition(-30, 30, m) geom = tomo.FanFlatGeometry(apart, dpart, src_radius=200, det_radius=100) # Ray transform return tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'cone3d': # Discrete reconstruction space reco_space = odl.uniform_discr([-20] * 3, [20] * 3, [n] * 3, dtype=dtype) # Geometry dpart = odl.uniform_partition([-60] * 2, [60] * 2, [m] * 2) geom = tomo.CircularConeFlatGeometry(apart, dpart, src_radius=200, det_radius=100) # Ray transform return tomo.RayTransform(reco_space, geom, impl=impl) elif geom == 'helical': # Discrete reconstruction space reco_space = odl.uniform_discr([-20, -20, 0], [20, 20, 40], [n] * 3, dtype=dtype) # overwrite angle apart = odl.uniform_partition(0, 8 * 2 * np.pi, n_angles) dpart = odl.uniform_partition([-30, -3], [30, 3], [m] * 2) geom = tomo.HelicalConeFlatGeometry(apart, dpart, pitch=5.0, src_radius=200, det_radius=100) # Ray transform return tomo.RayTransform(reco_space, geom, impl=impl) else: raise ValueError('geom not valid')
path = '/export/scratch2/kohr/data/Charge_Density/MIP' fname = 'MIP_LH31' with odl.tomo.FileReaderMRC(os.path.join(path, fname)) as reader: header = reader.read_header() data_arr = reader.read_data() angles = np.deg2rad([ -48.0000, -44.0000, -42.0000, -40.0000, -38.0000, -36.0000, -34.0000, -32.0000, -26.0000, -24.0000, -17.0000, -14.0000, -12.0000, -10.0000, -8.00000, 1.00000, 3.00000, 6.00000, 9.00000, 12.0000, 15.0000, 18.0000, 21.0000, 24.0000, 27.0000, 30.0000, 33.0000, 36.0000, 40.0000, 43.0000, 46.0000 ]) angle_part = odl.nonuniform_partition(angles, min_pt=angles[0], max_pt=angles[-1]) num_angles = reader.data_shape[0] assert num_angles == angles.size det_shape = reader.data_shape[1:] det_width = np.array(det_shape, dtype=float) det_part = odl.uniform_partition(-det_width / 2, det_width / 2, det_shape) geom = odl.tomo.Parallel3dAxisGeometry(angle_part, det_part) # Do some crude data rescaling and cropping win_size = 50 # Subtract the background means
def load_projections(folder, proj_start=1, proj_end=-1): """Load geometry and data stored in Mayo format from folder. Parameters ---------- folder : str Path to the folder where the Mayo DICOM files are stored. proj_start : int Index of the first projection to use. Used for subsampling. proj_end : int Index of the final projection to use. Returns ------- geometry : ConeFlatGeometry Geometry corresponding to the Mayo projector. proj_data : `numpy.ndarray` Projection data, given as the line integral of the linear attenuation coefficient (g/cm^3). Its unit is thus g/cm^2. """ datasets, projections = _read_projections(folder, proj_start, proj_end) data_array = np.empty((len(projections), ) + projections[0].shape, dtype='float32') # Move data to a big array, change order for i, proj in enumerate(projections): data_array[i] = proj[:, ::-1] # Get the angles angles = [d.DetectorFocalCenterAngularPosition for d in datasets] angles = -np.unwrap(angles) - np.pi # different defintion of angles # Make a parallel beam geometry with flat detector angle_partition = odl.nonuniform_partition(angles) # Set minimum and maximum point shape = np.array([ datasets[0].NumberofDetectorColumns, datasets[0].NumberofDetectorRows ]) pixel_size = np.array([ datasets[0].DetectorElementTransverseSpacing, datasets[0].DetectorElementAxialSpacing ]) minp = -(np.array(datasets[0].DetectorCentralElement) - 0.5) * pixel_size maxp = minp + shape * pixel_size # Create partition for detector detector_partition = odl.uniform_partition(minp, maxp, shape) # Select geometry parameters src_radius = datasets[0].DetectorFocalCenterRadialDistance det_radius = (datasets[0].ConstantRadialDistance - datasets[0].DetectorFocalCenterRadialDistance) # Convert pitch and offset to odl defintions pitch = (pixel_size[1] * shape[1] * datasets[0].SpiralPitchFactor * src_radius / (src_radius + det_radius)) offset_along_axis = (datasets[0].DetectorFocalCenterAxialPosition - angles[0] / (2 * np.pi) * pitch) # Get flying focal spot data offset_axial = np.array([d.SourceAxialPositionShift for d in datasets]) offset_angular = np.array([d.SourceAngularPositionShift for d in datasets]) offset_radial = np.array([d.SourceRadialDistanceShift for d in datasets]) angles_offset = angles - offset_angular src_rad_offset = src_radius + offset_radial offset_x = (np.cos(angles_offset) * (-src_rad_offset) - np.cos(angles) * (-src_radius)) offset_y = (np.sin(angles_offset) * (-src_rad_offset) - np.sin(angles) * (-src_radius)) offset_z = offset_axial # TODO: WE CURRENTLY IGNORE THE OFFSETS DUE TO FLYING FOCAL SPOT source_offsets = np.array([offset_x, offset_y, offset_z]).T # Assemble geometry geometry = odl.tomo.ConeFlatGeometry(angle_partition, detector_partition, src_radius=src_radius, det_radius=det_radius, pitch=pitch, offset_along_axis=offset_along_axis) # Create a *temporary* ray transform (we need its range) spc = odl.uniform_discr([-1] * 3, [1] * 3, [32] * 3) ray_trafo = odl.tomo.RayTransform(spc, geometry, interp='linear') # convert coordinates theta, up, vp = ray_trafo.range.grid.meshgrid d = src_radius + det_radius u = d * np.arctan(up / d) v = d / np.sqrt(d**2 + up**2) * vp # Calculate projection data in rectangular coordinates since we have no # backend that supports cylindrical proj_data_cylinder = ray_trafo.range.element(data_array) interpolated_values = proj_data_cylinder.interpolation((theta, u, v)) proj_data = ray_trafo.range.element(interpolated_values) return geometry, proj_data.asarray()