]> git.sesse.net Git - movit/commitdiff
Add a deinterlacer based on YADIF.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 22 Nov 2015 17:34:26 +0000 (18:34 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 21 Dec 2015 21:58:05 +0000 (22:58 +0100)
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
README
deinterlace_effect.cpp [new file with mode: 0644]
deinterlace_effect.frag [new file with mode: 0644]
deinterlace_effect.h [new file with mode: 0644]
deinterlace_effect_test.cpp [new file with mode: 0644]

index f15fabf29ec6508223cbfbede591e4559ce5beda..342bcfdd835063e449a9828f25672c35c04d7561 100644 (file)
@@ -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 05ba9f3cd89f292cf9a10e1d8fd93a71b9f5fb00..62c99d0c936b9ec4ee8b5f6ae5a146c9217e8ee1 100644 (file)
--- 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 (file)
index 0000000..8d9a96c
--- /dev/null
@@ -0,0 +1,128 @@
+#include <epoxy/gl.h>
+
+#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 *)&current_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 <self_offset>, 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 (file)
index 0000000..140f420
--- /dev/null
@@ -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 (file)
index 0000000..7935322
--- /dev/null
@@ -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 <epoxy/gl.h>
+#include <string>
+
+#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
+       // <current_field_position>.
+
+       // 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 (file)
index 0000000..31bd363
--- /dev/null
@@ -0,0 +1,196 @@
+// Unit tests for DeinterlaceEffect.
+
+#include <epoxy/gl.h>
+
+#include <algorithm>
+
+#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