API References

Top Level Module

Globals

This module contains registries that store data, functions or classes. Useful for registering items within this library or libraries built on Tomo Base.

xp module-attribute

xp = EnvironmentContext()

logger module-attribute

logger = get_logger()

TOMOBASE_TRANSFORM_CATEGORIES module-attribute

TOMOBASE_TRANSFORM_CATEGORIES = TransformItemDict(Image_Processing=None, Align=None, Project=None, Reconstruct=None, Deform=None, Quantification=None)

TOMOBASE_PROCESSES module-attribute

TOMOBASE_PROCESSES = ProcessItemDict()

TOMOBASE_DATATYPES module-attribute

TOMOBASE_DATATYPES = DataItemDict(Data=None, Image=None, Sinogram=None, Volume=None)

TOMOBASE_TILTSCHEMES module-attribute

TOMOBASE_TILTSCHEMES = TiltSchemeItemDict()

TOMOBASE_PHANTOMS module-attribute

TOMOBASE_PHANTOMS = PhantomItemDict()

GPUContext

Bases: Enum

Source code in tomobase/registrations/environment.py
7
8
9
class GPUContext(enum.Enum):
    CUPY = 1
    NUMPY = 2

ItemDictNonSingleton

Source code in tomobase/registrations/base.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
class ItemDictNonSingleton():      
    def __init__(self, **kwargs):

        # Note For Developers check setattr otherwise youll include your variables as dict keys and this will mess the whole dict up
        self._index = 0
        self._dict = {}
        self._item_class = kwargs.get('item_class', Item)

        #Default Values for using the plugins system 
        self._module = 'tomobase'
        self._folder = 'plugins'    
        self._hook = 'default'

        reserved_keys = ['_index', '_dict', '_module', '_folder', '_hook', '_item_class', 'item_class']
        for key, value in kwargs.items():
            if key not in reserved_keys:
                newkey = key.upper()
                newkey = newkey.replace(' ', '_')
                if value is None:
                    value = self._index
                self._dict[newkey] = self._item_class(value, key)
                self._index += 1

    def __setattr__(self, key, value):
        if key in ['_index', '_dict', '_module', '_folder', '_hook', '_item_class']:
            # Allow these keys to be set as attributes
            super().__setattr__(key, value)
        else:
            # Add other keys to the internal dictionary
            self._dict[key] = value

    def __getattr__(self, key):
        try:
            return self._dict[key]
        except KeyError:
            logger.warning(f" object has no attribute '{key}'")

    def __getitem__(self, key):
        return self._dict[key]

    def __setitem__(self, key, value):
        if key in self._dict:
            pass
        else:
            if value is None:
                value = self._index
            newkey = key.upper()
            newkey = newkey.replace(' ', '_')
            self._dict[newkey] = self._item_class(value, key)
            self._index += 1

    def __len__(self):
        return self._index

    def loc(self, index):
        for key, item in self._dict.items():
            if item.value == index:
                return self._dict[key]
        logger.warning(f"Index {index} not found in the dictionary")

    def items(self):
        return self._dict.items()

    def key(self, index):
        for key, item in self._dict.items():
            if item.value == index:
                return key
        logger.warning(f"Index {index} not found in the dictionary")

    def append(self, **kwargs):
        for key, value in kwargs.items():
            self[key] = value

    def help(self):
        msg = "\n"
        for key, value in self._dict.items():
            msg += f"{Fore.BLUE}{key}{Style.RESET_ALL}: {value.name}, {value.value}\n"
        logger.info(msg)


    def update(self):
        spec = importlib.util.find_spec(self._module)
        if spec is None or spec.origin is None:
            raise ImportError(f"Cannot find the {self._module} package")

        path = os.path.dirname(spec.origin)
        tiltscheme_path = os.path.join(path, self._folder)
        for root, _, files in os.walk(tiltscheme_path):
            for filename in files:
                if filename.endswith('.py'):
                    module_path = os.path.relpath(os.path.join(root, filename), start=path)
                    module_name = self._module+'.'+ module_path.replace(os.sep, '.')[:-3]
                    module = importlib.import_module(module_name)
                    for name, obj in inspect.getmembers(module):
                        if inspect.isclass(obj) or inspect.isfunction(obj):
                            if hasattr(obj, self._hook):
                                self._update_item(obj)

    def _update_item(self, obj):
        self[obj.tomobase_name] = obj

ItemDict

Bases: ItemDictNonSingleton

Source code in tomobase/registrations/base.py
159
160
161
162
163
164
165
166
167
168
class ItemDict(ItemDictNonSingleton):
    _instances = {}

    def __new__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(ItemDict, cls).__new__(cls)
        return cls._instances[cls]

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

Item

Bases: object

Source code in tomobase/registrations/base.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Item(object):

    def __init__(self, value, name):
        self._value = value    
        self._name = name

    def __call__(self, *args, **kwargs):
        if callable(self._value):
            return self._value(*args, **kwargs)
        else:
            logger.warning(f"Item {self._name} is not callable!")

    def __setitem__(self, name, value):
        if isinstance(self._value, dict) or isinstance(self._value, ItemDictNonSingleton):
            self._value[name] = value
        else:
            super().__setattr__(name, value)

    def __getitem__(self, key):
        if isinstance(self._value, dict) or isinstance(self._value, ItemDictNonSingleton):
            return self._value[key]
        else:
            super().__getattr__(key)

    @property
    def value(self):
        return self._value

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @value.setter
    def value(self, value):
        self._value = value

    def items(self):
        if isinstance(self._value, dict) or isinstance(self._value, ItemDictNonSingleton) or isinstance(self._value, ItemDict):
            return self._value.items()
        else:
            _dict = {}
            return _dict.items()

Data

The Data module contains the data types supported by the Tomo Base library. Each module is registered with an id stored in TOMOBASE_DATATYPES in the global registers.

Data

Bases: ABC

Abstract base class for microscopy and tomography datasets. To implement a child of this class you must:

  • implement methods to read data from a file, these should be class methods that return an instance of the class

  • implement methods to write data to a file

  • create the class variables _readers and _writers which are dictionaries that link each supported filetype to the correct reader or writer respectively, the keys should be the extensions in lowercase

Attributes:
  • pixelsize (float) –

    The size of the pixels in the dataset

  • metadata (dict) –

    A dictionary containing metadata about the dataset

Source code in tomobase/data/base.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class Data(ABC):
    """
    Abstract base class for microscopy and tomography datasets. To implement a child of this class you must:

    - implement methods to read data from a file, these should be class methods
      that return an instance of the class

    - implement methods to write data to a file

    - create the class variables ``_readers`` and ``_writers`` which are
      dictionaries that link each supported filetype to the correct reader or
      writer respectively, the keys should be the extensions in lowercase

    Attributes:
        pixelsize (float): The size of the pixels in the dataset
        metadata (dict): A dictionary containing metadata about the dataset

    """


    def __init__(self, pixelsize: float=1.0, metadata: dict = {}):
        """Initialize the Data object

        Args:
            pixelsize (float, optional): The size of the pixels in the dataset. Defaults to 1.0 nm
            metadata (dict, optional): A dictionary containing metadata about the dataset. Defaults to {}.
        """
        self.pixelsize = pixelsize
        self.metadata = metadata

        # Note these are used to swap between numpy and cupy context
        self._context = GPUContext.NUMPY
        self._device = 0

    @classmethod
    def from_file(cls, filename: pathlib.Path | None = None, **kwargs):
        """Read a dataset from a file

        Args:
            filename (pathlib.Path | None): The name of the file from which to read the data, a GUI will be opened to select a file if none is specified (default: None)

        Exceptions:
            ValueError: If the file type is not supported or the file cannot be found

        Returns:
            Data: An instance of the Data class containing the read data
        """
        if filename is None:
            app = QApplication([])
            filename, _ = QFileDialog.getOpenFileName(None, "Select File", "", 
                                    [(f"{ext.upper()} files", f"*.{ext}") for ext in cls._readers.keys()])
            if not filename:
                raise Exception("No file selected or file could not be found")
            app.quit()

        _, ext = os.path.splitext(filename)
        ext = ext[1:]  # remove the dot

        try:
            reader = cls._readers[ext]
            reader(filename, **kwargs)
            data = reader(filename, **kwargs)
            return data
        except KeyError:
            raise ValueError(f"The given file type {ext.upper()} is not supported.")


    def to_file(self, filename: pathlib.Path | None = None, **kwargs):
        """Save the data to a file

        Arguments:
            filename (pathlib.Path | None): The name of the file to which to write the data, a GUI will be opened to select a file if none is specified (default: None)

        Exceptions:
            ValueError: If the file type is not supported or the file is not found
        """
        if filename is None:
            app = QApplication([])
            filename, _ = QFileDialog.getSaveFileName(None, "Save File", "", ";;".join([f"{ext.upper()} files (*.{ext})"
                                                                            for ext in self._writers.keys()]))
            if not filename:
                raise Exception("No file selected or file could not be found")
            app.quit()

        _, ext = os.path.splitext(filename)
        ext = ext[1:]  # remove the dot

        try:
            writer = self._writers[ext]
            writer(self, filename, **kwargs)
        except KeyError:
            raise ValueError(f"The given file type {ext.upper()} is not supported.")


    @property
    @abstractmethod
    def _writers(self):
        pass

    @property
    @abstractmethod
    def _readers(self):
        pass

    @classmethod
    def _get_type_id(cls) -> int:
        # Only use with napari used to identify the object from the layer etc
        class_name_upper = cls.__name__.upper()
        return TOMOBASE_DATATYPES[class_name_upper].value

    def _set_context(self, context:GPUContext | None = None, device:int | None = None):
        # used to set the context 
        if context is None:
            context = xp.context
        if device is None:
            device = xp.device

        self.data = xp.asarray(self.data, context, device)
        self._context = context
        self._device = device

    def layer_metadata(self, metadata: dict = {}):
        """Get the layer metadata in the format required for napari implementation

        Args:
            metadata (dict): the associated metadata for the layer

        Returns:
            dict: the layer metadata in the required format. By default the dict is wrapped in another dict of key 'ct metadata'. This avoids the metadata from conflicting with other packages.
        """

        meta = {}
        if 'ct metadata' not in metadata:
            meta['ct metadata'] = {}
        else:
            meta['ct metadata'] = metadata['ct metadata']

        for key, value in self.metadata.items():
            meta['ct metadata'][key] = value

        #must replace with the correct type in subclasses
        meta['ct metadata']['type'] = TOMOBASE_DATATYPES.DATA.value
        return meta

    def layer_attributes(self, attributes: dict = {}):
        """Get the layer attributes in the format required for napari implementation

        Args:
            attributes (dict): the associated attributes for the layer. 

        Returns:
            dict: the layer attributes in the required format. By default the structure is as follows:

        """
        attr = {}
        attr['name'] = attributes.get('name', 'Data')
        attr['scale'] = attributes.get('pixelsize' ,(self.pixelsize, self.pixelsize, self.pixelsize))
        attr['colormap'] = attributes.get('colormap', 'gray')
        attr['contrast_limits'] = attributes.get('contrast_limits', [0, np.max(self.data)*1.5])
        return attr

    def to_data_tuple(self, attributes:dict={}, metadata:dict={}):
        """ 
        Builds a Napari Layer Data Tuple

        Args:
            attributes (dict): the associated attributes for the layer. Defaults to {}.
            metadata (dict): the associated metadata for the layer. Defaults to {}.

        Returns:
            tuple: A tuple of (data, attributes, 'image') where data is the data array, attributes is a dictionary of attributes and 'image' is the layer type for napari.
        """
        attributes = self.layer_attributes(attributes)
        metadata = self.layer_metadata(metadata)
        attributes['metadata'] = metadata
        layerdata = (self.data, attributes, 'image')
        return layerdata

    @classmethod
    def from_data_tuple(cls, layer, attributes:dict|None=None):
        """Create an instance of the class from a Napari layer data tuple.

        Args:
            layer (napari layer): The Napari layer to convert.
            attributes (dict | None, optional): The associated attributes for the layer. Defaults to None.

        Returns:
           Data: An instance of the Data class.
        """

        if attributes is None:
            data = layer.data
            scale = layer.scale[0]
            layer_metadata = layer.metadata['ct metadata']
        else:
            data = layer
            scale = attributes['scale'][0]
            layer_metadata = attributes['metadata']['ct metadata']

        return cls(data, scale, layer_metadata)

from_file classmethod

from_file(filename: Path | None = None, **kwargs)

Read a dataset from a file

Parameters:
  • filename (Path | None, default: None ) –

    The name of the file from which to read the data, a GUI will be opened to select a file if none is specified (default: None)

Raises:
  • ValueError

    If the file type is not supported or the file cannot be found

Returns:
  • Data

    An instance of the Data class containing the read data

Source code in tomobase/data/base.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@classmethod
def from_file(cls, filename: pathlib.Path | None = None, **kwargs):
    """Read a dataset from a file

    Args:
        filename (pathlib.Path | None): The name of the file from which to read the data, a GUI will be opened to select a file if none is specified (default: None)

    Exceptions:
        ValueError: If the file type is not supported or the file cannot be found

    Returns:
        Data: An instance of the Data class containing the read data
    """
    if filename is None:
        app = QApplication([])
        filename, _ = QFileDialog.getOpenFileName(None, "Select File", "", 
                                [(f"{ext.upper()} files", f"*.{ext}") for ext in cls._readers.keys()])
        if not filename:
            raise Exception("No file selected or file could not be found")
        app.quit()

    _, ext = os.path.splitext(filename)
    ext = ext[1:]  # remove the dot

    try:
        reader = cls._readers[ext]
        reader(filename, **kwargs)
        data = reader(filename, **kwargs)
        return data
    except KeyError:
        raise ValueError(f"The given file type {ext.upper()} is not supported.")

to_file

to_file(filename: Path | None = None, **kwargs)

Save the data to a file

Parameters:
  • filename (Path | None, default: None ) –

    The name of the file to which to write the data, a GUI will be opened to select a file if none is specified (default: None)

Raises:
  • ValueError

    If the file type is not supported or the file is not found

Source code in tomobase/data/base.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def to_file(self, filename: pathlib.Path | None = None, **kwargs):
    """Save the data to a file

    Arguments:
        filename (pathlib.Path | None): The name of the file to which to write the data, a GUI will be opened to select a file if none is specified (default: None)

    Exceptions:
        ValueError: If the file type is not supported or the file is not found
    """
    if filename is None:
        app = QApplication([])
        filename, _ = QFileDialog.getSaveFileName(None, "Save File", "", ";;".join([f"{ext.upper()} files (*.{ext})"
                                                                        for ext in self._writers.keys()]))
        if not filename:
            raise Exception("No file selected or file could not be found")
        app.quit()

    _, ext = os.path.splitext(filename)
    ext = ext[1:]  # remove the dot

    try:
        writer = self._writers[ext]
        writer(self, filename, **kwargs)
    except KeyError:
        raise ValueError(f"The given file type {ext.upper()} is not supported.")

layer_metadata

layer_metadata(metadata: dict = {})

Get the layer metadata in the format required for napari implementation

Parameters:
  • metadata (dict, default: {} ) –

    the associated metadata for the layer

Returns:
  • dict

    the layer metadata in the required format. By default the dict is wrapped in another dict of key 'ct metadata'. This avoids the metadata from conflicting with other packages.

Source code in tomobase/data/base.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def layer_metadata(self, metadata: dict = {}):
    """Get the layer metadata in the format required for napari implementation

    Args:
        metadata (dict): the associated metadata for the layer

    Returns:
        dict: the layer metadata in the required format. By default the dict is wrapped in another dict of key 'ct metadata'. This avoids the metadata from conflicting with other packages.
    """

    meta = {}
    if 'ct metadata' not in metadata:
        meta['ct metadata'] = {}
    else:
        meta['ct metadata'] = metadata['ct metadata']

    for key, value in self.metadata.items():
        meta['ct metadata'][key] = value

    #must replace with the correct type in subclasses
    meta['ct metadata']['type'] = TOMOBASE_DATATYPES.DATA.value
    return meta

layer_attributes

layer_attributes(attributes: dict = {})

Get the layer attributes in the format required for napari implementation

Parameters:
  • attributes (dict, default: {} ) –

    the associated attributes for the layer.

Returns:
  • dict

    the layer attributes in the required format. By default the structure is as follows:

Source code in tomobase/data/base.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def layer_attributes(self, attributes: dict = {}):
    """Get the layer attributes in the format required for napari implementation

    Args:
        attributes (dict): the associated attributes for the layer. 

    Returns:
        dict: the layer attributes in the required format. By default the structure is as follows:

    """
    attr = {}
    attr['name'] = attributes.get('name', 'Data')
    attr['scale'] = attributes.get('pixelsize' ,(self.pixelsize, self.pixelsize, self.pixelsize))
    attr['colormap'] = attributes.get('colormap', 'gray')
    attr['contrast_limits'] = attributes.get('contrast_limits', [0, np.max(self.data)*1.5])
    return attr

to_data_tuple

to_data_tuple(attributes: dict = {}, metadata: dict = {})

Builds a Napari Layer Data Tuple

Parameters:
  • attributes (dict, default: {} ) –

    the associated attributes for the layer. Defaults to {}.

  • metadata (dict, default: {} ) –

    the associated metadata for the layer. Defaults to {}.

Returns:
  • tuple

    A tuple of (data, attributes, 'image') where data is the data array, attributes is a dictionary of attributes and 'image' is the layer type for napari.

Source code in tomobase/data/base.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def to_data_tuple(self, attributes:dict={}, metadata:dict={}):
    """ 
    Builds a Napari Layer Data Tuple

    Args:
        attributes (dict): the associated attributes for the layer. Defaults to {}.
        metadata (dict): the associated metadata for the layer. Defaults to {}.

    Returns:
        tuple: A tuple of (data, attributes, 'image') where data is the data array, attributes is a dictionary of attributes and 'image' is the layer type for napari.
    """
    attributes = self.layer_attributes(attributes)
    metadata = self.layer_metadata(metadata)
    attributes['metadata'] = metadata
    layerdata = (self.data, attributes, 'image')
    return layerdata

from_data_tuple classmethod

from_data_tuple(layer, attributes: dict | None = None)

Create an instance of the class from a Napari layer data tuple.

Parameters:
  • layer (napari layer) –

    The Napari layer to convert.

  • attributes (dict | None, default: None ) –

    The associated attributes for the layer. Defaults to None.

Returns:
  • Data

    An instance of the Data class.

Source code in tomobase/data/base.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
@classmethod
def from_data_tuple(cls, layer, attributes:dict|None=None):
    """Create an instance of the class from a Napari layer data tuple.

    Args:
        layer (napari layer): The Napari layer to convert.
        attributes (dict | None, optional): The associated attributes for the layer. Defaults to None.

    Returns:
       Data: An instance of the Data class.
    """

    if attributes is None:
        data = layer.data
        scale = layer.scale[0]
        layer_metadata = layer.metadata['ct metadata']
    else:
        data = layer
        scale = attributes['scale'][0]
        layer_metadata = attributes['metadata']['ct metadata']

    return cls(data, scale, layer_metadata)

Image

Bases: Data

A class for single image datasets.

Supported File Formats
  • .png
  • .jpg
  • .jpeg
  • .bmp
  • .tif
  • .tiff
Attributes:
  • data (ndarray | ndarray) –

    The image data. The data is indexed using the (rows, columns, channels) standard, which corresponds to (x, y, color)

Source code in tomobase/data/image.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Image(Data):
    """ A class for single image datasets.

    Supported File Formats:
        - .png
        - .jpg
        - .jpeg
        - .bmp
        - .tif
        - .tiff

    Attributes:
        data (numpy.ndarray | cupy.ndarray): The image data. The data is indexed using the (rows, columns, channels) standard, which corresponds to (x, y, color)
    """

    def __init__(self, data, pixelsize: float = 1.0, metadata: dict = {}):
        """
        Initialize the Image object

        Args:
            data (numpy.ndarray) : the image data
            pixelsize (float): The size of the pixels in the dataset. Defaults to 1.0 nm
            metadata (dict): A dictionary containing metadata about the dataset. Defaults to {}.
        """

        self.data = data
        super().__init__(pixelsize, metadata=metadata)

    @staticmethod
    def _read_emi(filename, **kwargs):
        # TODO make this method independent of Hyperspy
        """
        content = hs.load(filename, lazy=False, reader='emi')
        data = np.transpose(np.asarray(content.data, dtype=float), (1, 0))   # With transpose we make sure that
                                                                # the orientation is correct in the
                                                                # case where the scanning rotation
                                                                # was set to -90 (which is the
                                                                # default of the FEI tomo software)
        pixelsize = content.axes_manager[0].scale  # Hyperspy uses nm
        im = Image(data, pixelsize)
        im.metadata['alpha_tilt'] = content.metadata.Acquisition_instrument.TEM.Stage.tilt_alpha
        return im
        """

    @staticmethod
    def _read_image(filename, **kwargs):
        return Image(xp.asarray(iio.imread(filename), dtype=float))

    def _write_image(self, filename, **kwargs):
        iio.imwrite(filename, self.data)

    def layer_metadata(self, metadata={}):
        meta = super().layer_metadata(metadata)
        meta['ct metadata']['type'] = TOMOBASE_DATATYPES.IMAGE.value
        meta['ct metadata']['axis'] = ['Signal', 'y', 'x'] if len(self.data.shape) == 3 else ['y', 'x']
        return meta

    def layer_attributes(self, attributes={}):
        attr = super().layer_attributes(attributes)
        attr['name'] = attr.get('name', 'Image')
        attr['scale'] = attr.get('pixelsize' ,(self.pixelsize, self.pixelsize))
        attr['contrast_limits'] = attr.get('contrast_limits', [0, np.max(self.data)*1.5])
        return attr


    _readers = {}
    _writers = {
        'png': _write_image,
        'bmp': _write_image,
        'tif': _write_image,
        'tiff': _write_image,
    }

Sinogram

Bases: Data

The sinogram is a stack of projection images, indexed using the (n, x, y) orientation.

Supported File Types
  • .h5
  • .mrc
  • .emi
  • .mat (experimental)
Attributes:
  • data (ndarray) –

    The sinogram data, indexed using the (n, x, y) orientation.

  • angles (ndarray) –

    The tilt angles in degrees corresponding to the projection images.

  • times (ndarray) –

    The times of acquisition corresponding to the projection images. This defaults to the projection index starting at 1. Otherwise it should be provided in seconds

Source code in tomobase/data/sinogram.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
class Sinogram(Data):
    """
    The sinogram is a stack of projection images, indexed using the
    (n, x, y) orientation. 

    Supported File Types:
        - .h5
        - .mrc
        - .emi
        - .mat (experimental)

    Attributes:
        data (numpy.ndarray): The sinogram data, indexed using the (n, x, y) orientation.
        angles (numpy.ndarray): The tilt angles in degrees corresponding to the projection images.
        times (numpy.ndarray): The times of acquisition corresponding to the projection images. This defaults to the projection index starting at 1. Otherwise it should be provided in seconds


    """

    def __init__(self, data, angles: np.ndarray, pixelsize: float = 1.0, times: np.ndarray | None = None, metadata: dict = {}):
        """Initialize a sinogram class

        Arguments:
            data (numpy.ndarray): The sinogram data, indexed using the (n, x, y) orientation.
            angles (numpy.ndarray): The tilt angles in degrees corresponding to the projection images.
            pixelsize (float): The width of the pixels in nanometer (default 1.0)
            times (numpy.ndarray | None): The times of acquisition corresponding to the projection images. This defaults to the projection index starting at 1. Otherwise it should be provided in seconds.
            metadata (dict): Additional metadata to store with the sinogram.

        """

        if len(angles) != data.shape[0]:
            raise ValueError(("There should be the same number of projection images as tilt angles."))
        if times is None:
            times = np.linspace(1, len(angles), len(angles))
        elif len(times) != len(angles):
            raise ValueError(("There should be the same number of projection images as times."))

        self.times = times
        self.data = data
        super().__init__(pixelsize, metadata)
        self.angles = np.asarray(angles)
        self.dim_default = 3

    def sort(self, bytime:bool = False):
        """
        Sort the sinogram by angles or by time

        Args:
            bytime (bool): Sort by time instead of angles
        """
        if bytime:
            indices = np.argsort(self.times)
            self.times = self.times[indices]
            self.angles = self.angles[indices] 
            self.data = self.data[indices,:,:]
        else:
            indices = np.argsort(self.angles)
            self.angles = self.angles[indices]
            self.times = self.times[indices]
            self.data = self.data[indices,:,:]

    def insert(self, img: np.ndarray, angle: float, time: float | None = None):
        """
        Insert a new image into the sinogram

        Args:
            img (numpy.ndarray): The image to insert
            angle (float): The angle of the image
            time (float): The time of acquisition
        """
        if time is None:
            time = self.times[-1] + 1

        if self.data.ndim == 2:
            # If `self.data` is 2D, add a new first axis and stack `img`
            self.data = np.stack((self.data, img), axis=0)
        else:
            # If `self.data` is already 3D, concatenate along the first axis
            self.data = np.concatenate(    (self.data, img), axis=0)
        #self.data = np.dstack((self.data, img))
        self.angles = np.append(self.angles, angle)
        self.times = np.append(self.times, time)

    def remove(self, index: int):
        """
        Remove an image from the sinogram

        Args:
            index (int): The index of the image to remove
        """
        self.data = np.delete(self.data, index, axis=0)
        self.angles = np.delete(self.angles, index)
        self.times = np.delete(self.times, index)

    @staticmethod
    def _read_h5(filename):
        f = h5py.File(filename, 'r')
        nt = len(f.keys())-2
        times = np.zeros(nt)
        angles = np.zeros(nt)
        for i in range(nt):
            key = 'image '+str(i)
            if i == 0:
                nx, ny = f[key]['HAADF'].shape
                data = xp.zeros([nx,ny,nt])
            data[:,:,i] = f[key]['HAADF']
            times[i] = np.array(f[key]['acquisition timee (s)']).item()
            angles[i] = np.array(f[key]['alpha tilt (deg)']).item()
        return Sinogram(data, angles, times=times)

    @staticmethod
    def _read_mrc(filename, **kwargs):
        data, metadata = mrcz.readMRC(filename)
        data = xp.asarray(data)
        pixelsize = metadata['pixelsize'][0]
        angles = metadata['angles']
        if 'times' in metadata:
            times = metadata['times']
        else:
            times = np.linspace(1, len(angles), len(angles)+1)
        return Sinogram(data, angles, pixelsize, times)


    @staticmethod
    def _read_mat(path):
        obj = loadmat(path)
        #if obj has a series or stack key

        if 'series' in obj:
            key = 'series'
        elif 'stack' in obj:
            key = 'stack'
        else:
            key = 'obj'


        d = obj[key]['data'][0][0]
        a = obj[key]['angles'][0][0]
        p = obj[key]['pixelsize'][0][0]
        if 'times' in obj[key]:
            t = obj[key]['times'][0][0]
        else:
            t = np.linspace(1, len(a), len(a)+1)

        ts = Sinogram(xp.asarray(d.squeeze()), a.squeeze(), p.squeeze(), t.squeeze())
        return ts

    def _write_mrc(self, filename, **kwargs):
        mrcz.writeMRC(self.data, filename, meta={'angles': self.angles, 'times': self.times}, pixelsize=[self.pixelsize, self.pixelsize, self.pixelsize])

    def _write_mat(self, filename, **kwargs):
        myrec = {'data':self.data, 'angles':self.angles, 'pixelsize':self.pixelsize, 'times':self.times} 
        savemat(filename, {'obj': myrec})

    @staticmethod
    def _read_emi_stack(filename, **kwargs):
        # List all EMI files in the directory of the selected file
        dirname = os.path.dirname(os.path.realpath(filename))
        filenames = glob.glob(dirname + "*.emi")
        # Read the first file for image size and metadata
        im = Image.from_file(filenames[0])
        data = np.zeros((len(filenames), im.data.shape[0], im.data.shape[1] ))
        angles = np.zeros(len(filenames))
        # Set the contents of the first file
        data[0, :, :] = im.data
        angles[0] = im.metadata['alpha_tilt']
        pixelsize = im.pixelsize
        # Loop over the other files to get all the projection images
        for i, filename in enumerate(filenames[1:], start=1):
            im = Image.from_file(filenames[0])
            data[i, :, :] = im.data
            angles[i] = im.metadata['alpha_tilt']
        # Sort the images by tilt angle and return the sinogram
        sorted_indices = np.argsort(angles)
        data = data[:, :, sorted_indices]
        angles = angles[sorted_indices]
        return Sinogram(data, angles, pixelsize)

    _readers = {}
    _writers = {
        'mrc': _write_mrc,
        'mat': _write_mat,
        'ali': _write_mrc,
    }



    def layer_attributes(self, attributes={}):
        attr = super().layer_attributes(attributes)
        attr['name'] = attributes.get('name', 'Sinogram')
        return attr

    def layer_metadata(self, metadata={}):
        meta = super().layer_metadata(metadata)
        meta['ct metadata']['type'] = TOMOBASE_DATATYPES.SINOGRAM.value
        meta['ct metadata']['angles'] = self.angles
        meta['ct metadata']['times'] = self.times
        meta['ct metadata']['axis'] = ['Projection', 'y', 'x'] if len(self.data.shape) == 3 else ['Projection', 'Signal', 'y', 'x']

        return meta

    @classmethod
    def from_data_tuple(cls, layer, attributes=None):
        if attributes is None:
            data = layer.data
            scale = layer.scale[0]
            times = layer.metadata['ct metadata']['times']
            angles = layer.metadata['ct metadata']['angles']
            layer_metadata = layer.metadata['ct metadata']
        else:
            data = layer
            scale = attributes['scale'][0]
            layer_metadata = attributes['metadata']['ct metadata']
            times =attributes['metadata']['ct metadata']['times']
            angles = attributes['metadata']['ct metadata']['angles']

        layer_metadata.pop('times', None)
        layer_metadata.pop('angles', None)

        return cls(data, angles, scale, times, layer_metadata)

sort

sort(bytime: bool = False)

Sort the sinogram by angles or by time

Parameters:
  • bytime (bool, default: False ) –

    Sort by time instead of angles

Source code in tomobase/data/sinogram.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def sort(self, bytime:bool = False):
    """
    Sort the sinogram by angles or by time

    Args:
        bytime (bool): Sort by time instead of angles
    """
    if bytime:
        indices = np.argsort(self.times)
        self.times = self.times[indices]
        self.angles = self.angles[indices] 
        self.data = self.data[indices,:,:]
    else:
        indices = np.argsort(self.angles)
        self.angles = self.angles[indices]
        self.times = self.times[indices]
        self.data = self.data[indices,:,:]

insert

insert(img: ndarray, angle: float, time: float | None = None)

Insert a new image into the sinogram

Parameters:
  • img (ndarray) –

    The image to insert

  • angle (float) –

    The angle of the image

  • time (float, default: None ) –

    The time of acquisition

Source code in tomobase/data/sinogram.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def insert(self, img: np.ndarray, angle: float, time: float | None = None):
    """
    Insert a new image into the sinogram

    Args:
        img (numpy.ndarray): The image to insert
        angle (float): The angle of the image
        time (float): The time of acquisition
    """
    if time is None:
        time = self.times[-1] + 1

    if self.data.ndim == 2:
        # If `self.data` is 2D, add a new first axis and stack `img`
        self.data = np.stack((self.data, img), axis=0)
    else:
        # If `self.data` is already 3D, concatenate along the first axis
        self.data = np.concatenate(    (self.data, img), axis=0)
    #self.data = np.dstack((self.data, img))
    self.angles = np.append(self.angles, angle)
    self.times = np.append(self.times, time)

remove

remove(index: int)

Remove an image from the sinogram

Parameters:
  • index (int) –

    The index of the image to remove

Source code in tomobase/data/sinogram.py
102
103
104
105
106
107
108
109
110
111
def remove(self, index: int):
    """
    Remove an image from the sinogram

    Args:
        index (int): The index of the image to remove
    """
    self.data = np.delete(self.data, index, axis=0)
    self.angles = np.delete(self.angles, index)
    self.times = np.delete(self.times, index)

Volume

Bases: Data

A 3D volume that is the result of a tomographic reconstruction.

Supported file formats
  • .rec
  • .tiff
Attributes:
  • data (ndarray) –

    The data represented by voxels. The data is indexed using (y, x, z) notation

Source code in tomobase/data/volume.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
class Volume(Data):
    """
    A 3D volume that is the result of a tomographic reconstruction. 

    Supported file formats:
        - .rec
        - .tiff

    Attributes:
        data (numpy.ndarray): The data represented by voxels. The data is indexed using  (y, x, z) notation

    """

    def __init__(self, data:np.ndarray, pixelsize:float=1.0, metadata:dict={}):
        """
        Initialize a volume object.

        Arguments:
            data (numpy.ndarray): The volume data.
            pixelsize (float): The size of the pixels in nanometers (default 1.0).
            metadata (dict): Additional metadata for the volume (default empty).
        """
        self.data = data
        super().__init__(pixelsize, metadata)


    @staticmethod
    def _read_rec(filename, normalize=True, **kwargs):
        with open(filename, 'rb') as f:
            # Data dimensions and type
            nx, ny, nz = np.fromfile(f, count=3, dtype='int32')
            datatype = np.fromfile(f, count=1, dtype='int32')
            if datatype == 0:
                datatype = 'uint8'
            elif datatype == 1:
                datatype = 'int16'
            elif datatype == 2:
                datatype = 'float32'
            elif datatype == 6:
                datatype = 'uint16'
            else:
                raise ValueError("Unsupported datatype in REC data.")

            # Pixel size in nm
            f.seek(10)
            cell_size = np.fromfile(f, count=1, dtype='int32')
            pixelsize = cell_size.astype('float32') / nx

            # Skip header
            f.seek(92)
            header_size = np.fromfile(f, count=1, dtype='int32')
            f.seek(1024 + header_size.item())

            # Read data
            data = np.fromfile(f, count=nx*ny*nz, dtype=datatype)
            data = np.reshape(data, [nx, ny, nz], order='F')
            data = np.transpose(data, (1, 0, 2))

            if normalize:
                return _rescale(Volume(data.astype(float), pixelsize=1.0))
            else:
                return Volume(data, pixelsize)

    def _write_rec(self, filename, normalize=True, **kwargs):
        # Convert data to (X, Y, Z)
        data = np.transpose(self.data, (1, 0, 2))

        # Create MRC header
        header = np.zeros(256, dtype='int32')
        header[:3] = data.shape  # Array dimensions
        if data.dtype == np.uint8 or normalize:
            header[3] = 0
        elif data.dtype == np.int16:
            header[3] = 1
        elif data.dtype == np.float32:
            header[3] = 2
        elif data.dtype == np.uint16:
            header[3] = 6
        else:
            raise TypeError("Unsupported data type for writing in REC file.")
        # Sampling along X, Y and Z. Same as array dimensions
        header[7:10] = data.shape
        # Physical dimensions in nm. Preserve float32 data type
        dimensions = self.pixelsize * np.array(data.shape, dtype='float32')
        header[10:13] = dimensions.view('int32')

        data = data.flatten(order='F')
        if normalize:
            data = data.astype(np.float32)
            data -= data.min()
            data *= 255 / data.max()
            data = data.astype('uint8')

        with open(filename, 'wb') as f:
            header.tofile(f)
            data.tofile(f)

    @staticmethod
    def _read_tiff(filename, **kwargs):
        raise NotImplementedError

    def _write_tiff(self, filename, **kwargs):
        raise NotImplementedError

    _readers = {}
    _writers = {
        'rec': _write_rec,
        'tif': _write_tiff,
        'tiff': _write_tiff,
    }


    def layer_attributes(self, attributes={}):
        attr = super().layer_attributes(attributes)
        attr['name'] = attributes.get('name', 'Volume')
        attr['scale'] = attributes.get('pixelsize', (self.pixelsize, self.pixelsize, self.pixelsize))
        attr['colormap'] = attributes.get('colormap', 'magma')
        attr['rendering'] = attributes.get('rendering', 'attenuated_mip')
        attr['contrast_limits'] = attributes.get('contrast_limits', [0, np.max(self.data)*1.5])
        return attr



    def layer_metadata(self, metadata={}):
        meta = super().layer_metadata(metadata)
        meta['ct metadata']['type'] = TOMOBASE_DATATYPES.VOLUME.value
        meta['ct metadata']['axis'] = ['z', 'y', 'x'] if len(self.data.shape) == 3 else ['z', 'Signal', 'y', 'x']

        return meta

Phantoms

The phantoms functions are functions which generate a Volume class for sample data. Registered globally to the TOMOBASE_PHANTOMS register. To register a phantom to the library use the phantom_hook function.

get_nanocage

get_nanocage()

Creates a nanocage phantom.

Returns:
  • Volume

    The created nanocage phantom.

Source code in tomobase/phantoms/nanocage.py
 8
 9
10
11
12
13
14
15
16
17
18
@phantom_hook(name='nanocage')
def get_nanocage():
    """
    Creates a nanocage phantom.

    Returns:
        Volume: The created nanocage phantom.
    """
    path = os.path.dirname(__file__)
    path = os.path.join(path, 'nanocage.pkl')
    return Volume(pickle.load(open(path,'rb')))

get_nanorod

get_nanorod(dim: int = 512, length: int = 300, radius: int = 100, proportion: float = 0.5, intensity: float = 0.3)

Creates a nanorod phantom.

Parameters:
  • dim (int, default: 512 ) –

    The dimension of the volume. Defaults to 512.

  • length (int, default: 300 ) –

    The length of the rod. Defaults to 300.

  • radius (int, default: 100 ) –

    The radius of the rod. Defaults to 100.

  • proportion (float, default: 0.5 ) –

    The proportion of the rod's radius to its length. Defaults to 0.5.

  • intensity (float, default: 0.3 ) –

    The intensity of the rod. Defaults to 0.3.

Returns:
  • Volume

    The created nanorod phantom.

Source code in tomobase/phantoms/nanorod.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@phantom_hook(name='nanorod')
def get_nanorod(dim:int=512,length:int=300,radius:int=100,proportion:float=0.5,intensity:float=0.3):
    """Creates a nanorod phantom.

    Args:
        dim (int, optional): The dimension of the volume. Defaults to 512.
        length (int, optional): The length of the rod. Defaults to 300.
        radius (int, optional): The radius of the rod. Defaults to 100.
        proportion (float, optional): The proportion of the rod's radius to its length. Defaults to 0.5.
        intensity (float, optional): The intensity of the rod. Defaults to 0.3.

    Returns:
        Volume: The created nanorod phantom.
    """
    pradius = 2*(radius/dim)
    obj = np.zeros((dim,dim,dim),dtype=np.float32)
    img = np.zeros((dim,dim),dtype=np.float32)
    x = np.linspace(-1, 1, dim)
    y = np.linspace(-1, 1, dim)
    z = np.linspace(-1, 1, dim)

    X, Y = np.meshgrid(x, y)
    disk1= (X**2 + Y**2) <= pradius**2
    dissk2 = (X**2 + Y**2) <= (pradius*proportion)**2
    img[disk1>0] = intensity
    img[dissk2>0] = 1

    L = length - 2*radius

    z1, z2 = dim//2 - L//2, dim//2 + L//2
    for i in tqdm(range(dim), label="building nanorod slice by slice"):
        if i >= z1 and i <= z2:
            obj[:,:,i] = img

    sphere = np.zeros((dim,dim,dim),dtype=np.float32)
    X,Y,Z = np.meshgrid(x,y,z)
    sphere1 = (X**2 + Y**2 + Z**2) <= pradius**2
    a,b,c = pradius*proportion, pradius*proportion, pradius
    sphere2 = ((X**2)/(a**2) + (Y**2)/(b**2) + (Z**2)/(c**2)) <= 1
    sphere[sphere1>0] = intensity
    sphere[sphere2>0] = 1

    y1,y2 = dim//2 + radius, dim//2 - radius
    xz1,xz2 = dim//2 - radius, dim//2 + radius
    top_limit,bottom_limit  = z2+radius, z1-radius
    obj[xz1:xz2, xz1:xz2, z2:top_limit] = sphere[xz1:xz2, xz1:xz2, dim//2:y1] 
    obj[xz1:xz2, xz1:xz2, bottom_limit:z1] = sphere[xz1:xz2, xz1:xz2, y2:dim//2]
    return Volume(obj)

get_nanocube

get_nanocube(size: int = 256, dim: int = 512)

Creates a nanocube phantom.

Parameters:
  • size (int, default: 256 ) –

    The size of the cube. Defaults to 256.

  • dim (int, default: 512 ) –

    The dimension of the volume. Defaults to 512.

Returns:
  • Volume

    The created nanocube phantom.

Source code in tomobase/phantoms/nanocube.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@phantom_hook(name='nanocube')
def get_nanocube(size:int=256,dim:int=512):
    """
    Creates a nanocube phantom.

    Args:
        size (int, optional): The size of the cube. Defaults to 256.
        dim (int, optional): The dimension of the volume. Defaults to 512.

    Returns:
        Volume: The created nanocube phantom.
    """
    obj = np.zeros((dim,dim,dim),dtype=np.float32)
    start = int(dim//2-size//2)
    end = int(dim//2+size//2)
    obj[start:end,start:end,start:end] = 1
    return Volume(obj)

Tilt Schemes

This module contains classes used to implement a tilt scheme. Registered with TOMOBASE_TILTSCHEMES and added to the registration by using the decorator tiltscheme_hook.

TiltScheme

Bases: ABC

Base Class for a Tilt Schemes.

This class provides the basic structure and functionality for all tilt schemes.

Attributes:
  • index (int) –

    The current index in the tilt series.

  • _isfinished (bool) –

    Whether the tilt scheme has finished.

Source code in tomobase/tiltschemes/tiltscheme.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class TiltScheme(ABC):
    """Base Class for a Tilt Schemes.

    This class provides the basic structure and functionality for all tilt schemes.

    Attributes:
        index (int): The current index in the tilt series.
        _isfinished (bool): Whether the tilt scheme has finished.

    """
    def __init__(self):
        self.index = 0
        self._isfinished = False

    @property
    def isfinished(self):
        return self._isfinished

    @abstractmethod
    def get_angle(self):
        """Get the tilt angle for the current index.

        """
        pass

    @abstractmethod
    def get_angle_array(self, indices):
        pass  

get_angle abstractmethod

get_angle()

Get the tilt angle for the current index.

Source code in tomobase/tiltschemes/tiltscheme.py
22
23
24
25
26
27
@abstractmethod
def get_angle(self):
    """Get the tilt angle for the current index.

    """
    pass

Incremental

Bases: TiltScheme

Incremental Tilt Scheme.

Attributes:
  • angle_start (float) –

    The starting angle for the tilt series.

  • angle_end (float) –

    The ending angle for the tilt series.

  • step (float) –

    The step size for each increment.

Source code in tomobase/tiltschemes/incremental.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@tiltscheme_hook('INCREMENTAL')  
class Incremental(TiltScheme):
    """Incremental Tilt Scheme.

    Attributes:
        angle_start (float): The starting angle for the tilt series.
        angle_end (float): The ending angle for the tilt series.
        step (float): The step size for each increment.
    """
    def __init__(self, angle_start:float=-70, angle_end:float=70, step:float=2):
        super().__init__()
        self.angle_start = angle_start
        self.angle_end = angle_end  
        self.step = step

    def get_angle(self):
        angle = self.angle_start + (self.index*self.step)
        self.index += 1
        if self.angle_end > self.angle_start:
            if angle + self.step > self.angle_end:
                self._isfinished = True
        else:
            if angle + self.step < self.angle_end:
                self._isfinished = True
        return angle

Binary

Bases: TiltScheme

Binary Tilt Scheme Class. The purpose of this class is to calculate angles using the binary acquisition tilt scheme.

Attributes:
  • angle_min (float) –

    The minimum angle in the tilt series.

  • angle_max (float) –

    The maximum angle in the tilt series.

  • k (int) –

    The number of angles in one subdivision of the tiltscheme.

  • isbidirectional (bool) –

    Default is True. Determines wether the tiltscheme is calculated performed by going unidirectionally negative to positve. If true the tiltscheme will be calculated bidirectionally. The first k angles are collected from min to max but and reversed in next k angles. If false the tiltscheme will be calculated unidirectionally min to max for all sets of 8 angles.

Source code in tomobase/tiltschemes/binary.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@tiltscheme_hook('BINARY')  
class Binary(TiltScheme):
    """Binary Tilt Scheme Class.
    The purpose of this class is to calculate angles using the binary acquisition tilt scheme.


    Attributes:
        angle_min (float): The minimum angle in the tilt series.
        angle_max (float): The maximum angle in the tilt series.
        k (int): The number of angles in one subdivision of the tiltscheme.
        isbidirectional (bool): Default is True. Determines wether the tiltscheme is calculated performed by going unidirectionally negative to positve.
            If true the tiltscheme will be calculated bidirectionally. The first k angles are collected from min to max but and reversed in next k angles.
            If false the tiltscheme will be calculated unidirectionally min to max for all sets of 8 angles.
    """
    def __init__(self, angle_min:float=-70, angle_max:float=70, k:int=8, isbidirectional:bool=True):
        """Initialize the Binary Tilt Scheme.

        Args:
            angle_min (float, optional): The minimum angle in the tilt series. Defaults to -70.
            angle_max (float, optional): The maximum angle in the tilt series. Defaults to 70.
            k (int, optional): The number of angles in one subdivision of the tiltscheme. Defaults to 8.
            isbidirectional (bool, optional): Determines whether the tiltscheme is calculated bidirectionally. Defaults to True.
        """
        super().__init__()
        self.angle_max = angle_max
        self.angle_min = angle_min
        self.k = k

        #Setting parameters
        self.isbidirectional = isbidirectional
        if isbidirectional:
            self.isforward=True

        self.step = (self.angle_max - self.angle_min)/(k+0.5)
        self.i = 0
        self.offset = 0
        self.offset_set = 2
        self.offset_run = 1/self.offset_set
        self.angle = 0
        self.max_cutoff = self.angle_max - (self.step/2)

    def get_angle(self):
        if self.isbidirectional:
            return self._get_angle_bidirectional()
        else:
            return self._get_angle_unidirectional() 

    def _get_angle_bidirectional(self):
        if self.i == 0:
            self.angle = self.angle_min
        elif self.isforward:
            if np.isclose(self.angle + self.step, self.angle_max) or (self.angle+self.step) > self.angle_max:
                self._get_offsets()
                self.isforward = False
                if np.isclose(self.max_cutoff + (self.step*self.offset), self.angle_max) or (self.max_cutoff + (self.step*self.offset)) >=  self.angle_max:
                    self.angle = self.max_cutoff + (self.step*self.offset) - self.step
                else:
                    self.angle = self.max_cutoff + (self.step*self.offset)
                self.step *= -1 
            else:
                self.angle = self.angle + self.step
        else:
            if np.isclose(self.angle+self.step, self.angle_max) or (self.angle+self.step) < self.angle_min:
                self._get_offsets()
                self.isforward = True
                self.angle = self.angle_min + (np.abs(self.step)*self.offset)
                self.step *= -1
            else:
                self.angle = self.angle + self.step
        self.i += 1
        return np.round(self.angle,2)


    def _get_angle_unidirectional(self):
        if self.i == 0:
            self.angle = self.angle_min
        elif np.isclose(self.angle + self.step, self.angle_max) or (self.angle + self.step) > self.angle_max:
            self._get_offsets()
            self.angle = self.angle_min + (self.step * self.offset)
        else:
            self.angle += self.step
        self.i += 1
        return np.round(self.angle, 2)

    def _get_offsets(self):
        if (self.offset + 0.5) >= 1:
            if self.offset == ((self.offset_set-1)/(self.offset_set)):
                self.offset_set = self.offset_set*2
                self.offset_run = 1/self.offset_set
                self.offset = self.offset_run
            else:
                self.offset_run += 2/self.offset_set
                self.offset = self.offset_run
        else:
            self.offset += 0.5  

    def get_angle_array(self, indices):
        return super().get_angle_array(indices)

GRS

Bases: TiltScheme

Golden Ratio Sequence Tilt Scheme.

Attributes:
  • angle_min (float) –

    The minimum angle in the tilt series.

  • angle_max (float) –

    The maximum angle in the tilt series.

  • index (int) –

    The index to start acquisition from.

Source code in tomobase/tiltschemes/grs.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@tiltscheme_hook('GRS')  
class GRS(TiltScheme):
    """Golden Ratio Sequence Tilt Scheme.

    Attributes:
        angle_min (float): The minimum angle in the tilt series.
        angle_max (float): The maximum angle in the tilt series.
        index (int): The index to start acquisition from.
    """

    def __init__(self, angle_min:float=-70, angle_max:float=70, index:int=1):
        """Initialize the Golden Ratio Sequence Tilt Scheme.

        Args:
            angle_min (float, optional): The minimum angle in the tilt series. Defaults to -70.
            angle_max (float, optional): The maximum angle in the tilt series. Defaults to 70.
            index (int, optional): The index to start acquisition from. Defaults to 1.
        """
        super().__init__()
        self.angle_max = angle_max
        self.angle_min = angle_min
        self.range = np.radians(angle_max - angle_min)
        self.gr = (1+np.sqrt(5))/2
        self.index = index

    def get_angle(self):
        angle_rad = np.mod(self.index*self.gr*self.range, self.range) + np.radians(self.angle_min)
        self.index += 1
        return np.round(np.degrees(angle_rad),2)

Hooks

phantom_hook

phantom_hook(name: str | None = None) -> Callable

A decorator used to mark a function as a phantom. The function must return a Volume class.

Parameters:
  • name (str | None, default: None ) –

    The name of the phantom. Should be human readable casing.

Returns:
  • Callable( Callable ) –

    The decorated function.

Source code in tomobase/hooks.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def phantom_hook(name:str| None= None) -> Callable:
    #use sphynx style
    """
    A decorator used to mark a function as a phantom. The function must return a Volume class.

    Args:
        name (str | None): The name of the phantom. Should be human readable casing.

    Returns:
        Callable: The decorated function.
    """
    def decorator(func):
        hook_name = name if name is not None else func.__name__.replace('_', ' ')
        func.tomobase_name = name
        func.is_tomobase_phantom = True

        return func
    return decorator

tiltscheme_hook

tiltscheme_hook(name: str) -> Callable

A decorator used to mark a class as a tiltscheme. The class must be a child of the TiltScheme class.

:param name: the name of the tilt scheme. Should be human readable casing. :type name: str :return: the decorated class :rtype: Callable

Source code in tomobase/hooks.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def tiltscheme_hook(name: str) -> Callable:
    """
    A decorator used to mark a class as a tiltscheme. The class must be a child of the TiltScheme class.

    :param name: the name of the tilt scheme. Should be human readable casing.
    :type name: str
    :return: the decorated class
    :rtype: Callable
    """
    def decorator(cls):
        #TODO: Check if the class is a child of the TiltScheme class
        cls.tomobase_name = name
        cls.is_tomobase_tiltscheme = True
        return cls
    return decorator

tomobase_hook_process

tomobase_hook_process(**kwargs)

A decorator used to mark a function or class as a tomography process. The function or class is either a standard function or class used to define the process or a QWidget used to attach to napari. Args: name (str): the name of the process. Should be readable casing and spaces. category (enum.TransformCategory or List[enum.TransformCategory]): the category of the process. Should be a member of the TransformCategories enum. includes (list[enum.DataModules]): a list of data types that the process can handle. Either Numpy Cupy or Torch. excludes (list[enum.DataModules]): a list of strings that define the data types that the process cannot handle. Cannot define both includes and excludes subcategories (dict(enum.TransformCategory,[list[str]])): a list of strings that define the subcategories of the process. Used when adding the process to the napari menu.

Source code in tomobase/hooks.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def tomobase_hook_process(**kwargs):
    """A decorator used to mark a function or class as a tomography process. The function or class is either a standard function or class used to define the process or a QWidget used to attach to napari.
    Args:
        name (str): the name of the process. Should be readable casing and spaces.
        category (enum.TransformCategory or List[enum.TransformCategory]): the category of the process. Should be a member of the TransformCategories enum.
        includes (list[enum.DataModules]): a list of data types that the process can handle. Either Numpy Cupy or Torch.
        excludes (list[enum.DataModules]): a list of strings that define the data types that the process cannot handle. Cannot define both includes and excludes
        subcategories (dict(enum.TransformCategory,[list[str]])): a list of strings that define the subcategories of the process. Used when adding the process to the napari menu.
    """
    use_numpy = kwargs.get("use_numpy", False)
    isquantification = kwargs.get("isquantification", False)
    units = kwargs.get("units", None)
    def decorator(obj):
        if inspect.isfunction(obj):
                wrapper = _function_wrapper(obj, use_numpy, isquantification, units)
        obj = _registration(wrapper, **kwargs)
        return obj
    return decorator

Processes

rotational_misalignment

rotational_misalignment(sino: Sinogram, tilt_theta: float = 3, tilt_alpha: float = 2, backlash: float = 0.5, backlash_backwards: bool = True)

Apply a random rotational misalignment to the sinogram. Args: sino (Sinogram): The projection data tilt_theta (float): The maximum rotation angle in degrees (default: 3) tilt_alpha (float): The maximum offset in degrees (default: 2) backlash (float): The maximum backlash in degrees (default: 0.5) backlash_backwards (bool): Whether to apply the backlash backwards or forwards (default: True)

Returns:
  • sino( Sinogram ) –

    The result

  • rotations( ndarray ) –

    The rotations applied to each projection (only if extend_return is True)

Source code in tomobase/processes/image_processing/misalignments.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def rotational_misalignment(sino: Sinogram, 
                            tilt_theta:float = 3,
                            tilt_alpha:float=2, 
                            backlash:float=0.5, 
                            backlash_backwards:bool =  True):
    """ Apply a random rotational misalignment to the sinogram.
    Args:
        sino (Sinogram): The projection data
        tilt_theta (float): The maximum rotation angle in degrees (default: 3)
        tilt_alpha (float): The maximum offset in degrees (default: 2)
        backlash (float): The maximum backlash in degrees (default: 0.5)
        backlash_backwards (bool): Whether to apply the backlash backwards or forwards (default: True)

    Returns:
        sino (Sinogram): The result
        rotations (ndarray): The rotations applied to each projection (only if extend_return is True)
    """

    angles_original =  deepcopy(sino.angles)  
    rotations = xp.xupy.zeros(sino.data.shape[0])
    for i in tqdm(range(sino.data.shape[0]), label='Rotational Misalignment'):
        rotations[i] = tilt_theta * xp.xupy.random.uniform(-1, 1)
        sino.data[i, :, : ] = xp.scipy.ndimage.rotate(sino.data[i, :, :], rotations[i], reshape=False)


    for i in range(sino.data.shape[0]):
        offset = tilt_alpha * xp.xupy.random.uniform(-1, 1)
        if i > 0:
            if backlash_backwards and sino.angles[i] < sino.angles[i-1]:
                offset += backlash
            elif not backlash_backwards and sino.angles[i] > sino.angles[i-1]:
                offset += backlash
        sino.angles = sino.angles + offset

    return sino, rotations, angles_original

translational_misalignment

translational_misalignment(sino: Sinogram, offset: float = 0.25)

Apply a random translational misalignment to the sinogram. Arguments: sino (Sinogram): The projection data offset (float): The maximum offset in pixels (default: 0.25)

Returns:
  • sino( Sinogram ) –

    The result

  • shifts( ndarray ) –

    The shifts applied to each projection (only if extend_return is True)

Source code in tomobase/processes/image_processing/misalignments.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def translational_misalignment(sino: Sinogram, offset:float=0.25):
    """ Apply a random translational misalignment to the sinogram.
    Arguments:
        sino (Sinogram): The projection data
        offset (float): The maximum offset in pixels (default: 0.25)

    Returns:
        sino (Sinogram): The result
        shifts (ndarray): The shifts applied to each projection (only if extend_return is True)
    """

    shifts = xp.xupy.zeros((sino.data.shape[0], 2))
    for i in tqdm(range(sino.data.shape[0]), label='Translational Misalignment'):
        if i == 0:
            shifts[i, :] = 0
            continue
        image_offset_x = int(xp.xupy.round(sino.data.shape[1] * xp.xupy.random.uniform(-offset, offset)))
        image_offset_y = int(xp.xupy.round(sino.data.shape[2] * xp.xupy.random.uniform(-offset, offset)))
        sino.data[i, :, :] = xp.xupy.roll(sino.data[i, :, :], (image_offset_x, image_offset_y), axis=(0, 1))
        shifts[i, :] = (image_offset_x, image_offset_y)


    return sino, shifts

poisson_noise

poisson_noise(obj: Data, rescale: float = True)

Add Poisson noise to the sinogram. Args: obj (Data): The input data object rescale (float): Rescale the data to the range of the Poisson noise (default: True)

Returns:
  • Data

    The result

Source code in tomobase/processes/image_processing/misalignments.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def poisson_noise(obj: Data, 
                  rescale:float=True):
    """Add Poisson noise to the sinogram.
    Args:
        obj (Data): The input data object
        rescale (float): Rescale the data to the range of the Poisson noise (default: True)

    Returns:
        Data: The result
    """
    obj.data = obj.data*rescale
    obj.data = xp.xupy.random.poisson(obj.data)
    return obj

gaussian_filter

gaussian_filter(obj: Data, gaussian_sigma: float = 1)

Add Gaussian noise to the sinogram. Args: obj (Data): The input data object gaussian_sigma (float): Standard deviation of the Gaussian noise (default: 1) inplace (bool): Whether to do the operation in-place in the input data object (Default: True) Returns: Data: The result

Source code in tomobase/processes/image_processing/misalignments.py
13
14
15
16
17
18
19
20
21
22
23
24
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def gaussian_filter(obj: Data, gaussian_sigma:float=1,):
    """Add Gaussian noise to the sinogram.
    Args:
        obj (Data): The input data object
        gaussian_sigma (float): Standard deviation of the Gaussian noise (default: 1)
        inplace (bool): Whether to do the operation in-place in the input data object (Default: True)
    Returns:
        Data: The result
    """
    obj.data = xp.scipy.ndimage.gaussian_filter(obj.data, gaussian_sigma)
    return obj

background_subtract_median

background_subtract_median(image: Data)

Subtract the median of the sinogram from the sinogram."

Parameters:
  • image (Data) –

    The input image data

Returns:
  • Data

    The resulting image data

Source code in tomobase/processes/image_processing/background.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def background_subtract_median(image: Data):
    """Subtract the median of the sinogram from the sinogram."

    Args:
        image (Data): The input image data

    Returns:
        Data: The resulting image data
    """

    median = xp.xupy.median(image.data)
    image.data[image.data<median] = 0

    return image

pad_sinogram

pad_sinogram(sino: Sinogram, x: int = 0, y: int = 0)

Pad the sinogram to the specified size.

Parameters:
  • sino (Sinogram) –

    The projection data

  • x (int, default: 0 ) –

    The target size for the x dimension

  • y (int, default: 0 ) –

    The target size for the y dimension

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@tomobase_hook_process(name='Pad Sinogram', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def pad_sinogram(sino: Sinogram, x: int = 0, y: int = 0):
    """ Pad the sinogram to the specified size.

    Args:
        sino (Sinogram): The projection data
        x (int): The target size for the x dimension
        y (int): The target size for the y dimension

    Returns:
        Sinogram: The result
    """

    pad_x = x - sino.data.shape[-2]
    pad_y = y - sino.data.shape[-1]
    if pad_x < 0 or pad_y < 0:
        raise ValueError("Cannot pad to a smaller size")
    sino.data = xp.xupy.pad(sino.data, ( (0, 0), (pad_x // 2, pad_x // 2), (pad_y // 2, pad_y // 2)), mode='constant')

    return sino

bin

bin(obj: Data, factor: int = 2)

Bin the sinogram data by a specified factor.

Parameters:
  • sino (Sinogram) –

    The projection data

  • factor (int, default: 2 ) –

    The binning factor (default: 2)

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@tomobase_hook_process(name='Bin Data', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def bin(obj: Data, factor: int = 2):
    """Bin the sinogram data by a specified factor.

    Args:
        sino (Sinogram): The projection data
        factor (int): The binning factor (default: 2)

    Returns:
        Sinogram: The result
    """

    skipped_axis = 0
    if isinstance(obj, Sinogram):
        skipped_axis += 1
    if obj.data.ndim > obj.dim_default:
        skipped_axis += 1

    axes = range(obj.data.ndim)
    factors = [1 if (i < skipped_axis) else factor for i in axes]


     # Check divisibility
    for i, (dim, b) in enumerate(zip(obj.data.shape, factors)):
        if dim % b != 0:
            raise ValueError(f"Axis {i} size {dim} not divisible by bin factor {b}")

    # Compute new shape for reshaping
    reshaped = []
    for dim, b in zip(obj.data.shape, factors):
        reshaped.extend([dim // b, b])
    obj.data = obj.data.reshape(reshaped)

    # Compute mean over binning axes
    for i in reversed(range(obj.data.ndim // 2)):
        obj.data = obj.data.mean(axis=i * 2 + 1) 

    if not obj.pixelsize == 1.0:
        obj.pixelsize = obj.pixelsize * factor

    return obj

normalize

normalize(sino: Sinogram)

Normalize the sinogram data to the range [0, 1].

Parameters:
  • sino (Sinogram) –

    The projection data

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@tomobase_hook_process(name='Normalize', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def normalize(sino: Sinogram):
    """Normalize the sinogram data to the range [0, 1].

    Args:
        sino (Sinogram): The projection data

    Returns:
        Sinogram: The result

    """
    sino.data = (sino.data - xp.xupy.min(sino.data)) / (xp.xupy.max(sino.data) - xp.xupy.min(sino.data))
    return sino

align_sinogram_xcorr

align_sinogram_xcorr(sino: Sinogram, shifts=None)

Align the projection images using cross-correlation Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) shifts (np.ndarray): A list of shifts to apply in pixels, if None is given it will be calculated (default: None) extend_return (bool): If True, the return value will be a tuple with the shifts in the second item (default: False) Returns: Sinogram: The result shifts (xp.ndarray): The shifts in pixels

Source code in tomobase/processes/alignments/translation.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@tomobase_hook_process(name='Align Sinogram XCorrelation', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def align_sinogram_xcorr(sino: Sinogram, shifts=None):
    """Align the projection images using cross-correlation
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        shifts (np.ndarray): A list of shifts to apply in pixels, if None is given it will be calculated (default: None)
        extend_return (bool): If True, the return value will be a tuple with the shifts in the second item (default: False)
    Returns:
        Sinogram: The result
        shifts (xp.ndarray): The shifts in pixels
    """

    if shifts is None:
        shifts = xp.xupy.zeros((sino.data.shape[0], 2))
        fft_fixed = xp.xupy.fft.fft2(sino.data[0, :, :])
        for i in tqdm(range(sino.data.shape[0] - 1), label='Calculating shifts with cross-correlation'):
            fft_moving = xp.xupy.fft.fft2(sino.data[i + 1, :, :])
            xcorr = xp.xupy.fft.ifft2(xp.xupy.multiply(fft_fixed, xp.xupy.conj(fft_moving)))
            fft_fixed = fft_moving
            rel_shift = xp.xupy.asarray(xp.xupy.unravel_index(xp.xupy.argmax(xcorr), xcorr.shape))
            shifts[i + 1, :] = shifts[i, :] + rel_shift

        shifts %= xp.xupy.asarray(sino.data.shape[1:])[None, :]
        shifts = xp.xupy.rint(shifts).astype(int)

    for i in tqdm(range(sino.data.shape[0]), label='Aligning sinogram with cross-correlation'):
        sino.data[i, :, :] = xp.xupy.roll(sino.data[i, :, :], shifts[i, :], axis=(0, 1))

    return sino, shifts

align_sinogram_center_of_mass

align_sinogram_center_of_mass(sino: Sinogram)

Align the projection images using the center of mass Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False) Returns: Sinogram: The result offset (xp.ndarray): The offset in pixels

Source code in tomobase/processes/alignments/translation.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@tomobase_hook_process(name='Centre of Mass', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def align_sinogram_center_of_mass(sino: Sinogram):
    """Align the projection images using the center of mass
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False)
    Returns:
        Sinogram: The result
        offset (xp.ndarray): The offset in pixels
    """

    offset = xp.xupy.asarray(sino.data.shape[1:]) / 2 - xp.scipy.ndimage.center_of_mass(xp.xupy.sum(sino.data, axis=0))
    sino.data = xp.xupy.shift(sino.data, (0, offset[0], offset[1]))
    return sino, offset

weight_by_angle

weight_by_angle(sino: Sinogram)

Weight the sinogram by the angle Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the weights in the second item (default: False) Returns: Sinogram: The result weights (xp.ndarray): The weights in pixels

Source code in tomobase/processes/alignments/translation.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@tomobase_hook_process(name='Weight by Angle', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def weight_by_angle(sino: Sinogram):
    """Weight the sinogram by the angle
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the weights in the second item (default: False)
    Returns:
        Sinogram: The result
        weights (xp.ndarray): The weights in pixels
    """

    #Dont bother progress tracking for short processes
    indices = xp.xupy.argsort(sino.angles)
    sino.angles = sino.angles[indices]
    sino.data = sino.data[indices, :, :]
    weights = xp.xupy.ones_like(sino.angles)

    sorted_angles = copy.deepcopy(sino.angles) + 90
    n_angles = len(sorted_angles)

    for i in range(len(sorted_angles)):
        if i == 0:
            weights[i] = 0.5 * (180 - sorted_angles[n_angles - 1] + sorted_angles[i + 1])
        elif i == len(sorted_angles) - 1:
            weights[i] = 0.5 * (180 - sorted_angles[n_angles - 2] + sorted_angles[0])
        else:
            weights[i] = 0.5 * ((sorted_angles[i + 1] - sorted_angles[i]) + (sorted_angles[i] - sorted_angles[i - 1]))
    ratio = 180 / (n_angles - 1)
    weights = weights / ratio

    for i in range(sino.data.shape[0]):
        sino.data[i, :, :] = sino.data[i, :, :] * weights[i]

    return sino, weights

align_tilt_axis_rotation

align_tilt_axis_rotation(sino: Sinogram, method: str = 'fbp', angle: float = 0.0, **kwargs)

Align the tilt axis rotation of a sinogram using reprojection Args: sino (Sinogram): The projection data method (str): The reconstruction algorithm (default: 'fbp') angle (float): A pre-calculated angle in degrees, this is useful for aligning multiple sinograms simultaneously (default: None) angles (np.ndarray): A list of angles to try in degrees, if None is given it will use numpy.arange(-4, 5) (default: None) inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False) kwargs (dict): Other keyword arguments are passed to reconstruct see astra reconstruct

Returns:
  • Sinogram

    The result

  • angle( float ) –

    The angle in degrees

Source code in tomobase/processes/alignments/rotation.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@tomobase_hook_process(name='Tilt Rotation', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories, use_numpy=True)
def align_tilt_axis_rotation(sino:Sinogram, method:str='fbp', angle:float=0.0, **kwargs):
    """Align the tilt axis rotation of a sinogram using reprojection
    Args:
        sino (Sinogram): The projection data
        method (str): The reconstruction algorithm (default: 'fbp')
        angle (float): A pre-calculated angle in degrees, this is useful for aligning multiple sinograms simultaneously (default: None)
        angles (np.ndarray): A list of angles to try in degrees, if None is given it will use ``numpy.arange(-4, 5)`` (default: None)
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False)
        kwargs (dict): Other keyword arguments are passed to ``reconstruct`` see astra reconstruct

    Returns:
        Sinogram: The result
        angle (float): The angle in degrees
    """

    #TODO: Add context shifting
    angles=None


    if angle == 0.0:
        if angles is None:
            angles = np.arange(-4, 5)
        mse = np.zeros(len(angles))
        sino_rot = copy(sino)

        for i in tqdm(range(len(angles)), label='Aligning tilt axis rotation'):
            sino_rot.data = rotate(sino.data, angles[i], reshape=False)
            reproj = project(astra_reconstruct(sino_rot, method, **kwargs),
                            sino.angles)
            mse[i] = np.mean((sino_rot.data - reproj.data) ** 2)

        angle = angles[np.argmin(mse)]

    sino.data = rotate(sino.data, angle, reshape=False)

    return sino, angle

align_tilt_axis_shift

align_tilt_axis_shift(sino: Sinogram, method: str = 'fbp', offsets: float = 0.0, **kwargs)

Align the tilt axis shift of a sinogram using reprojection

Parameters:
  • sino (Sinogram) –

    The projection data

  • method (str, default: 'fbp' ) –

    The reconstruction algorithm (default: 'fbp')

  • offsets (ndarray, default: 0.0 ) –

    A list of offsets to try in pixels, if None is given it will use numpy.arange(-10, 11) (default: None)

  • offset (float) –

    A pre-calculated offset in pixels, this is useful for aligning multiple sinograms simultaneously (default: None)

  • inplace (bool) –

    Whether to do the alignment in-place in the input data object (default: True)

  • extend_return (bool) –

    If True, the return value will be a tuple with the offset in the second item (default: False)

  • kwargs (dict, default: {} ) –

    Other keyword arguments are passed to reconstruct see astra reconstruct

Returns:
  • Sinogram

    The result

  • offset( float ) –

    The offset in pixels

Source code in tomobase/processes/alignments/rotation.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@tomobase_hook_process(name='Tilt Shift', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories, use_numpy=True)
def align_tilt_axis_shift(sino: Sinogram, method:str='fbp', offsets:float=0.0, **kwargs):
    """Align the tilt axis shift of a sinogram using reprojection

    Args:
        sino (Sinogram): The projection data
        method (str): The reconstruction algorithm (default: 'fbp')
        offsets (np.ndarray): A list of offsets to try in pixels, if None is given it will use ``numpy.arange(-10, 11)`` (default: None)
        offset (float): A pre-calculated offset in pixels, this is useful for aligning multiple sinograms simultaneously (default: None)
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False)
        kwargs (dict): Other keyword arguments are passed to ``reconstruct`` see astra reconstruct

    Returns:
        Sinogram: The result
        offset (float): The offset in pixels
    """
    offset = None
    if offsets == 0.0:
        offsets = np.arange(-10, 11)
    mse = np.zeros(len(offsets))
    sino_shifted = copy(sino)

    for i in tqdm(range(len(offsets)), label='Aligning tilt axis shift'):
        sino_shifted.data = shift(sino.data, (0, offsets[i], 0))
        reproj = project(astra_reconstruct(sino_shifted, method, **kwargs), sino.angles)
        mse[i] = np.mean((sino_shifted.data - reproj.data) ** 2)
    offset = offsets[np.argmin(mse)]

    sino.data = shift(sino.data, (0, offset, 0))

    return sino, offset

alignments

align_sinogram_xcorr

align_sinogram_xcorr(sino: Sinogram, shifts=None)

Align the projection images using cross-correlation Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) shifts (np.ndarray): A list of shifts to apply in pixels, if None is given it will be calculated (default: None) extend_return (bool): If True, the return value will be a tuple with the shifts in the second item (default: False) Returns: Sinogram: The result shifts (xp.ndarray): The shifts in pixels

Source code in tomobase/processes/alignments/translation.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@tomobase_hook_process(name='Align Sinogram XCorrelation', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def align_sinogram_xcorr(sino: Sinogram, shifts=None):
    """Align the projection images using cross-correlation
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        shifts (np.ndarray): A list of shifts to apply in pixels, if None is given it will be calculated (default: None)
        extend_return (bool): If True, the return value will be a tuple with the shifts in the second item (default: False)
    Returns:
        Sinogram: The result
        shifts (xp.ndarray): The shifts in pixels
    """

    if shifts is None:
        shifts = xp.xupy.zeros((sino.data.shape[0], 2))
        fft_fixed = xp.xupy.fft.fft2(sino.data[0, :, :])
        for i in tqdm(range(sino.data.shape[0] - 1), label='Calculating shifts with cross-correlation'):
            fft_moving = xp.xupy.fft.fft2(sino.data[i + 1, :, :])
            xcorr = xp.xupy.fft.ifft2(xp.xupy.multiply(fft_fixed, xp.xupy.conj(fft_moving)))
            fft_fixed = fft_moving
            rel_shift = xp.xupy.asarray(xp.xupy.unravel_index(xp.xupy.argmax(xcorr), xcorr.shape))
            shifts[i + 1, :] = shifts[i, :] + rel_shift

        shifts %= xp.xupy.asarray(sino.data.shape[1:])[None, :]
        shifts = xp.xupy.rint(shifts).astype(int)

    for i in tqdm(range(sino.data.shape[0]), label='Aligning sinogram with cross-correlation'):
        sino.data[i, :, :] = xp.xupy.roll(sino.data[i, :, :], shifts[i, :], axis=(0, 1))

    return sino, shifts

align_sinogram_center_of_mass

align_sinogram_center_of_mass(sino: Sinogram)

Align the projection images using the center of mass Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False) Returns: Sinogram: The result offset (xp.ndarray): The offset in pixels

Source code in tomobase/processes/alignments/translation.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@tomobase_hook_process(name='Centre of Mass', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def align_sinogram_center_of_mass(sino: Sinogram):
    """Align the projection images using the center of mass
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False)
    Returns:
        Sinogram: The result
        offset (xp.ndarray): The offset in pixels
    """

    offset = xp.xupy.asarray(sino.data.shape[1:]) / 2 - xp.scipy.ndimage.center_of_mass(xp.xupy.sum(sino.data, axis=0))
    sino.data = xp.xupy.shift(sino.data, (0, offset[0], offset[1]))
    return sino, offset

weight_by_angle

weight_by_angle(sino: Sinogram)

Weight the sinogram by the angle Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the weights in the second item (default: False) Returns: Sinogram: The result weights (xp.ndarray): The weights in pixels

Source code in tomobase/processes/alignments/translation.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@tomobase_hook_process(name='Weight by Angle', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def weight_by_angle(sino: Sinogram):
    """Weight the sinogram by the angle
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the weights in the second item (default: False)
    Returns:
        Sinogram: The result
        weights (xp.ndarray): The weights in pixels
    """

    #Dont bother progress tracking for short processes
    indices = xp.xupy.argsort(sino.angles)
    sino.angles = sino.angles[indices]
    sino.data = sino.data[indices, :, :]
    weights = xp.xupy.ones_like(sino.angles)

    sorted_angles = copy.deepcopy(sino.angles) + 90
    n_angles = len(sorted_angles)

    for i in range(len(sorted_angles)):
        if i == 0:
            weights[i] = 0.5 * (180 - sorted_angles[n_angles - 1] + sorted_angles[i + 1])
        elif i == len(sorted_angles) - 1:
            weights[i] = 0.5 * (180 - sorted_angles[n_angles - 2] + sorted_angles[0])
        else:
            weights[i] = 0.5 * ((sorted_angles[i + 1] - sorted_angles[i]) + (sorted_angles[i] - sorted_angles[i - 1]))
    ratio = 180 / (n_angles - 1)
    weights = weights / ratio

    for i in range(sino.data.shape[0]):
        sino.data[i, :, :] = sino.data[i, :, :] * weights[i]

    return sino, weights

align_tilt_axis_rotation

align_tilt_axis_rotation(sino: Sinogram, method: str = 'fbp', angle: float = 0.0, **kwargs)

Align the tilt axis rotation of a sinogram using reprojection Args: sino (Sinogram): The projection data method (str): The reconstruction algorithm (default: 'fbp') angle (float): A pre-calculated angle in degrees, this is useful for aligning multiple sinograms simultaneously (default: None) angles (np.ndarray): A list of angles to try in degrees, if None is given it will use numpy.arange(-4, 5) (default: None) inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False) kwargs (dict): Other keyword arguments are passed to reconstruct see astra reconstruct

Returns:
  • Sinogram

    The result

  • angle( float ) –

    The angle in degrees

Source code in tomobase/processes/alignments/rotation.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@tomobase_hook_process(name='Tilt Rotation', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories, use_numpy=True)
def align_tilt_axis_rotation(sino:Sinogram, method:str='fbp', angle:float=0.0, **kwargs):
    """Align the tilt axis rotation of a sinogram using reprojection
    Args:
        sino (Sinogram): The projection data
        method (str): The reconstruction algorithm (default: 'fbp')
        angle (float): A pre-calculated angle in degrees, this is useful for aligning multiple sinograms simultaneously (default: None)
        angles (np.ndarray): A list of angles to try in degrees, if None is given it will use ``numpy.arange(-4, 5)`` (default: None)
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False)
        kwargs (dict): Other keyword arguments are passed to ``reconstruct`` see astra reconstruct

    Returns:
        Sinogram: The result
        angle (float): The angle in degrees
    """

    #TODO: Add context shifting
    angles=None


    if angle == 0.0:
        if angles is None:
            angles = np.arange(-4, 5)
        mse = np.zeros(len(angles))
        sino_rot = copy(sino)

        for i in tqdm(range(len(angles)), label='Aligning tilt axis rotation'):
            sino_rot.data = rotate(sino.data, angles[i], reshape=False)
            reproj = project(astra_reconstruct(sino_rot, method, **kwargs),
                            sino.angles)
            mse[i] = np.mean((sino_rot.data - reproj.data) ** 2)

        angle = angles[np.argmin(mse)]

    sino.data = rotate(sino.data, angle, reshape=False)

    return sino, angle

align_tilt_axis_shift

align_tilt_axis_shift(sino: Sinogram, method: str = 'fbp', offsets: float = 0.0, **kwargs)

Align the tilt axis shift of a sinogram using reprojection

Parameters:
  • sino (Sinogram) –

    The projection data

  • method (str, default: 'fbp' ) –

    The reconstruction algorithm (default: 'fbp')

  • offsets (ndarray, default: 0.0 ) –

    A list of offsets to try in pixels, if None is given it will use numpy.arange(-10, 11) (default: None)

  • offset (float) –

    A pre-calculated offset in pixels, this is useful for aligning multiple sinograms simultaneously (default: None)

  • inplace (bool) –

    Whether to do the alignment in-place in the input data object (default: True)

  • extend_return (bool) –

    If True, the return value will be a tuple with the offset in the second item (default: False)

  • kwargs (dict, default: {} ) –

    Other keyword arguments are passed to reconstruct see astra reconstruct

Returns:
  • Sinogram

    The result

  • offset( float ) –

    The offset in pixels

Source code in tomobase/processes/alignments/rotation.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@tomobase_hook_process(name='Tilt Shift', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories, use_numpy=True)
def align_tilt_axis_shift(sino: Sinogram, method:str='fbp', offsets:float=0.0, **kwargs):
    """Align the tilt axis shift of a sinogram using reprojection

    Args:
        sino (Sinogram): The projection data
        method (str): The reconstruction algorithm (default: 'fbp')
        offsets (np.ndarray): A list of offsets to try in pixels, if None is given it will use ``numpy.arange(-10, 11)`` (default: None)
        offset (float): A pre-calculated offset in pixels, this is useful for aligning multiple sinograms simultaneously (default: None)
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False)
        kwargs (dict): Other keyword arguments are passed to ``reconstruct`` see astra reconstruct

    Returns:
        Sinogram: The result
        offset (float): The offset in pixels
    """
    offset = None
    if offsets == 0.0:
        offsets = np.arange(-10, 11)
    mse = np.zeros(len(offsets))
    sino_shifted = copy(sino)

    for i in tqdm(range(len(offsets)), label='Aligning tilt axis shift'):
        sino_shifted.data = shift(sino.data, (0, offsets[i], 0))
        reproj = project(astra_reconstruct(sino_shifted, method, **kwargs), sino.angles)
        mse[i] = np.mean((sino_shifted.data - reproj.data) ** 2)
    offset = offsets[np.argmin(mse)]

    sino.data = shift(sino.data, (0, offset, 0))

    return sino, offset

rotation

align_tilt_axis_shift
align_tilt_axis_shift(sino: Sinogram, method: str = 'fbp', offsets: float = 0.0, **kwargs)

Align the tilt axis shift of a sinogram using reprojection

Parameters:
  • sino (Sinogram) –

    The projection data

  • method (str, default: 'fbp' ) –

    The reconstruction algorithm (default: 'fbp')

  • offsets (ndarray, default: 0.0 ) –

    A list of offsets to try in pixels, if None is given it will use numpy.arange(-10, 11) (default: None)

  • offset (float) –

    A pre-calculated offset in pixels, this is useful for aligning multiple sinograms simultaneously (default: None)

  • inplace (bool) –

    Whether to do the alignment in-place in the input data object (default: True)

  • extend_return (bool) –

    If True, the return value will be a tuple with the offset in the second item (default: False)

  • kwargs (dict, default: {} ) –

    Other keyword arguments are passed to reconstruct see astra reconstruct

Returns:
  • Sinogram

    The result

  • offset( float ) –

    The offset in pixels

Source code in tomobase/processes/alignments/rotation.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@tomobase_hook_process(name='Tilt Shift', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories, use_numpy=True)
def align_tilt_axis_shift(sino: Sinogram, method:str='fbp', offsets:float=0.0, **kwargs):
    """Align the tilt axis shift of a sinogram using reprojection

    Args:
        sino (Sinogram): The projection data
        method (str): The reconstruction algorithm (default: 'fbp')
        offsets (np.ndarray): A list of offsets to try in pixels, if None is given it will use ``numpy.arange(-10, 11)`` (default: None)
        offset (float): A pre-calculated offset in pixels, this is useful for aligning multiple sinograms simultaneously (default: None)
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False)
        kwargs (dict): Other keyword arguments are passed to ``reconstruct`` see astra reconstruct

    Returns:
        Sinogram: The result
        offset (float): The offset in pixels
    """
    offset = None
    if offsets == 0.0:
        offsets = np.arange(-10, 11)
    mse = np.zeros(len(offsets))
    sino_shifted = copy(sino)

    for i in tqdm(range(len(offsets)), label='Aligning tilt axis shift'):
        sino_shifted.data = shift(sino.data, (0, offsets[i], 0))
        reproj = project(astra_reconstruct(sino_shifted, method, **kwargs), sino.angles)
        mse[i] = np.mean((sino_shifted.data - reproj.data) ** 2)
    offset = offsets[np.argmin(mse)]

    sino.data = shift(sino.data, (0, offset, 0))

    return sino, offset
align_tilt_axis_rotation
align_tilt_axis_rotation(sino: Sinogram, method: str = 'fbp', angle: float = 0.0, **kwargs)

Align the tilt axis rotation of a sinogram using reprojection Args: sino (Sinogram): The projection data method (str): The reconstruction algorithm (default: 'fbp') angle (float): A pre-calculated angle in degrees, this is useful for aligning multiple sinograms simultaneously (default: None) angles (np.ndarray): A list of angles to try in degrees, if None is given it will use numpy.arange(-4, 5) (default: None) inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False) kwargs (dict): Other keyword arguments are passed to reconstruct see astra reconstruct

Returns:
  • Sinogram

    The result

  • angle( float ) –

    The angle in degrees

Source code in tomobase/processes/alignments/rotation.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@tomobase_hook_process(name='Tilt Rotation', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories, use_numpy=True)
def align_tilt_axis_rotation(sino:Sinogram, method:str='fbp', angle:float=0.0, **kwargs):
    """Align the tilt axis rotation of a sinogram using reprojection
    Args:
        sino (Sinogram): The projection data
        method (str): The reconstruction algorithm (default: 'fbp')
        angle (float): A pre-calculated angle in degrees, this is useful for aligning multiple sinograms simultaneously (default: None)
        angles (np.ndarray): A list of angles to try in degrees, if None is given it will use ``numpy.arange(-4, 5)`` (default: None)
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False)
        kwargs (dict): Other keyword arguments are passed to ``reconstruct`` see astra reconstruct

    Returns:
        Sinogram: The result
        angle (float): The angle in degrees
    """

    #TODO: Add context shifting
    angles=None


    if angle == 0.0:
        if angles is None:
            angles = np.arange(-4, 5)
        mse = np.zeros(len(angles))
        sino_rot = copy(sino)

        for i in tqdm(range(len(angles)), label='Aligning tilt axis rotation'):
            sino_rot.data = rotate(sino.data, angles[i], reshape=False)
            reproj = project(astra_reconstruct(sino_rot, method, **kwargs),
                            sino.angles)
            mse[i] = np.mean((sino_rot.data - reproj.data) ** 2)

        angle = angles[np.argmin(mse)]

    sino.data = rotate(sino.data, angle, reshape=False)

    return sino, angle
backlash_correct
backlash_correct(sino: Sinogram, tolerance: float = 10.0, method: str = 'bounded')

Correct the backlash of a sinogram using reprojection - Note this method is currently experimental Arguments: sino (Sinogram): The projection data tolerance (float): The maximum tolerance in degrees (default: 10.0) method (str): The optimization method to use (default: 'bounded') inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False) Returns: Sinogram: The result angle (float): The angle in degrees

Source code in tomobase/processes/alignments/rotation.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def backlash_correct(sino: Sinogram, tolerance:float= 10.0, method:str='bounded'):
    """Correct the backlash of a sinogram using reprojection -  Note this method is currently experimental
    Arguments:
        sino (Sinogram): The projection data
        tolerance (float): The maximum tolerance in degrees (default: 10.0)
        method (str): The optimization method to use (default: 'bounded')
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the angle in the second item (default: False)
    Returns:
        Sinogram: The result
        angle (float): The angle in degrees
    """

    progress = 0

    def objective_function(value, sino, indices):
        angles = copy(sino.angles)
        sino.angles[indices] += value
        reproj = project(astra_reconstruct(sino, 'fbp'), sino.angles[indices])        
        error = np.sqrt(np.mean((sino.data[indices,:,: ] - reproj.data) ** 2))
        sino.angles = angles
        logger.debug(f'Error: {error}')
        progress += 1


        return  error

    indices = np.where(np.diff(sino.angles) < 0)[0] + 1
    value = 0

    if method == 'bounded':
        result = minimize_scalar(objective_function, value, args=(sino, indices), bounds=(-tolerance, +tolerance), method=method)
    else:
        result = minimize_scalar(objective_function, value, args=(sino, indices), method=method)

    sino.angles[indices] += result.x
    logger.debug(f'Final Error: {result.fun}, Angle Shift: {result.x}')
    return sino, result.x

translation

align_sinogram_xcorr
align_sinogram_xcorr(sino: Sinogram, shifts=None)

Align the projection images using cross-correlation Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) shifts (np.ndarray): A list of shifts to apply in pixels, if None is given it will be calculated (default: None) extend_return (bool): If True, the return value will be a tuple with the shifts in the second item (default: False) Returns: Sinogram: The result shifts (xp.ndarray): The shifts in pixels

Source code in tomobase/processes/alignments/translation.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@tomobase_hook_process(name='Align Sinogram XCorrelation', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def align_sinogram_xcorr(sino: Sinogram, shifts=None):
    """Align the projection images using cross-correlation
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        shifts (np.ndarray): A list of shifts to apply in pixels, if None is given it will be calculated (default: None)
        extend_return (bool): If True, the return value will be a tuple with the shifts in the second item (default: False)
    Returns:
        Sinogram: The result
        shifts (xp.ndarray): The shifts in pixels
    """

    if shifts is None:
        shifts = xp.xupy.zeros((sino.data.shape[0], 2))
        fft_fixed = xp.xupy.fft.fft2(sino.data[0, :, :])
        for i in tqdm(range(sino.data.shape[0] - 1), label='Calculating shifts with cross-correlation'):
            fft_moving = xp.xupy.fft.fft2(sino.data[i + 1, :, :])
            xcorr = xp.xupy.fft.ifft2(xp.xupy.multiply(fft_fixed, xp.xupy.conj(fft_moving)))
            fft_fixed = fft_moving
            rel_shift = xp.xupy.asarray(xp.xupy.unravel_index(xp.xupy.argmax(xcorr), xcorr.shape))
            shifts[i + 1, :] = shifts[i, :] + rel_shift

        shifts %= xp.xupy.asarray(sino.data.shape[1:])[None, :]
        shifts = xp.xupy.rint(shifts).astype(int)

    for i in tqdm(range(sino.data.shape[0]), label='Aligning sinogram with cross-correlation'):
        sino.data[i, :, :] = xp.xupy.roll(sino.data[i, :, :], shifts[i, :], axis=(0, 1))

    return sino, shifts
align_sinogram_center_of_mass
align_sinogram_center_of_mass(sino: Sinogram)

Align the projection images using the center of mass Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False) Returns: Sinogram: The result offset (xp.ndarray): The offset in pixels

Source code in tomobase/processes/alignments/translation.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@tomobase_hook_process(name='Centre of Mass', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def align_sinogram_center_of_mass(sino: Sinogram):
    """Align the projection images using the center of mass
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the offset in the second item (default: False)
    Returns:
        Sinogram: The result
        offset (xp.ndarray): The offset in pixels
    """

    offset = xp.xupy.asarray(sino.data.shape[1:]) / 2 - xp.scipy.ndimage.center_of_mass(xp.xupy.sum(sino.data, axis=0))
    sino.data = xp.xupy.shift(sino.data, (0, offset[0], offset[1]))
    return sino, offset
weight_by_angle
weight_by_angle(sino: Sinogram)

Weight the sinogram by the angle Arguments: sino (Sinogram): The projection data inplace (bool): Whether to do the alignment in-place in the input data object (default: True) extend_return (bool): If True, the return value will be a tuple with the weights in the second item (default: False) Returns: Sinogram: The result weights (xp.ndarray): The weights in pixels

Source code in tomobase/processes/alignments/translation.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@tomobase_hook_process(name='Weight by Angle', category=TOMOBASE_TRANSFORM_CATEGORIES.ALIGN.value, subcategories=_subcategories)
def weight_by_angle(sino: Sinogram):
    """Weight the sinogram by the angle
    Arguments:
        sino (Sinogram): The projection data
        inplace (bool): Whether to do the alignment in-place in the input data object (default: True)
        extend_return (bool): If True, the return value will be a tuple with the weights in the second item (default: False)
    Returns:
        Sinogram: The result
        weights (xp.ndarray): The weights in pixels
    """

    #Dont bother progress tracking for short processes
    indices = xp.xupy.argsort(sino.angles)
    sino.angles = sino.angles[indices]
    sino.data = sino.data[indices, :, :]
    weights = xp.xupy.ones_like(sino.angles)

    sorted_angles = copy.deepcopy(sino.angles) + 90
    n_angles = len(sorted_angles)

    for i in range(len(sorted_angles)):
        if i == 0:
            weights[i] = 0.5 * (180 - sorted_angles[n_angles - 1] + sorted_angles[i + 1])
        elif i == len(sorted_angles) - 1:
            weights[i] = 0.5 * (180 - sorted_angles[n_angles - 2] + sorted_angles[0])
        else:
            weights[i] = 0.5 * ((sorted_angles[i + 1] - sorted_angles[i]) + (sorted_angles[i] - sorted_angles[i - 1]))
    ratio = 180 / (n_angles - 1)
    weights = weights / ratio

    for i in range(sino.data.shape[0]):
        sino.data[i, :, :] = sino.data[i, :, :] * weights[i]

    return sino, weights

beamdamage

beamdamage

beamdamage(volume: Volume, knock_on: float = 0.01, elastic_deform: float = 0.1, normalize: bool = True)

Apply beam damage simulation to a volume.

Parameters:
  • volume (Volume) –

    The input volume to be deformed.

  • knock_on (float, default: 0.01 ) –

    The knock-on effect strength. Defaults to 0.01.

  • elastic_deform (float, default: 0.1 ) –

    The elastic deformation strength. Defaults to 0.1.

  • normalize (bool, default: True ) –

    Whether to normalize the output. Defaults to True.

Returns:
  • Volume

    The deformed volume.

Source code in tomobase/processes/deformations/beamdamage.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@tomobase_hook_process(name='Beam Damage', category=TOMOBASE_TRANSFORM_CATEGORIES.DEFORM.value)
def beamdamage(volume: Volume, knock_on: float = 0.01, elastic_deform:float=0.1, normalize:bool=True):
    """Apply beam damage simulation to a volume.

    Args:
        volume (Volume): The input volume to be deformed.
        knock_on (float, optional): The knock-on effect strength. Defaults to 0.01.
        elastic_deform (float, optional): The elastic deformation strength. Defaults to 0.1.
        normalize (bool, optional): Whether to normalize the output. Defaults to True.

    Returns:
        Volume: The deformed volume.
    """

    volume.data = _deform(volume.data, elastic_deform, normalize)
    volume.data = _knockon(volume.data, knock_on)
    return volume

deformations

beamdamage

beamdamage
beamdamage(volume: Volume, knock_on: float = 0.01, elastic_deform: float = 0.1, normalize: bool = True)

Apply beam damage simulation to a volume.

Parameters:
  • volume (Volume) –

    The input volume to be deformed.

  • knock_on (float, default: 0.01 ) –

    The knock-on effect strength. Defaults to 0.01.

  • elastic_deform (float, default: 0.1 ) –

    The elastic deformation strength. Defaults to 0.1.

  • normalize (bool, default: True ) –

    Whether to normalize the output. Defaults to True.

Returns:
  • Volume

    The deformed volume.

Source code in tomobase/processes/deformations/beamdamage.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@tomobase_hook_process(name='Beam Damage', category=TOMOBASE_TRANSFORM_CATEGORIES.DEFORM.value)
def beamdamage(volume: Volume, knock_on: float = 0.01, elastic_deform:float=0.1, normalize:bool=True):
    """Apply beam damage simulation to a volume.

    Args:
        volume (Volume): The input volume to be deformed.
        knock_on (float, optional): The knock-on effect strength. Defaults to 0.01.
        elastic_deform (float, optional): The elastic deformation strength. Defaults to 0.1.
        normalize (bool, optional): Whether to normalize the output. Defaults to True.

    Returns:
        Volume: The deformed volume.
    """

    volume.data = _deform(volume.data, elastic_deform, normalize)
    volume.data = _knockon(volume.data, knock_on)
    return volume

forward_project

project

project(volume: Volume, angles: ndarray, use_gpu: bool = True)

Create a sinogram from a volume using forward projection. The GPU Context is overriden due to underlying astra gpu usage. Args: volume (Volume): The input volume to be projected. angles (np.array): The angles at which to project the volume. use_gpu (bool): Whether to use GPU for projection. Default is True. Returns: Sinogram: The resulting sinogram.

Source code in tomobase/processes/forward_project.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@tomobase_hook_process(name='Project', category=TOMOBASE_TRANSFORM_CATEGORIES.PROJECT.value, use_numpy=True)
def project(volume:Volume, angles:np.ndarray, use_gpu:bool=True):
    """Create a sinogram from a volume using forward projection. The GPU Context is overriden due to underlying astra gpu usage. 
    Args:
        volume (Volume): The input volume to be projected.
        angles (np.array): The angles at which to project the volume.
        use_gpu (bool): Whether to use GPU for projection. Default is True.
    Returns:
        Sinogram: The resulting sinogram.

    """
    data = np.transpose(volume.data, (2, 1, 0))  # ASTRA expects (z, y, x)
    angles = np.asarray(angles)
    use_gpu = use_gpu and astra.use_cuda()

    z, y, x = data.shape
    proj_id = _create_projector(x, y, angles, use_gpu)

    sino = np.empty((z, len(angles), max(x, y)))
    for i in trange(z, label="Forward projecting"):
        sino_id, sino[i, :, :] = astra.creators.create_sino(data[i, :, :], proj_id)
        astra.astra.delete(sino_id)

    sinogram = Sinogram(np.transpose(sino, (1,0,2)), angles, volume.pixelsize)  # ASTRA gives (z, n, d)
    astra.astra.delete(proj_id)
    return sinogram

image_processing

rotational_misalignment

rotational_misalignment(sino: Sinogram, tilt_theta: float = 3, tilt_alpha: float = 2, backlash: float = 0.5, backlash_backwards: bool = True)

Apply a random rotational misalignment to the sinogram. Args: sino (Sinogram): The projection data tilt_theta (float): The maximum rotation angle in degrees (default: 3) tilt_alpha (float): The maximum offset in degrees (default: 2) backlash (float): The maximum backlash in degrees (default: 0.5) backlash_backwards (bool): Whether to apply the backlash backwards or forwards (default: True)

Returns:
  • sino( Sinogram ) –

    The result

  • rotations( ndarray ) –

    The rotations applied to each projection (only if extend_return is True)

Source code in tomobase/processes/image_processing/misalignments.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def rotational_misalignment(sino: Sinogram, 
                            tilt_theta:float = 3,
                            tilt_alpha:float=2, 
                            backlash:float=0.5, 
                            backlash_backwards:bool =  True):
    """ Apply a random rotational misalignment to the sinogram.
    Args:
        sino (Sinogram): The projection data
        tilt_theta (float): The maximum rotation angle in degrees (default: 3)
        tilt_alpha (float): The maximum offset in degrees (default: 2)
        backlash (float): The maximum backlash in degrees (default: 0.5)
        backlash_backwards (bool): Whether to apply the backlash backwards or forwards (default: True)

    Returns:
        sino (Sinogram): The result
        rotations (ndarray): The rotations applied to each projection (only if extend_return is True)
    """

    angles_original =  deepcopy(sino.angles)  
    rotations = xp.xupy.zeros(sino.data.shape[0])
    for i in tqdm(range(sino.data.shape[0]), label='Rotational Misalignment'):
        rotations[i] = tilt_theta * xp.xupy.random.uniform(-1, 1)
        sino.data[i, :, : ] = xp.scipy.ndimage.rotate(sino.data[i, :, :], rotations[i], reshape=False)


    for i in range(sino.data.shape[0]):
        offset = tilt_alpha * xp.xupy.random.uniform(-1, 1)
        if i > 0:
            if backlash_backwards and sino.angles[i] < sino.angles[i-1]:
                offset += backlash
            elif not backlash_backwards and sino.angles[i] > sino.angles[i-1]:
                offset += backlash
        sino.angles = sino.angles + offset

    return sino, rotations, angles_original

translational_misalignment

translational_misalignment(sino: Sinogram, offset: float = 0.25)

Apply a random translational misalignment to the sinogram. Arguments: sino (Sinogram): The projection data offset (float): The maximum offset in pixels (default: 0.25)

Returns:
  • sino( Sinogram ) –

    The result

  • shifts( ndarray ) –

    The shifts applied to each projection (only if extend_return is True)

Source code in tomobase/processes/image_processing/misalignments.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def translational_misalignment(sino: Sinogram, offset:float=0.25):
    """ Apply a random translational misalignment to the sinogram.
    Arguments:
        sino (Sinogram): The projection data
        offset (float): The maximum offset in pixels (default: 0.25)

    Returns:
        sino (Sinogram): The result
        shifts (ndarray): The shifts applied to each projection (only if extend_return is True)
    """

    shifts = xp.xupy.zeros((sino.data.shape[0], 2))
    for i in tqdm(range(sino.data.shape[0]), label='Translational Misalignment'):
        if i == 0:
            shifts[i, :] = 0
            continue
        image_offset_x = int(xp.xupy.round(sino.data.shape[1] * xp.xupy.random.uniform(-offset, offset)))
        image_offset_y = int(xp.xupy.round(sino.data.shape[2] * xp.xupy.random.uniform(-offset, offset)))
        sino.data[i, :, :] = xp.xupy.roll(sino.data[i, :, :], (image_offset_x, image_offset_y), axis=(0, 1))
        shifts[i, :] = (image_offset_x, image_offset_y)


    return sino, shifts

poisson_noise

poisson_noise(obj: Data, rescale: float = True)

Add Poisson noise to the sinogram. Args: obj (Data): The input data object rescale (float): Rescale the data to the range of the Poisson noise (default: True)

Returns:
  • Data

    The result

Source code in tomobase/processes/image_processing/misalignments.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def poisson_noise(obj: Data, 
                  rescale:float=True):
    """Add Poisson noise to the sinogram.
    Args:
        obj (Data): The input data object
        rescale (float): Rescale the data to the range of the Poisson noise (default: True)

    Returns:
        Data: The result
    """
    obj.data = obj.data*rescale
    obj.data = xp.xupy.random.poisson(obj.data)
    return obj

gaussian_filter

gaussian_filter(obj: Data, gaussian_sigma: float = 1)

Add Gaussian noise to the sinogram. Args: obj (Data): The input data object gaussian_sigma (float): Standard deviation of the Gaussian noise (default: 1) inplace (bool): Whether to do the operation in-place in the input data object (Default: True) Returns: Data: The result

Source code in tomobase/processes/image_processing/misalignments.py
13
14
15
16
17
18
19
20
21
22
23
24
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def gaussian_filter(obj: Data, gaussian_sigma:float=1,):
    """Add Gaussian noise to the sinogram.
    Args:
        obj (Data): The input data object
        gaussian_sigma (float): Standard deviation of the Gaussian noise (default: 1)
        inplace (bool): Whether to do the operation in-place in the input data object (Default: True)
    Returns:
        Data: The result
    """
    obj.data = xp.scipy.ndimage.gaussian_filter(obj.data, gaussian_sigma)
    return obj

background_subtract_median

background_subtract_median(image: Data)

Subtract the median of the sinogram from the sinogram."

Parameters:
  • image (Data) –

    The input image data

Returns:
  • Data

    The resulting image data

Source code in tomobase/processes/image_processing/background.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def background_subtract_median(image: Data):
    """Subtract the median of the sinogram from the sinogram."

    Args:
        image (Data): The input image data

    Returns:
        Data: The resulting image data
    """

    median = xp.xupy.median(image.data)
    image.data[image.data<median] = 0

    return image

pad_sinogram

pad_sinogram(sino: Sinogram, x: int = 0, y: int = 0)

Pad the sinogram to the specified size.

Parameters:
  • sino (Sinogram) –

    The projection data

  • x (int, default: 0 ) –

    The target size for the x dimension

  • y (int, default: 0 ) –

    The target size for the y dimension

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@tomobase_hook_process(name='Pad Sinogram', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def pad_sinogram(sino: Sinogram, x: int = 0, y: int = 0):
    """ Pad the sinogram to the specified size.

    Args:
        sino (Sinogram): The projection data
        x (int): The target size for the x dimension
        y (int): The target size for the y dimension

    Returns:
        Sinogram: The result
    """

    pad_x = x - sino.data.shape[-2]
    pad_y = y - sino.data.shape[-1]
    if pad_x < 0 or pad_y < 0:
        raise ValueError("Cannot pad to a smaller size")
    sino.data = xp.xupy.pad(sino.data, ( (0, 0), (pad_x // 2, pad_x // 2), (pad_y // 2, pad_y // 2)), mode='constant')

    return sino

bin

bin(obj: Data, factor: int = 2)

Bin the sinogram data by a specified factor.

Parameters:
  • sino (Sinogram) –

    The projection data

  • factor (int, default: 2 ) –

    The binning factor (default: 2)

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@tomobase_hook_process(name='Bin Data', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def bin(obj: Data, factor: int = 2):
    """Bin the sinogram data by a specified factor.

    Args:
        sino (Sinogram): The projection data
        factor (int): The binning factor (default: 2)

    Returns:
        Sinogram: The result
    """

    skipped_axis = 0
    if isinstance(obj, Sinogram):
        skipped_axis += 1
    if obj.data.ndim > obj.dim_default:
        skipped_axis += 1

    axes = range(obj.data.ndim)
    factors = [1 if (i < skipped_axis) else factor for i in axes]


     # Check divisibility
    for i, (dim, b) in enumerate(zip(obj.data.shape, factors)):
        if dim % b != 0:
            raise ValueError(f"Axis {i} size {dim} not divisible by bin factor {b}")

    # Compute new shape for reshaping
    reshaped = []
    for dim, b in zip(obj.data.shape, factors):
        reshaped.extend([dim // b, b])
    obj.data = obj.data.reshape(reshaped)

    # Compute mean over binning axes
    for i in reversed(range(obj.data.ndim // 2)):
        obj.data = obj.data.mean(axis=i * 2 + 1) 

    if not obj.pixelsize == 1.0:
        obj.pixelsize = obj.pixelsize * factor

    return obj

normalize

normalize(sino: Sinogram)

Normalize the sinogram data to the range [0, 1].

Parameters:
  • sino (Sinogram) –

    The projection data

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@tomobase_hook_process(name='Normalize', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def normalize(sino: Sinogram):
    """Normalize the sinogram data to the range [0, 1].

    Args:
        sino (Sinogram): The projection data

    Returns:
        Sinogram: The result

    """
    sino.data = (sino.data - xp.xupy.min(sino.data)) / (xp.xupy.max(sino.data) - xp.xupy.min(sino.data))
    return sino

background

background_subtract_median
background_subtract_median(image: Data)

Subtract the median of the sinogram from the sinogram."

Parameters:
  • image (Data) –

    The input image data

Returns:
  • Data

    The resulting image data

Source code in tomobase/processes/image_processing/background.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def background_subtract_median(image: Data):
    """Subtract the median of the sinogram from the sinogram."

    Args:
        image (Data): The input image data

    Returns:
        Data: The resulting image data
    """

    median = xp.xupy.median(image.data)
    image.data[image.data<median] = 0

    return image

misalignments

gaussian_filter
gaussian_filter(obj: Data, gaussian_sigma: float = 1)

Add Gaussian noise to the sinogram. Args: obj (Data): The input data object gaussian_sigma (float): Standard deviation of the Gaussian noise (default: 1) inplace (bool): Whether to do the operation in-place in the input data object (Default: True) Returns: Data: The result

Source code in tomobase/processes/image_processing/misalignments.py
13
14
15
16
17
18
19
20
21
22
23
24
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def gaussian_filter(obj: Data, gaussian_sigma:float=1,):
    """Add Gaussian noise to the sinogram.
    Args:
        obj (Data): The input data object
        gaussian_sigma (float): Standard deviation of the Gaussian noise (default: 1)
        inplace (bool): Whether to do the operation in-place in the input data object (Default: True)
    Returns:
        Data: The result
    """
    obj.data = xp.scipy.ndimage.gaussian_filter(obj.data, gaussian_sigma)
    return obj
poisson_noise
poisson_noise(obj: Data, rescale: float = True)

Add Poisson noise to the sinogram. Args: obj (Data): The input data object rescale (float): Rescale the data to the range of the Poisson noise (default: True)

Returns:
  • Data

    The result

Source code in tomobase/processes/image_processing/misalignments.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def poisson_noise(obj: Data, 
                  rescale:float=True):
    """Add Poisson noise to the sinogram.
    Args:
        obj (Data): The input data object
        rescale (float): Rescale the data to the range of the Poisson noise (default: True)

    Returns:
        Data: The result
    """
    obj.data = obj.data*rescale
    obj.data = xp.xupy.random.poisson(obj.data)
    return obj
translational_misalignment
translational_misalignment(sino: Sinogram, offset: float = 0.25)

Apply a random translational misalignment to the sinogram. Arguments: sino (Sinogram): The projection data offset (float): The maximum offset in pixels (default: 0.25)

Returns:
  • sino( Sinogram ) –

    The result

  • shifts( ndarray ) –

    The shifts applied to each projection (only if extend_return is True)

Source code in tomobase/processes/image_processing/misalignments.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def translational_misalignment(sino: Sinogram, offset:float=0.25):
    """ Apply a random translational misalignment to the sinogram.
    Arguments:
        sino (Sinogram): The projection data
        offset (float): The maximum offset in pixels (default: 0.25)

    Returns:
        sino (Sinogram): The result
        shifts (ndarray): The shifts applied to each projection (only if extend_return is True)
    """

    shifts = xp.xupy.zeros((sino.data.shape[0], 2))
    for i in tqdm(range(sino.data.shape[0]), label='Translational Misalignment'):
        if i == 0:
            shifts[i, :] = 0
            continue
        image_offset_x = int(xp.xupy.round(sino.data.shape[1] * xp.xupy.random.uniform(-offset, offset)))
        image_offset_y = int(xp.xupy.round(sino.data.shape[2] * xp.xupy.random.uniform(-offset, offset)))
        sino.data[i, :, :] = xp.xupy.roll(sino.data[i, :, :], (image_offset_x, image_offset_y), axis=(0, 1))
        shifts[i, :] = (image_offset_x, image_offset_y)


    return sino, shifts
rotational_misalignment
rotational_misalignment(sino: Sinogram, tilt_theta: float = 3, tilt_alpha: float = 2, backlash: float = 0.5, backlash_backwards: bool = True)

Apply a random rotational misalignment to the sinogram. Args: sino (Sinogram): The projection data tilt_theta (float): The maximum rotation angle in degrees (default: 3) tilt_alpha (float): The maximum offset in degrees (default: 2) backlash (float): The maximum backlash in degrees (default: 0.5) backlash_backwards (bool): Whether to apply the backlash backwards or forwards (default: True)

Returns:
  • sino( Sinogram ) –

    The result

  • rotations( ndarray ) –

    The rotations applied to each projection (only if extend_return is True)

Source code in tomobase/processes/image_processing/misalignments.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@tomobase_hook_process(category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def rotational_misalignment(sino: Sinogram, 
                            tilt_theta:float = 3,
                            tilt_alpha:float=2, 
                            backlash:float=0.5, 
                            backlash_backwards:bool =  True):
    """ Apply a random rotational misalignment to the sinogram.
    Args:
        sino (Sinogram): The projection data
        tilt_theta (float): The maximum rotation angle in degrees (default: 3)
        tilt_alpha (float): The maximum offset in degrees (default: 2)
        backlash (float): The maximum backlash in degrees (default: 0.5)
        backlash_backwards (bool): Whether to apply the backlash backwards or forwards (default: True)

    Returns:
        sino (Sinogram): The result
        rotations (ndarray): The rotations applied to each projection (only if extend_return is True)
    """

    angles_original =  deepcopy(sino.angles)  
    rotations = xp.xupy.zeros(sino.data.shape[0])
    for i in tqdm(range(sino.data.shape[0]), label='Rotational Misalignment'):
        rotations[i] = tilt_theta * xp.xupy.random.uniform(-1, 1)
        sino.data[i, :, : ] = xp.scipy.ndimage.rotate(sino.data[i, :, :], rotations[i], reshape=False)


    for i in range(sino.data.shape[0]):
        offset = tilt_alpha * xp.xupy.random.uniform(-1, 1)
        if i > 0:
            if backlash_backwards and sino.angles[i] < sino.angles[i-1]:
                offset += backlash
            elif not backlash_backwards and sino.angles[i] > sino.angles[i-1]:
                offset += backlash
        sino.angles = sino.angles + offset

    return sino, rotations, angles_original

scaling

normalize
normalize(sino: Sinogram)

Normalize the sinogram data to the range [0, 1].

Parameters:
  • sino (Sinogram) –

    The projection data

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@tomobase_hook_process(name='Normalize', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def normalize(sino: Sinogram):
    """Normalize the sinogram data to the range [0, 1].

    Args:
        sino (Sinogram): The projection data

    Returns:
        Sinogram: The result

    """
    sino.data = (sino.data - xp.xupy.min(sino.data)) / (xp.xupy.max(sino.data) - xp.xupy.min(sino.data))
    return sino
bin
bin(obj: Data, factor: int = 2)

Bin the sinogram data by a specified factor.

Parameters:
  • sino (Sinogram) –

    The projection data

  • factor (int, default: 2 ) –

    The binning factor (default: 2)

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@tomobase_hook_process(name='Bin Data', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def bin(obj: Data, factor: int = 2):
    """Bin the sinogram data by a specified factor.

    Args:
        sino (Sinogram): The projection data
        factor (int): The binning factor (default: 2)

    Returns:
        Sinogram: The result
    """

    skipped_axis = 0
    if isinstance(obj, Sinogram):
        skipped_axis += 1
    if obj.data.ndim > obj.dim_default:
        skipped_axis += 1

    axes = range(obj.data.ndim)
    factors = [1 if (i < skipped_axis) else factor for i in axes]


     # Check divisibility
    for i, (dim, b) in enumerate(zip(obj.data.shape, factors)):
        if dim % b != 0:
            raise ValueError(f"Axis {i} size {dim} not divisible by bin factor {b}")

    # Compute new shape for reshaping
    reshaped = []
    for dim, b in zip(obj.data.shape, factors):
        reshaped.extend([dim // b, b])
    obj.data = obj.data.reshape(reshaped)

    # Compute mean over binning axes
    for i in reversed(range(obj.data.ndim // 2)):
        obj.data = obj.data.mean(axis=i * 2 + 1) 

    if not obj.pixelsize == 1.0:
        obj.pixelsize = obj.pixelsize * factor

    return obj
pad_sinogram
pad_sinogram(sino: Sinogram, x: int = 0, y: int = 0)

Pad the sinogram to the specified size.

Parameters:
  • sino (Sinogram) –

    The projection data

  • x (int, default: 0 ) –

    The target size for the x dimension

  • y (int, default: 0 ) –

    The target size for the y dimension

Returns:
  • Sinogram

    The result

Source code in tomobase/processes/image_processing/scaling.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@tomobase_hook_process(name='Pad Sinogram', category=TOMOBASE_TRANSFORM_CATEGORIES.IMAGE_PROCESSING.value, subcategories=_subcategories)
def pad_sinogram(sino: Sinogram, x: int = 0, y: int = 0):
    """ Pad the sinogram to the specified size.

    Args:
        sino (Sinogram): The projection data
        x (int): The target size for the x dimension
        y (int): The target size for the y dimension

    Returns:
        Sinogram: The result
    """

    pad_x = x - sino.data.shape[-2]
    pad_y = y - sino.data.shape[-1]
    if pad_x < 0 or pad_y < 0:
        raise ValueError("Cannot pad to a smaller size")
    sino.data = xp.xupy.pad(sino.data, ( (0, 0), (pad_x // 2, pad_x // 2), (pad_y // 2, pad_y // 2)), mode='constant')

    return sino
crop_sinogram
crop_sinogram(sino: Sinogram, x: int = 0, y: int = 0)

Crop the sinogram to the specified size.

Parameters: sino (Sinogram): Input sinogram to be cropped. x (int): Target size for the x dimension. y (int): Target size for the y dimension. inplace (bool): Whether to modify the array in place or return a new array.

Returns: Sinogram: Cropped sinogram.

Source code in tomobase/processes/image_processing/scaling.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def crop_sinogram(sino: Sinogram, x: int = 0, y: int = 0):
    """
    Crop the sinogram to the specified size.

    Parameters:
    sino (Sinogram): Input sinogram to be cropped.
    x (int): Target size for the x dimension.
    y (int): Target size for the y dimension.
    inplace (bool): Whether to modify the array in place or return a new array.

    Returns:
    Sinogram: Cropped sinogram.
    """
    crop_x = sino.data.shape[-2] - x
    crop_y = sino.data.shape[-1] - y
    if crop_x < 0 or crop_y < 0:
        raise ValueError("Cannot crop to a smaller size")
    sino.data = sino.data[:, :, crop_x // 2:-crop_x // 2, crop_y // 2:-crop_y // 2]

    return sino

reconstruct

optomo_reconstruct

optomo_reconstruct(sino: Sinogram, iterations: int = 0, use_gpu: bool = True, weighted: bool = False)

Reconstruct a volume from a given sinogram using SIRT ASTRA. Allows for projections to be weighted by angular distribution. Arguments: sino (Sinogram): The projection data iterations (int): The number of iterations when using an iterative reconstructor, leaving this at None will select the default number of iterations for the given algorithm (default: None) use_gpu (bool): Use a GPU if it is available (default: True) weighted (bool): Use a weighted backprojection (default: False)

Returns:
  • Volume

    The reconstructed volume

Source code in tomobase/processes/reconstruct.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@tomobase_hook_process(name='OpTomo', category=TOMOBASE_TRANSFORM_CATEGORIES.RECONSTRUCT.value, use_numpy=True)
def optomo_reconstruct(sino:Sinogram, iterations:int=0, use_gpu:bool=True, weighted:bool=False):
    """Reconstruct a volume from a given sinogram using SIRT ASTRA. Allows for projections to be weighted by angular distribution.
    Arguments:
        sino (Sinogram): The projection data
        iterations (int):
            The number of iterations when using an iterative reconstructor,
            leaving this at None will select the default number of iterations
            for the given algorithm (default: None)
        use_gpu (bool): Use a GPU if it is available (default: True)
        weighted (bool): Use a weighted backprojection (default: False)

    Returns:    
        Volume: The reconstructed volume
    """
    indices = np.argsort(sino.angles)
    sino.angles = sino.angles[indices]
    data = sino.data[indices, : ,:]
    data = np.transpose(data, (1,0,2)) # ASTRA expects (z, n, d)

    weights= np.ones_like(sino.angles)
    if weighted == True:
        sorted_angles = deepcopy(sino.angles) +  90
        n_angles = len(sorted_angles)
        for i in range(len(sorted_angles)):
            if i == 0:
                weights[i] = 0.5*(180 - sorted_angles[n_angles-1] + sorted_angles[i+1])
            elif i == len(sorted_angles)-1:
                weights[i] = 0.5*(180 - sorted_angles[n_angles-2] + sorted_angles[0])
            else:
                weights[i] = 0.5*((sorted_angles[i+1] - sorted_angles[i]) + (sorted_angles[i] - sorted_angles[i-1]))
        ratio = 180/(n_angles-1)
        weights = weights/ratio


    use_gpu = use_gpu and astra.use_cuda()

    if iterations is None:
        iterations = _get_default_iterations('sirt')

    z, n, d = data.shape
    proj_id = _create_projector(d, d, sino.angles, use_gpu)


    vol = np.zeros((z, d, d))
    default_mask = _circle_mask(d)

    mask = None
    if mask is None:
        mask = np.ones((z, d, d), dtype=bool)
    else:
        mask = np.transpose(mask, (2, 1, 0))  # ASTRA expects (z, y, x)

    W = astra.OpTomo(proj_id)
    domain_shape = np.ones((d, d))
    range_shape = np.ones((n, d))
    R = np.reshape(1/(W*domain_shape), (n, d))
    C = np.reshape(1/(W.T*range_shape), (d, d))
    R = np.minimum(R, 1 / 10**-6)
    C = np.minimum(C, 1 / 10**-6)



    for i in trange(z, label='Reconstruction Slice'):
        for j in trange(iterations,label='Reconstruction Iteration'):
            A = np.transpose(np.transpose(np.reshape(W*vol[i, :, :], (n, d)), (1,0))*weights,(1,0)) 
            B = np.transpose(np.reshape(np.transpose(data[i, :, :],(1,0))*weights,(d,n)), (1,0))
            D = R*(B - A)
            vol[i, :, :] += C*np.reshape(W.T*D,(d,d))
            vol[i, :, :] = np.reshape(np.minimum(vol[i, :, :], data.max()), (d, d))
            vol[i, :, :] = np.reshape(np.maximum(vol[i, :, :], 0), (d, d))
            vol[i, :, :] = vol[i, :, :] * (default_mask & mask[i, :, :])

    volume = Volume(np.transpose(vol, (2, 1,0)), sino.pixelsize)  # ASTRA gives (z, y, x)
    astra.astra.delete(proj_id)
    logger.info('type of volume: ' + str(type(volume)))
    return volume

astra_reconstruct

astra_reconstruct(sino: Sinogram, method: str = 'sirt', iterations: int = 0, use_gpu: bool = True)

Reconstruct a volume from a given sinogram.

Returns:
  • Volume The reconstructed volume

Source code in tomobase/processes/reconstruct.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
@tomobase_hook_process(name='Astra', category=TOMOBASE_TRANSFORM_CATEGORIES.RECONSTRUCT.value, use_numpy=True)
def astra_reconstruct(sino:Sinogram, method:str='sirt', iterations:int=0, use_gpu:bool=True):
    """Reconstruct a volume from a given sinogram.

    Arguments:
        sinogram (Sinogram)
            The projection data
        method (str)
            The reconstruction algorithm; supported algorithms are: `'bp'`,
            `'fbp'`, `'sirt'`, `'em'`, `'sart'` and `'cgls'`
        iterations (int)
            The number of iterations when using an iterative reconstructor,
            leaving this at None will select the default number of iterations
            for the given algorithm (default: None)
        use_gpu (bool)
            Use a GPU if it is available (default: True)
        mask (numpy.ndarray)
            Boolean mask that indicates which voxels should be used in the
            reconstruction (default: None)

    Returns:
        Volume
            The reconstructed volume
    """
    logger.info('Reconstructing...')
    data = np.transpose(sino.data, (1,0,2))  # ASTRA expects (z, n, d)
    use_gpu = use_gpu and astra.use_cuda()

    method = method.upper()
    if use_gpu:
        method += '_CUDA'

    # EM is not yet supported on the CPU
    if method == 'EM':
        raise NotImplementedError("The EM method is not yet supported on the CPU.")

    if iterations is None:
        iterations = _get_default_iterations(method)

    z, n, d = data.shape
    proj_id = _create_projector(d, d, sino.angles, use_gpu)

    message = f"Reconstruction using the {method} algorithm on the {'GPU' if use_gpu else 'CPU'}..."
    logger.info(message)


    vol = np.empty((z, d, d))
    default_mask = _circle_mask(d)

    mask = None
    if mask is None:
        mask = np.ones((z, d, d), dtype=bool)
    else:
        mask = np.transpose(mask, (2, 1, 0))  # ASTRA expects (z, y, x)


    for i in trange(z, label='Reconstruction Slice'):
        vol_id, vol[i, :, :] = astra.creators.create_reconstruction(
            method, proj_id, data[i, :, :], iterations,
            use_minc='yes', minc=0.0,           # min is zero
            use_maxc='yes', maxc=data.max(),    # max voxel can't be larger than max from sino
            use_mask='yes', mask=default_mask & mask[i, :, :],
        )
        astra.astra.delete(vol_id)


    volume = Volume(np.transpose(vol, (2, 1, 0)), sino.pixelsize)  # ASTRA gives (z, y, x)
    astra.astra.delete(proj_id)
    logger.info('type of volume: ' + str(type(volume)))
    return volume