Primitive auto-crop (#1477).
authorCarl Hetherington <cth@carlh.net>
Sat, 1 Jan 2022 21:20:51 +0000 (21:20 +0000)
committerCarl Hetherington <cth@carlh.net>
Sat, 15 Jan 2022 23:38:53 +0000 (00:38 +0100)
21 files changed:
src/lib/config.cc
src/lib/config.h
src/lib/player.cc
src/lib/player.h
src/lib/rect.h
src/lib/video_content.cc
src/lib/video_content.h
src/wx/auto_crop_dialog.cc [new file with mode: 0644]
src/wx/auto_crop_dialog.h [new file with mode: 0644]
src/wx/content_menu.cc
src/wx/content_menu.h
src/wx/content_panel.cc
src/wx/film_viewer.cc
src/wx/film_viewer.h
src/wx/gl_video_view.cc
src/wx/gl_video_view.h
src/wx/simple_video_view.cc
src/wx/timeline.cc
src/wx/video_view.cc
src/wx/video_view.h
src/wx/wscript

index bf294aec957e03a3c95d7d6ff8f7677112d66073..da7082a8eed8ec1ea9ca2242b27128443d37767c 100644 (file)
@@ -183,6 +183,7 @@ Config::set_defaults ()
        _audio_mapping = boost::none;
        _custom_languages.clear ();
        _add_files_path = boost::none;
+       _auto_crop_threshold = 0.1;
 
        _allowed_dcp_frame_rates.clear ();
        _allowed_dcp_frame_rates.push_back (24);
@@ -583,6 +584,7 @@ try
        }
 
        _add_files_path = f.optional_string_child("AddFilesPath");
+       _auto_crop_threshold = f.optional_number_child<double>("AutoCropThreshold").get_value_or(0.1);
 
        if (boost::filesystem::exists (_cinemas_file)) {
                cxml::Document f ("Cinemas");
@@ -1014,6 +1016,7 @@ Config::write_config () const
                /* [XML] AddFilesPath The default path that will be offered in the picker when adding files to a film. */
                root->add_child("AddFilesPath")->add_child_text(_add_files_path->string());
        }
+       root->add_child("AutoCropThreshold")->add_child_text(raw_convert<string>(_auto_crop_threshold));
 
        auto target = config_write_file();
 
index 5289656e37fdcbdc1da5873d0325490886bf5418..bdf6c83951da3a7a3498052649e1b4ad70eaca16 100644 (file)
@@ -84,6 +84,7 @@ public:
                HISTORY,
                SHOW_EXPERIMENTAL_AUDIO_PROCESSORS,
                AUDIO_MAPPING,
+               AUTO_CROP_THRESHOLD,
                OTHER
        };
 
@@ -547,6 +548,10 @@ public:
                return _add_files_path;
        }
 
+       double auto_crop_threshold () const {
+               return _auto_crop_threshold;
+       }
+
        /* SET (mostly) */
 
        void set_master_encoding_threads (int n) {
@@ -1055,6 +1060,10 @@ public:
                changed ();
        }
 
+       void set_auto_crop_threshold (double threshold) {
+               maybe_set (_auto_crop_threshold, threshold, AUTO_CROP_THRESHOLD);
+       }
+
        void changed (Property p = OTHER);
        boost::signals2::signal<void (Property)> Changed;
        /** Emitted if read() failed on an existing Config file.  There is nothing
@@ -1268,6 +1277,7 @@ private:
        boost::optional<AudioMapping> _audio_mapping;
        std::vector<dcp::LanguageTag> _custom_languages;
        boost::optional<boost::filesystem::path> _add_files_path;
+       double _auto_crop_threshold;
 
        static int const _current_version;
 
index 01413115b48af9a507566b37e2c41cb590c16a3b..f1fd060f4344c40c559ff8350ffbeddced4a5b97 100644 (file)
@@ -1384,7 +1384,7 @@ Player::set_dcp_decode_reduction (optional<int> reduction)
 
 
 optional<DCPTime>
-Player::content_time_to_dcp (shared_ptr<Content> content, ContentTime t)
+Player::content_time_to_dcp (shared_ptr<const Content> content, ContentTime t)
 {
        boost::mutex::scoped_lock lm (_mutex);
 
@@ -1399,6 +1399,22 @@ Player::content_time_to_dcp (shared_ptr<Content> content, ContentTime t)
 }
 
 
+optional<ContentTime>
+Player::dcp_to_content_time (shared_ptr<const Content> content, DCPTime t)
+{
+       boost::mutex::scoped_lock lm (_mutex);
+
+       for (auto i: _pieces) {
+               if (i->content == content) {
+                       return dcp_to_content_time (i, t);
+               }
+       }
+
+       /* We couldn't find this content; perhaps things are being changed over */
+       return {};
+}
+
+
 shared_ptr<const Playlist>
 Player::playlist () const
 {
index 0d0116e92534546371b11347c5ab44fd85896afe..3bd293889ac9f9f56b8fc4757513a2a8ac9bdcd1 100644 (file)
@@ -101,7 +101,8 @@ public:
        void set_play_referenced ();
        void set_dcp_decode_reduction (boost::optional<int> reduction);
 
-       boost::optional<dcpomatic::DCPTime> content_time_to_dcp (std::shared_ptr<Content> content, dcpomatic::ContentTime t);
+       boost::optional<dcpomatic::DCPTime> content_time_to_dcp (std::shared_ptr<const Content> content, dcpomatic::ContentTime t);
+       boost::optional<dcpomatic::ContentTime> dcp_to_content_time (std::shared_ptr<const Content> content, dcpomatic::DCPTime t);
 
        boost::signals2::signal<void (ChangeType, int, bool)> Change;
 
index 5f807f4999a155a8de737adc30eac87fe1a43d65..a01e0f8858ffba6d31eb6b9675d0cff571b4571d 100644 (file)
@@ -119,6 +119,13 @@ public:
 };
 
 
+template <class T>
+bool operator== (Rect<T> const& a, Rect<T> const& b)
+{
+       return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height;
+}
+
+
 }
 
 
index 27eb04fe0df4d802f2b3a316677a5dc8614a0aa4..1b24101f3c7651d5d76c21f9fe7f082474f4f439 100644 (file)
@@ -531,6 +531,12 @@ VideoContent::set_length (Frame len)
 }
 
 
+void
+VideoContent::set_crop (Crop c)
+{
+       maybe_set (_crop, c, VideoContentProperty::CROP);
+}
+
 void
 VideoContent::set_left_crop (int c)
 {
index 2adf941d97cc3cd4a26a5b0e0c13ca33abd95464..d16af14d8d8bc6797dcc3165c4141ab0a627320b 100644 (file)
@@ -89,6 +89,7 @@ public:
 
        void set_frame_type (VideoFrameType);
 
+       void set_crop (Crop crop);
        void set_left_crop (int);
        void set_right_crop (int);
        void set_top_crop (int);
diff --git a/src/wx/auto_crop_dialog.cc b/src/wx/auto_crop_dialog.cc
new file mode 100644 (file)
index 0000000..bfc08be
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+    Copyright (C) 2021 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 "auto_crop_dialog.h"
+#include "dcpomatic_spin_ctrl.h"
+#include "wx_util.h"
+#include "lib/config.h"
+#include "lib/types.h"
+
+
+AutoCropDialog::AutoCropDialog (wxWindow* parent, Crop crop)
+       : TableDialog (parent, _("Auto crop"), 2, 1, true)
+{
+       add (_("Left"), true);
+       _left = add(new SpinCtrl(this, DCPOMATIC_SPIN_CTRL_WIDTH));
+       add (_("Right"), true);
+       _right = add(new SpinCtrl(this, DCPOMATIC_SPIN_CTRL_WIDTH));
+       add (_("Top"), true);
+       _top = add(new SpinCtrl(this, DCPOMATIC_SPIN_CTRL_WIDTH));
+       add (_("Bottom"), true);
+       _bottom = add(new SpinCtrl(this, DCPOMATIC_SPIN_CTRL_WIDTH));
+       add (_("Threshold"), true);
+       _threshold = add(new SpinCtrl(this, DCPOMATIC_SPIN_CTRL_WIDTH));
+
+       _left->SetRange(0, 4096);
+       _right->SetRange(0, 4096);
+       _top->SetRange(0, 4096);
+       _bottom->SetRange(0, 4096);
+
+       set (crop);
+       _threshold->SetValue (std::round(Config::instance()->auto_crop_threshold() * 100));
+
+       layout ();
+
+       _left->Bind (wxEVT_SPINCTRL, [this](wxSpinEvent&) { Changed(get()); });
+       _right->Bind (wxEVT_SPINCTRL, [this](wxSpinEvent&) { Changed(get()); });
+       _top->Bind (wxEVT_SPINCTRL, [this](wxSpinEvent&) { Changed(get()); });
+       _bottom->Bind (wxEVT_SPINCTRL, [this](wxSpinEvent&) { Changed(get()); });
+       _threshold->Bind (wxEVT_SPINCTRL, [](wxSpinEvent& ev) { Config::instance()->set_auto_crop_threshold(ev.GetPosition() / 100.0); });
+}
+
+
+Crop
+AutoCropDialog::get () const
+{
+       return Crop(_left->GetValue(), _right->GetValue(), _top->GetValue(), _bottom->GetValue());
+}
+
+
+void
+AutoCropDialog::set (Crop crop)
+{
+       _left->SetValue (crop.left);
+       _right->SetValue (crop.right);
+       _top->SetValue (crop.top);
+       _bottom->SetValue (crop.bottom);
+}
+
diff --git a/src/wx/auto_crop_dialog.h b/src/wx/auto_crop_dialog.h
new file mode 100644 (file)
index 0000000..0615c14
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+    Copyright (C) 2021 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 "table_dialog.h"
+#include "lib/types.h"
+#include <boost/signals2.hpp>
+
+
+class SpinCtrl;
+
+
+class AutoCropDialog : public TableDialog
+{
+public:
+       AutoCropDialog (wxWindow* parent, Crop crop);
+
+       Crop get () const;
+       void set (Crop crop);
+
+       boost::signals2::signal<void (Crop)> Changed;
+
+private:
+       SpinCtrl* _left;
+       SpinCtrl* _right;
+       SpinCtrl* _top;
+       SpinCtrl* _bottom;
+       SpinCtrl* _threshold;
+};
+
index 5d208acc59bcf26a3794c512c1fd15312a7304ef..48d5354ef83d8e78575b259d366c8520f1305458 100644 (file)
 */
 
 
+#include "auto_crop_dialog.h"
 #include "content_advanced_dialog.h"
 #include "content_menu.h"
 #include "content_properties_dialog.h"
+#include "film_viewer.h"
 #include "repeat_dialog.h"
 #include "timeline_audio_content_view.h"
 #include "timeline_video_content_view.h"
 #include "lib/ffmpeg_content.h"
 #include "lib/film.h"
 #include "lib/find_missing.h"
+#include "lib/guess_crop.h"
 #include "lib/image_content.h"
 #include "lib/job_manager.h"
 #include "lib/playlist.h"
+#include "lib/video_content.h"
 #include <dcp/cpl.h>
 #include <dcp/decrypted_kdm.h>
 #include <dcp/exceptions.h>
@@ -61,6 +65,7 @@ using boost::optional;
 #if BOOST_VERSION >= 106100
 using namespace boost::placeholders;
 #endif
+using namespace dcpomatic;
 
 
 enum {
@@ -71,6 +76,7 @@ enum {
        ID_properties,
        ID_advanced,
        ID_re_examine,
+       ID_auto_crop,
        ID_kdm,
        ID_ov,
        ID_choose_cpl,
@@ -79,15 +85,17 @@ enum {
 };
 
 
-ContentMenu::ContentMenu (wxWindow* p)
+ContentMenu::ContentMenu (wxWindow* p, weak_ptr<FilmViewer> viewer)
        : _menu (new wxMenu)
        , _parent (p)
        , _pop_up_open (false)
+       , _viewer (viewer)
 {
        _repeat = _menu->Append (ID_repeat, _("Repeat..."));
        _join = _menu->Append (ID_join, _("Join"));
        _find_missing = _menu->Append (ID_find_missing, _("Find missing..."));
        _re_examine = _menu->Append (ID_re_examine, _("Re-examine..."));
+       _auto_crop = _menu->Append (ID_auto_crop, _("Auto-crop..."));
        _properties = _menu->Append (ID_properties, _("Properties..."));
        _advanced = _menu->Append (ID_advanced, _("Advanced settings..."));
        _menu->AppendSeparator ();
@@ -105,6 +113,7 @@ ContentMenu::ContentMenu (wxWindow* p)
        _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::properties, this), ID_properties);
        _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::advanced, this), ID_advanced);
        _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::re_examine, this), ID_re_examine);
+       _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::auto_crop, this), ID_auto_crop);
        _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::kdm, this), ID_kdm);
        _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::ov, this), ID_ov);
        _parent->Bind (wxEVT_MENU, boost::bind (&ContentMenu::set_dcp_settings, this), ID_set_dcp_settings);
@@ -139,6 +148,7 @@ ContentMenu::popup (weak_ptr<Film> film, ContentList c, TimelineContentViewList
        _properties->Enable (_content.size() == 1);
        _advanced->Enable (_content.size() == 1);
        _re_examine->Enable (!_content.empty ());
+       _auto_crop->Enable (_content.size() == 1);
 
        if (_content.size() == 1) {
                auto dcp = dynamic_pointer_cast<DCPContent> (_content.front());
@@ -481,3 +491,84 @@ ContentMenu::cpl_selected (wxCommandEvent& ev)
        DCPOMATIC_ASSERT (film);
        JobManager::instance()->add (make_shared<ExamineContentJob>(film, dcp));
 }
+
+
+void
+ContentMenu::auto_crop ()
+{
+       DCPOMATIC_ASSERT (_content.size() == 1);
+
+       auto film = _film.lock ();
+       DCPOMATIC_ASSERT (film);
+       auto viewer = _viewer.lock ();
+       DCPOMATIC_ASSERT (viewer);
+
+       auto update_viewer = [this](Crop crop) {
+               auto film = _film.lock();
+               DCPOMATIC_ASSERT (film);
+               auto viewer = _viewer.lock ();
+               DCPOMATIC_ASSERT (viewer);
+               auto const content = _content.front();
+               auto const current_crop = content->video->actual_crop();
+               viewer->set_crop_guess (
+                       Rect<float>(
+                               static_cast<float>(std::max(0, crop.left - current_crop.left)) / content->video->size().width,
+                               static_cast<float>(std::max(0, crop.top - current_crop.top)) / content->video->size().height,
+                               1.0f - (static_cast<float>(std::max(0, crop.left - current_crop.left + crop.right - current_crop.right)) / content->video->size().width),
+                               1.0f - (static_cast<float>(std::max(0, crop.top - current_crop.top + crop.bottom - current_crop.bottom)) / content->video->size().height)
+                               ));
+       };
+
+       auto guess_crop_for_content = [this, film, viewer]() {
+               auto position = viewer->position_in_content(_content.front()).get_value_or(
+                       ContentTime::from_frames(_content.front()->video->length(), _content.front()->video_frame_rate().get_value_or(24))
+                       );
+               return guess_crop(film, _content.front(), Config::instance()->auto_crop_threshold(), position);
+       };
+
+       /* Make an initial guess in the view and open the dialog */
+
+       auto const crop = guess_crop_for_content ();
+       update_viewer (crop);
+
+       if (_auto_crop_dialog) {
+               _auto_crop_dialog->Destroy();
+               _auto_crop_dialog = nullptr;
+       }
+       _auto_crop_dialog = new AutoCropDialog (_parent, crop);
+       _auto_crop_dialog->Show ();
+
+       /* Update the dialog and view when the crop threshold changes */
+       _auto_crop_config_connection = Config::instance()->Changed.connect([this, guess_crop_for_content, update_viewer](Config::Property property) {
+               auto film = _film.lock();
+               DCPOMATIC_ASSERT (film);
+               if (property == Config::AUTO_CROP_THRESHOLD) {
+                       auto const crop = guess_crop_for_content();
+                       _auto_crop_dialog->set(crop);
+                       update_viewer(crop);
+               }
+       });
+
+       /* Also update the dialog and view when we're looking at a different frame */
+       _auto_crop_viewer_connection = viewer->ImageChanged.connect([this, guess_crop_for_content, update_viewer](shared_ptr<PlayerVideo>) {
+               auto const crop = guess_crop_for_content();
+               _auto_crop_dialog->set(crop);
+               update_viewer(crop);
+       });
+
+       /* Handle the user closing the dialog (with OK or cancel) */
+       _auto_crop_dialog->Bind (wxEVT_BUTTON, [this, viewer](wxCommandEvent& ev) {
+               if (ev.GetId() == wxID_OK) {
+                       _content.front()->video->set_crop(_auto_crop_dialog->get());
+               }
+               _auto_crop_dialog->Show (false);
+               viewer->unset_crop_guess ();
+               _auto_crop_config_connection.disconnect ();
+               _auto_crop_viewer_connection.disconnect ();
+       });
+
+       /* Update the view when something in the dialog is changed */
+       _auto_crop_dialog->Changed.connect([this, update_viewer](Crop crop) {
+               update_viewer (crop);
+       });
+}
index 03f657d3882f1618d1cb9b5e264e811a90fe2315..147f95946d318c112ed183ea5c53f1350b03e33b 100644 (file)
 
 */
 
+
 #ifndef DCPOMATIC_CONTENT_MENU_H
 #define DCPOMATIC_CONTENT_MENU_H
 
+
 #include "timeline_content_view.h"
 #include "lib/types.h"
 #include <wx/wx.h>
 #include <memory>
 
+
+class AutoCropDialog;
+class DCPContent;
 class Film;
+class FilmViewer;
 class Job;
-class DCPContent;
+
 
 class ContentMenu
 {
 public:
-       explicit ContentMenu (wxWindow* p);
+       ContentMenu (wxWindow* parent, std::weak_ptr<FilmViewer> viewer);
 
        ContentMenu (ContentMenu const &) = delete;
        ContentMenu& operator= (ContentMenu const &) = delete;
@@ -47,6 +53,7 @@ private:
        void properties ();
        void advanced ();
        void re_examine ();
+       void auto_crop ();
        void kdm ();
        void ov ();
        void set_dcp_settings ();
@@ -59,6 +66,7 @@ private:
        std::weak_ptr<Film> _film;
        wxWindow* _parent;
        bool _pop_up_open;
+       std::weak_ptr<FilmViewer> _viewer;
        ContentList _content;
        TimelineContentViewList _views;
        wxMenuItem* _repeat;
@@ -67,11 +75,17 @@ private:
        wxMenuItem* _properties;
        wxMenuItem* _advanced;
        wxMenuItem* _re_examine;
+       wxMenuItem* _auto_crop;
        wxMenuItem* _kdm;
        wxMenuItem* _ov;
        wxMenuItem* _choose_cpl;
        wxMenuItem* _set_dcp_settings;
        wxMenuItem* _remove;
+
+       AutoCropDialog* _auto_crop_dialog = nullptr;
+       boost::signals2::scoped_connection _auto_crop_config_connection;
+       boost::signals2::scoped_connection _auto_crop_viewer_connection;
 };
 
+
 #endif
index b2b72b216a1f626c628f136b3b9b1a3c491f4380..e2a236937f24f2877a670c65675275dd4ca78bb7 100644 (file)
@@ -84,7 +84,7 @@ ContentPanel::ContentPanel (wxNotebook* n, shared_ptr<Film> film, weak_ptr<FilmV
        _splitter = new LimitedSplitter (n);
        _top_panel = new wxPanel (_splitter);
 
-       _menu = new ContentMenu (_splitter);
+       _menu = new ContentMenu (_splitter, _film_viewer);
 
        {
                auto s = new wxBoxSizer (wxHORIZONTAL);
index d91a9db9ce981275c4c743c3dcf0e5c390d024f1..e2f09f238d42fd0c43f11ec0b0b99883bfbeb77f 100644 (file)
@@ -118,7 +118,7 @@ FilmViewer::~FilmViewer ()
 }
 
 
-/** Ask for ::get() to be called next time we are idle */
+/** Ask for ::idle_handler() to be called next time we are idle */
 void
 FilmViewer::request_idle_display_next_frame ()
 {
@@ -712,6 +712,13 @@ FilmViewer::dcp_decode_reduction () const
 }
 
 
+optional<ContentTime>
+FilmViewer::position_in_content (shared_ptr<const Content> content) const
+{
+       return _player->dcp_to_content_time (content, position());
+}
+
+
 DCPTime
 FilmViewer::one_video_frame () const
 {
@@ -795,3 +802,21 @@ FilmViewer::set_optimise_for_j2k (bool o)
        _video_view->set_optimise_for_j2k (o);
 }
 
+
+void
+FilmViewer::set_crop_guess (Rect<float> crop)
+{
+       if (crop != _crop_guess) {
+               _crop_guess = crop;
+               _video_view->update ();
+       }
+}
+
+
+void
+FilmViewer::unset_crop_guess ()
+{
+       _crop_guess = {};
+       _video_view->update ();
+}
+
index 64ac885e37bdcbe73b0637332c8a2007c28534be..af15ded943acbb180555a74e5103767a99ad899f 100644 (file)
@@ -80,6 +80,7 @@ public:
        dcpomatic::DCPTime position () const {
                return _video_view->position();
        }
+       boost::optional<dcpomatic::ContentTime> position_in_content (std::shared_ptr<const Content> content) const;
        dcpomatic::DCPTime one_video_frame () const;
 
        void start ();
@@ -99,6 +100,8 @@ public:
        void set_eyes (Eyes e);
        void set_pad_black (bool p);
        void set_optimise_for_j2k (bool o);
+       void set_crop_guess (dcpomatic::Rect<float> crop);
+       void unset_crop_guess ();
 
        void slow_refresh ();
 
@@ -133,6 +136,9 @@ public:
        }
        void finished ();
        void image_changed (std::shared_ptr<PlayerVideo> video);
+       boost::optional<dcpomatic::Rect<float>> crop_guess () const {
+               return _crop_guess;
+       }
 
        bool pending_idle_get () const {
                return _idle_get;
@@ -206,5 +212,7 @@ private:
        /** true if an get() is required next time we are idle */
        bool _idle_get = false;
 
+       boost::optional<dcpomatic::Rect<float>> _crop_guess;
+
        boost::signals2::scoped_connection _config_changed_connection;
 };
index ed99e3430a25639a841b352b12afc387e2d9b9e8..39280c905f214f3e78dfcf73e2beb2c65f2d3d5a 100644 (file)
@@ -196,12 +196,14 @@ static constexpr char fragment_source[] =
 "\n"
 "uniform sampler2D texture_sampler;\n"
 /* type = 0: draw outline content rectangle
- * type = 1: draw XYZ image
- * type = 2: draw RGB image
+ * type = 1: draw crop guess rectangle
+ * type = 2: draw XYZ image
+ * type = 3: draw RGB image
  * See FragmentType enum below.
  */
 "uniform int type = 0;\n"
 "uniform vec4 outline_content_colour;\n"
+"uniform vec4 crop_guess_colour;\n"
 "uniform mat4 colour_conversion;\n"
 "\n"
 "out vec4 FragColor;\n"
@@ -263,6 +265,9 @@ static constexpr char fragment_source[] =
 "                      FragColor = outline_content_colour;\n"
 "                      break;\n"
 "              case 1:\n"
+"                       FragColor = crop_guess_colour;\n"
+"                       break;\n"
+"              case 2:\n"
 "                      FragColor = texture_bicubic(texture_sampler, TexCoord);\n"
 "                      FragColor.x = pow(FragColor.x, IN_GAMMA) / DCI_COEFFICIENT;\n"
 "                      FragColor.y = pow(FragColor.y, IN_GAMMA) / DCI_COEFFICIENT;\n"
@@ -272,7 +277,7 @@ static constexpr char fragment_source[] =
 "                      FragColor.y = pow(FragColor.y, OUT_GAMMA);\n"
 "                      FragColor.z = pow(FragColor.z, OUT_GAMMA);\n"
 "                      break;\n"
-"              case 2:\n"
+"              case 3:\n"
 "                      FragColor = texture_bicubic(texture_sampler, TexCoord);\n"
 "                      break;\n"
 "      }\n"
@@ -282,8 +287,9 @@ static constexpr char fragment_source[] =
 enum class FragmentType
 {
        OUTLINE_CONTENT = 0,
-       XYZ_IMAGE = 1,
-       RGB_IMAGE = 2,
+       CROP_GUESS = 1,
+       XYZ_IMAGE = 2,
+       RGB_IMAGE = 3,
 };
 
 
@@ -308,6 +314,8 @@ static constexpr int indices_subtitle_texture_offset = indices_video_texture_off
 static constexpr int indices_subtitle_texture_number = 6;
 static constexpr int indices_outline_content_offset = indices_subtitle_texture_offset + indices_subtitle_texture_number;
 static constexpr int indices_outline_content_number = 8;
+static constexpr int indices_crop_guess_offset = indices_outline_content_offset + indices_outline_content_number;
+static constexpr int indices_crop_guess_number = 8;
 
 static constexpr unsigned int indices[] = {
        0, 1, 3, // video texture triangle #1
@@ -318,12 +326,17 @@ static constexpr unsigned int indices[] = {
        9, 10,   // outline content line #2
        10, 11,  // outline content line #3
        11, 8,   // outline content line #4
+       12, 13,  // crop guess line #1
+       13, 14,  // crop guess line #2
+       14, 15,  // crop guess line #3
+       15, 12,  // crop guess line #4
 };
 
 /* Offsets of things in the GL_ARRAY_BUFFER */
 static constexpr int array_buffer_video_offset = 0;
 static constexpr int array_buffer_subtitle_offset = array_buffer_video_offset + 4 * 5 * sizeof(float);
 static constexpr int array_buffer_outline_content_offset = array_buffer_subtitle_offset + 4 * 5 * sizeof(float);
+static constexpr int array_buffer_crop_guess_offset = array_buffer_outline_content_offset + 4 * 5 * sizeof(float);
 
 
 void
@@ -439,6 +452,7 @@ GLVideoView::setup_shaders ()
        _fragment_type = glGetUniformLocation (program, "type");
        check_gl_error ("glGetUniformLocation");
        set_outline_content_colour (program);
+       set_crop_guess_colour (program);
 
        auto conversion = dcp::ColourConversion::rec709_to_xyz();
        boost::numeric::ublas::matrix<double> matrix = conversion.xyz_to_rgb ();
@@ -453,7 +467,7 @@ GLVideoView::setup_shaders ()
        check_gl_error ("glGetUniformLocation");
        glUniformMatrix4fv (colour_conversion, 1, GL_TRUE, gl_matrix);
 
-       glLineWidth (1.0f);
+       glLineWidth (2.0f);
        check_gl_error ("glLineWidth");
        glEnable (GL_BLEND);
        check_gl_error ("glEnable");
@@ -461,7 +475,7 @@ GLVideoView::setup_shaders ()
        check_gl_error ("glBlendFunc");
 
        /* Reserve space for the GL_ARRAY_BUFFER */
-       glBufferData(GL_ARRAY_BUFFER, 12 * 5 * sizeof(float), nullptr, GL_STATIC_DRAW);
+       glBufferData(GL_ARRAY_BUFFER, 16 * 5 * sizeof(float), nullptr, GL_STATIC_DRAW);
        check_gl_error ("glBufferData");
 }
 
@@ -477,6 +491,17 @@ GLVideoView::set_outline_content_colour (GLuint program)
 }
 
 
+void
+GLVideoView::set_crop_guess_colour (GLuint program)
+{
+       auto uniform = glGetUniformLocation (program, "crop_guess_colour");
+       check_gl_error ("glGetUniformLocation");
+       auto colour = crop_guess_colour ();
+       glUniform4f (uniform, colour.Red() / 255.0f, colour.Green() / 255.0f, colour.Blue() / 255.0f, 1.0f);
+       check_gl_error ("glUniform4f");
+}
+
+
 void
 GLVideoView::draw ()
 {
@@ -511,6 +536,11 @@ GLVideoView::draw ()
                glDrawElements (GL_LINES, indices_outline_content_number, GL_UNSIGNED_INT, reinterpret_cast<void*>(indices_outline_content_offset * sizeof(int)));
                check_gl_error ("glDrawElements");
        }
+       if (auto guess = _viewer->crop_guess()) {
+               glUniform1i(_fragment_type, 1);
+               glDrawElements (GL_LINES, indices_crop_guess_number, GL_UNSIGNED_INT, reinterpret_cast<void*>(indices_crop_guess_offset * sizeof(int)));
+               check_gl_error ("glDrawElements");
+       }
 
        glFlush();
        check_gl_error ("glFlush");
@@ -553,6 +583,7 @@ GLVideoView::set_image (shared_ptr<const PlayerVideo> pv)
        auto const inter_position = player_video().first->inter_position();
        auto const inter_size = player_video().first->inter_size();
        auto const out_size = player_video().first->out_size();
+       auto const crop_guess = _viewer->crop_guess();
 
        auto x_offset = std::max(0, (canvas_width - out_size.width) / 2);
        auto y_offset = std::max(0, (canvas_height - out_size.height) / 2);
@@ -562,6 +593,7 @@ GLVideoView::set_image (shared_ptr<const PlayerVideo> pv)
        _last_inter_position.set_next (inter_position);
        _last_inter_size.set_next (inter_size);
        _last_out_size.set_next (out_size);
+       _last_crop_guess.set_next (crop_guess);
 
        class Rectangle
        {
@@ -631,8 +663,9 @@ GLVideoView::set_image (shared_ptr<const PlayerVideo> pv)
                float _vertices[20];
        };
 
-       if (_last_canvas_size.changed() || _last_inter_position.changed() || _last_inter_size.changed() || _last_out_size.changed()) {
+       auto const sizing_changed = _last_canvas_size.changed() || _last_inter_position.changed() || _last_inter_size.changed() || _last_out_size.changed();
 
+       if (sizing_changed) {
                const auto video = _optimise_for_j2k ?
                        Rectangle(canvas_size, inter_position.x + x_offset, inter_position.y + y_offset, inter_size)
                        : Rectangle(canvas_size, x_offset, y_offset, out_size);
@@ -645,6 +678,17 @@ GLVideoView::set_image (shared_ptr<const PlayerVideo> pv)
                check_gl_error ("glBufferSubData (outline_content)");
        }
 
+       if ((sizing_changed || _last_crop_guess.changed()) && crop_guess) {
+               auto const crop_guess_rectangle = Rectangle(
+                       canvas_size,
+                       inter_position.x + x_offset + inter_size.width * crop_guess->x,
+                       inter_position.y + y_offset + inter_size.height * crop_guess->y,
+                       dcp::Size(inter_size.width * crop_guess->width, inter_size.height * crop_guess->height)
+                       );
+               glBufferSubData (GL_ARRAY_BUFFER, array_buffer_crop_guess_offset, crop_guess_rectangle.size(), crop_guess_rectangle.vertices());
+               check_gl_error ("glBufferSubData (crop_guess_rectangle)");
+       }
+
        if (_have_subtitle_to_render) {
                const auto subtitle = Rectangle(canvas_size, inter_position.x + x_offset + text->position.x, inter_position.y + y_offset + text->position.y, text->image->size());
                glBufferSubData (GL_ARRAY_BUFFER, array_buffer_subtitle_offset, subtitle.size(), subtitle.vertices());
index 098b8ce8e6e0bac79fcda957e14b7d25592ea70f..7a3aa41b0045128d89eb8d20c44e273024bf75f9 100644 (file)
@@ -100,6 +100,7 @@ private:
        void size_changed (wxSizeEvent const &);
        void setup_shaders ();
        void set_outline_content_colour (GLuint program);
+       void set_crop_guess_colour (GLuint program);
 
        wxGLCanvas* _canvas;
        wxGLContext* _context;
@@ -130,6 +131,7 @@ private:
        Last<Position<int>> _last_inter_position;
        Last<dcp::Size> _last_inter_size;
        Last<dcp::Size> _last_out_size;
+       Last<boost::optional<dcpomatic::Rect<float>>> _last_crop_guess;
 
        boost::atomic<wxSize> _canvas_size;
        std::unique_ptr<Texture> _video_texture;
index 7f816c31d74905d8309b8c69480fec571254f84c..45fcf4405b94f888d5f541f47d9835baeaa073fd 100644 (file)
@@ -117,6 +117,19 @@ SimpleVideoView::paint ()
                dc.DrawRectangle (subs->x * out_size.width, subs->y * out_size.height, subs->width * out_size.width, subs->height * out_size.height);
        }
 
+       if (_viewer->crop_guess()) {
+               wxPen p (crop_guess_colour(), 2);
+               dc.SetPen (p);
+               dc.SetBrush (*wxTRANSPARENT_BRUSH);
+               auto const crop_guess = _viewer->crop_guess().get();
+               dc.DrawRectangle (
+                       _inter_position.x + _inter_size.width * crop_guess.x,
+                       _inter_position.y + _inter_size.height * crop_guess.y,
+                       _inter_size.width * crop_guess.width,
+                       _inter_size.height * crop_guess.height
+                       );
+       }
+
         _state_timer.unset();
 }
 
index 1c5937ae0252350ec6ff2cc915dcfa5579cc7ce4..60b17c45c4123c3e766e719356953f928e60032e 100644 (file)
@@ -77,7 +77,7 @@ Timeline::Timeline (wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film, w
        , _left_down (false)
        , _down_view_position (0)
        , _first_move (false)
-       , _menu (this)
+       , _menu (this, viewer)
        , _snap (true)
        , _tool (SELECT)
        , _x_scroll_rate (16)
index 1c645f11f688bd6adab4924c07ad9b0fcd25811c..64226e88c9627c4160b503e06cdec7222b8cd0ec 100644 (file)
@@ -185,4 +185,3 @@ VideoView::pad_colour () const
                return wxColour(240, 240, 240);
        }
 }
-
index 5353f213f8803e3e2a9865c6a0567610edf51fc9..496888d43db4ddf05dc243b98290109ce53f8b9d 100644 (file)
@@ -146,6 +146,10 @@ protected:
                return wxColour(0, 255, 0);
        }
 
+       wxColour crop_guess_colour () const {
+               return wxColour(0, 0, 255);
+       }
+
        int video_frame_rate () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_frame_rate;
index 1976397d1d9f9a7a05afdf9ac994e7963cd173cb..8569872b7a3f97271b83a3fc70fbb641ff582371 100644 (file)
@@ -31,6 +31,7 @@ sources = """
           audio_mapping_view.cc
           audio_panel.cc
           audio_plot.cc
+          auto_crop_dialog.cc
           barco_alchemy_certificate_panel.cc
           batch_job_view.cc
           check_box.cc