Given the RGB components of a color, how can I decide if it is perceived as gray by humans? - colors

One simple way is to say that when the RGB components are equal, they form a gray color.
However, this is not the whole story, because if they only have a slight difference, they will still look gray.
Assuming the viewer has a healthy vision of color, how can I decide if the given values would be perceived as gray (presumably with an adjustable threshold level for "grayness")?

A relatively straightforward method would be to convert RGB value to HSV color space and use threshold on the saturation component, e.g. "if saturation < 0.05 then 'almost grey', else not grey".
Saturation is actually the "grayness/colorfulness" by definition.
This method is much more accurate than using differences between R, G and B channels (since human eye perceives saturation differently on light and dark colors). On the other hand, converting RGB to HSV is computationally intensive. It is up to you to decide what is of more value - precise answer (grey/not grey) or performance.
If you need an even more precise method, you may use L*a*b* color space and compute chroma as sqrt(a*a + b*b) (see here), and then apply thresholding to this value. However, this would be even more computationally intensive.
You can also combine multiple methods:
Calculate simple differences between R, G, B components. If the color can be identified as definitely desaturated (e.g. max(abs(R-G), abs(R-B), abs(G-B)) <= 5) or definitely saturated (e.g. max(abs(R-G), abs(R-B), abs(G-B)) > 100), then stop.
Otherwise, convert to L*a*b*, compute chroma as sqrt(a*a + b*b) and use thresholding on this value.

r = 160;
g = 179;
b = 151;
tolerance = 20;
if (Math.abs(r-g) < 20 && Math.abs(r-b) < 20) {
#then perceived as gray
}

Related

QColor hsl/hsv representaion is wrong?

I'm trying to change color space of given image by using PyQt. I can't understand how QColor works.
Speaking about HSV we have 3 channels: H - from 0 to 359, S - from 0 to 100, V - from 0 to 100. But in documentation:
The value of s, v, and a must all be in the range 0-255; the value of h must be in the range 0-359.
How can be S and V values be in range 0-255? The same question is about HSL, where S and L should be in range 0-100
The value of s, l, and a must all be in the range 0-255; the value of h must be in the range 0-359.
And one more question. Should be the image, converted from rgb to hsl / rgb to hsv look the same and has the same colors?
Speaking about HSV we have 3 channels: H - from 0 to 359, S - from 0 to 100, V - from 0 to 100.
That's just a common convention, but it's not part of the HSV color space definition, nor its "parent" HSL from which it origined.
Those values are always intended as a range between a minimum and a maximum, not a discrete-based value range.
First of all, they both are alternative representations of the RGB color model.[1]
Then, colors are not discrete, our "digital usage" forces us to make them so, and their value range is completely arbitrary.
The commonly used RGB model is based on 8 bits for each primary color (providing a 256 value range for each of them, from 0 to 255), but even if it's normally fine for most usage, it's actually limited, especially when shown in a video or animation: in some cases (notably, with gradients), even a value change of 1 in a component can be clearly seen.
Color model representations in digital world commonly use discrete integer values of color spaces using limited ranges for performance reasons, and that's also valid for the values you're referring to. The range depends on the implementation.
For instance, the CSS rgb() notation accepts values with the 8-bit notation and percentages. Those values are almost never consistent, and for obvious reasons: while the theoretical range is of 256 values, the range of a percentage always refers to the maximum (255), meaning that 50% (or 0.5) is actually 127.5.
In fact, rgb(50%, 50%, 50%) normally results in #808080, which is rgb(128, 128, 128) (since 127.5 is rounded), meaning that rgb(50%, 50%, 50%) and rgb(128, 128, 128) are not the same, conceptually speaking.[2]
So, to the point, the value range only depends on the implementation. The only difference is that the hue component is wrapping because it's based on a circle, meaning that it always truly is a 0-360 range value: 50% (or 0.5) will always be 180 degrees, and that's because the maximum (360°, or 100%) equals the minimum (0).
Qt chose to use a 8-bit standard (0-255) for integer values that, following convention, use 0-255 or percentage ranges, with the exception of the hue component that uses the common 360 degrees notation.
If you want something more consistent with your habits, then you can add it with a simple helper function, but remember that, as the documentation explains, "components are stored using 16-bit integers" (note that this is still valid even for Qt6[3]), meaning that results might slightly differ.
def fromHsv100(*args, alpha=None):
if isinstance(args[0], QColor):
args = args[1:]
h, s, v = args[:3]
if alpha is None:
if len(args) == 4:
alpha = args[3]
else:
alpha = 100
return QColor.fromHsvF(
(h / 360) % 1,
(s * .01) % 1,
(v * .01) % 1,
(alpha * .01) % 1
)
def getHsv100(color):
return (
color.hue(),
round(color.saturationF() * 100),
round(color.valueF() * 100),
round(color.alphaF() * 100)
)
QColor.fromHsv100 = fromHsv100
QColor.getHsv100 = getHsv100
# usage:
color = QColor.fromHsv100(120, 55, 89)
print(color.getHsv100())
Finally, remember that, due to the nature of hue-based color models, you can create different colors that are always shown as "black" if their value (for HSV) or lightness (for HSL) component is 0, while they can have different hue and saturation values:
>> print(QColor.fromHsv(60, 0, 0).name())
#000000
>> print(QColor.fromHsv(240, 50, 0).name())
#000000
About your last question, since HSL and HSV are just alternative representations of the RGB color model, an image created with any of the above will theoretically look the same as long as it uses the same color space, and as long as the resulting integer values of the colors are compatible and rounded in the same way. But, since those values are always rounded based on their ranges, and those ranges are proportional to the actual model (which is not consistent for obvious reasons), that might not always happen.
For instance:
>>> hue = 290
>>> rgb = QColor.fromHsv(hue, 150, 150).getRgb()
>>> print(rgb)
(135, 62, 150, 255)
>>> newHue = QColor.fromRgb(*rgb).hue()
>>> print(hue == newHue, hue, newHue)
False 290 289
This means that if you create or edit images using multiple conversions between different color spaces, you might end up with images that are not actually identical.
[1] See the related Wikipedia article
[2] Actual values of the resulting 24-bit RGB (which, as of late 2022, is the final color shown by a non-HDR browser/system) might depend on the browser and its rounding implementation; note that rounding is not always consistent, for instance, Python uses the Round half to even (aka, the "bankers' rounding") method for round(), meaning that both 127.5 and 128.5 are rounded to 128.
[3] Even if most modern devices support wider color dynamic ranges, QColor is intended for basic, performant behavior, since it's used in a lot of basic classes that expect fast results, like displaying labels, buttons or texts of items in a model view; things for which such dynamic ranges are quite pointless.

Same come different Pixels in Processing

I am working in Processing and I would like to compare the color of 2 the pixels of 2 different images.
let's say we comparing the pixel in position 10
color c1= image1.pixels[10]; color c2= image2.pixels[10];
if(c1==c2) { //so something }
Firstly I was playing with brightnsess
if(brightness(c1)==brightness(c2))
Generally it was working but not exactly as I wanted as the pixels were a little bit similar but not exactly the same color.
if you want to compare colours you are probably better off comparing the three basic ones instead of the actual number that "color" is. Thus instead of
if(c1 == c2)
where you compare two large numbers like 13314249 you can go
if(red(c1) == red(c2) && green(c1) == green(c2) && blue(c1) == blue(c2))
where you compare numbers from 0 - 255, the possible values of red or green or blue you can get from a colour. As for the "little bit similar" colours, you can set a threshold and any difference below that threshold will be considered negligible thus the colours are the same. Something like this:
int threshold = 5
if(abs(red(c1) red(c2)) < threshold && abs(green(c1) - green(c2)) < threshold && abs(blue(c1) == blue(c2)) < threshold)
Remember, you have to take the absolute difference! This way, if you decrease the threshold only very similar colours are considered the same while is you increase it different colours can be considered the same. That threshold number depends on your likings!
This would also work with your brightness example...
int threshold = 5
if(abs(brightness(c1) - brightness(c2)) < threshold)
To extend on Petros's answer. Generally, when I am comparing image pixels, I normalize, so that the code will work with images that are not in standard range 0-255. It also is good when you are doing many operations on the images to keep in mind the range you are currently working with for scaling purposes.
MAX_PIXEL=255 //maybe range is different for some reason
MIN_PIXEL=0
pixel_difference = 10
threshold = pixel_difference/(MAX_PIXEL-MIN_PIXEL)
if ( abs( (brightness(c1)-brightness(c2))/(MAX_PIXEL-MIN_PIXEL))< threshold ) {
//then the pixels are similar.
}
Sometimes you can gain more ground by transforming to a difference color space.
And depending on your task at hand you can build a background model that can adapt over time or compare higher level global features such as histograms or local features such as Scale Invariant Feature Transform (SIFT), or Corners, Edges.

How to get colors with the same perceived brightness?

Is there a tool / program / color system that enables you to get colors of the same luminance (perceived brightness)?
Say I pick a color (determine RGB values) and the program gives me all the colors around the color wheel with the same luminance but different hues?
I haven't seen such tool yet, all I came across were three different algorithms for color luminance:
(0.2126*R) + (0.7152*G) + (0.0722*B)
(0.299*R + 0.587*G + 0.114*B)
sqrt( 0.241*R^2 + 0.691*G^2 + 0.068*B^2 )
Just to be clear, I'm talking about color luminance / perceived brightness or whatever you want to call it - the attribute that encounters that we perceive red hue brighter than blue for example. (So 255,0,0 has higher luminance value than 0,0,255.)
P.S.: Does anyone know which algorithm is used to determine color luminence on this website: http://www.workwithcolor.com/hsl-color-picker-01.htm
It looks like they used none of the posted algorithms.
In the HSL color picker you linked to, it looks like they are using the 3rd Lightness equation given here, and then making it a percentage. So the equation is:
L = (100 * 0.5 * (max(r,g,b) + min(r,g,b))) / 255
Edit: Actually, I just realized that they have an L value and a Lum value shown on that color picker. The equation above applies to the L value, but I don't know how they are arriving at the Lum value. It doesn't seem to follow any of the standard equations.

Fade through more more natural rainbow spectrum in HSV/HSB

I'm trying to control some RGB LEDs and fade from red to violet. I'm using an HSV to RGB conversion so that I can just sweep from hue 0 to hue 300 (beyond that it moves back towards red). The problem I noticed though is that it seems to spend far to much time in the cyan and blue section of the spectrum. So I looked up what the HSV spectrum is supposed to look like, and found thisL
I didn't realize that more than half the spectrum was spent between green and blue.
But I'd really like it to look much more like this:
With a nice even blend of that "standard" rainbow colors.
I'd imagine that this would end up being some sort of s-curve of the normal hue values, but am not really sure how to calculate that curve.
An actual HSV to RGB algorithm that handles this internally would be great (any code really, though it's for an Arduino) but even just an explanation of how I could calculate that hue curve would be greatly appreciated.
http://www.fourmilab.ch/documents/specrend/ has a fairly detailed description of how to convert a wavelength to CIE components (which roughly correspond to the outputs of the three kinds of cone sensors in your eyes) and then how to convert those to RGB values (with a warning that some wavelengths don't have RGB equivalents in a typical RGB gamut).
Or: there are various "perceptually uniform colour spaces" like CIE L*a*b* (see e.g. http://en.wikipedia.org/wiki/Lab_color_space); you could pick one of those, take equal steps along a straight line joining your starting and ending colours in that space, and convert to RGB.
Either of those is likely to be overkill for your application, though, and there's no obvious reason why they should be much -- or any -- better than something simpler and purely empirical. So why not do the following:
Choose your starting and ending colours. For simplicity, let's suppose they have S=1 and V=1 in HSV space. Note them down.
Look along the hue "spectrum" that you posted and find a colour that looks to you about halfway between your starting and ending points. Note this down.
Now bisect again: find colours halfway between start and mid, and halfway between mid and end.
Repeat once or twice more, so that you've divided the hue scale into 8 or 16 "perceptually equal" parts.
Convert to RGB, stick them in a lookup table, and interpolate linearly in between.
Tweak the RGB values a bit until you have something that looks good.
This is totally ad hoc and has nothing principled about it at all, but it'll probably work pretty well and the final code will be basically trivial:
void compute_rgb(int * rp, int * gp, int * bp, int t) {
// t in the range 0..255 (for convenience)
int segment = t>>5; // 0..7
int delta = t&31;
int a=rgb_table[segment].r, b=rgb_table[segment+1].r;
*rp = a + ((delta*(b-a))>>5);
a=rgb_table[segment].g; b=rgb_table[segment+1].g;
*gp = a + ((delta*(b-a))>>5);
a=rgb_table[segment].b; b=rgb_table[segment+1].b;
*bp = a + ((delta*(b-a))>>5);
}
(you can make the code somewhat clearer if you don't care about saving every available cycle).
For what it's worth, my eyes put division points at hue values of about (0), 40, 60, 90, 150, 180, 240, 270, (300). Your mileage may vary.
FastLED does a a version of this: https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors
HSLUV is another option: http://www.hsluv.org/. They have libraries in a bunch of different languages.
Also, this is an interesting technique: https://www.shadertoy.com/view/4l2cDm
const float tau = acos(-1.)*2.;
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord.xy / iResolution.xy;
vec3 rainbow = sqrt( //gamma
sin( (uv.x+vec3(0,2,1)/3.)*tau ) * .5 + .5
);
fragColor.rgb = rainbow;
}
Also see:
https://en.wikipedia.org/wiki/Rainbow#Number_of_colours_in_spectrum_or_rainbow for more info.

Distance measure between HSL colours

I am coding a program that allows a user to choose various foreground and background colours in RGB. I want to not allow them to chose foreground and backgrounds that are too similar and decided to convert to HSL and use HSL euclidean distance as a way to check for similarity.
Is there a good weighting to use for HSL space (rather than equal weighting for H, S and L)? I've looked at various sites and not found the exact thing I need; just things saying that HSL or HSB is better than RGB.
first convert the colors to Lab. This colorspace is designed so that the vectorial difference between any two colors closely approximate a 'subjective distance'.
In color management, a 'delta E' value is given as a measure of how perceptually faithful a given color transformation is. it's just the magnitude of the vector difference between original and final colors as expressed in Lab space.
My advice would be to skip HSL/HSB entirely, and go directly from RGB to LAB. Once you've done that, you can do a standard delta E computation.
I don't have exact figures for you, but I'd use a much higher weight for L than H or S. The eye is bad at discriminating between equal colors of different saturation, and nearly as bad at distinguishing different hues - expecially if it's fine detail you're trying to see, like text.
I just concluded an interesting study into color spaces. As others mentioned here, converting RGB to CIE-Lab and doing a Delta E computation will give you perceptual color distance. It produces okay results.
My goal was to find the closest index in a limited color palette. However, I found using CIE-Lab Delta E calculations ended up with "wrong" colors. Particularly grayscale would wind up getting too much saturation and select a red instead of a gray from the palette but other colors had issues too (I don't remember which ones). For better or worse, I wound up weighting hues at a 1.2x multiplier, saturation at 1.5x, and B values at either 1.0x or 2.0x depending on the direction. The results more or less work out better than just Delta E alone.
Calculating the distance of Hue is a bit tricky since it is a circle. For example, Hue 0 and Hue 359 are a distance of 1. The solution is to select the minimum of two different distances.
Here's my code based on the above:
// Finds the nearest color index in a RGB palette that matches the requested color.
// This function uses HSB instead of CIE-Lab since this function is intended to be called after GetReadableTextForegroundColors() and results in more consistent color accuracy.
public static function FindNearestPaletteColorIndex($palette, $r, $g, $b)
{
$hsb1 = self::ConvertRGBToHSB($r, $g, $b);
$result = false;
$founddist = false;
foreach ($palette as $key => $rgb)
{
$rgb = array_values($rgb);
$r = $rgb[0];
$g = $rgb[1];
$b = $rgb[2];
$hsb2 = self::ConvertRGBToHSB($r, $g, $b);
$hdiff = min(abs($hsb1["h"] - $hsb2["h"]), abs($hsb1["h"] - $hsb2["h"] + ($hsb1["h"] < $hsb2["h"] ? -360.0 : 360.0))) * 1.2;
$sdiff = ($hsb1["s"] - $hsb2["s"]) * 1.5;
$bdiff = $hsb1["b"] - $hsb2["b"];
if ($hsb1["b"] < $hsb2["b"]) $bdiff *= 2.0;
$hdiff *= $hdiff;
$sdiff *= $sdiff;
$bdiff *= $bdiff;
$dist = $hdiff + $sdiff + $bdiff;
if ($result === false || $founddist >= $dist)
{
$result = $key;
$founddist = $dist;
}
}
return $result;
}
Source: https://github.com/cubiclesoft/php-misc/blob/master/support/color_tools.php
Converting the above to use HSL instead of HSB/HSV shouldn't be too difficult. I prefer the HSB color space since it mirrors Photoshop, which allows me to confirm the numbers I'm looking for in software.

Resources