WIP: ENH: Enhance ImageVisual with polar capabilities, add example#2133
WIP: ENH: Enhance ImageVisual with polar capabilities, add example#2133kmuehlbauer wants to merge 7 commits into
Conversation
|
If anyone has ideas how to test this, please let me know. |
almarklein
left a comment
There was a problem hiding this comment.
Nice work! I made a few comments. I'm not a heavy user of polar plots, so I mostly looked at the technical aspects.
|
Thanks @almarklein. I need to rework the polar transform shader again, since in some conditions theta is running out of[0, 2 * pi] ... |
|
I'm still thinking how to test this... |
@requires_application()
@pytest.mark.parametrize('ori', ["UL", "UR", "LR", "LL"])
@pytest.mark.parametrize('dir', ["cw", "ccw"])
@pytest.mark.parametrize('loc', ["N", "NW", "W", "SW", "S", "SE", "E", "NE"])
def test_image_polar(ori, dir, loc):
"""Test image visual"""
csize = 32
with TestingCanvas(size=(csize, csize), bgcolor='w') as c:
if ori in ["UL", "LR"]:
tr1 = (STTransform(scale=(3.0, 3.0, 1), translate=(csize / 2, csize / 2, 0)))
else:
tr1 = (
STTransform(scale=(1.0, 1.0, 1), translate=(csize / 2, csize / 2, 0)))
shape = (csize // 4, csize // 2) + ()
np.random.seed(379823)
data = np.random.randint(0, 255, size=shape, dtype=np.uint8)
image = Image(data,
polar=dict(dir=dir, loc=loc, ori=ori),
cmap="grays",
method='impostor',
interpolation="nearest",
parent=c.scene)
image.transform = tr1
assert_image_approved(c.render(), f"visuals/polar_images/image_polar_{ori}_{dir}_{loc}.png")This creates 64 32x32 px grayscale images, each approx 850 bytes. That sums up to roughly 54kB. Would this work for https://github.com/vispy/test-data? Then I could add those files to the test-repo. |
Sounds good to me - 54k is very modest. |
|
I have mixed feeling about this PR. In the end these updates are working with coordinates. In this PR they are texture coordinates, but in the original PolarTransform they are vertex coordinates. My first issue is that this update to the ImageVisual doesn't fix anything about the PolarTransform and for users who may want to use it in other Visuals. Maybe this being done in the ImageVisual is necessary to do this "right" and get the most accurate polar display with the most flexibility. If so, I'm fine with that. Could some or all of this logic be moved to the PolarTransform and have the user control these direction, location, and origin parameters in Could Does the impostor method need to be used here because operating on vertex coordinates will produce bad results? Like drawing a straight line between two vertices instead of along a polar "path"? |
True.
Also true. This machinery does only work on ImageVisual, where we can extract the needed coordinates directly from the texture.
I'm not sure if there is a way, but in vispy code it is written, that non-linear transforms work best with
That might work. I did similar before using MatrixTransform (eg. rotation), but something still blocked that way of solving.
That's the impression one get, if I still have the feeling, that this should work the |
|
@djhoese Thanks for asking these questions. They keep me going 😀 I've made some progress with |
|
Well that looks good...right? Did the code get ugly? Does it still have to be done in the fragment shader? |
|
@djhoese No, it's plain simple in the vertex shader. But it's not configurable atm. I'll try to show some code tomorrow. |
import numpy as np
import vispy.app
from vispy import gloo
from vispy import visuals
from vispy.visuals.transforms import STTransform, PolarTransform, MatrixTransform
directions = ["cw", "ccw"]
dir_index = 0
locations = ['N', 'NW', 'W', 'SW', 'S', 'SE', 'E', 'NE']
loc_index = 0
origins = ["top", "bottom"]
ori_index = 0
polar = [True, False]
pol_index = 0
csize = 800
def cycle_state(states, index):
new_index = (index + 1) % len(states)
return states[new_index], new_index
class Canvas(vispy.app.Canvas):
def __init__(self):
vispy.app.Canvas.__init__(self, keys='interactive', size=(csize, csize))
# Create image
xmax = csize // 4
xres = xmax / 360
ymax = csize // 2
yres = ymax / 100
image = np.ones((xmax, ymax), dtype=np.uint8)
print(image.shape)
grad = np.linspace(0, 255, ymax, dtype=np.uint8)[None, :]
image *= grad
bl_y1_start, bl_y1_stop = int(yres * 80), int(yres * 90)
bl_y2_start, bl_y2_stop = int(yres * 95), int(yres * 100)
wt_y1_start, wt_y1_stop = int(yres * 5), int(yres * 10)
wt_y2_start, wt_y2_stop = int(yres * 90), int(yres * 95)
image[:, wt_y1_start:wt_y1_stop] = 255
image[:, wt_y2_start:wt_y2_stop] = 255
image[:, bl_y1_start:bl_y1_stop] = 0
image[:, bl_y2_start:bl_y2_stop] = 0
image[int(xres*10):int(xres*80)] = 255
image[int(xres*340):int(xres*350)] = 255
self.image = visuals.ImageVisual(image,
cmap="grays",
grid=(1, 360),
interpolation="nearest",
method='subdivide')
tr1 = (STTransform(scale=(1.0, 1.0, 1), translate=(csize/2, csize/2, 0)))
self.image.transform = tr1
self.visuals = [self.image]
self.title = (f"Direction (d): {directions[dir_index]} - "
f"Location (l): {locations[loc_index]} - "
f"Origin (o): {origins[ori_index]} - "
f"Polar (p): {polar[pol_index]}"
)
self.show()
def on_draw(self, ev):
gloo.clear(color='w', depth=True)
for vis in self.visuals:
vis.draw()
def on_resize(self, event):
# Set canvas viewport and reconfigure visual transforms to match.
vp = (0, 0, self.physical_size[0], self.physical_size[1])
self.context.set_viewport(*vp)
for vis in self.visuals:
vis.transforms.configure(canvas=self, viewport=vp)
def on_key_press(self, event):
global dir_index, loc_index, ori_index, pol_index
if event.key == 'd':
dir0, dir_index = cycle_state(directions, dir_index)
elif event.key == 'l':
loc0, loc_index = cycle_state(locations, loc_index)
elif event.key == 'o':
ori0, ori_index = cycle_state(origins, ori_index)
elif event.key == "p":
pol0, pol_index = cycle_state(polar, pol_index)
else:
pass
self.title = (f"Direction (d): {directions[dir_index]} - "
f"Location (l): {locations[loc_index]} - "
f"Origin (o): {origins[ori_index]} - "
f"Polar (p): {polar[pol_index]}")
# direction
dir0 = (-1. if directions[dir_index] == 'cw' else 1.)
# location
locmap = {
'N': np.pi * 0.0,
'NW': np.pi * 0.25,
'W': np.pi * 0.5,
'SW': np.pi * 0.75,
'S': np.pi * 1.0,
'SE': np.pi * 1.25,
'E': np.pi * 1.5,
'NE': np.pi * 1.75}
loc0 = locmap[locations[loc_index]] / (np.pi * 2)
# origin
srcmap = {
"top": 0,
"bottom": 1,
}
ori0 = srcmap[origins[ori_index]]
if polar[pol_index]:
tr = (
# move image to center and scale
STTransform(scale=(1.0, 1.0), translate=(400, 400))
# 0
# just plain simple polar transform
* PolarTransform()
# 1
# pre scale image to work with polar transform
# PolarTransform does not work without this
* STTransform(scale=(2 * np.pi / self.image.size[0], 1.0))
# 2
# origin switch via translate.y, fix translate.y
* STTransform(translate=(self.image.size[0] * (ori0 % 2) * 0.5,
-self.image.size[1] * (ori0 % 2)))
# 3
# location change via translate.x
* STTransform(translate=(self.image.size[0] * (loc0 - 0.25), 0.0))
# 4
# direction switch via inverting scale.x
* STTransform(scale=(dir0, 1.0))
)
else:
tr = (
STTransform(scale=(1.0, 1.0), translate=(400, 400))
# origin switch via translate.y, fix translate.y
* STTransform(translate=(self.image.size[0] * (ori0 % 2) * 0.5,
-self.image.size[1] * (ori0 % 2)))
# location change via translate.x
* STTransform(translate=(self.image.size[0] * (loc0 - 0.25), 0.0))
# direction switch via inverting scale.x
* STTransform(scale=(dir0, 1.0))
)
self.visuals[0].transform = tr
self.update()
if __name__ == '__main__':
win = Canvas()
import sys
if sys.flags.interactive != 1:
vispy.app.run() |
|
The above now works with
It looks like we can't source out everything to I'll not have time to give this more love until early august, so if 0.8 is due before, just remove it from the milestone. |
|
Very nice @kmuehlbauer! How do you want to go forward with this? Do you think this PR is needed? Does the above example work fine with If we want to advertise the PolarTransform as an option like in the example above maybe we can update the docstring for it with examples and mention these "gotchas" of the ImageVisual including these chains of transforms. |
|
@djhoese No, the PR is probably not needed anymore. I haven't tested with I'll clean up this mess here tomorrow and work along your suggestion. |
|
Amazing! Nice job. Can't wait to see the final code. |
|
@djhoese Unfortunately this does not work with I'll not have time to give this more love until early august, so I will set this to draft here and remove the milestone. I've answered the SO question with the current findings (https://stackoverflow.com/questions/68177737/how-to-convert-visuals-image-to-polar-using-vispy). |
|
@djhoese @almarklein I'll pick this up again in a few days. |
|
Just for the record, here are some examples of polar transforms with matplotlib. The idea is just to see which one can be implemented with this PR (it's mostly a matter of parameters): And another one: |
|
Hi, |
|
@ViNOJ-DAViS |
This PullRequest enhances the ImageVisual to display a polar representation of itself via
kwargpolar(eg.polar=(dir, loc, origin))polarto work on existing ImageVisualsimpostoris neededTodo: Add tests, add imap-functionality to remap coordinates.
Context:
As asked on SO (https://stackoverflow.com/questions/68177737/how-to-convert-visuals-image-to-polar-using-vispy) and the chat on gitter (https://gitter.im/vispy/vispy?at=60e1e0194e325e6132bc5cf9) the normal transform system can't be used (AFAICT) to create what we want. We do not have access to all these image attributes (size, direction, location, origin) within the transform chain. So I decided to put this directly into ImageVisual. That way we leave the transform chain alone and can do all other kinds of transforms. Not sure, if this the best approach, but it works quite nicely. See also the attached example.