Skip to content

use cmap library for colormaps #390

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 15 commits into from
Jul 31, 2024

Conversation

tlambert03
Copy link
Contributor

@tlambert03 tlambert03 commented Dec 4, 2023

this PR is an example of using https://github.com/tlambert03/cmap instead of matplotlib for colormaps.

It does add cmap to setup.py ... but if you'd prefer not to go that far, we could guard it all behind an import and say "must install cmap to display stuff with colormaps"

closes #387

@kushalkolar
Copy link
Member

Seems like there are slight color differences between the cmap library and matplotlib ones? You can download the screenshot diffs at the bottom: https://github.com/fastplotlib/fastplotlib/actions/runs/7090258613

I don't think it's an issue, but I'm curious why it's different.

@tlambert03
Copy link
Contributor Author

thanks for the heads up @kushalkolar. I'll have a look at it and figure out what's going on.

I do actually test for parity with matplotlib here (both naming and the actual effect of applying the cmap to an image), so i suspect that the differences might be in the way that colormaps are actually applied in the functions module. I did notice a very slight difference in the values of get_cmap as well, so I suspect that might be the root of it. I'll do some digging and get back to you :)

@tlambert03
Copy link
Contributor Author

btw, I'd like to run these tests locally, but i ran into #388 ... any thoughts there?

@kushalkolar
Copy link
Member

Thanks!

Everything in the regenerate screenshots workflow seems fine visually, so you can replace the ground truth of the failing tests with those.

@tlambert03 tlambert03 changed the title example using cmap use cmap library for colormaps Dec 7, 2023
Copy link
Member

@kushalkolar kushalkolar left a comment

Choose a reason for hiding this comment

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

Other than that docstring lgtm! The colormap text files can now be removed too.

Curious what the difference was previously when the colors weren't matching?

@tlambert03
Copy link
Contributor Author

Curious what the difference was previously when the colors weren't matching?

oh good. it's working on CI too!
Yeah, the difference is actually a good one for you to know about:

the cmap.Colormap object behaves very much like a matplotlib.colors.Colormap, with the main API being the __call__ method, which takes an array of scalars and returns an array of colors. And, just like the matplotlib API, if the input array is of dtype float, it expects values from 0-1, and it's of dtype int, it expects 0-255.

In [27]: import cmap

In [28]: viridis = cmap.Colormap('viridis')

In [29]: viridis(np.linspace(0, 255, 5, dtype=int))
Out[29]:

array([[0.267004, 0.004874, 0.329415, 1.      ],
       [0.231674, 0.318106, 0.544834, 1.      ],
       [0.128729, 0.563265, 0.551229, 1.      ],
       [0.360741, 0.785964, 0.387814, 1.      ],
       [0.993248, 0.906157, 0.143936, 1.      ]])

In [31]: from matplotlib import colormaps

In [32]: mpl_viridis = colormaps['viridis']

In [33]: mpl_viridis(np.linspace(0, 255, 5, dtype=int))
Out[33]:

array([[0.267004, 0.004874, 0.329415, 1.      ],
       [0.231674, 0.318106, 0.544834, 1.      ],
       [0.128729, 0.563265, 0.551229, 1.      ],
       [0.360741, 0.785964, 0.387814, 1.      ],
       [0.993248, 0.906157, 0.143936, 1.      ]])

There's of course a subtle difference in the input array there, for N evenly spaced numbers:

In [35]: np.linspace(0, 1, 5, dtype=float)
Out[35]: array([0.  , 0.25, 0.5 , 0.75, 1.  ])

In [36]: np.linspace(0, 255, 5, dtype=int)/255
Out[36]: array([0.        , 0.24705882, 0.49803922, 0.74901961, 1.        ])

and

In [29]: viridis(np.linspace(0, 255, 5, dtype=int))
Out[29]:

array([[0.267004, 0.004874, 0.329415, 1.      ],
       [0.231674, 0.318106, 0.544834, 1.      ],
       [0.128729, 0.563265, 0.551229, 1.      ],
       [0.360741, 0.785964, 0.387814, 1.      ],
       [0.993248, 0.906157, 0.143936, 1.      ]])

In [30]: viridis(np.linspace(0, 1, 5, dtype=float))
Out[30]:

array([[0.267004, 0.004874, 0.329415, 1.      ],
       [0.229739, 0.322361, 0.545706, 1.      ],
       [0.127568, 0.566949, 0.550556, 1.      ],
       [0.369214, 0.788888, 0.382914, 1.      ],
       [0.993248, 0.906157, 0.143936, 1.      ]])

... so, in my original PR, I used a slightly lower level construct to retrieve the N evenly spaced colors. Rather than using Colormap.__call__(some_array), I used Colormap.lut(n_colors) ... which was slightly different than the existing code you had here which used np.take on a linspace array of int:

In [37]: viridis.lut(5)
Out[37]:

array([[0.267004  , 0.004874  , 0.329415  , 1.        ],
       [0.23022275, 0.32129725, 0.545488  , 1.        ],
       [0.1281485 , 0.565107  , 0.5508925 , 1.        ],
       [0.36285925, 0.786695  , 0.386589  , 1.        ],
       [0.993248  , 0.906157  , 0.143936  , 1.        ]])

so I switched it to the __call__ approach and then everything matched the current screenshots. probably visually imperceptible, but might as well keep the same behavior :) Don't hesitate to ask if you have any questions about the cmap object. But in general, the __call__ method was designed (and tested) to behave exactly like matplotlibs.

@tlambert03
Copy link
Contributor Author

The colormap text files can now be removed too.

oh wow... to be honest, I somehow missed the distinction there that matplotlib was only being used as a fallback.

So, a question for you: I can definitely understand wanting to keep dependencies to the absolute minimum. So, would you like me to update this PR slightly to keep the current behavior for named colormaps that you already bundle, remove cmap from setup.py and fallback to cmap only in the case that a string name is unrecognized? That would still allow you to support custom colormaps using all the previously discussed syntax, but the user would have to opt-in to that behavior. Thoughts?

@kushalkolar
Copy link
Member

There's of course a subtle difference in the input array there, for N evenly spaced numbers:

In [35]: np.linspace(0, 1, 5, dtype=float)
Out[35]: array([0.  , 0.25, 0.5 , 0.75, 1.  ])

In [36]: np.linspace(0, 255, 5, dtype=int)/255
Out[36]: array([0.        , 0.24705882, 0.49803922, 0.74901961, 1.        ])

That is a subtle difference, thanks for tracking it down! I think the reason we used the approach that we did, and use int, is because it makes it easier to work with qualitative colormaps when you give n_colors. (not entirely sure, I wrote that part of the code years ago in a previous library)

So, a question for you: I can definitely understand wanting to keep dependencies to the absolute minimum. So, would you like me to update this PR slightly to keep the current behavior for named colormaps that you already bundle, remove cmap from setup.py and fallback to cmap only in the case that a string name is unrecognized? That would still allow you to support custom colormaps using all the previously discussed syntax, but the user would have to opt-in to that behavior. Thoughts?

Since cmap only depends on numpy I'm fine with keeping it in, if there are issues down the road I would rather vendor it in, but I hope you're able to keep producing all the cool things you've been doing in the imaging ecosystem! :)

I like that this will now enable us to do things like line.cmap = ["blue", "green", "yellow"] to create custom arbitrary colormaps. Speaking of which, would you be interested in implementing that? No worries if you don't have time, I can easily do it in another PR 😄

@kushalkolar
Copy link
Member

One more thing, does the cmap lib allow centering diverging colormaps arbitrarily?

For example if the following values exist and you want to center the seismic colormap around 0, instead of the middle value here which would be -2?

[-6, -5, -4, -3, -2, -1, 0, 1, 2]

@tlambert03
Copy link
Contributor Author

I like that this will now enable us to do things like line.cmap = ["blue", "green", "yellow"] to create custom arbitrary colormaps.

I just changed examples/notebooks/lines_cmap.ipynb to the following code:

plot.graphics[0].cmap = ["blue", "green", "yellow", "#FA499E", (0, 0.2, 0.99)]

and it seemed to work just fine:

Screenshot 2023-12-08 at 10 47 32 AM

... was there something else you were expecting would be needed?

@kushalkolar
Copy link
Member

kushalkolar commented Dec 8, 2023

Ah that's nice when two libraries work together without extra work, maybe speaks to the design of both 😄 .

Does the list of colors also work with the line collection and image example? If that works I will merge, thanks a lot! :D

I'm curious what your thoughts are on this part of our API, our goal was make it easier than and more intuitive than other libraries

@tlambert03
Copy link
Contributor Author

image example:

Screenshot 2023-12-08 at 12 32 39 PM

I wasn't able to find a LineCollection example, which one should I try?

@kushalkolar
Copy link
Member

This example: https://github.com/fastplotlib/fastplotlib/blob/main/examples/desktop/line_collection/line_stack.py

If you have pyqt6 installed you can use the %gui qt hook. Otherwise you can force the glfw canvas with plot = fpl.Plot(canvas="glfw")

@tlambert03
Copy link
Contributor Author

thanks. Yeah that example works, but it does get more confusing due to the overloading of the argument as a colormap or list of colormaps. So, one has to be rather careful there. If I do this:

# line stack takes all the same arguments as line collection and behaves similarly
cmaps = ["jet"] * len(data)
cmaps[-2:] = ["cubehelix", ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]]
plot.add_line_stack(data, cmap=cmaps)

then it works and I see this:
Screenshot 2023-12-08 at 1 29 10 PM

... but it's rather easy to get into trouble there

@kushalkolar
Copy link
Member

Yes you are right, because then this will raise:

plot.add_line_stack(data, cmap=["red", "blue", "green"])`

But if a line collection is already created, then this will work:

plot.graphics[0].cmap = ["red", "blue", "green"]
File ~/repos/fastplotlib/fastplotlib/graphics/line_collection.py:125, in LineCollection.__init__(self, data, z_position, thickness, colors, alpha, cmap, cmap_values, name, metadata, *args, **kwargs)
    123 elif isinstance(cmap, (tuple, list)):
    124     if len(cmap) != len(data):
--> 125         raise ValueError(
    126             "cmap argument must be a single cmap or a list of cmaps "
    127             "with the same length as the data"
    128         )
    129     single_color = False
    130 else:

ValueError: cmap argument must be a single cmap or a list of cmaps with the same length as the data

Question: We could enforce that the cmap kwarg should represent a single cmap. If the user wants to pass a list of cmaps we make a new kwarg called cmaps?

After the line collection has been created, this still works to change cmaps of individual lines:

plot.graphics[0][2:5].cmap = ["blue", "yellow", "green"]

@tlambert03
Copy link
Contributor Author

Question: We could enforce that the cmap kwarg should represent a single cmap. If the user wants to pass a list of cmaps we make a new kwarg called cmaps?

I can see arguments for and against. (notably, it's a breaking change). With the appropriate type checking and exception messages, it should be possible to keep a single cmap argument, but you just need to be careful about the inspection of the object and provide clear help/feedback

@almarklein almarklein mentioned this pull request Mar 13, 2024
2 tasks
@kushalkolar
Copy link
Member

Question: We could enforce that the cmap kwarg should represent a single cmap. If the user wants to pass a list of cmaps we make a new kwarg called cmaps?

I can see arguments for and against. (notably, it's a breaking change). With the appropriate type checking and exception messages, it should be possible to keep a single cmap argument, but you just need to be careful about the inspection of the object and provide clear help/feedback

Hi, sorry for the late reply, been slow getting back into this after the winter holidays!

I think we can allow the following types of use for the cmap argument in LineCollection:

  1. If it is a str of a valid cmap name, ex. jet, then it is handled how it currently is; first line would be dark blue, last line would be red.
  2. If it is a list of str check if it is possible to make a cmap from it using your library (so this should be a list of valid color names). If not go to possiblity (3) below.
  3. If it is a list of str with the same length as the number of lines, then check to make sure every str in the list is a valid cmap and each individual line uses this cmap across the length of the line.
  4. If it is a list of list, if len(outerlist) == n_lines, check that each sublist can be made into a cmap from your library and use this to color each individual line.

I think this should cover all the cases? 😄

@kushalkolar kushalkolar mentioned this pull request Mar 29, 2024
11 tasks
@kushalkolar kushalkolar requested a review from clewis7 as a code owner June 4, 2024 16:53
@kushalkolar kushalkolar mentioned this pull request Jun 19, 2024
4 tasks
@kushalkolar kushalkolar changed the base branch from main to cmap-intermediate July 31, 2024 05:24
@kushalkolar kushalkolar merged commit 1c92d97 into fastplotlib:cmap-intermediate Jul 31, 2024
7 of 10 checks passed
kushalkolar added a commit that referenced this pull request Jul 31, 2024
* use cmap library for colormaps (#390)

* example with cmap

* add to deps

* fix shape

* use full map

* remove break

* undo changes to screenshots

* fix parse

* remove diffs

* remove x.py

* minimize diff

* add docstring

* remove colormaps

---------

Co-authored-by: Kushal Kolar <kushalkolar@gmail.com>

* fix pygfx docs move

* black

---------

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
kushalkolar added a commit that referenced this pull request Aug 28, 2024
* example with cmap

* add to deps

* fix shape

* use full map

* remove break

* undo changes to screenshots

* fix parse

* remove diffs

* remove x.py

* minimize diff

* add docstring

* remove colormaps

---------

Co-authored-by: Kushal Kolar <kushalkolar@gmail.com>
kushalkolar added a commit that referenced this pull request Sep 29, 2024
* ndarray.ptp -> np.ptp for numpy v2

* use cmap library for colormaps (#390)

* example with cmap

* add to deps

* fix shape

* use full map

* remove break

* undo changes to screenshots

* fix parse

* remove diffs

* remove x.py

* minimize diff

* add docstring

* remove colormaps

---------

Co-authored-by: Kushal Kolar <kushalkolar@gmail.com>

* fix pygfx docs move

* black

* start imgui implementation

* right click menu, hard-code toolbar height

* happy with right click menu

* black

* remove old id counter global

* cmap picker menu working, going to use cmap next to get textures working

* remove unused texture sampler

* black

* use selectable instead of menu_item

* bug and filter out wheel events too

* sort cmaps by category

* imgui imagewidget sliders working

* reset vmin vmax

* image widget with imgui done

* image widget video example

* better organization of kwargs for EdgeWindow

* add custom imgui with imagewidget example

* remove old stuff from subplot

* canvas kwargs to set initial size

* test examples works with imgui, but image widget sliders cut off for some reason

* screenshot tests work with imgui

* reset viewports when EdgeWindows added

* no more output contexts in Figure

* no more output contexts :D

* cleanup imagewidget.show()

* rename standard right click menu

* update examples

* docstrings

* better edge window

* simplify example

* cleanup

* more cleanup

* modify for docs

* modify API docs

* docstrings

* docstrings

* add UI To docs

* docs

* add UI to docs, more

* UI section in docs gallery

* docs gallery dir in conf

* add imgui-bundle to setup.py

* black

* bugfix

* use cmap library for colormaps (#390)

* example with cmap

* add to deps

* fix shape

* use full map

* remove break

* undo changes to screenshots

* fix parse

* remove diffs

* remove x.py

* minimize diff

* add docstring

* remove colormaps

---------

Co-authored-by: Kushal Kolar <kushalkolar@gmail.com>

* fix pygfx docs move

* black

* start imgui implementation

* right click menu, hard-code toolbar height

* happy with right click menu

* black

* remove old id counter global

* cmap picker menu working, going to use cmap next to get textures working

* remove unused texture sampler

* black

* use selectable instead of menu_item

* bug and filter out wheel events too

* sort cmaps by category

* imgui imagewidget sliders working

* reset vmin vmax

* image widget with imgui done

* image widget video example

* better organization of kwargs for EdgeWindow

* add custom imgui with imagewidget example

* remove old stuff from subplot

* canvas kwargs to set initial size

* test examples works with imgui, but image widget sliders cut off for some reason

* screenshot tests work with imgui

* reset viewports when EdgeWindows added

* no more output contexts in Figure

* no more output contexts :D

* cleanup imagewidget.show()

* rename standard right click menu

* update examples

* docstrings

* better edge window

* simplify example

* cleanup

* more cleanup

* modify for docs

* modify API docs

* docstrings

* docstrings

* add UI To docs

* docs

* add UI to docs, more

* UI section in docs gallery

* docs gallery dir in conf

* add imgui-bundle to setup.py

* black

* bugfix

* basic imgui example

* finish basic custom imgui example

* start with zero noise so screenshots always match

* reset button

* comments

* plain Figure.get_pygfx_render_area()

* add get_pygfx_render_area() to basic Figure

* option to use basic Figure even if imgui installed

* image widget examples in regular examples

* black, cleanup

* image widget event when index changes

* imgui toolbar can be hidden

* black

* more black

* update api docs

* doc

* comments iw imgui sliders

* comment

* fix

* comments

* black

* update iw test nb w.r.t. imgui changes

* add iw dir to tests

* api docs

* edit api docs stuff

* readme to iw examples for docs

* docs conf.py

* stuff to make sure gui examples render in docs

* image[pyav] for docs

* fix iw movie example

* move stuff in docstring

* black

* smaller vid example for iw

* delete other heatmap examples for now

* smaller heatmap

* very small heatmap

* new screenshots with imgui

* add imgui-bundle to desktop tests

* are selectors causing OOM issues with docs

* actually bring back guis

* remove selectors

* remove misc also

* settable texture limit for 2D textures, texture test with smaller RAM footprint

* heatmap with wgpu limits set

* for examples gallery limit must be set in docs conf.py

* set texture limit for examples tests

* wgpu limits set in conftest.py

* fix linked controllers change with imgui

* autouse texture limit setup for pytest session

* correct way to set wgpu limits for pytest session

* can we now get away with phree github actions

* random seed so test screenshot looks the same

* update heatmap and iw videos test screenshots

* remove heatmap nb, add note in heatmap example

* add back selector examples

* remove linear selectors example see if docs build

* only lines for linear selector example

* separate example for image selector to see if it doesn't overrun RAM

* use real image for linear seletor

* figuring out wtf is wrong with image linear selctor on rtd

* remove zoom, IDK know anymore, wtf is up with rtd

* idk wtf is going on, makes no senze, just going to show code and not render image linear selector example for no

* bring back misc examples

* heavily subsample cockatoo vid because sphinx gallery has a memory leak :/

* iw video examples play

* add imagewidget grid to docs gallery

* proper imgui render for docs

* add subsampled iw video screenshots

* add image widget grid example

* tweak lorenz example

* added the wrong screenshot

* bring back linear selector image example

* update examples

* update setup.py

* I a comma

* slider tweaks

* update user guide

* update doc for image widget example

* fix guide

* update guide images

* other update docs

---------

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

consider cmap library?
2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy