In Graphics, when do I need to account for Gamma? - c++

So I've got some code that's intended to generate a Linear Gradient between two input colors:
struct color {
float r, g, b, a;
}
color produce_gradient(const color & c1, const color & c2, float ratio) {
color output_color;
output_color.r = c1.r + (c2.r - c1.r) * ratio;
output_color.g = c1.g + (c2.g - c1.g) * ratio;
output_color.b = c1.b + (c2.b - c1.b) * ratio;
output_color.a = c1.a + (c2.a - c1.a) * ratio;
return output_color;
}
I've also written (semantically identical) code into my shaders as well.
The problem is that using this kind of code produces "dark bands" in the middle where the colors meet, due to the quirks of how brightness translates between a computer screen and the raw data used to represent those pixels.
So the questions I have are:
Do I need to correct for gamma in the host function, the device function, both, or neither?
What's the best way to correct the function to properly handle gamma? Does the code I'm providing below convert the colors in a way that is appropriate?
Code:
color produce_gradient(const color & c1, const color & c2, float ratio) {
color output_color;
output_color.r = pow(pow(c1.r,2.2) + (pow(c2.r,2.2) - pow(c1.r,2.2)) * ratio, 1/2.2);
output_color.g = pow(pow(c1.g,2.2) + (pow(c2.g,2.2) - pow(c1.g,2.2)) * ratio, 1/2.2);
output_color.b = pow(pow(c1.b,2.2) + (pow(c2.b,2.2) - pow(c1.b,2.2)) * ratio, 1/2.2);
output_color.a = pow(pow(c1.a,2.2) + (pow(c2.a,2.2) - pow(c1.a,2.2)) * ratio, 1/2.2);
return output_color;
}
EDIT: For reference, here's a post that is related to this issue, for the purposes of explaining what the "bug" looks like in practice: https://graphicdesign.stackexchange.com/questions/64890/in-gimp-how-do-i-get-the-smudge-blur-tools-to-work-properly

I think there is a flaw in your code.
first i would make sure that 0 <= ratio <=1
second i would use the formula c1.x * (1-ratio) + c2.x *ratio
the way you have set up your calculations at the moment allow for negative results, which would explain the dark spots.

There is no pat answer for when you have to worry about gamma.
You generally want to work in linear color space when mixing, blending, computing lighting, etc.
If your inputs are not in linear space (e.g., that are gamma corrected or are in some color space like sRGB), then you generally want to convert them at once to linear. You haven't told us whether your inputs are in linear RGB.
When you're done, you want to ensure your linear values are corrected for the color space of the output device, whether that's a simple gamma or other color space transform. Again, there's no pat answer here, because you have to know if that conversion is being done for you implicitly at a lower level in the stack or if it's your responsibility.
That said, a lot of code gets away with cheating. They'll take their inputs in sRGB and apply alpha blending or fades as though they're in linear RGB and then output the results as is (probably with clamping). Sometimes that's a reasonable trade off.

your problem lies entirely in the field of perceptual color implementation.
to take care of perceptual lightness aberrations you can use one of the many algorithms found online
one such algorithm is Luma
float luma(color c){
return 0.30 * c.r + 0.59 * c.g + 0.11 * c.b;
}
at this point I would like to point out that the standard method would be to apply all algorithms in the perceptual color space, then convert to rgb color space for display.
colorRGB --(convert)--> colorPerceptual --(input)--> f (colorPerceptual) --(output)--> colorPerceptual' --(convert)--> colorRGB
but if you want to adjust for lightness only (perceptual chromatic aberrations will not be fixed), you can do it efficiently in the following manner
//define color of unit lightness. based on Luma algorithm
color unit_l(1/0.3/3, 1/0.59/3, 1/0.11/3);
color produce_gradient(const color & c1, const color & c2, float ratio) {
color output_color;
output_color.r = c1.r + (c2.r - c1.r) * ratio;
output_color.g = c1.g + (c2.g - c1.g) * ratio;
output_color.b = c1.b + (c2.b - c1.b) * ratio;
output_color.a = c1.a + (c2.a - c1.a) * ratio;
float target_lightness = luma(c1) + (luma(c2) - luma(c1)) * ratio; //linearly interpolate perceptual lightness
float delta_lightness = target_lightness - luma(output_color); //calculate required lightness change magnitude
//adjust lightness
output_color.g += unit_l.r * delta_lightness;
output_color.b += unit_l.g * delta_lightness;
output_color.a += unit_l.b * delta_lightness;
//at this point luma(output_color) approximately equals target_lightness which takes care of the perceptual lightness aberrations
return output_color;
}

Your second code example is perfectly correct, except that the alpha channel is generally not gamma corrected so you shouldn't use pow on it. For efficiency's sake it would be better to do the gamma correction once for each channel, instead of doubling up.
The general rule is that you must do gamma in both directions whenever you're adding or subtracting values. If you're only multiplying or dividing, it makes no difference: pow(pow(x, 2.2) * pow(y, 2.2), 1/2.2) is mathematically equivalent to x * y.
Sometimes you might find that you get better results by working in uncorrected space. For example if you're resizing an image, you should do gamma correction if you're downsizing but not if you're upsizing. I forget where I read this, but I verified it myself - the artifacts from upsizing were much less objectionable if you used gamma corrected pixel values vs. linear ones.

Related

Motion Vector - how to calculate it properly?

I'm trying to wrap my head around calculating motion vectors (also called velocity buffer). I found this tutorial, but I'm not satisfied with explanations of how motion vector are calculated. Here is the code:
vec2 a = (vPosition.xy / vPosition.w) * 0.5 + 0.5;
vec2 b = (vPrevPosition.xy / vPrevPosition.w) * 0.5 + 0.5;
oVelocity = a - b;
Why are we multiplying our position vectors by 0.5 and then adding 0.5? I'm guessing that we're trying to get from clip space to NDC, but why? I completly don't understand that.
This is a mapping from the [-1, 1] clip space onto the [0, 1] texture space. Since lookups in the blur shader have to read from a textured at a position offset by the velocity vector, it's necessary to perform this conversion.
Note, that the + 0.5 part is actually unnecessary, since it cancels out in a-b anyway. So the same result would have been achieved by using something like
vec2 a = (vPosition.xy / vPosition.w);
vec2 b = (vPrevPosition.xy / vPrevPosition.w);
oVelocity = (a - b) * 0.5;
I don't know if there is any reason to prefer the first over the second, but my guess is that this code is written in the way it is because it builds up on a previous tutorial where the calculation had been the same.

What is the correct gamma correction function?

Currently I use the following formula to gamma correct colors (convert them from RGB to sRGB color space) after the lighting pass:
output = pow(color, vec3(1.0/2.2));
Is this formula the correct formula for gamma correction? I ask because I have encountered a few people saying that its not, and that the correct formula is more complicated and has something to do with power 2.4 rather than 2.2. I also heard something that the three color R, G and B should have different weights (something like 0.2126, 0.7152, 0.0722).
I am also curious which function does OpenGL use when GL_FRAMEBUFFER_SRGB is enabled.
Edit:
This is one of many topics covered in Guy Davidson's talk "Everything you know about color is wrong". The gamma correction function is covered here, but the whole talk is related to color spaces including sRGB and gamma correction.
Gamma correction may have any value, but considering linear RGB / non-linear sRGB conversion, 2.2 is an approximate, so that your formula may be considered both wrong and correct:
https://en.wikipedia.org/wiki/SRGB#Theory_of_the_transformation
Real sRGB transfer function is based on 2.4 gamma coefficient and has discontinuity at dark values like this:
float Convert_sRGB_FromLinear (float theLinearValue) {
return theLinearValue <= 0.0031308f
? theLinearValue * 12.92f
: powf (theLinearValue, 1.0f/2.4f) * 1.055f - 0.055f;
}
float Convert_sRGB_ToLinear (float thesRGBValue) {
return thesRGBValue <= 0.04045f
? thesRGBValue / 12.92f
: powf ((thesRGBValue + 0.055f) / 1.055f, 2.4f);
}
In fact, you may find even more rough approximations in some GLSL code using 2.0 coefficient instead of 2.2 and 2.4, so that to avoid usage of expensive pow() (x*x and sqrt() are used instead). This is to achieve maximum performance (in context of old graphics hardware) and code simplicity, while sacrificing color reproduction. Practically speaking, the sacrifice is not that noticeable, and most games apply additional tone-mapping and user-managed gamma correction coefficient, so that result is not directly correlated to sRGB standard.
GL_FRAMEBUFFER_SRGB and sampling from GL_SRGB8 textures are expected to use more correct formula (in case of texture sampling it is more likely pre-computed lookup table on GPU rather than real formula as there are only 256 values to convert). See, for instance, comments to GL_ARB_framebuffer_sRGB extension:
Given a linear RGB component, cl, convert it to an sRGB component, cs, in the range [0,1], with this pseudo-code:
if (isnan(cl)) {
/* Map IEEE-754 Not-a-number to zero. */
cs = 0.0;
} else if (cl > 1.0) {
cs = 1.0;
} else if (cl < 0.0) {
cs = 0.0;
} else if (cl < 0.0031308) {
cs = 12.92 * cl;
} else {
cs = 1.055 * pow(cl, 0.41666) - 0.055;
}
The NaN behavior in the pseudo-code is recommended but not specified in the actual specification language.
sRGB components are typically stored as unsigned 8-bit fixed-point values.
If cs is computed with the above pseudo-code, cs can be converted to a [0,255] integer with this formula:
csi = floor(255.0 * cs + 0.5)
Here is another article describing sRGB usage in OpenGL applications, which you may find useful: https://unlimited3d.wordpress.com/2020/01/08/srgb-color-space-in-opengl/

Map angle to RGB color

This video shows what I think is a great visualization of gradient angle by mapping angle (in [-pi,pi]) to RGB color:
I would like to know if it is possible in OpenCV C++ to map a floating point value angle, whose range is -M_PI to M_PI, to an RGB value in some preset colorwheel. Thank you!
Look up hsv to rgb. H, or hue, is the angle you are looking for. You probably want full saturated values with maximum value, but if you turn s and v down a notch, the coding will look less artificial and computery.
Can you calculate this directly from the angle and the edge strength?
red = edgeStrength * sin(angle);
green = edgeStrength * sin(angle + 2*M_PI / 3.); // + 60°
blue = edgeStrength * sin(angle + 4*M_PI / 3.); // + 120°

How do you calculate a "highlight color"? [duplicate]

Given a system (a website for instance) that lets a user customize the background color for some section but not the font color (to keep number of options to a minimum), is there a way to programmatically determine if a "light" or "dark" font color is necessary?
I'm sure there is some algorithm, but I don't know enough about colors, luminosity, etc to figure it out on my own.
I encountered similar problem. I had to find a good method of selecting contrastive font color to display text labels on colorscales/heatmaps. It had to be universal method and generated color had to be "good looking", which means that simple generating complementary color was not good solution - sometimes it generated strange, very intensive colors that were hard to watch and read.
After long hours of testing and trying to solve this problem, I found out that the best solution is to select white font for "dark" colors, and black font for "bright" colors.
Here's an example of function I am using in C#:
Color ContrastColor(Color color)
{
int d = 0;
// Counting the perceptive luminance - human eye favors green color...
double luminance = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B)/255;
if (luminance > 0.5)
d = 0; // bright colors - black font
else
d = 255; // dark colors - white font
return Color.FromArgb(d, d, d);
}
This was tested for many various colorscales (rainbow, grayscale, heat, ice, and many others) and is the only "universal" method I found out.
Edit
Changed the formula of counting a to "perceptive luminance" - it really looks better! Already implemented it in my software, looks great.
Edit 2
#WebSeed provided a great working example of this algorithm: http://codepen.io/WebSeed/full/pvgqEq/
Based on Gacek's answer but directly returning color constants (additional modifications see below):
public Color ContrastColor(Color iColor)
{
// Calculate the perceptive luminance (aka luma) - human eye favors green color...
double luma = ((0.299 * iColor.R) + (0.587 * iColor.G) + (0.114 * iColor.B)) / 255;
// Return black for bright colors, white for dark colors
return luma > 0.5 ? Color.Black : Color.White;
}
Note: I removed the inversion of the luma value to make bright colors have a higher value, what seems more natural to me and is also the 'default' calculation method.
(Edit: This has since been adopted in the original answer, too)
I used the same constants as Gacek from here since they worked great for me.
You can also implement this as an Extension Method using the following signature:
public static Color ContrastColor(this Color iColor)
You can then easily call it via
foregroundColor = backgroundColor.ContrastColor().
Thank you #Gacek. Here's a version for Android:
#ColorInt
public static int getContrastColor(#ColorInt int color) {
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
int d;
if (a < 0.5) {
d = 0; // bright colors - black font
} else {
d = 255; // dark colors - white font
}
return Color.rgb(d, d, d);
}
And an improved (shorter) version:
#ColorInt
public static int getContrastColor(#ColorInt int color) {
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return a < 0.5 ? Color.BLACK : Color.WHITE;
}
My Swift implementation of Gacek's answer:
func contrastColor(color: UIColor) -> UIColor {
var d = CGFloat(0)
var r = CGFloat(0)
var g = CGFloat(0)
var b = CGFloat(0)
var a = CGFloat(0)
color.getRed(&r, green: &g, blue: &b, alpha: &a)
// Counting the perceptive luminance - human eye favors green color...
let luminance = 1 - ((0.299 * r) + (0.587 * g) + (0.114 * b))
if luminance < 0.5 {
d = CGFloat(0) // bright colors - black font
} else {
d = CGFloat(1) // dark colors - white font
}
return UIColor( red: d, green: d, blue: d, alpha: a)
}
Javascript [ES2015]
const hexToLuma = (colour) => {
const hex = colour.replace(/#/, '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return [
0.299 * r,
0.587 * g,
0.114 * b
].reduce((a, b) => a + b) / 255;
};
Ugly Python if you don't feel like writing it :)
'''
Input a string without hash sign of RGB hex digits to compute
complementary contrasting color such as for fonts
'''
def contrasting_text_color(hex_str):
(r, g, b) = (hex_str[:2], hex_str[2:4], hex_str[4:])
return '000' if 1 - (int(r, 16) * 0.299 + int(g, 16) * 0.587 + int(b, 16) * 0.114) / 255 < 0.5 else 'fff'
Thanks for this post.
For whoever might be interested, here's an example of that function in Delphi:
function GetContrastColor(ABGColor: TColor): TColor;
var
ADouble: Double;
R, G, B: Byte;
begin
if ABGColor <= 0 then
begin
Result := clWhite;
Exit; // *** EXIT RIGHT HERE ***
end;
if ABGColor = clWhite then
begin
Result := clBlack;
Exit; // *** EXIT RIGHT HERE ***
end;
// Get RGB from Color
R := GetRValue(ABGColor);
G := GetGValue(ABGColor);
B := GetBValue(ABGColor);
// Counting the perceptive luminance - human eye favors green color...
ADouble := 1 - (0.299 * R + 0.587 * G + 0.114 * B) / 255;
if (ADouble < 0.5) then
Result := clBlack // bright colors - black font
else
Result := clWhite; // dark colors - white font
end;
This is such a helpful answer. Thanks for it!
I'd like to share an SCSS version:
#function is-color-light( $color ) {
// Get the components of the specified color
$red: red( $color );
$green: green( $color );
$blue: blue( $color );
// Compute the perceptive luminance, keeping
// in mind that the human eye favors green.
$l: 1 - ( 0.299 * $red + 0.587 * $green + 0.114 * $blue ) / 255;
#return ( $l < 0.5 );
}
Now figuring out how to use the algorithm to auto-create hover colors for menu links. Light headers get a darker hover, and vice-versa.
Short Answer:
Calculate the luminance (Y) of the given color, and flip the text either black or white based on a pre-determined middle contrast figure. For a typical sRGB display, flip to white when Y < 0.4 (i.e. 40%)
Longer Answer
Not surprisingly, nearly every answer here presents some misunderstanding, and/or is quoting incorrect coefficients. The only answer that is actually close is that of Seirios, though it relies on WCAG 2 contrast which is known to be incorrect itself.
If I say "not surprisingly", it is due in part to the massive amount of misinformation on the internet on this particular subject. The fact this field is still a subject of active research and unsettled science adds to the fun. I come to this conclusion as the result of the last few years of research into a new contrast prediction method for readability.
The field of visual perception is dense and abstract, as well as developing, so it is common for misunderstandings to exist. For instance, HSV and HSL are not even close to perceptually accurate. For that you need a perceptually uniform model such as CIELAB or CIELUV or CIECAM02 etc.
Some misunderstandings have even made their way into standards, such as the contrast part of WCAG 2 (1.4.3), which has been demonstrated as incorrect over much of its range.
First Fix:
The coefficients shown in many answers here are (.299, .587, .114) and are wrong, as they pertain to a long obsolete system known as NTSC YIQ, the analog broadcast system in North America some decades ago. While they may still be used in some YCC encoding specs for backwards compatibility, they should not be used in an sRGB context.
The coefficients for sRGB and Rec.709 (HDTV) are:
Red: 0.2126
Green: 0.7152
Blue: 0.0722
Other color spaces like Rec2020 or AdobeRGB use different coefficients, and it is important to use the correct coefficients for a given color space.
The coefficients can not be applied directly to 8 bit sRGB encoded image or color data. The encoded data must first be linearized, then the coefficients applied to find the luminance (light value) of the given pixel or color.
For sRGB there is a piecewise transform, but as we are only interested in the perceived lightness contrast to find the point to "flip" the text from black to white, we can take a shortcut via the simple gamma method.
Andy's Shortcut to Luminance & Lightness
Divide each sRGB color by 255.0, then raise to the power of 2.2, then multiply by the coefficients and sum them to find estimated luminance.
let Ys = Math.pow(sR/255.0,2.2) * 0.2126 +
Math.pow(sG/255.0,2.2) * 0.7152 +
Math.pow(sB/255.0,2.2) * 0.0722; // Andy's Easy Luminance for sRGB. For Rec709 HDTV change the 2.2 to 2.4
Here, Y is the relative luminance from an sRGB monitor, on a 0.0 to 1.0 scale. This is not relative to perception though, and we need further transforms to fit our human visual perception of the relative lightness, and also of the perceived contrast.
The 40% Flip
But before we get there, if you are only looking for a basic point to flip the text from black to white or vice versa, the cheat is to use the Y we just derived, and make the flip point about Y = 0.40;. so for colors higher than 0.4 Y, make the text black #000 and for colors darker than 0.4 Y, make the text white #fff.
let textColor = (Ys < 0.4) ? "#fff" : "#000"; // Low budget down and dirty text flipper.
Why 40% and not 50%? Our human perception of lightness/darkness and of contrast is not linear. For a self illuminated display, it so happens that 0.4 Y is about middle contrast under most typical conditions.
Yes it varies, and yes this is an over simplification. But if you are flipping text black or white, the simple answer is a useful one.
Perceptual Bonus Round
Predicting the perception of a given color and lightness is still a subject of active research, and not entirely settled science. The L* (Lstar) of CIELAB or LUV has been used to predict perceptual lightness, and even to predict perceived contrast. However, L* works well for surface colors in a very defined/controlled environment, and does not work as well for self illuminated displays.
While this varies depending on not only the display type and calibration, but also your environment and the overall page content, if you take the Y from above, and raise it by around ^0.685 to ^0.75, you'll find that 0.5 is typically the middle point to flip the text from white to black.
let textColor = (Math.pow(Ys,0.75) < 0.5) ? "#fff" : "#000"; // perceptually based text flipper.
Using the exponent 0.685 will make the text color swap on a darker color, and using 0.8 will make the text swap on a lighter color.
Spatial Frequency Double Bonus Round
It is useful to note that contrast is NOT just the distance between two colors. Spatial frequency, in other words font weight and size, are also CRITICAL factors that cannot be ignored.
That said, you may find that when colors are in the midrange, that you'd want to increase the size and or weight of the font.
let textSize = "16px";
let textWeight = "normal";
let Ls = Math.pow(Ys,0.7);
if (Ls > 0.33 && Ls < 0.66) {
textSize = "18px";
textWeight = "bold";
} // scale up fonts for the lower contrast mid luminances.
Hue R U
It's outside the scope of this post to delve deeply, but above we are ignoring hue and chroma. Hue and chroma do have an effect, such as Helmholtz Kohlrausch, and the simpler luminance calculations above do not always predict intensity due to saturated hues.
To predict these more subtle aspects of perception, a complete appearance model is needed. R. Hunt, M. Fairshild, E. Burns are a few authors worth looking into if you want to plummet down the rabbit hole of human visual perception...
For this narrow purpose, we could re-weight the coefficients slightly, knowing that green makes up the majority of of luminance, and pure blue and pure red should always be the darkest of two colors. What tends to happen using the standard coefficients, is middle colors with a lot of blue or red may flip to black at a lower than ideal luminance, and colors with a high green component may do the opposite.
That said, I find this is best addressed by increasing font size and weight in the middle colors.
Putting it all together
So we'll assume you'll send this function a hex string, and it will return a style string that can be sent to a particular HTML element.
Check out the CODEPEN, inspired by the one Seirios did:
CodePen: Fancy Font Flipping
One of the things the Codepen code does is increase the text size for the lower contrast midrange. Here's a sample:
And if you want to play around with some of these concepts, see the SAPC development site at https://www.myndex.com/SAPC/ clicking on "research mode" provides interactive experiments to demonstrate these concepts.
Terms of enlightenment
Luminance: Y (relative) or L (absolute cd/m2) a spectrally weighted but otherwise linear measure of light. Not to be confused with "Luminosity".
Luminosity: light over time, useful in astronomy.
Lightness: L* (Lstar) perceptual lightness as defined by the CIE. Some models have a related lightness J*.
I had the same problem but i had to develop it in PHP. I used #Garek's solution and i also used this answer:
Convert hex color to RGB values in PHP to convert HEX color code to RGB.
So i'm sharing it.
I wanted to use this function with given Background HEX color, but not always starting from '#'.
//So it can be used like this way:
$color = calculateColor('#804040');
echo $color;
//or even this way:
$color = calculateColor('D79C44');
echo '<br/>'.$color;
function calculateColor($bgColor){
//ensure that the color code will not have # in the beginning
$bgColor = str_replace('#','',$bgColor);
//now just add it
$hex = '#'.$bgColor;
list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x");
$color = 1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b)/255;
if ($color < 0.5)
$color = '#000000'; // bright colors - black font
else
$color = '#ffffff'; // dark colors - white font
return $color;
}
Flutter implementation
Color contrastColor(Color color) {
if (color == Colors.transparent || color.alpha < 50) {
return Colors.black;
}
double luminance = (0.299 * color.red + 0.587 * color.green + 0.114 * color.blue) / 255;
return luminance > 0.5 ? Colors.black : Colors.white;
}
Based on Gacek's answer, and after analyzing #WebSeed's example with the WAVE browser extension, I've come up with the following version that chooses black or white text based on contrast ratio (as defined in W3C's Web Content Accessibility Guidelines (WCAG) 2.1), instead of luminance.
This is the code (in javascript):
// As defined in WCAG 2.1
var relativeLuminance = function (R8bit, G8bit, B8bit) {
var RsRGB = R8bit / 255.0;
var GsRGB = G8bit / 255.0;
var BsRGB = B8bit / 255.0;
var R = (RsRGB <= 0.03928) ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
var G = (GsRGB <= 0.03928) ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
var B = (BsRGB <= 0.03928) ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};
var blackContrast = function(r, g, b) {
var L = relativeLuminance(r, g, b);
return (L + 0.05) / 0.05;
};
var whiteContrast = function(r, g, b) {
var L = relativeLuminance(r, g, b);
return 1.05 / (L + 0.05);
};
// If both options satisfy AAA criterion (at least 7:1 contrast), use preference
// else, use higher contrast (white breaks tie)
var chooseFGcolor = function(r, g, b, prefer = 'white') {
var Cb = blackContrast(r, g, b);
var Cw = whiteContrast(r, g, b);
if(Cb >= 7.0 && Cw >= 7.0) return prefer;
else return (Cb > Cw) ? 'black' : 'white';
};
A working example may be found in my fork of #WebSeed's codepen, which produces zero low contrast errors in WAVE.
As Kotlin / Android extension:
fun Int.getContrastColor(): Int {
// Counting the perceptive luminance - human eye favors green color...
val a = 1 - (0.299 * Color.red(this) + 0.587 * Color.green(this) + 0.114 * Color.blue(this)) / 255
return if (a < 0.5) Color.BLACK else Color.WHITE
}
An implementation for objective-c
+ (UIColor*) getContrastColor:(UIColor*) color {
CGFloat red, green, blue, alpha;
[color getRed:&red green:&green blue:&blue alpha:&alpha];
double a = ( 0.299 * red + 0.587 * green + 0.114 * blue);
return (a > 0.5) ? [[UIColor alloc]initWithRed:0 green:0 blue:0 alpha:1] : [[UIColor alloc]initWithRed:255 green:255 blue:255 alpha:1];
}
iOS Swift 3.0 (UIColor extension):
func isLight() -> Bool
{
if let components = self.cgColor.components, let firstComponentValue = components[0], let secondComponentValue = components[1], let thirdComponentValue = components[2] {
let firstComponent = (firstComponentValue * 299)
let secondComponent = (secondComponentValue * 587)
let thirdComponent = (thirdComponentValue * 114)
let brightness = (firstComponent + secondComponent + thirdComponent) / 1000
if brightness < 0.5
{
return false
}else{
return true
}
}
print("Unable to grab components and determine brightness")
return nil
}
Swift 4 Example:
extension UIColor {
var isLight: Bool {
let components = cgColor.components
let firstComponent = ((components?[0]) ?? 0) * 299
let secondComponent = ((components?[1]) ?? 0) * 587
let thirdComponent = ((components?[2]) ?? 0) * 114
let brightness = (firstComponent + secondComponent + thirdComponent) / 1000
return !(brightness < 0.6)
}
}
UPDATE - Found that 0.6 was a better test bed for the query
Note there is an algorithm for this in the google closure library that references a w3c recommendation: http://www.w3.org/TR/AERT#color-contrast. However, in this API you provide a list of suggested colors as a starting point.
/**
* Find the "best" (highest-contrast) of the suggested colors for the prime
* color. Uses W3C formula for judging readability and visual accessibility:
* http://www.w3.org/TR/AERT#color-contrast
* #param {goog.color.Rgb} prime Color represented as a rgb array.
* #param {Array<goog.color.Rgb>} suggestions Array of colors,
* each representing a rgb array.
* #return {!goog.color.Rgb} Highest-contrast color represented by an array.
*/
goog.color.highContrast = function(prime, suggestions) {
var suggestionsWithDiff = [];
for (var i = 0; i < suggestions.length; i++) {
suggestionsWithDiff.push({
color: suggestions[i],
diff: goog.color.yiqBrightnessDiff_(suggestions[i], prime) +
goog.color.colorDiff_(suggestions[i], prime)
});
}
suggestionsWithDiff.sort(function(a, b) { return b.diff - a.diff; });
return suggestionsWithDiff[0].color;
};
/**
* Calculate brightness of a color according to YIQ formula (brightness is Y).
* More info on YIQ here: http://en.wikipedia.org/wiki/YIQ. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb Color represented by a rgb array.
* #return {number} brightness (Y).
* #private
*/
goog.color.yiqBrightness_ = function(rgb) {
return Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000);
};
/**
* Calculate difference in brightness of two colors. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb1 Color represented by a rgb array.
* #param {goog.color.Rgb} rgb2 Color represented by a rgb array.
* #return {number} Brightness difference.
* #private
*/
goog.color.yiqBrightnessDiff_ = function(rgb1, rgb2) {
return Math.abs(
goog.color.yiqBrightness_(rgb1) - goog.color.yiqBrightness_(rgb2));
};
/**
* Calculate color difference between two colors. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb1 Color represented by a rgb array.
* #param {goog.color.Rgb} rgb2 Color represented by a rgb array.
* #return {number} Color difference.
* #private
*/
goog.color.colorDiff_ = function(rgb1, rgb2) {
return Math.abs(rgb1[0] - rgb2[0]) + Math.abs(rgb1[1] - rgb2[1]) +
Math.abs(rgb1[2] - rgb2[2]);
};
base R version of #Gacek's answer to get luminance (you can apply your own threshold easily)
# vectorized
luminance = function(col) c(c(.299, .587, .114) %*% col2rgb(col)/255)
Usage:
luminance(c('black', 'white', '#236FAB', 'darkred', '#01F11F'))
# [1] 0.0000000 1.0000000 0.3730039 0.1629843 0.5698039
If you're manipulating color spaces for visual effect it's generally easier to work in HSL (Hue, Saturation and Lightness) than RGB. Moving colours in RGB to give naturally pleasing effects tends to be quite conceptually difficult, whereas converting into HSL, manipulating there, then converting back out again is more intuitive in concept and invariably gives better looking results.
Wikipedia has a good introduction to HSL and the closely related HSV. And there's free code around the net to do the conversion (for example here is a javascript implementation)
What precise transformation you use is a matter of taste, but personally I'd have thought reversing the Hue and Lightness components would be certain to generate a good high contrast colour as a first approximation, but you can easily go for more subtle effects.
You can have any hue text on any hue background and ensure that it is legible. I do it all the time. There's a formula for this in Javascript on Readable Text in Colour – STW*
As it says on that link, the formula is a variation on the inverse-gamma adjustment calculation, though a bit more manageable IMHO.
The menus on the right-hand side of that link and its associated pages use randomly-generated colours for text and background, always legible. So yes, clearly it can be done, no problem.
An Android variation that captures the alpha as well.
(thanks #thomas-vos)
/**
* Returns a colour best suited to contrast with the input colour.
*
* #param colour
* #return
*/
#ColorInt
public static int contrastingColour(#ColorInt int colour) {
// XXX https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(colour) + 0.587 * Color.green(colour) + 0.114 * Color.blue(colour)) / 255;
int alpha = Color.alpha(colour);
int d = 0; // bright colours - black font;
if (a >= 0.5) {
d = 255; // dark colours - white font
}
return Color.argb(alpha, d, d, d);
}
I would have commented on the answer by #MichaelChirico but I don't have enough reputation. So, here's an example in R with returning the colours:
get_text_colour <- function(
background_colour,
light_text_colour = 'white',
dark_text_colour = 'black',
threshold = 0.5
) {
background_luminance <- c(
c( .299, .587, .114 ) %*% col2rgb( background_colour ) / 255
)
return(
ifelse(
background_luminance < threshold,
light_text_colour,
dark_text_colour
)
)
}
> get_text_colour( background_colour = 'blue' )
[1] "white"
> get_text_colour( background_colour = c( 'blue', 'yellow', 'pink' ) )
[1] "white" "black" "black"
> get_text_colour( background_colour = c('black', 'white', '#236FAB', 'darkred', '#01F11F') )
[1] "white" "black" "white" "white" "black"

Advanced moiré a pattern reduction in HLSL / GLSL procedural textures shader - antialiasing

I am working on a procedural texture, it looks fine, except very far away, the small texture pixels disintegrate into noise and moiré patterns.
I have set out to find a solution to average and quantise the scale of the pattern far away and close up, so that close by it is in full detail, and far away it is rounded off so that one pixel of a distant mountain only represents one colour found there, and not 10 or 20 colours at that point.
It is easy to do it by rounding the World_Position that the volumetric texture is based on using an if statement i.e.:
if( camera-pixel_distance > 1200 meters ) {wpos = round(wpos/3)*3;}//---round far away pixels
return texturefucntion(wpos);
the result of rounding far away textures is that they will look like this, except very far away:
the trouble with this is i have to make about 5 if conditions for the various distances, and i have to estimate a random good rounding value
I tried to make a function that cuts the distance of the pixel into distance steps, and applies a LOD devider to the pixel_worldposition value to make it progressively rounder at distance but i got nonsense results, actually the HLSL was totally flipping out. here is the attempt:
float cmra= floor(_WorldSpaceCameraPos/500)*500; //round camera distance by steps of 500m
float dst= (1-distance(cmra,pos)/4500)*1000 ; //maximum faraway view is 4500 meters
pos= floor(pos/dst)*dst;//close pixels are rounded by 1000, far ones rounded by 20,30 etc
it returned nonsense patterns that i could not understand.
Are there good documented algorithms for smoothing and rounding distance texture artifacts? can i use the scren pixel resolution, combined with the distance of the pixel, to round each pixel to one color that stays a stable color?
Are you familiar with the GLSL (and I would assume HLSL) functions dFdx() and dFdy() or fwidth()? They were made specifically to solve this problem. From the GLSL Spec:
genType dFdy (genType p)
Returns the derivative in y using local differencing for the input argument p.
These two functions are commonly used to estimate the filter width used to anti-alias procedural textures.
and
genType fwidth (genType p)
Returns the sum of the absolute derivative in x and y using local differencing for the input argument p, i.e.: abs (dFdx (p)) + abs (dFdy (p));
OK i found some great code and a tutorial for the solution, it's a simple code that can be tweaked by distance and many parameters.
from this tutorial:
http://www.yaldex.com/open-gl/ch17lev1sec4.html#ch17fig04
half4 frag (v2f i) : COLOR
{
float Frequency = 0.020;
float3 pos = mul (_Object2World, i.uv).xyz;
float V = pos.z;
float sawtooth = frac(V * Frequency);
float triangle = (abs(2.0 * sawtooth - 1.0));
//return triangle;
float dp = length(float2(ddx(V), ddy(V)));
float edge = dp * Frequency * 8.0;
float square = smoothstep(0.5 - edge, 0.5 + edge, triangle);
// gl_FragColor = vec4(vec3(square), 1.0);
if (pos.x>0.){return float4(float3(square), 1.0);}
if (pos.x<0.){return float4(float3(triangle), 1.0);}
}