Obey requests to change the video range of RGB content.
authorCarl Hetherington <cth@carlh.net>
Sun, 8 Nov 2020 21:34:18 +0000 (22:34 +0100)
committerCarl Hetherington <cth@carlh.net>
Mon, 16 Nov 2020 00:40:36 +0000 (01:40 +0100)
Video that comes in with RGB pixels will not have its video level
ranges changed by libswscale (it only does this for YUV and greyscale).
Here we add code to do it ourselves for RGB content coming in
via image files (e.g. PNG/DPX etc).  Part of #1851.

14 files changed:
src/lib/ffmpeg_image_proxy.cc
src/lib/ffmpeg_image_proxy.h
src/lib/image.cc
src/lib/image.h
src/lib/image_decoder.cc
src/lib/image_examiner.cc
src/lib/types.cc
src/lib/types.h
src/lib/util.cc
test/image_proxy_test.cc
test/image_test.cc
test/test.cc
test/video_level_test.cc [new file with mode: 0644]
test/wscript

index 602185bb8b4ade76013935cd6a096a08c957d995..c978fc3834f95f8e63c691876315744ace4c5746 100644 (file)
@@ -30,6 +30,7 @@
 extern "C" {
 #include <libavcodec/avcodec.h>
 #include <libavformat/avformat.h>
+#include <libavutil/pixdesc.h>
 }
 DCPOMATIC_DISABLE_WARNINGS
 #include <libxml++/libxml++.h>
@@ -48,23 +49,26 @@ using boost::optional;
 using boost::dynamic_pointer_cast;
 using dcp::raw_convert;
 
-FFmpegImageProxy::FFmpegImageProxy (boost::filesystem::path path)
+FFmpegImageProxy::FFmpegImageProxy (boost::filesystem::path path, VideoRange video_range)
        : _data (path)
+       , _video_range (video_range)
        , _pos (0)
        , _path (path)
 {
 
 }
 
-FFmpegImageProxy::FFmpegImageProxy (dcp::ArrayData data)
+FFmpegImageProxy::FFmpegImageProxy (dcp::ArrayData data, VideoRange video_range)
        : _data (data)
+       , _video_range (video_range)
        , _pos (0)
 {
 
 }
 
-FFmpegImageProxy::FFmpegImageProxy (shared_ptr<cxml::Node>, shared_ptr<Socket> socket)
-       : _pos (0)
+FFmpegImageProxy::FFmpegImageProxy (shared_ptr<cxml::Node> node, shared_ptr<Socket> socket)
+       : _video_range (string_to_video_range(node->string_child("VideoRange")))
+       , _pos (0)
 {
        uint32_t const size = socket->read_uint32 ();
        _data = dcp::ArrayData (size);
@@ -188,7 +192,16 @@ FFmpegImageProxy::image (optional<dcp::Size>) const
                throw DecodeError (N_("could not decode video"));
        }
 
-       _image.reset (new Image (frame));
+       AVPixelFormat const pix_fmt = static_cast<AVPixelFormat>(frame->format);
+
+       _image.reset (new Image(frame));
+       if (_video_range == VIDEO_RANGE_VIDEO && av_pix_fmt_desc_get(pix_fmt)->flags & AV_PIX_FMT_FLAG_RGB) {
+               /* Asking for the video range to be converted by libswscale (in Image) will not work for
+                * RGB sources since that method only processes video range in YUV and greyscale.  So we have
+                * to do it ourselves here.
+                */
+               _image->video_range_to_full_range();
+       }
 
        av_packet_unref (&packet);
        av_frame_free (&frame);
@@ -206,6 +219,7 @@ void
 FFmpegImageProxy::add_metadata (xmlpp::Node* node) const
 {
        node->add_child("Type")->add_child_text (N_("FFmpeg"));
+       node->add_child("VideoRange")->add_child_text(video_range_to_string(_video_range));
 }
 
 void
index 62b99d280f42e4b7f42f790e2cb52f0be66d475b..4fca899f49626c92223691e010f97df221a2bb37 100644 (file)
@@ -19,6 +19,7 @@
 */
 
 #include "image_proxy.h"
+#include "types.h"
 #include <dcp/array_data.h>
 #include <boost/thread/mutex.hpp>
 #include <boost/filesystem.hpp>
@@ -26,8 +27,8 @@
 class FFmpegImageProxy : public ImageProxy
 {
 public:
-       explicit FFmpegImageProxy (boost::filesystem::path);
-       explicit FFmpegImageProxy (dcp::ArrayData);
+       explicit FFmpegImageProxy (boost::filesystem::path, VideoRange video_range);
+       explicit FFmpegImageProxy (dcp::ArrayData, VideoRange video_range);
        FFmpegImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket);
 
        Result image (
@@ -44,6 +45,7 @@ public:
 
 private:
        dcp::ArrayData _data;
+       VideoRange _video_range;
        mutable int64_t _pos;
        /** Path of a file that this image came from, if applicable; stored so that
            failed-decode errors can give more detail.
index 03f1bf6dc626664dfd88727674629e4b6d8abef1..891715a46c2c1abcf9a1dad5401e05d227e4ab13 100644 (file)
@@ -1319,3 +1319,46 @@ Image::as_png () const
 
        return dcp::ArrayData (state.data, state.size);
 }
+
+
+void
+Image::video_range_to_full_range ()
+{
+       switch (_pixel_format) {
+       case AV_PIX_FMT_RGB24:
+       {
+               float const factor = 256.0 / 219.0;
+               uint8_t* p = data()[0];
+               int const lines = sample_size(0).height;
+               for (int y = 0; y < lines; ++y) {
+                       uint8_t* q = p;
+                       for (int x = 0; x < line_size()[0]; ++x) {
+                               *q = int((*q - 16) * factor);
+                               ++q;
+                       }
+                       p += stride()[0];
+               }
+               break;
+       }
+       case AV_PIX_FMT_GBRP12LE:
+       {
+               float const factor = 4096.0 / 3504.0;
+               for (int c = 0; c < 3; ++c) {
+                       uint16_t* p = reinterpret_cast<uint16_t*>(data()[c]);
+                       int const lines = sample_size(c).height;
+                       for (int y = 0; y < lines; ++y) {
+                               uint16_t* q = p;
+                               int const line_size_pixels = line_size()[c] / 2;
+                               for (int x = 0; x < line_size_pixels; ++x) {
+                                       *q = int((*q - 256) * factor);
+                                       ++q;
+                               }
+                       }
+               }
+               break;
+       }
+       default:
+               throw PixelFormatError ("video_range_to_full_range()", _pixel_format);
+       }
+}
+
index ab9b3c78a40f4436527f077b2facd3cb98d883d7..c648fda1b8f0598bb6d4a0738c1292b72f10e41a 100644 (file)
@@ -74,6 +74,7 @@ public:
        void alpha_blend (boost::shared_ptr<const Image> image, Position<int> pos);
        void copy (boost::shared_ptr<const Image> image, Position<int> pos);
        void fade (float);
+       void video_range_to_full_range ();
 
        void read_from_socket (boost::shared_ptr<Socket>);
        void write_to_socket (boost::shared_ptr<Socket>) const;
index 15187b11b6aa27ef084e24ea759c27163603194f..7757cc4aac47d869697704765f76f68b43ddf744 100644 (file)
@@ -70,7 +70,7 @@ ImageDecoder::pass ()
                        */
                        _image.reset (new J2KImageProxy (path, _image_content->video->size(), pf));
                } else {
-                       _image.reset (new FFmpegImageProxy (path));
+                       _image.reset (new FFmpegImageProxy(path, _image_content->video->range()));
                }
        }
 
index 6586a0d09bc5ad10ce09660a14d159ce5c695f6f..aa80d0daad8adcf424b55ca84088df23eddc98cb 100644 (file)
@@ -63,7 +63,7 @@ ImageExaminer::ImageExaminer (shared_ptr<const Film> film, shared_ptr<const Imag
                }
                delete[] buffer;
        } else {
-               FFmpegImageProxy proxy(content->path(0));
+               FFmpegImageProxy proxy(content->path(0), content->video->range());
                _video_size = proxy.image().image->size();
        }
 
index e7acf6992eec60d128be640525ba4b32680abc3a..5687a5d48b070c261a7559f03a945d8442fc6d6a 100644 (file)
@@ -229,3 +229,31 @@ bool operator== (NamedChannel const& a, NamedChannel const& b)
        return a.name == b.name && a.index == b.index;
 }
 
+
+string
+video_range_to_string (VideoRange r)
+{
+       switch (r) {
+       case VIDEO_RANGE_FULL:
+               return "full";
+       case VIDEO_RANGE_VIDEO:
+               return "video";
+       default:
+               DCPOMATIC_ASSERT (false);
+       }
+}
+
+
+VideoRange
+string_to_video_range (string s)
+{
+       if (s == "full") {
+               return VIDEO_RANGE_FULL;
+       } else if (s == "video") {
+               return VIDEO_RANGE_VIDEO;
+       }
+
+       DCPOMATIC_ASSERT (false);
+       return VIDEO_RANGE_FULL;
+}
+
index 2ba0408adb3019b2f59c6b6569948be117f7a27e..a10b26a6384fb355704920e43fd2fdf40d60fec5 100644 (file)
@@ -139,12 +139,17 @@ enum ChangeType
        CHANGE_TYPE_CANCELLED
 };
 
+
 enum VideoRange
 {
        VIDEO_RANGE_FULL, ///< full,  or "JPEG" (0-255 for 8-bit)
        VIDEO_RANGE_VIDEO ///< video, or "MPEG" (16-235 for 8-bit)
 };
 
+extern std::string video_range_to_string (VideoRange r);
+extern VideoRange string_to_video_range (std::string s);
+
+
 /** Type of captions.
  *
  *  The generally accepted definitions seem to be:
index ac868c17360068a429de9f5dec93abca4534b49e..0a060e96084ccf55cd9e913cbf88adc5d6c5afac 100644 (file)
@@ -953,7 +953,7 @@ void
 emit_subtitle_image (ContentTimePeriod period, dcp::SubtitleImage sub, dcp::Size size, shared_ptr<TextDecoder> decoder)
 {
        /* XXX: this is rather inefficient; decoding the image just to get its size */
-       FFmpegImageProxy proxy (sub.png_image());
+       FFmpegImageProxy proxy (sub.png_image(), VIDEO_RANGE_FULL);
        shared_ptr<Image> image = proxy.image().image;
        /* set up rect with height and width */
        dcpomatic::Rect<double> rect(0, 0, image->size().width / double(size.width), image->size().height / double(size.height));
index 061df9eed56522b5079de50e47a05abe487e39f9..0999d4b1064a164e4a1c82623a00336ecede2e3a 100644 (file)
@@ -53,14 +53,14 @@ BOOST_AUTO_TEST_CASE (j2k_image_proxy_same_test)
 BOOST_AUTO_TEST_CASE (ffmpeg_image_proxy_same_test)
 {
        {
-               shared_ptr<FFmpegImageProxy> proxy1(new FFmpegImageProxy(data_file0));
-               shared_ptr<FFmpegImageProxy> proxy2(new FFmpegImageProxy(data_file0));
+               shared_ptr<FFmpegImageProxy> proxy1(new FFmpegImageProxy(data_file0, VIDEO_RANGE_FULL));
+               shared_ptr<FFmpegImageProxy> proxy2(new FFmpegImageProxy(data_file0, VIDEO_RANGE_FULL));
                BOOST_CHECK (proxy1->same(proxy2));
        }
 
        {
-               shared_ptr<FFmpegImageProxy> proxy1(new FFmpegImageProxy(data_file0));
-               shared_ptr<FFmpegImageProxy> proxy2(new FFmpegImageProxy(data_file1));
+               shared_ptr<FFmpegImageProxy> proxy1(new FFmpegImageProxy(data_file0, VIDEO_RANGE_FULL));
+               shared_ptr<FFmpegImageProxy> proxy2(new FFmpegImageProxy(data_file1, VIDEO_RANGE_FULL));
                BOOST_CHECK (!proxy1->same(proxy2));
        }
 }
index bdd34c665df657f458fe6f47ee44c49b1c4e072e..e2b1d71c7d0899a8a7e60b4a242e0632f7706e62 100644 (file)
@@ -137,7 +137,7 @@ BOOST_AUTO_TEST_CASE (compact_image_test)
 void
 alpha_blend_test_one (AVPixelFormat format, string suffix)
 {
-       shared_ptr<FFmpegImageProxy> proxy (new FFmpegImageProxy (TestPaths::private_data() / "prophet_frame.tiff"));
+       shared_ptr<FFmpegImageProxy> proxy (new FFmpegImageProxy (TestPaths::private_data() / "prophet_frame.tiff", VIDEO_RANGE_FULL));
        shared_ptr<Image> raw = proxy->image().image;
        shared_ptr<Image> background = raw->convert_pixel_format (dcp::YUV_TO_RGB_REC709, format, true, false);
 
@@ -259,7 +259,7 @@ BOOST_AUTO_TEST_CASE (merge_test2)
 /** Test Image::crop_scale_window with YUV420P and some windowing */
 BOOST_AUTO_TEST_CASE (crop_scale_window_test)
 {
-       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/flat_red.png"));
+       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/flat_red.png", VIDEO_RANGE_FULL));
        shared_ptr<Image> raw = proxy->image().image;
        shared_ptr<Image> out = raw->crop_scale_window(Crop(), dcp::Size(1998, 836), dcp::Size(1998, 1080), dcp::YUV_TO_RGB_REC709, VIDEO_RANGE_FULL, AV_PIX_FMT_YUV420P, true, false);
        shared_ptr<Image> save = out->scale(dcp::Size(1998, 1080), dcp::YUV_TO_RGB_REC709, AV_PIX_FMT_RGB24, false, false);
@@ -277,7 +277,7 @@ BOOST_AUTO_TEST_CASE (crop_scale_window_test2)
 
 BOOST_AUTO_TEST_CASE (crop_scale_window_test3)
 {
-       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png"));
+       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png", VIDEO_RANGE_FULL));
        shared_ptr<Image> xyz = proxy->image().image->convert_pixel_format(dcp::YUV_TO_RGB_REC709, AV_PIX_FMT_RGB24, true, false);
        shared_ptr<Image> cropped = xyz->crop_scale_window(Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUV_TO_RGB_REC709, VIDEO_RANGE_FULL, AV_PIX_FMT_RGB24, false, false);
        write_image(cropped, "build/test/crop_scale_window_test3.png");
@@ -286,7 +286,7 @@ BOOST_AUTO_TEST_CASE (crop_scale_window_test3)
 
 BOOST_AUTO_TEST_CASE (crop_scale_window_test4)
 {
-       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png"));
+       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png", VIDEO_RANGE_FULL));
        shared_ptr<Image> xyz = proxy->image().image->convert_pixel_format(dcp::YUV_TO_RGB_REC709, AV_PIX_FMT_RGB24, true, false);
        shared_ptr<Image> cropped = xyz->crop_scale_window(Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUV_TO_RGB_REC709, VIDEO_RANGE_FULL, AV_PIX_FMT_XYZ12LE, false, false);
        write_image(cropped, "build/test/crop_scale_window_test4.png");
@@ -295,7 +295,7 @@ BOOST_AUTO_TEST_CASE (crop_scale_window_test4)
 
 BOOST_AUTO_TEST_CASE (crop_scale_window_test5)
 {
-       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png"));
+       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png", VIDEO_RANGE_FULL));
        shared_ptr<Image> xyz = proxy->image().image->convert_pixel_format(dcp::YUV_TO_RGB_REC709, AV_PIX_FMT_XYZ12LE, true, false);
        shared_ptr<Image> cropped = xyz->crop_scale_window(Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUV_TO_RGB_REC709, VIDEO_RANGE_FULL, AV_PIX_FMT_RGB24, false, false);
        write_image(cropped, "build/test/crop_scale_window_test5.png");
@@ -304,7 +304,7 @@ BOOST_AUTO_TEST_CASE (crop_scale_window_test5)
 
 BOOST_AUTO_TEST_CASE (crop_scale_window_test6)
 {
-       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png"));
+       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/player_seek_test_0.png", VIDEO_RANGE_FULL));
        shared_ptr<Image> xyz = proxy->image().image->convert_pixel_format(dcp::YUV_TO_RGB_REC709, AV_PIX_FMT_XYZ12LE, true, false);
        shared_ptr<Image> cropped = xyz->crop_scale_window(Crop(512, 0, 0, 0), dcp::Size(1486, 1080), dcp::Size(1998, 1080), dcp::YUV_TO_RGB_REC709, VIDEO_RANGE_FULL, AV_PIX_FMT_XYZ12LE, false, false);
        write_image(cropped, "build/test/crop_scale_window_test6.png");
@@ -313,7 +313,7 @@ BOOST_AUTO_TEST_CASE (crop_scale_window_test6)
 
 BOOST_AUTO_TEST_CASE (as_png_test)
 {
-       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/3d_test/000001.png"));
+       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/3d_test/000001.png", VIDEO_RANGE_FULL));
        shared_ptr<Image> image_rgb = proxy->image().image;
        shared_ptr<Image> image_bgr = image_rgb->convert_pixel_format(dcp::YUV_TO_RGB_REC709, AV_PIX_FMT_BGRA, true, false);
        image_rgb->as_png().write ("build/test/as_png_rgb.png");
@@ -339,7 +339,7 @@ fade_test_format_black (AVPixelFormat f, string name)
 static void
 fade_test_format_red (AVPixelFormat f, float amount, string name)
 {
-       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/flat_red.png"));
+       shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/flat_red.png", VIDEO_RANGE_FULL));
        shared_ptr<Image> red = proxy->image().image->convert_pixel_format(dcp::YUV_TO_RGB_REC709, f, true, false);
        red->fade (amount);
        string const filename = "fade_test_red_" + name + ".png";
index 9ac202b8051c11a305c058fcbc8dd417b7162414..5981697fbccf799799d4a33c2f9fd71a896b8fe5 100644 (file)
@@ -336,9 +336,9 @@ static
 double
 rms_error (boost::filesystem::path ref, boost::filesystem::path check)
 {
-       FFmpegImageProxy ref_proxy (ref);
+       FFmpegImageProxy ref_proxy (ref, VIDEO_RANGE_FULL);
        shared_ptr<Image> ref_image = ref_proxy.image().image;
-       FFmpegImageProxy check_proxy (check);
+       FFmpegImageProxy check_proxy (check, VIDEO_RANGE_FULL);
        shared_ptr<Image> check_image = check_proxy.image().image;
 
        BOOST_REQUIRE_EQUAL (ref_image->pixel_format(), check_image->pixel_format());
diff --git a/test/video_level_test.cc b/test/video_level_test.cc
new file mode 100644 (file)
index 0000000..2849910
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+    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/>.
+
+*/
+
+
+/** @file  test/video_level_test.cc
+ *  @brief Test that video level ranges are handled correctly.
+ *  @ingroup specific
+ */
+
+
+#include "lib/ffmpeg_image_proxy.h"
+#include "lib/image.h"
+#include "test.h"
+#include <boost/test/unit_test.hpp>
+
+
+using boost::shared_ptr;
+
+
+static
+shared_ptr<Image>
+grey_image (dcp::Size size, uint8_t pixel)
+{
+       shared_ptr<Image> grey(new Image(AV_PIX_FMT_RGB24, size, true));
+       for (int y = 0; y < size.height; ++y) {
+               uint8_t* p = grey->data()[0] + y * grey->stride()[0];
+               for (int x = 0; x < size.width; ++x) {
+                       *p++ = pixel;
+                       *p++ = pixel;
+                       *p++ = pixel;
+               }
+       }
+
+       return grey;
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_image_full_range_not_changed)
+{
+       dcp::Size size(640, 480);
+       uint8_t const grey_pixel = 128;
+       boost::filesystem::path const file = "build/test/ffmpeg_image_full_range_not_changed.png";
+
+       write_image (grey_image(size, grey_pixel), file);
+
+       FFmpegImageProxy proxy (file, VIDEO_RANGE_FULL);
+       ImageProxy::Result result = proxy.image ();
+       BOOST_REQUIRE (!result.error);
+
+       for (int y = 0; y < size.height; ++y) {
+               uint8_t* p = result.image->data()[0] + y * result.image->stride()[0];
+               for (int x = 0; x < size.width; ++x) {
+                       BOOST_REQUIRE (*p++ == grey_pixel);
+               }
+       }
+}
+
+
+BOOST_AUTO_TEST_CASE (ffmpeg_image_video_range_expanded)
+{
+       dcp::Size size(640, 480);
+       uint8_t const grey_pixel = 128;
+       uint8_t const expanded_grey_pixel = static_cast<uint8_t>((grey_pixel - 16) * 256.0 / 219);
+       boost::filesystem::path const file = "build/test/ffmpeg_image_video_range_expanded.png";
+
+       write_image (grey_image(size, grey_pixel), file);
+
+       FFmpegImageProxy proxy (file, VIDEO_RANGE_VIDEO);
+       ImageProxy::Result result = proxy.image ();
+       BOOST_REQUIRE (!result.error);
+
+       for (int y = 0; y < size.height; ++y) {
+               uint8_t* p = result.image->data()[0] + y * result.image->stride()[0];
+               for (int x = 0; x < size.width; ++x) {
+                       BOOST_REQUIRE_EQUAL (*p++, expanded_grey_pixel);
+               }
+       }
+}
+
+
index 50fe91b43f635d0a975d054f82a5335694ac4fdd..176f4ca675ed70d5b99b91538fceff53039683a2 100644 (file)
@@ -130,6 +130,7 @@ def build(bld):
                  util_test.cc
                  vf_test.cc
                  video_content_scale_test.cc
+                 video_level_test.cc
                  video_mxf_content_test.cc
                  vf_kdm_test.cc
                  zipper_test.cc