-
-
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
Conversation
8f48367
to
3342ff1
Compare
lib/matplotlib/colors.py
Outdated
|
||
h, l, s = colorsys.rgb_to_hls(*rgb) | ||
|
||
l *= 1. + float(frac) |
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.
Not clear to me that this is a more useful definition than l += frac
.
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.
I'd argue that most people have no idea what the L value of a color is, but they know they want it 50% lighter.
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.
FWIW and based solely on my limited own experience, I would be inclined to agree with @jklymak about the relative vs. absolute increment scheme.
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.
The question here is not whether users understand L, but whether they expect brighten_color(brighten_color(x, 0,2), 0.2) == brighten_color(x, 0.4)
(as opposed to 0.44
, as you implement it).
Also the float
is pointless here, as you convert it above
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.
Given that the formula is explicitly in the docs, I hope they realize they are incrementing by 20% each time.
0893966
to
96642c5
Compare
@@ -0,0 +1,9 @@ | |||
Easy color brightening |
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.
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 | ||
slightly brighten or darken it, by giving a positive or negative fraction. |
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.
I think you don't need slightly since the level of brightness or darkness is relative to the fraction. Please explain what the fraction is?
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.
Its in the docstring which is linked, and should be obvious from the example...
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 comment
The reason will be displayed to describe this comment to others. Learn more.
don't think these all need to be on separate lines
assert_equal(sc, expected) | ||
|
||
|
||
def test_color_brightening(): |
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.
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 comment
The 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm, well, there was no need for the helper fcn anyway. And
@pytest.mark.parametrize('color, shade, expected',
zip(test_colors, test_brighten, known_brighten_result))
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 comment
The 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 comment
The 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.
96642c5
to
15ef029
Compare
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 comment
The 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
@@ -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): |
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
or adjust_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 have color
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.
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Seems foolish to use the non-vectorized colorsys
functions when we already have a vectorized rgb_to_hsv
higher up in the file.
I know that HSV and HSL are different, but either we should write a vectorized rgb_to_hsl
for consistency, or just use the HSV one that's already here
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.
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.
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Are these both tuples? Just assert sc == expected
.
HSL using `colorsys.rgb_to_hsl`. The lightness is modified according | ||
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 comment
The 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 comment
The 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 comment
The 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 comment
The 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 color.hsl_replace(l=...)
, color.hsv_replace(s=...)
(and allowing multiple kwargs of course, just leaving the ones not specified untouched).
OK, based on the feedback above I'd say that folks see merit in being able to manipulate colors, but want something more fulsome than this PR. i.e. a color management class. That seems a bit of a big project, particularly as it would make sense to let such a color class be a valid input to most places a color is now accepted. Though maybe mostly a documentation project because I think I'm closing and opening an issue linking back to this... |
PR Summary
Redo of #8895. Simple utility to brighten a color. Changed name from shade to brighten because positive was brightening. "Lightening" is also possible, since we are changing the L channel in HSL, but is clunky linguistically.
PR Checklist