Replace shapes in the Shapes layer with new shapes

Hi,

I’d like to replace the shapes of my Shapes layer with some new shapes. Something like this:

from skimage import data
import napari
import numpy as np


def enable_gui_qt():
    from IPython import get_ipython

    ipython = get_ipython()
    ipython.magic('gui qt')


enable_gui_qt()

viewer = napari.view_image(data.astronaut(), rgb=True)
old_shapes = [np.array([[217.18092301, 14.32894795],
                        [531.98354928, 14.32894795],
                        [531.98354928, 277.02631195],
                        [217.18092301, 277.02631195]]),
              np.array([[81.49013583, 199.95394483],
                        [287.74013235, 199.95394483],
                        [287.74013235, 396.43420467],
                        [81.49013583, 396.43420467]])]

shapes_layer = viewer.add_shapes(data=old_shapes,
                                 face_color='transparent',
                                 edge_color='red',
                                 edge_width=4,
                                 name='some_name')

new_shapes = [np.array([[266.0296064, 300.61183407],
                        [490.77960153, 300.61183407],
                        [490.77960153, 510.63156727],
                        [266.0296064, 510.63156727]])]

shapes_layer.data = new_shapes

This works as expected. However, this approach fails when having properties:

from skimage import data
import napari
import numpy as np


def enable_gui_qt():
    from IPython import get_ipython

    ipython = get_ipython()
    ipython.magic('gui qt')


enable_gui_qt()

viewer = napari.view_image(data.astronaut(), rgb=True)
old_shapes = [np.array([[217.18092301, 14.32894795],
                        [531.98354928, 14.32894795],
                        [531.98354928, 277.02631195],
                        [217.18092301, 277.02631195]]),
              np.array([[81.49013583, 199.95394483],
                        [287.74013235, 199.95394483],
                        [287.74013235, 396.43420467],
                        [81.49013583, 396.43420467]])]

old_scores = list(np.random.rand(len(old_shapes)))
old_labels = [f'old_label_{i}' for i in range(len(old_shapes))]

properties = {
    'labels': old_labels,
    'scores': old_scores
}

text_parameters = {
    'text': '{labels} {scores:.2f}',
    'size': 12,
    'color': 'white',
    'anchor': 'upper_left',
    'translation': [-1, 0],
}

shapes_layer = viewer.add_shapes(data=old_shapes,
                                 face_color='transparent',
                                 edge_color='red',
                                 edge_width=4,
                                 name='some_name',
                                 properties=properties,
                                 text=text_parameters)

new_shapes = [np.array([[220.18092301, 14.32894795],
                        [531.98354928, 14.32894795],
                        [531.98354928, 277.02631195],
                        [220.18092301, 277.02631195]]),
              np.array([[90.49013583, 199.95394483],
                        [287.74013235, 199.95394483],
                        [287.74013235, 396.43420467],
                        [90.49013583, 396.43420467]]),
              np.array([[266.0296064, 300.61183407],
                        [490.77960153, 300.61183407],
                        [490.77960153, 510.63156727],
                        [266.0296064, 510.63156727]])]

shapes_layer.data = new_shapes

How do I update the properties correctly without destroying the layer? Can anyone point me in the right direction?

Sorry about that! Maybe @kevinyamauchi can take a look at this. I can also try taking a look later too!!

1 Like

Hello @johschmidt42 ! Thanks for posting this issue. I took a quick look and I have a guess as to where the bug is. Since each property in Shapes.properties should have one element per shape in the layer, the layer needs to add an additional property value when the layer data goes 2 shapes to 3 shapes (your second example). I agree that this should be possible and you’re experiencing a bug. My recollection is that the intended behavior is that the properties get “padded” with the property values in shapes_layer.current_properties. shapes_layer.current_properties contains the property values to be given to the next shape(s) to be added. After initializing the layer, shapes_layer.current_properties is the last property value by default and then through the GUI it is modified by selecting a shape (it is set to the property values of the selected shape).

I can take a look tomorrow to try and pin this down. Sorry about the bug and thanks again for the report!

2 Likes

Hello @kevinyamauchi and @sofroniewn, thanks for the quick response.
Here’s what I’ve also tried: I removed the old shapes

shapes_layer.selected_data = set(range(shapes_layer.nshapes))
shapes_layer.remove_selected()

which also gets rid of the properties (not completely as the keys are still in place, but that doesn’t matter):

shapes_layer.properties

{'labels': array([], dtype='<U11'), 'scores': array([], dtype=float64)}

And the shapes_layer.current_properties still hold the information from the last shape.
So I could add a new shape or new shapes by using shapes_layer.add(), which would take the information from shapes_layer.current_properties. I can overwrite this information and create the shapes that I want. Here’s an example.

two_new_shapes = [np.array([[220.18092301, 14.32894795],
                            [531.98354928, 14.32894795],
                            [531.98354928, 277.02631195],
                            [220.18092301, 277.02631195]]),
                  np.array([[90.49013583, 199.95394483],
                            [287.74013235, 199.95394483],
                            [287.74013235, 396.43420467],
                            [90.49013583, 396.43420467]])]

shapes_layer.selected_data = set(range(shapes_layer.nshapes))
shapes_layer.remove_selected()

shapes_layer.current_properties['labels'] = np.array(['new_label_1', 'new_label_2'])
shapes_layer.current_properties['scores'] = np.array([0.9, 0.1])

shapes_layer.add(two_new_shapes)

However, there’s one thing I still need to figure out:
How do I change the existing text parameters? Sure I could adjust the size of the text by changing the value of ‘shapes_layer.text.size’, but is there maybe a better solution, a method that takes in a dict with parameters? So basically the code that was run when the shape_layer was initialized.

new_text_parameters = {
    'text': '{labels} {scores:.2f}',
    'size': 4,
    'color': 'black',
    'anchor': 'upper_left',
    'translation': [-1, 0],
}

Thanks for the help!

Hey @johschmidt42 . Nice sleuthing! It looks like you’ve found a good work around. From our side, I’ve tracked down the bug (see this issue). I have a couple of things in the queue, but I can try to make a fix that will allow directly changing the layer data via shapes_layer.data early next week.

With regards to updating multiple text properties at once, that is a feature on the roadmap! See this issue. Unfortunately, for now, that means the best way is to do it via the text attributes (e.g., shapes_layer.text.size as you suggested).

Thanks for you patience and please let us know if you have any more questions or comments!

3 Likes

Wow, I didn’t expect a response this soon! Thanks for writing up the issue. Napari is just amazing and I can’t wait to see the upcoming changes!

2 Likes

No problem, @johschmidt42 ! I’ll link back here when I’ve opened the PR.