Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an implementation of RPDF dither on the final output.
- Loading branch information
Showing
9 changed files
with
273 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
#include <math.h> | ||
#include <assert.h> | ||
|
||
#include "dither_effect.h" | ||
#include "util.h" | ||
#include "opengl.h" | ||
|
||
namespace { | ||
|
||
// A simple LCG (linear congruental generator) random generator. | ||
// We implement our own so we can be deterministic from frame to frame | ||
// and run to run; we don't have special needs for speed or quality, | ||
// as long as the period is reasonably long. The output is in range | ||
// [0, 2^31>. | ||
// | ||
// This comes from http://en.wikipedia.org/wiki/Linear_congruential_generator. | ||
unsigned lcg_rand(unsigned x) | ||
{ | ||
return (x * 1103515245U + 12345U) & ((1U << 31) - 1); | ||
} | ||
|
||
} // namespace | ||
|
||
DitherEffect::DitherEffect() | ||
: width(1280), height(720), num_bits(8), | ||
last_width(-1), last_height(-1), last_num_bits(-1) | ||
{ | ||
register_int("output_width", &width); | ||
register_int("output_height", &height); | ||
register_int("num_bits", &num_bits); | ||
|
||
glGenTextures(1, &texnum); | ||
} | ||
|
||
DitherEffect::~DitherEffect() | ||
{ | ||
glDeleteTextures(1, &texnum); | ||
} | ||
|
||
std::string DitherEffect::output_fragment_shader() | ||
{ | ||
return read_file("dither_effect.frag"); | ||
} | ||
|
||
void DitherEffect::update_texture(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) | ||
{ | ||
float *dither_noise = new float[width * height]; | ||
float dither_double_amplitude = 1.0f / (1 << num_bits); | ||
|
||
// Using the resolution as a seed gives us a consistent dither from frame to frame. | ||
// It also gives a different dither for e.g. different aspect ratios, which _feels_ | ||
// good, but probably shouldn't matter. | ||
unsigned seed = (width << 16) ^ height; | ||
for (int i = 0; i < width * height; ++i) { | ||
seed = lcg_rand(seed); | ||
float normalized_rand = seed * (1.0f / (1U << 31)) - 0.5; // [-0.5, 0.5> | ||
dither_noise[i] = dither_double_amplitude * normalized_rand; | ||
} | ||
|
||
glActiveTexture(GL_TEXTURE0 + *sampler_num); | ||
check_error(); | ||
glBindTexture(GL_TEXTURE_2D, texnum); | ||
check_error(); | ||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | ||
check_error(); | ||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | ||
check_error(); | ||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | ||
check_error(); | ||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE16F_ARB, width, height, 0, GL_LUMINANCE, GL_FLOAT, dither_noise); | ||
check_error(); | ||
|
||
delete[] dither_noise; | ||
} | ||
|
||
void DitherEffect::set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num) | ||
{ | ||
Effect::set_gl_state(glsl_program_num, prefix, sampler_num); | ||
|
||
if (width != last_width || height != last_height || num_bits != last_num_bits) { | ||
update_texture(glsl_program_num, prefix, sampler_num); | ||
last_width = width; | ||
last_height = height; | ||
last_num_bits = num_bits; | ||
} | ||
|
||
glActiveTexture(GL_TEXTURE0 + *sampler_num); | ||
check_error(); | ||
glBindTexture(GL_TEXTURE_2D, texnum); | ||
check_error(); | ||
|
||
set_uniform_int(glsl_program_num, prefix, "dither_tex", *sampler_num); | ||
++sampler_num; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
uniform sampler2D PREFIX(dither_tex); | ||
|
||
vec4 FUNCNAME(vec2 tc) { | ||
// We also choose to dither alpha, just in case. | ||
// Maybe it should in theory have a separate dither, | ||
// but I doubt it matters much. We currently don't | ||
// really handle alpha in any case. | ||
return INPUT(tc) + texture2D(PREFIX(dither_tex), tc).xxxx; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
#ifndef _DITHER_EFFECT_H | ||
#define _DITHER_EFFECT_H 1 | ||
|
||
// Implements simple rectangular-PDF dither. | ||
// | ||
// Although all of our processing internally is in floating-point (a mix of 16- | ||
// and 32-bit), eventually most pipelines will end up downconverting to a fixed-point | ||
// format, typically 8-bits unsigned integer (GL_RGBA8). | ||
// | ||
// The hardware will typically do proper rounding for us, so that we minimize | ||
// quantization noise, but for some applications, if you look closely, you can still | ||
// see some banding; 8 bits is not really all that much (and if we didn't have the | ||
// perceptual gamma curve, it would be a lot worse). | ||
// | ||
// The standard solution to this is dithering; in short, to add a small random component | ||
// to each pixel before quantization. This increases the overall noise floor slightly, | ||
// but allows us to represent frequency components with an amplitude lower than 1/256. | ||
// | ||
// My standard reference on dither is: | ||
// | ||
// Cameron Nicklaus Christou: “Optimal Dither and Noise Shaping in Image Processing” | ||
// http://uwspace.uwaterloo.ca/bitstream/10012/3867/1/thesis.pdf | ||
// | ||
// However, we need to make two significant deviations from the recommendations it makes. | ||
// First of all, it recommends using a triangular-PDF (TPDF) dither (which can be synthesized | ||
// effectively by adding two uniformly distributed random numbers) instead of rectangular-PDF | ||
// (RPDF; using one uniformly distributed random number), in order to make the second moment | ||
// of the error signal independent from the original image. However, since the recommended | ||
// TPDF must be twice as wide as the RPDF, it means it can go to +/- 1, which means that | ||
// some of the time, it will add enough noise to change a pixel just by itself. Given that | ||
// a very common use case for us is converting 8-bit -> 8-bit (ie., no bit reduction at all), | ||
// it would seem like a more important goal to have no noise in that situation than to | ||
// improve the dither further. | ||
// | ||
// Second, the thesis recommends noise shaping (also known as error diffusion in the image | ||
// processing world). This is, however, very hard to implement properly on a GPU, since it | ||
// almost by definition feeds the value of output pixels into the neighboring input pixels. | ||
// Maybe one could make a version that implemented the noise shapers by way of FIR filters | ||
// instead of IIR like this, but it would seem a lot of work for very subtle gain. | ||
// | ||
// We keep the dither noise fixed as long as the output resolution doesn't change; | ||
// this ensures we don't upset video codecs too much. (One could also dither in time, | ||
// like many LCD monitors do, but it starts to get very hairy, again, for limited gains.) | ||
// The dither is also deterministic across runs. | ||
|
||
#include "effect.h" | ||
|
||
class DitherEffect : public Effect { | ||
public: | ||
DitherEffect(); | ||
~DitherEffect(); | ||
virtual std::string effect_type_id() const { return "DitherEffect"; } | ||
std::string output_fragment_shader(); | ||
|
||
void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num); | ||
|
||
private: | ||
void update_texture(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num); | ||
|
||
int width, height, num_bits; | ||
int last_width, last_height, last_num_bits; | ||
|
||
GLuint texnum; | ||
bool need_texture_update; | ||
}; | ||
|
||
#endif // !defined(_DITHER_EFFECT_H) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Unit tests for DitherEffect. | ||
|
||
#include <math.h> | ||
|
||
#include "test_util.h" | ||
#include "gtest/gtest.h" | ||
|
||
TEST(DitherEffectTest, NoDitherOnExactValues) { | ||
const int size = 4; | ||
|
||
float data[size * size] = { | ||
0.0, 1.0, 0.0, 1.0, | ||
0.0, 1.0, 1.0, 0.0, | ||
0.0, 0.2, 1.0, 0.2, | ||
0.0, 0.0, 0.0, 0.0, | ||
}; | ||
unsigned char expected_data[size * size] = { | ||
0, 255, 0, 255, | ||
0, 255, 255, 0, | ||
0, 51, 255, 51, | ||
0, 0, 0, 0, | ||
}; | ||
unsigned char out_data[size * size]; | ||
|
||
EffectChainTester tester(data, size, size, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8); | ||
tester.get_chain()->set_dither_bits(8); | ||
tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); | ||
|
||
expect_equal(expected_data, out_data, size, size); | ||
} | ||
|
||
TEST(DitherEffectTest, SinusoidBelowOneLevelComesThrough) { | ||
const float frequency = 0.3f * M_PI; | ||
const unsigned size = 2048; | ||
const float amplitude = 0.25f / 255.0f; // 6 dB below what can be represented without dithering. | ||
|
||
float data[size]; | ||
for (unsigned i = 0; i < size; ++i) { | ||
data[i] = 0.2 + amplitude * sin(i * frequency); | ||
} | ||
unsigned char out_data[size]; | ||
|
||
EffectChainTester tester(data, size, 1, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, GL_RGBA8); | ||
tester.get_chain()->set_dither_bits(8); | ||
tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); | ||
|
||
// Measure how strong the given sinusoid is in the output. | ||
float sum = 0.0f; | ||
for (unsigned i = 0; i < size; ++i) { | ||
sum += 2.0 * (int(out_data[i]) - 0.2*255.0) * sin(i * frequency); | ||
} | ||
|
||
EXPECT_NEAR(amplitude, sum / (size * 255.0f), 1e-5); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters