Skip to content

WIP: ENH: Enhance ImageVisual with polar capabilities, add example#2133

Draft
kmuehlbauer wants to merge 7 commits into
vispy:mainfrom
kmuehlbauer:polar_image
Draft

WIP: ENH: Enhance ImageVisual with polar capabilities, add example#2133
kmuehlbauer wants to merge 7 commits into
vispy:mainfrom
kmuehlbauer:polar_image

Conversation

@kmuehlbauer

Copy link
Copy Markdown
Contributor

This PullRequest enhances the ImageVisual to display a polar representation of itself via kwarg polar (eg. polar=(dir, loc, origin))

  • added property polar to work on existing ImageVisuals
  • method impostor is needed
  • dir - direction ("cw", "ccw", clockwise/counterclockwise)
  • loc (location of theta=0, eg "N", "E" etc. )
  • origin (which point is taken to map theta/range, eg. "UL", "LR")

Todo: 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.

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

If anyone has ideas how to test this, please let me know.

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

screen_cast

@almarklein almarklein left a comment

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.

Nice work! I made a few comments. I'm not a heavy user of polar plots, so I mostly looked at the technical aspects.

Comment thread vispy/visuals/image.py Outdated
Comment thread vispy/visuals/image.py Outdated
Comment thread vispy/visuals/image.py Outdated
@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

Thanks @almarklein. I need to rework the polar transform shader again, since in some conditions theta is running out of[0, 2 * pi] ...

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

I'm still thinking how to test this...

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author
@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.

@almarklein

Copy link
Copy Markdown
Member

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.

@djhoese

djhoese commented Jul 12, 2021

Copy link
Copy Markdown
Member

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 PolarTransform.__init__?

Could origin also be accomplished by providing a negative scale value for the X or Y axis in an STTransform?

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"?

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

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.

True.

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.

Also true. This machinery does only work on ImageVisual, where we can extract the needed coordinates directly from the texture.

Could some or all of this logic be moved to the PolarTransform and have the user control these direction, location, and origin parameters in PolarTransform.__init__?

I'm not sure if there is a way, but in vispy code it is written, that non-linear transforms work best with impostor mode. This is also the mode which get's chosen if method="auto". I've tried to work this out using PolarTransform but it didn't find a way.

Could origin also be accomplished by providing a negative scale value for the X or Y axis in an STTransform?

That might work. I did similar before using MatrixTransform (eg. rotation), but something still blocked that way of solving.

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"?

That's the impression one get, if PolarTransform is applied to ImageVisual if subdivide is chosen as method.

I still have the feeling, that this should work the transform-way but either I can't really envision it to myself or I have other problems in understanding.

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

@djhoese Thanks for asking these questions. They keep me going 😀 I've made some progress with subdivide but don't know how exactly this works (my bad).
subdivide

@djhoese

djhoese commented Jul 13, 2021

Copy link
Copy Markdown
Member

Well that looks good...right? Did the code get ugly? Does it still have to be done in the fragment shader?

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

@djhoese No, it's plain simple in the vertex shader. But it's not configurable atm. I'll try to show some code tomorrow.

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author
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()

@kmuehlbauer

kmuehlbauer commented Jul 14, 2021

Copy link
Copy Markdown
Contributor Author

The above now works with subdivide and the transforms in the vertex shader under the following constraints:

  1. Initialize ImageVisual with kwarggrid=(1, N), where N is a reasonable high number (eg. 360)
  2. Before PolarTransform several STTransform have to be applied in order to move the vertexes into the needed locations
  3. These STTransform depend on the size of the ImageVisual, so the whole procedure can't just live as a standalone transform
  4. Only top and bottom can be collapsed to center.

It looks like we can't source out everything to PolarTransform since knowledge of the image size is needed. So this has to live in ImageVisual or in a subclassed PolarImageVisual.

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.

@djhoese

djhoese commented Jul 14, 2021

Copy link
Copy Markdown
Member

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 impostor still?

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.

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

@djhoese No, the PR is probably not needed anymore.

I haven't tested with impostor yet. I was too happy to have subdivide running.

I'll clean up this mess here tomorrow and work along your suggestion.

@djhoese

djhoese commented Jul 14, 2021

Copy link
Copy Markdown
Member

Amazing! Nice job. Can't wait to see the final code.

@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

@djhoese Unfortunately this does not work with impostor as it works with subdivide.

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

@kmuehlbauer kmuehlbauer removed this from the Version 0.8.0 milestone Jul 15, 2021
@kmuehlbauer kmuehlbauer changed the title ENH: Enhance ImageVisual with polar capabilities, add example WIP: ENH: Enhance ImageVisual with polar capabilities, add example Jul 15, 2021
@kmuehlbauer kmuehlbauer marked this pull request as draft July 15, 2021 09:06
@kmuehlbauer

Copy link
Copy Markdown
Contributor Author

@djhoese @almarklein I'll pick this up again in a few days.

@rougier

rougier commented Aug 24, 2021

Copy link
Copy Markdown
Contributor

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):

import numpy as np
import matplotlib.pyplot as plt

R = np.random.uniform(1, 5, 250)
T = np.random.uniform(0, 45/180*np.pi, 250)

fig = plt.figure(figsize=(10,4), dpi=100)

ax = plt.subplot(1, 3, 1, projection="polar")
ax.scatter(T, R, 1, "black", zorder=10)
ax.set_rmin(1)                # Minimum radius
ax.set_rmax(5)                # Maximum radius
ax.set_rorigin(1)             # Origin position
ax.set_rticks(1+np.arange(5)) # Maximum radius
ax.set_thetamin(0)            # Minimum angle (degrees)
ax.set_thetamax(45)           # Maximum radius (degrees)
ax.set_theta_offset(0)        # Origin orientation

ax = plt.subplot(1, 3, 2, projection="polar")
ax.scatter(T, R, 1, "black", zorder=10)
ax.set_rmin(1) 
ax.set_rmax(5) 
ax.set_rorigin(0)
ax.set_rticks([1,5])
ax.set_thetamin(0)
ax.set_thetamax(180)
ax.set_theta_offset(0)
ax.set_xticks([])

ax = plt.subplot(1, 3, 3, projection="polar")
ax.scatter(T, R, 1, "black", zorder=10)
ax.set_rmin(1)
ax.set_rmax(5) 
ax.set_rorigin(-10)
ax.set_rticks([1,2,3,4,5])
ax.set_thetamin(0)
ax.set_thetamax(45)
ax.set_theta_offset(0.5 * 3*np.pi/4)
ax.set_xticks([0, np.pi/4])

plt.show();

And another one:

import numpy as np
import matplotlib.pyplot as plt


X,Y = np.meshgrid(np.linspace(-3, 3, 200),
                  np.linspace(-3, 3, 200))
Z =(1-X/2+X**5+Y**3)*np.exp(-X**2-Y**2)

fig = plt.figure(figsize=(10,10), dpi=300)

# Regular imshow
ax = plt.subplot(1, 3, 1)
ax.imshow(Z)
ax.set_xticks([])
ax.set_yticks([])

# Full polar "imshow" (pcolormesh)
ax = plt.subplot(1, 3, 2, projection="polar")
ax.pcolormesh(np.linspace(0, 2*np.pi, 200),
              np.linspace(0, 1, 200), Z[::-1,::-1], shading='auto')
ax.set_rticks([])
ax.set_xticks([])

# Partial polar "imshow" (pcolormesh)
ax = plt.subplot(1, 3, 3, projection="polar")
ax.pcolormesh(np.linspace(0, np.pi/2, 200),
              np.linspace(0, 1, 200), Z[::-1,::-1], shading='auto')
ax.set_rticks([])
ax.set_xticks([])
ax.set_rmin(0); ax.set_rmax(1)
ax.set_rorigin(-.5)
ax.set_thetamin(0)
ax.set_thetamax(90)

plt.show();

@ViNOJ-DAViS

Copy link
Copy Markdown

Hi,
I am unable to have same polar effect for an scene.visuals.Image
How can I achieve same polar effect similar to ImageVisual

@djhoese

djhoese commented Sep 3, 2021

Copy link
Copy Markdown
Member

@ViNOJ-DAViS Image and ImageVisual are the same Visual code. They both even have a .transforms property although I forget every difference involved here. To get the functionality that @kmuehlbauer has demonstrated in these comments, you should theoretically only have to add an STTransform or two to the chain of transforms being made to handle the "Visual" <-> "Screen" coordinate conversion. If you'd like more help on this please file a new issue and reference this one. Be sure to include a minimal example of your code and what you've tried and what isn't working. Thanks.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants