]> 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 += 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
 
 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
 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
 
 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.)
 
 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
 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”?
 
 
 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