New way of checking for 2D content mislabelled as 3D (#1565).
authorCarl Hetherington <cth@carlh.net>
Wed, 19 Feb 2020 16:02:24 +0000 (16:02 +0000)
committerCarl Hetherington <cth@carlh.net>
Wed, 19 Feb 2020 16:02:27 +0000 (16:02 +0000)
Required because of the change to the way video frame timing
is done.

src/lib/dcp_decoder.cc
src/lib/ffmpeg_decoder.cc
src/lib/frame_interval_checker.cc [new file with mode: 0644]
src/lib/frame_interval_checker.h [new file with mode: 0644]
src/lib/image_decoder.cc
src/lib/video_decoder.cc
src/lib/video_decoder.h
src/lib/video_mxf_decoder.cc
src/lib/wscript
test/frame_interval_checker_test.cc [new file with mode: 0644]
test/wscript

index 7af89e84d074b3c791032b96327465068b645e11..e8ace8beedcfb61b2b0604103acd099c64e4b5d0 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
-    Copyright (C) 2014-2018 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2014-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
     This file is part of DCP-o-matic.
 
@@ -28,6 +28,7 @@
 #include "ffmpeg_image_proxy.h"
 #include "image.h"
 #include "config.h"
 #include "ffmpeg_image_proxy.h"
 #include "image.h"
 #include "config.h"
+#include "frame_interval_checker.h"
 #include <dcp/dcp.h>
 #include <dcp/cpl.h>
 #include <dcp/reel.h>
 #include <dcp/dcp.h>
 #include <dcp/cpl.h>
 #include <dcp/reel.h>
index fd248c39a67a0b319a3ddc0cb910ad2bc3d02920..bb852d9d849833a074841c149abac0bcc92a53ee 100644 (file)
@@ -42,6 +42,7 @@
 #include "compose.hpp"
 #include "text_content.h"
 #include "audio_content.h"
 #include "compose.hpp"
 #include "text_content.h"
 #include "audio_content.h"
+#include "frame_interval_checker.h"
 #include <dcp/subtitle_string.h>
 #include <sub/ssa_reader.h>
 #include <sub/subtitle.h>
 #include <dcp/subtitle_string.h>
 #include <sub/ssa_reader.h>
 #include <sub/subtitle.h>
diff --git a/src/lib/frame_interval_checker.cc b/src/lib/frame_interval_checker.cc
new file mode 100644 (file)
index 0000000..0569730
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+    Copyright (C) 2020 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+#include "frame_interval_checker.h"
+#include <iostream>
+
+int const FrameIntervalChecker::_frames = 16;
+
+void
+FrameIntervalChecker::feed (ContentTime time, double frame_rate)
+{
+       /* The caller isn't meant to feed too much data before
+          calling guess() and destroying the FrameIntervalChecker.
+        */
+       DCPOMATIC_ASSERT (_intervals.size() < _frames);
+
+       if (_last) {
+               _intervals.push_back ((time - *_last).seconds() * frame_rate);
+       }
+
+       _last = time;
+}
+
+FrameIntervalChecker::Guess
+FrameIntervalChecker::guess () const
+{
+       if (_intervals.size() < _frames) {
+               /* How soon can you land?
+                * I can't tell.
+                * You can tell me, I'm a doctor.
+                * Nom I mean I'm just not sure.
+                * Can't you take a guess?
+                * Well, not for another two hours.
+                * You can't take a guess for another two hours?
+                */
+               return AGAIN;
+       }
+
+       int near_1 = 0;
+       BOOST_FOREACH (double i, _intervals) {
+               if (i > 0.5) {
+                       ++near_1;
+               }
+       }
+
+       return (near_1 < 3 * _frames / 4) ? PROBABLY_3D : PROBABLY_NOT_3D;
+}
+
diff --git a/src/lib/frame_interval_checker.h b/src/lib/frame_interval_checker.h
new file mode 100644 (file)
index 0000000..14b715c
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    Copyright (C) 2020 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+#include "dcpomatic_time.h"
+#include <boost/optional.hpp>
+#include <boost/noncopyable.hpp>
+#include <vector>
+
+class FrameIntervalChecker : public boost::noncopyable
+{
+public:
+       void feed (ContentTime time, double frame_rate);
+
+       enum Guess
+       {
+               AGAIN,
+               PROBABLY_3D,
+               PROBABLY_NOT_3D
+       };
+
+       Guess guess () const;
+
+private:
+       boost::optional<ContentTime> _last;
+       /** Intervals of last frames, in fractions of a frame; i.e. 1 in here
+        *  means the last two frames were one frame interval apart according
+        *  to the frame rate passed to ::feed().
+        */
+       std::vector<double> _intervals;
+
+       static int const _frames;
+};
+
index ce45abdd906d2473d4c11224e54097376d2979ec..69104d2f7a940e57806fddb867352b5d3feca55f 100644 (file)
@@ -27,6 +27,7 @@
 #include "film.h"
 #include "exceptions.h"
 #include "video_content.h"
 #include "film.h"
 #include "exceptions.h"
 #include "video_content.h"
+#include "frame_interval_checker.h"
 #include <boost/filesystem.hpp>
 #include <iostream>
 
 #include <boost/filesystem.hpp>
 #include <iostream>
 
index 613ec9c7311f8c162b34b7306c341f8c0ad4fdd7..e817c4754caea68e82498098f608ab8d9adbafe0 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
-    Copyright (C) 2012-2018 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
 
     This file is part of DCP-o-matic.
 
@@ -23,6 +23,7 @@
 #include "raw_image_proxy.h"
 #include "film.h"
 #include "log.h"
 #include "raw_image_proxy.h"
 #include "film.h"
 #include "log.h"
+#include "frame_interval_checker.h"
 #include "compose.hpp"
 #include <boost/foreach.hpp>
 #include <iostream>
 #include "compose.hpp"
 #include <boost/foreach.hpp>
 #include <iostream>
@@ -39,6 +40,7 @@ using boost::optional;
 VideoDecoder::VideoDecoder (Decoder* parent, shared_ptr<const Content> c)
        : DecoderPart (parent)
        , _content (c)
 VideoDecoder::VideoDecoder (Decoder* parent, shared_ptr<const Content> c)
        : DecoderPart (parent)
        , _content (c)
+       , _frame_interval_checker (new FrameIntervalChecker())
 {
 
 }
 {
 
 }
@@ -62,6 +64,31 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im
        double const afr = _content->active_video_frame_rate(film);
        VideoFrameType const vft = _content->video->frame_type();
 
        double const afr = _content->active_video_frame_rate(film);
        VideoFrameType const vft = _content->video->frame_type();
 
+       ContentTime frame_time = ContentTime::from_frames (decoder_frame, afr);
+
+       /* Do some heuristics to try and spot the case where the user sets content to 3D
+        * when it is not.  We try to tell this by looking at the differences in time between
+        * the first few frames.  Real 3D content should have two frames for each timestamp.
+        */
+       if (_frame_interval_checker) {
+               _frame_interval_checker->feed (frame_time, afr);
+               if (_frame_interval_checker->guess() == FrameIntervalChecker::PROBABLY_NOT_3D && _content->video->frame_type() == VIDEO_FRAME_TYPE_3D) {
+                       boost::throw_exception (
+                               DecodeError(
+                                       String::compose(
+                                               _("The content file %1 is set as 3D but does not appear to contain 3D images.  Please set it to 2D.  "
+                                                 "You can still make a 3D DCP from this content by ticking the 3D option in the DCP video tab."),
+                                               _content->path(0)
+                                               )
+                                       )
+                               );
+               }
+
+               if (_frame_interval_checker->guess() != FrameIntervalChecker::AGAIN) {
+                       _frame_interval_checker.reset ();
+               }
+       }
+
        Frame frame;
        if (!_position) {
                /* This is the first data we have received since initialisation or seek.  Set
        Frame frame;
        if (!_position) {
                /* This is the first data we have received since initialisation or seek.  Set
@@ -105,20 +132,6 @@ VideoDecoder::emit (shared_ptr<const Film> film, shared_ptr<const ImageProxy> im
                   frame this one is.
                */
                bool const same = (_last_emitted_frame && _last_emitted_frame.get() == frame);
                   frame this one is.
                */
                bool const same = (_last_emitted_frame && _last_emitted_frame.get() == frame);
-               if (!same && _last_emitted_eyes && *_last_emitted_eyes == EYES_LEFT) {
-                       /* We just got a new frame index but the last frame was left-eye; it looks like
-                          this content is not really 3D.
-                       */
-                       boost::throw_exception (
-                               DecodeError(
-                                       String::compose(
-                                               _("The content file %1 is set as 3D but does not appear to contain 3D images.  Please set it to 2D.  "
-                                                 "You can still make a 3D DCP from this content by ticking the 3D option in the DCP video tab."),
-                                               _content->path(0)
-                                               )
-                                       )
-                               );
-               }
                Eyes const eyes = same ? EYES_RIGHT : EYES_LEFT;
                Data (ContentVideo (image, frame, eyes, PART_WHOLE));
                _last_emitted_frame = frame;
                Eyes const eyes = same ? EYES_RIGHT : EYES_LEFT;
                Data (ContentVideo (image, frame, eyes, PART_WHOLE));
                _last_emitted_frame = frame;
@@ -160,4 +173,5 @@ VideoDecoder::seek ()
        _position = boost::none;
        _last_emitted_frame.reset ();
        _last_emitted_eyes.reset ();
        _position = boost::none;
        _last_emitted_frame.reset ();
        _last_emitted_eyes.reset ();
+       _frame_interval_checker.reset (new FrameIntervalChecker());
 }
 }
index 820783b2e16d35120fdbfec952ad93aa1e41a24a..18be80ba60528b85811bbb5aef67e6148972dac2 100644 (file)
@@ -37,6 +37,7 @@ class VideoContent;
 class ImageProxy;
 class Image;
 class Log;
 class ImageProxy;
 class Image;
 class Log;
+class FrameIntervalChecker;
 
 /** @class VideoDecoder
  *  @brief Parent for classes which decode video.
 
 /** @class VideoDecoder
  *  @brief Parent for classes which decode video.
@@ -66,6 +67,7 @@ private:
        boost::optional<Frame> _last_emitted_frame;
        boost::optional<Eyes> _last_emitted_eyes;
        boost::optional<ContentTime> _position;
        boost::optional<Frame> _last_emitted_frame;
        boost::optional<Eyes> _last_emitted_eyes;
        boost::optional<ContentTime> _position;
+       boost::scoped_ptr<FrameIntervalChecker> _frame_interval_checker;
 };
 
 #endif
 };
 
 #endif
index 3c4002ca2b6d9acbbe20079627ebd5f3f39f0c94..adef544a7d201947b6d4750a6d871553201b3bbd 100644 (file)
@@ -22,6 +22,7 @@
 #include "video_decoder.h"
 #include "video_mxf_content.h"
 #include "j2k_image_proxy.h"
 #include "video_decoder.h"
 #include "video_mxf_content.h"
 #include "j2k_image_proxy.h"
+#include "frame_interval_checker.h"
 #include <dcp/mono_picture_asset.h>
 #include <dcp/mono_picture_asset_reader.h>
 #include <dcp/stereo_picture_asset.h>
 #include <dcp/mono_picture_asset.h>
 #include <dcp/mono_picture_asset_reader.h>
 #include <dcp/stereo_picture_asset.h>
index 267600bf808ed9049c2d3717fb96a2bbbb6a608f..6b59e9f8bdcac73ff1cc8834c8e138e45828b802 100644 (file)
@@ -104,6 +104,7 @@ sources = """
           filter.cc
           ffmpeg_image_proxy.cc
           font.cc
           filter.cc
           ffmpeg_image_proxy.cc
           font.cc
+          frame_interval_checker.cc
           frame_rate_change.cc
           hints.cc
           internet.cc
           frame_rate_change.cc
           hints.cc
           internet.cc
diff --git a/test/frame_interval_checker_test.cc b/test/frame_interval_checker_test.cc
new file mode 100644 (file)
index 0000000..77cf7ca
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+    Copyright (C) 2020 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+#include "lib/frame_interval_checker.h"
+#include <boost/test/unit_test.hpp>
+
+/** Test of 2D-ish frame timings */
+BOOST_AUTO_TEST_CASE (frame_interval_checker_test1)
+{
+       FrameIntervalChecker checker;
+       ContentTime t(3888);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4012);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4000);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4000);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(3776);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(3779);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4010);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4085);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4085);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4012);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4000);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4000);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(3776);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(3779);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4010);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4085);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4085);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::PROBABLY_NOT_3D);
+}
+
+/** Test of 3D-ish frame timings */
+BOOST_AUTO_TEST_CASE (frame_interval_checker_test2)
+{
+       FrameIntervalChecker checker;
+       ContentTime t(3888);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(0);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4000);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(0);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(3776);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(50);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4010);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(2);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4011);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(0);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4000);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(0);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(3776);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(50);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4010);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(2);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::AGAIN);
+       t += ContentTime(4011);
+       checker.feed (t, 24);
+       BOOST_CHECK (checker.guess() == FrameIntervalChecker::PROBABLY_3D);
+}
+
+
index db773fb4692e32cd64a7535e108ec18176cf5d9a..34f6c84ab8def364a498808f60336b9da49512c5 100644 (file)
@@ -76,6 +76,7 @@ def build(bld):
                  file_log_test.cc
                  file_naming_test.cc
                  film_metadata_test.cc
                  file_log_test.cc
                  file_naming_test.cc
                  film_metadata_test.cc
+                 frame_interval_checker_test.cc
                  frame_rate_test.cc
                  image_content_fade_test.cc
                  image_filename_sorter_test.cc
                  frame_rate_test.cc
                  image_content_fade_test.cc
                  image_filename_sorter_test.cc