Is it possible to constrain regions where it is possible to insert points?

Hello.

I’m developing a tool where I would like to forbid the user from inserting points where the label layer is zero.

I’m trying to solve this by using a condition in the mouse_drag_callbacks but this is not working. Is there a more appropriate way to tackle this in Napari?

Example:

image = ...
label = ...
points = ...

with napari.gui_qt():
    viewer = napari.view_image(image)
    label_layer = viewer.add_labels(label)
    points_layer = viewer.add_points(points)

    def mouse_drag(layer, event):
        if layer.mode == 'select':
            ...
        elif layer.mode == 'add':
            if not label[layer.coordinates]:
                raise StopIteration  # results in RuntimeError :(

    points_layer.mouse_drag_callbacks.insert(0, mouse_drag)

Hi @jookuma thanks for that feature request! Looks like you were off to the right start. He’s one possible implementation Hope this points you in the right direction!!

import numpy as np
import napari
from skimage import data

image = np.random.random((128, 128))
blobs = data.binary_blobs(
    length=128, blob_size_fraction=0.05, n_dim=2, volume_fraction=0.3
)


with napari.gui_qt():
    viewer = napari.view_image(image)
    labels = viewer.add_labels(blobs)
    points = viewer.add_points(size=4)
    points.editable = False

    def mouse_drag(viewer, event):
        labels = None
        points = None
        for layer in viewer.layers:
            if type(layer).__name__ == 'Labels':
                labels = layer
            elif type(layer).__name__ == 'Points':
                points = layer
        if labels is None:
            raise ValueError('No labels layer found')
        if points is None:
            raise ValueError('No points layer found')

        # only exectute when points is active
        if viewer.active_layer == points:
            # Round coordinates
            coords = tuple(np.round(points.coordinates).astype(int))
            # Check if label > 0
            if labels.data[coords]:
                print(f'Point added at {coords}')
                points.add(coords)
            else:
                print(f'No label at {coords} so no point added')

    # Add callback to viewer
    viewer.mouse_drag_callbacks.append(mouse_drag)

Note i made the points layer non editable, and you have to have to points layer selected for the callback to work. I also bind the callback to the viewer so it can access both points and labels. Note also that if you have more than one points or more than one labels it will get confused. In this version its also not possible to more the points after they have been added, but that could be changed too.

Here’s a gif:

Was a fun one to think about!!

4 Likes

@sofroniewn I appreciate your response. It helped me understand a bit more about Napari.

Your solution locks the points layer in the PAN_ZOOM mode. To enable the SELECT mode I added a button to switch between the editable states (switching between constrained addition and selection), but when editable=True the user is able to change to ADD mode and insert points freely.

Currently, I think the most appropriate solution would be to inherit the Point class and change the add method. However, when I do this, the create_vispy_visual from utils crashes because my class is not present in the layer_to_visual dictionary.

Do you think it would be appropriate to pull request to change the create_vispy_visual;

From this:

layer_to_visual = {
    Image: VispyImageLayer,
    Labels: VispyImageLayer,
    Points: VispyPointsLayer,
    Shapes: VispyShapesLayer,
    Surface: VispySurfaceLayer,
    Vectors: VispyVectorsLayer,
}


def create_vispy_visual(layer):
    """Create vispy visual for a layer based on its layer type.

    Parameters
    ----------
    layer : napari.layers._base_layer.Layer
        Layer that needs its property widget created.

    Returns
    -------
    visual : vispy.scene.visuals.VisualNode
        Vispy visual node
    """
    visual = layer_to_visual[type(layer)](layer)

    return visual

To this, note that the Image and Labels use the same Vispy layer:

layer_to_visual = {
    Image: VispyImageLayer,
    Points: VispyPointsLayer,
    Shapes: VispyShapesLayer,
    Surface: VispySurfaceLayer,
    Vectors: VispyVectorsLayer,
}


def create_vispy_visual(layer):
    """Create vispy visual for a layer based on its layer type.

    Parameters
    ----------
    layer : napari.layers._base_layer.Layer
        Layer that needs its property widget created.

    Returns
    -------
    visual : vispy.scene.visuals.VisualNode
        Vispy visual node
    """
    for layer_type, visual in layer_to_visual.items():
        if isinstance(layer, layer_type):
            return visual(layer)

    raise ValueError

It would also be necessary to change the create_qt_controls function in layers.utils.

I think this change would let the Napari’s interface a bit more flexible; it will be beneficial for the development of other plugins.

1 Like

absolutely! … we’ve been wanting to kill that dict for an isinstance check for a long time :slight_smile: see https://github.com/napari/napari/issues/1176

2 Likes

@talley I have opened a PR.

Thanks!

2 Likes