from __future__ import annotations
from warnings import warn
import napari
from napari.utils.notifications import Notification, notification_manager
from napari.utils import history
from napari.layers import Image, Shapes, Points, Labels, Tracks
import magicgui
from functools import wraps
from qtpy.QtWidgets import QFileDialog, QAction
from qtpy.QtGui import QCursor
from .widgets import *
from .widgets.table import read_csv
from .utils import (
get_a_selected_layer,
iter_selected_layer,
crop_rectangle,
crop_rotated_rectangle,
get_viewer_scale,
make_world_scale,
to_labels
)
from .._const import Const, SetConst
from ..arrays import ImgArray, Label, PhaseArray, LabeledArray, LazyImgArray
__all__ = ["add_imread_menu",
"add_imsave_menu",
"add_read_csv_menu",
"add_explorer_menu",
"add_duplicate_menu",
"add_proj_menu",
"add_crop_menu",
"add_layer_to_labels_menu",
"add_time_stamper_menu",
"add_text_layer_menu",
"add_get_props_menu",
"add_label_menu",
"add_plane_clipper",
"add_note_widget",
"add_threshold",
"add_rotator",
"add_filter",
"add_regionprops",
"add_rectangle_editor",
"layer_template_matcher",
"function_handler",
]
FILTERS = ["None", "gaussian_filter", "median_filter", "mean_filter", "dog_filter", "doh_filter", "log_filter",
"erosion", "dilation", "opening", "closing", "entropy_filter", "std_filter", "coef_filter",
"tophat", "rolling_ball"]
def catch_notification(func):
@wraps(func)
def wrapped(*args, **kwargs):
try:
out = func(*args, **kwargs)
except Exception as e:
out = None
notification_manager.dispatch(Notification.from_exception(e))
return out
return wrapped
[docs]def add_plane_clipper(viewer:"napari.Viewer"):
action = QAction("Clipping plane", viewer.window._qt_window)
@action.triggered.connect
def _(*args):
layer = get_a_selected_layer(viewer)
wid = PlaneClipRange(viewer)
wid.connectLayer(layer)
viewer.window.add_dock_widget(wid, name="Plane Clip", area="left")
return None
viewer.window.layer_menu.addAction(action)
return None
def add_gui_to_function_menu(viewer:"napari.Viewer", gui:type, name:str):
action = QAction(name, viewer.window._qt_window)
@action.triggered.connect
def _():
if name in viewer.window._dock_widgets:
viewer.window._dock_widgets[name].show()
else:
viewer.window.add_dock_widget(gui(viewer), area="left", name=name)
return None
viewer.window.function_menu.addAction(action)
return None
[docs]def add_filter(viewer:"napari.Viewer"):
return add_gui_to_function_menu(viewer, FunctionCaller, "Filters")
[docs]def add_threshold(viewer:"napari.Viewer"):
return add_gui_to_function_menu(viewer, ThresholdAndLabel, "Threshold/Label")
[docs]def add_rectangle_editor(viewer:"napari.Viewer"):
return add_gui_to_function_menu(viewer, RectangleEditor, "Rectangle Editor")
[docs]def add_rotator(viewer:"napari.Viewer"):
return add_gui_to_function_menu(viewer, Rotator, "Rotation")
[docs]def add_regionprops(viewer:"napari.Viewer"):
action = QAction("Measure Region Properties", viewer.window._qt_window)
@action.triggered.connect
def _():
dlg = RegionPropsDialog(viewer)
dlg.exec_()
viewer.window.function_menu.addAction(action)
return None
[docs]def layer_template_matcher(viewer:"napari.Viewer"):
action = QAction("Template Matcher", viewer.window._qt_window)
@action.triggered.connect
@catch_notification
def _(*args):
@magicgui.magicgui(call_button="Match",
img={"label": "image",
"tooltip": "Reference image. This image will not move."},
template={"label": "temp",
"tooltip": "Template image. This image will move."},
ndim={"choices": [2, 3]})
def template_matcher(img: Image, template: Image, ndim=2):
step = viewer.dims.current_step[:-min(ndim, img.ndim)]
img_ = img.data[step]
template_ = template.data[step]
with SetConst("SHOW_PROGRESS", False):
res = img_.ncc_filter(template_)
maxima = np.unravel_index(np.argmax(res), res.shape)
maxima = tuple((m - l//2)*s for m, l, s in zip(maxima, template_.shape, template.scale))
template.translate = img.translate + np.array(step + maxima)
viewer.window.add_dock_widget(template_matcher, area="left", name="Template Matcher")
return None
viewer.window.function_menu.addAction(action)
return None
[docs]def function_handler(viewer:"napari.Viewer"):
action = QAction("Function Handler", viewer.window._qt_window)
@action.triggered.connect
@catch_notification
def _(*args):
@magicgui.magicgui(call_button="Run")
def run_func(method="gaussian_filter",
arguments="",
update=False) -> napari.types.LayerDataTuple:
"""
Run image analysis in napari window.
Parameters
----------
method : str, default is "gaussian_filter"
Name of method to be called.
arguments : str, default is ""
Input arguments and keyword arguments. If you want to run `self.median_filter(2, dims=2)` then
the value should be `"2, dims=2"`.
update : bool, default is False
If update the layer's data. The original data will NOT be updated.
Returns
-------
napari.types.LayerDataTuple
This is passed to napari and is directly visualized.
"""
from ..frame import MarkerFrame, TrackFrame, PathFrame
outlist = []
for input in viewer.layers.selection:
data = layer_to_impy_object(viewer, input)
try:
if method.startswith("[") and method.endswith("]"):
arguments = method[1:-1]
func = data.__getitem__
else:
func = getattr(data, method)
except AttributeError as e:
viewer.status = f"{method} finished with AttributeError: {e}"
continue
viewer.status = f"{method} ..."
try:
args, kwargs = str_to_args(arguments)
out = func(*args, **kwargs)
except Exception as e:
viewer.status = f"{method} finished with {e.__class__.__name__}: {e}"
continue
else:
viewer.status = f"{method} finished"
scale = make_world_scale(data)
# determine name of the new layer
if update and type(data) is type(out):
name = input.name
else:
name = f"Result of {input.name}"
if isinstance(out, ImgArray):
out_ = image_tuple(input, out, name=name)
elif isinstance(out, PhaseArray):
out_ = (out,
dict(scale=scale, name=name, colormap="hsv", translate=input.translate,
contrast_limits=out.border),
"image")
elif isinstance(out, Label):
label_tuple(input, out, name=name)
elif isinstance(out, MarkerFrame):
kw = dict(size=3.2, face_color=[0,0,0,0], translate=input.translate,
edge_color=viewer.window.cmap(),
metadata={"axes": str(out._axes), "scale": out.scale},
scale=scale)
out_ = (out, kw, "points")
elif isinstance(out, TrackFrame):
out_ = (out,
dict(scale=scale, translate=input.translate,
metadata={"axes": str(out._axes), "scale":out.scale}),
"tracks")
elif isinstance(out, PathFrame):
out_ = (out,
dict(scale=scale, translate=input.translate,
shape_type="path", edge_color="lime", edge_width=0.3,
metadata={"axes": str(out._axes), "scale":out.scale}),
"shapes")
else:
continue
outlist.append(out_)
if len(outlist) == 0:
return None
else:
return outlist
viewer.window.add_dock_widget(run_func, area="left", name="Function Handler")
return None
viewer.window.function_menu.addAction(action)
return None
def str_to_args(s:str) -> tuple[list, dict]:
args_or_kwargs = list(_iter_args_and_kwargs(s))
if args_or_kwargs[0] == "":
return [], {}
args = []
kwargs = {}
for a in args_or_kwargs:
if "=" in a and a[0] not in ("'", '"'):
k, v = a.split("=")
v = interpret_type(v)
kwargs[k] = v
else:
a = interpret_type(a)
args.append(a)
return args, kwargs
def interpret_type(s:str):
return eval(s, {"np": np})
def _iter_args_and_kwargs(string:str):
stack = 0
start = 0
for i, s in enumerate(string):
if s in ("(", "[", "{"):
stack += 1
elif s in (")", "]", "}"):
stack -= 1
elif stack == 0 and s == ",":
yield string[start:i].strip()
start = i + 1
if start == 0:
yield string
def _get_property(layer:Shapes|Points, i):
try:
prop = layer.properties["text"][i]
except (KeyError, IndexError):
prop = ""
if prop != "":
prop = prop + " of "
return prop