What is the correct gamma correction function? - opengl

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/

Related

OpenGL: issues with converting floats from texture to integers in fragment shader

I render to a texture which is in the format GL_RGBA8.
When I render to this texture I have a fragment shader whose output is set to color = (1/255, 0, 0, 1). Triangles are overlapping each other and I set the blend mode to (GL_ONE, GL_ONE) so for example if 2 triangles overlap for a given fragment, the resulting pixel at that fragment position will have value (2/255.0).
I then use this texture in a second pass (applied to a quad filling up the screen). My goal at this point when I read the values back from the texture is to convert the values (which are in floating point format in the range [0:1]) back to integers in the range [0:255]. If I look at the pixel that add value (2.0/255.0) I should have the result (2.0/255.0) * 255.0 = 2.0 but I don't.
If I do
float a = (texture(colorTexture, texCoord).x * 255);
float b = (a == 2) ? 1.0 : 0;
color = vec4(0, b, 0, 1);
I get a black image. If I do
float a = (texture(colorTexture, texCoord).x * 255);
float b = (a > 1.999 && a <= 2) ? 1.0 : 0;
color = vec4(0, b, 0, 1);
I get the expected result. So in summary it seems like the convention back to [0:255] suffers from floating precision issues.
precision highp float;
Doesn't make a difference. I also turned filtering off (and no mipmaps).
This would work:
float a = ceil(texture(colorTexture, texCoord).x * 255);
Though in general that doesn't like very robust as a solution (why would ceil work and not floor for example, why is the value 1.999999 rather than 2.00001 and can I be sure it will always be that way?). People must have done that before so I am sure there's a much better way to guaranteeing you get an accurate result without doing too much fiddling with the numbers. Any hints would be greatly appreciated.
EDIT
As pointed in 2 comments, it's right from the way floating point numbers are encoded that you can't get a guarantee that you will get a "integer" number back even if the number is even (that's good to be reminded of this important point). So I reformulate my question which is then, is there a preferred way in GLSL to clamp number to its closest integer values?
And that would be round:
float a = round(texture(colorTexture, texCoord).x * 255);
Hope this can help other people in the future though.

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

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.

JPEG: YCrCb <-> RGB conversion precision

I've implemented rgb->ycrcb and ycrcb->rgb conversion using JPEG conversion formulae from
http://www.w3.org/Graphics/JPEG/jfif3.pdf
(the same at: http://en.wikipedia.org/wiki/YCbCr (JPEG conversion)).
When checking whether results are correct (original->YCrCb->RGB), some of pixels differ by one, e.g 201->200.
Average percent of precision errors is 0.1%, so it's not critical.
/// converts RGB pixel to YCrCb using { en.wikipedia.org/wiki/YCbCr: JPEG conversion }
ivect4 rgb2ycrcb(int r, int g, int b)
{
int y = round(0.299*r + 0.587*g + 0.114*b) ;
int cb = round(128.0 - (0.1687*r) - (0.3313*g) + (0.5*b));
int cr = round(128.0 + (0.5*r) - (0.4187*g) - (0.0813*b));
return ivect4(y, cr, cb, 255);
}
/// converts YCrCb pixel to RGB using { en.wikipedia.org/wiki/YCbCr: JPEG conversion }
ivect4 ycrcb2rgb(int y, int cr, int cb)
{
int r = round(1.402*(cr-128) + y);
int g = round(-0.34414*(cb-128)-0.71414*(cr-128) + y);
int b = round(1.772*(cb-128) + y);
return ivect4(r, g, b, 255);
}
I use round formula:
floor((x) + 0.5)
When using other types of rounding, e.g. float(int), or std::ceil(), results are even worse.
So, does there exist the way to do YCrCb <-> RGB conversion without loss in precision?
The problem isn't rounding modes.
Even if you converted your floating point constants to ratios and used only integer math, you'd still see different values after the inverse.
To see why, consider a function where I tell you I'm going to shift the numbers 0 through N to the range 0 through N-2. The fact is that this transform is just doesn't have an inverse. You can represent it more or less exactly with a floating point computation (f(x) = x*(N-2)/N), but some of the neighboring values will map to the same result in integer math (pigeonhole principle!). This is a simplification and "compresses" the range, but the same thing happens in arbitrary affine transforms like this one you are using.
If you had r, g, b in floating point, and kept it that way until you quantized to integer, that would be a different story - but in integers you will necessarily always see some difference between the original and the inverse.
Only about 60% of all RGB values can be represented in YCbCr space when using the same amount of bits for both triplets. This means the most damage happens in RGB->YCbCr when you take a 3*8 bit RGB triplet, convert and round it back to 3*8 bits of precision. The trick is to store the YCbCr triplet at a higher precision until it's time to do forward DCT. There, the data needs to be scaled up anyway, so you can do e.g. 16 bit * 16 bit -> MSB16 multiplies, which are well supported by various SIMD instruction sets.
At the decoder it's the reverse: The results of inverse DCT have to be stored at higher precision until it's time to do the YCbCr->RGB conversion.
This doesn't make the process lossless, but for JPEG, it may buy a few dB of PSNR at the extreme high end of the quality scale, i.e. where the difference can't be seen with a naked eye but can be measured.
Yes, supposedly JPEG XR defines a color conversion that is reversible. The code is open source if you want to investigate in depth how they're doing it. The method is loosely described on the Wiki-page I linked to.
Also this SO post might give you some insights.
Another problem is that there is not a 1 to 1 mapping between rgb and YCbCR. There are YCbCr values with no corresponding RGB value and RBG values with no corresponding YCbCR values.

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);}
}

How do you implement a calculated Gaussian kernel?

I am struggling with my ability to implement a calculated gaussian kernel to return a blurred image.
My current code that calculates the kernel is below:
const int m = 5;
const int n = 5;
double sigma = std;
Mat Gauss;
double kernel[m][n];
for ( int x = 0; x < m; ++x )
for ( int y = 0; y < n; ++y )
{
kernel[x][y] = (1 / (sigma * (sqrt(2 * M_PI))))
* exp(-0.5 * (std::pow((x - avg) / sigma, 2.0)
+ pow((y - avg) / sigma, 2.0) ) / (2 * M_PI * sigma * sigma));
}
However, I can't figure out how to apply this to the image in a way that I am returned a blurred image.
I would appreciate it if anyone could give me some pointers in a way that I can apply this to an image.
I was thinking of using a for loop to replace the pixels of the original image but I could not properly implement this idea.
Thank you for your time.
It sounds like you want to compute a convolution of the original image with a Gaussian kernel, something like this:
blurred[x][y] = Integral (kernel[s][t] * original[x-s][y-t]) ds dt
There are a number of techniques for that:
Direct convolution: go through the grid and compute the above integral at each point. This works well for kernels with very small support, on the order of 5 grid points in each direction, but for kernels with larger support becomes too slow. For Gaussian kernels a rule of thumb for truncating support is about 3*sigma, so it's not unreasonable to do direct convolution with sigma under 2 grid points.
Fast Fourier Transform (FFT). This works reasonable fast for any kernel. Therefore FFT became the standard way to compute convolution of nearly anything with nearly anything. Direct convolution beats FFT only for kernel with very small support.
Analytical: integrals of some kernels have analytical expressions. In particular, integral of a Gaussian is the Erf function, and, at least on Unix systems, it's available as a function call. Moreover, on some hardware (such as GPUs) Erf is implemented in hardware. In some rare (but important) cases of coarse bi-level images one can replace convolution with Gaussian with a loop of Erf function calls.
For most computational system your best bet would be to go with FFT: it's fast and it's flexible enough to handle correctly any kernels and images.