def test_record_parameters_in_metadata(self): imgs = Images(np.asarray([1])) imgs.record_operation('test_func', 'A pretty name', this=765, that=495.0, roi=(1, 2, 3, 4)) expected = { 'operation_history': [{ 'name': 'test_func', 'display_name': 'A pretty name', 'kwargs': { 'this': 765, 'that': 495.0, 'roi': (1, 2, 3, 4) }, }] } self.assertIn(const.TIMESTAMP, imgs.metadata[const.OPERATION_HISTORY][0]) imgs.metadata[const.OPERATION_HISTORY][0].pop(const.TIMESTAMP) self.assertEqual(imgs.metadata, expected)
def test_metadata_round_trip(self): # Create dummy image stack sample = th.gen_img_shared_array_with_val(42.) images = Images(sample) images.metadata['message'] = 'hello, world!' # Save image stack saver.save(images, self.output_directory) # Load image stack back dataset = loader.load(self.output_directory) loaded_images = dataset.sample # Ensure properties have been preserved self.assertEqual(loaded_images.metadata, images.metadata) loaded_images.free_memory() if dataset.dark_before: dataset.dark_before.free_memory() if dataset.dark_after: dataset.dark_after.free_memory() if dataset.flat_before: dataset.flat_before.free_memory() if dataset.flat_after: dataset.flat_after.free_memory()
def full(images: Images, cors: List[ScalarCoR], recon_params: ReconstructionParameters, progress: Optional[Progress] = None) -> Images: progress = Progress.ensure_instance(progress, num_steps=images.height) output_shape = (images.num_sinograms, images.width, images.width) output_images: Images = Images.create_empty_images( output_shape, images.dtype, images.metadata) output_images.record_operation('AstraRecon.full', 'Reconstruction', **recon_params.to_dict()) # FIXME multiple GPU support - just starting up a Pool doesn't seem to work # the GPUs can't initialise the memory properly. Not sure why # num_gpus = AstraRecon._count_gpus() # LOG.info(f"Running with {num_gpus} GPUs") # partial = ptsm.create_partial(AstraRecon.single, ptsm.fwd_gpu_recon, # num_gpus=num_gpus, cors=cors, # proj_angles=proj_angles, recon_params=recon_params) # ptsm.execute(images.sinograms, output_images.data, partial, num_gpus, progress=progress) proj_angles = images.projection_angles() for i in range(images.height): output_images.data[i] = AstraRecon.single_sino( images.sino(i), cors[i], proj_angles, recon_params) progress.update(1, "Reconstructed slice") return output_images
def execute(load_func, sample_path, flat_path, dark_path, img_format, dtype, indices, progress=None) -> Dataset: """ Reads a stack of images into memory, assuming dark and flat images are in separate directories. If several files are found in the same directory (for example you give image0001.fits and there's also image0002.fits, image0003.fits) these will also be loaded as the usual convention in ImageJ and related imaging tools, using the last digits to sort the images in the stack. Usual type in fits is 16-bit pixel depth, data type is denoted with: '>i2' - uint16 '>f2' - float16 '>f4' - float32 :returns: Images object """ if not sample_path: raise RuntimeError("No filenames were provided.") # The following codes assume that all images have the same size and properties as the first. # This is always true in the case of raw data first_sample_img = load_func(sample_path[0]) # select the files loaded based on the indices, if any are provided chosen_input_filenames = sample_path[ indices[0]:indices[1]:indices[2]] if indices else sample_path # get the shape of all images img_shape = first_sample_img.shape # forward all arguments to internal class for easy re-usage il = ImageLoader(load_func, img_format, img_shape, dtype, indices, progress) # we load the flat and dark first, because if they fail we don't want to # fail after we've loaded a big stack into memory flat_data, flat_filenames, flat_mfname = il.load_data(flat_path) dark_data, dark_filenames, dark_mfname = il.load_data(dark_path) sample_data, sample_mfname = il.load_sample_data(chosen_input_filenames) return Dataset( Images(sample_data, chosen_input_filenames, indices, memory_filename=sample_mfname), Images(flat_data, flat_filenames, memory_filename=flat_mfname) if flat_data is not None else None, Images(dark_data, dark_filenames, memory_filename=dark_mfname) if dark_data is not None else None)
def update_image_operations(images: Images, model): """ TODO: remove this function / integrate in recon GUI Updates the image operation history with the results in the given model. """ images.record_operation(const.OPERATION_NAME_COR_TILT_FINDING, display_name="Calculated COR/Tilt", **model.stack_properties)
def _set_random_data(data, shape, array_name): n = np.random.rand(*shape) # move the data in the shared array data[:] = n[:] images = Images(data) images.memory_filename = array_name return images
def filter_func(images: Images, rebin_param=0.5, mode=None, cores=None, chunksize=None, progress=None) -> Images: """ :param images: Sample data which is to be processed. Expects radiograms :param rebin_param: int, float or tuple int - Percentage of current size. float - Fraction of current size. tuple - Size of the output image (x, y). :param mode: Interpolation to use for re-sizing ('nearest', 'lanczos', 'bilinear', 'bicubic' or 'cubic'). :param cores: The number of cores that will be used to process the data. :param chunksize: The number of chunks that each worker will receive. :return: The processed 3D numpy.ndarray """ h.check_data_stack(images) if isinstance(rebin_param, tuple): param_valid = rebin_param[0] > 0 and rebin_param[1] > 0 else: param_valid = rebin_param > 0 if param_valid: sample = images.data sample_name: Optional[str] if images.memory_filename is not None: sample_name = images.memory_filename images.free_memory(delete_filename=False) else: # this case is true when the filter preview is being calculated sample_name = None # force single core execution as it's faster for a single image cores = 1 empty_resized_data = _create_reshaped_array( sample.shape, sample.dtype, rebin_param, sample_name) f = ptsm.create_partial(skimage.transform.resize, ptsm.return_to_second_but_dont_use_it, mode=mode, output_shape=empty_resized_data.shape[1:]) ptsm.execute(sample, empty_resized_data, f, cores, chunksize, progress=progress, msg="Applying Rebin") images.data = empty_resized_data return images
def test_image_eq_method(self): data_array = np.arange(64, dtype=float).reshape([4, 4, 4]) data_images = Images(data_array.copy()) data_images2 = Images(data_array.copy()) self.assertEqual(data_images, data_array) self.assertEqual(data_images, data_images2) data_array[1, 1, 1] *= 2 self.assertNotEqual(data_images, data_array) self.assertRaises(ValueError, lambda a, b: a == b, data_images, 1.0)
def __init__(self, images: Images, slice_idx: int, initial_cor: ScalarCoR, recon_params: ReconstructionParameters): self.image_width = images.width self.sino = images.sino(slice_idx) # Initial parameters self.centre_cor = initial_cor.value self.cor_step = 50 # Cache projection angles self.proj_angles = images.projection_angles() self.recon_params = recon_params self.reconstructor = get_reconstructor_for(recon_params.algorithm)
def test_parse_metadata_file(self): json_file = io.StringIO('{"a_int": 42, "a_string": "yes", "a_arr": ["one", "two", ' '"three"], "a_float": 3.65e-05, "a_bool": true}') imgs = Images(np.asarray([1])) imgs.load_metadata(json_file) def validate_prop(k, v): self.assertEqual(imgs.metadata[k], v) validate_prop('a_bool', True) validate_prop('a_string', 'yes') validate_prop('a_int', 42) validate_prop('a_arr', ['one', 'two', 'three'])
def test_do_stack_reconstruct_slice(self): test_data = Images(np.ndarray(shape=(200, 250), dtype=np.float32)) test_data.record_operation = mock.Mock() task_mock = mock.Mock(result=test_data, error=None) self.presenter._get_slice_index = mock.Mock(return_value=7) self.presenter._on_stack_reconstruct_slice_done(task_mock) self.view.show_recon_volume.assert_called_once() np.array_equal(self.view.show_recon_volume.call_args[0][0].data, test_data) test_data.record_operation.assert_called_once_with('AstraRecon.single_sino', 'Slice Reconstruction', slice_idx=7, **self.view.recon_params().to_dict())
def test_metadata_round_trip(self): # Create dummy image stack sample = th.gen_img_numpy_rand() images = Images(sample) images.metadata['message'] = 'hello, world!' # Save image stack saver.save(images, self.output_directory) # Load image stack back dataset = loader.load(self.output_directory) loaded_images = dataset.sample # Ensure properties have been preserved self.assertEqual(loaded_images.metadata, images.metadata)
def execute(load_func, file_name, dtype, name, indices=None, progress=None): """ Load a single image FILE that is expected to be a stack of images. Parallel execution can be slower depending on the storage system. On HDD I've found it's about 50% SLOWER, thus not recommended! :param file_name: list of image file paths given as strings :param load_func: file name extension if fixed (to set the expected image format) :param dtype: data type for the output numpy array :return: stack of images as a 3-elements tuple: numpy array with sample images, white image, and dark image. """ # create shared array new_data = load_func(file_name) if indices: new_data = new_data[indices[0]:indices[1]:indices[2]] img_shape = new_data.shape data = pu.create_array(img_shape, dtype=dtype) # we could just move with data[:] = new_data[:] but then we don't get # loading bar information, and I doubt there's any performance gain data = do_stack_load_seq(data, new_data, img_shape, name, progress) # Nexus doesn't load flat/dark images yet, if the functionality is # requested it should be changed here return Images(data, file_name)
def _set_random_data(data, shape): n = np.random.rand(*shape) # move the data in the shared array data[:] = n[:] images = Images(data) return images
def find_center(images: Images, progress: Progress) -> Tuple[ScalarCoR, Degrees]: # assume the ROI is the full image, i.e. the slices are ALL rows of the image slices = np.arange(images.height) with pu.temp_shared_array((images.height, )) as shift: # this is the area that is looked into for the shift after overlapping the images search_range = get_search_range(images.width) func = shared_mem.create_partial(do_search, shared_mem.fwd_index_only, image_width=images.width, p0=images.projection(0), p180=np.fliplr( images.proj180deg.data[0]), search_range=search_range) shared_mem.execute(shift, func, progress=progress, msg="Finding correlation on row") par = np.polyfit(slices, shift, deg=1) m = par[0] q = par[1] LOG.debug(f"m={m}, q={q}") theta = Degrees(np.rad2deg(np.arctan(0.5 * m))) offset = np.round(m * images.height * 0.5 + q) * 0.5 LOG.info(f"found offset: {-offset} and tilt {theta}") return ScalarCoR(images.h_middle + -offset), theta
def _set_random_data(data, shape, seed: Optional[int] = None): n = gen_img_numpy_rand(shape, seed=seed) # move the data in the shared array data[:] = n[:] images = Images(data) return images
def filter_func(images: Images, min_input: float = 0.0, max_input: float = 10000.0, max_output: float = 256.0, progress=None, data_type=None) -> Images: images.data[images.data < min_input] = 0 images.data[images.data > max_input] = 0 images.data *= (max_output / images.data.max()) if data_type is not None: if data_type == uint16 and not images.dtype == uint16: images.data = images.data.astype(uint16) elif data_type == float32 and not images.dtype == float32: images.data = images.data.astype(float32) return images
def find_cor(images: Images, slice_idx: int, start_cor: float, recon_params: ReconstructionParameters) -> float: return tomopy.find_center(images.sinograms, images.projection_angles( recon_params.max_projection_angle).value, ind=slice_idx, init=start_cor, sinogram_order=True)
def setUp(self): self.model = ReconstructWindowModel(CorTiltPointQtModel()) # Mock stack self.stack = mock.create_autospec(StackVisualiserView) data = Images(data=np.ndarray(shape=(10, 128, 256), dtype=np.float32)) self.stack.presenter = StackVisualiserPresenter(self.stack, data) self.model.initial_select_data(self.stack)
def setUp(self): # Mock view self.make_view() self.presenter = ReconstructWindowPresenter(self.view, None) # Mock stack self.sv_view = mock.create_autospec(StackVisualiserView) data = Images(data=np.ndarray(shape=(128, 10, 128), dtype=np.float32)) data.pixel_size = TEST_PIXEL_SIZE self.sv_view.presenter = StackVisualiserPresenter(self.sv_view, data) self.presenter.model.initial_select_data(self.sv_view) self.view.get_stack_visualiser = mock.Mock(return_value=self.sv_view) import uuid self.uuid = uuid.uuid4()
def full(images: Images, cors: List[ScalarCoR], recon_params: ReconstructionParameters, progress: Optional[Progress] = None) -> Images: progress = Progress.ensure_instance(progress, num_steps=images.height) output_shape = (images.num_sinograms, images.width, images.width) output_images: Images = Images.create_empty_images( output_shape, images.dtype, images.metadata) output_images.record_operation('AstraRecon.full', 'Reconstruction', **recon_params.to_dict()) proj_angles = images.projection_angles( recon_params.max_projection_angle) for i in range(images.height): output_images.data[i] = AstraRecon.single_sino( images.sino(i), cors[i], proj_angles, recon_params) progress.update(1, "Reconstructed slice") return output_images
def filter_func(data: Images) -> Images: """ Executes the filter algorithm on a given set of image data with the given parameters. :param data: the image data to apply the filter to :param kwargs: any additional arguments which the specific filter uses :return: the image data after applying the filter """ raise_not_implemented("filter_func") return Images(np.asarray([]))
def create_stack_window(self, stack: Images, position: Qt.DockWidgetArea = Qt.DockWidgetArea. RightDockWidgetArea, floating: bool = False) -> StackVisualiserView: stack.make_name_unique(self.stack_names) stack_vis = StackVisualiserView(self, stack) # this puts the new stack window into the centre of the window self.splitter.addWidget(stack_vis) self.setCentralWidget(self.splitter) # add the dock widget into the main window self.addDockWidget(position, stack_vis) stack_vis.setFloating(floating) self.presenter.add_stack_to_dictionary(stack_vis) return stack_vis
def _create_images(self, data_array: np.ndarray, name: str) -> Images: """ Use a data array to create an Images object. :param data_array: The images array obtained from the NeXus file. :param name: The name of the image dataset. :return: An Images object. """ data = pu.create_array(data_array.shape, self.view.pixelDepthComboBox.currentText()) data[:] = data_array return Images(data, [f"{name} {self.title}"])
def filter_func(images: Images, min_input: float = 0.0, max_input: float = 10000.0, max_output: float = 256.0, progress=None, data_type=None) -> Images: np.clip(images.data, min_input, max_input, out=images.data) # offset - it removes any negative values so that they don't overflow when in uint16 range images.data -= nanmin(images.data) data_max = nanmax(images.data) # slope images.data *= (max_output / data_max) if data_type is not None: if data_type == uint16 and not images.dtype == uint16: images.data = images.data.astype(uint16) elif data_type == float32 and not images.dtype == float32: images.data = images.data.astype(float32) return images
def filter_func(data: Images) -> Images: """ Executes the filter algorithm on a given set of image data with the given parameters. The body of this function does not need to include pre and post processing steps - these should be included in the do_before_wrapper and do_after_wrapper, respectively. :param data: the image data to apply the filter to :param kwargs: any additional arguments which the specific filter uses :return: the image data after applying the filter """ raise_not_implemented("filter_func") return Images(np.asarray([]))
def filter_func(images: Images, region_of_interest: Optional[Union[List[int], List[float], SensibleROI]] = None, progress=None) -> Images: """ Execute the Crop Coordinates by Region of Interest filter. This does NOT do any checks if the Region of interest is out of bounds! If the region of interest is out of bounds, the crop will **FAIL** at runtime. If the region of interest is in bounds, but has overlapping coordinates the crop give back a 0 shape of the coordinates that were wrong. :param images: Input data as a 3D numpy.ndarray :param region_of_interest: Crop original images using these coordinates. The selection is a rectangle and expected order is - Left Top Right Bottom. :return: The processed 3D numpy.ndarray """ if region_of_interest is None: region_of_interest = SensibleROI.from_list([0, 0, 50, 50]) if isinstance(region_of_interest, list): region_of_interest = SensibleROI.from_list(region_of_interest) assert isinstance(region_of_interest, SensibleROI) h.check_data_stack(images) sample = images.data shape = (sample.shape[0], region_of_interest.height, region_of_interest.width) sample_name = images.memory_filename if sample_name is not None: images.free_memory(delete_filename=False) output = pu.create_array(shape, sample.dtype, sample_name) images.data = execute_single(sample, region_of_interest, progress, out=output) return images
def __init__(self, images: Images, slice_idx: int, initial_cor: ScalarCoR, recon_params: ReconstructionParameters, iters_mode: bool): self.image_width = images.width self.sino = images.sino(slice_idx) # Initial parameters if iters_mode: self.centre_value: Union[int, float] = INIT_ITERS_CENTRE_VALUE self.step = INIT_ITERS_STEP self.initial_cor = initial_cor self._recon_preview = self._recon_iters_preview self._divide_step = self._divide_iters_step else: self.centre_value = initial_cor.value self.step = self.image_width * 0.05 self._recon_preview = self._recon_cor_preview self._divide_step = self._divide_cor_step # Cache projection angles self.proj_angles = images.projection_angles(recon_params.max_projection_angle) self.recon_params = recon_params self.reconstructor = get_reconstructor_for(recon_params.algorithm)
def filter_func(images: Images, region_of_interest: Optional[Union[List[int], List[float], SensibleROI]] = None, progress=None) -> Images: """Execute the Crop Coordinates by Region of Interest filter. This does NOT do any checks if the Region of interest is out of bounds! If the region of interest is out of bounds, the crop will **FAIL** at runtime. If the region of interest is in bounds, but has overlapping coordinates the crop give back a 0 shape of the coordinates that were wrong. :param images: Input data as a 3D numpy.ndarray :param region_of_interest: Crop original images using these coordinates. The selection is a rectangle and expected order is - Left Top Right Bottom. :return: The processed 3D numpy.ndarray """ if region_of_interest is None: region_of_interest = SensibleROI.from_list([0, 0, 50, 50]) if isinstance(region_of_interest, list): region_of_interest = SensibleROI.from_list(region_of_interest) assert isinstance(region_of_interest, SensibleROI) h.check_data_stack(images) sample = images.data shape = (sample.shape[0], region_of_interest.height, region_of_interest.width) if any((s < 0 for s in shape)): raise ValueError( "It seems the Region of Interest is outside of the current image dimensions.\n" "This can happen on the image preview right after a previous Crop Coordinates." ) # allocate output first BEFORE freeing the original data, # otherwise it's possible to free and then fail allocation for output # at which point you're left with no data output = pu.allocate_output(images, shape) images.data = execute_single(sample, region_of_interest, progress, out=output) return images
def _execute_gpu(data, size, mode, progress=None): log = getLogger(__name__) progress = Progress.ensure_instance(progress, num_steps=data.shape[0], task_name="Median filter GPU") cuda = gpu.CudaExecuter(data.dtype) with progress: log.info("GPU median filter, with pixel data type: {0}, filter " "size/width: {1}.".format(data.dtype, size)) data = cuda.median_filter(data, size, mode, progress) return Images(data)