csi_images.csi_tiles

Contains the Tile class, which represents a collection of frames at the same position in a scan. The module comes with several helper functions that allow for gathering tiles based on their position in the scan.

  1"""
  2Contains the Tile class, which represents a collection of frames at the same position
  3in a scan. The module comes with several helper functions that allow for gathering tiles
  4based on their position in the scan.
  5"""
  6
  7from typing import Self, Iterable, Sequence
  8
  9import numpy as np
 10
 11from csi_images.csi_scans import Scan
 12
 13
 14class Tile:
 15    """
 16    A class that represents a tile in a scan. This class encodes the position of a group
 17    of frames in a scan, based on the scan's metadata. The module comes with several
 18    helper functions that allow for gathering tiles based on their position in the scan.
 19    """
 20
 21    def __init__(self, scan: Scan, coordinates: int | tuple[int, int], n_roi: int = 0):
 22        """
 23
 24        :param scan: Scan object to associate with this tile
 25        :param coordinates: n or (x, y) coordinates of the tile in the scan
 26        :param n_roi: the region of interest to use, defaults to 0
 27        """
 28        self.scan = scan
 29
 30        # Check that the n_roi is valid
 31        if n_roi >= len(self.scan.roi):
 32            raise ValueError(f"n_roi {n_roi} is out of bounds for scan.")
 33        self.n_roi = int(n_roi)
 34
 35        # Check that the coordinates are valid
 36        tile_rows = scan.roi[n_roi].tile_rows
 37        tile_cols = scan.roi[n_roi].tile_cols
 38        total_tiles = tile_rows * tile_cols
 39        if np.issubdtype(type(coordinates), np.integer):
 40            # We received "n" as the coordinates
 41            if 0 > coordinates or coordinates > total_tiles:
 42                raise ValueError(
 43                    f"n ({coordinates}) must be between 0 and the "
 44                    f"number of tiles in ROI {self.n_roi} ({total_tiles})."
 45                )
 46            self.n = int(coordinates)
 47            self.x, self.y = self.n_to_position()
 48        elif (
 49            isinstance(coordinates, Sequence)
 50            and len(coordinates) == 2
 51            and all([np.issubdtype(type(coord), np.integer) for coord in coordinates])
 52        ):
 53            # We received (x, y) as the coordinates
 54            if 0 > coordinates[0] or coordinates[0] >= tile_cols:
 55                raise ValueError(
 56                    f"x ({coordinates[0]}) must be between 0 and the "
 57                    f"number of columns in ROI {self.n_roi} ({tile_cols})."
 58                )
 59            if 0 > coordinates[1] or coordinates[1] >= tile_rows:
 60                raise ValueError(
 61                    f"y ({coordinates[1]}) must be between 0 and the "
 62                    f"number of rows in ROI {self.n_roi} ({tile_rows})."
 63                )
 64            self.x, self.y = int(coordinates[0]), int(coordinates[1])
 65            self.n = self.position_to_n()
 66        else:
 67            raise ValueError(
 68                "Coordinates must be an integer n or a tuple of (x, y) coordinates."
 69            )
 70
 71    def __key(self) -> tuple:
 72        return self.scan.slide_id, self.n_roi, self.n
 73
 74    def __hash__(self) -> int:
 75        return hash(self.__key())
 76
 77    def __repr__(self) -> str:
 78        return f"{self.scan.slide_id}-{self.n_roi}-{self.n}"
 79
 80    def __eq__(self, other) -> bool:
 81        return self.__repr__() == other.__repr__()
 82
 83    # Helper functions that convert ***indices***, which are 0-indexed
 84    def position_to_n(self, position: tuple[int, int] = (-1, -1)) -> int:
 85        """
 86        Convert the x, y coordinates to the n coordinate, based on this tile's scan
 87        metadata and ROI. Can be provided alternative x, y to convert for convenience.
 88        :param position: optional (x, y) coordinates to find the n for.
 89                         If none provided, this tile's (x, y) will be used.
 90        :return: the coordinate n, which depends on the scanner and scan layout.
 91        """
 92        if position == (-1, -1):
 93            position = self.x, self.y
 94        x, y = position
 95        if self.scan.scanner_id.startswith(self.scan.Type.AXIOSCAN7.value):
 96            n = y * self.scan.roi[self.n_roi].tile_cols + x
 97        elif self.scan.scanner_id.startswith(self.scan.Type.BZSCANNER.value):
 98            n = y * self.scan.roi[self.n_roi].tile_cols
 99            if y % 2 == 0:
100                n += x
101            else:
102                n += (self.scan.roi[0].tile_cols - 1) - x
103        else:
104            raise ValueError(f"Scanner type {self.scan.scanner_id} not supported.")
105        return n
106
107    def n_to_position(self, n: int = -1) -> tuple[int, int]:
108        """
109        Convert the n coordinate to x, y coordinates, based on this tile's scan
110        metadata and ROI. Can be provided alternative n to convert for convenience.
111        :param n: an optional n coordinate to find the position for.
112                  If none provided, this tile's n will be used.
113        :return: x, y coordinates of the tile in the scan's coordinate system.
114        """
115        if n == -1:
116            n = self.n
117        if n < 0:
118            raise ValueError(f"n ({n}) must be non-negative.")
119        if self.scan.scanner_id.startswith(self.scan.Type.AXIOSCAN7.value):
120            x = n % self.scan.roi[0].tile_cols
121            y = n // self.scan.roi[0].tile_cols
122            return x, y
123        elif self.scan.scanner_id.startswith(self.scan.Type.BZSCANNER.value):
124            y = n // self.scan.roi[0].tile_cols
125            if y % 2 == 0:
126                x = n % self.scan.roi[0].tile_cols
127            else:
128                x = (self.scan.roi[0].tile_cols - 1) - (n % self.scan.roi[0].tile_cols)
129        else:
130            raise ValueError(f"Scanner type {self.scan.scanner_id} not supported.")
131        return x, y
132
133    @classmethod
134    def get_tiles(
135        cls,
136        scan: Scan,
137        coordinates: Iterable[int] | Iterable[tuple[int, int]] = None,
138        n_roi: int = 0,
139        as_flat: bool = True,
140    ) -> list[Self] | list[list[Self]]:
141        """
142        The simplest way to gather a list of Tile objects. By default, it will gather all
143        tiles in the scan. To gather specific tiles, provide a list of coordinates.
144        :param scan: the scan metadata.
145        :param coordinates: a list of n-based indices or (x, y) coordinates.
146                            Leave as None to include all tiles.
147        :param n_roi: the region of interest to use. Defaults to 0.
148        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
149        :return: if as_flat: a list of Tile objects in the same order as the coordinates;
150                 if not as_flat: a list of lists of Tile objects in their relative coordinates.
151        """
152        if as_flat:
153            if coordinates is None:
154                # Populate coordinates with all n's.
155                coordinates = list(
156                    range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols)
157                )
158            tiles = []
159            for coordinate in coordinates:
160                tiles.append(cls(scan, coordinate, n_roi))
161        else:
162            if coordinates is None:
163                # Populate coordinates with all (x, y) pairs in row-major order
164                coordinates = []
165                for y in range(scan.roi[n_roi].tile_rows):
166                    for x in range(scan.roi[n_roi].tile_cols):
167                        coordinates.append((x, y))
168            elif isinstance(coordinates, Sequence) and isinstance(coordinates[0], int):
169                # Convert n's to (x, y) coordinates
170                coordinates = [
171                    (cls(scan, n, n_roi).x, cls(scan, n, n_roi).y) for n in coordinates
172                ]
173            # Check that the coordinates are contiguous, otherwise we can't make a grid
174            # Find the min and max x, y values
175            x_min = scan.roi[n_roi].tile_cols
176            x_max = 0
177            y_min = scan.roi[n_roi].tile_rows
178            y_max = 0
179            for x, y in coordinates:
180                x_min = min(x_min, x)
181                x_max = max(x_max, x)
182                y_min = min(y_min, y)
183                y_max = max(y_max, y)
184
185            # Check that the coordinates are contiguous
186            if (x_max - x_min + 1) * (y_max - y_min + 1) != len(coordinates):
187                raise ValueError(
188                    "Coordinates must be a contiguous square to form "
189                    "a grid; number of coordinates does not match."
190                )
191
192            # Create a list based on number of rows
193            tiles = [[None] * (x_max - x_min + 1) for _ in range(y_max - y_min + 1)]
194            for coordinate in coordinates:
195                x, y = coordinate
196                tiles[y - y_min][x - x_min] = cls(scan, coordinate, n_roi)
197
198        return tiles
199
200    @classmethod
201    def get_tiles_by_row_col(
202        cls,
203        scan: Scan,
204        rows: Iterable[int] = None,
205        cols: Iterable[int] = None,
206        n_roi: int = 0,
207        as_flat: bool = True,
208    ) -> list[Self] | list[list[Self]]:
209        """
210        Gather a list of Tile objects based on the row and column indices provided.
211        If left as None, it will gather all rows and/or columns.
212        :param scan: the scan metadata.
213        :param rows: a list of 0-indexed rows (y-positions) in the scan axes.
214                     Leave as None to include all rows.
215        :param cols: a list of 0-indexed columns (x-positions) in the scan axes.
216                     Leave as None to include all columns.
217        :param n_roi: the region of interest to use. Defaults to 0.
218        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
219        :return: if as_flat: a list of Tile objects in row-major order;
220                 if not as_flat: a list of lists of Tile objects in their relative coordinates
221        """
222        if rows is None:
223            rows = range(scan.roi[n_roi].tile_rows)
224        if cols is None:
225            cols = range(scan.roi[n_roi].tile_cols)
226
227        # Populate coordinates
228        coordinates = []
229        for row in rows:
230            for col in cols:
231                coordinates.append((col, row))
232
233        return cls.get_tiles(scan, coordinates, n_roi, as_flat)
234
235    @classmethod
236    def get_tiles_by_xy_bounds(
237        cls,
238        scan: Scan,
239        bounds: tuple[int, int, int, int],
240        n_roi: int = 0,
241        as_flat: bool = True,
242    ) -> list[Self] | list[list[Self]]:
243        """
244        Gather a list of Tile objects based on the x, y bounds provided. The bounds are
245        exclusive, like indices, so the tiles at the far ends are NOT included in the list.
246        :param scan: the scan metadata.
247        :param bounds: a tuple of (x_0, y_0, x_1, y_1) in the scan axes.
248        :param n_roi: the region of interest to use. Defaults to 0.
249        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
250        :return: if as_flat: a list of Tile objects in row-major order;
251                 if not as_flat: a list of lists of Tile objects in their relative coordinates
252        """
253        x_0, y_0, x_1, y_1 = bounds
254        coordinates = []
255        for y in range(y_0, y_1):
256            for x in range(x_0, x_1):
257                coordinates.append((x, y))
258        return cls.get_tiles(scan, coordinates, n_roi, as_flat)
class Tile:
 15class Tile:
 16    """
 17    A class that represents a tile in a scan. This class encodes the position of a group
 18    of frames in a scan, based on the scan's metadata. The module comes with several
 19    helper functions that allow for gathering tiles based on their position in the scan.
 20    """
 21
 22    def __init__(self, scan: Scan, coordinates: int | tuple[int, int], n_roi: int = 0):
 23        """
 24
 25        :param scan: Scan object to associate with this tile
 26        :param coordinates: n or (x, y) coordinates of the tile in the scan
 27        :param n_roi: the region of interest to use, defaults to 0
 28        """
 29        self.scan = scan
 30
 31        # Check that the n_roi is valid
 32        if n_roi >= len(self.scan.roi):
 33            raise ValueError(f"n_roi {n_roi} is out of bounds for scan.")
 34        self.n_roi = int(n_roi)
 35
 36        # Check that the coordinates are valid
 37        tile_rows = scan.roi[n_roi].tile_rows
 38        tile_cols = scan.roi[n_roi].tile_cols
 39        total_tiles = tile_rows * tile_cols
 40        if np.issubdtype(type(coordinates), np.integer):
 41            # We received "n" as the coordinates
 42            if 0 > coordinates or coordinates > total_tiles:
 43                raise ValueError(
 44                    f"n ({coordinates}) must be between 0 and the "
 45                    f"number of tiles in ROI {self.n_roi} ({total_tiles})."
 46                )
 47            self.n = int(coordinates)
 48            self.x, self.y = self.n_to_position()
 49        elif (
 50            isinstance(coordinates, Sequence)
 51            and len(coordinates) == 2
 52            and all([np.issubdtype(type(coord), np.integer) for coord in coordinates])
 53        ):
 54            # We received (x, y) as the coordinates
 55            if 0 > coordinates[0] or coordinates[0] >= tile_cols:
 56                raise ValueError(
 57                    f"x ({coordinates[0]}) must be between 0 and the "
 58                    f"number of columns in ROI {self.n_roi} ({tile_cols})."
 59                )
 60            if 0 > coordinates[1] or coordinates[1] >= tile_rows:
 61                raise ValueError(
 62                    f"y ({coordinates[1]}) must be between 0 and the "
 63                    f"number of rows in ROI {self.n_roi} ({tile_rows})."
 64                )
 65            self.x, self.y = int(coordinates[0]), int(coordinates[1])
 66            self.n = self.position_to_n()
 67        else:
 68            raise ValueError(
 69                "Coordinates must be an integer n or a tuple of (x, y) coordinates."
 70            )
 71
 72    def __key(self) -> tuple:
 73        return self.scan.slide_id, self.n_roi, self.n
 74
 75    def __hash__(self) -> int:
 76        return hash(self.__key())
 77
 78    def __repr__(self) -> str:
 79        return f"{self.scan.slide_id}-{self.n_roi}-{self.n}"
 80
 81    def __eq__(self, other) -> bool:
 82        return self.__repr__() == other.__repr__()
 83
 84    # Helper functions that convert ***indices***, which are 0-indexed
 85    def position_to_n(self, position: tuple[int, int] = (-1, -1)) -> int:
 86        """
 87        Convert the x, y coordinates to the n coordinate, based on this tile's scan
 88        metadata and ROI. Can be provided alternative x, y to convert for convenience.
 89        :param position: optional (x, y) coordinates to find the n for.
 90                         If none provided, this tile's (x, y) will be used.
 91        :return: the coordinate n, which depends on the scanner and scan layout.
 92        """
 93        if position == (-1, -1):
 94            position = self.x, self.y
 95        x, y = position
 96        if self.scan.scanner_id.startswith(self.scan.Type.AXIOSCAN7.value):
 97            n = y * self.scan.roi[self.n_roi].tile_cols + x
 98        elif self.scan.scanner_id.startswith(self.scan.Type.BZSCANNER.value):
 99            n = y * self.scan.roi[self.n_roi].tile_cols
100            if y % 2 == 0:
101                n += x
102            else:
103                n += (self.scan.roi[0].tile_cols - 1) - x
104        else:
105            raise ValueError(f"Scanner type {self.scan.scanner_id} not supported.")
106        return n
107
108    def n_to_position(self, n: int = -1) -> tuple[int, int]:
109        """
110        Convert the n coordinate to x, y coordinates, based on this tile's scan
111        metadata and ROI. Can be provided alternative n to convert for convenience.
112        :param n: an optional n coordinate to find the position for.
113                  If none provided, this tile's n will be used.
114        :return: x, y coordinates of the tile in the scan's coordinate system.
115        """
116        if n == -1:
117            n = self.n
118        if n < 0:
119            raise ValueError(f"n ({n}) must be non-negative.")
120        if self.scan.scanner_id.startswith(self.scan.Type.AXIOSCAN7.value):
121            x = n % self.scan.roi[0].tile_cols
122            y = n // self.scan.roi[0].tile_cols
123            return x, y
124        elif self.scan.scanner_id.startswith(self.scan.Type.BZSCANNER.value):
125            y = n // self.scan.roi[0].tile_cols
126            if y % 2 == 0:
127                x = n % self.scan.roi[0].tile_cols
128            else:
129                x = (self.scan.roi[0].tile_cols - 1) - (n % self.scan.roi[0].tile_cols)
130        else:
131            raise ValueError(f"Scanner type {self.scan.scanner_id} not supported.")
132        return x, y
133
134    @classmethod
135    def get_tiles(
136        cls,
137        scan: Scan,
138        coordinates: Iterable[int] | Iterable[tuple[int, int]] = None,
139        n_roi: int = 0,
140        as_flat: bool = True,
141    ) -> list[Self] | list[list[Self]]:
142        """
143        The simplest way to gather a list of Tile objects. By default, it will gather all
144        tiles in the scan. To gather specific tiles, provide a list of coordinates.
145        :param scan: the scan metadata.
146        :param coordinates: a list of n-based indices or (x, y) coordinates.
147                            Leave as None to include all tiles.
148        :param n_roi: the region of interest to use. Defaults to 0.
149        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
150        :return: if as_flat: a list of Tile objects in the same order as the coordinates;
151                 if not as_flat: a list of lists of Tile objects in their relative coordinates.
152        """
153        if as_flat:
154            if coordinates is None:
155                # Populate coordinates with all n's.
156                coordinates = list(
157                    range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols)
158                )
159            tiles = []
160            for coordinate in coordinates:
161                tiles.append(cls(scan, coordinate, n_roi))
162        else:
163            if coordinates is None:
164                # Populate coordinates with all (x, y) pairs in row-major order
165                coordinates = []
166                for y in range(scan.roi[n_roi].tile_rows):
167                    for x in range(scan.roi[n_roi].tile_cols):
168                        coordinates.append((x, y))
169            elif isinstance(coordinates, Sequence) and isinstance(coordinates[0], int):
170                # Convert n's to (x, y) coordinates
171                coordinates = [
172                    (cls(scan, n, n_roi).x, cls(scan, n, n_roi).y) for n in coordinates
173                ]
174            # Check that the coordinates are contiguous, otherwise we can't make a grid
175            # Find the min and max x, y values
176            x_min = scan.roi[n_roi].tile_cols
177            x_max = 0
178            y_min = scan.roi[n_roi].tile_rows
179            y_max = 0
180            for x, y in coordinates:
181                x_min = min(x_min, x)
182                x_max = max(x_max, x)
183                y_min = min(y_min, y)
184                y_max = max(y_max, y)
185
186            # Check that the coordinates are contiguous
187            if (x_max - x_min + 1) * (y_max - y_min + 1) != len(coordinates):
188                raise ValueError(
189                    "Coordinates must be a contiguous square to form "
190                    "a grid; number of coordinates does not match."
191                )
192
193            # Create a list based on number of rows
194            tiles = [[None] * (x_max - x_min + 1) for _ in range(y_max - y_min + 1)]
195            for coordinate in coordinates:
196                x, y = coordinate
197                tiles[y - y_min][x - x_min] = cls(scan, coordinate, n_roi)
198
199        return tiles
200
201    @classmethod
202    def get_tiles_by_row_col(
203        cls,
204        scan: Scan,
205        rows: Iterable[int] = None,
206        cols: Iterable[int] = None,
207        n_roi: int = 0,
208        as_flat: bool = True,
209    ) -> list[Self] | list[list[Self]]:
210        """
211        Gather a list of Tile objects based on the row and column indices provided.
212        If left as None, it will gather all rows and/or columns.
213        :param scan: the scan metadata.
214        :param rows: a list of 0-indexed rows (y-positions) in the scan axes.
215                     Leave as None to include all rows.
216        :param cols: a list of 0-indexed columns (x-positions) in the scan axes.
217                     Leave as None to include all columns.
218        :param n_roi: the region of interest to use. Defaults to 0.
219        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
220        :return: if as_flat: a list of Tile objects in row-major order;
221                 if not as_flat: a list of lists of Tile objects in their relative coordinates
222        """
223        if rows is None:
224            rows = range(scan.roi[n_roi].tile_rows)
225        if cols is None:
226            cols = range(scan.roi[n_roi].tile_cols)
227
228        # Populate coordinates
229        coordinates = []
230        for row in rows:
231            for col in cols:
232                coordinates.append((col, row))
233
234        return cls.get_tiles(scan, coordinates, n_roi, as_flat)
235
236    @classmethod
237    def get_tiles_by_xy_bounds(
238        cls,
239        scan: Scan,
240        bounds: tuple[int, int, int, int],
241        n_roi: int = 0,
242        as_flat: bool = True,
243    ) -> list[Self] | list[list[Self]]:
244        """
245        Gather a list of Tile objects based on the x, y bounds provided. The bounds are
246        exclusive, like indices, so the tiles at the far ends are NOT included in the list.
247        :param scan: the scan metadata.
248        :param bounds: a tuple of (x_0, y_0, x_1, y_1) in the scan axes.
249        :param n_roi: the region of interest to use. Defaults to 0.
250        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
251        :return: if as_flat: a list of Tile objects in row-major order;
252                 if not as_flat: a list of lists of Tile objects in their relative coordinates
253        """
254        x_0, y_0, x_1, y_1 = bounds
255        coordinates = []
256        for y in range(y_0, y_1):
257            for x in range(x_0, x_1):
258                coordinates.append((x, y))
259        return cls.get_tiles(scan, coordinates, n_roi, as_flat)

A class that represents a tile in a scan. This class encodes the position of a group of frames in a scan, based on the scan's metadata. The module comes with several helper functions that allow for gathering tiles based on their position in the scan.

Tile( scan: csi_images.csi_scans.Scan, coordinates: int | tuple[int, int], n_roi: int = 0)
22    def __init__(self, scan: Scan, coordinates: int | tuple[int, int], n_roi: int = 0):
23        """
24
25        :param scan: Scan object to associate with this tile
26        :param coordinates: n or (x, y) coordinates of the tile in the scan
27        :param n_roi: the region of interest to use, defaults to 0
28        """
29        self.scan = scan
30
31        # Check that the n_roi is valid
32        if n_roi >= len(self.scan.roi):
33            raise ValueError(f"n_roi {n_roi} is out of bounds for scan.")
34        self.n_roi = int(n_roi)
35
36        # Check that the coordinates are valid
37        tile_rows = scan.roi[n_roi].tile_rows
38        tile_cols = scan.roi[n_roi].tile_cols
39        total_tiles = tile_rows * tile_cols
40        if np.issubdtype(type(coordinates), np.integer):
41            # We received "n" as the coordinates
42            if 0 > coordinates or coordinates > total_tiles:
43                raise ValueError(
44                    f"n ({coordinates}) must be between 0 and the "
45                    f"number of tiles in ROI {self.n_roi} ({total_tiles})."
46                )
47            self.n = int(coordinates)
48            self.x, self.y = self.n_to_position()
49        elif (
50            isinstance(coordinates, Sequence)
51            and len(coordinates) == 2
52            and all([np.issubdtype(type(coord), np.integer) for coord in coordinates])
53        ):
54            # We received (x, y) as the coordinates
55            if 0 > coordinates[0] or coordinates[0] >= tile_cols:
56                raise ValueError(
57                    f"x ({coordinates[0]}) must be between 0 and the "
58                    f"number of columns in ROI {self.n_roi} ({tile_cols})."
59                )
60            if 0 > coordinates[1] or coordinates[1] >= tile_rows:
61                raise ValueError(
62                    f"y ({coordinates[1]}) must be between 0 and the "
63                    f"number of rows in ROI {self.n_roi} ({tile_rows})."
64                )
65            self.x, self.y = int(coordinates[0]), int(coordinates[1])
66            self.n = self.position_to_n()
67        else:
68            raise ValueError(
69                "Coordinates must be an integer n or a tuple of (x, y) coordinates."
70            )
Parameters
  • scan: Scan object to associate with this tile
  • coordinates: n or (x, y) coordinates of the tile in the scan
  • n_roi: the region of interest to use, defaults to 0
scan
n_roi
def position_to_n(self, position: tuple[int, int] = (-1, -1)) -> int:
 85    def position_to_n(self, position: tuple[int, int] = (-1, -1)) -> int:
 86        """
 87        Convert the x, y coordinates to the n coordinate, based on this tile's scan
 88        metadata and ROI. Can be provided alternative x, y to convert for convenience.
 89        :param position: optional (x, y) coordinates to find the n for.
 90                         If none provided, this tile's (x, y) will be used.
 91        :return: the coordinate n, which depends on the scanner and scan layout.
 92        """
 93        if position == (-1, -1):
 94            position = self.x, self.y
 95        x, y = position
 96        if self.scan.scanner_id.startswith(self.scan.Type.AXIOSCAN7.value):
 97            n = y * self.scan.roi[self.n_roi].tile_cols + x
 98        elif self.scan.scanner_id.startswith(self.scan.Type.BZSCANNER.value):
 99            n = y * self.scan.roi[self.n_roi].tile_cols
100            if y % 2 == 0:
101                n += x
102            else:
103                n += (self.scan.roi[0].tile_cols - 1) - x
104        else:
105            raise ValueError(f"Scanner type {self.scan.scanner_id} not supported.")
106        return n

Convert the x, y coordinates to the n coordinate, based on this tile's scan metadata and ROI. Can be provided alternative x, y to convert for convenience.

Parameters
  • position: optional (x, y) coordinates to find the n for. If none provided, this tile's (x, y) will be used.
Returns

the coordinate n, which depends on the scanner and scan layout.

def n_to_position(self, n: int = -1) -> tuple[int, int]:
108    def n_to_position(self, n: int = -1) -> tuple[int, int]:
109        """
110        Convert the n coordinate to x, y coordinates, based on this tile's scan
111        metadata and ROI. Can be provided alternative n to convert for convenience.
112        :param n: an optional n coordinate to find the position for.
113                  If none provided, this tile's n will be used.
114        :return: x, y coordinates of the tile in the scan's coordinate system.
115        """
116        if n == -1:
117            n = self.n
118        if n < 0:
119            raise ValueError(f"n ({n}) must be non-negative.")
120        if self.scan.scanner_id.startswith(self.scan.Type.AXIOSCAN7.value):
121            x = n % self.scan.roi[0].tile_cols
122            y = n // self.scan.roi[0].tile_cols
123            return x, y
124        elif self.scan.scanner_id.startswith(self.scan.Type.BZSCANNER.value):
125            y = n // self.scan.roi[0].tile_cols
126            if y % 2 == 0:
127                x = n % self.scan.roi[0].tile_cols
128            else:
129                x = (self.scan.roi[0].tile_cols - 1) - (n % self.scan.roi[0].tile_cols)
130        else:
131            raise ValueError(f"Scanner type {self.scan.scanner_id} not supported.")
132        return x, y

Convert the n coordinate to x, y coordinates, based on this tile's scan metadata and ROI. Can be provided alternative n to convert for convenience.

Parameters
  • n: an optional n coordinate to find the position for. If none provided, this tile's n will be used.
Returns

x, y coordinates of the tile in the scan's coordinate system.

@classmethod
def get_tiles( cls, scan: csi_images.csi_scans.Scan, coordinates: Union[Iterable[int], Iterable[tuple[int, int]]] = None, n_roi: int = 0, as_flat: bool = True) -> list[typing.Self] | list[list[typing.Self]]:
134    @classmethod
135    def get_tiles(
136        cls,
137        scan: Scan,
138        coordinates: Iterable[int] | Iterable[tuple[int, int]] = None,
139        n_roi: int = 0,
140        as_flat: bool = True,
141    ) -> list[Self] | list[list[Self]]:
142        """
143        The simplest way to gather a list of Tile objects. By default, it will gather all
144        tiles in the scan. To gather specific tiles, provide a list of coordinates.
145        :param scan: the scan metadata.
146        :param coordinates: a list of n-based indices or (x, y) coordinates.
147                            Leave as None to include all tiles.
148        :param n_roi: the region of interest to use. Defaults to 0.
149        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
150        :return: if as_flat: a list of Tile objects in the same order as the coordinates;
151                 if not as_flat: a list of lists of Tile objects in their relative coordinates.
152        """
153        if as_flat:
154            if coordinates is None:
155                # Populate coordinates with all n's.
156                coordinates = list(
157                    range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols)
158                )
159            tiles = []
160            for coordinate in coordinates:
161                tiles.append(cls(scan, coordinate, n_roi))
162        else:
163            if coordinates is None:
164                # Populate coordinates with all (x, y) pairs in row-major order
165                coordinates = []
166                for y in range(scan.roi[n_roi].tile_rows):
167                    for x in range(scan.roi[n_roi].tile_cols):
168                        coordinates.append((x, y))
169            elif isinstance(coordinates, Sequence) and isinstance(coordinates[0], int):
170                # Convert n's to (x, y) coordinates
171                coordinates = [
172                    (cls(scan, n, n_roi).x, cls(scan, n, n_roi).y) for n in coordinates
173                ]
174            # Check that the coordinates are contiguous, otherwise we can't make a grid
175            # Find the min and max x, y values
176            x_min = scan.roi[n_roi].tile_cols
177            x_max = 0
178            y_min = scan.roi[n_roi].tile_rows
179            y_max = 0
180            for x, y in coordinates:
181                x_min = min(x_min, x)
182                x_max = max(x_max, x)
183                y_min = min(y_min, y)
184                y_max = max(y_max, y)
185
186            # Check that the coordinates are contiguous
187            if (x_max - x_min + 1) * (y_max - y_min + 1) != len(coordinates):
188                raise ValueError(
189                    "Coordinates must be a contiguous square to form "
190                    "a grid; number of coordinates does not match."
191                )
192
193            # Create a list based on number of rows
194            tiles = [[None] * (x_max - x_min + 1) for _ in range(y_max - y_min + 1)]
195            for coordinate in coordinates:
196                x, y = coordinate
197                tiles[y - y_min][x - x_min] = cls(scan, coordinate, n_roi)
198
199        return tiles

The simplest way to gather a list of Tile objects. By default, it will gather all tiles in the scan. To gather specific tiles, provide a list of coordinates.

Parameters
  • scan: the scan metadata.
  • coordinates: a list of n-based indices or (x, y) coordinates. Leave as None to include all tiles.
  • n_roi: the region of interest to use. Defaults to 0.
  • as_flat: whether to return a flat list of Tile objects or a list of lists.
Returns

if as_flat: a list of Tile objects in the same order as the coordinates; if not as_flat: a list of lists of Tile objects in their relative coordinates.

@classmethod
def get_tiles_by_row_col( cls, scan: csi_images.csi_scans.Scan, rows: Iterable[int] = None, cols: Iterable[int] = None, n_roi: int = 0, as_flat: bool = True) -> list[typing.Self] | list[list[typing.Self]]:
201    @classmethod
202    def get_tiles_by_row_col(
203        cls,
204        scan: Scan,
205        rows: Iterable[int] = None,
206        cols: Iterable[int] = None,
207        n_roi: int = 0,
208        as_flat: bool = True,
209    ) -> list[Self] | list[list[Self]]:
210        """
211        Gather a list of Tile objects based on the row and column indices provided.
212        If left as None, it will gather all rows and/or columns.
213        :param scan: the scan metadata.
214        :param rows: a list of 0-indexed rows (y-positions) in the scan axes.
215                     Leave as None to include all rows.
216        :param cols: a list of 0-indexed columns (x-positions) in the scan axes.
217                     Leave as None to include all columns.
218        :param n_roi: the region of interest to use. Defaults to 0.
219        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
220        :return: if as_flat: a list of Tile objects in row-major order;
221                 if not as_flat: a list of lists of Tile objects in their relative coordinates
222        """
223        if rows is None:
224            rows = range(scan.roi[n_roi].tile_rows)
225        if cols is None:
226            cols = range(scan.roi[n_roi].tile_cols)
227
228        # Populate coordinates
229        coordinates = []
230        for row in rows:
231            for col in cols:
232                coordinates.append((col, row))
233
234        return cls.get_tiles(scan, coordinates, n_roi, as_flat)

Gather a list of Tile objects based on the row and column indices provided. If left as None, it will gather all rows and/or columns.

Parameters
  • scan: the scan metadata.
  • rows: a list of 0-indexed rows (y-positions) in the scan axes. Leave as None to include all rows.
  • cols: a list of 0-indexed columns (x-positions) in the scan axes. Leave as None to include all columns.
  • n_roi: the region of interest to use. Defaults to 0.
  • as_flat: whether to return a flat list of Tile objects or a list of lists.
Returns

if as_flat: a list of Tile objects in row-major order; if not as_flat: a list of lists of Tile objects in their relative coordinates

@classmethod
def get_tiles_by_xy_bounds( cls, scan: csi_images.csi_scans.Scan, bounds: tuple[int, int, int, int], n_roi: int = 0, as_flat: bool = True) -> list[typing.Self] | list[list[typing.Self]]:
236    @classmethod
237    def get_tiles_by_xy_bounds(
238        cls,
239        scan: Scan,
240        bounds: tuple[int, int, int, int],
241        n_roi: int = 0,
242        as_flat: bool = True,
243    ) -> list[Self] | list[list[Self]]:
244        """
245        Gather a list of Tile objects based on the x, y bounds provided. The bounds are
246        exclusive, like indices, so the tiles at the far ends are NOT included in the list.
247        :param scan: the scan metadata.
248        :param bounds: a tuple of (x_0, y_0, x_1, y_1) in the scan axes.
249        :param n_roi: the region of interest to use. Defaults to 0.
250        :param as_flat: whether to return a flat list of Tile objects or a list of lists.
251        :return: if as_flat: a list of Tile objects in row-major order;
252                 if not as_flat: a list of lists of Tile objects in their relative coordinates
253        """
254        x_0, y_0, x_1, y_1 = bounds
255        coordinates = []
256        for y in range(y_0, y_1):
257            for x in range(x_0, x_1):
258                coordinates.append((x, y))
259        return cls.get_tiles(scan, coordinates, n_roi, as_flat)

Gather a list of Tile objects based on the x, y bounds provided. The bounds are exclusive, like indices, so the tiles at the far ends are NOT included in the list.

Parameters
  • scan: the scan metadata.
  • bounds: a tuple of (x_0, y_0, x_1, y_1) in the scan axes.
  • n_roi: the region of interest to use. Defaults to 0.
  • as_flat: whether to return a flat list of Tile objects or a list of lists.
Returns

if as_flat: a list of Tile objects in row-major order; if not as_flat: a list of lists of Tile objects in their relative coordinates