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