From 3443fdbc38f56f4c83776d000f6aa61d00305cc8 Mon Sep 17 00:00:00 2001 From: Carl Hetherington Date: Wed, 19 Feb 2020 16:02:24 +0000 Subject: [PATCH] New way of checking for 2D content mislabelled as 3D (#1565). Required because of the change to the way video frame timing is done. --- src/lib/dcp_decoder.cc | 3 +- src/lib/ffmpeg_decoder.cc | 1 + src/lib/frame_interval_checker.cc | 65 +++++++++++++ src/lib/frame_interval_checker.h | 50 ++++++++++ src/lib/image_decoder.cc | 1 + src/lib/video_decoder.cc | 44 ++++++--- src/lib/video_decoder.h | 2 + src/lib/video_mxf_decoder.cc | 1 + src/lib/wscript | 1 + test/frame_interval_checker_test.cc | 138 ++++++++++++++++++++++++++++ test/wscript | 1 + 11 files changed, 291 insertions(+), 16 deletions(-) create mode 100644 src/lib/frame_interval_checker.cc create mode 100644 src/lib/frame_interval_checker.h create mode 100644 test/frame_interval_checker_test.cc diff --git a/src/lib/dcp_decoder.cc b/src/lib/dcp_decoder.cc index 7af89e84d..e8ace8bee 100644 --- a/src/lib/dcp_decoder.cc +++ b/src/lib/dcp_decoder.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2014-2018 Carl Hetherington + Copyright (C) 2014-2020 Carl Hetherington This file is part of DCP-o-matic. @@ -28,6 +28,7 @@ #include "ffmpeg_image_proxy.h" #include "image.h" #include "config.h" +#include "frame_interval_checker.h" #include #include #include diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc index fd248c39a..bb852d9d8 100644 --- a/src/lib/ffmpeg_decoder.cc +++ b/src/lib/ffmpeg_decoder.cc @@ -42,6 +42,7 @@ #include "compose.hpp" #include "text_content.h" #include "audio_content.h" +#include "frame_interval_checker.h" #include #include #include diff --git a/src/lib/frame_interval_checker.cc b/src/lib/frame_interval_checker.cc new file mode 100644 index 000000000..056973039 --- /dev/null +++ b/src/lib/frame_interval_checker.cc @@ -0,0 +1,65 @@ +/* + Copyright (C) 2020 Carl Hetherington + + 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 . + +*/ + +#include "frame_interval_checker.h" +#include + +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 index 000000000..14b715c17 --- /dev/null +++ b/src/lib/frame_interval_checker.h @@ -0,0 +1,50 @@ +/* + Copyright (C) 2020 Carl Hetherington + + 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 . + +*/ + +#include "dcpomatic_time.h" +#include +#include +#include + +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 _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 _intervals; + + static int const _frames; +}; + diff --git a/src/lib/image_decoder.cc b/src/lib/image_decoder.cc index ce45abdd9..69104d2f7 100644 --- a/src/lib/image_decoder.cc +++ b/src/lib/image_decoder.cc @@ -27,6 +27,7 @@ #include "film.h" #include "exceptions.h" #include "video_content.h" +#include "frame_interval_checker.h" #include #include diff --git a/src/lib/video_decoder.cc b/src/lib/video_decoder.cc index 613ec9c73..e817c4754 100644 --- a/src/lib/video_decoder.cc +++ b/src/lib/video_decoder.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2018 Carl Hetherington + Copyright (C) 2012-2020 Carl Hetherington This file is part of DCP-o-matic. @@ -23,6 +23,7 @@ #include "raw_image_proxy.h" #include "film.h" #include "log.h" +#include "frame_interval_checker.h" #include "compose.hpp" #include #include @@ -39,6 +40,7 @@ using boost::optional; VideoDecoder::VideoDecoder (Decoder* parent, shared_ptr c) : DecoderPart (parent) , _content (c) + , _frame_interval_checker (new FrameIntervalChecker()) { } @@ -62,6 +64,31 @@ VideoDecoder::emit (shared_ptr film, shared_ptr im 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 @@ -105,20 +132,6 @@ VideoDecoder::emit (shared_ptr film, shared_ptr im 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; @@ -160,4 +173,5 @@ VideoDecoder::seek () _position = boost::none; _last_emitted_frame.reset (); _last_emitted_eyes.reset (); + _frame_interval_checker.reset (new FrameIntervalChecker()); } diff --git a/src/lib/video_decoder.h b/src/lib/video_decoder.h index 820783b2e..18be80ba6 100644 --- a/src/lib/video_decoder.h +++ b/src/lib/video_decoder.h @@ -37,6 +37,7 @@ class VideoContent; class ImageProxy; class Image; class Log; +class FrameIntervalChecker; /** @class VideoDecoder * @brief Parent for classes which decode video. @@ -66,6 +67,7 @@ private: boost::optional _last_emitted_frame; boost::optional _last_emitted_eyes; boost::optional _position; + boost::scoped_ptr _frame_interval_checker; }; #endif diff --git a/src/lib/video_mxf_decoder.cc b/src/lib/video_mxf_decoder.cc index 3c4002ca2..adef544a7 100644 --- a/src/lib/video_mxf_decoder.cc +++ b/src/lib/video_mxf_decoder.cc @@ -22,6 +22,7 @@ #include "video_decoder.h" #include "video_mxf_content.h" #include "j2k_image_proxy.h" +#include "frame_interval_checker.h" #include #include #include diff --git a/src/lib/wscript b/src/lib/wscript index 267600bf8..6b59e9f8b 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -104,6 +104,7 @@ sources = """ filter.cc ffmpeg_image_proxy.cc font.cc + frame_interval_checker.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 index 000000000..77cf7ca5d --- /dev/null +++ b/test/frame_interval_checker_test.cc @@ -0,0 +1,138 @@ +/* + Copyright (C) 2020 Carl Hetherington + + 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 . + +*/ + +#include "lib/frame_interval_checker.h" +#include + +/** 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); +} + + diff --git a/test/wscript b/test/wscript index db773fb46..34f6c84ab 100644 --- a/test/wscript +++ b/test/wscript @@ -76,6 +76,7 @@ def build(bld): 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 -- 2.30.2