内容简介:This is a brain-dump inspired by a thread on twitter about correct™ dither inSo, this topic came up on twitter:I had previously spent some time to wrap my head around this exact problem, so I shot from the hip with some pseudo code that I used in
This is a brain-dump inspired by a thread on twitter about correct™ dither in sRGB , meaning, to choose the dither pattern in such a way as to preserve the physical brightness of the original pixels. This is in principle a solved problem, but the devil is in the details that are easily overlooked, especially when dithering to only a few quantization levels.
So, this topic came up on twitter:
I had previously spent some time to wrap my head around this exact problem, so I shot from the hip with some pseudo code that I used in Space Glider on Shadertoy. Code postings on twitter are never a good idea, so here is a cleaned up version wrapped up in a proper function:
vec3 oetf( vec3 ); // = pow( .4545 ) vec3 eotf( vec3 ); // = pow( 2.2 ) vec3 dither( vec3 linear_color, vec3 noise, float quant ) { vec3 c0 = floor( oetf( linear_color ) / quant ) * quant; vec3 c1 = c0 + quant; vec3 discr = mix( eotf( c0 ), eotf( c1 ), noise ); return mix( c0, c1, lessThan( discr, linear_color ) ); }
Contents
How the code works
The linear_color
is the value that is going to be written out to the render target. This is the value to be dithered and is supposed to be in linear RGB . The noise
argument can be any uniform noise in the range 0 to 1 (preferably some form of blue noise, or it could be an ordered Bayer-pattern). Lastly, the quant
argument is the quantization interval, which is “one over one minus the number of levels”; for example: 1/255 for quantization to 256 levels, or 51/255 so emulate the palette of web-safe colors (6 levels). The free function oetf
is used here to stand for an arbitrary opto-electronic transfer function , which for sRGB is nothing more than the good old gamma curve.
Here is how the dither
function works: It first computes the quantized lower and upper bounds, c0
and c1
, that bracket the input value. The output is then selected as either c0
or c1
based on a comparison of the input against a discriminant, discr
. The salient point is that this comparison is performed in linear space!
So why is it necessary to compute the discriminant in linear space? Because what matters is physical brightness, which is linear in the number of pixels (at least it should be, on a sane display), but it is not in general linear in the RGB value ifself.
Why the code works
To illustrate further lets continue with the web palette example where there are 6 quantization levels. The following table shows how these 6 levels should map to physical luminance, according to the sRGB-standard:
value PERCENT ) |
value (8 bit) |
Value HEX ) |
luminance (cD per sq meter) |
example |
---|---|---|---|---|
0% | 0 | #00 | 0 | |
20% | 51 | #33 | 3,31 | |
40% | 102 | #66 | 13,3 | |
60% | 153 | #99 | 31,9 | |
80% | 204 | # CC | 60,4 | |
100% | 255 | # FF | 80 |
The luminance values here were calculated by following the sRGB transfer function and under the assumption of the standard 80 cd/m² display brightness. Now consider for example that we want to match the luminance of the #33 grey value (3,31 cd/m²) with a dither pattern. According to table we should choose a 25% pattern when using the #66 pixels (3,31 into 13,3), a 10% pattern for the #99 pixels (3,31 into 31,9), a 5,4% pattern for the # CC pixels (3,31 into 60,4) or a 4,1% pattern for the # FF pixels (3,31 into 80). This has been realized in the following image:
All tiles in this image should appear approximately with the same brightness. They may not match perfectly on your display, but they should do at least ok. Make sure the image is viewed at its original size. To minimize resizing errors I have included a 2× version for retina displays that should get automatically selected on MacBooks and the like.
In contrast, using the raw RGB value as the basis for the dither pattern as shown above does not produce a matching appearance. In this case I used a 50% pattern with the #66 pixels (20 into 40), a 33% pattern with #99 pixels (20 into 60), a 25% pattern with # CC pixels (20 into 80) and a 20% pattern with # FF pixels (20 into 100). See for yourself how that does not match!
A real world example
As I said in the beginning, I came up with the above dithering code as a side effect of the continued tinkering with Space Glider, as I wanted to have a somewhat faithful rendition of twilight and night situations, and that means that without dithering, the sky gradient would produce very noticable banding, especially so in VR .
To illiustrate, a took a screenshot of a twilight scene, standing in the mountains with the landing lights on. The darkest pixel in this image is #020204, which is somewhere in the lower left corner. With a VR headset on, and with the eyes dark-adapted, the jumps between #02, #03 and #04 are clearly visible and proper dithering is a must.
I will now show how the code shown in the beginning is working as intended by dithering this image to 2, 3, 4, 6 and 8 quantization levels by simply chanching the quant
variable. Again, all images should match physical brightness impression (and again, on the condition that your browser does not mess with the pixels). The noise
input used here is just the shadertoy builtin blue noise texture, but 2 copies were added together at different scales to make it effectively one 16-bit noise texture.
Conclusion
So that’s it as this is only a quick reaction post. To recap, the dithering problem is complicated by the fact that display brightness in linear in the number of pixels, but non-linear in the RGB value. Getting it right matters for the lowest quantization levels, be it either the dark parts of an image with many quantization levels, or if there are only a few quantization levels overall.
以上所述就是小编给大家介绍的《Correct SRGB Dithering》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。