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)
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.
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
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.
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.
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.
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
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