Ejemplo n.º 1
0
    def test_fp_with_Astra(self):
        AOp = AstraProjectionOperator(self.ig, self.ag)
        fp_ASTRA = AOp.direct(self.golden_data_cs)

        TOp = ProjectionOperator(self.ig, self.ag)
        fp_TIGRE = TOp.direct(self.golden_data_cs)

        mean_diff = (fp_ASTRA - fp_TIGRE).abs().mean()
        self.assertLess(mean_diff, 1e-2)
        np.testing.assert_allclose(fp_TIGRE.as_array(),
                                   fp_ASTRA.as_array(),
                                   atol=1)

        astra_ag3D = self.ag3D.copy()
        astra_ag3D.set_labels(['vertical', 'angle', 'horizontal'])

        AOp = AstraProjectionOperator(self.ig3D, astra_ag3D)
        fp_ASTRA = AOp.direct(self.golden_data)

        fp_ASTRA.reorder(['angle', 'vertical', 'horizontal'])
        mean_diff = (fp_ASTRA - self.fp).abs().mean()
        self.assertLess(mean_diff, 1)
        np.testing.assert_allclose(self.fp.as_array(),
                                   fp_ASTRA.as_array(),
                                   atol=5)
Ejemplo n.º 2
0
    def run(self, out=None, verbose=1):
        """
        Runs the configured FDK recon and returns the reconstruction

        Parameters
        ----------
        out : ImageData, optional
           Fills the referenced ImageData with the reconstructed volume and suppresses the return
        verbose : int, default=1
           Contols the verbosity of the reconstructor. 0: No output is logged, 1: Full configuration is logged

        Returns
        -------
        ImageData
            The reconstructed volume. Suppressed if `out` is passed
        """

        if verbose:
            print(self)

        if self.filter_inplace is False:
            proj_filtered = self.input.copy()
        else:
            proj_filtered = self.input

        self._pre_filtering(proj_filtered)
        operator = ProjectionOperator(self.image_geometry,
                                      self.acquisition_geometry,
                                      adjoint_weights='FDK')

        if out is None:
            return operator.adjoint(proj_filtered)
        else:
            operator.adjoint(proj_filtered, out=out)
Ejemplo n.º 3
0
    def test_backward_results(self):
        #create checker-board projection
        checker = np.zeros((16, 16))
        ones = np.ones((4, 4))
        for j in range(4):
            for i in range(4):
                if (i + j) % 2 == 0:
                    checker[j * 4:(j + 1) * 4, i * 4:(i + 1) * 4] = ones

        #create backprojection of checker-board
        res = np.zeros((16, 16, 16))
        ones = np.ones((4, 16, 4))
        for k in range(4):
            for i in range(4):
                if (i + k) % 2 == 0:
                    res[k * 4:(k + 1) * 4, :, i * 4:(i + 1) * 4] = ones

        if self.ag_small.dimension == '2D':
            checker = checker[0]
            res = res[0]

        ig = self.ag_small.get_ImageGeometry()
        data = self.ag_small.allocate(0)

        data.fill(checker)

        Op = ProjectionOperator(ig, self.ag_small)
        bp = Op.adjoint(data)

        if self.ag_small.geom_type == 'cone':
            #as cone beam res is not perfect grid
            bp.array = np.round(bp.array, 0)

        np.testing.assert_equal(bp.array, res)
Ejemplo n.º 4
0
def has_gpu_tigre():

    if not has_tigre:
        return False

    has_gpu = True
    if has_nvidia_smi():
        from cil.plugins.tigre import ProjectionOperator
        from tigre.utilities.errors import TigreCudaCallError

        N = 3
        angles = np.linspace(0, np.pi, 2, dtype='float32')

        ag = AcquisitionGeometry.create_Cone2D([0,-100],[0,200])\
                                .set_angles(angles, angle_unit='radian')\
                                .set_panel(N, 0.1)\
                                .set_labels(['angle', 'horizontal'])

        ig = ag.get_ImageGeometry()

        data = ig.allocate(1)

        Op = ProjectionOperator(ig, ag)

        try:
            Op.direct(data)
            has_gpu = True
        except TigreCudaCallError:
            has_gpu = False
    else:
        has_gpu = False

    print("has_gpu_tigre\t{}".format(has_gpu))
    return has_gpu
Ejemplo n.º 5
0
    def setUp(self):
        #%% Setup Geometry
        voxel_num_xy = 255
        voxel_num_z = 15
        cs_ind = (voxel_num_z - 1) // 2

        mag = 2
        src_to_obj = 50
        src_to_det = src_to_obj * mag

        pix_size = 0.2
        det_pix_x = voxel_num_xy
        det_pix_y = voxel_num_z

        num_projections = 1000
        angles = np.linspace(0, 360, num=num_projections, endpoint=False)

        self.ag = AcquisitionGeometry.create_Cone2D([0,-src_to_obj],[0,src_to_det-src_to_obj])\
                                           .set_angles(angles)\
                                           .set_panel(det_pix_x, pix_size)\
                                           .set_labels(['angle','horizontal'])

        self.ig = self.ag.get_ImageGeometry()

        self.ag3D = AcquisitionGeometry.create_Cone3D([0,-src_to_obj,0],[0,src_to_det-src_to_obj,0])\
                                     .set_angles(angles)\
                                     .set_panel((det_pix_x,det_pix_y), (pix_size,pix_size))\
                                     .set_labels(['angle','vertical','horizontal'])
        self.ig3D = self.ag3D.get_ImageGeometry()

        #%% Create phantom
        kernel_size = voxel_num_xy
        kernel_radius = (kernel_size - 1) // 2
        y, x = np.ogrid[-kernel_radius:kernel_radius + 1,
                        -kernel_radius:kernel_radius + 1]

        circle1 = [5, 0, 0]  #r,x,y
        dist1 = ((x - circle1[1])**2 + (y - circle1[2])**2)**0.5

        circle2 = [5, 80, 0]  #r,x,y
        dist2 = ((x - circle2[1])**2 + (y - circle2[2])**2)**0.5

        circle3 = [25, 0, 80]  #r,x,y
        dist3 = ((x - circle3[1])**2 + (y - circle3[2])**2)**0.5

        mask1 = (dist1 - circle1[0]).clip(0, 1)
        mask2 = (dist2 - circle2[0]).clip(0, 1)
        mask3 = (dist3 - circle3[0]).clip(0, 1)
        phantom = 1 - np.logical_and(np.logical_and(mask1, mask2), mask3)

        self.golden_data = self.ig3D.allocate(0)
        for i in range(4):
            self.golden_data.fill(array=phantom, vertical=7 + i)

        self.golden_data_cs = self.golden_data.get_slice(vertical=cs_ind,
                                                         force=True)

        self.Op = ProjectionOperator(self.ig3D, self.ag3D)
        self.fp = self.Op.direct(self.golden_data)
Ejemplo n.º 6
0
    def test_backward(self):

        #this checks mechanics but not value
        Op = ProjectionOperator(self.ig, self.ag, adjoint_weights='matched')
        bp = Op.adjoint(self.acq_data)

        bp2 = bp.copy()
        bp2.fill(0)
        Op.adjoint(self.acq_data, out=bp2)
        np.testing.assert_allclose(bp.as_array(), bp2.as_array(), 1e-8)
Ejemplo n.º 7
0
    def _setup_PO_for_chunks(self, num_slices):

        if num_slices > 1:
            ag_slice = self.acquisition_geometry.copy()
            ag_slice.pixel_num_v = num_slices
        else:
            ag_slice = self.acquisition_geometry.get_slice(vertical=0)

        ig_slice = ag_slice.get_ImageGeometry()
        self.data_slice = ag_slice.allocate()
        self.operator = ProjectionOperator(ig_slice, ag_slice)
Ejemplo n.º 8
0
class Test_ProjectionOperator(unittest.TestCase):
    def setUp(self):

        N = 3
        angles = np.linspace(0, np.pi, 2, dtype='float32')

        self.ag = AcquisitionGeometry.create_Cone2D([0,-100],[0,200])\
                                .set_angles(angles, angle_unit='radian')\
                                .set_panel(N, 0.1)\
                                .set_labels(['angle', 'horizontal'])

        self.ig = self.ag.get_ImageGeometry()
        self.Op = ProjectionOperator(self.ig, self.ag)

        self.ag3D = AcquisitionGeometry.create_Cone3D([0,-100,0],[0,200,0])\
                                .set_angles(angles, angle_unit='radian')\
                                .set_panel((N,N), (0.1,0.1))\
                                .set_labels(['angle', 'vertical', 'horizontal'])

        self.ig3D = self.ag3D.get_ImageGeometry()
        self.Op3D = ProjectionOperator(self.ig3D, self.ag3D)

    @unittest.skipUnless(has_tigre, "TIGRE not installed")
    def test_norm(self):
        n = self.Op.norm()
        self.assertAlmostEqual(n, 0.08165, places=3)

        n3D = self.Op3D.norm()
        self.assertAlmostEqual(n3D, 0.08165, places=3)

    @unittest.skipUnless(has_tigre, "TIGRE not installed")
    def test_bp(self):
        gold = np.zeros((3, 3))
        gold.fill(2 / 30)

        ad = self.ag.allocate(1)
        res = self.Op.adjoint(ad)
        self.assertEqual(res.shape, self.ig.shape)
        np.testing.assert_allclose(res.as_array(), gold, atol=1e-6)

        res = self.ig.allocate(None)
        self.Op.adjoint(ad, out=res)
        self.assertEqual(res.shape, self.ig.shape)
        np.testing.assert_allclose(res.as_array(), gold, atol=1e-6)

    @unittest.skipUnless(has_tigre, "TIGRE not installed")
    def test_fp(self):
        gold = np.zeros((2, 3))
        gold.fill(0.1000017)

        data = self.ig.allocate(1)
        res = self.Op.direct(data)
        self.assertEqual(res.shape, self.ag.shape)
        np.testing.assert_allclose(res.as_array(), gold, atol=1e-6)

        res = self.ag.allocate(None)
        self.Op.direct(data, out=res)
        self.assertEqual(res.shape, self.ag.shape)
        np.testing.assert_allclose(res.as_array(), gold, atol=1e-6)
Ejemplo n.º 9
0
    def setUp(self):

        N = 3
        angles = np.linspace(0, np.pi, 2, dtype='float32')

        self.ag = AcquisitionGeometry.create_Cone2D([0,-100],[0,200])\
                                .set_angles(angles, angle_unit='radian')\
                                .set_panel(N, 0.1)\
                                .set_labels(['angle', 'horizontal'])

        self.ig = self.ag.get_ImageGeometry()
        self.Op = ProjectionOperator(self.ig, self.ag)

        self.ag3D = AcquisitionGeometry.create_Cone3D([0,-100,0],[0,200,0])\
                                .set_angles(angles, angle_unit='radian')\
                                .set_panel((N,N), (0.1,0.1))\
                                .set_labels(['angle', 'vertical', 'horizontal'])

        self.ig3D = self.ag3D.get_ImageGeometry()
        self.Op3D = ProjectionOperator(self.ig3D, self.ag3D)
Ejemplo n.º 10
0
    def test_bp_with_Astra(self):

        AOp = AstraProjectionOperator(self.ig, self.ag)
        bp_ASTRA = AOp.adjoint(self.fp.subset(vertical='centre'))
        TOp = ProjectionOperator(self.ig, self.ag)
        bp_TIGRE = TOp.adjoint(self.fp.subset(vertical='centre'))
        mean_diff = (bp_ASTRA - bp_TIGRE).abs().mean()
        self.assertLess(mean_diff, 1)
        np.testing.assert_allclose(bp_ASTRA.as_array(),
                                   bp_TIGRE.as_array(),
                                   atol=10)

        astra_ag3D = self.ag3D.subset(['vertical', 'angle', 'horizontal'])
        AOp = AstraProjectionOperator(self.ig3D, astra_ag3D)
        bp_ASTRA = AOp.adjoint(
            self.fp.subset(['vertical', 'angle', 'horizontal']))
        bp_TIGRE = self.Op.adjoint(self.fp)
        mean_diff = (bp_ASTRA - bp_TIGRE).abs().mean()
        self.assertLess(mean_diff, 1)
        np.testing.assert_allclose(bp_ASTRA.as_array(),
                                   bp_TIGRE.as_array(),
                                   atol=5)
Ejemplo n.º 11
0
    def compare_backward_FDK_matched(self):
        #this checks mechanics but not value
        Op = ProjectionOperator(self.ig, self.ag, adjoint_weights='matched')
        bp = Op.adjoint(self.acq_data)

        Op = ProjectionOperator(self.ig, self.ag, adjoint_weights='FDK')
        bp3 = Op.adjoint(self.acq_data)

        #checks weights parameter calls different backend
        diff = (bp3 - bp).abs().mean()
        self.assertGreater(diff, 1000)
Ejemplo n.º 12
0
    def compare_forward(self, direct_method, atol):

        Op = ProjectionOperator(self.ig, self.ag, direct_method=direct_method)
        fp = Op.direct(self.img_data)
        np.testing.assert_allclose(fp.as_array(),
                                   self.acq_data.as_array(),
                                   atol=atol)

        bp = Op.adjoint(fp)
        fp2 = fp.copy()
        fp2.fill(0)
        Op.direct(self.img_data, out=fp2)
        np.testing.assert_allclose(fp.as_array(), fp2.as_array(), 1e-8)
Ejemplo n.º 13
0
    def run(self, out=None, verbose=1):
        """
        Runs the configured FBP recon and returns the reconstruction

        Parameters
        ----------
        out : ImageData, optional
           Fills the referenced ImageData with the reconstructed volume and suppresses the return

        verbose : int, default=1
           Contols the verbosity of the reconstructor. 0: No output is logged, 1: Full configuration is logged

        Returns
        -------
        ImageData
            The reconstructed volume. Suppressed if `out` is passed
        """

        if verbose:
            print(self)

        if self.slices_per_chunk:

            if self.acquisition_geometry.dimension == '2D':
                raise ValueError(
                    "Only 3D datasets can be processed in chunks with `set_split_processing`"
                )
            elif self.acquisition_geometry.system_description == 'advanced':
                raise ValueError(
                    "Only simple and offset geometries can be processed in chunks with `set_split_processing`"
                )
            elif self.acquisition_geometry.get_ImageGeometry(
            ) != self.image_geometry:
                raise ValueError(
                    "Only default image geometries can be processed in chunks `set_split_processing`"
                )

            if out is None:
                ret = self.image_geometry.allocate()
            else:
                ret = out

            if self.filter_inplace:
                self._pre_filtering(self.input)

            tot_slices = self.acquisition_geometry.pixel_num_v
            remainder = tot_slices % self.slices_per_chunk
            num_chunks = int(
                np.ceil(self.image_geometry.shape[0] / self._slices_per_chunk))

            if verbose:
                pbar = tqdm(total=num_chunks)

            #process dataset by requested chunk size
            self._setup_PO_for_chunks(self.slices_per_chunk)
            for i in range(0, tot_slices - remainder, self.slices_per_chunk):
                self._process_chunk(i, self.slices_per_chunk, ret)
                if verbose:
                    pbar.update(1)

            #process excess rows
            if remainder:
                i = tot_slices - remainder
                self._setup_PO_for_chunks(remainder)
                self._process_chunk(i, remainder, ret)
                if verbose:
                    pbar.update(1)

            if verbose:
                pbar.close()

            if out is None:
                return ret

        else:

            if self.filter_inplace is False:
                proj_filtered = self.input.copy()
            else:
                proj_filtered = self.input

            self._pre_filtering(proj_filtered)

            operator = ProjectionOperator(self.image_geometry,
                                          self.acquisition_geometry)

            if out is None:
                return operator.adjoint(proj_filtered)
            else:
                operator.adjoint(proj_filtered, out=out)
Ejemplo n.º 14
0
class FBP(GenericFilteredBackProjection):
    """
    Creates an FBP reconstructor based on your parallel-beam acquisition data.

    Parameters
    ----------
    input : AcquisitionData
        The input data to reconstruct. The reconstructor is set-up based on the geometry of the data.
        
    image_geometry : ImageGeometry, default used if None
        A description of the area/volume to reconstruct
        
    filter : string, numpy.ndarray, default='ram-lak'
        The filter to be applied. Can be a string from: 'ram-lak' or a numpy array.

    Example
    -------
    >>> fbp = FBP(data)
    >>> out = fbp.run()

    Notes
    -----
    The reconstructor can be futher customised using additional 'set' methods provided.
    """
    @property
    def slices_per_chunk(self):
        return self._slices_per_chunk

    def __init__(self, input, image_geometry=None, filter='ram-lak'):

        super().__init__(input, image_geometry, filter)
        self.set_split_processing(False)

        if input.geometry.geom_type != AcquisitionGeometry.PARALLEL:
            raise TypeError(
                "This reconstructor is for parallel-beam data only.")

    def set_split_processing(self, slices_per_chunk=0):
        """
        Splits the processing in to chunks. Default, 0 will process the data in a single call.

        Parameters
        ----------
        out : slices_per_chunk, optional
            Process the data in chunks of n slices. It is recommended to use value of power-of-two.

        Notes
        -----
        This will reduce memory use but may increase computation time.
        It is recommended to tune it too your hardware requirements using 8, 16 or 32 slices.
        
        This can only be used on simple and offset data-geometries.
        """

        try:
            num_slices = int(slices_per_chunk)
        except:
            num_slices = 0

        if num_slices >= self.acquisition_geometry.pixel_num_v:
            num_slices = self.acquisition_geometry.pixel_num_v

        self._slices_per_chunk = num_slices

    def _calculate_weights(self, acquisition_geometry):

        ag = acquisition_geometry
        weight = (2 * np.pi / ag.num_projections) / (4 * ag.pixel_size_h)

        self._weights = np.full((ag.pixel_num_v, ag.pixel_num_h),
                                weight,
                                dtype=np.float32)

    def _setup_PO_for_chunks(self, num_slices):

        if num_slices > 1:
            ag_slice = self.acquisition_geometry.copy()
            ag_slice.pixel_num_v = num_slices
        else:
            ag_slice = self.acquisition_geometry.get_slice(vertical=0)

        ig_slice = ag_slice.get_ImageGeometry()
        self.data_slice = ag_slice.allocate()
        self.operator = ProjectionOperator(ig_slice, ag_slice)

    def _process_chunk(self, i, step, out):
        self.data_slice.fill(np.squeeze(self.input.array[:, i:i + step, :]))
        if not self.filter_inplace:
            self._pre_filtering(self.data_slice)
        out.array[i:i + step, :, :] = self.operator.adjoint(
            self.data_slice).array[:]

    def run(self, out=None, verbose=1):
        """
        Runs the configured FBP recon and returns the reconstruction

        Parameters
        ----------
        out : ImageData, optional
           Fills the referenced ImageData with the reconstructed volume and suppresses the return

        verbose : int, default=1
           Contols the verbosity of the reconstructor. 0: No output is logged, 1: Full configuration is logged

        Returns
        -------
        ImageData
            The reconstructed volume. Suppressed if `out` is passed
        """

        if verbose:
            print(self)

        if self.slices_per_chunk:

            if self.acquisition_geometry.dimension == '2D':
                raise ValueError(
                    "Only 3D datasets can be processed in chunks with `set_split_processing`"
                )
            elif self.acquisition_geometry.system_description == 'advanced':
                raise ValueError(
                    "Only simple and offset geometries can be processed in chunks with `set_split_processing`"
                )
            elif self.acquisition_geometry.get_ImageGeometry(
            ) != self.image_geometry:
                raise ValueError(
                    "Only default image geometries can be processed in chunks `set_split_processing`"
                )

            if out is None:
                ret = self.image_geometry.allocate()
            else:
                ret = out

            if self.filter_inplace:
                self._pre_filtering(self.input)

            tot_slices = self.acquisition_geometry.pixel_num_v
            remainder = tot_slices % self.slices_per_chunk
            num_chunks = int(
                np.ceil(self.image_geometry.shape[0] / self._slices_per_chunk))

            if verbose:
                pbar = tqdm(total=num_chunks)

            #process dataset by requested chunk size
            self._setup_PO_for_chunks(self.slices_per_chunk)
            for i in range(0, tot_slices - remainder, self.slices_per_chunk):
                self._process_chunk(i, self.slices_per_chunk, ret)
                if verbose:
                    pbar.update(1)

            #process excess rows
            if remainder:
                i = tot_slices - remainder
                self._setup_PO_for_chunks(remainder)
                self._process_chunk(i, remainder, ret)
                if verbose:
                    pbar.update(1)

            if verbose:
                pbar.close()

            if out is None:
                return ret

        else:

            if self.filter_inplace is False:
                proj_filtered = self.input.copy()
            else:
                proj_filtered = self.input

            self._pre_filtering(proj_filtered)

            operator = ProjectionOperator(self.image_geometry,
                                          self.acquisition_geometry)

            if out is None:
                return operator.adjoint(proj_filtered)
            else:
                operator.adjoint(proj_filtered, out=out)

    def reset(self):
        """
        Resets all optional configuration parameters to their default values
        """
        super().reset()
        self.set_split_processing(0)

    def __str__(self):

        repres = "FBP recon\n"

        repres += self._str_data_size()

        repres += "\nReconstruction Options:\n"
        repres += "\tBackend: {}\n".format(self._backend)
        repres += "\tFilter: {}\n".format(self._filter)
        repres += "\tFFT order: {}\n".format(self._fft_order)
        repres += "\tFilter_inplace: {}\n".format(self._filter_inplace)
        repres += "\tSplit processing: {}\n".format(self._slices_per_chunk)

        if self._slices_per_chunk:
            num_chunks = int(
                np.ceil(self.image_geometry.shape[0] / self._slices_per_chunk))
        else:
            num_chunks = 1

        repres += "\nReconstructing in {} chunk(s):\n".format(num_chunks)

        return repres
Ejemplo n.º 15
0
 def compare_norm(self, direct_method, norm):
     ig = self.ag_small.get_ImageGeometry()
     Op = ProjectionOperator(ig, self.ag_small, direct_method=direct_method)
     n = Op.norm()
     self.assertAlmostEqual(n, norm, places=1)
Ejemplo n.º 16
0
ig.voxel_num_x = int(num_pixels_x - 200 / bins)
ig.voxel_num_y = int(num_pixels_x - 600 / bins)
ig.voxel_num_z = int(400 // bins)
print(ig)

#%% Reconstruct with FDK
fbp = FBP(ig, ag)
fbp.set_input(aq_data)
FBP_3D_gpu = fbp.get_output()

show2D(FBP_3D_gpu,
       slice_list=[('vertical', 204 // bins), ('horizontal_y', 570 // bins)],
       title="FBP reconstruction",
       fix_range=(-0.02, 0.07))
#%% Setup least sqaures and force pre-calculation of Lipschitz constant
Projector = ProjectionOperator(ig, ag)
LS = LeastSquares(A=Projector, b=aq_data)
print("Lipschitz constant =", LS.L)

#%% Setup FISTA to solve for least squares
fista = FISTA(x_init=ig.allocate(0),
              f=LS,
              g=ZeroFunction(),
              max_iteration=1000)
fista.update_objective_interval = 10

#%% Run FISTA
fista.run(300)
LS_reco = fista.get_output()
show2D(LS_reco,
       slice_list=[('vertical', 204 // bins), ('horizontal_y', 570 // bins)],
Ejemplo n.º 17
0
class Test_results(unittest.TestCase):
    def setUp(self):
        #%% Setup Geometry
        voxel_num_xy = 255
        voxel_num_z = 15
        cs_ind = (voxel_num_z - 1) // 2

        mag = 2
        src_to_obj = 50
        src_to_det = src_to_obj * mag

        pix_size = 0.2
        det_pix_x = voxel_num_xy
        det_pix_y = voxel_num_z

        num_projections = 1000
        angles = np.linspace(0, 360, num=num_projections, endpoint=False)

        self.ag = AcquisitionGeometry.create_Cone2D([0,-src_to_obj],[0,src_to_det-src_to_obj])\
                                           .set_angles(angles)\
                                           .set_panel(det_pix_x, pix_size)\
                                           .set_labels(['angle','horizontal'])

        self.ig = self.ag.get_ImageGeometry()

        self.ag3D = AcquisitionGeometry.create_Cone3D([0,-src_to_obj,0],[0,src_to_det-src_to_obj,0])\
                                     .set_angles(angles)\
                                     .set_panel((det_pix_x,det_pix_y), (pix_size,pix_size))\
                                     .set_labels(['angle','vertical','horizontal'])
        self.ig3D = self.ag3D.get_ImageGeometry()

        #%% Create phantom
        kernel_size = voxel_num_xy
        kernel_radius = (kernel_size - 1) // 2
        y, x = np.ogrid[-kernel_radius:kernel_radius + 1,
                        -kernel_radius:kernel_radius + 1]

        circle1 = [5, 0, 0]  #r,x,y
        dist1 = ((x - circle1[1])**2 + (y - circle1[2])**2)**0.5

        circle2 = [5, 80, 0]  #r,x,y
        dist2 = ((x - circle2[1])**2 + (y - circle2[2])**2)**0.5

        circle3 = [25, 0, 80]  #r,x,y
        dist3 = ((x - circle3[1])**2 + (y - circle3[2])**2)**0.5

        mask1 = (dist1 - circle1[0]).clip(0, 1)
        mask2 = (dist2 - circle2[0]).clip(0, 1)
        mask3 = (dist3 - circle3[0]).clip(0, 1)
        phantom = 1 - np.logical_and(np.logical_and(mask1, mask2), mask3)

        self.golden_data = self.ig3D.allocate(0)
        for i in range(4):
            self.golden_data.fill(array=phantom, vertical=7 + i)

        self.golden_data_cs = self.golden_data.get_slice(vertical=cs_ind,
                                                         force=True)

        self.Op = ProjectionOperator(self.ig3D, self.ag3D)
        self.fp = self.Op.direct(self.golden_data)

    @unittest.skipUnless(has_tigre, "TIGRE not installed")
    def test_FBP(self):
        reco_out = FBP(self.ig, self.ag)(self.fp.get_slice(vertical='centre'))
        mean_diff = (self.golden_data_cs - reco_out).abs().mean()
        self.assertLess(mean_diff, 0.01)
        np.testing.assert_allclose(self.golden_data_cs.as_array(),
                                   reco_out.as_array(),
                                   atol=1)

        reco_out3D = FBP(self.ig3D, self.ag3D)(self.fp)
        diff = (self.golden_data - reco_out3D).abs()
        self.assertLess(diff.mean(), 0.01)
        np.testing.assert_allclose(self.golden_data.as_array(),
                                   reco_out3D.as_array(),
                                   atol=1)

    @unittest.skipUnless(has_tigre and has_astra,
                         "TIGRE or ASTRA not installed")
    def test_fp_with_Astra(self):
        AOp = AstraProjectionOperator(self.ig, self.ag)
        fp_ASTRA = AOp.direct(self.golden_data_cs)

        TOp = ProjectionOperator(self.ig, self.ag)
        fp_TIGRE = TOp.direct(self.golden_data_cs)

        mean_diff = (fp_ASTRA - fp_TIGRE).abs().mean()
        self.assertLess(mean_diff, 1e-2)
        np.testing.assert_allclose(fp_TIGRE.as_array(),
                                   fp_ASTRA.as_array(),
                                   atol=1)

        astra_ag3D = self.ag3D.copy()
        astra_ag3D.set_labels(['vertical', 'angle', 'horizontal'])

        AOp = AstraProjectionOperator(self.ig3D, astra_ag3D)
        fp_ASTRA = AOp.direct(self.golden_data)

        fp_ASTRA.reorder(['angle', 'vertical', 'horizontal'])
        mean_diff = (fp_ASTRA - self.fp).abs().mean()
        self.assertLess(mean_diff, 1)
        np.testing.assert_allclose(self.fp.as_array(),
                                   fp_ASTRA.as_array(),
                                   atol=5)

    @unittest.skipUnless(has_tigre and has_astra,
                         "TIGRE or ASTRA not installed")
    def test_bp_with_Astra(self):

        AOp = AstraProjectionOperator(self.ig, self.ag)
        bp_ASTRA = AOp.adjoint(self.fp.get_slice(vertical='centre'))
        TOp = ProjectionOperator(self.ig, self.ag)
        bp_TIGRE = TOp.adjoint(self.fp.get_slice(vertical='centre'))
        mean_diff = (bp_ASTRA - bp_TIGRE).abs().mean()
        self.assertLess(mean_diff, 1)
        np.testing.assert_allclose(bp_ASTRA.as_array(),
                                   bp_TIGRE.as_array(),
                                   atol=10)

        astra_fp = self.fp.copy()
        astra_fp.reorder(['vertical', 'angle', 'horizontal'])

        AOp = AstraProjectionOperator(self.ig3D, astra_fp.geometry)
        bp_ASTRA = AOp.adjoint(astra_fp)

        bp_TIGRE = self.Op.adjoint(self.fp)
        mean_diff = (bp_ASTRA - bp_TIGRE).abs().mean()
        self.assertLess(mean_diff, 1)
        np.testing.assert_allclose(bp_ASTRA.as_array(),
                                   bp_TIGRE.as_array(),
                                   atol=5)

    @unittest.skipUnless(has_tigre and has_astra,
                         "TIGRE or ASTRA not installed")
    def test_FBP_with_Astra(self):
        reco_ASTRA = AstraFBP(self.ig,
                              self.ag)(self.fp.get_slice(vertical='centre'))
        reco_TIGRE = FBP(self.ig,
                         self.ag)(self.fp.get_slice(vertical='centre'))
        mean_diff = (reco_ASTRA - reco_TIGRE).abs().mean()
        self.assertLess(mean_diff, 1e-4)
        np.testing.assert_allclose(reco_ASTRA.as_array(),
                                   reco_TIGRE.as_array(),
                                   atol=1e-2)

        astra_fp = self.fp.copy()
        astra_fp.reorder(['vertical', 'angle', 'horizontal'])
        reco_ASTRA3D = AstraFBP(self.ig3D, astra_fp.geometry)(astra_fp)
        reco_TIGRE3D = FBP(self.ig3D, self.ag3D)(self.fp)
        diff = (reco_ASTRA3D - reco_TIGRE3D).abs()
        self.assertLess(diff.mean(), 1e-4)
        np.testing.assert_allclose(reco_ASTRA3D.as_array(),
                                   reco_TIGRE3D.as_array(),
                                   atol=1e-2)