csi_images.csi_frames

Contains the Frame class, which represents a single frame of an image. The Frame class does not hold the image data, but allows for easy loading of the image data from the appropriate file. This module also contains functions for creating RGB and RGBW composite images from a tile and a set of channels.

  1"""
  2Contains the Frame class, which represents a single frame of an image. The Frame class
  3does not hold the image data, but allows for easy loading of the image data from the
  4appropriate file. This module also contains functions for creating RGB and RGBW
  5composite images from a tile and a set of channels.
  6"""
  7
  8import os
  9import typing
 10
 11import tifffile
 12import numpy as np
 13
 14from csi_images.csi_scans import Scan
 15from csi_images.csi_tiles import Tile
 16from csi_images import csi_image_utils
 17
 18
 19class Frame:
 20    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
 21        self.scan = scan
 22        self.tile = tile
 23        if isinstance(channel, int):
 24            self.channel = channel
 25            if self.channel < 0 or self.channel >= len(scan.channels):
 26                raise ValueError(
 27                    f"Channel index {self.channel} is out of bounds for scan."
 28                )
 29        elif isinstance(channel, str):
 30            self.channel = self.scan.get_channel_indices([channel])
 31
 32    def __repr__(self) -> str:
 33        return f"{self.scan.slide_id}-{self.tile.n}-{self.scan.channels[self.channel].name}"
 34
 35    def __eq__(self, other) -> bool:
 36        return self.__repr__() == other.__repr__()
 37
 38    def get_file_path(
 39        self, input_path: str = None, file_extension: str = ".tif"
 40    ) -> str:
 41        """
 42        Get the file path for the frame, optionally changing
 43        the scan path and file extension.
 44        :param input_path: the path to the scan's directory. If None, defaults to
 45                           the path loaded in the frame's tile's scan object.
 46        :param file_extension: the image file extension. Defaults to .tif.
 47        :return: the file path.
 48        """
 49        if input_path is None:
 50            input_path = self.scan.path
 51            if len(self.scan.roi) > 1:
 52                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
 53        # Remove trailing slashes
 54        if input_path[-1] == os.sep:
 55            input_path = input_path[:-1]
 56        # Append proc if it's pointing to the base bzScanner directory
 57        if input_path.endswith("bzScanner"):
 58            input_path = os.path.join(input_path, "proc")
 59        # Should be a directory; append the file name
 60        if os.path.isdir(input_path):
 61            input_path = os.path.join(input_path, self.get_file_name())
 62        else:
 63            raise ValueError(f"Input path {input_path} is not a directory.")
 64        return input_path
 65
 66    def get_file_name(self, file_extension: str = ".tif") -> str:
 67        """
 68        Get the file name for the frame, handling different name conventions by scanner.
 69        :param file_extension: the image file extension. Defaults to .tif.
 70        :return: the file name.
 71        """
 72        if self.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
 73            channel_name = self.scan.channels[self.channel].name
 74            x = self.tile.x
 75            y = self.tile.y
 76            file_name = f"{channel_name}-X{x:03}-Y{y:03}{file_extension}"
 77        elif self.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
 78            channel_name = self.scan.channels[self.channel].name
 79            real_channel_index = list(self.scan.BZSCANNER_CHANNEL_MAP.values()).index(
 80                channel_name
 81            )
 82            total_tiles = self.scan.roi[0].tile_rows * self.scan.roi[0].tile_cols
 83            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
 84            n_bzscanner = self.tile.n + tile_offset
 85            file_name = f"Tile{n_bzscanner:06}{file_extension}"
 86        else:
 87            raise ValueError(f"Scanner {self.scan.scanner_id} not supported.")
 88        return file_name
 89
 90    def get_image(self, input_path: str = None) -> np.ndarray:
 91        """
 92        Loads the image for this frame. Handles .tif (will return 16-bit images) and
 93        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
 94        .jpg/.jpeg images (compressed, using .tags files).
 95        :param input_path: the path to the scan's directory. If None, defaults to
 96                           the path loaded in the frame's tile's scan object.
 97        :return: the array representing the image.
 98        """
 99
100        file_path = self.get_file_path(input_path)
101
102        # Check for the file
103        if not os.path.exists(file_path):
104            # Alternative: could be a .jpg/.jpeg file, test both
105            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
106            if os.path.exists(jpeg_path):
107                file_path = jpeg_path
108            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
109            if os.path.exists(jpeg_path):
110                file_path = jpeg_path
111            # If we've found a .jpg/.jpeg, try loading it as compressed
112            if file_path == jpeg_path:
113                return self._get_jpeg_image(file_path)
114            else:
115                raise FileNotFoundError(f"Could not find image at {file_path}")
116        else:
117            # Load the image
118            image = tifffile.imread(file_path)
119            if image is None or image.size == 0:
120                raise ValueError(f"Could not load image from {file_path}")
121            return image
122
123    def _get_jpeg_image(self, input_path: str) -> np.ndarray:
124        raise NotImplementedError("JPEG image loading not yet implemented.")
125
126    @classmethod
127    def get_frames(
128        cls, tile: Tile, channels: tuple[int | str] = None
129    ) -> list[typing.Self]:
130        """
131        Get the frames for a tile and a set of channels. By default, gets all channels.
132        :param tile: the tile.
133        :param channels: the channels, as indices or names. Defaults to all channels.
134        :return: the frames, in order of the channels.
135        """
136        if channels is None:
137            channels = range(len(tile.scan.channels))
138        frames = []
139        for channel in channels:
140            frames.append(Frame(tile.scan, tile, channel))
141        return frames
142
143    @classmethod
144    def get_all_frames(
145        cls,
146        scan: Scan,
147        channels: tuple[int | str] = None,
148        n_roi: int = 0,
149        as_flat: bool = True,
150    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
151        """
152        Get all frames for a scan and a set of channels.
153        :param scan: the scan metadata.
154        :param channels: the channels, as indices or names. Defaults to all channels.
155        :param n_roi: the region of interest to use. Defaults to 0.
156        :param as_flat: whether to flatten the frames into a 2D list.
157        :return: if as_flat: 2D list of frames, organized as [n][channel];
158                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
159        """
160        if as_flat:
161            frames = []
162            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
163                tile = Tile(scan, n, n_roi)
164                frames.append(cls.get_frames(tile, channels))
165        else:
166            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
167            for x in range(scan.roi[n_roi].tile_cols):
168                for y in range(scan.roi[n_roi].tile_rows):
169                    tile = Tile(scan, (x, y), n_roi)
170                    frames[y][x] = cls.get_frames(tile, channels)
171        return frames
172
173    @classmethod
174    def make_rgb_image(
175        cls,
176        tile: Tile,
177        channels: dict[int, tuple[float, float, float]],
178        input_path=None,
179    ) -> np.ndarray:
180        """
181        Convenience method for creating an RGB image from a tile and a set of channels
182        without manually extracting any frames.
183        :param tile: the tile for which the image should be made.
184        :param channels: a dictionary of scan channel indices and RGB gains.
185        :param input_path: the path to the input images. Will use metadata if not provided.
186        :return: the image as a numpy array.
187        """
188        images = []
189        colors = []
190        for channel_index, color in channels.items():
191            if channel_index == -1:
192                continue
193            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
194            images.append(image)
195            colors.append(color)
196        return csi_image_utils.make_rgb(images, colors)
class Frame:
 20class Frame:
 21    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
 22        self.scan = scan
 23        self.tile = tile
 24        if isinstance(channel, int):
 25            self.channel = channel
 26            if self.channel < 0 or self.channel >= len(scan.channels):
 27                raise ValueError(
 28                    f"Channel index {self.channel} is out of bounds for scan."
 29                )
 30        elif isinstance(channel, str):
 31            self.channel = self.scan.get_channel_indices([channel])
 32
 33    def __repr__(self) -> str:
 34        return f"{self.scan.slide_id}-{self.tile.n}-{self.scan.channels[self.channel].name}"
 35
 36    def __eq__(self, other) -> bool:
 37        return self.__repr__() == other.__repr__()
 38
 39    def get_file_path(
 40        self, input_path: str = None, file_extension: str = ".tif"
 41    ) -> str:
 42        """
 43        Get the file path for the frame, optionally changing
 44        the scan path and file extension.
 45        :param input_path: the path to the scan's directory. If None, defaults to
 46                           the path loaded in the frame's tile's scan object.
 47        :param file_extension: the image file extension. Defaults to .tif.
 48        :return: the file path.
 49        """
 50        if input_path is None:
 51            input_path = self.scan.path
 52            if len(self.scan.roi) > 1:
 53                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
 54        # Remove trailing slashes
 55        if input_path[-1] == os.sep:
 56            input_path = input_path[:-1]
 57        # Append proc if it's pointing to the base bzScanner directory
 58        if input_path.endswith("bzScanner"):
 59            input_path = os.path.join(input_path, "proc")
 60        # Should be a directory; append the file name
 61        if os.path.isdir(input_path):
 62            input_path = os.path.join(input_path, self.get_file_name())
 63        else:
 64            raise ValueError(f"Input path {input_path} is not a directory.")
 65        return input_path
 66
 67    def get_file_name(self, file_extension: str = ".tif") -> str:
 68        """
 69        Get the file name for the frame, handling different name conventions by scanner.
 70        :param file_extension: the image file extension. Defaults to .tif.
 71        :return: the file name.
 72        """
 73        if self.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
 74            channel_name = self.scan.channels[self.channel].name
 75            x = self.tile.x
 76            y = self.tile.y
 77            file_name = f"{channel_name}-X{x:03}-Y{y:03}{file_extension}"
 78        elif self.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
 79            channel_name = self.scan.channels[self.channel].name
 80            real_channel_index = list(self.scan.BZSCANNER_CHANNEL_MAP.values()).index(
 81                channel_name
 82            )
 83            total_tiles = self.scan.roi[0].tile_rows * self.scan.roi[0].tile_cols
 84            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
 85            n_bzscanner = self.tile.n + tile_offset
 86            file_name = f"Tile{n_bzscanner:06}{file_extension}"
 87        else:
 88            raise ValueError(f"Scanner {self.scan.scanner_id} not supported.")
 89        return file_name
 90
 91    def get_image(self, input_path: str = None) -> np.ndarray:
 92        """
 93        Loads the image for this frame. Handles .tif (will return 16-bit images) and
 94        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
 95        .jpg/.jpeg images (compressed, using .tags files).
 96        :param input_path: the path to the scan's directory. If None, defaults to
 97                           the path loaded in the frame's tile's scan object.
 98        :return: the array representing the image.
 99        """
100
101        file_path = self.get_file_path(input_path)
102
103        # Check for the file
104        if not os.path.exists(file_path):
105            # Alternative: could be a .jpg/.jpeg file, test both
106            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
107            if os.path.exists(jpeg_path):
108                file_path = jpeg_path
109            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
110            if os.path.exists(jpeg_path):
111                file_path = jpeg_path
112            # If we've found a .jpg/.jpeg, try loading it as compressed
113            if file_path == jpeg_path:
114                return self._get_jpeg_image(file_path)
115            else:
116                raise FileNotFoundError(f"Could not find image at {file_path}")
117        else:
118            # Load the image
119            image = tifffile.imread(file_path)
120            if image is None or image.size == 0:
121                raise ValueError(f"Could not load image from {file_path}")
122            return image
123
124    def _get_jpeg_image(self, input_path: str) -> np.ndarray:
125        raise NotImplementedError("JPEG image loading not yet implemented.")
126
127    @classmethod
128    def get_frames(
129        cls, tile: Tile, channels: tuple[int | str] = None
130    ) -> list[typing.Self]:
131        """
132        Get the frames for a tile and a set of channels. By default, gets all channels.
133        :param tile: the tile.
134        :param channels: the channels, as indices or names. Defaults to all channels.
135        :return: the frames, in order of the channels.
136        """
137        if channels is None:
138            channels = range(len(tile.scan.channels))
139        frames = []
140        for channel in channels:
141            frames.append(Frame(tile.scan, tile, channel))
142        return frames
143
144    @classmethod
145    def get_all_frames(
146        cls,
147        scan: Scan,
148        channels: tuple[int | str] = None,
149        n_roi: int = 0,
150        as_flat: bool = True,
151    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
152        """
153        Get all frames for a scan and a set of channels.
154        :param scan: the scan metadata.
155        :param channels: the channels, as indices or names. Defaults to all channels.
156        :param n_roi: the region of interest to use. Defaults to 0.
157        :param as_flat: whether to flatten the frames into a 2D list.
158        :return: if as_flat: 2D list of frames, organized as [n][channel];
159                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
160        """
161        if as_flat:
162            frames = []
163            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
164                tile = Tile(scan, n, n_roi)
165                frames.append(cls.get_frames(tile, channels))
166        else:
167            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
168            for x in range(scan.roi[n_roi].tile_cols):
169                for y in range(scan.roi[n_roi].tile_rows):
170                    tile = Tile(scan, (x, y), n_roi)
171                    frames[y][x] = cls.get_frames(tile, channels)
172        return frames
173
174    @classmethod
175    def make_rgb_image(
176        cls,
177        tile: Tile,
178        channels: dict[int, tuple[float, float, float]],
179        input_path=None,
180    ) -> np.ndarray:
181        """
182        Convenience method for creating an RGB image from a tile and a set of channels
183        without manually extracting any frames.
184        :param tile: the tile for which the image should be made.
185        :param channels: a dictionary of scan channel indices and RGB gains.
186        :param input_path: the path to the input images. Will use metadata if not provided.
187        :return: the image as a numpy array.
188        """
189        images = []
190        colors = []
191        for channel_index, color in channels.items():
192            if channel_index == -1:
193                continue
194            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
195            images.append(image)
196            colors.append(color)
197        return csi_image_utils.make_rgb(images, colors)
Frame( scan: csi_images.csi_scans.Scan, tile: csi_images.csi_tiles.Tile, channel: int | str)
21    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
22        self.scan = scan
23        self.tile = tile
24        if isinstance(channel, int):
25            self.channel = channel
26            if self.channel < 0 or self.channel >= len(scan.channels):
27                raise ValueError(
28                    f"Channel index {self.channel} is out of bounds for scan."
29                )
30        elif isinstance(channel, str):
31            self.channel = self.scan.get_channel_indices([channel])
scan
tile
def get_file_path(self, input_path: str = None, file_extension: str = '.tif') -> str:
39    def get_file_path(
40        self, input_path: str = None, file_extension: str = ".tif"
41    ) -> str:
42        """
43        Get the file path for the frame, optionally changing
44        the scan path and file extension.
45        :param input_path: the path to the scan's directory. If None, defaults to
46                           the path loaded in the frame's tile's scan object.
47        :param file_extension: the image file extension. Defaults to .tif.
48        :return: the file path.
49        """
50        if input_path is None:
51            input_path = self.scan.path
52            if len(self.scan.roi) > 1:
53                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
54        # Remove trailing slashes
55        if input_path[-1] == os.sep:
56            input_path = input_path[:-1]
57        # Append proc if it's pointing to the base bzScanner directory
58        if input_path.endswith("bzScanner"):
59            input_path = os.path.join(input_path, "proc")
60        # Should be a directory; append the file name
61        if os.path.isdir(input_path):
62            input_path = os.path.join(input_path, self.get_file_name())
63        else:
64            raise ValueError(f"Input path {input_path} is not a directory.")
65        return input_path

Get the file path for the frame, optionally changing the scan path and file extension.

Parameters
  • input_path: the path to the scan's directory. If None, defaults to the path loaded in the frame's tile's scan object.
  • file_extension: the image file extension. Defaults to .tif.
Returns

the file path.

def get_file_name(self, file_extension: str = '.tif') -> str:
67    def get_file_name(self, file_extension: str = ".tif") -> str:
68        """
69        Get the file name for the frame, handling different name conventions by scanner.
70        :param file_extension: the image file extension. Defaults to .tif.
71        :return: the file name.
72        """
73        if self.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
74            channel_name = self.scan.channels[self.channel].name
75            x = self.tile.x
76            y = self.tile.y
77            file_name = f"{channel_name}-X{x:03}-Y{y:03}{file_extension}"
78        elif self.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
79            channel_name = self.scan.channels[self.channel].name
80            real_channel_index = list(self.scan.BZSCANNER_CHANNEL_MAP.values()).index(
81                channel_name
82            )
83            total_tiles = self.scan.roi[0].tile_rows * self.scan.roi[0].tile_cols
84            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
85            n_bzscanner = self.tile.n + tile_offset
86            file_name = f"Tile{n_bzscanner:06}{file_extension}"
87        else:
88            raise ValueError(f"Scanner {self.scan.scanner_id} not supported.")
89        return file_name

Get the file name for the frame, handling different name conventions by scanner.

Parameters
  • file_extension: the image file extension. Defaults to .tif.
Returns

the file name.

def get_image(self, input_path: str = None) -> numpy.ndarray:
 91    def get_image(self, input_path: str = None) -> np.ndarray:
 92        """
 93        Loads the image for this frame. Handles .tif (will return 16-bit images) and
 94        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
 95        .jpg/.jpeg images (compressed, using .tags files).
 96        :param input_path: the path to the scan's directory. If None, defaults to
 97                           the path loaded in the frame's tile's scan object.
 98        :return: the array representing the image.
 99        """
100
101        file_path = self.get_file_path(input_path)
102
103        # Check for the file
104        if not os.path.exists(file_path):
105            # Alternative: could be a .jpg/.jpeg file, test both
106            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
107            if os.path.exists(jpeg_path):
108                file_path = jpeg_path
109            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
110            if os.path.exists(jpeg_path):
111                file_path = jpeg_path
112            # If we've found a .jpg/.jpeg, try loading it as compressed
113            if file_path == jpeg_path:
114                return self._get_jpeg_image(file_path)
115            else:
116                raise FileNotFoundError(f"Could not find image at {file_path}")
117        else:
118            # Load the image
119            image = tifffile.imread(file_path)
120            if image is None or image.size == 0:
121                raise ValueError(f"Could not load image from {file_path}")
122            return image

Loads the image for this frame. Handles .tif (will return 16-bit images) and .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing .jpg/.jpeg images (compressed, using .tags files).

Parameters
  • input_path: the path to the scan's directory. If None, defaults to the path loaded in the frame's tile's scan object.
Returns

the array representing the image.

@classmethod
def get_frames( cls, tile: csi_images.csi_tiles.Tile, channels: tuple[int | str] = None) -> list[typing.Self]:
127    @classmethod
128    def get_frames(
129        cls, tile: Tile, channels: tuple[int | str] = None
130    ) -> list[typing.Self]:
131        """
132        Get the frames for a tile and a set of channels. By default, gets all channels.
133        :param tile: the tile.
134        :param channels: the channels, as indices or names. Defaults to all channels.
135        :return: the frames, in order of the channels.
136        """
137        if channels is None:
138            channels = range(len(tile.scan.channels))
139        frames = []
140        for channel in channels:
141            frames.append(Frame(tile.scan, tile, channel))
142        return frames

Get the frames for a tile and a set of channels. By default, gets all channels.

Parameters
  • tile: the tile.
  • channels: the channels, as indices or names. Defaults to all channels.
Returns

the frames, in order of the channels.

@classmethod
def get_all_frames( cls, scan: csi_images.csi_scans.Scan, channels: tuple[int | str] = None, n_roi: int = 0, as_flat: bool = True) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
144    @classmethod
145    def get_all_frames(
146        cls,
147        scan: Scan,
148        channels: tuple[int | str] = None,
149        n_roi: int = 0,
150        as_flat: bool = True,
151    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
152        """
153        Get all frames for a scan and a set of channels.
154        :param scan: the scan metadata.
155        :param channels: the channels, as indices or names. Defaults to all channels.
156        :param n_roi: the region of interest to use. Defaults to 0.
157        :param as_flat: whether to flatten the frames into a 2D list.
158        :return: if as_flat: 2D list of frames, organized as [n][channel];
159                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
160        """
161        if as_flat:
162            frames = []
163            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
164                tile = Tile(scan, n, n_roi)
165                frames.append(cls.get_frames(tile, channels))
166        else:
167            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
168            for x in range(scan.roi[n_roi].tile_cols):
169                for y in range(scan.roi[n_roi].tile_rows):
170                    tile = Tile(scan, (x, y), n_roi)
171                    frames[y][x] = cls.get_frames(tile, channels)
172        return frames

Get all frames for a scan and a set of channels.

Parameters
  • scan: the scan metadata.
  • channels: the channels, as indices or names. Defaults to all channels.
  • n_roi: the region of interest to use. Defaults to 0.
  • as_flat: whether to flatten the frames into a 2D list.
Returns

if as_flat: 2D list of frames, organized as [n][channel]; if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].

@classmethod
def make_rgb_image( cls, tile: csi_images.csi_tiles.Tile, channels: dict[int, tuple[float, float, float]], input_path=None) -> numpy.ndarray:
174    @classmethod
175    def make_rgb_image(
176        cls,
177        tile: Tile,
178        channels: dict[int, tuple[float, float, float]],
179        input_path=None,
180    ) -> np.ndarray:
181        """
182        Convenience method for creating an RGB image from a tile and a set of channels
183        without manually extracting any frames.
184        :param tile: the tile for which the image should be made.
185        :param channels: a dictionary of scan channel indices and RGB gains.
186        :param input_path: the path to the input images. Will use metadata if not provided.
187        :return: the image as a numpy array.
188        """
189        images = []
190        colors = []
191        for channel_index, color in channels.items():
192            if channel_index == -1:
193                continue
194            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
195            images.append(image)
196            colors.append(color)
197        return csi_image_utils.make_rgb(images, colors)

Convenience method for creating an RGB image from a tile and a set of channels without manually extracting any frames.

Parameters
  • tile: the tile for which the image should be made.
  • channels: a dictionary of scan channel indices and RGB gains.
  • input_path: the path to the input images. Will use metadata if not provided.
Returns

the image as a numpy array.