Skip to content
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

Make point size isotropic #5582

Merged
merged 36 commits into from Jun 22, 2023
Merged

Make point size isotropic #5582

merged 36 commits into from Jun 22, 2023

Conversation

brisvag
Copy link
Contributor

@brisvag brisvag commented Feb 22, 2023

Description

With vispy 0.12 (#5312) we introduced some changes to how point sizes are computed. Specifically, the scale is no longer used to determine the size.

The initial reason was #2213. For a long explanation (with all the caveats and complications), see vispy/vispy#2453 (specifically, this comment has a summary of the behaviour).

While the current state not ideal (and we're trying to figure out over at vispy if we can solve it), this at least solves nasty problems like #2213. However, it means that a size of 10 is still 10 even if the scale of the layer is 5 (and not 50).

This may "break" some code that relies on this transformation; in napari, one of the consequences is that the new points layer button on the viewer gui will now create tiny (or massive) points if the scale is different.

To test, try this:

import napari
import numpy as np

v = napari.Viewer()
v.add_image(np.random.rand(100, 100), scale=[10, 10])

then click the "new points" button and try to annotate some points manually. On main, they'll be so small that they're invisible.


This also exposed another issue (which became the main point of this PR): while point sizes are currently anisotropic, this information is not properly used in many places (visualisation being the primary); not only it's mostly unused, but it's handwaved in a few places (we arbitrarily take the average size over each dimension to determine the visualised size, we only allow setting isotropic sizes from gui and the current_size, and so on).

Ultimately, we decided that we can't reasonably support anisotropic sizes, so we should do away with them. Most changes in this PR have to do with that.

Type of change

  • Bug-fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

@github-actions github-actions bot added the qt Relates to qt label Feb 22, 2023
@codecov
Copy link

codecov bot commented Feb 22, 2023

Codecov Report

Merging #5582 (c154f0c) into main (37c7ec2) will decrease coverage by 0.02%.
The diff coverage is 95.55%.

@@            Coverage Diff             @@
##             main    #5582      +/-   ##
==========================================
- Coverage   91.47%   91.45%   -0.02%     
==========================================
  Files         571      571              
  Lines       49771    49710      -61     
==========================================
- Hits        45527    45463      -64     
- Misses       4244     4247       +3     
Impacted Files Coverage Δ
napari/_vispy/layers/points.py 94.01% <90.00%> (+0.26%) ⬆️
napari/layers/points/points.py 97.71% <90.00%> (-0.39%) ⬇️
.../_qt/layer_controls/_tests/test_qt_points_layer.py 100.00% <100.00%> (ø)
napari/layers/points/_slice.py 100.00% <100.00%> (ø)
napari/layers/points/_tests/test_points.py 100.00% <100.00%> (ø)
napari/utils/_tests/test_misc.py 100.00% <100.00%> (ø)

@psobolewskiPhD
Copy link
Member

Maybe I'm missing something or perhaps it's a macOS thing (or arm64 thing), but the example from OP look fine to me? I mean the points are small, but on the same order as the pixels? If you set a face color to not white or black it's good?
image
I can make them bigger by increasing the size...

And with this PR it seems no different?
image

@brisvag
Copy link
Contributor Author

brisvag commented Mar 1, 2023

Maybe I shoudl have given a more extreme example. If you use a scale of (1000, 1000), they will be hardly visible (to the point I thought there was a visualisation bug until I realized the issue). Most importantly, the behaviour is different from pre-vispy012, where regardless of scale you'd always get relatively big points when you clicked that button.

@psobolewskiPhD
Copy link
Member

With larger image, indeed on main the points are invisible. But I still see no difference in behavior when I switch to this PR branch 😢

@brisvag
Copy link
Contributor Author

brisvag commented Mar 7, 2023

It worked when setting things manually, but not once I put it in the layer initialization. Turns out, the way we initialize size in Points simply ignores the given size if it's not a scalar...

EDIT: it's worse; while size is an (n, D) array to allow anisotropicity, _current_size only stores a scalar :/

@brisvag
Copy link
Contributor Author

brisvag commented Mar 7, 2023

I'm surprised I never encountered issue here... But I'm not sure how to solve. We also expose size in the gui as a single value. To be honest, I'm not sure why we have anisotropic sizes in the first place, since they cause several issues and we're not even able to display them. AFAIK out_of_slice_display is the only place where anisotropic sizes are relevant... which is a bit weird in the first place :/

@Czaki
Copy link
Collaborator

Czaki commented Mar 10, 2023

Should a similar change be also introduced in the context of add points in the constructor?

I think also about the scenario of saving and load points via csv.

@brisvag
Copy link
Contributor Author

brisvag commented Mar 10, 2023

Should a similar change be also introduced in the context of add points in the constructor?

This is tricky, and comes with all the issues that we had when discussing #4705.

@Czaki
Copy link
Collaborator

Czaki commented Mar 10, 2023

Why cannot calculate the size of the layer property and calculate it based on layer extent.steep?

@brisvag
Copy link
Contributor Author

brisvag commented Mar 10, 2023

Why cannot calculate the size of the layer property and calculate it based on layer extent.steep?

Not sure I follow... the main issue is that we're missing a MISSING value that we can use to determine what to do when the layer gets added to the viewer.

@Czaki
Copy link
Collaborator

Czaki commented Mar 10, 2023

As I understand, the problem is that Vispy ignores the scale parameter of point.
So why do not apply scale before passing points to vispy?

@brisvag
Copy link
Contributor Author

brisvag commented Mar 10, 2023

I think we're losing track of the issue here :P

The problem was initially that anisotropic scales were causing issues because they make no sense for non-deformable objects like markers. This was apparent in your issue in #2213. So I disabled that in vispy by having the scale no longer affect the size of the point.

Now, this revealed an extra problem, which is that not just scale, but also size can be anisotropic, which has exactly the same problem.

I'm wondering if it makes sense to keep that as it it, since we can't even render anisotropic markers, so the only effect that anisotropic size has is to affect out of slice display.

@Czaki
Copy link
Collaborator

Czaki commented Mar 10, 2023

For me using min(scale[-2:]) could be a good heuristic to calculate the scale for the size of points.

@brisvag
Copy link
Contributor Author

brisvag commented Mar 13, 2023

For me using min(scale[-2:]) could be a good heuristic to calculate the scale for the size of points.

Why not max or mean though? Any of these is pretty arbitrary, and might be surprising to the user.

@brisvag
Copy link
Contributor Author

brisvag commented Mar 14, 2023

Notes from chatting with @Czaki:

  • even having anisotropic size does not solve it cause to pass to vispy we need to multiply by scale

Maybe a good heuristic is to just use the last dimension. It solves the problem now, and anisotropic scales/sizes are the only ones affected (which anyways don't work on main).

We're still left with a better situation compared to #2213 because at least points don't zoom in/out when rotating.

@github-actions github-actions bot added the tests Something related to our tests label Apr 12, 2023
@brisvag
Copy link
Contributor Author

brisvag commented Apr 12, 2023

@andy-sweet and @mstabrin: git says you both touched this code quite a bit, so you might be interested in checking this!

I'm a bit confused about the failing tests...

@brisvag brisvag changed the title Use current scale to set new point layer size Make point size isotropic Apr 12, 2023
@brisvag
Copy link
Contributor Author

brisvag commented Jun 2, 2023

This is ready to review and merge now :)

@brisvag brisvag requested review from andy-sweet, Czaki and jni June 2, 2023 09:51
Copy link
Member

@andy-sweet andy-sweet left a comment

Choose a reason for hiding this comment

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

Pretty much looks good and seemed to work as advertised for me. Only request is to fix/clarify the extent augmented issue.

@@ -60,20 +57,23 @@ def _on_data_change(self):

set_data = self.node._subvisuals[0].set_data

# use only last dimension to scale point sizes, see #5582
scale = self.layer.scale[-1]
Copy link
Member

Choose a reason for hiding this comment

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

Optional: my intuitive pick for this would be the root of the volume scaling factor of the transform associated with the node. But this is better than nothing, should work for common cases, and I don't feel strongly here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

root of the volume scaling factor of the transform associated with the node

You mean for all dimensions? but yeah we discussed around this before, every option has some downside, so we settled on this for now and we'll see if people complain :P

self.size[i, :] = (self.size[i, :] > 0) * size
self._clear_extent_augmented()
idx = np.fromiter(self.selected_data, dtype=int)
# TODO: explain why this check; only if size > 0?
Copy link
Member

Choose a reason for hiding this comment

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

Is this a TODO for you or for someone in the future? I can't explain the need for the check either, so I'd also be down to simplify it to self.size[idx] *= size and remove the TODO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added it because I didn't understand this check but it was already there, so I left it for the future. However, I now played around a bit, and I'm pretty sure it's wrong (we definitely want to allow points to have size 0, and nothing seems to break on the vispy side when doing so... I'm running tests now).

napari/layers/points/points.py Show resolved Hide resolved
@brisvag
Copy link
Contributor Author

brisvag commented Jun 6, 2023

While remove the "todo" above, I also simplified the code in two related places as I was trying to understand how it worked. They are just refactors, so nothing should change.

@andy-sweet
Copy link
Member

While remove the "todo" above, I also simplified the code in two related places as I was trying to understand how it worked. They are just refactors, so nothing should change.

I think you forgot to push?

Copy link
Member

@andy-sweet andy-sweet left a comment

Choose a reason for hiding this comment

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

Looks good to me now. I haven't had a chance to retest this manually since the last commit, but I assume all is good.

@brisvag brisvag added the ready to merge Last chance for comments! Will be merged in ~24h label Jun 7, 2023
@brisvag
Copy link
Contributor Author

brisvag commented Jun 7, 2023

@Czaki: this is a pretty big backward-incompatible change; however, there is an argument to be made for including this if #5312 is included in v0.4.18, because the points size behaviour for anyone who uses isotropic sizes (=most users) will then be more consisent between 0.4.17 and 0.4.18.

@jni jni merged commit b9bd525 into napari:main Jun 22, 2023
33 of 34 checks passed
@psobolewskiPhD psobolewskiPhD removed the ready to merge Last chance for comments! Will be merged in ~24h label Jun 22, 2023
Czaki pushed a commit that referenced this pull request Jun 22, 2023
With vispy 0.12 (#5312) we introduced some changes to how point sizes
are computed. Specifically, the `scale` is no longer used to determine
the size.

The initial reason was #2213. For a long explanation (with all the
caveats and complications), see vispy/vispy#2453 (specifically, [this
comment has a summary of the
behaviour](vispy/vispy#2453 (comment))).

While the current state not ideal (and we're trying to figure out over
at vispy if we can solve it), this at least solves nasty problems like
scale of the layer is `5` (and not `50`).

This may "break" some code that relies on this transformation; in
napari, one of the consequences is that the `new points layer` button on
the viewer gui will now create tiny (or massive) points if the scale is
different.

To test, try this:

```py
import napari
import numpy as np

v = napari.Viewer()
v.add_image(np.random.rand(100, 100), scale=[10, 10])
```

then click the "new points" button and try to annotate some points
manually. On `main`, they'll be so small that they're invisible.

---

This also exposed another issue (which became the main point of this
PR): while point sizes are currently anisotropic, this information is
not properly used in many places (visualisation being the primary); not
only it's mostly unused, but it's handwaved in a few places (we
arbitrarily take the average size over each dimension to determine the
visualised size, we only allow setting isotropic sizes from gui and the
`current_size`, and so on).

Ultimately, we decided that we can't reasonably support anisotropic
sizes, so we should do away with them. Most changes in this PR have to
do with that.

<!-- Please delete options that are not relevant. -->
- [x] Bug-fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] This change requires a documentation update

---------

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>
Co-authored-by: Wouter-Michiel Vierdag <w-mv@hotmail.com>
Co-authored-by: Juan Nunez-Iglesias <jni@fastmail.com>
melissawm pushed a commit to melissawm/napari that referenced this pull request Jul 3, 2023
# Description
With vispy 0.12 (napari#5312) we introduced some changes to how point sizes
are computed. Specifically, the `scale` is no longer used to determine
the size.

The initial reason was napari#2213. For a long explanation (with all the
caveats and complications), see vispy/vispy#2453 (specifically, [this
comment has a summary of the
behaviour](vispy/vispy#2453 (comment))).

While the current state not ideal (and we're trying to figure out over
at vispy if we can solve it), this at least solves nasty problems like
napari#2213. However, it means that a size of `10` is still `10` even if the
scale of the layer is `5` (and not `50`).

This may "break" some code that relies on this transformation; in
napari, one of the consequences is that the `new points layer` button on
the viewer gui will now create tiny (or massive) points if the scale is
different.

To test, try this:

```py
import napari
import numpy as np

v = napari.Viewer()
v.add_image(np.random.rand(100, 100), scale=[10, 10])
```

then click the "new points" button and try to annotate some points
manually. On `main`, they'll be so small that they're invisible.

---

This also exposed another issue (which became the main point of this
PR): while point sizes are currently anisotropic, this information is
not properly used in many places (visualisation being the primary); not
only it's mostly unused, but it's handwaved in a few places (we
arbitrarily take the average size over each dimension to determine the
visualised size, we only allow setting isotropic sizes from gui and the
`current_size`, and so on).

Ultimately, we decided that we can't reasonably support anisotropic
sizes, so we should do away with them. Most changes in this PR have to
do with that.

## Type of change
<!-- Please delete options that are not relevant. -->
- [x] Bug-fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] This change requires a documentation update

---------

Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>
Co-authored-by: Wouter-Michiel Vierdag <w-mv@hotmail.com>
Co-authored-by: Juan Nunez-Iglesias <jni@fastmail.com>
@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/anisotropic-point-sizes/83388/2

brisvag added a commit that referenced this pull request Jul 23, 2023
…5802)

# Description

Scale layer interactivity and visualisation are currently not accounting
for the layer scale.

Try the following:

```py
import numpy as np
import napari
v = napari.Viewer()
il = v.add_image(np.random.rand(100, 100))
sl = v.add_shapes([[0, 0], [1, 1]], shape_type='path', scale=[100, 100], edge_width=0.1)
```

You'll see a few issues:
- highlight line is not in screen space as advertised. (This is actually
a problem with points as well)
- the `rotate` handle is way too far because its position is also
calculated not accounting for scale
- interacting with the shape in any mode is basically impossible, cause
coordinates do not account for the scaling of the layer.

This PR basically adds a `/ layer.scale[-1]` in several places to get
back into correct "sceen space". Note that I used the last dimension as
we cannot do anisotropic sizes, just like we recently chose to do with
points in #5582.

This PR also fixes #4538 by changing the logic of how the "minimum drag"
is calculated.

PS: ideally we want to transition to using the `SelectionOverlay` in the
future, but this is a much bigger effort.
 
Also fixes #5752.

## Type of change
<!-- Please delete options that are not relevant. -->
- [x] Bug-fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] This change requires a documentation update
Czaki pushed a commit that referenced this pull request Nov 13, 2023
…5802)

Scale layer interactivity and visualisation are currently not accounting
for the layer scale.

Try the following:

```py
import numpy as np
import napari
v = napari.Viewer()
il = v.add_image(np.random.rand(100, 100))
sl = v.add_shapes([[0, 0], [1, 1]], shape_type='path', scale=[100, 100], edge_width=0.1)
```

You'll see a few issues:
- highlight line is not in screen space as advertised. (This is actually
a problem with points as well)
- the `rotate` handle is way too far because its position is also
calculated not accounting for scale
- interacting with the shape in any mode is basically impossible, cause
coordinates do not account for the scaling of the layer.

This PR basically adds a `/ layer.scale[-1]` in several places to get
back into correct "sceen space". Note that I used the last dimension as
we cannot do anisotropic sizes, just like we recently chose to do with
points in #5582.

This PR also fixes #4538 by changing the logic of how the "minimum drag"
is calculated.

PS: ideally we want to transition to using the `SelectionOverlay` in the
future, but this is a much bigger effort.

Also fixes #5752.

<!-- Please delete options that are not relevant. -->
- [x] Bug-fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] This change requires a documentation update
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change Breaking change requiring a deprecation notice bugfix PR with bugfix qt Relates to qt tests Something related to our tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants