Skip to content

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

Closed
wants to merge 1 commit into from
Closed

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Dec 12, 2017

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

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)

@jklymak jklymak added this to the v2.2 milestone Dec 12, 2017
@jklymak jklymak force-pushed the enh-color-shading branch 2 times, most recently from 8f48367 to 3342ff1 Compare December 12, 2017 18:48
@jklymak jklymak mentioned this pull request Dec 12, 2017
7 tasks

h, l, s = colorsys.rgb_to_hls(*rgb)

l *= 1. + float(frac)
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Contributor

@eric-wieser eric-wieser Dec 12, 2017

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

Copy link
Member Author

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.

@jklymak jklymak force-pushed the enh-color-shading branch 2 times, most recently from 0893966 to 96642c5 Compare December 12, 2017 21:53
@@ -0,0 +1,9 @@
Easy color brightening
Copy link
Member

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.
Copy link
Member

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?

Copy link
Member Author

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)
Copy link
Member

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():
Copy link
Member

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)

Copy link
Member Author

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...

Copy link
Member

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.

Copy link
Member Author

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!

Copy link
Member

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.

Copy link
Member

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)
Copy link
Contributor

@eric-wieser eric-wieser Dec 13, 2017

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):
Copy link
Member

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).

Copy link
Contributor

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?

Copy link
Member Author

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
Copy link
Contributor

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

Copy link
Member Author

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)
Copy link
Member

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
Copy link
Contributor

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.

Copy link
Contributor

@eric-wieser eric-wieser Dec 13, 2017

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

Copy link
Member Author

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.

Copy link
Contributor

@anntzer anntzer Dec 13, 2017

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).

@jklymak
Copy link
Member Author

jklymak commented Dec 13, 2017

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 ~.colorConverter typically handles the conversion.

I'm closing and opening an issue linking back to this...

@jklymak jklymak closed this Dec 13, 2017
@tacaswell tacaswell modified the milestones: v2.2, unassigned Dec 13, 2017
@jklymak jklymak deleted the enh-color-shading branch March 5, 2019 16:09
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.

8 participants