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