Source code for mantidimaging.core.io.loader.loader

# Copyright (C) 2023 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations
import os
from dataclasses import dataclass, field
from logging import getLogger
from pathlib import Path
from typing import Tuple, List, Optional, Union, TYPE_CHECKING, Callable

import numpy as np
import astropy.io.fits as fits
from tifffile import tifffile

from mantidimaging.core.io.loader import img_loader
from mantidimaging.core.io.utility import find_first_file_that_is_possibly_a_sample
from mantidimaging.core.utility.data_containers import Indices, FILE_TYPES, ProjectionAngles
from mantidimaging.core.utility.imat_log_file_parser import IMATLogFile
from mantidimaging.core.io.filenames import FilenameGroup

if TYPE_CHECKING:
    import numpy.typing as npt
    from mantidimaging.core.data import ImageStack
    from mantidimaging.core.utility.progress_reporting import Progress

LOG = getLogger(__name__)

DEFAULT_IS_SINOGRAM = False
DEFAULT_PIXEL_SIZE = 0
DEFAULT_PIXEL_DEPTH = "float32"


[docs]@dataclass class ImageParameters: """ Dataclass to hold info about an image stack that is to be loaded. Used with LoadingParameters """ file_group: FilenameGroup log_file: Optional[Path] = None indices: Optional[Indices] = None
[docs]@dataclass class LoadingParameters: """ Dataclass to hold info about a dataset that is about to be loaded. Used to transfer information from ImageLoadDialog to the loading code. """ image_stacks: dict[FILE_TYPES, ImageParameters] = field(default_factory=dict) pixel_size: int = DEFAULT_PIXEL_SIZE name: str = "" dtype: str = DEFAULT_PIXEL_DEPTH sinograms: bool = DEFAULT_IS_SINOGRAM
def _fitsread(filename: Union[Path, str]) -> np.ndarray: """ Read one image and return it as a 2d numpy array :param filename :: name of the image file, can be relative or absolute path :param img_format: format of the image ('fits') """ image = fits.open(filename) if len(image) < 1: raise RuntimeError("Could not load at least one FITS image/table file from: {0}".format(filename)) # get the image data return image[0].data def _imread(filename: Union[Path, str]) -> np.ndarray: try: return tifffile.imread(filename) except tifffile.TiffFileError as e: raise RuntimeError(f"TiffFileError {e.args[0]}: {filename}") from e
[docs]def get_loader(in_format: str) -> Callable[[Union[Path, str]], np.ndarray]: if in_format in ['fits', 'fit']: load_func = _fitsread elif in_format in ['tiff', 'tif']: load_func = _imread else: raise NotImplementedError("Loading not implemented for:", in_format) return load_func
[docs]def read_image_dimensions(file_path: Path) -> Tuple[int, int]: load_func = get_loader(file_path.suffix.replace(".", "")) img = load_func(file_path) return img.shape
[docs]def load_log(log_file: Path) -> IMATLogFile: with open(log_file, 'r') as f: return IMATLogFile(f.readlines(), log_file)
[docs]def load_stack_from_group(group: FilenameGroup, progress: Optional[Progress] = None) -> ImageStack: return load(filename_group=group, progress=progress)
[docs]def load_stack_from_image_params(image_params: ImageParameters, progress: Optional[Progress] = None, dtype: npt.DTypeLike = np.float32): return load(filename_group=image_params.file_group, progress=progress, dtype=dtype, indices=image_params.indices, log_file=image_params.log_file)
[docs]def load(filename_group: FilenameGroup, dtype: 'npt.DTypeLike' = np.float32, indices: Optional[Union[List[int], Indices]] = None, progress: Optional[Progress] = None, log_file: Optional[Path] = None) -> ImageStack: """ Loads a stack, including sample, white and dark images. :param dtype: Default:np.float32, data type for the input images :param filename_group: FilenameGroup to provided file names for loading :param indices: Specify which indices are loaded from the found files. This **DOES NOT** check for the number in the image filename, but removes all indices from the filenames list that are not selected :param progress: The progress reporting instance :return: an ImageStack """ if indices and len(indices) < 3: raise ValueError("Indices at this point MUST have 3 elements: [start, stop, step]!") file_names = [str(p) for p in filename_group.all_files()] in_format = filename_group.first_file().suffix.lstrip('.') load_func = get_loader(in_format) if log_file is not None: log_data = load_log(log_file) angles = log_data.projection_angles().value angle_order = np.argsort(angles) file_names = [file_names[i] for i in angle_order] image_stack = img_loader.execute(load_func, file_names, in_format, dtype, indices, progress) if log_file is not None: image_stack.log_file = log_data image_stack.set_projection_angles(ProjectionAngles(angles[angle_order])) # Search for and load metadata file metadata_filename = filename_group.metadata_path if metadata_filename: with open(metadata_filename) as f: image_stack.load_metadata(f) LOG.debug('Loaded metadata from: {}'.format(metadata_filename)) else: LOG.debug('No metadata file found') return image_stack
[docs]def create_loading_parameters_for_file_path(file_path: Path) -> Optional[LoadingParameters]: sample_file = find_first_file_that_is_possibly_a_sample(str(file_path)) if sample_file is None: return None loading_parameters = LoadingParameters() loading_parameters.name = os.path.basename(sample_file) sample_fg = FilenameGroup.from_file(sample_file) sample_fg.find_all_files() sample_fg.find_log_file() loading_parameters.image_stacks[FILE_TYPES.SAMPLE] = ImageParameters(sample_fg, sample_fg.log_path) for file_type in [ft for ft in FILE_TYPES if ft.mode in ["images", "180"]]: fg = sample_fg.find_related(file_type) if fg is None: continue fg.find_all_files() if file_type.tname == "Flat": fg.find_log_file() loading_parameters.image_stacks[file_type] = ImageParameters(fg, fg.log_path) return loading_parameters