mapbox/carto

Implement color luma change function

Closed this issue · 6 comments

When dealing with data-driven colors, one often faces plain colors like yellow (255,255,0), red (255,0,0) etc. Cartographically speaking, these color are too saturated and far less than ideal.
My use case is the coloring of crosscountry skiing route relations for OSM, but I guess it’s the same for any route with a color tag.

One could desaturate() and lighten() or darken() the color, however, for some color it’s not satisfactory. For instance, the yellow visibility over a light background is not sufficient. At Opensnowmap, I implemented a while ago a pre-processing that alter the colors luma (see https://en.wikipedia.org/wiki/HSL_and_HSV).

Luma is the Y’ in Y’UV color space, and is defined by Y′601 ≈ 0.30R + 0.59G + 0.11B.
See an example below of the color change made in the Opensowmap pre-processing generating the ski routes style :
deluma
My question is, is this worth it to try to submit a PR implementing a deluma() function in function.js to decrease the luma of a color, or this is out of scope here? What would be a good name for the inverse function to increase luma ?

Below is my actual implementation in python :

def deluma(color,factor):
    # reduce color luma by a given factor. 
    r,g,b,a=(color.r, color.g, color.b, color.a)
    r=int(r*(1-0.3*factor))
    g=int(g*(1-0.59*factor))
    b=int(b*(1-0.11*factor))
    return mapnik.Color(r,g,b,a)

Before talking about the actual function I'm trying to understand why you need it in the first place. You are unsatisfied with the non-uniformity of the lightness of HSL, is that correct? Do you know that carto supports HSLuv (http://www.hsluv.org/) which tries to modify CIELUV to allow the same colour definitions as in HSL. Internally carto uses either HSL or HSLuv, see https://cartocss.readthedocs.io/en/latest/language_elements.html#color. Cf also #354, #453. Some argue that HSLuv is not really a perceptual colour space, but it solves the problem of HSL without the possibility of yielding non-representable colours like in CIELUV.

Would using HSLuv solve your problem without needing this function?

This would avoid the problem of conversions as internally colours are represented as HSL/HSLuv, so you would have to convert to RGB first, then change the luma value, convert back to HSL. Needless to say that there would be a lot of rounding errors.

I played a little bit with HSLuv: HSLuv solves the issue of the lightness uniformity when you choose your colors.
However here, I don't choose them, the color are from the OSM contributors, but I'd like to change them to give them a more uniform lightness. The deluma function kind of reduce the lightness range: white become (pinkish) grey, black stays black.

I also figured out that my lightening function is more a whitening function:

 def lighten(color,factor):
     r,g,b,a=(color.r, color.g, color.b, color.a)
     r = int((1-factor)*r + factor*255)
     g = int((1-factor)*g + factor*255)
     b = int((1-factor)*b + factor*255)
     return mapnik.Color(r,g,b,a)

Given a non-chosen set of colors, these two color functions allows to:

  • Solve an issue with the visibility over a white background (white, yellow) (deluma)
  • Reduce saturation (whitening)

I just fiddled with these color change and in the end they give a valid result to my eye, so I would certainly have a hard time to defend the theory behind them :-)
So if if somebody else is seduced, I can add them to cartocss. Otherwise I keep them to me. Another option maybe an example using HSLuv conversion to give as similar result ?

Here is another example on colors sorted by r+g+b :
capture du 2018-05-08 21-40-23

I think data driven colours would be a Mapnik issue, not a Carto one, and suggest closing this issue as out of scope.

I would also not recommend Luma. Convert to a standard perceptual colour space, set the colour properties you want, project to the gamut, and use that colour.

Hmm, in any case, this works as expected for data driven colors:
[color!='']{line-color:[color];}

But this doesn't:
[color!='']{line-color:lighten([color],50%);}
Error: ../pistes-carto/routes_dyn.mss:23:51 incorrect arguments given to lighten()

And this neither:
[lightness(color) > 100]{line-color: #3B3B3B;}
Error: ../pistes-carto/routes_dyn.mss:27:7 Missing closing ] of filter.

So it seems there is something else to improve before discussing this anyway.

Hmm, in any case, this works as expected for data driven colors:
[color!='']{line-color:[color];}

This translates to Mapnik XML that roughly

<LineSymbolizer stroke="[color]" />

The other things you want are Mapnik issues.