Fix cropping of subsampled images.
authorCarl Hetherington <cth@carlh.net>
Mon, 16 Nov 2020 23:00:50 +0000 (00:00 +0100)
committerCarl Hetherington <cth@carlh.net>
Mon, 16 Nov 2020 23:16:58 +0000 (00:16 +0100)
The calculations for how to crop subsampled components of YUV images
were wrong, causing strange effects like misregistration of colour
components in cropped images.  Should fix #1872.

src/lib/image.cc
test/data
test/image_test.cc

index 9a9fb8c43f12751e614a83c19ad586798cf80d66..abbc6e71aa87c5bdee0983496850f9efd48f5329 100644 (file)
@@ -122,6 +122,23 @@ Image::planes () const
        return d->nb_components;
 }
 
+
+static
+int
+round_width_for_subsampling (int p, AVPixFmtDescriptor const * desc)
+{
+       return p & ~ ((1 << desc->log2_chroma_w) - 1);
+}
+
+
+static
+int
+round_height_for_subsampling (int p, AVPixFmtDescriptor const * desc)
+{
+       return p & ~ ((1 << desc->log2_chroma_h) - 1);
+}
+
+
 /** Crop this image, scale it to `inter_size' and then place it in a black frame of `out_size'.
  *  @param crop Amount to crop by.
  *  @param inter_size Size to scale the cropped image to.
@@ -158,8 +175,23 @@ Image::crop_scale_window (
        shared_ptr<Image> out (new Image(out_format, out_size, out_aligned));
        out->make_black ();
 
+       AVPixFmtDescriptor const * in_desc = av_pix_fmt_desc_get (_pixel_format);
+       if (!in_desc) {
+               throw PixelFormatError ("crop_scale_window()", _pixel_format);
+       }
+
+       /* Round down so that we crop only the number of pixels that is straightforward
+        * considering any subsampling.
+        */
+       Crop rounded_crop(
+               round_width_for_subsampling(crop.left, in_desc),
+               round_width_for_subsampling(crop.right, in_desc),
+               round_height_for_subsampling(crop.top, in_desc),
+               round_height_for_subsampling(crop.bottom, in_desc)
+               );
+
        /* Size of the image after any crop */
-       dcp::Size const cropped_size = crop.apply (size ());
+       dcp::Size const cropped_size = rounded_crop.apply (size());
 
        /* Scale context for a scale from cropped_size to inter_size */
        struct SwsContext* scale_context = sws_getContext (
@@ -196,35 +228,27 @@ Image::crop_scale_window (
                0, 1 << 16, 1 << 16
                );
 
-       AVPixFmtDescriptor const * in_desc = av_pix_fmt_desc_get (_pixel_format);
-       if (!in_desc) {
-               throw PixelFormatError ("crop_scale_window()", _pixel_format);
-       }
-
        /* Prepare input data pointers with crop */
        uint8_t* scale_in_data[planes()];
        for (int c = 0; c < planes(); ++c) {
-               /* To work out the crop in bytes, start by multiplying
-                  the crop by the (average) bytes per pixel.  Then
-                  round down so that we don't crop a subsampled pixel until
-                  we've cropped all of its Y-channel pixels.
-               */
-               int const x = lrintf (bytes_per_pixel(c) * crop.left) & ~ ((int) in_desc->log2_chroma_w);
-               scale_in_data[c] = data()[c] + x + stride()[c] * (crop.top / vertical_factor(c));
+               int const x = lrintf(bytes_per_pixel(c) * rounded_crop.left);
+               scale_in_data[c] = data()[c] + x + stride()[c] * (rounded_crop.top / vertical_factor(c));
        }
 
-       /* Corner of the image within out_size */
-       Position<int> const corner ((out_size.width - inter_size.width) / 2, (out_size.height - inter_size.height) / 2);
-
        AVPixFmtDescriptor const * out_desc = av_pix_fmt_desc_get (out_format);
        if (!out_desc) {
                throw PixelFormatError ("crop_scale_window()", out_format);
        }
 
+       /* Corner of the image within out_size */
+       Position<int> const corner (
+               round_width_for_subsampling((out_size.width - inter_size.width) / 2, out_desc),
+               round_height_for_subsampling((out_size.height - inter_size.height) / 2, out_desc)
+               );
+
        uint8_t* scale_out_data[out->planes()];
        for (int c = 0; c < out->planes(); ++c) {
-               /* See the note in the crop loop above */
-               int const x = lrintf (out->bytes_per_pixel(c) * corner.x) & ~ ((int) out_desc->log2_chroma_w);
+               int const x = lrintf(out->bytes_per_pixel(c) * corner.x);
                scale_out_data[c] = out->data()[c] + x + out->stride()[c] * (corner.y / out->vertical_factor(c));
        }
 
@@ -237,7 +261,7 @@ Image::crop_scale_window (
 
        sws_freeContext (scale_context);
 
-       if (crop != Crop() && cropped_size == inter_size && _pixel_format == out_format) {
+       if (rounded_crop != Crop() && cropped_size == inter_size) {
                /* We are cropping without any scaling or pixel format conversion, so FFmpeg may have left some
                   data behind in our image.  Clear it out.  It may get to the point where we should just stop
                   trying to be clever with cropping.
index afc23969020af54cfb5e6e2f368ca7d395ae4282..30022e624852127f464933f5008f8ac6226d831c 160000 (submodule)
--- a/test/data
+++ b/test/data
@@ -1 +1 @@
-Subproject commit afc23969020af54cfb5e6e2f368ca7d395ae4282
+Subproject commit 30022e624852127f464933f5008f8ac6226d831c
index 2f2c9a9b70a92fd30b23ade993b47c2431cb3c5c..0aa450321b18be1f02bd0cf2bd41e8cef4e804d1 100644 (file)
@@ -24,6 +24,7 @@
  *  @see test/make_black_test.cc, test/pixel_formats_test.cc
  */
 
+#include "lib/compose.hpp"
 #include "lib/image.h"
 #include "lib/ffmpeg_image_proxy.h"
 #include "test.h"
@@ -325,6 +326,33 @@ BOOST_AUTO_TEST_CASE (crop_scale_window_test6)
        check_image("test/data/crop_scale_window_test6.png", "build/test/crop_scale_window_test6.png", 35000);
 }
 
+
+/** Test some small crops with an image that shows up errors in registration of the YUV planes (#1872) */
+BOOST_AUTO_TEST_CASE (crop_scale_window_test7)
+{
+       using namespace boost::filesystem;
+       for (int left_crop = 0; left_crop < 8; ++left_crop) {
+               shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/rgb_grey_testcard.png", VIDEO_RANGE_FULL));
+               shared_ptr<Image> yuv = proxy->image().image->convert_pixel_format(dcp::YUV_TO_RGB_REC709, AV_PIX_FMT_YUV420P, true, false);
+               int rounded = left_crop - (left_crop % 2);
+               shared_ptr<Image> cropped = yuv->crop_scale_window(
+                       Crop(left_crop, 0, 0, 0),
+                       dcp::Size(1998 - rounded, 1080),
+                       dcp::Size(1998 - rounded, 1080),
+                       dcp::YUV_TO_RGB_REC709,
+                       VIDEO_RANGE_VIDEO,
+                       AV_PIX_FMT_RGB24,
+                       VIDEO_RANGE_VIDEO,
+                       true,
+                       false
+                       );
+               path file = String::compose("crop_scale_window_test7-%1.png", left_crop);
+               write_image(cropped, path("build") / "test" / file);
+               check_image(path("test") / "data" / file, path("build") / "test" / file, 10);
+       }
+}
+
+
 BOOST_AUTO_TEST_CASE (as_png_test)
 {
        shared_ptr<FFmpegImageProxy> proxy(new FFmpegImageProxy("test/data/3d_test/000001.png", VIDEO_RANGE_FULL));