Example #1
0
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)
Example #2
0
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)
Example #3
0
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)
Example #4
0
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
Example #5
0
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
Example #6
0
    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]))
Example #7
0
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')
Example #8
0
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
Example #9
0
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')
Example #11
0
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
Example #12
0
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
Example #13
0
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')
Example #14
0
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()
Example #15
0
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')
Example #16
0
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
Example #17
0
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()