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 # Multiply by the gain if it hasn't been applied, avoiding overflow 144 dtype = image.dtype 145 image = image.astype(np.uint32) 146 image = image * self.tile.scan.channels[self.channel].intensity 147 image = np.clip(image, 0, np.iinfo(dtype).max).astype(dtype) 148 return image 149 150 @staticmethod 151 def _get_jpeg_image(input_path: str) -> np.ndarray: 152 if imageio is None: 153 raise ModuleNotFoundError( 154 "imageio libraries not installed! " 155 "run `pip install csi_images[imageio]` to resolve." 156 ) 157 image = imageio.imread(input_path) 158 if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"): 159 min_name = "PreservedMinValue" 160 max_name = "PreservedMaxValue" 161 min_value, max_value = -1, -1 162 with open(os.path.splitext(input_path)[0] + ".tags", "r") as f: 163 for line in f: 164 if line.startswith(min_name): 165 min_value = float(line.split("=")[1].strip()) 166 elif line.startswith(max_name): 167 max_value = float(line.split("=")[1].strip()) 168 if min_value != -1 and max_value != -1: 169 break 170 if min_value != -1 and max_value != -1: 171 if max_value > 1: 172 raise ValueError(f"{max_name} is greater than 1; unexpected .tags") 173 if min_value < 0: 174 raise ValueError(f"{min_name} is less than 0; unexpected .tags") 175 # Return to [0, 1], scale + offset, then return to a 16-bit image 176 image = image / np.iinfo(image.dtype).max 177 image = image * (max_value - min_value) + min_value 178 image = (image * np.iinfo(np.uint16).max).astype(np.uint16) 179 else: 180 raise ValueError( 181 f"Could not find {min_name} and {max_name} in .tags file." 182 ) 183 else: 184 raise FileNotFoundError(f"Could not find .tags file for {input_path}") 185 return image 186 187 def check_image(self, input_path: str = None) -> bool: 188 """ 189 Check if the image for this frame exists. 190 :param input_path: the path to the scan's directory. If None, defaults to 191 the path loaded in the frame's tile's scan object. 192 :return: whether the image exists. 193 """ 194 file_path = self.get_file_path(input_path) 195 # 72 is the minimum size for a valid TIFF file 196 if os.path.exists(file_path) and os.path.getsize(file_path) > 72: 197 return True 198 else: 199 # Alternative: could be a .jpg/.jpeg file, test both 200 jpeg_path = os.path.splitext(file_path)[0] + ".jpg" 201 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 202 file_path = jpeg_path 203 jpeg_path = os.path.splitext(file_path)[0] + ".jpeg" 204 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 205 file_path = jpeg_path 206 # If we've found a .jpg/.jpeg, it must have a .tags file with it 207 if file_path == jpeg_path: 208 tags_path = os.path.splitext(file_path)[0] + ".tags" 209 # Tags are text files that should include at least a few bytes 210 if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20: 211 return True 212 # Didn't hit any of those, return false 213 return False 214 215 @classmethod 216 def check_all_images(cls, scan: Scan): 217 """ 218 Check if all images for a scan exist, either in .tif or .jpg form. 219 :param scan: 220 :return: 221 """ 222 for n in range(len(scan.roi)): 223 for frames in cls.get_all_frames(scan, n_roi=n): 224 for frame in frames: 225 if not frame.check_image(): 226 return False 227 return True 228 229 @classmethod 230 def get_frames( 231 cls, 232 tile: Tile, 233 channels: Iterable[int | str] = None, 234 ) -> list[Self]: 235 """ 236 Get the frames for a tile and a set of channels. By default, gets all channels. 237 :param tile: the tile. 238 :param channels: the channels, as indices or names. Defaults to all channels. 239 :return: the frames, in order of the channels. 240 """ 241 if channels is None: 242 channels = range(len(tile.scan.channels)) 243 244 frames = [] 245 for channel in channels: 246 frames.append(Frame(tile, channel)) 247 return frames 248 249 @classmethod 250 def get_all_frames( 251 cls, 252 scan: Scan, 253 channels: Iterable[int | str] = None, 254 n_roi: int = 0, 255 as_flat: bool = True, 256 ) -> list[list[Self]] | list[list[list[Self]]]: 257 """ 258 Get all frames for a scan and a set of channels. 259 :param scan: the scan metadata. 260 :param channels: the channels, as indices or names. Defaults to all channels. 261 :param n_roi: the region of interest to use. Defaults to 0. 262 :param as_flat: whether to flatten the frames into a 2D list. 263 :return: if as_flat: 2D list of frames, organized as [n][channel]; 264 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel]. 265 """ 266 if as_flat: 267 frames = [] 268 for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols): 269 tile = Tile(scan, n, n_roi) 270 frames.append(cls.get_frames(tile, channels)) 271 else: 272 frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows 273 for x in range(scan.roi[n_roi].tile_cols): 274 for y in range(scan.roi[n_roi].tile_rows): 275 tile = Tile(scan, (x, y), n_roi) 276 frames[y][x] = cls.get_frames(tile, channels) 277 return frames 278 279 @classmethod 280 def make_rgb_image( 281 cls, 282 tile: Tile, 283 channels: dict[int, tuple[float, float, float]], 284 input_path=None, 285 ) -> np.ndarray: 286 """ 287 Convenience method for creating an RGB image from a tile and a set of channels 288 without manually extracting any frames. 289 :param tile: the tile for which the image should be made. 290 :param channels: a dictionary of scan channel indices and RGB gains. 291 :param input_path: the path to the input images. Will use metadata if not provided. 292 :return: the image as a numpy array. 293 """ 294 if csi_images is None: 295 raise ModuleNotFoundError( 296 "imageio libraries not installed! " 297 "run `pip install csi_images[imageio]` to resolve." 298 ) 299 images = [] 300 colors = [] 301 for channel_index, color in channels.items(): 302 if channel_index == -1: 303 continue 304 image = Frame(tile, channel_index).get_image(input_path) 305 images.append(image) 306 colors.append(color) 307 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 # Multiply by the gain if it hasn't been applied, avoiding overflow 145 dtype = image.dtype 146 image = image.astype(np.uint32) 147 image = image * self.tile.scan.channels[self.channel].intensity 148 image = np.clip(image, 0, np.iinfo(dtype).max).astype(dtype) 149 return image 150 151 @staticmethod 152 def _get_jpeg_image(input_path: str) -> np.ndarray: 153 if imageio is None: 154 raise ModuleNotFoundError( 155 "imageio libraries not installed! " 156 "run `pip install csi_images[imageio]` to resolve." 157 ) 158 image = imageio.imread(input_path) 159 if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"): 160 min_name = "PreservedMinValue" 161 max_name = "PreservedMaxValue" 162 min_value, max_value = -1, -1 163 with open(os.path.splitext(input_path)[0] + ".tags", "r") as f: 164 for line in f: 165 if line.startswith(min_name): 166 min_value = float(line.split("=")[1].strip()) 167 elif line.startswith(max_name): 168 max_value = float(line.split("=")[1].strip()) 169 if min_value != -1 and max_value != -1: 170 break 171 if min_value != -1 and max_value != -1: 172 if max_value > 1: 173 raise ValueError(f"{max_name} is greater than 1; unexpected .tags") 174 if min_value < 0: 175 raise ValueError(f"{min_name} is less than 0; unexpected .tags") 176 # Return to [0, 1], scale + offset, then return to a 16-bit image 177 image = image / np.iinfo(image.dtype).max 178 image = image * (max_value - min_value) + min_value 179 image = (image * np.iinfo(np.uint16).max).astype(np.uint16) 180 else: 181 raise ValueError( 182 f"Could not find {min_name} and {max_name} in .tags file." 183 ) 184 else: 185 raise FileNotFoundError(f"Could not find .tags file for {input_path}") 186 return image 187 188 def check_image(self, input_path: str = None) -> bool: 189 """ 190 Check if the image for this frame exists. 191 :param input_path: the path to the scan's directory. If None, defaults to 192 the path loaded in the frame's tile's scan object. 193 :return: whether the image exists. 194 """ 195 file_path = self.get_file_path(input_path) 196 # 72 is the minimum size for a valid TIFF file 197 if os.path.exists(file_path) and os.path.getsize(file_path) > 72: 198 return True 199 else: 200 # Alternative: could be a .jpg/.jpeg file, test both 201 jpeg_path = os.path.splitext(file_path)[0] + ".jpg" 202 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 203 file_path = jpeg_path 204 jpeg_path = os.path.splitext(file_path)[0] + ".jpeg" 205 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 206 file_path = jpeg_path 207 # If we've found a .jpg/.jpeg, it must have a .tags file with it 208 if file_path == jpeg_path: 209 tags_path = os.path.splitext(file_path)[0] + ".tags" 210 # Tags are text files that should include at least a few bytes 211 if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20: 212 return True 213 # Didn't hit any of those, return false 214 return False 215 216 @classmethod 217 def check_all_images(cls, scan: Scan): 218 """ 219 Check if all images for a scan exist, either in .tif or .jpg form. 220 :param scan: 221 :return: 222 """ 223 for n in range(len(scan.roi)): 224 for frames in cls.get_all_frames(scan, n_roi=n): 225 for frame in frames: 226 if not frame.check_image(): 227 return False 228 return True 229 230 @classmethod 231 def get_frames( 232 cls, 233 tile: Tile, 234 channels: Iterable[int | str] = None, 235 ) -> list[Self]: 236 """ 237 Get the frames for a tile and a set of channels. By default, gets all channels. 238 :param tile: the tile. 239 :param channels: the channels, as indices or names. Defaults to all channels. 240 :return: the frames, in order of the channels. 241 """ 242 if channels is None: 243 channels = range(len(tile.scan.channels)) 244 245 frames = [] 246 for channel in channels: 247 frames.append(Frame(tile, channel)) 248 return frames 249 250 @classmethod 251 def get_all_frames( 252 cls, 253 scan: Scan, 254 channels: Iterable[int | str] = None, 255 n_roi: int = 0, 256 as_flat: bool = True, 257 ) -> list[list[Self]] | list[list[list[Self]]]: 258 """ 259 Get all frames for a scan and a set of channels. 260 :param scan: the scan metadata. 261 :param channels: the channels, as indices or names. Defaults to all channels. 262 :param n_roi: the region of interest to use. Defaults to 0. 263 :param as_flat: whether to flatten the frames into a 2D list. 264 :return: if as_flat: 2D list of frames, organized as [n][channel]; 265 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel]. 266 """ 267 if as_flat: 268 frames = [] 269 for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols): 270 tile = Tile(scan, n, n_roi) 271 frames.append(cls.get_frames(tile, channels)) 272 else: 273 frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows 274 for x in range(scan.roi[n_roi].tile_cols): 275 for y in range(scan.roi[n_roi].tile_rows): 276 tile = Tile(scan, (x, y), n_roi) 277 frames[y][x] = cls.get_frames(tile, channels) 278 return frames 279 280 @classmethod 281 def make_rgb_image( 282 cls, 283 tile: Tile, 284 channels: dict[int, tuple[float, float, float]], 285 input_path=None, 286 ) -> np.ndarray: 287 """ 288 Convenience method for creating an RGB image from a tile and a set of channels 289 without manually extracting any frames. 290 :param tile: the tile for which the image should be made. 291 :param channels: a dictionary of scan channel indices and RGB gains. 292 :param input_path: the path to the input images. Will use metadata if not provided. 293 :return: the image as a numpy array. 294 """ 295 if csi_images is None: 296 raise ModuleNotFoundError( 297 "imageio libraries not installed! " 298 "run `pip install csi_images[imageio]` to resolve." 299 ) 300 images = [] 301 colors = [] 302 for channel_index, color in channels.items(): 303 if channel_index == -1: 304 continue 305 image = Frame(tile, channel_index).get_image(input_path) 306 images.append(image) 307 colors.append(color) 308 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 # Multiply by the gain if it hasn't been applied, avoiding overflow 145 dtype = image.dtype 146 image = image.astype(np.uint32) 147 image = image * self.tile.scan.channels[self.channel].intensity 148 image = np.clip(image, 0, np.iinfo(dtype).max).astype(dtype) 149 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.
188 def check_image(self, input_path: str = None) -> bool: 189 """ 190 Check if the image for this frame exists. 191 :param input_path: the path to the scan's directory. If None, defaults to 192 the path loaded in the frame's tile's scan object. 193 :return: whether the image exists. 194 """ 195 file_path = self.get_file_path(input_path) 196 # 72 is the minimum size for a valid TIFF file 197 if os.path.exists(file_path) and os.path.getsize(file_path) > 72: 198 return True 199 else: 200 # Alternative: could be a .jpg/.jpeg file, test both 201 jpeg_path = os.path.splitext(file_path)[0] + ".jpg" 202 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 203 file_path = jpeg_path 204 jpeg_path = os.path.splitext(file_path)[0] + ".jpeg" 205 if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107: 206 file_path = jpeg_path 207 # If we've found a .jpg/.jpeg, it must have a .tags file with it 208 if file_path == jpeg_path: 209 tags_path = os.path.splitext(file_path)[0] + ".tags" 210 # Tags are text files that should include at least a few bytes 211 if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20: 212 return True 213 # Didn't hit any of those, return false 214 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.
216 @classmethod 217 def check_all_images(cls, scan: Scan): 218 """ 219 Check if all images for a scan exist, either in .tif or .jpg form. 220 :param scan: 221 :return: 222 """ 223 for n in range(len(scan.roi)): 224 for frames in cls.get_all_frames(scan, n_roi=n): 225 for frame in frames: 226 if not frame.check_image(): 227 return False 228 return True
Check if all images for a scan exist, either in .tif or .jpg form.
Parameters
- scan:
Returns
230 @classmethod 231 def get_frames( 232 cls, 233 tile: Tile, 234 channels: Iterable[int | str] = None, 235 ) -> list[Self]: 236 """ 237 Get the frames for a tile and a set of channels. By default, gets all channels. 238 :param tile: the tile. 239 :param channels: the channels, as indices or names. Defaults to all channels. 240 :return: the frames, in order of the channels. 241 """ 242 if channels is None: 243 channels = range(len(tile.scan.channels)) 244 245 frames = [] 246 for channel in channels: 247 frames.append(Frame(tile, channel)) 248 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.
250 @classmethod 251 def get_all_frames( 252 cls, 253 scan: Scan, 254 channels: Iterable[int | str] = None, 255 n_roi: int = 0, 256 as_flat: bool = True, 257 ) -> list[list[Self]] | list[list[list[Self]]]: 258 """ 259 Get all frames for a scan and a set of channels. 260 :param scan: the scan metadata. 261 :param channels: the channels, as indices or names. Defaults to all channels. 262 :param n_roi: the region of interest to use. Defaults to 0. 263 :param as_flat: whether to flatten the frames into a 2D list. 264 :return: if as_flat: 2D list of frames, organized as [n][channel]; 265 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel]. 266 """ 267 if as_flat: 268 frames = [] 269 for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols): 270 tile = Tile(scan, n, n_roi) 271 frames.append(cls.get_frames(tile, channels)) 272 else: 273 frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows 274 for x in range(scan.roi[n_roi].tile_cols): 275 for y in range(scan.roi[n_roi].tile_rows): 276 tile = Tile(scan, (x, y), n_roi) 277 frames[y][x] = cls.get_frames(tile, channels) 278 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].
280 @classmethod 281 def make_rgb_image( 282 cls, 283 tile: Tile, 284 channels: dict[int, tuple[float, float, float]], 285 input_path=None, 286 ) -> np.ndarray: 287 """ 288 Convenience method for creating an RGB image from a tile and a set of channels 289 without manually extracting any frames. 290 :param tile: the tile for which the image should be made. 291 :param channels: a dictionary of scan channel indices and RGB gains. 292 :param input_path: the path to the input images. Will use metadata if not provided. 293 :return: the image as a numpy array. 294 """ 295 if csi_images is None: 296 raise ModuleNotFoundError( 297 "imageio libraries not installed! " 298 "run `pip install csi_images[imageio]` to resolve." 299 ) 300 images = [] 301 colors = [] 302 for channel_index, color in channels.items(): 303 if channel_index == -1: 304 continue 305 image = Frame(tile, channel_index).get_image(input_path) 306 images.append(image) 307 colors.append(color) 308 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.