summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2020-11-17 00:00:50 +0100
committerCarl Hetherington <cth@carlh.net>2020-11-17 09:59:30 +0100
commit27c0e43d75218b926068fff3d6d1364b476e56ff (patch)
tree47328853ebd6dae4859d67fd0eac0051dd1f5f4e
parent451e5465bbd10827dafe76b35704102e589c0980 (diff)
Fix cropping of subsampled images.
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.
-rw-r--r--src/lib/image.cc62
m---------test/data0
-rw-r--r--test/image_test.cc26
3 files changed, 69 insertions, 19 deletions
diff --git a/src/lib/image.cc b/src/lib/image.cc
index 6becdea42..1a5b11ecb 100644
--- a/src/lib/image.cc
+++ b/src/lib/image.cc
@@ -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.
@@ -148,8 +165,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 (
@@ -187,35 +219,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));
}
@@ -228,7 +252,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.
diff --git a/test/data b/test/data
-Subproject 054ce8c174147046d343cf1972e4e1405745ad8
+Subproject 3b2a8c56dff15050a9eca576214399ef4a5abc0
diff --git a/test/image_test.cc b/test/image_test.cc
index 3c89d5da7..9fe793d70 100644
--- a/test/image_test.cc
+++ b/test/image_test.cc
@@ -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"
@@ -307,6 +308,31 @@ BOOST_AUTO_TEST_CASE (crop_scale_window_test6)
write_image(cropped, "build/test/crop_scale_window_test6.png", "RGB", MagickCore::ShortPixel);
}
+
+/** 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"));
+ shared_ptr<Image> yuv = proxy->image().first->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,
+ AV_PIX_FMT_RGB24,
+ true,
+ false
+ );
+ path file = String::compose("crop_scale_window_test7-%1.png", left_crop);
+ write_image(cropped, path("build") / "test" / file, "RGB");
+ 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"));