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 9from typing import Self, Iterable 10 11import numpy as np 12 13from .csi_scans import Scan 14from .csi_tiles import Tile 15 16# Optional dependencies; will raise errors in particular functions if not installed 17try: 18 from . import csi_images 19except ImportError: 20 csi_images = None 21try: 22 import imageio.v3 as imageio 23except ImportError: 24 imageio = None 25 26 27class Frame: 28 def __init__(self, tile: Tile, channel: int | str): 29 self.tile = tile 30 if isinstance(channel, int): 31 self.channel = channel 32 if self.channel < 0 or self.channel >= len(tile.scan.channels): 33 raise ValueError( 34 f"Channel index {self.channel} is out of bounds for scan." 35 ) 36 elif isinstance(channel, str): 37 self.channel = tile.scan.get_channel_indices([channel])[0] 38 else: 39 raise ValueError("Channel must be an integer or a string.") 40 41 def __key(self) -> tuple: 42 return self.tile, self.channel 43 44 def __hash__(self) -> int: 45 return hash(self.__key()) 46 47 def __repr__(self) -> str: 48 return f"{self.tile}-{self.tile.scan.channels[self.channel].name}" 49 50 def __eq__(self, other) -> bool: 51 return self.__repr__() == other.__repr__() 52 53 def get_file_path(self, input_path: str = None, ext: str = ".tif") -> str: 54 """ 55 Get the file path for the frame, optionally changing 56 the scan path and file extension. 57 :param input_path: the path to the scan's directory. If None, defaults to 58 the path loaded in the frame's tile's scan object. 59 :param ext: the image file extension. Defaults to .tif. 60 :return: the file path. 61 """ 62 if input_path is None: 63 input_path = self.tile.scan.path 64 if len(self.tile.scan.roi) > 1: 65 input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}") 66 # Remove trailing slashes 67 if input_path[-1] == os.sep: 68 input_path = input_path[:-1] 69 # Append proc if it's pointing to the base bzScanner directory 70 if input_path.endswith("bzScanner"): 71 input_path = os.path.join(input_path, "proc") 72 # Should be a directory; append the file name 73 if os.path.isdir(input_path): 74 input_path = os.path.join(input_path, self.get_file_name(ext)) 75 else: 76 raise ValueError(f"Input path {input_path} is not a directory.") 77 return input_path 78 79 def get_file_name(self, ext: str = ".tif") -> str: 80 """ 81 Get the file name for the frame, handling different name conventions by scanner. 82 :param ext: the image file extension. Defaults to .tif. 83 :return: the file name. 84 """ 85 if self.tile.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value): 86 channel_name = self.tile.scan.channels[self.channel].name 87 x = self.tile.x 88 y = self.tile.y 89 file_name = f"{channel_name}-X{x:03}-Y{y:03}{ext}" 90 elif self.tile.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value): 91 # BZScanner has channels in a specific order 92 channel_name = self.tile.scan.channels[self.channel].name 93 real_channel_index = list( 94 self.tile.scan.BZSCANNER_CHANNEL_MAP.values() 95 ).index(channel_name) 96 # Determine total tiles 97 roi = self.tile.scan.roi[self.tile.n_roi] 98 total_tiles = roi.tile_rows * roi.tile_cols 99 # Offset is based on total tiles and "real" channel index 100 tile_offset = (real_channel_index * total_tiles) + 1 # 1-indexed 101 n_bzscanner = self.tile.n + tile_offset 102 file_name = f"Tile{n_bzscanner:06}{ext}" 103 else: 104 raise ValueError(f"Scanner {self.tile.scan.scanner_id} not supported.") 105 return file_name 106 107 def get_image(self, input_path: str = None, apply_gain: bool = True) -> np.ndarray: 108 """ 109 Loads the image for this frame. Handles .tif (will return 16-bit images) and 110 .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing 111 .jpg/.jpeg images (compressed, using .tags files). 112 :param input_path: the path to the scan's directory. If None, defaults to 113 the path loaded in the frame's tile's scan object. 114 :param apply_gain: whether to apply the gain to the image. Only has an effect 115 if the scanner calculated but did not apply gain. Defaults to True. 116 :return: the array representing the image. 117 """ 118 file_path = self.get_file_path(input_path) 119 120 # Check for the file 121 if not os.path.exists(file_path): 122 # Alternative: could be a .jpg/.jpeg file, test both 123 if os.path.exists(os.path.splitext(file_path)[0] + ".jpg"): 124 image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpg") 125 elif os.path.exists(os.path.splitext(file_path)[0] + ".jpeg"): 126 image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpeg") 127 else: 128 raise FileNotFoundError( 129 f"Could not find image at {file_path} or " 130 f"any format alternatives (.jpg, .jpeg)." 131 ) 132 else: 133 # Load the image 134 if imageio is None: 135 raise ModuleNotFoundError( 136 "imageio libraries not installed! " 137 "run `pip install csi_images[imageio]` to resolve." 138 ) 139 image = imageio.imread(file_path) 140 if image is None or image.size == 0: 141 raise ValueError(f"Could not load image from {file_path}") 142 if apply_gain and not self.tile.scan.channels[self.channel].gain_applied: 143 image = image * self.tile.scan.channels[self.channel].intensity 144 return image 145 146 @staticmethod 147 def _get_jpeg_image(input_path: str) -> np.ndarray: 148 if imageio is None: 149 raise ModuleNotFoundError( 150 "imageio libraries not installed! " 151 "run `pip install csi_images[imageio]` to resolve." 152 ) 153 image = imageio.imread(input_path) 154 if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"): 155 min_name = "PreservedMinValue" 156 max_name = "PreservedMaxValue" 157 min_value, max_value = -1, -1 158 with open(os.path.splitext(input_path)[0] + ".tags", "r") as f: 159 for line in f: 160 if line.startswith(min_name): 161 min_value = float(line.split("=")[1].strip()) 162 elif line.startswith(max_name): 163 max_value = float(line.split("=")[1].strip()) 164 if min_value != -1 and max_value != -1: 165 break 166 if min_value != -1 and max_value != -1: 167 if max_value > 1: 168 raise ValueError(f"{max_name} is greater than 1; unexpected .tags") 169 if min_value < 0: 170 raise ValueError(f"{min_name} is less than 0; unexpected .tags") 171 # Return to [0, 1], scale + offset, then return to a 16-bit image 172 image = image / np.iinfo(image.dtype).max 173 image = image * (max_value - min_value) + min_value 174 image = (image * np.iinfo(np.uint16).max).astype(np.uint16) 175 else: 176 raise ValueError( 177 f"Could not find {min_name} and {max_name} in .tags file." 178 ) 179 else: 180 raise FileNotFoundError(f"Could not find .tags file for {input_path}") 181 return image 182 183 def check_image(self, input_path: str = None) -> bool: 184 """ 185 Check if the image for this frame exists. 186 :param input_path: the path to the scan's directory. If None, defaults to 187 the path loaded in the frame's tile's scan object. 188 :return: whether the image exists. 189 """ 190 file_path = self.get_file_path(input_path) 191 # 72 is the minimum size for a valid TIFF file 192 if os.path.exists(file_path) and os.path.getsize(file_path) > 72: 193 return True 194 else: 195 # Alternative: could be a .jpg/.jpeg file, test both 196 jpeg_path = os.path.splitext(file_path)[0] + ".jpg" 197 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 198 file_path = jpeg_path 199 jpeg_path = os.path.splitext(file_path)[0] + ".jpeg" 200 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 201 file_path = jpeg_path 202 # If we've found a .jpg/.jpeg, it must have a .tags file with it 203 if file_path == jpeg_path: 204 tags_path = os.path.splitext(file_path)[0] + ".tags" 205 # Tags are text files that should include at least a few bytes 206 if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20: 207 return True 208 # Didn't hit any of those, return false 209 return False 210 211 @classmethod 212 def check_all_images(cls, scan: Scan): 213 """ 214 Check if all images for a scan exist, either in .tif or .jpg form. 215 :param scan: 216 :return: 217 """ 218 for n in range(len(scan.roi)): 219 for frames in cls.get_all_frames(scan, n_roi=n): 220 for frame in frames: 221 if not frame.check_image(): 222 return False 223 return True 224 225 @classmethod 226 def get_frames( 227 cls, 228 tile: Tile, 229 channels: Iterable[int | str] = None, 230 ) -> list[Self]: 231 """ 232 Get the frames for a tile and a set of channels. By default, gets all channels. 233 :param tile: the tile. 234 :param channels: the channels, as indices or names. Defaults to all channels. 235 :return: the frames, in order of the channels. 236 """ 237 if channels is None: 238 channels = range(len(tile.scan.channels)) 239 240 frames = [] 241 for channel in channels: 242 frames.append(Frame(tile, channel)) 243 return frames 244 245 @classmethod 246 def get_all_frames( 247 cls, 248 scan: Scan, 249 channels: Iterable[int | str] = None, 250 n_roi: int = 0, 251 as_flat: bool = True, 252 ) -> list[list[Self]] | list[list[list[Self]]]: 253 """ 254 Get all frames for a scan and a set of channels. 255 :param scan: the scan metadata. 256 :param channels: the channels, as indices or names. Defaults to all channels. 257 :param n_roi: the region of interest to use. Defaults to 0. 258 :param as_flat: whether to flatten the frames into a 2D list. 259 :return: if as_flat: 2D list of frames, organized as [n][channel]; 260 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel]. 261 """ 262 if as_flat: 263 frames = [] 264 for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols): 265 tile = Tile(scan, n, n_roi) 266 frames.append(cls.get_frames(tile, channels)) 267 else: 268 frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows 269 for x in range(scan.roi[n_roi].tile_cols): 270 for y in range(scan.roi[n_roi].tile_rows): 271 tile = Tile(scan, (x, y), n_roi) 272 frames[y][x] = cls.get_frames(tile, channels) 273 return frames 274 275 @classmethod 276 def make_rgb_image( 277 cls, 278 tile: Tile, 279 channels: dict[int, tuple[float, float, float]], 280 input_path=None, 281 ) -> np.ndarray: 282 """ 283 Convenience method for creating an RGB image from a tile and a set of channels 284 without manually extracting any frames. 285 :param tile: the tile for which the image should be made. 286 :param channels: a dictionary of scan channel indices and RGB gains. 287 :param input_path: the path to the input images. Will use metadata if not provided. 288 :return: the image as a numpy array. 289 """ 290 if csi_images is None: 291 raise ModuleNotFoundError( 292 "imageio libraries not installed! " 293 "run `pip install csi_images[imageio]` to resolve." 294 ) 295 images = [] 296 colors = [] 297 for channel_index, color in channels.items(): 298 if channel_index == -1: 299 continue 300 image = Frame(tile, channel_index).get_image(input_path) 301 images.append(image) 302 colors.append(color) 303 return csi_images.make_rgb(images, colors)
28class Frame: 29 def __init__(self, tile: Tile, channel: int | str): 30 self.tile = tile 31 if isinstance(channel, int): 32 self.channel = channel 33 if self.channel < 0 or self.channel >= len(tile.scan.channels): 34 raise ValueError( 35 f"Channel index {self.channel} is out of bounds for scan." 36 ) 37 elif isinstance(channel, str): 38 self.channel = tile.scan.get_channel_indices([channel])[0] 39 else: 40 raise ValueError("Channel must be an integer or a string.") 41 42 def __key(self) -> tuple: 43 return self.tile, self.channel 44 45 def __hash__(self) -> int: 46 return hash(self.__key()) 47 48 def __repr__(self) -> str: 49 return f"{self.tile}-{self.tile.scan.channels[self.channel].name}" 50 51 def __eq__(self, other) -> bool: 52 return self.__repr__() == other.__repr__() 53 54 def get_file_path(self, input_path: str = None, ext: str = ".tif") -> str: 55 """ 56 Get the file path for the frame, optionally changing 57 the scan path and file extension. 58 :param input_path: the path to the scan's directory. If None, defaults to 59 the path loaded in the frame's tile's scan object. 60 :param ext: the image file extension. Defaults to .tif. 61 :return: the file path. 62 """ 63 if input_path is None: 64 input_path = self.tile.scan.path 65 if len(self.tile.scan.roi) > 1: 66 input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}") 67 # Remove trailing slashes 68 if input_path[-1] == os.sep: 69 input_path = input_path[:-1] 70 # Append proc if it's pointing to the base bzScanner directory 71 if input_path.endswith("bzScanner"): 72 input_path = os.path.join(input_path, "proc") 73 # Should be a directory; append the file name 74 if os.path.isdir(input_path): 75 input_path = os.path.join(input_path, self.get_file_name(ext)) 76 else: 77 raise ValueError(f"Input path {input_path} is not a directory.") 78 return input_path 79 80 def get_file_name(self, ext: str = ".tif") -> str: 81 """ 82 Get the file name for the frame, handling different name conventions by scanner. 83 :param ext: the image file extension. Defaults to .tif. 84 :return: the file name. 85 """ 86 if self.tile.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value): 87 channel_name = self.tile.scan.channels[self.channel].name 88 x = self.tile.x 89 y = self.tile.y 90 file_name = f"{channel_name}-X{x:03}-Y{y:03}{ext}" 91 elif self.tile.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value): 92 # BZScanner has channels in a specific order 93 channel_name = self.tile.scan.channels[self.channel].name 94 real_channel_index = list( 95 self.tile.scan.BZSCANNER_CHANNEL_MAP.values() 96 ).index(channel_name) 97 # Determine total tiles 98 roi = self.tile.scan.roi[self.tile.n_roi] 99 total_tiles = roi.tile_rows * roi.tile_cols 100 # Offset is based on total tiles and "real" channel index 101 tile_offset = (real_channel_index * total_tiles) + 1 # 1-indexed 102 n_bzscanner = self.tile.n + tile_offset 103 file_name = f"Tile{n_bzscanner:06}{ext}" 104 else: 105 raise ValueError(f"Scanner {self.tile.scan.scanner_id} not supported.") 106 return file_name 107 108 def get_image(self, input_path: str = None, apply_gain: bool = True) -> np.ndarray: 109 """ 110 Loads the image for this frame. Handles .tif (will return 16-bit images) and 111 .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing 112 .jpg/.jpeg images (compressed, using .tags files). 113 :param input_path: the path to the scan's directory. If None, defaults to 114 the path loaded in the frame's tile's scan object. 115 :param apply_gain: whether to apply the gain to the image. Only has an effect 116 if the scanner calculated but did not apply gain. Defaults to True. 117 :return: the array representing the image. 118 """ 119 file_path = self.get_file_path(input_path) 120 121 # Check for the file 122 if not os.path.exists(file_path): 123 # Alternative: could be a .jpg/.jpeg file, test both 124 if os.path.exists(os.path.splitext(file_path)[0] + ".jpg"): 125 image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpg") 126 elif os.path.exists(os.path.splitext(file_path)[0] + ".jpeg"): 127 image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpeg") 128 else: 129 raise FileNotFoundError( 130 f"Could not find image at {file_path} or " 131 f"any format alternatives (.jpg, .jpeg)." 132 ) 133 else: 134 # Load the image 135 if imageio is None: 136 raise ModuleNotFoundError( 137 "imageio libraries not installed! " 138 "run `pip install csi_images[imageio]` to resolve." 139 ) 140 image = imageio.imread(file_path) 141 if image is None or image.size == 0: 142 raise ValueError(f"Could not load image from {file_path}") 143 if apply_gain and not self.tile.scan.channels[self.channel].gain_applied: 144 image = image * self.tile.scan.channels[self.channel].intensity 145 return image 146 147 @staticmethod 148 def _get_jpeg_image(input_path: str) -> np.ndarray: 149 if imageio is None: 150 raise ModuleNotFoundError( 151 "imageio libraries not installed! " 152 "run `pip install csi_images[imageio]` to resolve." 153 ) 154 image = imageio.imread(input_path) 155 if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"): 156 min_name = "PreservedMinValue" 157 max_name = "PreservedMaxValue" 158 min_value, max_value = -1, -1 159 with open(os.path.splitext(input_path)[0] + ".tags", "r") as f: 160 for line in f: 161 if line.startswith(min_name): 162 min_value = float(line.split("=")[1].strip()) 163 elif line.startswith(max_name): 164 max_value = float(line.split("=")[1].strip()) 165 if min_value != -1 and max_value != -1: 166 break 167 if min_value != -1 and max_value != -1: 168 if max_value > 1: 169 raise ValueError(f"{max_name} is greater than 1; unexpected .tags") 170 if min_value < 0: 171 raise ValueError(f"{min_name} is less than 0; unexpected .tags") 172 # Return to [0, 1], scale + offset, then return to a 16-bit image 173 image = image / np.iinfo(image.dtype).max 174 image = image * (max_value - min_value) + min_value 175 image = (image * np.iinfo(np.uint16).max).astype(np.uint16) 176 else: 177 raise ValueError( 178 f"Could not find {min_name} and {max_name} in .tags file." 179 ) 180 else: 181 raise FileNotFoundError(f"Could not find .tags file for {input_path}") 182 return image 183 184 def check_image(self, input_path: str = None) -> bool: 185 """ 186 Check if the image for this frame exists. 187 :param input_path: the path to the scan's directory. If None, defaults to 188 the path loaded in the frame's tile's scan object. 189 :return: whether the image exists. 190 """ 191 file_path = self.get_file_path(input_path) 192 # 72 is the minimum size for a valid TIFF file 193 if os.path.exists(file_path) and os.path.getsize(file_path) > 72: 194 return True 195 else: 196 # Alternative: could be a .jpg/.jpeg file, test both 197 jpeg_path = os.path.splitext(file_path)[0] + ".jpg" 198 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 199 file_path = jpeg_path 200 jpeg_path = os.path.splitext(file_path)[0] + ".jpeg" 201 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 202 file_path = jpeg_path 203 # If we've found a .jpg/.jpeg, it must have a .tags file with it 204 if file_path == jpeg_path: 205 tags_path = os.path.splitext(file_path)[0] + ".tags" 206 # Tags are text files that should include at least a few bytes 207 if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20: 208 return True 209 # Didn't hit any of those, return false 210 return False 211 212 @classmethod 213 def check_all_images(cls, scan: Scan): 214 """ 215 Check if all images for a scan exist, either in .tif or .jpg form. 216 :param scan: 217 :return: 218 """ 219 for n in range(len(scan.roi)): 220 for frames in cls.get_all_frames(scan, n_roi=n): 221 for frame in frames: 222 if not frame.check_image(): 223 return False 224 return True 225 226 @classmethod 227 def get_frames( 228 cls, 229 tile: Tile, 230 channels: Iterable[int | str] = None, 231 ) -> list[Self]: 232 """ 233 Get the frames for a tile and a set of channels. By default, gets all channels. 234 :param tile: the tile. 235 :param channels: the channels, as indices or names. Defaults to all channels. 236 :return: the frames, in order of the channels. 237 """ 238 if channels is None: 239 channels = range(len(tile.scan.channels)) 240 241 frames = [] 242 for channel in channels: 243 frames.append(Frame(tile, channel)) 244 return frames 245 246 @classmethod 247 def get_all_frames( 248 cls, 249 scan: Scan, 250 channels: Iterable[int | str] = None, 251 n_roi: int = 0, 252 as_flat: bool = True, 253 ) -> list[list[Self]] | list[list[list[Self]]]: 254 """ 255 Get all frames for a scan and a set of channels. 256 :param scan: the scan metadata. 257 :param channels: the channels, as indices or names. Defaults to all channels. 258 :param n_roi: the region of interest to use. Defaults to 0. 259 :param as_flat: whether to flatten the frames into a 2D list. 260 :return: if as_flat: 2D list of frames, organized as [n][channel]; 261 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel]. 262 """ 263 if as_flat: 264 frames = [] 265 for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols): 266 tile = Tile(scan, n, n_roi) 267 frames.append(cls.get_frames(tile, channels)) 268 else: 269 frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows 270 for x in range(scan.roi[n_roi].tile_cols): 271 for y in range(scan.roi[n_roi].tile_rows): 272 tile = Tile(scan, (x, y), n_roi) 273 frames[y][x] = cls.get_frames(tile, channels) 274 return frames 275 276 @classmethod 277 def make_rgb_image( 278 cls, 279 tile: Tile, 280 channels: dict[int, tuple[float, float, float]], 281 input_path=None, 282 ) -> np.ndarray: 283 """ 284 Convenience method for creating an RGB image from a tile and a set of channels 285 without manually extracting any frames. 286 :param tile: the tile for which the image should be made. 287 :param channels: a dictionary of scan channel indices and RGB gains. 288 :param input_path: the path to the input images. Will use metadata if not provided. 289 :return: the image as a numpy array. 290 """ 291 if csi_images is None: 292 raise ModuleNotFoundError( 293 "imageio libraries not installed! " 294 "run `pip install csi_images[imageio]` to resolve." 295 ) 296 images = [] 297 colors = [] 298 for channel_index, color in channels.items(): 299 if channel_index == -1: 300 continue 301 image = Frame(tile, channel_index).get_image(input_path) 302 images.append(image) 303 colors.append(color) 304 return csi_images.make_rgb(images, colors)
29 def __init__(self, tile: Tile, channel: int | str): 30 self.tile = tile 31 if isinstance(channel, int): 32 self.channel = channel 33 if self.channel < 0 or self.channel >= len(tile.scan.channels): 34 raise ValueError( 35 f"Channel index {self.channel} is out of bounds for scan." 36 ) 37 elif isinstance(channel, str): 38 self.channel = tile.scan.get_channel_indices([channel])[0] 39 else: 40 raise ValueError("Channel must be an integer or a string.")
54 def get_file_path(self, input_path: str = None, ext: str = ".tif") -> str: 55 """ 56 Get the file path for the frame, optionally changing 57 the scan path and file extension. 58 :param input_path: the path to the scan's directory. If None, defaults to 59 the path loaded in the frame's tile's scan object. 60 :param ext: the image file extension. Defaults to .tif. 61 :return: the file path. 62 """ 63 if input_path is None: 64 input_path = self.tile.scan.path 65 if len(self.tile.scan.roi) > 1: 66 input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}") 67 # Remove trailing slashes 68 if input_path[-1] == os.sep: 69 input_path = input_path[:-1] 70 # Append proc if it's pointing to the base bzScanner directory 71 if input_path.endswith("bzScanner"): 72 input_path = os.path.join(input_path, "proc") 73 # Should be a directory; append the file name 74 if os.path.isdir(input_path): 75 input_path = os.path.join(input_path, self.get_file_name(ext)) 76 else: 77 raise ValueError(f"Input path {input_path} is not a directory.") 78 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.
- ext: the image file extension. Defaults to .tif.
Returns
the file path.
80 def get_file_name(self, ext: str = ".tif") -> str: 81 """ 82 Get the file name for the frame, handling different name conventions by scanner. 83 :param ext: the image file extension. Defaults to .tif. 84 :return: the file name. 85 """ 86 if self.tile.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value): 87 channel_name = self.tile.scan.channels[self.channel].name 88 x = self.tile.x 89 y = self.tile.y 90 file_name = f"{channel_name}-X{x:03}-Y{y:03}{ext}" 91 elif self.tile.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value): 92 # BZScanner has channels in a specific order 93 channel_name = self.tile.scan.channels[self.channel].name 94 real_channel_index = list( 95 self.tile.scan.BZSCANNER_CHANNEL_MAP.values() 96 ).index(channel_name) 97 # Determine total tiles 98 roi = self.tile.scan.roi[self.tile.n_roi] 99 total_tiles = roi.tile_rows * roi.tile_cols 100 # Offset is based on total tiles and "real" channel index 101 tile_offset = (real_channel_index * total_tiles) + 1 # 1-indexed 102 n_bzscanner = self.tile.n + tile_offset 103 file_name = f"Tile{n_bzscanner:06}{ext}" 104 else: 105 raise ValueError(f"Scanner {self.tile.scan.scanner_id} not supported.") 106 return file_name
Get the file name for the frame, handling different name conventions by scanner.
Parameters
- ext: the image file extension. Defaults to .tif.
Returns
the file name.
108 def get_image(self, input_path: str = None, apply_gain: bool = True) -> np.ndarray: 109 """ 110 Loads the image for this frame. Handles .tif (will return 16-bit images) and 111 .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing 112 .jpg/.jpeg images (compressed, using .tags files). 113 :param input_path: the path to the scan's directory. If None, defaults to 114 the path loaded in the frame's tile's scan object. 115 :param apply_gain: whether to apply the gain to the image. Only has an effect 116 if the scanner calculated but did not apply gain. Defaults to True. 117 :return: the array representing the image. 118 """ 119 file_path = self.get_file_path(input_path) 120 121 # Check for the file 122 if not os.path.exists(file_path): 123 # Alternative: could be a .jpg/.jpeg file, test both 124 if os.path.exists(os.path.splitext(file_path)[0] + ".jpg"): 125 image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpg") 126 elif os.path.exists(os.path.splitext(file_path)[0] + ".jpeg"): 127 image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpeg") 128 else: 129 raise FileNotFoundError( 130 f"Could not find image at {file_path} or " 131 f"any format alternatives (.jpg, .jpeg)." 132 ) 133 else: 134 # Load the image 135 if imageio is None: 136 raise ModuleNotFoundError( 137 "imageio libraries not installed! " 138 "run `pip install csi_images[imageio]` to resolve." 139 ) 140 image = imageio.imread(file_path) 141 if image is None or image.size == 0: 142 raise ValueError(f"Could not load image from {file_path}") 143 if apply_gain and not self.tile.scan.channels[self.channel].gain_applied: 144 image = image * self.tile.scan.channels[self.channel].intensity 145 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.
- apply_gain: whether to apply the gain to the image. Only has an effect if the scanner calculated but did not apply gain. Defaults to True.
Returns
the array representing the image.
184 def check_image(self, input_path: str = None) -> bool: 185 """ 186 Check if the image for this frame exists. 187 :param input_path: the path to the scan's directory. If None, defaults to 188 the path loaded in the frame's tile's scan object. 189 :return: whether the image exists. 190 """ 191 file_path = self.get_file_path(input_path) 192 # 72 is the minimum size for a valid TIFF file 193 if os.path.exists(file_path) and os.path.getsize(file_path) > 72: 194 return True 195 else: 196 # Alternative: could be a .jpg/.jpeg file, test both 197 jpeg_path = os.path.splitext(file_path)[0] + ".jpg" 198 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 199 file_path = jpeg_path 200 jpeg_path = os.path.splitext(file_path)[0] + ".jpeg" 201 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 202 file_path = jpeg_path 203 # If we've found a .jpg/.jpeg, it must have a .tags file with it 204 if file_path == jpeg_path: 205 tags_path = os.path.splitext(file_path)[0] + ".tags" 206 # Tags are text files that should include at least a few bytes 207 if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20: 208 return True 209 # Didn't hit any of those, return false 210 return False
Check if the image for this frame exists.
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
whether the image exists.
212 @classmethod 213 def check_all_images(cls, scan: Scan): 214 """ 215 Check if all images for a scan exist, either in .tif or .jpg form. 216 :param scan: 217 :return: 218 """ 219 for n in range(len(scan.roi)): 220 for frames in cls.get_all_frames(scan, n_roi=n): 221 for frame in frames: 222 if not frame.check_image(): 223 return False 224 return True
Check if all images for a scan exist, either in .tif or .jpg form.
Parameters
- scan:
Returns
226 @classmethod 227 def get_frames( 228 cls, 229 tile: Tile, 230 channels: Iterable[int | str] = None, 231 ) -> list[Self]: 232 """ 233 Get the frames for a tile and a set of channels. By default, gets all channels. 234 :param tile: the tile. 235 :param channels: the channels, as indices or names. Defaults to all channels. 236 :return: the frames, in order of the channels. 237 """ 238 if channels is None: 239 channels = range(len(tile.scan.channels)) 240 241 frames = [] 242 for channel in channels: 243 frames.append(Frame(tile, channel)) 244 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.
246 @classmethod 247 def get_all_frames( 248 cls, 249 scan: Scan, 250 channels: Iterable[int | str] = None, 251 n_roi: int = 0, 252 as_flat: bool = True, 253 ) -> list[list[Self]] | list[list[list[Self]]]: 254 """ 255 Get all frames for a scan and a set of channels. 256 :param scan: the scan metadata. 257 :param channels: the channels, as indices or names. Defaults to all channels. 258 :param n_roi: the region of interest to use. Defaults to 0. 259 :param as_flat: whether to flatten the frames into a 2D list. 260 :return: if as_flat: 2D list of frames, organized as [n][channel]; 261 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel]. 262 """ 263 if as_flat: 264 frames = [] 265 for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols): 266 tile = Tile(scan, n, n_roi) 267 frames.append(cls.get_frames(tile, channels)) 268 else: 269 frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows 270 for x in range(scan.roi[n_roi].tile_cols): 271 for y in range(scan.roi[n_roi].tile_rows): 272 tile = Tile(scan, (x, y), n_roi) 273 frames[y][x] = cls.get_frames(tile, channels) 274 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].
276 @classmethod 277 def make_rgb_image( 278 cls, 279 tile: Tile, 280 channels: dict[int, tuple[float, float, float]], 281 input_path=None, 282 ) -> np.ndarray: 283 """ 284 Convenience method for creating an RGB image from a tile and a set of channels 285 without manually extracting any frames. 286 :param tile: the tile for which the image should be made. 287 :param channels: a dictionary of scan channel indices and RGB gains. 288 :param input_path: the path to the input images. Will use metadata if not provided. 289 :return: the image as a numpy array. 290 """ 291 if csi_images is None: 292 raise ModuleNotFoundError( 293 "imageio libraries not installed! " 294 "run `pip install csi_images[imageio]` to resolve." 295 ) 296 images = [] 297 colors = [] 298 for channel_index, color in channels.items(): 299 if channel_index == -1: 300 continue 301 image = Frame(tile, channel_index).get_image(input_path) 302 images.append(image) 303 colors.append(color) 304 return csi_images.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.