Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
8f48367
to3342ff1
Comparelib/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 thanl += 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.
eric-wieserDec 12, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 expectbrighten_color(brighten_color(x, 0,2), 0.2) == brighten_color(x, 0.4)
(as opposed to0.44
, as you implement it).
Also thefloat
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
to96642c5
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))deftest_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
forcolor,brighten,expectedinzip(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.
for color, brighten, expected in zip(test_colors, | ||
test_brighten, | ||
known_brighten_result): | ||
_brighten_test_helper(color, brighten, expected) |
eric-wieserDec 13, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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_colorassert_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 beadjust_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 modulecolors
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.
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-vectorizedcolorsys
functions when we already have a vectorizedrgb_to_hsv
higher up in the file.
I know that HSV and HSL are different, but either we should write a vectorizedrgb_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? Justassert 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.
eric-wieserDec 13, 2017 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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.
Seethis 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 ascolor.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... |
Uh oh!
There was an error while loading.Please reload this page.
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