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)
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)
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)
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
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)
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)
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)
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)
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)
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)
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)
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)
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)
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
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)
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)],
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)