Skip to content

Add remove() in Shapes and Points - #8031

Merged
brisvag merged 22 commits into
napari:mainfrom
rahul713rk:issue7964
Sep 26, 2025
Merged

Add remove() in Shapes and Points#8031
brisvag merged 22 commits into
napari:mainfrom
rahul713rk:issue7964

Conversation

@rahul713rk

@rahul713rk rahul713rk commented Jun 16, 2025

Copy link
Copy Markdown
Contributor

References and relevant issues

Closes #7964

Description

This PR enhances shape and point layer functionality by introducing new methods and improving existing ones:

  • ➕ Added remove() method to both Shapes and Points layers for explicit item removal
  • 🔧 Modified remove_selected() to use the new remove() internally, improving code clarity and modularity

EDIT : Added tests for remove for both Points and Shapes (test_removing_points , test_removing_shapes )
and Remove pop method from this PR for follow-up issue #8070

@rahul713rk
rahul713rk requested a review from a team as a code owner June 16, 2025 10:43
@rahul713rk rahul713rk changed the title [Update] Added remove , 'remove_selected and pop` in Shapes and Points [Update] Added remove , remove_selected and pop in Shapes and Points Jun 16, 2025
@psobolewskiPhD

Copy link
Copy Markdown
Member

The test fails appear legit, because the event is moved from remove_selected to remove.
I think we want to keep the events the same for the time being.
the question is whether there should also be an event for remove -- but this would risk double events.

Comment thread src/napari/layers/points/points.py
@TimMonko TimMonko added this to the 0.6.2 milestone Jun 20, 2025
@rahul713rk

Copy link
Copy Markdown
Contributor Author

@TimMonko @psobolewskiPhD — thank you for pointing out the mistake .
Initially, I thought it was code duplication, which is why I removed it. After revisiting the module, I realized its significance and have now rectified the mistake.
Please let me know if there are any further changes or improvements I should make.

@psobolewskiPhD

psobolewskiPhD commented Jun 26, 2025

Copy link
Copy Markdown
Member

@rahul713rk There's some minor merge conflicts, do you think you can resolve them or would you rather one of the maintainers do so?

Edit: also we will need some tests for the new methods. I think you can just copy and modify the existing test, e.g. for points:

def test_removing_selected_points():

@psobolewskiPhD

Copy link
Copy Markdown
Member

@jni you had thoughts on this previously, could you take a look at the implementation and see what you think?

@TimMonko

Copy link
Copy Markdown
Member

Moving to 0.6.3 so we have time to look at this, which unfortunately I don't have right now! Thanks @rahul713rk for the work you've done so far!

@TimMonko TimMonko modified the milestones: 0.6.2, 0.6.3 Jun 29, 2025
@github-actions github-actions Bot added the tests Something related to our tests label Jul 1, 2025
@rahul713rk

Copy link
Copy Markdown
Contributor Author

Thanks @psobolewskiPhD , @TimMonko for your support and guidance , i have resolved the conflict and add test cases with name test_removing_points , test_pop_points , test_removing_shapes , test_pop_shapes

@TimMonko
TimMonko dismissed their stale review July 1, 2025 18:03

stale

@codecov

codecov Bot commented Jul 1, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.06%. Comparing base (fd7a743) to head (7215479).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
src/napari/layers/shapes/shapes.py 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8031      +/-   ##
==========================================
+ Coverage   93.00%   93.06%   +0.06%     
==========================================
  Files         702      702              
  Lines       63269    63321      +52     
==========================================
+ Hits        58841    58930      +89     
+ Misses       4428     4391      -37     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/napari/layers/points/points.py Outdated
Comment on lines +2061 to +2068
def pop(self) -> None:
"""Remove the last point from the layer."""
visible_indices = self._indices_view.tolist()
if not visible_indices:
# If no points are visible, do nothing
show_warning('No points to remove.')
return
self.remove([visible_indices[-1]])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want pop to work on visible only? I think i lean towards just removing the last item regardless?
On the other hand, there may be some utility in removing from visible items only 🤔
Also is it worth making pop more like the Python List method, where it returns the item? that method also accepts an index for what it's worth...

@psobolewskiPhD

Copy link
Copy Markdown
Member

Thanks for resolving the conflicts and your patience @rahul713rk
For me the remove/remove_selected are nice, but I still am uncertain about the pop functionality:

  • should it be limited to visible, as you have it, or all points
  • should it also return the point, like Python list pop
  • should it accept an index?
    Please bear with us as we try to get more feedback on that!

@brisvag

brisvag commented Jul 3, 2025

Copy link
Copy Markdown
Contributor

Yes, IMO we should aim to stick as close as possible to what pop means in other python contexts (like list). So the signature I would expect is:

	def pop(self, index=-1) -> npt.ArrayLike: # of shape (D,)

And it shouldn't use shown.

@psobolewskiPhD

Copy link
Copy Markdown
Member

Another option is to split off the pop method into a new PR and then we can just get remove merged, which would be a nice benefit.

@rahul713rk

Copy link
Copy Markdown
Contributor Author

Yes, IMO we should aim to stick as close as possible to what pop means in other python contexts (like list). So the signature I would expect is:

	def pop(self, index=-1) -> npt.ArrayLike: # of shape (D,)

And it shouldn't use shown.

shouldn't we return dictionary (for poped item only) containing

 output =  {data = data,
            shape_type=shape_type,
            edge_width=edge_width,
            edge_color=edge_color,
            edge_color_cycle=edge_color_cycle,
            edge_colormap=edge_colormap,
            edge_contrast_limits=edge_contrast_limits,
            face_color=face_color,
            face_color_cycle=face_color_cycle,
            face_colormap=face_colormap,
            face_contrast_limits=face_contrast_limits,
            z_index=z_index,}

instead of returning a list containing data of poped item

output = [data]

@brisvag

brisvag commented Jul 4, 2025

Copy link
Copy Markdown
Contributor

Uh, I was thinking to only return the data itself (coordinates), but I guess we could do like you said and return a dict.

Another option is to split off the pop method into a new PR and then we can just get remove merged, which would be a nice benefit.

Yeah good idea! @rahul713rk do you think you could split this pop part out into a separate PR so we can discuss there, and in the meantime we can merge the remove/remove_selected changes?

@rahul713rk

Copy link
Copy Markdown
Contributor Author

thanks @brisvag , sure going to remove pop part from this PR and opening a new issue on pop.

@psobolewskiPhD

Copy link
Copy Markdown
Member

I pushed the requested change @brisvag from .sort() method to sorted
So now both points and shapes remove use sorted.

@psobolewskiPhD

Copy link
Copy Markdown
Member

The windows test fails are unrelated, happening everywhere. Maybe from the windows runner changes.

Comment thread src/napari/layers/points/points.py Outdated
Comment thread src/napari/layers/points/points.py
@psobolewskiPhD

Copy link
Copy Markdown
Member

@brisvag OK, i think selection works correctly now. it was actually tricky, because when you remove items from the set, the indices of remaining items change! So you need to update the indices that are in selected_data.

@brisvag brisvag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Only thing is maybe to check for performance, but otherwise LGTM.

Comment thread src/napari/layers/points/points.py Outdated
Comment on lines +2043 to +2049
for idx in self.selected_data:
# If the selected index was removed, skip it
if idx in indices:
continue
# Count how many indicies have been removed prior
shift = bisect.bisect_left(indices, idx)
new_selected.add(idx - shift)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test with big arrays if this is slow?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so what's the worst case scenario here, it's large number of points and then selecting all of them?
Should probably check for that case and short circuit the loop.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But otherwise large number of points with most selected and some being deleted?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, I guess it depends on what's slower: the loop or calling bisect_left on a big array?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so performance in the case of many selected points and removing some subset of points was not great.
The last two commits improve that case significantly, like 10X, by using sets better and using essentially the same approach as bisect, but numpy vectorized. (I got a little help from Gemini, testing it out.)

Removing selected shapes is horribly slow. I made an issue for that:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, it's technically not a regression, because on main you can't have some points selected and remove some other/overlaping set of points.
Removing all selected is not affected performance wise.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing all selected shapes is now marginally faster, but still horrendously slow once you get beyond 100s of shapes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for diving in!

Comment thread src/napari/layers/points/points.py Outdated
selected_not_removed = self.selected_data - set(indices)
if selected_not_removed:
indices_array = np.array(indices)
remaining_selected = np.array(list(selected_not_removed))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using np.fromiter seems almost 2X faster on my machine than passing through a list intermediate.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea using that is a bit faster. Like 10% or so. I will push the change when I get some free time.

@Czaki

Czaki commented Sep 17, 2025

Copy link
Copy Markdown
Collaborator

Hm. I think we should add benchmarks for removing shapes and points and run on this PR.

@brisvag @TimMonko @psobolewskiPhD Should I make a separate PR with benchmarks or expect contributor to do it here?

@brisvag

brisvag commented Sep 17, 2025

Copy link
Copy Markdown
Contributor

I think it's best if you do it since yopu're most familiar; also I think @psobolewskiPhD mostly topok over this PR anyways :)

@psobolewskiPhD

Copy link
Copy Markdown
Member

IMO deep optimization is out of scope here? I made an issue for the slow shape removal. It's super slow on main too.

@brisvag

brisvag commented Sep 19, 2025

Copy link
Copy Markdown
Contributor

I think it's more whether we are introducting a regression. But yeah, if you think it's good on that, we can do the benchmark separately IMO 👍

@psobolewskiPhD

Copy link
Copy Markdown
Member

I think it's more whether we are introducting a regression.

There is no regression, because prior to this PR you could only remove selected shapes/points and the code for that is unchanged.

What this adds is removing by index, which means you can potentially have a set of selected shapes/points and remove some other set of points/shapes. This case requires shifting the indexes of the selected points, which I implemented. There may be optimizations possible, by using more numpy set functions? However, to be honest, I suspect that most people will either use remove() and not have things selected or will select things and then remove_selected(). In both of these cases, the index shifting required to update the selection will not be used.

Removing shapes is just slow period, with or without this pr: #8264

@brisvag brisvag left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, at last I think we're good to go and get this merged :) Probably immediatly followed up by #8070, once conflicts are solved!

Thanks @rahul713rk, sorry again for taking long on this :)

@brisvag brisvag added the ready to merge Last chance for comments! Will be merged in ~24h label Sep 24, 2025
@brisvag
brisvag merged commit 4fd9863 into napari:main Sep 26, 2025
59 of 60 checks passed
@github-actions github-actions Bot removed the ready to merge Last chance for comments! Will be merged in ~24h label Sep 26, 2025
@brisvag brisvag changed the title [Update] Added remove and remove_selected in Shapes and Points Add remove() and remove_selected() in Shapes and Points Sep 26, 2025
@brisvag brisvag changed the title Add remove() and remove_selected() in Shapes and Points Add remove() in Shapes and Points Sep 26, 2025
@brisvag brisvag added the highlight PR that should be mentioned in next release notes label Sep 26, 2025
brisvag added a commit that referenced this pull request Sep 26, 2025
# References & Related Issues

* Closes #8070
* Follow-up to: #7964 ,  #8031

---

# Description

* Implemented a `pop` method for both the `Points` and `Shapes` layers,
enabling removal and return of point/shape data at a specified index.
* Added tests  for  `pop` method.

---------

Co-authored-by: a <a>
Co-authored-by: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com>
Co-authored-by: Lorenzo Gaifas <brisvag@gmail.com>
@imagesc-bot

Copy link
Copy Markdown

This pull request has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/napari-v0-6-5-is-out/116715/1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to pre-existing features highlight PR that should be mentioned in next release notes tests Something related to our tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider a remove method for Shapes and Points

6 participants