def test_linear_interpolation_2d(): """Test linear interpolation in 2d.""" coord_vecs = [[0.125, 0.375, 0.625, 0.875], [0.25, 0.75]] f = np.array([[1, 2], [3, 4], [5, 6], [7, 8]], dtype='float64') interpolator = linear_interpolator(f, coord_vecs) # Evaluate at single point val = interpolator([0.3, 0.6]) l1 = (0.3 - 0.125) / (0.375 - 0.125) l2 = (0.6 - 0.25) / (0.75 - 0.25) true_val = ((1 - l1) * (1 - l2) * f[0, 0] + (1 - l1) * l2 * f[0, 1] + l1 * (1 - l2) * f[1, 0] + l1 * l2 * f[1, 1]) assert val == pytest.approx(true_val) # Input array, with and without output array pts = np.array([[0.3, 0.6], [0.1, 0.25], [1.0, 1.0]]) l1 = (0.3 - 0.125) / (0.375 - 0.125) l2 = (0.6 - 0.25) / (0.75 - 0.25) true_val_1 = ((1 - l1) * (1 - l2) * f[0, 0] + (1 - l1) * l2 * f[0, 1] + l1 * (1 - l2) * f[1, 0] + l1 * l2 * f[1, 1]) l1 = (0.125 - 0.1) / (0.375 - 0.125) # l2 = 0 true_val_2 = (1 - l1) * f[0, 0] # only lower left contributes l1 = (1.0 - 0.875) / (0.875 - 0.625) l2 = (1.0 - 0.75) / (0.75 - 0.25) true_val_3 = (1 - l1) * (1 - l2) * f[3, 1] # lower left only true_arr = [true_val_1, true_val_2, true_val_3] assert all_equal(interpolator(pts.T), true_arr) out = np.empty(3, dtype='float64') interpolator(pts.T, out=out) assert all_equal(out, true_arr) # Input meshgrid, with and without output array mg = sparse_meshgrid([0.3, 1.0], [0.4, 0.75]) # Indices: (1, 3) x (0, 1) lx1 = (0.3 - 0.125) / (0.375 - 0.125) lx2 = (1.0 - 0.875) / (0.875 - 0.625) ly1 = (0.4 - 0.25) / (0.75 - 0.25) # ly2 = 0 true_val_11 = ((1 - lx1) * (1 - ly1) * f[0, 0] + (1 - lx1) * ly1 * f[0, 1] + lx1 * (1 - ly1) * f[1, 0] + lx1 * ly1 * f[1, 1]) true_val_12 = ((1 - lx1) * f[0, 1] + lx1 * f[1, 1] # ly2 = 0 ) true_val_21 = ((1 - lx2) * (1 - ly1) * f[3, 0] + (1 - lx2) * ly1 * f[3, 1] # high node 1.0, no upper ) true_val_22 = (1 - lx2) * f[3, 1] # ly2 = 0, no upper for 1.0 true_mg = [[true_val_11, true_val_12], [true_val_21, true_val_22]] assert all_equal(interpolator(mg), true_mg) out = np.empty((2, 2), dtype='float64') interpolator(mg, out=out) assert all_equal(out, true_mg)
def test_linear_interpolation_1d(): """Test linear interpolation in 1d.""" coord_vecs = [[0.1, 0.3, 0.5, 0.7, 0.9]] f = np.array([1, 2, 3, 4, 5], dtype="float64") interpolator = linear_interpolator(f, coord_vecs) # Evaluate at single point val = interpolator(0.35) true_val = 0.75 * 2 + 0.25 * 3 assert val == pytest.approx(true_val) # Input array, with and without output array pts = np.array([0.4, 0.0, 0.65, 0.95]) true_arr = [2.5, 0.5, 3.75, 3.75] assert all_almost_equal(interpolator(pts), true_arr)
def test_collocation_interpolation_identity(): """Check if collocation is left-inverse to interpolation.""" # Interpolation followed by collocation on the same grid should be # the identity coord_vecs = [[0.125, 0.375, 0.625, 0.875], [0.25, 0.75]] f = np.array([[1, 2], [3, 4], [5, 6], [7, 8]], dtype='float64') interpolators = [ nearest_interpolator(f, coord_vecs), linear_interpolator(f, coord_vecs), per_axis_interpolator(f, coord_vecs, interp=['linear', 'nearest']), ] for interpolator in interpolators: mg = sparse_meshgrid(*coord_vecs) ident_f = point_collocation(interpolator, mg) assert all_almost_equal(ident_f, f)
def interpolator(x, out=None): x = (x[0], np.clip(x[1], min_x, max_x)) interpolator = linear_interpolator( sinogram, skimage_range.grid.coord_vectors ) return interpolator(x, out=out)
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 : ConeBeamGeometry 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 definition 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 definitions 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.ConeBeamGeometry(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 interpolator = linear_interpolator(data_array, ray_trafo.range.coord_vectors) proj_data = interpolator((theta, u, v)) return geometry, proj_data.asarray()
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