Source code for locan.visualize.render_napari.utilities

"""

Utility functions for interacting with napari.

"""

from __future__ import annotations

import logging
import os
from collections.abc import Iterable, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal

import numpy as np
import numpy.typing as npt

from locan.data import metadata_pb2
from locan.data.regions.region import Ellipse, Polygon, Rectangle, Region
from locan.dependencies import HAS_DEPENDENCY
from locan.rois import Roi
from locan.visualize.render_napari.render2d import render_2d_napari

if HAS_DEPENDENCY["napari"]:
    import napari

if HAS_DEPENDENCY["qt"]:
    from qtpy.QtWidgets import QFileDialog

if TYPE_CHECKING:
    from locan.data.locdata import LocData

logger = logging.getLogger(__name__)

__all__: list[str] = [
    "select_by_drawing_napari",
    "get_rois",
    "save_rois",
]


[docs] def select_by_drawing_napari( locdata: LocData, napari_run: bool = True, **kwargs: Any ) -> list[Roi]: """ Select region of interest from rendered image by drawing rois in napari. Rois will be created from shapes in napari.viewer.layers['Shapes']. Parameters ---------- locdata The localization data from which to select localization data. napari_run If `True` napari.run is called (set to `False` for testing). kwargs Other parameters passed to :func:`render_2d_napari`. Returns ------- list[Roi] See Also -------- :func:`locan.scripts.rois` : script for drawing rois """ # select roi viewer = render_2d_napari(locdata, **kwargs) if "Rois" not in viewer.layers: viewer.add_shapes(name="Rois", edge_width=0.1) if napari_run: napari.run() roi_list = get_rois(viewer.layers["Rois"], reference=locdata) return roi_list
def _shape_to_region(vertices: npt.ArrayLike, shape_type: str) -> Region: """ Convert napari shape to `locan.Region`. Parameters ---------- vertices Sequence of point coordinates as returned by napari. shape_type One of rectangle, ellipse, or polygon. Returns ------- Region """ vertices = np.asarray(vertices) if shape_type == "rectangle": if len(set(vertices[:, 0].astype(int))) != 2: raise NotImplementedError("Rotated rectangles are not implemented.") mins = vertices.min(axis=0) maxs = vertices.max(axis=0) corner_x, corner_y = mins width, height = maxs - mins angle = 0 region: Region = Rectangle((corner_x, corner_y), width, height, angle) elif shape_type == "ellipse": if len(set(vertices[:, 0].astype(int))) != 2: raise NotImplementedError("Rotated ellipses are not implemented.") mins = vertices.min(axis=0) maxs = vertices.max(axis=0) width, height = maxs - mins center_x, center_y = mins[0] + width / 2, mins[1] + height / 2 angle = 0 region = Ellipse((center_x, center_y), width, height, angle) elif shape_type == "polygon": region = Polygon(np.concatenate([vertices, [vertices[0]]], axis=0)) else: raise TypeError(f" Type {shape_type} is not defined.") return region def _shapes_to_regions(shapes_data: napari.types.ShapesData) -> list[Region]: """ Convert napari shapes to `locan.Region`. Parameters ---------- shapes_data Shapes data with list of shapes Returns ------- list[Region] """ if shapes_data[2] != "shapes": raise ValueError("shapes_data[2] must equal 'shapes'.") data = shapes_data[0] shape_types = shapes_data[1]["shape_type"] regions = [ _shape_to_region(vertices=vertices, shape_type=shape_type) for vertices, shape_type in zip(data, shape_types) ] return regions
[docs] def get_rois( shapes_layer: napari.layers.Shapes, reference: ( LocData | dict[str, str] | metadata_pb2.Metadata | metadata_pb2.File | None ) = None, loc_properties: Sequence[str] | None = None, ) -> list[Roi]: """ Create rois from shapes in napari.viewer.Shapes. Parameters ---------- shapes_layer Napari shapes layer like `viewer.layers["Shapes"]` reference Reference to localization data for which the region of interest is defined. It can be a LocData object, a reference to a saved SMLM file, or None for indicating no specific reference. When dict it must have keys `file_path`and `file_type`. When Metadata message it must have keys `file.path` and `file.type` for a path pointing to a localization file and an integer or string indicating the file type. Integer or string should be according to locan.constants.FileType. loc_properties Localization properties in LocData object on which the region selection will be applied (for instance the coordinate_keys). Returns ------- list[Roi] See Also -------- :func:`locan.scripts.rois` : script for drawing rois """ shapes_data = shapes_layer.as_layer_data_tuple() regions = _shapes_to_regions(shapes_data=shapes_data) rois = [ Roi(region=reg, reference=reference, loc_properties=loc_properties) for reg in regions ] return rois
[docs] def save_rois( rois: Iterable[Roi], file_path: str | os.PathLike[Any] | Literal["roi_reference"] | None = None, roi_file_indicator: str = "_roi", ) -> list[Path]: """ Save list of Roi objects. Parameters ---------- rois The rois to be saved. file_path Base name for roi files or existing directory to save rois in. If "roi_reference", roi.reference.file.path is used. If None, a file dialog is opened. roi_file_indicator Indicator to add to the localization file name and use as roi file name (with further extension .yaml). Returns ------- list[Path] New created roi file paths. """ # choose file interactively if file_path is None: fname = QFileDialog.getSaveFileName( None, "Set file path and base name...", "", filter="", # options=QFileDialog.DontConfirmOverwrite # kwargs: parent, message, directory, filter # but kw_names are different for different qt_bindings ) if isinstance(fname, tuple): new_file_path = Path(fname[0]) else: new_file_path = Path(fname) elif file_path == "roi_reference": new_file_path = None elif Path(file_path).is_dir(): new_file_path = Path(file_path) / "my" # just a simple name else: new_file_path = Path(file_path) # create roi file names and save rois roi_path_list = [] for i, roi in enumerate(rois): if new_file_path is None: try: new_file_path = Path(roi.reference.file.path) # type: ignore[union-attr] except AttributeError: raise roi_file = new_file_path.stem + roi_file_indicator + f"_{i}.yaml" roi_path = new_file_path.with_name(roi_file) roi_path_list.append(roi_path) roi.to_yaml(path=roi_path) return roi_path_list