8000 Fix issues displaying polygons with holes in Shapes by jni · Pull Request #6654 · napari/napari · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fix issues displaying polygons with holes in Shapes #6654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 110 commits into from
Apr 16, 2025
Merged

Conversation

jni
Copy link
Member
@jni jni commented Feb 12, 2024

References

Partial fix for Closes #5673. (!!!)
Closes #1653. 🥳
Closes #1058. 🥳
Closes #7799.

Depends on #7622

Description

(Updated 29/2/2024.)
(Updated 16/4/2025.)

This PR adds three preprocessing steps to polygon data during meshing in Shapes layers:

  • discard duplicate polygon vertices, and recast edges in terms of unique vertices.
  • discard duplicate edges — these are found when going to interior polygons depicting holes and when coming back.
  • remove triangles from the triangulation that are not in the polygon. (only for the triangle backend, which otherwise includes these triangles.)

For the VisPy step, we go directly into the Triangulation class, because the PolygonData class completely ignores input edges when building a triangulation.

Current limitations:

  • only works when triangle is installed (py-triangle in conda-forge) Edit: nope, works with plain VisPy too!
  • currently on macOS, I am getting random segfaults when calling triangle.triangulate. I don't know what triggers them, but I suspect we're just going to have to implement our own triangulation, maybe with Numba. Edit: these were caused by repeated vertices, against which triangulation algorithms are generally not robust. Uniquifying the vertices fixed the problem!
  • I still need to filter out the edges of the polygon for drawing, and maybe also for highlighting. Currently, the edges connecting the interior and exterior polygons are visible, which looks crap. Edit: this is now done!
  • the polygons are processed twice, once for the interiors and once for the edges. Edit: fixed by @Czaki 🥳
  • the processing is probably a fair bit of overhead when many datasets would get no benefit. It might be worthwhile adding a flag to turn these checks on/off. Edit: fixed by @Czaki 🥳

Summary of the issue and strategies:

It turns out that what we actually need is a constrained Delaunay triangulation1, that is, a Delaunay triangulation that preserves specific edges from the vertices. That’s what the 'p' option in triangle.triangulate does but it doesn’t work if the edges are actually not planar — that’s why the triangulation was failing for this polygon even if triangle was installed.

This PR fixes that by finding edges that appear twice in the polygon and removing them.

Additionally, certain triangles from the resulting triangulation need to be removed, and this is easy to do by running skimage.measure.points_in_poly on the triangle centers, which are fast to compute with NumPy indexing.

jni added 2 commits February 12, 2024 14:33
This is a partial fix for napari#5673. Current limitations:

- only works when `triangle` is installed (`py-triangle` in conda-forge)
- currently on macOS, this works fine running in IPython but it segfaults when called from plain Python. I don’t know whether it’s just an env thing.

Summary:

It turns out that what we actually need is a *constrained Delaunay triangulation*[1], that is, a Delaunay triangulation that preserves specific edges from the vertices. That’s what the 'p' option in `triangle.triangulate` does *but* it doesn’t work if the edges are actually not planar — that’s why the triangulation was failing for this polygon even if triangle was installed. This PR fixes that by finding edges that appear twice in the polygon and removing them.

Additionally, certain triangles from the resulting triangulation need to be removed, and this is easy to do by running `skimage.measure.points_in_poly` on the triangle centers, which are fast to compute with NumPy indexing.

[1]: http://en.wikipedia.org/wiki/Constrained_Delaunay_triangulation
@jni
Copy link
Member Author
jni commented Feb 12, 2024

Current output when it doesn't segfault 😅

Screenshot 2024-02-12 at 3 36 41 pm

@jni
Copy link
Member Author
jni commented Feb 12, 2024

Based on playing around with it in IPython, the segfault happens when calling triangulate, not when accessing the arrays later, so it seems to be a bug in triangulate, not an issue with releasing the memory too early.

Copy link
codecov bot commented Feb 12, 2024

Codecov Report

Attention: Patch coverage is 91.07505% with 44 lines in your changes missing coverage. Please review.

Project coverage is 92.93%. Comparing base (5d5a725) to head (c36bedd).
Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
napari/layers/shapes/_shapes_utils.py 71.42% 30 Missing ⚠️
napari/layers/shapes/_accelerated_triangulate.py 96.62% 5 Missing ⚠️
...layers/shapes/_accelerated_triangulate_dispatch.py 95.19% 5 Missing ⚠️
napari/layers/shapes/_shapes_models/shape.py 80.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6654      +/-   ##
==========================================
- Coverage   93.01%   92.93%   -0.09%     
==========================================
  Files         633      635       +2     
  Lines       59445    59860     +415     
==========================================
+ Hits        55292    55628     +336     
- Misses       4153     4232      +79     

☔ 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.

@Czaki
Copy link
Collaborator
Czaki commented Feb 12, 2024

So as I understand, we need a better triangulate algorithm?

@jni
Copy link
Member Author
jni commented Feb 12, 2024

So as I understand, we need a better triangulate algorithm?

Yes. I started to figure this out while writing out this question on the mpl discourse, which I thought would be a good place since mpl can draw these polygons no problem.

Unfortunately, as you can see from the answer there, there's not really a lot of good options in Python, so I think we'll have to write our own or wrap our own. Not sure yet which is better. But at least I'm happy that I understand the problem space a bit better.

@jni
Copy link
Member Author
jni commented Feb 12, 2024

btw @Czaki if you can pull this down and run it on your Linux and/or Windows machine to check whether the segfault happens there, that would be useful — maybe this problem can be fixed by improving the triangle binaries... (Though I am not really holding my breath...)

@psobolewskiPhD
Copy link
Member

@jni can you clarify, are you using arm64 binary of py-triangle from conda-forge?

@jni
Copy link
Member Author
jni commented Feb 12, 2024

can you clarify, are you using arm64 binary of py-triangle from conda-forge?

yup!

@jni
Copy link
Member Author
jni commented Feb 12, 2024
$ µ list | grep triangle
  py-triangle                           20230923      py311h05b510d_2       conda-forge
  triangle                              1.6           h9ac270d_4            conda-forge

(... Yes, I aliased my micromamba to µ, which you can type as alt-m on macOS. 😃)

@imagesc-bot
Copy link

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

https://forum.image.sc/t/some-edges-of-polygon-extend-to-infinity/90454/12

The issue was that the triangulation input cannot have repeated vertices. I fixed it by removing repeated vertices and translating all index coordinates to the unique vertices. This involves a bit more bookkeeping but it’s fine!
@jni
Copy link
Member Author
jni commented Feb 16, 2024

It's working! 😭

The issue was that the triangulation input cannot have repeated vertices. I fixed it by removing repeated vertices and translating all index coordinates to the unique vertices.

@jni jni marked this pull request as ready for review February 28, 2024 12:22
Copy link
Member Author
@jni jni Apr 15, 2025

Choose a reason for hiding this comment

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

@Czaki two edit three 😅 things here:

  1. (most important) Is this expected to include edges connecting inner and outer polygons? The docstring should explicitly specify whether "inter-polygon" edges are included and removed by the algorithm, or whether they should already be removed.
  2. is this just connected components or is it something else? (also good to include in docstring)
  3. (mostly just me trying to understand) I was trying to figure out what numba.typed.List was and found this thread which shows that it's very slow outside of njit functions. So (a) do these lists every make it out of numba? (I will try to follow the code but wanted to write down my thoughts) (b) if we are creating the lists within numba and all the computation is within numba, is there a performance advantage? (ie would numba just promote a 'list' declaration to a typed list internally?)

Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. improved docstring
  2. improved docstring
  3. It looks like code compiled without numba.typed.List, so removed. However, this code is never triggered without numba.

@njit(cache=True)
def reconstruct_polygons_from_edges(
vertices: CoordinateArray, edges: EdgeArray
) -> list[CoordinateArray2D] | list[CoordinateArray3D]:
# In case of an open chain, exit.
break
polygons.append([vertices[x] for x in poly])
return polygons
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm having a hard time following this algorithm. I'm wondering if we can avoid the breaks inside nested if statements, which make it difficult to understand to what level of nesting we are breaking out. One small suggestion is changing:

for i in range(n_edges):
    if not used[i]:
        # huge loop body

to

for i in range(n_edges):
    if used[i]:
        continue
    # huge loop body

Same for the incident edges loop.

I'm also wondering about "found" vs "closed". It seems to me that open chains are just ignored? In what situations can this algorithm encounter open chains?

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually changing the nesting locally made it clear — open chains are just added anyway?

Again, maybe none of this would be surprising if I understood what this function is supposed to do... 😅

@jni jni added the ready to merge Last chance for comments! Will be merged in ~24h label Apr 16, 2025
@jni jni merged commit c8b260e into napari:main Apr 16, 2025
53 of 56 checks passed
@github-actions github-actions bot removed the ready to merge Last chance for comments! Will be merged in ~24h label Apr 16, 2025
@jni jni added enhancement and removed needs:decision Needs a decision to move forward labels Apr 16, 2025
@LucaMarconato
Copy link
Contributor

🚀

@jni
Copy link
Member Author
jni commented Apr 22, 2025

Haha thanks for dropping by @LucaMarconato! Unfortunately there is still a bug in some situations, see vispy/vispy#2666 😭 But we'll get there! 😅 (And the bug does not exist in PartSegCore-compiled-backend or bermuda, the latter of which will be available after #7747.)

@imagesc-bot
Copy link

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

https://forum.image.sc/t/upcoming-napari-0-6-0-release/108954/10

@imagesc-bot
Copy link

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

https://forum.image.sc/t/napari-0-6-0-released/112147/1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working as expected bugfix PR with bugfix enhancement highlight PR that should be mentioned in next release notes qt Relates to qt tests Something related to our tests
Projects
None yet
0