elf.htm.visualization
1import string 2from typing import Dict, List, Optional, Tuple, Union 3 4import numpy as np 5 6try: 7 import napari 8 from napari.experimental import link_layers 9except ImportError: 10 napari = None 11 link_layers = None 12 13 14def parse_wells(well_names, zero_based): 15 """@private 16 """ 17 well_x, well_y = [], [] 18 for name in well_names: 19 letter, y = name[0], int(name[1:]) 20 if not zero_based: 21 y -= 1 22 assert y >= 0 23 x = string.ascii_uppercase.index(letter) 24 well_x.append(x) 25 well_y.append(y) 26 assert len(well_x) == len(well_y) == len(well_names) 27 well_positions = {name: (x, y) for name, x, y in zip(well_names, well_x, well_y)} 28 well_start = (min(well_x), min(well_y)) 29 well_stop = (max(well_x) + 1, max(well_y) + 1) 30 return well_positions, well_start, well_stop 31 32 33def get_world_position(well_x, well_y, pos, well_shape, well_spacing, site_spacing, shape): 34 """@private 35 """ 36 unraveled = np.unravel_index([pos], well_shape) 37 pos_x, pos_y = unraveled[0][0], unraveled[1][0] 38 i = well_x * well_shape[0] + pos_x 39 j = well_y * well_shape[1] + pos_y 40 # print(well_x, well_y, pos, ":", i, j) 41 42 sx = shape[-2] 43 sy = shape[-1] 44 x = sx * i + i * site_spacing + well_x * well_spacing 45 y = sy * j + j * site_spacing + well_y * well_spacing 46 47 ndim_non_spatial = len(shape) - 2 48 world_pos = ndim_non_spatial * [0] + [x, y] 49 return world_pos 50 51 52def add_grid_sources( 53 grid_sources, well_positions, well_shape, well_spacing, site_spacing, add_source, source_settings=None 54): 55 """@private 56 """ 57 if source_settings is None: 58 source_settings = {} 59 for channel_name, well_sources in grid_sources.items(): 60 shape = None 61 settings = source_settings.get(channel_name, {}) 62 channel_layers = [] 63 for well_name, sources in well_sources.items(): 64 well_x, well_y = well_positions[well_name] 65 for pos, source in enumerate(sources): 66 layer = add_source(source, name=f"{channel_name}_{well_name}_{pos}", **settings) 67 if shape is None: 68 shape = source.shape 69 if "scale" in settings: 70 scale = settings["scale"] 71 assert len(scale) == len(shape) 72 shape = tuple(sc * sh for sc, sh in zip(shape, scale)) 73 else: 74 assert source.shape == shape, f"{source.shape}, {shape}" 75 world_pos = get_world_position(well_x, well_y, pos, well_shape, well_spacing, site_spacing, shape) 76 layer.translate = world_pos 77 channel_layers.append(layer) 78 link_layers(channel_layers) 79 return shape 80 81 82def _add_layout(viewer, name, box_names, boxes, edge_width, measurements=None, color="coral"): 83 properties = {"names": box_names} 84 text = "{names}" 85 if measurements is not None: 86 text += " - " 87 for measure_name, measure_values in measurements.items(): 88 this_measurements = [measure_values[box_name] for box_name in box_names] 89 properties[measure_name] = this_measurements 90 if isinstance(this_measurements[0], float): 91 text += f"{measure_name}: {{{measure_name}:0.2f}}," 92 else: 93 text += f"{measure_name}: {{{measure_name}}}," 94 # get rid of the last comma 95 text = text[:-1] 96 text_properties = { 97 "text": text, 98 "anchor": "upper_left", 99 "translation": [-5, 0], 100 "size": 32, 101 "color": "coral" 102 } 103 viewer.add_shapes(boxes, 104 name=name, 105 properties=properties, 106 text=text_properties, 107 shape_type="rectangle", 108 edge_width=edge_width, 109 edge_color="coral", 110 face_color="transparent") 111 112 113def add_plate_layout( 114 viewer, well_names, well_positions, well_shape, well_spacing, site_spacing, shape, 115 measurements=None 116): 117 """@private 118 """ 119 well_boxes = [] 120 for well_name in well_names: 121 well_x, well_y = well_positions[well_name] 122 xmin, ymin = get_world_position( 123 well_x, well_y, 0, well_shape, well_spacing, site_spacing, shape 124 )[-2:] 125 xmin -= well_spacing // 2 126 ymin -= well_spacing // 2 127 128 xmax, ymax = get_world_position( 129 well_x + 1, well_y + 1, 0, well_shape, well_spacing, site_spacing, shape 130 )[-2:] 131 xmax += well_spacing // 2 132 ymax += well_spacing // 2 133 134 well_boxes.append(np.array([[xmin, ymin], [xmax, ymax]])) 135 _add_layout(viewer, "wells", well_names, well_boxes, well_spacing // 2, 136 measurements=measurements) 137 138 139def set_camera(viewer, well_start, well_stop, well_shape, well_spacing, site_spacing, shape): 140 """@private 141 """ 142 pix_start = get_world_position( 143 well_start[0], well_start[1], 0, well_shape, well_spacing, site_spacing, shape 144 )[-2:] 145 pix_stop = get_world_position( 146 well_stop[0], well_stop[1], 0, well_shape, well_spacing, site_spacing, shape 147 )[-2:] 148 camera_center = tuple((start + stop) // 2 for start, stop in zip(pix_start, pix_stop)) 149 viewer.camera.center = camera_center 150 151 # zoom out so that we see all wells 152 plate_extent = [wstop - wstart for wstart, wstop in zip(well_start, well_stop)] 153 max_extent = max(plate_extent) 154 max_len = well_shape[np.argmax(plate_extent)] 155 viewer.camera.zoom /= (max_len * max_extent) 156 157 158def view_plate( 159 image_data: Dict[str, Dict[str, List[np.ndarray]]], 160 label_data: Optional[Dict[str, Dict[str, List[np.ndarray]]]] = None, 161 image_settings: Optional[Dict[str, Dict]] = None, 162 label_settings: Optional[Dict[str, Dict]] = None, 163 well_measurements: Optional[Dict[str, Dict[str, Union[float, int, str]]]] = None, 164 well_shape: Optional[Tuple[int, int]] = None, 165 zero_based: bool = True, 166 well_spacing: int = 16, 167 site_spacing: int = 4, 168 show: bool = True, 169) -> "napari.Viewer": 170 """Visualize data from a multi-well plate using napari. 171 172 Args: 173 image_data: Dict of image sources, each dict must map the channel names to 174 a dict which maps the well names (e.g. A1, B3) to 175 the image data for this well (one array per well position). 176 label_data: Dict of label sources, each dict must map the label name to 177 a dict which maps the well names (e.g. A1, B3) to 178 the label data for this well (one array per well position). 179 image_settings: Image settings for the channels. 180 label_settings: Settings for the label layers. 181 well_measurements: Measurements associated with the wells. 182 well_shape: the 2D shape of a well in terms of images, if not given will be derived. 183 Well shape can only be derived for square wells and must be passed otherwise. 184 zero_based: Whether the well indexing is zero-based. 185 well_spacing: Spacing between wells, in pixels. 186 site_spacing: Spacing between sites, in pixels. 187 show: Whether to show the viewer. 188 189 Returns: 190 The napari viewer. 191 """ 192 assert napari is not None and link_layers is not None, "Requires napari" 193 194 # find the number of positions per well 195 first_channel_sources = next(iter(image_data.values())) 196 pos_per_well = len(next(iter(first_channel_sources.values()))) 197 198 # find the well shape 199 if well_shape is None: # well shape can only be derived for square wells 200 assert pos_per_well in (1, 4, 9, 25, 36, 49), f"well is not square: {pos_per_well}" 201 well_len = int(np.sqrt(pos_per_well)) 202 well_shape = (well_len, well_len) 203 else: 204 assert len(well_shape) == 2 205 pos_per_well_exp = np.prod(list(well_shape)) 206 assert pos_per_well_exp == pos_per_well, f"{pos_per_well_exp} != {pos_per_well}" 207 208 def process_sources(sources, well_names): 209 for well_sources in sources.values(): 210 # make sure all wells have the same number of label 211 n_pos_well = [len(sources) for sources in well_sources.values()] 212 assert all(n_pos == pos_per_well for n_pos in n_pos_well), f"{pos_per_well} != {n_pos_well}" 213 well_names.extend(list(well_sources.keys())) 214 return well_names 215 216 # find the well names for all sources 217 well_names = process_sources(image_data, []) 218 if label_data is not None: 219 well_names = process_sources(label_data, well_names) 220 well_names = list(set(well_names)) 221 well_names.sort() 222 223 # compute the well extent and well positions 224 well_positions, well_start, well_stop = parse_wells(well_names, zero_based) 225 assert len(well_positions) == len(well_names) 226 227 # start the veiwer and add all sources 228 viewer = napari.Viewer() 229 shape = add_grid_sources( 230 image_data, well_positions, well_shape, well_spacing, site_spacing, viewer.add_image, image_settings 231 ) 232 if label_data is not None: 233 add_grid_sources( 234 label_data, well_positions, well_shape, well_spacing, site_spacing, viewer.add_labels, label_settings 235 ) 236 237 # add shape layer corresponding to the well positions 238 add_plate_layout( 239 viewer, well_names, well_positions, well_shape, well_spacing, site_spacing, shape, 240 measurements=well_measurements 241 ) 242 243 # set the camera so that the initial view is centered around the existing wells 244 # and zoom out so that the central well is fully visible 245 set_camera(viewer, well_start, well_stop, well_shape, well_spacing, site_spacing, shape) 246 247 if show: 248 napari.run() 249 return viewer 250 251 252def add_positional_sources(positional_sources, positions, add_source, source_settings=None): 253 """@private 254 """ 255 if source_settings is None: 256 source_settings = {} 257 for channel_name, sources in positional_sources.items(): 258 settings = source_settings.get(channel_name, {}) 259 channel_layers = [] 260 for sample, source in sources.items(): 261 layer = add_source(source, name=f"{channel_name}_{sample}", **settings) 262 position = positions[sample] 263 if len(source.shape) > len(position): 264 ndim_non_spatial = len(source.shape) - len(position) 265 position = ndim_non_spatial * [0] + list(position) 266 layer.translate = list(position) 267 channel_layers.append(layer) 268 shape = source.shape 269 link_layers(channel_layers) 270 return shape 271 272 273def set_camera_positional(viewer, positions, shape): 274 """@private 275 """ 276 coords = list(positions.values()) 277 y = [coord[0] for coord in coords] 278 x = [coord[1] for coord in coords] 279 ymin, ymax = min(y), max(y) 280 xmin, xmax = min(x), max(x) 281 282 camera_center = [(ymax - ymin) // 2, (xmax - xmin) // 2] 283 viewer.camera.center = camera_center 284 285 extent = (ymax - ymin, xmax - xmin) 286 max_extent = max(extent) 287 zoom_out = max_extent / shape[np.argmax(extent)] 288 viewer.camera.zoom /= zoom_out 289 290 291def add_positional_layout(viewer, positions, shape, measurements=None, spacing=16): 292 """@private 293 """ 294 boxes = [] 295 sample_names = [] 296 for sample, position in positions.items(): 297 ymin, xmin = position 298 ymax, xmax = ymin + shape[0], xmin + shape[1] 299 300 xmin -= spacing 301 ymin -= spacing 302 xmax += spacing 303 ymax += spacing 304 305 boxes.append(np.array([[ymin, xmin], [ymax, xmax]])) 306 sample_names.append(sample) 307 _add_layout(viewer, "samples", sample_names, boxes, spacing // 2, measurements=measurements) 308 309 310def view_positional_images( 311 image_data: Dict[str, Dict[str, np.ndarray]], 312 positions: Dict[str, Tuple[int, int]], 313 label_data: Optional[Dict[str, Dict[str, np.ndarray]]] = None, 314 image_settings: Optional[Dict[str, Dict]] = None, 315 label_settings: Optional[Dict[str, Dict]] = None, 316 sample_measurements: Optional[Dict[str, Dict[str, Union[float, int, str]]]] = None, 317 show: bool = True, 318) -> "napari.Viewer": 319 """Similar to `view_plate`, but using position data to place the images. 320 321 Args: 322 image_data: The image data (outer dict is channels, inner is samples). 323 positions: The position for each sample. 324 label_data: The label data (outer dict is channels, inner is sample). 325 image_settings: Image settings for the channels. 326 label_settings: Settings for the label data. 327 sample_measurements: Measurements associated with the samples. 328 show: Whether to show the viewer. 329 330 Returns: 331 The napari viewer. 332 """ 333 assert napari is not None and link_layers is not None, "Requires napari" 334 335 all_samples = [] 336 for sources in image_data.values(): 337 all_samples.extend(list(sources.keys())) 338 if label_data is not None: 339 for sources in label_data.values(): 340 all_samples.extend(list(sources.keys())) 341 342 # make sure we have positional data for all the samples 343 assert all(sample in positions for sample in all_samples) 344 345 viewer = napari.Viewer() 346 347 shape = add_positional_sources(image_data, positions, viewer.add_image, image_settings) 348 if label_data is not None: 349 add_positional_sources(label_data, positions, viewer.add_labels, label_settings) 350 351 add_positional_layout(viewer, positions, shape, measurements=sample_measurements) 352 353 set_camera_positional(viewer, positions, shape) 354 355 if show: 356 napari.run() 357 return viewer
def
view_plate( image_data: Dict[str, Dict[str, List[numpy.ndarray]]], label_data: Dict[str, Dict[str, List[numpy.ndarray]]] | None = None, image_settings: Dict[str, Dict] | None = None, label_settings: Dict[str, Dict] | None = None, well_measurements: Dict[str, Dict[str, float | int | str]] | None = None, well_shape: Tuple[int, int] | None = None, zero_based: bool = True, well_spacing: int = 16, site_spacing: int = 4, show: bool = True) -> napari.viewer.Viewer:
159def view_plate( 160 image_data: Dict[str, Dict[str, List[np.ndarray]]], 161 label_data: Optional[Dict[str, Dict[str, List[np.ndarray]]]] = None, 162 image_settings: Optional[Dict[str, Dict]] = None, 163 label_settings: Optional[Dict[str, Dict]] = None, 164 well_measurements: Optional[Dict[str, Dict[str, Union[float, int, str]]]] = None, 165 well_shape: Optional[Tuple[int, int]] = None, 166 zero_based: bool = True, 167 well_spacing: int = 16, 168 site_spacing: int = 4, 169 show: bool = True, 170) -> "napari.Viewer": 171 """Visualize data from a multi-well plate using napari. 172 173 Args: 174 image_data: Dict of image sources, each dict must map the channel names to 175 a dict which maps the well names (e.g. A1, B3) to 176 the image data for this well (one array per well position). 177 label_data: Dict of label sources, each dict must map the label name to 178 a dict which maps the well names (e.g. A1, B3) to 179 the label data for this well (one array per well position). 180 image_settings: Image settings for the channels. 181 label_settings: Settings for the label layers. 182 well_measurements: Measurements associated with the wells. 183 well_shape: the 2D shape of a well in terms of images, if not given will be derived. 184 Well shape can only be derived for square wells and must be passed otherwise. 185 zero_based: Whether the well indexing is zero-based. 186 well_spacing: Spacing between wells, in pixels. 187 site_spacing: Spacing between sites, in pixels. 188 show: Whether to show the viewer. 189 190 Returns: 191 The napari viewer. 192 """ 193 assert napari is not None and link_layers is not None, "Requires napari" 194 195 # find the number of positions per well 196 first_channel_sources = next(iter(image_data.values())) 197 pos_per_well = len(next(iter(first_channel_sources.values()))) 198 199 # find the well shape 200 if well_shape is None: # well shape can only be derived for square wells 201 assert pos_per_well in (1, 4, 9, 25, 36, 49), f"well is not square: {pos_per_well}" 202 well_len = int(np.sqrt(pos_per_well)) 203 well_shape = (well_len, well_len) 204 else: 205 assert len(well_shape) == 2 206 pos_per_well_exp = np.prod(list(well_shape)) 207 assert pos_per_well_exp == pos_per_well, f"{pos_per_well_exp} != {pos_per_well}" 208 209 def process_sources(sources, well_names): 210 for well_sources in sources.values(): 211 # make sure all wells have the same number of label 212 n_pos_well = [len(sources) for sources in well_sources.values()] 213 assert all(n_pos == pos_per_well for n_pos in n_pos_well), f"{pos_per_well} != {n_pos_well}" 214 well_names.extend(list(well_sources.keys())) 215 return well_names 216 217 # find the well names for all sources 218 well_names = process_sources(image_data, []) 219 if label_data is not None: 220 well_names = process_sources(label_data, well_names) 221 well_names = list(set(well_names)) 222 well_names.sort() 223 224 # compute the well extent and well positions 225 well_positions, well_start, well_stop = parse_wells(well_names, zero_based) 226 assert len(well_positions) == len(well_names) 227 228 # start the veiwer and add all sources 229 viewer = napari.Viewer() 230 shape = add_grid_sources( 231 image_data, well_positions, well_shape, well_spacing, site_spacing, viewer.add_image, image_settings 232 ) 233 if label_data is not None: 234 add_grid_sources( 235 label_data, well_positions, well_shape, well_spacing, site_spacing, viewer.add_labels, label_settings 236 ) 237 238 # add shape layer corresponding to the well positions 239 add_plate_layout( 240 viewer, well_names, well_positions, well_shape, well_spacing, site_spacing, shape, 241 measurements=well_measurements 242 ) 243 244 # set the camera so that the initial view is centered around the existing wells 245 # and zoom out so that the central well is fully visible 246 set_camera(viewer, well_start, well_stop, well_shape, well_spacing, site_spacing, shape) 247 248 if show: 249 napari.run() 250 return viewer
Visualize data from a multi-well plate using napari.
Arguments:
- image_data: Dict of image sources, each dict must map the channel names to a dict which maps the well names (e.g. A1, B3) to the image data for this well (one array per well position).
- label_data: Dict of label sources, each dict must map the label name to a dict which maps the well names (e.g. A1, B3) to the label data for this well (one array per well position).
- image_settings: Image settings for the channels.
- label_settings: Settings for the label layers.
- well_measurements: Measurements associated with the wells.
- well_shape: the 2D shape of a well in terms of images, if not given will be derived. Well shape can only be derived for square wells and must be passed otherwise.
- zero_based: Whether the well indexing is zero-based.
- well_spacing: Spacing between wells, in pixels.
- site_spacing: Spacing between sites, in pixels.
- show: Whether to show the viewer.
Returns:
The napari viewer.
def
view_positional_images( image_data: Dict[str, Dict[str, numpy.ndarray]], positions: Dict[str, Tuple[int, int]], label_data: Dict[str, Dict[str, numpy.ndarray]] | None = None, image_settings: Dict[str, Dict] | None = None, label_settings: Dict[str, Dict] | None = None, sample_measurements: Dict[str, Dict[str, float | int | str]] | None = None, show: bool = True) -> napari.viewer.Viewer:
311def view_positional_images( 312 image_data: Dict[str, Dict[str, np.ndarray]], 313 positions: Dict[str, Tuple[int, int]], 314 label_data: Optional[Dict[str, Dict[str, np.ndarray]]] = None, 315 image_settings: Optional[Dict[str, Dict]] = None, 316 label_settings: Optional[Dict[str, Dict]] = None, 317 sample_measurements: Optional[Dict[str, Dict[str, Union[float, int, str]]]] = None, 318 show: bool = True, 319) -> "napari.Viewer": 320 """Similar to `view_plate`, but using position data to place the images. 321 322 Args: 323 image_data: The image data (outer dict is channels, inner is samples). 324 positions: The position for each sample. 325 label_data: The label data (outer dict is channels, inner is sample). 326 image_settings: Image settings for the channels. 327 label_settings: Settings for the label data. 328 sample_measurements: Measurements associated with the samples. 329 show: Whether to show the viewer. 330 331 Returns: 332 The napari viewer. 333 """ 334 assert napari is not None and link_layers is not None, "Requires napari" 335 336 all_samples = [] 337 for sources in image_data.values(): 338 all_samples.extend(list(sources.keys())) 339 if label_data is not None: 340 for sources in label_data.values(): 341 all_samples.extend(list(sources.keys())) 342 343 # make sure we have positional data for all the samples 344 assert all(sample in positions for sample in all_samples) 345 346 viewer = napari.Viewer() 347 348 shape = add_positional_sources(image_data, positions, viewer.add_image, image_settings) 349 if label_data is not None: 350 add_positional_sources(label_data, positions, viewer.add_labels, label_settings) 351 352 add_positional_layout(viewer, positions, shape, measurements=sample_measurements) 353 354 set_camera_positional(viewer, positions, shape) 355 356 if show: 357 napari.run() 358 return viewer
Similar to view_plate, but using position data to place the images.
Arguments:
- image_data: The image data (outer dict is channels, inner is samples).
- positions: The position for each sample.
- label_data: The label data (outer dict is channels, inner is sample).
- image_settings: Image settings for the channels.
- label_settings: Settings for the label data.
- sample_measurements: Measurements associated with the samples.
- show: Whether to show the viewer.
Returns:
The napari viewer.