-
-
Notifications
You must be signed in to change notification settings - Fork 256
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
Help with increasing chromaticity diagrams plotting speed. #362
Comments
Hi @brandondube, Thanks for looking into that, this is great! There is actually #240 about this very specific topic.
This parts scares me a little, would you mind detailing please?
I had started to implement faster diagrams on a related project using Vispy: https://github.com/colour-science/colour-analysis
sRGB cannot represent values outside its gamut, which is over half of the whole visible spectrum :) This issue is of interest in that regard: #191 |
I won't have time to look at that carefully before tonight but here is the code used with Vispy and |
I don't think there's a real need for vispy/gpu plotting -- removing the The timeits I posted above don't consider the view time -- it takes my laptop multiple seconds to draw the diagrams because there are so many points, while viewing an image, even with glyphs on top (plankian locust, wavelength locust, etc) takes mere milliseconds. There is also consequence for saving figures -- a 16M-element scatter plot saved to a vector format (svg, eps, pdf) will have 16 million objects it in, and bring any renderer to its knees while one with an image will contain an embedded png or other raster graphic for that portion that draws quickly. Anyway - the method I wrote code for works as follows:
This requires that the R,G,B "sub-arrays" fall inside the range [0,1], i.e. are valid (s)RGB coordinates. I know that some of each "end" of the horseshoe will have to clip since it isn't contained in sRGB, but I would still expect the interior to result in the correct colors. |
Absolutely, I was just giving that as a reference, I would not want Colour to depend on Vispy :) Your process seems to be fine by the look of it. Out of curiosity, did you try using |
I got that with small tweaking: # %%
import matplotlib.pyplot as plt
import numpy as np
import colour
from colour.plotting import *
#from prysm.geometry import generate_mask
samples = 258
xlim = (0, 1)
ylim = (0, 1)
illuminant = DEFAULT_PLOTTING_ILLUMINANT
wvl = np.arange(420, 700, 10)
wvl_XYZ = colour.wavelength_to_XYZ(wvl)
wvl_uv = colour.Luv_to_uv(colour.XYZ_to_Luv(wvl_XYZ, illuminant), illuminant)
wvl_pts = wvl_uv * samples
# wvl_mask = generate_mask(wvl_pts, samples)
# mask_idxs = np.where(wvl_mask == 0)
u = np.linspace(xlim[0], xlim[1], samples)
v = np.linspace(ylim[0], ylim[1], samples)
uu, vv = np.meshgrid(u, v)
# uu[mask_idxs] = 0.3
# vv[mask_idxs] = 0.3
# stack u and v for vectorized computations
uuvv = np.stack((vv,uu), axis=2)
# map -> xy -> XYZ -> sRGB
xy = colour.Luv_uv_to_xy(uuvv)
xyz = colour.xy_to_XYZ(xy)
dat = colour.XYZ_to_sRGB(xyz)
dat = colour.normalise_maximum(dat, axis=-1)
# now make an alpha/transparency mask to hide the background
# and flip u,v axes because of column-major symantics
alpha = np.ones((samples,samples)) # * wvl_mask
dat = np.swapaxes(np.dstack((dat, alpha)), 0, 1)
#dat /= dat[:, :, 0:2].max()
# lastly, duplicate the lowest wavelength so that the boundary line is closed
wvl_uv = np.vstack((wvl_uv, wvl_uv[0,:]))
fig, ax = plt.subplots(figsize=(8,8))
ax.imshow(dat,
extent=[xlim[0], xlim[1], ylim[0], ylim[1]],
interpolation='None',
origin='lower')
ax.set(xlim=(0,0.65), xlabel='CIE u\'',
ylim=(0,0.625), ylabel='CIE v\'')
ax.plot(wvl_uv[:,0], wvl_uv[:,1], c='0', lw=3) As I look at your code, it is actually not that much different from colour.plotting. CIE_1931_chromaticity_diagram_colours_plot, the biggest change is that you are drawing to the image buffer directly instead of using |
It's now 2:30am my time so I'll be a bit brief, but I looked over your code and made the same change (normalize before tacking on the alpha channel) and the plot is now correct! :D As a naggle, I still get a warning for invalid value in power: any idea what that is? The only power I see is L^(1/2.4), maybe it's a python 3 int/float division problem? Really not sure. The code for generate_mask is here: https://github.com/brandondube/prysm/blob/master/prysm/geometry.py#L179 It could be stolen and added to colour if that is within scope. Basically it fills the interior of a convex polygon defined by its vertices in array index coordinates (i.e. for 128x128, the "units" are in the range [0,127]. That makes adjustable bounds / not generating a mask on [0,1]x[0,1] a bit messy but it's not impossible to deal with. The time complexity is not very good, so it's a little bit slow. I think it is still faster than scipy delaunay, and numba could make it fly :) Using |
Excellent! I'm using I quickly adjusted the definition we ship to use your great approach, I have not timed it and it is dumb as I'm filling just too much void space now, but it gives an idea: import matplotlib
import pylab
from scipy.spatial import Delaunay
from colour import (Luv_to_uv, XYZ_to_Luv, tstack, xy_to_XYZ, Luv_uv_to_xy,
normalise_maximum, XYZ_to_sRGB, tsplit)
def CIE_1976_UCS_chromaticity_diagram_colours_plot(
samples=256,
cmfs='CIE 1931 2 Degree Standard Observer',
**kwargs):
"""
Plots the *CIE 1976 UCS Chromaticity Diagram* colours.
Parameters
----------
surface : numeric, optional
Generated markers surface.
samples : numeric, optional
Samples count on one axis.
cmfs : unicode, optional
Standard observer colour matching functions used for diagram bounds.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> CIE_1976_UCS_chromaticity_diagram_colours_plot() # doctest: +SKIP
"""
settings = {'figure_size': (8, 8)}
settings.update(kwargs)
canvas(**settings)
cmfs = get_cmfs(cmfs)
illuminant = DEFAULT_PLOTTING_ILLUMINANT
triangulation = Delaunay(
Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant),
qhull_options='QJ Qf')
xx, yy = np.meshgrid(
np.linspace(0, 1, samples), np.linspace(0, 1, samples))
xy = tstack((xx, yy))
mask = triangulation.find_simplex(xy) < 0
XYZ = xy_to_XYZ(Luv_uv_to_xy(xy))
RGB = normalise_maximum(XYZ_to_sRGB(XYZ, illuminant), axis=-1)
RGB[mask] = 1
settings.update({
'x_ticker': False,
'y_ticker': False,
'bounding_box': (0, 1, 0, 1)
})
settings.update(kwargs)
ax = matplotlib.pyplot.gca()
ax.imshow(RGB,
extent=[0, 1, 0, 1],
interpolation='None',
origin='lower')
matplotlib.pyplot.setp(ax, frame_on=False)
boundaries(**settings)
decorate(**settings)
return display(**settings)
CIE_1976_UCS_chromaticity_diagram_colours_plot() |
Almost same than above but with poor man antialiasing: import matplotlib
import pylab
from scipy.ndimage.filters import convolve
from scipy.spatial import Delaunay
from colour import (DEFAULT_FLOAT_DTYPE, Luv_to_uv, XYZ_to_Luv, tstack, xy_to_XYZ, Luv_uv_to_xy,
normalise_maximum, XYZ_to_sRGB, tsplit)
def CIE_1976_UCS_chromaticity_diagram_colours_plot(
samples=256,
cmfs='CIE 1931 2 Degree Standard Observer',
**kwargs):
"""
Plots the *CIE 1976 UCS Chromaticity Diagram* colours.
Parameters
----------
surface : numeric, optional
Generated markers surface.
samples : numeric, optional
Samples count on one axis.
cmfs : unicode, optional
Standard observer colour matching functions used for diagram bounds.
Other Parameters
----------------
\**kwargs : dict, optional
{:func:`boundaries`, :func:`canvas`, :func:`decorate`,
:func:`display`},
Please refer to the documentation of the previously listed definitions.
Returns
-------
Figure
Current figure or None.
Examples
--------
>>> CIE_1976_UCS_chromaticity_diagram_colours_plot() # doctest: +SKIP
"""
# settings = {'figure_size': (8, 8)}
settings = {}
settings.update(kwargs)
canvas(**settings)
cmfs = get_cmfs(cmfs)
illuminant = DEFAULT_PLOTTING_ILLUMINANT
triangulation = Delaunay(
Luv_to_uv(XYZ_to_Luv(cmfs.values, illuminant), illuminant),
qhull_options='QJ Qf')
xx, yy = np.meshgrid(
np.linspace(0, 1, samples), np.linspace(0, 1, samples))
xy = tstack((xx, yy))
mask = (triangulation.find_simplex(xy) < 0).astype(DEFAULT_FLOAT_DTYPE)
kernel = np.array([[0, 1, 0],
[1, 2, 1],
[0, 1, 0]]).astype(DEFAULT_FLOAT_DTYPE)
kernel /= np.sum(kernel)
mask = convolve(mask, kernel)[:, :, np.newaxis]
XYZ = xy_to_XYZ(Luv_uv_to_xy(xy))
RGB = normalise_maximum(XYZ_to_sRGB(XYZ, illuminant), axis=-1)
RGB[np.isnan(RGB)] = 1
RGB = np.ones(RGB.shape) * mask + RGB * (1 - mask)
settings.update({
'x_ticker': False,
'y_ticker': False,
'bounding_box': (0, 1, 0, 1)
})
settings.update(kwargs)
ax = matplotlib.pyplot.gca()
ax.imshow(RGB,
extent=[0, 1, 0, 1],
interpolation=None,
origin='lower')
matplotlib.pyplot.setp(ax, frame_on=False)
boundaries(**settings)
decorate(**settings)
return display(**settings)
CIE_1976_UCS_chromaticity_diagram_colours_plot() |
By the way, the current diagram plotting definitions, i.e. |
Would the anti-aliasing work better if you applied a convolution to the RGB as well as the mask? If I read it correctly it is applying a convolution blur to the mask, and then multiplying it by RGB. But RGB pixels outside the horseshoe will be white. Ideally I suppose you apply an "expand" filter to the RGB before the blurred mask. |
@nick-shaw : You mean antialiasing after applying the mask?, right now I'm doing |
Maybe. Not thought it through fully. It feels like there might be edge pixel issues similar to incorrect handling of premultiplied/unpremultiplied alpha. |
Ignore me. I realise the RGB square has colour edge to edge until you multiply by the mask, i.e. a straight alpha, so blurring only the mask is fine. |
Yes, I did that myself. Should have done so before posting! I was assuming that points outside the horseshoe would produce |
I have pushed a branch here with some updates: https://github.com/colour-science/colour/tree/feature%2Fchromaticity_diagrams I'll have to update the code tomorrow because the diagrams have lost their alpha channel. I also noticed a line at low resolution in the CIE 1976 UCS diagram, I suppose some NaNs are playing nasty here, so another check to do. Important Please notice that all the chromaticity diagram related definition and resources will be renamed in the near future for consistency with the remaining of the API:
|
The output looks good -- do you get the warnings from some timeit results old method: New method w/ 256 samples in [0,0.7] on u' and v' same laptop, but on battery / throttled right now. This doesn't include the draw time, and ~10ms are spent in make_1976_diagram destroying the figure object so it doesn't display from Including the drawing, the times are 1.64 s ± 95.2ms and 82.6 ms ± 6.27 ms, respectively. Note these are with my version (no Delaunay) and the rest of the things that happen inside a plotting call from colour are also not present. Still, the speedup is ~6x - 20x depending how you slice it. |
Yes! There are multiple warnings happening during the various transformations. We have The power related ones are because some negative numbers (out of sRGB gamut) are passed to the sRGB OETF. I have updated the cached images to have alpha channel and doing so I noticed that matplotlib does not handle it properly: matplotlib/matplotlib#3343 (comment) The updated code is now in develop. |
So for reference Matplotlib expects straight alpha as per matplotlib/matplotlib#9906, so I have regenerated the diagrams and updated the source code accordingly. I will close that issue for now, feel free to add any comments though. |
I ask that code derived from mine be properly attributed under the MIT license, or removed from colour. Intellectually, expression of computing the background image colours directly on the grid is mine; using delaunay instead of a custom function for the same purpose does not distinguish the two. |
Hi Brandon, I'm very sorry to see that you are acting like a very young kid. For reference Colour has been computing Chromaticity Diagram colours using meshgrid for over 2.5 years: https://github.com/colour-science/colour/blob/e9b0ccbbe13db280c8296557ddef572b94415e09/colour/plotting/diagrams.py or here in Colour - Analysis: https://github.com/colour-science/colour-analysis/blob/master/colour_analysis/visuals/diagrams.py#L40 If you take a careful look at our 2.5 years old code you will notice that the image = matplotlib.image.imread(
os.path.join(PLOTTING_RESOURCES_DIRECTORY,
'CIE_1931_Chromaticity_Diagram_{0}.png'.format(
cmfs.name.replace(' ', '_'))))
pylab.imshow(image, interpolation='nearest', extent=(0, 1, 0, 1)) Your great idea (remember one cannot copyright ideas) is to pass an RGB array directly to By the way, should I ask you to attribute the fixes I did to your implementation under the New BSD License?
Now something we do, and pretty much no one else does is giving people attribution if they participate in issues discussions which is why you are listed in the following locations:
I'm happy to give a reference to yourself and this thread in the module itself but since we did not copied any of your code, we don't have to do anything regarding licensing. |
I'm back again!
I was frustrated by the slow speed of the chromaticity diagram plotters, so I have begun to re-implement them. I learned that this requires re-writting most of the color space transforms into a numpy ufunc-like format. Versions of the necessary functions that have this format can be found here. Note these versions require that the input be a numpy array rather than an iterable, but the line
var = np.asarray(var)
can be added to the top of each function to remove this requriement. Some additional logic could be added so that e.g. if the input was a list, the output is a list, but this is a detail.The current plotters, as best I understand, generate a 4000x4000 point scatter plot and color the dots with sRGB tones -- matplotlib wasn't made for plotting 16 million data points quickly!
A more efficient scheme is to make a meshgrid in the desired color space (in my case, u' v', though this method is general), "block out" values outside of the horseshoe, and shade with sRGB tones. Additional things like plankian locusts can be added, but the performance issue is with the massive number of scatter points.
Progress towards this can be found here. Unfortunately, a warning is thrown in the XYZ->sRGB conversion; I presume this is because there are imaginary colors present. An image of the result, in sRGB, is below.
I would appreciate any help with debugging this and then pulling the changes into
colour
.timeit results from my laptop with an i7-7700HQ (4c/8t @ 3.6Ghz)
Since this includes expensive warning prints, I assume that this will run in closer to 25ms (a 20x improvement!) when it works properly. A 128x128 grid would also be ~4x faster, bringing the time to less than 10ms. This would allow time to budget e.g. expensive lanczos interpolation of the chromatic surface, achieving similar or superior visual quality in greatly reduced time.
What I don't know, is why I get imaginary colors. Any help in that regard would be appreciated.
The text was updated successfully, but these errors were encountered: