-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
ENH color brightening #9985
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
ENH color brightening #9985
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Color brightening | ||
---------------------- | ||
|
||
Often we would like the ability add subtle color variation to our plots, | ||
especially when similar element are plotted in close proximity to one another. | ||
`matplotlib.colors.brighten_color` can be used to take an existing color and | ||
brighten or darken it, by giving a positive or negative fraction. | ||
|
||
.. plot:: mpl_examples/color/color_brighten_demo.py |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
""" | ||
=================== | ||
Brighten color demo | ||
=================== | ||
|
||
Demo of `~.brighten_color` utility. | ||
|
||
""" | ||
|
||
import matplotlib.pyplot as plt | ||
import matplotlib.colors as mcolors | ||
import numpy as np | ||
|
||
fig, ax = plt.subplots() | ||
|
||
lightfacs = [-1., -0.5, 0., 0.5, 0.75, 1.0] | ||
N = len(lightfacs) | ||
y = np.linspace(0., 1., N+1) * N - 0.5 | ||
for n, lightfac in enumerate(lightfacs): | ||
brightened_color = mcolors.brighten_color('blue', lightfac) | ||
ax.fill_between([0, 1], [y[n], y[n]], [y[n+1], y[n+1]], | ||
facecolor=brightened_color, edgecolor='k') | ||
|
||
ax.set_yticklabels([''] + lightfacs) | ||
|
||
ax.set_xlim([0, 1]) | ||
ax.set_ylim(np.min(y), np.max(y)) | ||
ax.set_ylabel('Brightening Fraction') | ||
ax.set_xticks([]) | ||
ax.set_title('Brightening of Color Blue') | ||
plt.show() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,12 +51,14 @@ | |
from six.moves import zip | ||
|
||
from collections import Sized | ||
import colorsys | ||
import itertools | ||
import re | ||
import warnings | ||
|
||
import numpy as np | ||
import matplotlib.cbook as cbook | ||
|
||
from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS | ||
|
||
|
||
|
@@ -1992,3 +1994,49 @@ def from_levels_and_colors(levels, colors, extend='neither'): | |
|
||
norm = BoundaryNorm(levels, ncolors=n_data_colors) | ||
return cmap, norm | ||
|
||
|
||
def brighten_color(color, frac): | ||
"""A color helper utility to either darken or brighten the given color. | ||
|
||
This function first converts the given color to RGB using | ||
`~.colorConverter` and then to | ||
HSL using `colorsys.rgb_to_hsl`. The lightness is modified according | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems foolish to use the non-vectorized I know that HSV and HSL are different, but either we should write a vectorized There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is outside what I'm willing to do for this PR. If we could just use HSV, that'd be great, but my understanding is that to get white in HSV you need to change both S and V, whereas in HSL you can just change L. HSL seems the better colourspace for what we are doing here. Just for context, this is an old PR that was sitting around for 2+ years, so I'm willing to put it through, but I'm not interested enough in getting the functionality that I'm willing to write a whole rgb_to_hsl conversion routine. If someone wants to put in a PR that does that, either before this one is accepted, or after, then that'd be swell. But I think the PR has merit even if it is not vectorized. |
||
to the given fraction, clipped to be between 0 and 1, and then converted | ||
back to RGB using `colorsys.hsl_to_rgb`: | ||
L = np.clip(L0 * (1.0 + frac), 0., 1.), where L0 is the original | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not convinced that this operation really makes a lot of sense, e.g. is going from L=0.1 to L=0.2 (frac=1) really "equivalent" from going to L=0.5 to L=1? If anything I think this should something like a probit or logit scale. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See this comment - I agree with you There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t feel strongly about this. Do you want black to stay black or go grey? I could go either way. I still prefer fractional change because I think that’s what most people would mean when they say to lighten an image. I’d you want the histogram to move right but black would still stay black. We could also just forget any algorithm and just ask the user for the L value they want perhaps w an accompanying way to get L so they know where they are starting from. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would indeed prefer a design where we expose a color class with methods such as |
||
lighness of the color in HSL space. | ||
|
||
Parameters | ||
---------- | ||
color : string, list, hexvalue | ||
Any acceptable Matplotlib color value, such as 'red', | ||
'slategrey', '#FFEE11', (1,0,0) | ||
|
||
frac : float | ||
The amount by which to darken or brighten the color. Negative | ||
darkens, positive brightens. Lowest valid negative value is -1. | ||
Highest positive value is 1/L of the original color, where L is | ||
the lightness in HSL space. Entries outside this range are allowed, | ||
but the resulting lightness is clipped between 0. and 1. | ||
|
||
Returns | ||
------- | ||
color : tuple of floats | ||
tuple representing converted rgb values | ||
|
||
""" | ||
|
||
frac = float(frac) | ||
if not np.isfinite(frac): | ||
raise ValueError('argument frac must be a finite float') | ||
|
||
rgb = colorConverter.to_rgb(color) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't think these all need to be on separate lines |
||
|
||
h, l, s = colorsys.rgb_to_hls(*rgb) | ||
|
||
l *= 1. + frac | ||
|
||
l = np.clip(l, 0., 1.) | ||
|
||
return colorsys.hls_to_rgb(h, l, s) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,8 @@ | |
import numpy as np | ||
import pytest | ||
|
||
from numpy.testing.utils import assert_array_equal, assert_array_almost_equal | ||
from numpy.testing.utils import (assert_array_equal, | ||
assert_array_almost_equal, assert_equal) | ||
|
||
from matplotlib import cycler | ||
import matplotlib | ||
|
@@ -715,3 +716,34 @@ def __add__(self, other): | |
else: | ||
assert len(recwarn) == 0 | ||
recwarn.clear() | ||
|
||
|
||
def _brighten_test_helper(color, shade, expected): | ||
sc = mcolors.brighten_color(color, shade) | ||
assert_equal(sc, expected) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these both tuples? Just |
||
|
||
|
||
def test_color_brightening(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about parameterization? and do these floats need to be this long? TestBrighten(object):
test_colors = ( 'red', 'red', 'red', 'red', 'red', [0, .5, .9], 'slategrey')
test_brighten = (0, 0.50, 1.00, -0.50, -1.00, 0.20, -0.20)
known_brighten_result = ((1.0, 0.0, 0.0), (1.0, 0.5, 0.5),
(1.0, 1.0, 1.0), (0.5, 0.0, 0.0), (0.0, 0.0, 0.0),
(0.080000000000000071, 0.59111111111111092, 1.0),
(0.35097730430754981, 0.40156862745098038, 0.45215995059441105))
@pytest.mark.parametrize('color, shade, expected',
zip(test_colors, test_brighten, known_brighten_result))
def test_color_brightening(color, shade, expected):
sc = mcolors.brighten_color(color, shade)
assert_equal(sc, expected)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't write the test, so this is a disinterested comment, but: I'm still not sure what the advantage of parameterization is? I don't find the above very easy to parse... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't have to explicitly loop through the cases and in this case there's no needed for the secondary helper function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm, well, there was no need for the helper fcn anyway. And
is not any shorter than writing for color, brighten, expected in zip(test_colors,
test_brighten,
known_brighten_result): The latter has the benefit of being standard python, whereas the former requires knowing pytest decorators. I'm not adverse to change, but only if there is a compelling reason! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The two big wins from parameterization come from when you can stack more than 1 and get the outer-product of the input and because it lets each test fail independently. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I always forget about that, but the test failing independenty is the real reason to use parameterization becuase that's what makes it a true unit test. Plus in pytest you can add a list of lables for each paramterized set and so you can identify what the inputs are testing for. |
||
test_colors = ( | ||
'red', | ||
'red', | ||
'red', | ||
'red', | ||
'red', | ||
[0, .5, .9], | ||
'slategrey', | ||
) | ||
test_brighten = (0, 0.50, 1.00, -0.50, -1.00, 0.20, -0.20) | ||
known_brighten_result = ( | ||
(1.0, 0.0, 0.0), | ||
(1.0, 0.5, 0.5), | ||
(1.0, 1.0, 1.0), | ||
(0.5, 0.0, 0.0), | ||
(0.0, 0.0, 0.0), | ||
(0.080000000000000071, 0.59111111111111092, 1.0), | ||
(0.35097730430754981, 0.40156862745098038, 0.45215995059441105) | ||
) | ||
for color, brighten, expected in zip(test_colors, | ||
test_brighten, | ||
known_brighten_result): | ||
_brighten_test_helper(color, brighten, expected) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we're missing the obvious case that this should just be written: brighten = mcolors.brighten_color
assert_equal(brighten('red', 0.0), (1.0, 0.0, 0.0))
assert_equal(brighten('red', 0.5), (1.0, 0.5, 0.5))
assert_equal(brighten('red', 1.0), (1.0, 1.0, 1.0))
... These aren't test parameters - they're the tests themselves, and the loop and helper functions just obscure the pairing between the results and the expected values |
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.
to 🚲 🏠 the name a bit, should this be
adjust_color
oradjust_tint
or something like that? I almost left a comment after quickly skimming the code as "do we need to add a darken color too?" (and then I read the docstring and left this comment instead).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.
Does a function in the module
colors
really need to havecolor
as part of its name?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.
adjust_lightness? Opens up to adjust saturation.