I like to create a ratio between 0-1 of how "colorful" a color is, by colorful i mean:
Black colors have ratio 0 (no light)
White colors have ratio 0 (no saturation)
A fully saturated / lighted color has ratio 1
i tried by converting the color to HSV colorspace and calculating the ratio as:
ratio = (color.value / 100) * (color.saturation / 100)
This works somewhat, but it feels like the curve is wrong, for example a
HSV(hue=0, saturation=80, value=80)
only gives a ratio of 0.64 but looks very close to the fully saturated / lighted color.
Maybe i need to somehow create a "ease-out" on the values? i thought about taking human perception of colors into account aswell (using LAB or YUV colorspaces) but i dont think that is needed here, but i might be wrong.
The definition of saturation from HSV is not what you want, because it does not compensate for the lightness of the color.
From the Wikipedia article on Colorfulness:
[Perceived saturation] is the proportion of pure chromatic color in the total color sensation.
where Sab is the saturation, L* the lightness and C*ab is the chroma of the color.
This definition uses the L* and C*ab components of the CIELAB colorspace. My guess is that for your application you could use Lab colorspace instead. Then your code would look like this:
function perceived_saturation(L, a, b)
return 100 * sqrt(a*a + b*b) / sqrt(L*L + a*a + b*b)
For your example color RGB = (204, 41, 41), this returns a perceived saturation of about 86%, when converting the color via the path RGB → sRGB → Lab.
As a simple approximation, you could use max(r,g,b)/255 - min(r,g,b)/255. It has the properties you seek, where anything on the gray spectrum between black and white has a colorfulness of 0, and only fully lit colors will have a colorfulness of 1. A fully saturated but dark color will be in between, e.g. (128, 0, 0) will have a colorfulness of ~0.5.
Related
We are trying to produce ePub publications that adhere to the W3C accessibility standards. One of the remaining issues is insufficient color contrast between the text color and the background color. We use Ace by DAISY (great tool!) which provides information about this sort of issue in both textual form and JSON:
And here is the JSON (it's not very straightforward to extract the two color values from the dct:description, but with a regular expression we manage):
{
"#type": "earl:assertion",
"earl:result": {
"earl:outcome": "fail",
"dct:description": "Element has insufficient color contrast of 2.86 (foreground color: #fd5f07, background color: #fff5ea, font size: 28.1pt (37.52px), font weight: normal). Expected contrast ratio of 3:1",
"earl:pointer": {
"cfi": [
"/4/2/6/2",
"/4/2/6"
],
"css": [
".inbrief-title",
".inbrief"
]
},
"html": "<div xmlns=\"http://www.w3.org/1999/xhtml\" class=\"inbrief-title\">In Brief</div> <!--##--> <div xmlns=\"http://www.w3.org/1999/xhtml\" class=\"box-container inbrief\">"
},
"earl:assertedBy": "aXe",
"earl:mode": "automatic",
"earl:test": {
"earl:impact": "serious",
"dct:title": "color-contrast",
"dct:description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds",
"help": {
"url": "http://kb.daisy.org/publishing/docs/css/color.html",
"dct:title": "Color",
"dct:description": "Elements must have sufficient color contrast"
},
"rulesetTags": [
"cat.color",
"wcag2aa",
"wcag143"
]
}
},
However, I'd like to adapt these colors programmatically to create sufficient contrast between background and text colors by changing the colors as little as possible. For example, if there is a light yellow with white text, change the yellow to a darker hue until it meets the contrast requirement. Or, conversely, make the text black.
Is there an algorithm that one could implement that can do this? I couldn't find anything that seemed useful. When describing the use case above I found myself noticing that it is probably quite a subjective decision and therefore hard to automate.
The first thing you'd have to do is decide which color to change, the foreground or background. What I'd probably do is decide which color is the furthest from white or black because its value could be changed the most.
But before that, you have to figure out which color is light and which is dark. Fortunately, that's pretty easy. Since white is #fff and black is #000, whichever color value is the smallest (ie, closer to #000) is the darker one.
Then just subtract the light color from #fff (hexadecimal subtraction or convert the colors to decimal) and compare that to the darker color.
If the subtracted value for the light color is smaller than the dark color, then the light color is closer to white than the darker color is to black so you'll want to start modifying the darker color.
If the subtracted value for the light color is larger than the dark color, then the light color is further from white than the darker color is from black so you'll want to start modifying the light color.
When changing the color, just add or subtract 1 from each RGB component. Add 1 if you're making the light color lighter or subtract 1 if you're making the dark color darker.
After you add or subtract 1 from each RGB, compute the luminance contrast to see if you're above 4.5 (if the font is small), or above 3 (if the font is large - where "large" is defined as 14pt bold or 18pt normal).
The contrast ratio formula is kind of messy but doable.
(L1 + 0.05) / (L2 + 0.05)
L1 is the relative luminance of the lighter of the colors, and
L2 is the relative luminance of the darker of the colors.
You already know which color is lighter and which is darker.
The relative luminance value (L) is the messy part, although it initially looks simple. It's just:
L = 0.2126 * R + 0.7152 * G + 0.0722 * B
Essentially, it's 21% red, 71% blue, 7% green. But it's not the straight values of R, G, and B from your color. First you have to take each R, G, B value and divide by 255. Then depending on that value, you do some magic on the values.
R = R / 255
if R <= 0.03928 then R = R/12.92
else R = ((R+0.055)/1.055) ^ 2.4
Do that for G and B too. Now that you have new values for R, G, and B, then you can apply the 21%/71%/7% relative luminance above and that gives you L1 or L2, and then you can add 0.05 to both values and divide L1 by L2.
I told you it was messy but doable. This will give you the contrast value which should be between 1 and 21. (1 is when the two colors are the same [white on white, red on red, orange on orange, etc] and 21 is when the two colors are black (#000) and white (#fff).
If you haven't reached 4.5 (or 3.0, depending on the font size), then add or subtract 1 from each R/G/B again and compute the ratio again.
For funsies, here's an example:
color1 = #5EBA7D (the green background color on stackoverflow that shows the upvote value)
color2 = #FDF7E2 (the yellow background color on stackoverflow for the sidebar)
(The upvote text color is white but that's boring for an example so I chose yellow instead).
It doesn't matter which color is foreground or background. You get the same ratio. So lets use C1 as foreground and C2 as background. If you use a tool, such as CCA, it shows a value of 2.2:1, which fails WCAG 1.4.3.
Let's plug those numbers into the formula.
C1
#5EBA7D
R = #5E (94)
G = #BA (186)
B = #7D (125)
Divide each one by 255
R = 94/255 = 0.368
G = 186/255 = 0.729
B = 125/255 = 0.490
None of those values are less than 0.03928 so we have to apply the messy part. Add 0.055 then divide by 1.055 then raise that to the power of 2.4.
R = ((0.368 + 0.055)/1.055) ^ 2.4 = 0.112
G = ((0.729 + 0.055)/1.055) ^ 2.4 = 0.491
B = ((0.490 + 0.055)/1.055) ^ 2.4 = 0.205
Now you apply the percentages (21%/71%/7%) to get the first color's luminance value.
L1 = 0.2126 * R + 0.7152 * G + 0.0722 * B
L1 = 0.2126 * (.112) + 0.7152 * (.491) + 0.0722 * (.205)
L1 = 0.024 + 0.351 + 0.015
L1 = 0.390
Then you do it for the other color to get L2 = 0.929
L2 was the yellow color so is actually the lighter color and should be L1 in the original formula. So we'll make the green, which is darker, L2.
(L1 + 0.05) / (L2 + 0.05)
= (0.929 + 0.05) / (0.390 + 0.05)
= 2.226
If you use a color contrast tool, you should get the same value, although most tools typically round to one decimal place so you'll get 2.2.
Now that you know the ratio, which color is further away from black or white so you know which color to adjust?
color1 = #5EBA7D (green)
color2 = #FDF7E2 (yellow)
The smaller number is darker (closer to #000 black) so that would be #5EBA7D (green).
How far is the lighter color (#FDF7E2, yellow) from white (#fff)?
#FFFFFF - #FDF7E2 = #02081D
Now, which is bigger? The dark color, #5EBA7D (green), or the distance from yellow to white, #02081D? In this case, the green is bigger so it's further away from black than the yellow is from white.
Doing a gut check, that makes sense. The green isn't very dark, kind of a light-ish green so it's not very close to black. But yellow is very light and is close to white.
Since green is further from black than the yellow is from white, you can start adjusting the green and making it darker. Do this by subtracting 1 from each R/G/B value then recomputing the luminance. It probably won't change much so you'll have to keep adjusting the green and making it darker until you reach 4.5 (or 3.0, depending on font size).
If you program out this case, the green should eventually get to #258144 (which ends up subtracting 57 from each RGB value), which has a color ratio of 4.6 to the yellow.
If any of the RGB values reach 0 before you get to a decent contrast ratio, then you have to start making the light color lighter by adding 1 to each RGB value and recomputing the contrast. By adding or subtracting 1 from each RGB, you keep the color hue. The green is still green but is darker, or the yellow is still yellow but is lighter.
Are you wishing you hadn't asked the question now?
Minor Update:
Adjusting the RGB values by 1 each time and recomputing the luminance might not be very efficient. In my example, that would have to be done 57 times. You might want to add or subtract 10 from each RGB component instead of 1 and see how close to 4.5 you get. If not there yet, adjust by 10 again. Once you get greater than 4.5, you can tweak it back a bit and adjust by 2 in the opposite direction until you get as close to 4.5 without going under.
I'd like to convert this graded colored map into a matrix in which to each pixel a percentage of orange is assigned. In particular, taking the color palette of orange at the right of image.
I'd like to assign 100% to the brightest orange at the top and 0% to the black at the bottom. Hence, I'd like to assign to each pixel a percentage, based on the color of that single pixel in relation to the palette.
Finally, my goal consists of averaging these percentages in order to get a sort of mean percentage of orange. For the latter step, I shouldn't have any issue... but I've no idea how to face the former problem.
I am looking for a solution either in R, Matlab or Mathematica
Here's an idea that I had. Suppose you have an image with arbitrary colors; let's solve for a transformation that reduces your color from 3-dimensional space to a single dimension by running an optimizer. The cost function is derived from the error when the original colors are attempted to be recovered by the lossy color transformation.
See the image below for my result, scaled to 100% at the brightest pixel and 0% at the darkest.
img = imread('zpiaD.png');
opt = optimset('Display', 'Iter');
itf = fminsearch(#(itf) min_colormap(img, itf) , zeros(1,3), opt);
[~, rimg] = min_colormap(img, itf);
subplot(2,1,1), imshow(rimg), title 'single color dimension'
subplot(2,1,2)
rimg = double(rimg);
imagesc(100 * (rimg - min(min(rimg))) / (max(max(rimg)) - min(min(rimg)))),
colorbar, title 'scaled'
function [res, timg] = min_colormap(img, itf)
pimg = double(reshape(img, size(img,1)*size(img,2), size(img,3)));
timg = transpose(itf * pimg');
res = sum(((timg * itf) - pimg).^2, 'all');
timg = cast(reshape(timg, size(img,1), size(img,2)), 'uint8');
end
I am trying to create a Sass function that receives a foreground color and background color and calculates the contrast ratio. From there (and the part I'm stuck on) is that it would simply return the foreground color if it meets the target contrast ratio, but if it doesn't it would lighten or darken the foreground color to meet the target contrast ratio.
For example, if the background supplied was #000 and the foreground supplied was #444 (a contrast ratio of 2.15), this function would lighten the foreground to #757575 and return that color.
I've got everything working except for the part where I need to reverse the contrast calculation. My initial thought was to approach it with what percentage it was away from target and simply lighten/darken (depending on which color was originally darker) by 100 minus the percent difference. This approach, in hindsight, was a little naive and I'm afraid some more advanced math will be involved.
Here is what I created so far (and here is a simplified fiddle):
#function wcag-color($bg, $fg, $size: 16px, $level: "aa"){
#if ( $level == "aa" ){
$wcag_contrast_ratio: 4.5; //For text smaller than 18px
#if ( $size >= 19 ){
$wcag_contrast_ratio: 3; //For text larger than 19px
}
}
#if ( $level == "aaa" ){
$wcag_contrast_ratio: 7; //For text smaller than 18px
#if ( $size >= 19 ){
$wcag_contrast_ratio: 4.5; //For text larger than 19px
}
}
$actual_contrast_ratio: contrast($bg, $fg); //This function returns the contrast between the two colors.
#if ( $actual_contrast_ratio > $wcag_contrast_ratio ){
#return $fg; //Foreground color is acceptable
}
//Scale the lightness of the foreground to meet requested WCAG contrast ratio
$difference: 100 - $actual_contrast_ratio / $wcag_contrast_ratio * 100; //There is more to it than this...
//Edit: here are a few new lines to ponder. This assumes BG is darker than FG (would need to add a condition to compare luminance of each).
$acceptable_luminance: luminance($bg)*$wcag_contrast_ratio; //What the luminance of the FG must be to comply
$difference: ($acceptable_luminance - luminance($fg)); //How far away the FG luminance actually is (not sure if this helps anything...)
#return scale-color($fg, $lightness: $difference); //Unfortunately luminance is not the same as lightness.
}
Notice the commented line "There is more to it than this..." – that is where I need to reverse my contrast formula, but I'd love if there was a simpler formula to use since I already know what the target contrast ratio is.
I've been thinking about this for a few days an I'm stumped. I'd prefer to avoid a guess-and-check method by looping through 1% lightened/darkened colors and testing each individually for their contrast ratio– that would work, but I'm sure there is a more optimal solution.
This was my reference for my initial functions (contrast and luminance) and was very helpful: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796
Note: I am not using Compass or any other Sass libraries.
Edit: Here is a simplified fiddle for reference: https://www.sassmeister.com/gist/445836123feb42885a0cf7f4709261ff
So given #000000 and #444444, you can calculate the contrast ratio (2.15 in this case). The math is pretty straightforward, albeit a little hairy. (See the "relative luminance" definition.)
Now you want to go backwards? If you have #000000 and want a ratio of 4.5, starting with #444444, what should the color be? Is that what
I need to reverse my contrast formula
means?
It's a little complicated because you're solving for 3 variables, the red, green, and blue components, plus the luminance formula doesn't treat the red, green and blue equally. It's using 21.25% red, 71.5% green, and 7.25% blue.
Plus, the luminance formula isn't a simple linear formula so you can't just take a percentage short of luminance and bump the color value by that same percentage.
For example, in your case, the ratio was 2.15 but you need it to be 4.5. 2.15 is 108% short of 4.5, the desired value.
However, if you look at your original RGB values #444444 and you calculated it needed to be #757575 (in order to have a 4.5 ratio), then if you treat those RGB values as simple numbers (and convert to decimal), then #444444 (4473924) is 72% short of #757575 (7697781).
So you have a disconnect that your ratio is short by 108% but your RGB values are short by 72%. Thus you can't do a simple linear equation.
(The numbers aren't quite exact since #757575 gives you a 4.56 ratio, not an exact 4.5 ratio. If you use #747474, you get a 4.49 ratio, which is just a smidge too small for WCAG compliance but is closer to 4.5 than 4.56. However, #444444 is 71% short of #747474, so it's still not the same as 2.15 being 108% short of 4.5, so the basic concept still applies.)
Just for fun, I looked at the values of 0x11111 through 0x666666, incrementing by 0x111111, and calculated the contrast ratio. There weren't enough points on the graph so I added a color halfway between 0x111111 and 0x222222, then halfway between 0x222222 and 0x333333, etc.
RGB contrast % from 4.5 % from 0x747474
111111 1.11 305.41% 582.35%
191919 1.19 278.15% 364.00%
222222 1.32 240.91% 241.18%
2a2a2a 1.46 208.22% 176.19%
333333 1.66 171.08% 127.45%
3b3b3b 1.87 140.64% 96.61%
444444 2.16 108.33% 70.59%
4c4c4c 2.45 83.67% 52.63%
555555 2.82 59.57% 36.47%
5d5d5d 3.19 41.07% 24.73%
666666 3.66 22.95% 13.73%
6e6e6e 4.12 9.22% 5.45%
As you can see, the lines interset at the 3rd data point then converge toward each other. I'm sure there's a formula in there so you could take the contrast percentage, do some (probably logarithmic) function on it and get the percentage needed to change the color.
This would be a fascinating math problem that I currently don't have time to play with.
Update Jan 18, 2019
I got it to work going backwards, but it doesn't handle edge cases such as when making a dark color darker but you've already reached the max (or a light color lighter but you reached the max). But maybe you can play with it.
Test case
#ee0add for the light color (magenta-ish)
#445566 for the dark color (dark gray)
contrast ratio 2.09
When computing the "relative luminance" of a color, it has a conditional statement.
if X <= 0.03928 then
X = X/12.92
else
X = ((X+0.055)/1.055) ^ 2.4
Before X is used in that condition, it's divided by 255 to normalize the value between 0 and 1. So if you take the conditional value, 0.03928, and multiply by 255, you get 10.0164. Since RGB values must be integers, that means an RGB component of 10 (0x0A) or less will go through the "if" and anything 11 (0x0B) or bigger will go through the "else". So in my test case values, I wanted one of the color parts to be 10 (0x0A) (#EE0ADD).
The relative luminance for #ee0add is 0.23614683378171950172526363525113 (0.236)
The relative luminance for #445566 is 0.0868525191131797135799815832377 (0.0868)
The "contrast ratio" is
(0.236 + .05) / (0.0868 + .05) = 2.09
(You can verify this ratio on https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=445566)
If we want a ratio of 4.5, and we want #ee0add to not change, then we have to adjust #445566. That means you need to solve for:
4.5 = (0.236 + .05) / (XX + .05)
So the second luminance value (XX) needs to be 0.01358818528482655593894747450025 (0.0136)
The original second luminance value was 0.0868525191131797135799815832377 (0.0868), so to get 0.01358818528482655593894747450025 (0.0136), we need to multiply the original by 0.15645125119651910313960717062698 (0.0136 / 0.0868 = 0.156) (or 15.6% of the original value)
If we apply that 15.6% to each of the R, G, and B relative luminance values, and then work through the conditional statement above backwards, you can get the RGB values.
Original luminance for #445566
r = 0x44 = 68
g = 0x55 = 85
b = 0x66 = 102
r1 = 68 / 255 = 0.26666666666666666666666666666667
g1 = 85 / 255 = 0.33333333333333333333333333333333
b1 = 102 / 255 = 0.4
r2 = ((.267 + .055) / 1.055)^^2.4 = 0.05780543019106721120703816752337
g2 = ((.333 + .055) / 1.055)^^2.4 = 0.09084171118340767766490119106965
b2 = ((.400 + .055) / 1.055)^^2.4 = 0.13286832155381791428570549818868
l = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2
= 0.0868525191131797135799815832377
Working backwards, take 15.6% of the r2, g2, and b2 values
r2 = 0.05780543019106721120703816752337 * 15.6% = 0.00904373187934550551617004082875
g2 = 0.09084171118340767766490119106965 * 15.6% = 0.01421229937547695322310904970549
b2 = 0.13286832155381791428570549818868 * 15.6% = 0.02078741515147623990363062978804
Now undo that mess with ^^2.4 and the other stuff
to undo X^^2.4 you have to do the inverse, X^^(1/2.4) or X^^(0.4167)
then multiply by 1.055
then subtract 0.055
then multiply by 255
pow( 0.00904373187934550551617004082875, 1/2.4) = 0.14075965680504652191078668676178
pow( 0.01421229937547695322310904970549, 1/2.4) = 0.16993264267137740728089791717873
pow( 0.02078741515147623990363062978804, 1/2.4) = 0.19910562853770829265100914759565
multiply by 1.055
0.14075965680504652191078668676178 * 1.055 = 0.14850143792932408061587995453368
0.16993264267137740728089791717873 * 1.055 = 0.17927893801830316468134730262356
0.19910562853770829265100914759565 * 1.055 = 0.21005643810728224874681465071341
subtract 0.055
0.14850143792932408061587995453368 - 0.055 = 0.09350143792932408061587995453368
0.17927893801830316468134730262356 - 0.055 = 0.12427893801830316468134730262356
0.21005643810728224874681465071341 - 0.055 = 0.15505643810728224874681465071341
multiply by 255
0.09350143792932408061587995453368 * 255 = 23.842866671977640557049388406088 = 24 = 0x18
0.12427893801830316468134730262356 * 255 = 31.691129194667306993743562169008 = 32 = 0x20
0.15505643810728224874681465071341 * 255 = 39.53939171735697343043773593192 = 40 = 0x28
So the darker color is #182028. There are probably some rounding errors but if you check the original foreground color, #ee0add with the new color, #182028, you get a contrast ratio of 4.48. Just shy of 4.5, but like I said, probably some rounding errors.
https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=182028
I tried doing the same thing with #ee0add, keeping #445566 the same, but when going backwards and getting to the last step where you multiply by 255, I got numbers greater than 255, which are not valid RGB components (they can only go up to 0xFF). If I stopped the number at 255 then took the difference and added it to the smallest color value, I got a decent color but the ratio was 5.04, overshooting 4.5. I can post that math too if you want.
I am attempting to create a block of color in R based on density values. So for example given 10 values with a density distribution:
values=c(0,1,2,3,4,5,6,7,8,9)
densities=c(0.7, 0.1, 0.05,0,0,0,0,0.05,0.05,0.05)
I want to create what is essentially a colored bar where the greatest density is e.g. black and the least white, with the intermittent values somewhere in between but proportionally so i.e. 0.05 if half as dark as 0.1 AND similar values are the same color.
As I think of it I can just create a bar chart, with all bars the same height, no borders etc and the densities used to create the colors. However, no matter how hard I try I can't figure out how to get the color scheme correct.
I have created a gradient but this isn't linked to density. Also I have linked color to density with densCols but I haven't managed to make the colors sequential.
Can someone point me in the right direction? I have seen similar questions but none that have gotten me to the point I need to be at. I would prefer to do this with the base graphics package if possible.
Thanks in advance.
if you can normalize value in [0,1] range you can use functions like grey (grey scale).
## not with density but with probability example
val<- rnorm(100) < 0.5
mean(val)
grey(0) # black
grey(1) # white
## color intensity proportional to probability
grey(1 - prop.table(table(val)))
2 colors are mixed together. If i have the RGB for the resultant color and RGB for one of the colors mixed, then somehow i could calculate the 2nd color?
I will try to explain visually what i am trying to say. Here is a flickr link
http://www.flickr.com/photos/48150615#N08/4407414157
I know that the circle in the middle has an opacity of 20%
Is there any way to know the color value of the circle so that i can deduct that to get the same color value as the background color.
if i understood the task correctly...
let's do some school math
c is color of initial image, t is color of transparent color, a (for alpha) is transparency, c' is result color.
c' = (1 - a) * t + a * c
you want to find c.
c = (c' - (1 - a) * t) / a
you need to know a, t and c'
Firstly it depends how you're going to mix them. That is, you could average the RGB components (this means blue (0,0,255) + yellow (255,255,0) == grey (128,128,128)), or you could work on Hues, Saturation and Value, which often gives a much more "as expected" result.
Anyway, in either case, it's some simple maths:
if the way to get the average is C3 = (C1 + C2) / 2
then the way to find C2 is C2 = (C3 * 2) - C1
CIELAB color space is specially designed for calculating differences between colors, but it might be a overkill in you case. Probably HSV is easy solution for you.
Just subtract each component. The components are often stored in hexadecimal format, in which case you will need to convert the numbers to decimal or do hex math.