def lotus_root_geometry(): """Tomographic geometry for the lotus root dataset. Notes ----- See the article `Tomographic X-ray data of a lotus root filled with attenuating objects`_ for further information. See Also -------- lotus_root_geometry References ---------- .. _Tomographic X-ray data of a lotus root filled with attenuating objects: https://arxiv.org/abs/1609.07299 """ # To get the same rotation as in the reference article a_offset = np.pi / 2 apart = uniform_partition(a_offset, a_offset + 2 * np.pi * 366. / 360., 366) # TODO: Find exact value, determined experimentally d_offset = 0.35 dpart = uniform_partition(d_offset - 60, d_offset + 60, 2240) geometry = FanFlatGeometry(apart, dpart, src_radius=540, det_radius=90) return geometry
def walnut_geometry(): """Tomographic geometry for the walnut dataset. Notes ----- See the article `Tomographic X-ray data of a walnut`_ for further information. See Also -------- walnut_data References ---------- .. _Tomographic X-ray data of a walnut: https://arxiv.org/abs/1502.04064 """ # To get the same rotation as in the reference article a_offset = -np.pi / 2 apart = uniform_partition(a_offset, a_offset + 2 * np.pi, 1200) # TODO: Find exact value, determined experimentally d_offset = -0.279 dpart = uniform_partition(d_offset - 57.4, d_offset + 57.4, 2296) geometry = FanFlatGeometry(apart, dpart, src_radius=110, det_radius=190) return geometry
def limited_beam_geometry(space, num_angles=None, max_degree=None, det_shape=None): # Find maximum distance from rotation axis corners = space.domain.corners()[:, :2] # corners come from the imagespace rho = np.max(np.linalg.norm(corners, axis=1)) # Find default values according to Nyquist criterion. # We assume that the function is bandlimited by a wave along the x or y # axis. The highest frequency we can measure is then a standing wave with # period of twice the inter-node distance. min_side = min(space.partition.cell_sides[:2]) omega = np.pi / min_side num_px_horiz = 2 * int(np.ceil(rho * omega / np.pi)) + 1 if space.ndim == 2: det_min_pt = -rho det_max_pt = rho if det_shape is None: det_shape = num_px_horiz elif space.ndim == 3: num_px_vert = space.shape[2] min_h = space.domain.min_pt[2] max_h = space.domain.max_pt[2] det_min_pt = [-rho, min_h] det_max_pt = [rho, max_h] if det_shape is None: det_shape = [num_px_horiz, num_px_vert] if num_angles is None: num_angles = int(np.ceil(omega * rho)) max_radian = max_degree * (np.pi / 180) angle_partition = uniform_partition(0, max_radian, num_angles) det_partition = uniform_partition(det_min_pt, det_max_pt, det_shape) if space.ndim == 2: return Parallel2dGeometry(angle_partition, det_partition) elif space.ndim == 3: return Parallel3dAxisGeometry(angle_partition, det_partition) else: raise ValueError('``space.ndim`` must be 2 or 3.')
def skimage_proj_space(geometry, volume_space, proj_space): """Create a projection space adapted to the skimage radon geometry.""" padded_size = int(np.ceil(volume_space.shape[0] * np.sqrt(2))) det_width = volume_space.domain.extent[0] * np.sqrt(2) det_part = uniform_partition(-det_width / 2, det_width / 2, padded_size) part = geometry.motion_partition.insert(1, det_part) space = uniform_discr_frompartition(part, dtype=proj_space.dtype) return space
def scikit_sinogram_space(geometry, volume_space, sinogram_space): """Create a range adapted to the scikit radon geometry.""" padded_size = int(np.ceil(volume_space.shape[0] * np.sqrt(2))) det_width = volume_space.domain.extent()[0] * np.sqrt(2) scikit_detector_part = uniform_partition(-det_width / 2.0, det_width / 2.0, padded_size) scikit_range_part = geometry.motion_partition.insert( 1, scikit_detector_part) scikit_range = uniform_discr_frompartition(scikit_range_part, interp=sinogram_space.interp, dtype=sinogram_space.dtype) return scikit_range
def scikit_sinogram_space(geometry, volume_space, sinogram_space): """Create a range adapted to the scikit radon geometry.""" padded_size = int(np.ceil(volume_space.shape[0] * np.sqrt(2))) det_width = volume_space.domain.extent()[0] * np.sqrt(2) scikit_detector_part = uniform_partition(-det_width / 2.0, det_width / 2.0, padded_size) scikit_range_part = geometry.motion_partition.insert(1, scikit_detector_part) scikit_range = uniform_discr_frompartition(scikit_range_part, interp=sinogram_space.interp, dtype=sinogram_space.dtype) return scikit_range
def angle_beam_geometry( space, angle_array, det_shape=None ): #this function allows to specify a new geometry by providing the projection angles # Find maximum distance from rotation axis corners = space.domain.corners()[:, :2] rho = np.max(np.linalg.norm(corners, axis=1)) # Find default values according to Nyquist criterion. # We assume that the function is bandlimited by a wave along the x or y # axis. The highest frequency we can measure is then a standing wave with # period of twice the inter-node distance. min_side = min(space.partition.cell_sides[:2]) omega = np.pi / min_side num_px_horiz = 2 * int(np.ceil(rho * omega / np.pi)) + 1 if space.ndim == 2: det_min_pt = -rho det_max_pt = rho if det_shape is None: det_shape = num_px_horiz elif space.ndim == 3: num_px_vert = space.shape[2] min_h = space.domain.min_pt[2] max_h = space.domain.max_pt[2] det_min_pt = [-rho, min_h] det_max_pt = [rho, max_h] if det_shape is None: det_shape = [num_px_horiz, num_px_vert] angle_partition = nonuniform_partition(angle_array, min_pt=0, max_pt=np.pi) det_partition = uniform_partition(det_min_pt, det_max_pt, det_shape) if space.ndim == 2: return Parallel2dGeometry(angle_partition, det_partition) elif space.ndim == 3: return Parallel3dAxisGeometry(angle_partition, det_partition) else: raise ValueError('``space.ndim`` must be 2 or 3.')
def _resize_discr(discr, newshp, offset, discr_kwargs): """Return a space based on ``discr`` and ``newshp``. Use the domain of ``discr`` and its partition to create a new uniformly discretized space with ``newshp`` as shape. In axes where ``offset`` is given, it determines the number of added/removed cells to the left. Where ``offset`` is ``None``, the points are distributed evenly to left and right. The ``discr_kwargs`` parameter is passed to `uniform_discr` for further specification of discretization parameters. """ nodes_on_bdry = discr_kwargs.get('nodes_on_bdry', False) if np.shape(nodes_on_bdry) == (): nodes_on_bdry = ([(bool(nodes_on_bdry), bool(nodes_on_bdry))] * discr.ndim) elif discr.ndim == 1 and len(nodes_on_bdry) == 2: nodes_on_bdry = [nodes_on_bdry] elif len(nodes_on_bdry) != discr.ndim: raise ValueError('`nodes_on_bdry` has length {}, expected {}' ''.format(len(nodes_on_bdry), discr.ndim)) dtype = discr_kwargs.pop('dtype', discr.dtype) impl = discr_kwargs.pop('impl', discr.impl) exponent = discr_kwargs.pop('exponent', discr.exponent) interp = discr_kwargs.pop('interp', discr.interp) weighting = discr_kwargs.pop('weighting', discr.weighting) affected = np.not_equal(newshp, discr.shape) ndim = discr.ndim for i in range(ndim): if affected[i] and not discr.is_uniform_byaxis[i]: raise ValueError('cannot resize in non-uniformly discretized ' 'axis {}'.format(i)) grid_min, grid_max = discr.grid.min(), discr.grid.max() cell_size = discr.cell_sides new_minpt, new_maxpt = [], [] for axis, (n_orig, n_new, off, on_bdry) in enumerate( zip(discr.shape, newshp, offset, nodes_on_bdry)): if not affected[axis]: new_minpt.append(discr.min_pt[axis]) new_maxpt.append(discr.max_pt[axis]) continue n_diff = n_new - n_orig if off is None: num_r = n_diff // 2 num_l = n_diff - num_r else: num_r = n_diff - off num_l = off try: on_bdry_l, on_bdry_r = on_bdry except TypeError: on_bdry_l = on_bdry on_bdry_r = on_bdry if on_bdry_l: new_minpt.append(grid_min[axis] - num_l * cell_size[axis]) else: new_minpt.append(grid_min[axis] - (num_l + 0.5) * cell_size[axis]) if on_bdry_r: new_maxpt.append(grid_max[axis] + num_r * cell_size[axis]) else: new_maxpt.append(grid_max[axis] + (num_r + 0.5) * cell_size[axis]) fspace = FunctionSpace(IntervalProd(new_minpt, new_maxpt), out_dtype=dtype) tspace = tensor_space(newshp, dtype=dtype, impl=impl, exponent=exponent, weighting=weighting) # Stack together the (unchanged) nonuniform axes and the (new) uniform # axes in the right order part = uniform_partition([], [], ()) for i in range(ndim): if discr.is_uniform_byaxis[i]: part = part.append( uniform_partition(new_minpt[i], new_maxpt[i], newshp[i], nodes_on_bdry=nodes_on_bdry[i])) else: part = part.append(discr.partition.byaxis[i]) return DiscreteLp(fspace, part, tspace, interp=interp)
def parallel_beam_geometry(space, num_angles=None, det_shape=None): """Create default parallel beam geometry from ``space``. This is intended for simple test cases where users do not need the full flexibility of the geometries, but simply want a geometry that works. This default geometry gives a fully sampled sinogram according to the Nyquist criterion, which in general results in a very large number of samples. In particular, a ``space`` that is not centered at the origin can result in very large detectors. Parameters ---------- space : `DiscreteLp` Reconstruction space, the space of the volumetric data to be projected. Needs to be 2d or 3d. num_angles : int, optional Number of angles. Default: Enough to fully sample the data, see Notes. det_shape : int or sequence of int, optional Number of detector pixels. Default: Enough to fully sample the data, see Notes. Returns ------- geometry : `ParallelBeamGeometry` If ``space`` is 2d, return a `Parallel2dGeometry`. If ``space`` is 3d, return a `Parallel3dAxisGeometry`. Examples -------- Create a parallel beam geometry from a 2d space: >>> space = odl.uniform_discr([-1, -1], [1, 1], (20, 20)) >>> geometry = parallel_beam_geometry(space) >>> geometry.angles.size 45 >>> geometry.detector.size 31 Notes ----- According to [NW2001]_, pages 72--74, a function :math:`f : \\mathbb{R}^2 \\to \\mathbb{R}` that has compact support .. math:: \| x \| > \\rho \implies f(x) = 0, and is essentially bandlimited .. math:: \| \\xi \| > \\Omega \implies \\hat{f}(\\xi) \\approx 0, can be fully reconstructed from a parallel beam ray transform if (1) the projection angles are sampled with a spacing of :math:`\\Delta \psi` such that .. math:: \\Delta \psi \leq \\frac{\\pi}{\\rho \\Omega}, and (2) the detector is sampled with an interval :math:`\\Delta s` that satisfies .. math:: \\Delta s \leq \\frac{\\pi}{\\Omega}. The geometry returned by this function satisfies these conditions exactly. If the domain is 3-dimensional, the geometry is "separable", in that each slice along the z-dimension of the data is treated as independed 2d data. References ---------- .. [NW2001] Natterer, F and Wuebbeling, F. *Mathematical Methods in Image Reconstruction*. SIAM, 2001. https://dx.doi.org/10.1137/1.9780898718324 """ # Find maximum distance from rotation axis corners = space.domain.corners()[:, :2] rho = np.max(np.linalg.norm(corners, axis=1)) # Find default values according to Nyquist criterion. # We assume that the function is bandlimited by a wave along the x or y # axis. The highest frequency we can measure is then a standing wave with # period of twice the inter-node distance. min_side = min(space.partition.cell_sides[:2]) omega = np.pi / min_side num_px_horiz = 2 * int(np.ceil(rho * omega / np.pi)) + 1 if space.ndim == 2: det_min_pt = -rho det_max_pt = rho if det_shape is None: det_shape = num_px_horiz elif space.ndim == 3: num_px_vert = space.shape[2] min_h = space.domain.min_pt[2] max_h = space.domain.max_pt[2] det_min_pt = [-rho, min_h] det_max_pt = [rho, max_h] if det_shape is None: det_shape = [num_px_horiz, num_px_vert] if num_angles is None: num_angles = int(np.ceil(omega * rho)) angle_partition = uniform_partition(0, np.pi, num_angles) det_partition = uniform_partition(det_min_pt, det_max_pt, det_shape) if space.ndim == 2: return Parallel2dGeometry(angle_partition, det_partition) elif space.ndim == 3: return Parallel3dAxisGeometry(angle_partition, det_partition) else: raise ValueError('``space.ndim`` must be 2 or 3.')
print("generating adjoint operator tensorflow layer") odl_op_layer_adjoint = odl.contrib.tensorflow.as_tensorflow_layer( operator.adjoint, 'RayTransformAdjoint') partial_op = partial.PartialRay(space, impl='astra_cuda') print("preparing the partial layer") odl_op_partial_layer = partial.tensor_partial_layer(partial_op, 'PartialRayTransform') odl_op_partial_layer_adjoint = partial.tensor_partial_layer( partial_op.adjoint, 'PartialRayTransformAdjoint') # Retrieving the angle projections max_radian = max_degree * (np.pi / 180) print(max_radian) # the selected degree in radians angle_partition = uniform_partition(0, max_radian, n_angles) projections_array = (np.array(angle_partition.grid.coord_vectors)).ravel() selection_array = np.arange( n_angles) # this is the array, only going from 0,1,2,3,...n_angles n_data = 5 n_primal = 5 n_dual = 5 def generate_data(validation=False): """Generate a set of random data.""" n_generate = 1 if validation else n_data y_arr = np.empty( (n_generate, operator.range.shape[0], operator.range.shape[1], 1),
def project_data(data, old_detector, new_detector): """Transforms data one detector to another using linear interpolation. Parameters ---------- data : `numpy.ndarray` Data sampled on a partition of the detector. old_detector : Detector The detector on which the data was sampled. new_detector : Detector The detector to which the data is projected. Returns ------- resampled_data : `numpy.ndarray` Resampled data, which corresponds to a new detector Examples -------- Transforming a flat detector to a circular. In this example a flat detector has a range [-1, 1], with uniform discretization [-1, -0.5, 0, 0.5, 1]. The circular detector has a range [-pi/4, pi/4] with uniform discretization [-pi/4, -pi/8, 0, pi/8, pi/4], which corresponds to [-1, -0.41, 0, 0.41, 1] on the flat detector, since tg(pi/8) approx. 0.41. Thus, values at points -pi/8 and pi/8 obtained through interpolation are -0.83 and 0.83 (approx. 0.41/0.5). >>> part = odl.uniform_partition(-1, 1, 5, nodes_on_bdry=True) >>> det = odl.tomo.Flat1dDetector(part, axis=[1, 0]) >>> det.partition.meshgrid[0] array([-1. , -0.5, 0. , 0.5, 1. ]) >>> data = np.arange(-2, 3) >>> new_det = flat_to_curved(det, radius=1) >>> new_data = project_data(data, det, new_det) >>> np.round(new_data, 2) array([-2. , -0.83, 0. , 0.83, 2. ]) Transforming a circular detector to a flat. In this example a circular detector has a range [-pi/4, pi/4] with uniform discretization [-0.79, -0.39, 0, 0.39, 0.79]. The corresponding flat detector has uniform discretization [-1, -0.5, 0, 0.5, 1], which corresponds to points [-0.79, -0.46, 0, 0.46, 0.79] on the circular detector, since arctg(0.5) = 0.46. Thus, values at points -0.5 and 0.5 obtained through interpolation are -1.18 and 1.18 (approx. (0.79-0.46)/0.39*1 + (0.46-0.39)/0.39*2). >>> part = odl.uniform_partition(-np.pi / 4, np.pi / 4, 5, ... nodes_on_bdry=True) >>> det = odl.tomo.CircularDetector(part, axis=[1, 0], radius=1) >>> data = np.arange(-2, 3) >>> new_det = curved_to_flat(det) >>> new_data = project_data(data, det, new_det) >>> np.round(new_data, 2) array([-2. , -1.18, 0. , 1.18, 2. ]) Previous example extended to 2D cylindrical detector with height 2 and uniform partition along height axis [-1, -0.5, 0, 0.5, 1]. The height partition of corresponding 2D flat detector is [-1.41, -0.71, 0., 0.71, 1.41]. We can see that points that are closer to side edges of the cylinder are are projected higher on the flat detector. >>> part = odl.uniform_partition([-np.pi / 4, -1], [np.pi / 4, 1], (5, 5), ... nodes_on_bdry=True) >>> det = odl.tomo.CylindricalDetector(part, ... axes=[[1, 0, 0], [0, 0, 1]], ... radius=1) >>> data = np.zeros((5,5)) >>> data[:, 1:4] = 1 >>> new_det = curved_to_flat(det) >>> np.round(new_det.partition.meshgrid[1], 2) array([[-1.41, -0.71, 0. , 0.71, 1.41]]) >>> new_data = project_data(data, det, new_det) >>> np.round(new_data.T, 2) array([[ 0. , 0. , 0. , 0. , 0. ], [ 1. , 0.74, 0.59, 0.74, 1. ], [ 1. , 1. , 1. , 1. , 1. ], [ 1. , 0.74, 0.59, 0.74, 1. ], [ 0. , 0. , 0. , 0. , 0. ]]) Now projecting this back to curved detector. >>> new_data = project_data(new_data, new_det, det) >>> np.round(new_data.T, 2) array([[ 0. , 0.33, 0.34, 0.33, 0. ], [ 1. , 0.78, 0.71, 0.78, 1. ], [ 1. , 1. , 1. , 1. , 1. ], [ 1. , 0.78, 0.71, 0.78, 1. ], [ 0. , 0.33, 0.34, 0.33, 0. ]]) The method is vectorized, i.e., it can be called for multiple observations of values on the detector (most often corresponding to different angles): >>> part = odl.uniform_partition(-1, 1, 3, nodes_on_bdry=True) >>> det = odl.tomo.Flat1dDetector(part, axis=[1, 0]) >>> data_row = np.arange(-1, 2) >>> data = np.stack([data_row] * 2, axis=0) >>> new_det = flat_to_curved(det, 1) >>> new_data = project_data(data, det, new_det) >>> np.round(new_data, 2) array([[-1., 0., 1.], [-1., 0., 1.]]) """ assert isinstance(old_detector, Detector) assert isinstance(new_detector, Detector) part = old_detector.partition d = len(part.shape) if d == 1 and any(old_detector.axis != new_detector.axis): NotImplementedError('Detectors are axis not the same, {} and {}' ''.format(old_detector.axis, new_detector.axis)) elif d > 1 and (any(old_detector.axes[0] != new_detector.axes[0]) or any(old_detector.axes[1] != new_detector.axes[1])): NotImplementedError('Detectors are axis not the same, {} and {}' ''.format(old_detector.axes, new_detector.axes)) data = np.asarray(data, dtype=float) if data.shape[-d:] != part.shape: raise ValueError('Last dimensions of `data.shape` must ' 'correspond to the detector partitioning, ' 'got {} and {}'.format(data.shape[-d:], part.shape)) # find out if there are multiple data points if d < len(data.shape): n = data.shape[0] else: n = 1 if n > 1: # extend detectors partition for multiple samples data_part = uniform_partition(np.append([0], part.min_pt), np.append([n], part.max_pt), np.append([n], part.shape), nodes_on_bdry=part.nodes_on_bdry) else: data_part = part if isinstance(old_detector, Flat1dDetector): assert isinstance(new_detector, CircularDetector) phi = new_detector.partition.meshgrid[0] u = new_detector.radius * np.tan(phi) interpolator = linear_interpolator(data, data_part.coord_vectors) if n > 1: i = data_part.meshgrid[0] u = u.reshape(1, -1) resampled_data = interpolator((i, u)) else: resampled_data = interpolator(u) elif isinstance(old_detector, CircularDetector): assert isinstance(new_detector, Flat1dDetector) u = new_detector.partition.meshgrid phi = np.arctan2(u, old_detector.radius) interpolator = linear_interpolator(data, data_part.coord_vectors) if n > 1: i = data_part.meshgrid[0] phi = phi.reshape(-1, 1) resampled_data = interpolator((i, phi)) else: resampled_data = interpolator(phi) elif isinstance(old_detector, Flat2dDetector): assert isinstance(new_detector, (CylindricalDetector, SphericalDetector)) r = new_detector.radius if isinstance(new_detector, CylindricalDetector): phi, h = new_detector.partition.meshgrid u = r * np.tan(phi) v = h / r * np.sqrt(r * r + u * u) else: phi, theta = new_detector.partition.meshgrid u = r * np.tan(phi) v = np.tan(theta) * np.sqrt(r * r + u * u) interpolator = linear_interpolator(data, data_part.coord_vectors) if n > 1: i = data_part.meshgrid[0] u = np.expand_dims(u, axis=0) v = np.expand_dims(v, axis=0) resampled_data = interpolator((i, u, v)) else: u = np.repeat(u, v.shape[1], axis=-1) coord = np.stack([u, v], axis=-1).reshape((-1, 2)) resampled_data = interpolator(coord.T).reshape( new_detector.partition.shape) elif isinstance(old_detector, CylindricalDetector): assert isinstance(new_detector, (Flat2dDetector, SphericalDetector)) r = old_detector.radius if isinstance(new_detector, Flat2dDetector): u, v = new_detector.partition.meshgrid phi = np.arctan2(u, r) h = v * r / np.sqrt(r * r + u * u) else: phi, theta = new_detector.partition.meshgrid h = r * np.tan(theta) interpolator = linear_interpolator(data, data_part.coord_vectors) if n > 1: i = data_part.meshgrid[0] phi = np.expand_dims(phi, axis=0) h = np.expand_dims(h, axis=0) resampled_data = interpolator((i, phi, h)) else: phi = np.repeat(phi, h.shape[1], axis=-1) if h.shape[0] != phi.shape[0]: h = np.repeat(h, phi.shape[0], axis=0) coord = np.stack([phi, h], axis=-1).reshape((-1, 2)) resampled_data = interpolator(coord.T).reshape( new_detector.partition.shape) elif isinstance(old_detector, SphericalDetector): assert isinstance(new_detector, (Flat2dDetector, CylindricalDetector)) r = old_detector.radius if isinstance(new_detector, Flat2dDetector): u, v = new_detector.partition.meshgrid phi = np.arctan2(u, r) theta = np.arctan2(v, np.sqrt(r * r + u * u)) else: phi, h = new_detector.partition.meshgrid theta = np.arctan2(h, r) interpolator = linear_interpolator(data, data_part.coord_vectors) if n > 1: i = data_part.meshgrid[0] phi = np.expand_dims(phi, axis=0) theta = np.expand_dims(theta, axis=0) resampled_data = interpolator((i, phi, theta)) else: phi = np.repeat(phi, theta.shape[1], axis=-1) if theta.shape[0] != phi.shape[0]: theta = np.repeat(theta, phi.shape[0], axis=0) coord = np.stack([phi, theta], axis=-1).reshape((-1, 2)) resampled_data = interpolator(coord.T).reshape( new_detector.partition.shape) else: NotImplementedError('Data transformation between detectors {} and {}' 'is not implemented'.format( old_detector, new_detector)) return resampled_data
def flat_to_curved(detector, radius): """Transforms a flat detector to a curved. Parameters ---------- detector : `Flat1dDetector` or `Flat2dDetector` The detector on which the data was sampled. radius : nonnegatice float or 2-tuple of nonnegative floats Radius or radii of the detector curvature. If ``r`` a circular or cylindrical detector is created. If ``(r, None)`` or ``(r, float('inf'))``, a cylindrical detector is created. If ``(r1, r2)``, a spherical detector is created. Returns ------- transformed_detector : `CircularDetector`, `CylindricalDetector` or `SphericalDetector` A new transformed detector. Examples -------- Transforming a flat detector with range [-1, 1] to a circular with radius 1 and range [-pi/4, pi/4]. >>> part = odl.uniform_partition(-1, 1, 3) >>> det = odl.tomo.Flat1dDetector(part, axis=[1, 0]) >>> new_det = flat_to_curved(det, radius=1) >>> new_det CircularDetector( uniform_partition(-0.7854, 0.7854, 3), axis='[ 1., 0.]', radius='1.0' ) Transforming a flat detector with height 2 to a cylindrical with radius 1 and height 2. >>> part = odl.uniform_partition([-1, -1], [1, 1], (3, 2)) >>> det = odl.tomo.Flat2dDetector(part, axes=[[1, 0, 0], [0, 0, 1]]) >>> new_det = flat_to_curved(det, radius=1) >>> new_det CylindricalDetector( uniform_partition([-0.7854, -1. ], [ 0.7854, 1. ], (3, 2)), axes=('[ 1., 0., 0.]', '[ 0., 0., 1.]'), radius='1.0' ) """ part = detector.partition radius = np.array(radius, dtype=np.float, ndmin=1) if isinstance(detector, Flat1dDetector): if radius <= 0 or radius.size > 1: raise ValueError('`radius` must be positive float') min_pt = np.arctan2(part.min_pt, radius) max_pt = np.arctan2(part.max_pt, radius) transformed_part = uniform_partition(min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = CircularDetector(transformed_part, detector.axis, radius, check_bounds=detector.check_bounds) elif isinstance(detector, Flat2dDetector): if radius.size == 1 or radius[1] is None or radius[1] == float('inf'): if radius[0] <= 0: raise ValueError('`radius` must be positive float') min_pt = [np.arctan2(part.min_pt[0], radius[0]), part.min_pt[1]] max_pt = [np.arctan2(part.max_pt[0], radius[0]), part.max_pt[1]] transformed_part = uniform_partition( min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = CylindricalDetector( transformed_part, radius=radius[0], axes=detector.axes, check_bounds=detector.check_bounds) elif radius[0] == radius[1]: if radius[0] <= 0: raise ValueError('`radius` must be positive float') min_pt = np.arctan2(part.min_pt, radius) max_pt = np.arctan2(part.max_pt, radius) transformed_part = uniform_partition( min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = SphericalDetector( transformed_part, radius=radius[0], axes=detector.axes, check_bounds=detector.check_bounds) else: raise NotImplementedError('Curved detector with different ' 'curvature radii') else: raise ValueError('Detector must be flat, got '.format(detector)) return transformed_det
def curved_to_flat(detector): """Transforms a curved detector to a flat. Parameters ---------- detector : `CircularDetector`, `CylindricalDetector` or `SphericalDetector` The detector on which the data was sampled. Returns ------- transformed_detector : `Flat1dDetector` or `Flat2dDetector` A new transformed detector. Examples -------- Transforming a circular detector with range [-pi/4, pi/4] to a flat with range [-1, 1]. >>> part = odl.uniform_partition(-np.pi / 4, np.pi / 4, 3, ... nodes_on_bdry=True) >>> det = odl.tomo.CircularDetector(part, axis=[1, 0], radius=1) >>> new_det = curved_to_flat(det) >>> new_det Flat1dDetector( uniform_partition(-1.0, 1.0, 3, nodes_on_bdry=True), axis='[ 1., 0.]' ) Transforming a cylindrical detector with radius 1 and height 2 to a flat with height 2sqrt(2) (since edge point are projected higher). >>> part = odl.uniform_partition([-np.pi / 4, -1], [np.pi / 4, 1], (3, 2), ... nodes_on_bdry=True) >>> det = odl.tomo.CylindricalDetector(part, ... axes=[[1, 0, 0], [0, 0, 1]], ... radius=1) >>> new_det = curved_to_flat(det) >>> new_det Flat2dDetector( uniform_partition([-1. , -1.4142], [ 1. , 1.4142], (3, 2), nodes_on_bdry=True), axes=('[ 1., 0., 0.]', '[ 0., 0., 1.]') ) """ part = detector.partition if isinstance(detector, CircularDetector): min_pt = detector.radius * np.tan(part.min_pt) max_pt = detector.radius * np.tan(part.max_pt) transformed_part = uniform_partition(min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = Flat1dDetector(transformed_part, detector.axis, check_bounds=detector.check_bounds) elif isinstance(detector, CylindricalDetector): r = detector.radius R = r / np.minimum(np.cos(part.min_pt[0]), np.cos(part.max_pt[0])) min_pt = [r * np.tan(part.min_pt[0]), R / r * part.min_pt[1]] max_pt = [r * np.tan(part.max_pt[0]), R / r * part.max_pt[1]] transformed_part = uniform_partition(min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = Flat2dDetector(transformed_part, detector.axes, check_bounds=detector.check_bounds) elif isinstance(detector, SphericalDetector): r = detector.radius R = r / np.minimum(np.cos(part.min_pt[0]), np.cos(part.max_pt[0])) min_pt = [r * np.tan(part.min_pt[0]), R * np.tan(part.min_pt[1])] max_pt = [r * np.tan(part.max_pt[0]), R * np.tan(part.max_pt[1])] transformed_part = uniform_partition(min_pt, max_pt, part.shape, nodes_on_bdry=part.nodes_on_bdry) transformed_det = Flat2dDetector(transformed_part, detector.axes, check_bounds=detector.check_bounds) else: raise ValueError('Detector must be curved, got '.format(detector)) return transformed_det
# Create tensorflow layer from odl operator print("\ngenerating operator tensorflow layer") odl_op_layer = odl.contrib.tensorflow.as_tensorflow_layer(operator, 'RayTransform') print("generating adjoint operator tensorflow layer") odl_op_layer_adjoint = odl.contrib.tensorflow.as_tensorflow_layer(operator.adjoint, 'RayTransformAdjoint') partial_op = partial.PartialRay(space, impl=astra_impl) print("preparing the partial layer") odl_op_partial_layer = partial.tensor_partial_layer(partial_op, 'PartialRayTransform') odl_op_partial_layer_adjoint = partial.tensor_partial_layer(partial_op.adjoint, 'PartialRayTransformAdjoint') # Retrieving the angle projections angle_partition = uniform_partition(0, np.pi, n_angles) projections_array = (np.array(angle_partition.grid.coord_vectors)).ravel() selection_array = np.arange(n_angles) n_data = 5 n_iter = 2 n_primal = 5 n_dual = 5 def generate_data(validation=False): """Generate a set of random data.""" n_generate = 1 if validation else n_data y_arr = np.empty((n_generate, operator.range.shape[0], operator.range.shape[1], 1), dtype='float64') x_true_arr = np.empty((n_generate, space.shape[0], space.shape[1], 1), dtype='float64')
def cone_beam_geometry(space, src_radius, det_radius, num_angles=None, short_scan=False, det_shape=None): """Create a default fan or cone beam geometry from ``space``. This function is intended for simple test cases where users do not need the full flexibility of the geometries, but simply wants a geometry that works. The geometry returned by this function has equidistant angles that lie (strictly) between 0 and either ``2 * pi`` (full scan) or ``pi + fan_angle`` (short scan). The detector is centered around 0, and its size is chosen such that the whole ``space`` is covered with lines. The number of angles and detector elements is chosen such that the resulting sinogram is fully sampled according to the Nyquist criterion, which in general results in a very large number of samples. In particular, a ``space`` that is not centered at the origin can result in very large detectors since the latter is always origin-centered. Parameters ---------- space : `DiscreteLp` Reconstruction space, the space of the volumetric data to be projected. Must be 2- or 3-dimensional. src_radius : nonnegative float Radius of the source circle. Must be larger than the radius of the smallest vertical cylinder containing ``space.domain``, i.e., the source must be outside the volume for all rotations. det_radius : nonnegative float Radius of the detector circle. short_scan : bool, optional Use the minimum required angular range ``[0, pi + fan_angle]``. For ``True``, the `parker_weighting` should be used in FBP. By default, the range ``[0, 2 * pi]`` is used. num_angles : int, optional Number of angles. Default: Enough to fully sample the data, see Notes. det_shape : int or sequence of ints, optional Number of detector pixels. Default: Enough to fully sample the data, see Notes. Returns ------- geometry : `DivergentBeamGeometry` Projection geometry with equidistant angles and zero-centered detector as determined by sampling criteria. - If ``space`` is 2D, the result is a `FanFlatGeometry`. - If ``space`` is 3D, the result is a `ConeFlatGeometry`. Examples -------- Create a fan beam geometry from a 2d space: >>> space = odl.uniform_discr([-1, -1], [1, 1], (20, 20)) >>> geometry = cone_beam_geometry(space, src_radius=5, det_radius=5) >>> geometry.angles.size 78 >>> geometry.detector.size 57 For a short scan geometry (from 0 to ``pi + fan_angle``), the ``short_scan`` flag can be set, resulting in a smaller number of angles: >>> geometry = cone_beam_geometry(space, src_radius=5, det_radius=5, ... short_scan=True) >>> geometry.angles.size 46 If the source is close to the object, the detector becomes larger due to more magnification: >>> geometry = cone_beam_geometry(space, src_radius=3, det_radius=9) >>> geometry.angles.size 80 >>> geometry.detector.size 105 Notes ----- According to [NW2001]_, pages 75--76, a function :math:`f : \\mathbb{R}^2 \\to \\mathbb{R}` that has compact support .. math:: \| x \| > \\rho \implies f(x) = 0, and is essentially bandlimited .. math:: \| \\xi \| > \\Omega \implies \\hat{f}(\\xi) \\approx 0, can be fully reconstructed from a fan beam ray transform with source-detector distance :math:`r` (assuming all detector points have the same distance to the source) if (1) the projection angles are sampled with a spacing of :math:`\\Delta \psi` such that .. math:: \\Delta \psi \leq \\frac{r + \\rho}{r}\, \\frac{\\pi}{\\rho \\Omega}, and (2) the detector is sampled with an angular interval :math:`\\Delta \\alpha` that satisfies .. math:: \\Delta \\alpha \leq \\frac{\\pi}{r \\Omega}. For a flat detector, the angular interval is smallest in the center of the fan and largest at the boundaries. The worst-case relation between the linear and angular sampling intervals are .. math:: \\Delta s = R \\Delta \\alpha, \quad R^2 = r^2 + (w / 2)^2, where :math:`w` is the width of the detector. Thus, to satisfy the angular detector condition one can choose .. math:: \\Delta s \leq \\frac{\\pi \sqrt{r^2 + (w / 2)^2}}{r \\Omega}. The geometry returned by this function satisfies these conditions exactly. If the domain is 3-dimensional, a circular cone beam geometry is created with the third coordinate axis as rotation axis. This does, of course, not yield complete data, but is equivalent to the 2D fan beam case in the :math:`z = 0` slice. The vertical size of the detector is chosen such that it covers the object vertically with rays, using a containing cuboid :math:`[-\\rho, \\rho]^2 \\times [z_{\mathrm{min}}, z_{\mathrm{min}}]` to compute the cone angle. References ---------- .. [NW2001] Natterer, F and Wuebbeling, F. *Mathematical Methods in Image Reconstruction*. SIAM, 2001. https://dx.doi.org/10.1137/1.9780898718324 """ # Find maximum distance from rotation axis corners = space.domain.corners()[:, :2] rho = np.max(np.linalg.norm(corners, axis=1)) # Find default values according to Nyquist criterion. # We assume that the function is bandlimited by a wave along the x or y # axis. The highest frequency we can measure is then a standing wave with # period of twice the inter-node distance. min_side = min(space.partition.cell_sides[:2]) omega = np.pi / min_side # Compute minimum width of the detector to cover the object. The relation # used here is (w/2)/(rs+rd) = rho/rs since both are equal to tan(alpha), # where alpha is the half fan angle. rs = float(src_radius) if (rs <= rho): raise ValueError('source too close to the object, resulting in ' 'infinite detector for full coverage') rd = float(det_radius) r = src_radius + det_radius w = 2 * rho * (rs + rd) / rs # Compute minimum number of pixels given the constraint on the # sampling interval and the computed width rb = np.hypot(r, w / 2) # length of the boundary ray to the flat detector num_px_horiz = 2 * int(np.ceil(w * omega * r / (2 * np.pi * rb))) + 1 if space.ndim == 2: det_min_pt = -w / 2 det_max_pt = w / 2 if det_shape is None: det_shape = num_px_horiz elif space.ndim == 3: # Compute number of vertical pixels required to cover the object, # using the same sampling interval vertically as horizontally. # The reasoning is the same as for the computation of w. # Minimum distance of the containing cuboid edges to the source dist = rs - rho # Take angle of the rays going through the top and bottom corners # in that edge half_cone_angle = max(np.arctan(abs(space.partition.min_pt[2]) / dist), np.arctan(abs(space.partition.max_pt[2]) / dist)) h = 2 * np.sin(half_cone_angle) * (rs + rd) # Use the vertical spacing from the reco space, corrected for # magnification at the "back" of the object, i.e., where it is # minimal min_mag = (rs + rd) / (rs + rho) delta_h = min_mag * space.cell_sides[2] num_px_vert = int(np.ceil(h / delta_h)) h = num_px_vert * delta_h # make multiple of spacing det_min_pt = [-w / 2, -h / 2] det_max_pt = [w / 2, h / 2] if det_shape is None: det_shape = [num_px_horiz, num_px_vert] fan_angle = 2 * np.arctan(rho / rs) if short_scan: max_angle = min(np.pi + fan_angle, 2 * np.pi) else: max_angle = 2 * np.pi if num_angles is None: num_angles = int( np.ceil(max_angle * omega * rho / np.pi * r / (r + rho))) angle_partition = uniform_partition(0, max_angle, num_angles) det_partition = uniform_partition(det_min_pt, det_max_pt, det_shape) if space.ndim == 2: return FanFlatGeometry(angle_partition, det_partition, src_radius, det_radius) elif space.ndim == 3: return ConeFlatGeometry(angle_partition, det_partition, src_radius, det_radius) else: raise ValueError('``space.ndim`` must be 2 or 3.')
def parallel_beam_geometry(space, angles=None, det_shape=None): """Create default parallel beam geometry from ``space``. This is intended for simple test cases where users do not need the full flexibility of the geometries, but simply want a geometry that works. This default geometry gives a fully sampled sinogram according to the Nyquist criterion, which in general results in a very large number of samples. Parameters ---------- space : `DiscreteLp` Reconstruction space, the space of the volumetric data to be projected. Needs to be 2d or 3d. angles : int, optional Number of angles. Default: Enough to fully sample the data, see Notes. det_shape : int or sequence of int, optional Number of detector pixels. Default: Enough to fully sample the data, see Notes. Returns ------- geometry : `ParallelGeometry` If ``space`` is 2d, returns a `Parallel2dGeometry`. If ``space`` is 3d, returns a `Parallel3dAxisGeometry`. Examples -------- Create geometry from 2d space and check the number of data points: >>> space = odl.uniform_discr([-1, -1], [1, 1], [20, 20]) >>> geometry = parallel_beam_geometry(space) >>> geometry.angles.size 45 >>> geometry.detector.size 29 Notes ----- According to `Mathematical Methods in Image Reconstruction`_ (page 72), for a function :math:`f : \\mathbb{R}^2 \\to \\mathbb{R}` that has compact support .. math:: \| x \| > \\rho \implies f(x) = 0, and is essentially bandlimited .. math:: \| \\xi \| > \\Omega \implies \\hat{f}(\\xi) \\approx 0, then, in order to fully reconstruct the function from a parallel beam ray transform the function should be sampled at an angular interval :math:`\\Delta \psi` such that .. math:: \\Delta \psi \leq \\frac{\\pi}{\\rho \\Omega}, and the detector should be sampled with an interval :math:`Delta s` that satisfies .. math:: \\Delta s \leq \\frac{\\pi}{\\Omega}. The geometry returned by this function satisfies these conditions exactly. If the domain is 3-dimensional, the geometry is "separable", in that each slice along the z-dimension of the data is treated as independed 2d data. References ---------- .. _Mathematical Methods in Image Reconstruction: \ http://dx.doi.org/10.1137/1.9780898718324 """ # Find maximum distance from rotation axis corners = space.domain.corners()[:, :2] rho = np.max(np.linalg.norm(corners, axis=1)) # Find default values according to Nyquist criterion. # We assume that the function is bandlimited by a wave along the x or y # axis. The highest frequency we can measure is then a standing wave with # period of twice the inter-node distance. min_side = min(space.partition.cell_sides[:2]) omega = np.pi / min_side if det_shape is None: if space.ndim == 2: det_shape = int(2 * rho * omega / np.pi) + 1 elif space.ndim == 3: width = int(2 * rho * omega / np.pi) + 1 height = space.shape[2] det_shape = [width, height] if angles is None: angles = int(omega * rho) + 1 # Define angles angle_partition = uniform_partition(0, np.pi, angles) if space.ndim == 2: det_partition = uniform_partition(-rho, rho, det_shape) return Parallel2dGeometry(angle_partition, det_partition) elif space.ndim == 3: min_h = space.domain.min_pt[2] max_h = space.domain.max_pt[2] det_partition = uniform_partition([-rho, min_h], [rho, max_h], det_shape) return Parallel3dAxisGeometry(angle_partition, det_partition) else: raise ValueError('``space.ndim`` must be 2 or 3.')
from odl.discr import uniform_partition import xlsxwriter np.random.seed(0) name = 'learned_random_sub' sess = tf.InteractiveSession() astra_impl = 'astra_cuda' optimal_number = 202 # number optimal projection points n_angles = 60 # number of projection angles selection_projection = np.arange( optimal_number) # going from [0,1,2,3, ... , optinumber] opti_partition = uniform_partition( 0, np.pi, optimal_number) # uniform partition from the optimal number of projections opti_projection_points = (np.array( opti_partition.grid.coord_vectors)).ravel() # changing into an array print("defining user selected parameters") # User selected paramters angles_in_batch = 10 if (n_angles % angles_in_batch != 0): print( "number of angles must be divisible by the number of angles in the batch" ) exit() else: n_batches = int(n_angles / angles_in_batch)