-
Notifications
You must be signed in to change notification settings - Fork 616
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
Update clims in color transform whenever texture._data_limits changes #2245
Conversation
So what is the case that doesn't work with what is currently in |
ok, I think what we really need to know is whether the CPU texture's pre_lims = getattr(self._texture, '_data_limits', None)
self._texture.scale_and_set_data(self._data)
post_lims = getattr(self._texture, '_data_limits', None) or something else? I'm sorry, I don't have a great vispy-only MRE. this is based on the visual effect of changing the clims on an image in napari (which essentially just changed the vispy image clims) and based on this test. In some cases a full redraw is necessary to trigger the change, and this fixes that. |
Any idea what those cases are? @brisvag and I had looked at that test in the last month or so, but it was hard to understand what it was doing. I know the |
pretty sure the case is when like I said, I know that it would be great to have a vispy-only test here... but I don't have time this week to re-orient myself to those tests. perhaps @brisvag can narrow this down? |
I'll see if I can think of a test case in the next couple days and maybe we can narrow this down to the point that we know we are fixing it rather than only providing a workaround. |
I'll also look into it, but yeah it's complicated :) |
I just found out that this is a problem with |
@brisvag @tlambert03 One thing that doesn't make sense to me (again) about the napari test, if the visible toggle is calling |
Yes, that's also where I landed @djhoese. I'm a bit lost as to why this causes this inconsistent state. |
Due to the nature of "events" I can't figure out what the "set_data" event in napari does. Can someone point me to the code for what happens when it is emitted by the layer (at least as far as the layer/ImageVisual) is concerned? |
As far as I can tell, the only thing that gets fired in both cases (vispy only and napari) is
These are the lines fired by that event in napari. There are a few if statements for changing between volume and image visual, but then it simply calls |
will try to look closer this week, but approaching this from a different direction, I do think that the core change in this PR (updating the color_transform when the texture data_limits changes) makes a lot of sense. That really is the thing we need to coordinate... not the current |
I'd still like a test that fails without this PR and passes with this PR. |
yep yep! thats also what I meant by "I think we should do that" |
Yes, that's the thing that bugs me; I cannot find where things go differently in napari. I tried triggering |
Finally getting to work on this. first of all, this is specifically a Run this in rhe REPL to set up the canvas: from vispy import scene, app
import numpy as np
canvas = scene.SceneCanvas()
canvas.size = 300, 300
canvas.show()
view = canvas.central_widget.add_view()
np.random.seed(0)
data = np.random.rand(100, 100) * 100
image = scene.visuals.Image(data, parent=view.scene)
view.camera = scene.PanZoomCamera(aspect=1)
view.camera.set_range()
app.run() then, run image.clim = 0, 10 then run it again. If you're on |
Unfortunately, the same thing does not seem to cause problems with pytest. This passes on main: @requires_application()
def test_change_clim_float():
"""
Test that with an image of floats, clim is correctly set from the first try
see https://github.com/vispy/vispy/pull/2245
"""
size = (40, 40)
np.random.seed(0)
data = np.random.rand(*size) * 100
with TestingCanvas(size=size[::-1], bgcolor="w") as c:
image = Image(data=data, parent=c.scene)
image.clim = 0, 10
rendered1 = c.render()
# set clim to same values
image.clim = 0, 10
rendered2 = c.render()
assert np.allclose(rendered1, rendered2) |
There must be somthing with from vispy.scene.visuals import Image
from vispy.testing._testing import TestingCanvas
import numpy as np
size = (40, 40)
np.random.seed(0)
data = np.random.rand(*size) * 100
with TestingCanvas(size=size[::-1], bgcolor="w") as c:
image = Image(data=data, parent=c.scene)
image.clim = 0, 10
rendered1 = c.render()
# set clim to same values
image.clim = 0, 10
rendered2 = c.render()
assert np.allclose(rendered1, rendered2) @djhoese do you have any suggestions? |
I rectify: it's not |
Very interesting. What else does the context manager do on the test canvas? Can it be not used? |
I think we have to use it, cause it prevents some failures in windows and other CI-related issues. But the issue is already in |
Good job everyone. I'm really busy today and I'd really like to sit down and understand everything going on here before merging it, so I might not get to it today. I will try to finish reviewing this this weekend or early next week. If I don't get to it by then then we can just merge it and make a patch release and I'll fix anything I find later on. |
So set aside some time tonight and dove into this. All these shortcuts and trying to be smart is terrible. It is some how better than before my float textures PR, but still too many shortcuts. I found an alternate solution that I like better, but I'll wait to see what the rest of you think before pushing it to this PR. The test is unchanged and my solution also works. Here is the series of events and why things work or don't work (this may not make sense as it is how my brain walked through the process, but I may need this for reference in the future):
My proposed solution: diff --git a/vispy/visuals/image.py b/vispy/visuals/image.py
--- a/vispy/visuals/image.py (revision 3918872421dc59d4f7679660dd3cce9efadaea51)
+++ b/vispy/visuals/image.py (date 1637635555974)
@@ -521,18 +521,22 @@
self._prepare_transforms(view)
def _build_texture(self):
- pre_lims = getattr(self._texture, '_data_limits', None)
+ try:
+ pre_clims = self._texture.clim_normalized
+ except RuntimeError:
+ pre_clims = "auto"
pre_internalformat = self._texture.internalformat
self._texture.scale_and_set_data(self._data)
- post_lims = getattr(self._texture, '_data_limits', None)
+ post_clims = self._texture.clim_normalized
post_internalformat = self._texture.internalformat
# color transform needs rebuilding if the internalformat was changed
- # new color limits need to be assigned if the texture data limits changed
+ # new color limits need to be assigned if the normalized clims changed
# otherwise, the original color transform should be fine
- # Note that this assumes that if clim changed, clim_normalized changed
- if post_internalformat != pre_internalformat:
+ new_if = post_internalformat != pre_internalformat
+ new_cl = post_clims != pre_clims
+ if new_if:
self._need_colortransform_update = True
- elif post_lims != pre_lims and not self._need_colortransform_update:
+ elif new_cl and not self._need_colortransform_update:
# shortcut so we don't have to rebuild the whole color transform
self.shared_program.frag['color_transform'][1]['clim'] = self._texture.clim_normalized
self._need_texture_upload = False
The reasons I like this:
Thoughts? |
Also FYI @brisvag it is possible to get this to fail with integer data if you keep everything the same but on creation of the Image you pass Edit: I'll add a test for this no matter what we choose. |
I don't feel strongly either way. It seems that the core issue is that using |
Actually, your code breaks if passing @property
def clim_normalized(self):
"""Normalize current clims to match texture data inside the shader.
If data is scaled on the CPU then the texture data will be in the range
0-1 in the _build_texture() method. Inside the fragment shader the
final contrast adjustment will be applied based on this normalized
``clim``.
"""
if isinstance(self.clim, str) and self.clim == "auto":
raise RuntimeError("Can't return 'auto' normalized color limits "
"until data has been set. Call "
"'scale_and_set_data' first.")
> range_min, range_max = self._data_limits
E TypeError: cannot unpack non-iterable NoneType object |
Yeah, I think we need to access the private property to make sure this works on instantiation as well (when not everything is ready to go). an alternative is to check for both I propose we keep @tlambert03 's code. |
Part of the point here is that this logic is never needed for GPU textures, so it's not surprising that it's a CPU-scaled texture-specific thing. If you only want to use only the abstract interface, we could add |
I was thinking adding a check in the clim_normalized to see if data limits hasn't been set yet, but...
Yeah, I guess the only time the GPU-scaled texture has the normalized clim change is if the internal format changes. The clim being set previously should "stage" the normalized clim for all future steps. However, I think the point still stands that what we really care about is clim_normalized. There is a reason So either way, we need:
FYI if you guys are bored there is a vispy monthly meeting in ~10 minutes if you want to join (DM me on gitter). |
@tlambert03 I was going to push my changes to this branch. And idea why I don't have permissions? |
FYI I was going to push a new error check, an updated test, and my first of the _build_texture method as separate commits so we could revert in the future if needed. |
Sorry im not sure, I've had this issue before, but never unchecked the allow admins button... let me know what you want to do. Or, if you want to just open a new pr that's ok |
You could add me as a collaborator to your fork? If you aren't comfortable with that then yeah I can make PR on your fork or make a new PR here. |
Added!
… On Nov 23, 2021, at 12:39 PM, David Hoese ***@***.***> wrote:
You could add me as a collaborator to your fork? If you aren't comfortable with that then yeah I can make PR on your fork or make a new PR here.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
There was a problem hiding this 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! Test failures are also in other PRs, so I think they are unrelated.
Nah there are definitely some from my changes. That's what I get for trying to be right. |
whoops, spoke too fast! :D |
Ok osmesa tests now pass. Is my solution "good enough" now to use? and merge? |
LGTM. I'm happy with anything that gets @brisvag's test to pass :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good for me too!
Thanks guys. I just want to say that I wasn't ignoring your desire to have |
fixes #2229.
looks like the
if not new_if and new_cl and not self._need_colortransform_update:
conditional in the current_build_texture
method is a little too strict. Back in #1911, we fully updated the color transform whenever the image texture was rebuilt. #1920 tried to make that a little more performant, by only updating when necessary, but it looks like the heuristic isn't perfect. This is somewhere in the middle, only rebuilding the color transform when the internal format has changed, otherwise updating the texture clims stored in the color transform (which have likely changed during thescale_and_set_data
call)with the time available, I wasn't able to determine a better way to know for certain whether those texture clims have changed. but that could perhaps be a future improvement. As is, updating
self.shared_program.frag['color_transform'][1]['clim']
took 10s of microseconds on my computer... which is probably a fraction of the cost of the full_build_texture
method anyway.cc @brisvag