From c06f1c4cc39bbebe13fe8e42a9278a55b5d0a216 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sun, 22 Nov 2015 18:34:26 +0100 Subject: [PATCH] Add a deinterlacer based on YADIF. I tried a few different things before I finally settled on this, in particular Weston's 3-field deinterlacer (w3fdif). It's not perfect (see .h comments), but it works overall pretty well. --- Makefile.in | 1 + README | 7 +- deinterlace_effect.cpp | 128 +++++++++++++++++++++ deinterlace_effect.frag | 215 ++++++++++++++++++++++++++++++++++++ deinterlace_effect.h | 119 ++++++++++++++++++++ deinterlace_effect_test.cpp | 196 ++++++++++++++++++++++++++++++++ 6 files changed, 663 insertions(+), 3 deletions(-) create mode 100644 deinterlace_effect.cpp create mode 100644 deinterlace_effect.frag create mode 100644 deinterlace_effect.h create mode 100644 deinterlace_effect_test.cpp diff --git a/Makefile.in b/Makefile.in index f15fabf..342bcfd 100644 --- a/Makefile.in +++ b/Makefile.in @@ -77,6 +77,7 @@ TESTED_EFFECTS += complex_modulate_effect TESTED_EFFECTS += luma_mix_effect TESTED_EFFECTS += fft_convolution_effect TESTED_EFFECTS += ycbcr_conversion_effect +TESTED_EFFECTS += deinterlace_effect UNTESTED_EFFECTS = sandbox_effect UNTESTED_EFFECTS += mirror_effect diff --git a/README b/README index 05ba9f3..62c99d0 100644 --- a/README +++ b/README @@ -45,10 +45,10 @@ Blur, diffusion, FFT-based convolution, glow, lift/gamma/gain (color correction), mirror, mix (add two inputs), luma mix (use a map to wipe between two inputs), overlay (the Porter-Duff “over” operation), scale (bilinear and Lanczos), sharpen (both by unsharp mask and by Wiener filters), saturation -(or desaturation), vignette, and white balance. +(or desaturation), vignette, white balance, and a deinterlacer (YADIF). Yes, that's a short list. But they all look great, are fast and don't give -you any nasty surprises. (I'd love to include denoise, deinterlace and +you any nasty surprises. (I'd love to include denoise and framerate up-/downconversion to the list, but doing them well are all research-grade problems, and Movit is currently not there.) @@ -102,7 +102,8 @@ While from a programming standpoint I'd love to say that it's 2015 and interlacing does no longer exist, but that's not true (and interlacing, hated as it might be, is actually a useful and underrated technique for bandwidth reduction in broadcast video). Movit will eventually provide -limited support for working with interlaced video, but currently does not. +limited support for working with interlaced video; it has a deinterlacer, +but cannot currently process video in interlaced form. What do you mean by “high-performance”? diff --git a/deinterlace_effect.cpp b/deinterlace_effect.cpp new file mode 100644 index 0000000..8d9a96c --- /dev/null +++ b/deinterlace_effect.cpp @@ -0,0 +1,128 @@ +#include + +#include "deinterlace_effect.h" +#include "util.h" + +using namespace std; + +namespace movit { + +DeinterlaceEffect::DeinterlaceEffect() + : enable_spatial_interlacing_check(true), + current_field_position(TOP), + num_lines(1080) +{ + register_int("enable_spatial_interlacing_check", (int *)&enable_spatial_interlacing_check); + register_int("current_field_position", (int *)¤t_field_position); + register_uniform_float("num_lines", &num_lines); + register_uniform_float("inv_width", &inv_width); + register_uniform_float("self_offset", &self_offset); + register_uniform_float_array("current_offset", current_offset, 2); + register_uniform_float_array("other_offset", other_offset, 3); +} + +string DeinterlaceEffect::output_fragment_shader() +{ + char buf[256]; + snprintf(buf, sizeof(buf), "#define YADIF_ENABLE_SPATIAL_INTERLACING_CHECK %d\n", + enable_spatial_interlacing_check); + string frag_shader = buf; + + frag_shader += read_file("deinterlace_effect.frag"); + return frag_shader; +} + +void DeinterlaceEffect::inform_input_size(unsigned input_num, unsigned width, unsigned height) +{ + assert(input_num >= 0 && input_num < 5); + widths[input_num] = width; + heights[input_num] = height; + num_lines = height * 2; +} + +void DeinterlaceEffect::get_output_size(unsigned *width, unsigned *height, + unsigned *virtual_width, unsigned *virtual_height) const +{ + assert(widths[0] == widths[1]); + assert(widths[1] == widths[2]); + assert(widths[2] == widths[3]); + assert(widths[3] == widths[4]); + assert(heights[0] == heights[1]); + assert(heights[1] == heights[2]); + assert(heights[2] == heights[3]); + assert(heights[3] == heights[4]); + *width = *virtual_width = widths[0]; + *height = *virtual_height = heights[0] * 2; +} + +void DeinterlaceEffect::set_gl_state(GLuint glsl_program_num, const string &prefix, unsigned *sampler_num) +{ + Effect::set_gl_state(glsl_program_num, prefix, sampler_num); + + inv_width = 1.0 / widths[0]; + + // Texel centers: t = output texel center for top field, b = for bottom field, + // x = the input texel. (The same area is two pixels for output, one for input; + // thus the stippled line in the middle.) + // + // +---------+ + // | | + // | t | + // | | + // | - -x- - | + // | | + // | b | + // | | + // +---------+ + // + // Note as usual OpenGL's bottom-left convention. + if (current_field_position == 0) { + // Top. + self_offset = -0.5 / num_lines; + } else { + // Bottom. + assert(current_field_position == 1); + self_offset = 0.5 / num_lines; + } + + // Having now established where the texels lie for the uninterpolated samples, + // we can use that to figure out where to sample for the interpolation. Drawing + // the fields as what lines they represent, here for three-pixel high fields + // with current_field_position == 0 (plus an “o” to mark the pixel we're trying + // to interpolate, and “c” for corresponding texel in the other field): + // + // Prev Cur Next + // x + // x x + // x + // c o c + // x + // x x + // + // Obviously, for sampling in the current field, we are one half-texel off + // compared to , so sampling in the current field is easy: + current_offset[0] = self_offset - 0.5 / heights[0]; + current_offset[1] = self_offset + 0.5 / heights[0]; + + // Now to find the texel in the other fields corresponding to the pixel + // we're trying to interpolate, let's realign the diagram above: + // + // Prev Cur Next + // x x x + // + // c x c + // o + // x x x + // + // So obviously for this case, we need to center on the same place as + // current_offset[1] (the texel directly above the o; note again the + // bottom-left convention). For the case of current_field_position == 1, + // the shift in the alignment goes the other way, and what we want + // is current_offset[0] (the texel directly below the o). + float center_offset = current_offset[1 - current_field_position]; + other_offset[0] = center_offset - 1.0 / heights[0]; + other_offset[1] = center_offset; + other_offset[2] = center_offset + 1.0 / heights[0]; +} + +} // namespace movit diff --git a/deinterlace_effect.frag b/deinterlace_effect.frag new file mode 100644 index 0000000..140f420 --- /dev/null +++ b/deinterlace_effect.frag @@ -0,0 +1,215 @@ +// Implicit uniforms: +// uniform int PREFIX(current_field_position); +// uniform float PREFIX(num_lines); +// uniform float PREFIX(self_offset); +// uniform float PREFIX(inv_width); +// uniform float PREFIX(current_offset)[2]; +// uniform float PREFIX(other_offset)[3]; + +// The best explanation of YADIF that I've seen is actually a pseudocode +// reimplementation from the Doom9 forum: +// +// http://forum.doom9.org/showthread.php?p=980375#post980375 +// +// We generally follow its terminology instead of the original C source +// (which I'll refer to as “C YADIF”), although I've used the C source as a +// reference to double-check at times. We're not bit-exact the same as +// C YADIF; in particular, we work in linear light, and left/right edge +// handling might also be a bit different (for top/bottom edge handling, +// C YADIF repeats texels like we do). Also, C YADIF generally works on +// Y', Cb and Cr planes separately, while we work on the entire RGBA triplet +// and do our spatial interpolation decisions based on the pixel as a whole, +// so our decision metric also naturally becomes different. + +#define DIFF(s1, s2) dot((s1) - (s2), (s1) - (s2)) + +vec4 FUNCNAME(vec2 tc) { + int yi = int(round(tc.y * PREFIX(num_lines) - 0.5f)); + + // Figure out if we just want to keep the current line or if + // we need to interpolate. This branch is obviously divergent, + // but the very nature of deinterlacing would seem to require that. + // + // Note that since we have bottom-left origin, yi % 2 will return 0 + // for bottom and 1 for top. + if ((yi % 2) != PREFIX(current_field_position)) { + return INPUT3(vec2(tc.x, tc.y + PREFIX(self_offset))); + } + + // First, estimate the current pixel from the neighboring pixels in the + // same field (spatial interpolation). We try first 0 degrees (straight + // up/down), then ±45 degrees and then finally ±63 degrees. The best of + // these, as determined by the “spatial score” (basically sum of squared + // differences in three neighboring pixels), is kept. + // + // The C version of YADIF goesn't check +63° unless +45° gave an improvement, + // and similarly not -63° unless -45° did. The MMX version goes through pains + // to simulate the same, but notes that it “hurts both quality and speed”. + // We're not bit-exact the same as the C version anyway, and not sampling + // ±63° would probably be a rather divergent branch, so we just always do it. + + // a b c d e f g ↑ y + // x | + // h i j k l m n +--> x + + vec2 a_pos = vec2(tc.x - 3.0 * PREFIX(inv_width), tc.y + PREFIX(current_offset)[1]); + vec2 b_pos = vec2(tc.x - 2.0 * PREFIX(inv_width), a_pos.y); + vec2 c_pos = vec2(tc.x - PREFIX(inv_width), a_pos.y); + vec2 d_pos = vec2(tc.x, a_pos.y); + vec2 e_pos = vec2(tc.x + PREFIX(inv_width), a_pos.y); + vec2 f_pos = vec2(tc.x + 2.0 * PREFIX(inv_width), a_pos.y); + vec2 g_pos = vec2(tc.x + 3.0 * PREFIX(inv_width), a_pos.y); + + vec2 h_pos = vec2(tc.x - 3.0 * PREFIX(inv_width), tc.y + PREFIX(current_offset)[0]); + vec2 i_pos = vec2(tc.x - 2.0 * PREFIX(inv_width), h_pos.y); + vec2 j_pos = vec2(tc.x - PREFIX(inv_width), h_pos.y); + vec2 k_pos = vec2(tc.x, h_pos.y); + vec2 l_pos = vec2(tc.x + PREFIX(inv_width), h_pos.y); + vec2 m_pos = vec2(tc.x + 2.0 * PREFIX(inv_width), h_pos.y); + vec2 n_pos = vec2(tc.x + 3.0 * PREFIX(inv_width), h_pos.y); + + vec4 a = INPUT3(a_pos); + vec4 b = INPUT3(b_pos); + vec4 c = INPUT3(c_pos); + vec4 d = INPUT3(d_pos); + vec4 e = INPUT3(e_pos); + vec4 f = INPUT3(f_pos); + vec4 g = INPUT3(g_pos); + vec4 h = INPUT3(h_pos); + vec4 i = INPUT3(i_pos); + vec4 j = INPUT3(j_pos); + vec4 k = INPUT3(k_pos); + vec4 l = INPUT3(l_pos); + vec4 m = INPUT3(m_pos); + vec4 n = INPUT3(n_pos); + + // 0 degrees. Note that pred is actually twice the real spatial prediction; + // we halve it later to same some arithmetic. Also, our spatial score is not + // the same as in C YADIF; we use the total squared sum over all four + // channels instead of deinterlacing each channel separately. + // + // Note that there's a small, arbitrary bonus for this first alternative, + // so that vertical interpolation wins if everything else is equal. + vec4 pred = d + k; + float score; + float best_score = DIFF(c, j) + DIFF(d, k) + DIFF(e, l) - 1e-4; + + // -45 degrees. + score = DIFF(b, k) + DIFF(c, l) + DIFF(d, m); + if (score < best_score) { + pred = c + l; + best_score = score; + } + + // -63 degrees. + score = DIFF(a, l) + DIFF(b, m) + DIFF(c, n); + if (score < best_score) { + pred = b + m; + best_score = score; + } + + // +45 degrees. + score = DIFF(d, i) + DIFF(e, j) + DIFF(f, k); + if (score < best_score) { + pred = e + j; + best_score = score; + } + + // +63 degrees. + score = DIFF(e, h) + DIFF(f, i) + DIFF(g, j); + if (score < best_score) { + pred = f + i; + // best_score isn't used anymore. + } + + pred *= 0.5f; + + // Now we do a temporal prediction (p2) of this pixel based on the previous + // and next fields. The spatial prediction is clamped so that it is not + // too far from this temporal prediction, where “too far” is based on + // the amount of local temporal change. (In other words, the temporal prediction + // is the safe choice, and the question is how far away from that we'll let + // our spatial choice run.) Note that here, our difference metric + // _is_ the same as C YADIF, namely per-channel abs. + // + // The sample positions look like this; in order to avoid variable name conflicts + // with the spatial interpolation, we use uppercase names. x is, again, + // the current pixel we're trying to estimate. + // + // C H ↑ y + // A F K | + // D x I | + // B G L | + // E J +-----> time + // + vec2 AFK_pos = d_pos; + vec2 BGL_pos = k_pos; + vec4 A = INPUT1(AFK_pos); + vec4 B = INPUT1(BGL_pos); + vec4 F = d; + vec4 G = k; + vec4 K = INPUT5(AFK_pos); + vec4 L = INPUT5(BGL_pos); + + vec2 CH_pos = vec2(tc.x, tc.y + PREFIX(other_offset)[2]); + vec2 DI_pos = vec2(tc.x, tc.y + PREFIX(other_offset)[1]); + vec2 EJ_pos = vec2(tc.x, tc.y + PREFIX(other_offset)[0]); + + vec4 C = INPUT2(CH_pos); + vec4 D = INPUT2(DI_pos); + vec4 E = INPUT2(EJ_pos); + + vec4 H = INPUT4(CH_pos); + vec4 I = INPUT4(DI_pos); + vec4 J = INPUT4(EJ_pos); + + // Find temporal differences around this line, using all five fields. + // tdiff0 is around the current field, tdiff1 is around the previous one, + // tdiff2 is around the next one. + vec4 tdiff0 = abs(D - I); + vec4 tdiff1 = abs(A - F) + abs(B - G); // Actually twice tdiff1. + vec4 tdiff2 = abs(K - F) + abs(L - G); // Actually twice tdiff2. + vec4 diff = max(tdiff0, 0.5f * max(tdiff1, tdiff2)); + + // The following part is the spatial interlacing check, which loosens up the + // allowable temporal change. (See also the comments in the .h file.) + // It costs us four extra loads (C, E, H, J) and a few extra ALU ops; + // we're already very load-heavy, so the extra ALU is effectively free. + // It costs about 18% performance in some benchmarks, which squares + // well with going from 20 to 24 loads (a 20% increase), although for + // total overall performance in longer chains, the difference is nearly zero. + // + // The basic idea is seemingly to allow more change if there are large spatial + // vertical changes, even if there are few temporal changes. These differences + // are signed, though, which make it more tricky to follow, although they seem + // to reduce into some sort of pseudo-abs. I will not claim to understand them + // very well. + // + // We start by temporally interpolating the current vertical line (p0–p4): + // + // C p0 H ↑ y + // A p1 K | + // D p2 I | + // B p3 L | + // E p4 J +-----> time + // + // YADIF_ENABLE_SPATIAL_INTERLACING_CHECK will be #defined to 1 + // if the check is enabled. Otherwise, the compiler should + // be able to remove the dependent code quite easily. + vec4 p0 = 0.5f * (C + H); + vec4 p1 = F; + vec4 p2 = 0.5f * (D + I); + vec4 p3 = G; + vec4 p4 = 0.5f * (E + J); + +#if YADIF_ENABLE_SPATIAL_INTERLACING_CHECK + vec4 max_ = max(max(p2 - p3, p2 - p1), min(p0 - p1, p4 - p3)); + vec4 min_ = min(min(p2 - p3, p2 - p1), max(p0 - p1, p4 - p3)); + diff = max(diff, max(min_, -max_)); +#endif + + return clamp(pred, p2 - diff, p2 + diff); +} + +#undef DIFF +#undef YADIF_ENABLE_SPATIAL_INTERLACING_CHECK diff --git a/deinterlace_effect.h b/deinterlace_effect.h new file mode 100644 index 0000000..7935322 --- /dev/null +++ b/deinterlace_effect.h @@ -0,0 +1,119 @@ +#ifndef _MOVIT_DEINTERLACE_EFFECT_H +#define _MOVIT_DEINTERLACE_EFFECT_H 1 + +// YADIF deinterlacing filter (original by Michael Niedermayer, in MPlayer). +// +// Good deinterlacing is very hard. YADIF, despite its innocious-sounding +// name (Yet Another DeInterlacing Filter) is probably the most commonly +// used (non-trivial) deinterlacing filter in the open-source world. +// It works by trying to fill in the missing lines from neighboring ones +// (spatial interpolation), and then constrains that estimate within an +// interval found from previous and next frames (temporal interpolation). +// It's not very fast, even in GPU implementation, but 1080i60 -> 1080p60 +// realtime conversion is well within range for a mid-range GPU. +// +// The inner workings of YADIF are poorly documented; implementation details +// are generally explained the .frag file. However, a few things should be +// mentioned here: YADIF has two modes, with and without a “spatial interlacing +// check” which basically allows more temporal change in areas of high detail. +// (The variant with the check corresponds to the original's modes 0 and 1, and +// the variant without to modes 2 and 3. The remaining difference is whether it +// is frame-doubling or not, which in Movit is up to the driver, not the +// filter.) +// +// Neither mode is perfect by any means. If the spatial check is off, the +// filter possesses the potentially nice quality that a static picture +// deinterlaces exactly to itself. (If it's on, there's some flickering +// on very fine vertical detail. The picture is nice and stable if no such +// detail is present, though.) But then, certain patterns, like horizontally +// scrolling text, leaves residues. Both have issues with diagonal lines at +// certain angles leaving stray pixels, although in practical applications, +// YADIF is pretty good. +// +// In general, having the spatial check on (the default) is the safe choice. +// However, if you are reasonably certain that the image comes from a video source +// (ie., no graphical overlays), or if the case of still images is particularly +// important for you (e.g., slides from a laptop), you could turn it off. +// It is slightly faster, although in practice, it does not mean all that much. +// You need to decide before finalize(), as the choice gets compiled into the shader. +// +// YADIF needs five fields as input; the previous two, the current one, and +// then the two next ones. (By convention, they come in that order, although if +// you reverse them, it doesn't matter, as the filter is symmetric. It _does_ +// matter if you change the ordering in any other way, though.) They need to be +// of the same resolution, or the effect will assert-fail. If you cannot supply +// this, you could simply reuse the current field for previous/next as +// required; it won't be optimal in any way, but it also won't blow up on you. +// +// This requirement to “see the future” will mean you have an extra full frame +// of delay (33.3 ms at 60i, 40 ms at 50i). You will also need to tell the +// filter for each and every invocation if the current field (ie., the one in +// the middle input) is a top or bottom field (neighboring fields have opposite +// parity, so all the others are implicit). + +#include +#include + +#include "effect.h" + +namespace movit { + +class DeinterlaceEffect : public Effect { +public: + DeinterlaceEffect(); + virtual std::string effect_type_id() const { return "DeinterlaceEffect"; } + std::string output_fragment_shader(); + + void set_gl_state(GLuint glsl_program_num, const std::string &prefix, unsigned *sampler_num); + + // First = before previous, second = previous, third = current, + // fourth = next, fifth = after next. These are treated symmetrically, + // though. + // + // Note that if you have interlaced _frames_ and not _fields_, you will + // need to pull them apart first, for instance with SliceEffect. + virtual unsigned num_inputs() const { return 5; } + virtual bool needs_texture_bounce() const { return true; } + virtual bool changes_output_size() const { return true; } + + virtual AlphaHandling alpha_handling() const { return INPUT_PREMULTIPLIED_ALPHA_KEEP_BLANK; } + + virtual void inform_input_size(unsigned input_num, unsigned width, unsigned height); + virtual void get_output_size(unsigned *width, unsigned *height, + unsigned *virtual_width, unsigned *virtual_height) const; + + enum FieldPosition { TOP = 0, BOTTOM = 1 }; + +private: + unsigned widths[5], heights[5]; + + // See file-level comment for explanation of this option. + bool enable_spatial_interlacing_check; + + // Which field the current input (the middle one) is. + FieldPosition current_field_position; + + // Offset for one pixel in the horizontal direction (1/width). + float inv_width; + + // Vertical resolution of the output. + float num_lines; + + // All of these offsets are vertical texel offsets; they are needed to adjust + // for the changed texel center as the number of lines double, and depend on + // . + + // For sampling unchanged lines from the current field. + float self_offset; + + // For evaluating the low-pass filter (in the current field). Four taps. + float current_offset[2]; + + // For evaluating the high-pass filter (in the previous and next fields). + // Five taps, but evaluated twice since there are two fields. + float other_offset[3]; +}; + +} // namespace movit + +#endif // !defined(_MOVIT_DEINTERLACE_EFFECT_H) diff --git a/deinterlace_effect_test.cpp b/deinterlace_effect_test.cpp new file mode 100644 index 0000000..31bd363 --- /dev/null +++ b/deinterlace_effect_test.cpp @@ -0,0 +1,196 @@ +// Unit tests for DeinterlaceEffect. + +#include + +#include + +#include "effect_chain.h" +#include "gtest/gtest.h" +#include "image_format.h" +#include "input.h" +#include "deinterlace_effect.h" +#include "test_util.h" + +using namespace std; + +namespace movit { + +TEST(DeinterlaceTest, ConstantColor) { + float data[] = { + 0.3f, 0.3f, + 0.3f, 0.3f, + 0.3f, 0.3f, + }; + float expected_data[] = { + 0.3f, 0.3f, + 0.3f, 0.3f, + 0.3f, 0.3f, + 0.3f, 0.3f, + 0.3f, 0.3f, + 0.3f, 0.3f, + }; + float out_data[12]; + EffectChainTester tester(NULL, 2, 6); + Effect *input1 = tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 2, 3); + Effect *input2 = tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 2, 3); + Effect *input3 = tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 2, 3); + Effect *input4 = tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 2, 3); + Effect *input5 = tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, 2, 3); + Effect *deinterlace_effect = tester.get_chain()->add_effect(new DeinterlaceEffect(), input1, input2, input3, input4, input5); + + ASSERT_TRUE(deinterlace_effect->set_int("current_field_position", 0)); + tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); + expect_equal(expected_data, out_data, 2, 6); + + ASSERT_TRUE(deinterlace_effect->set_int("current_field_position", 1)); + tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); + expect_equal(expected_data, out_data, 2, 6); +} + +// Also tests that top/bottom change works like expected. +TEST(DeinterlaceTest, VerticalInterpolation) { + const int width = 11; + const int height = 2; + float data[width * height] = { + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.2f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, // Differs from previous. + }; + float expected_data_top[width * height * 2] = { + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.2f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, // Unchanged. + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.3f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, // Unchanged. + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, // Repeated. + }; + float expected_data_bottom[width * height * 2] = { + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.2f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, // Repeated + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.2f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, // Unchanged. + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.3f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, // Unchanged. + }; + float neg_blowout_data[width * height]; + float pos_blowout_data[width * height]; + float out_data[width * height * 2]; + + // Set previous and next fields to something so big that all the temporal checks + // are effectively turned off. + fill(neg_blowout_data, neg_blowout_data + width * height, -100.0f); + fill(neg_blowout_data, pos_blowout_data + width * height, 100.0f); + + EffectChainTester tester(NULL, width, height * 2); + Effect *input1 = tester.add_input(neg_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input2 = tester.add_input(neg_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input3 = tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input4 = tester.add_input(pos_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input5 = tester.add_input(pos_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *deinterlace_effect = tester.get_chain()->add_effect(new DeinterlaceEffect(), input1, input2, input3, input4, input5); + + ASSERT_TRUE(deinterlace_effect->set_int("current_field_position", 0)); + tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); + expect_equal(expected_data_top, out_data, width, height * 2); + + ASSERT_TRUE(deinterlace_effect->set_int("current_field_position", 1)); + tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR); + expect_equal(expected_data_bottom, out_data, width, height * 2); +} + +TEST(DeinterlaceTest, DiagonalInterpolation) { + const int width = 11; + const int height = 3; + float data[width * height] = { + 0.0f, 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.2f, 0.6f, 0.8f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, 0.0f, // Offset two pixels, one value modified. + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, // Offset four the other way. + }; + + // Expected degrees are marked in comments. Mostly we want +45 for the second line + // and -63 for the fourth, but due to the score being over three neighboring pixels, + // sometimes it doesn't work ideally like that. + float expected_data_top[width * height * 2] = { + 0.0f, 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.2f, 0.6f, 0.8f, 0.0f, 0.0f, // Unchanged. + // | / / / / / / / / / | + // 0 +45 +45 +45 +45 +45 +45 +45 +45 +45 0 + 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.3f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, + // | / / / / / / / / / | + 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, 0.0f, 0.0f, 0.0f, 0.0f, // Unchanged. + + // 0 -45 -63 -63 -63 -63 -63 -63 +63! +63! +63! + 0.0f, 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.2f, 0.3f, 0.2f, + + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, // Unchanged. + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.4f, 0.6f, 0.4f, 0.6f, 0.8f, // Repeated. + }; + float neg_blowout_data[width * height]; + float pos_blowout_data[width * height]; + float out_data[width * height * 2]; + + // Set previous and next fields to something so big that all the temporal checks + // are effectively turned off. + fill(neg_blowout_data, neg_blowout_data + width * height, -100.0f); + fill(pos_blowout_data, pos_blowout_data + width * height, 100.0f); + + EffectChainTester tester(NULL, width, height * 2); + Effect *input1 = tester.add_input(neg_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input2 = tester.add_input(neg_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input3 = tester.add_input(data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input4 = tester.add_input(pos_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *input5 = tester.add_input(pos_blowout_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *deinterlace_effect = tester.get_chain()->add_effect(new DeinterlaceEffect(), input1, input2, input3, input4, input5); + + ASSERT_TRUE(deinterlace_effect->set_int("current_field_position", 0)); + tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED); + expect_equal(expected_data_top, out_data, width, height * 2); +} + +TEST(DeinterlaceTest, FlickerBox) { + const int width = 4; + const int height = 4; + float white_data[width * height] = { + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + }; + float black_data[width * height] = { + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + }; + float striped_data[width * height * 2] = { + 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + }; + float out_data[width * height * 2]; + + { + EffectChainTester tester(NULL, width, height * 2); + Effect *white_input = tester.add_input(white_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *black_input = tester.add_input(black_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *deinterlace_effect = tester.get_chain()->add_effect(new DeinterlaceEffect(), white_input, black_input, white_input, black_input, white_input); + + ASSERT_TRUE(deinterlace_effect->set_int("current_field_position", 0)); + tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED); + expect_equal(white_data, out_data, width, height); + expect_equal(white_data, out_data + width * height, width, height); + } + + { + EffectChainTester tester(NULL, width, height * 2); + Effect *white_input = tester.add_input(white_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *black_input = tester.add_input(black_data, FORMAT_GRAYSCALE, COLORSPACE_sRGB, GAMMA_LINEAR, width, height); + Effect *deinterlace_effect = tester.get_chain()->add_effect(new DeinterlaceEffect(), white_input, black_input, white_input, black_input, white_input); + + ASSERT_TRUE(deinterlace_effect->set_int("enable_spatial_interlacing_check", 0)); + ASSERT_TRUE(deinterlace_effect->set_int("current_field_position", 0)); + tester.run(out_data, GL_RED, COLORSPACE_sRGB, GAMMA_LINEAR, OUTPUT_ALPHA_FORMAT_PREMULTIPLIED); + expect_equal(striped_data, out_data, width, height * 2); + } +} + +} // namespace movit -- 2.39.2