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.