blob: b2121741623606d89dc2f24326b3b9526b8f49e0 [file] [log] [blame]
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Decoding to custom color space test.
#include <string>
#include <tuple>
#include "extras/ccsp_imageio.h"
#include "extras/extras.h"
#include "imageio/image_dec.h"
#include "include/helpers.h"
#include "src/dsp/math.h"
#include "src/utils/csp.h"
#include "src/utils/plane.h"
#include "src/wp2/decode.h"
#include "src/wp2/encode.h"
namespace WP2 {
namespace {
//------------------------------------------------------------------------------
// Matrices and associated shifts from extras.h
enum class Matrix { kRGB8, kRGB10, kYCoCg, kYpUV };
constexpr const int16_t* const kRGBToCCSPMatrices[] = {
kRGBToRGB8Matrix, kRGBToRGB10Matrix, kRGBToYCoCgMatrix, kRGBToYpUVMatrix};
constexpr const uint32_t kRGBToCCSPShifts[] = {
kRGBToRGB8Shift, kRGBToRGB10Shift, kRGBToYCoCgShift, kRGBToYpUVShift};
class DecodeCustomCspTest
: public testing::TestWithParam<
std::tuple<std::string, bool, float, Matrix, bool, bool, bool, bool,
bool, bool>> {
void SetUp() override { WP2CSPConverterInit(); }
};
//------------------------------------------------------------------------------
TEST_P(DecodeCustomCspTest, Comparison) {
const std::string& src_file_name = std::get<0>(GetParam());
const bool keep_alpha = std::get<1>(GetParam());
const float quality = std::get<2>(GetParam());
const Matrix matrix = std::get<3>(GetParam());
const int16_t* const rgb_to_ccsp_matrix = kRGBToCCSPMatrices[(int)matrix];
const uint32_t rgb_to_ccsp_shift = kRGBToCCSPShifts[(int)matrix];
const bool pad_y = std::get<4>(GetParam());
const bool pad_u = std::get<5>(GetParam());
const bool pad_v = std::get<6>(GetParam());
const bool pad_a = std::get<7>(GetParam());
const bool get_alpha = std::get<8>(GetParam());
const bool get_metadata = std::get<9>(GetParam());
ArgbBuffer src;
ASSERT_WP2_OK(
ReadImage(testutil::GetTestDataPath(src_file_name).c_str(), &src));
if (!keep_alpha) {
ASSERT_WP2_OK(src.CompositeOver(Argb32b{255, 0, 0, 0}));
}
const bool is_opaque = !src.HasTransparency();
EncoderConfig encoder_config = EncoderConfig::kDefault;
encoder_config.quality = quality;
MemoryWriter data;
ASSERT_WP2_OK(Encode(src, &data, encoder_config));
// Decode through the regular pipeline to have a reference output.
ArgbBuffer ref;
ASSERT_WP2_OK(Decode(data.mem_, data.size_, &ref));
// No point in going further if the reference does not pass.
ASSERT_TRUE(testutil::Compare(src, ref, src_file_name,
testutil::GetExpectedDistortion(quality)));
YUVPlane custom_output; // Might contain RGB.
Metadata metadata;
const uint32_t padded_width = Pad(src.width(), kPredWidth);
const uint32_t padded_height = Pad(src.height(), kPredWidth);
ASSERT_WP2_OK(custom_output.Y.Resize(pad_y ? padded_width : src.width(),
pad_y ? padded_height : src.height()));
ASSERT_WP2_OK(custom_output.U.Resize(pad_u ? padded_width : src.width(),
pad_u ? padded_height : src.height()));
ASSERT_WP2_OK(custom_output.V.Resize(pad_v ? padded_width : src.width(),
pad_v ? padded_height : src.height()));
if (get_alpha) {
ASSERT_WP2_OK(custom_output.A.Resize(pad_a ? padded_width : src.width(),
pad_a ? padded_height : src.height()));
}
ASSERT_WP2_OK(Decode(
data.mem_, data.size_, rgb_to_ccsp_matrix, rgb_to_ccsp_shift,
custom_output.Y.Row(0), custom_output.Y.Step(), custom_output.Y.Size(),
custom_output.U.Row(0), custom_output.U.Step(), custom_output.U.Size(),
custom_output.V.Row(0), custom_output.V.Step(), custom_output.V.Size(),
custom_output.A.IsEmpty() ? nullptr : custom_output.A.Row(0),
custom_output.A.Step(), custom_output.A.Size(),
get_metadata ? &metadata : nullptr));
if (get_metadata) {
EXPECT_TRUE(testutil::HasSameData(metadata.iccp, ref.metadata_.iccp));
EXPECT_TRUE(testutil::HasSameData(metadata.xmp, ref.metadata_.xmp));
EXPECT_TRUE(testutil::HasSameData(metadata.exif, ref.metadata_.exif));
}
switch (matrix) {
case Matrix::kRGB8:
case Matrix::kRGB10: {
// Try the YUV pipeline with a custom YUV which is actually RGB, for
// easier comparison.
const YUVPlane& rgb = custom_output;
// Change the container but keep the data as is, except kRGB10 shift.
ArgbBuffer output;
ASSERT_WP2_OK(output.Resize(ref.width(), ref.height()));
for (uint32_t y = 0; y < ref.height(); ++y) {
for (uint32_t x = 0; x < ref.width(); ++x) {
const uint32_t px = x * WP2FormatBpp(output.format());
uint8_t* const pixel = &(output.GetRow8(y))[px];
pixel[0] = rgb.A.IsEmpty() ? (ref.GetRow8(y))[px + 0]
: (uint8_t)rgb.A.At(x, y);
constexpr int16_t min = 0, max = 255;
if (matrix == Matrix::kRGB8) {
pixel[1] = (uint8_t)Clamp(rgb.Y.At(x, y), min, max);
pixel[2] = (uint8_t)Clamp(rgb.U.At(x, y), min, max);
pixel[3] = (uint8_t)Clamp(rgb.V.At(x, y), min, max);
} else {
pixel[1] =
(uint8_t)Clamp(RightShiftRound(rgb.Y.At(x, y), 2), min, max);
pixel[2] =
(uint8_t)Clamp(RightShiftRound(rgb.U.At(x, y), 2), min, max);
pixel[3] =
(uint8_t)Clamp(RightShiftRound(rgb.V.At(x, y), 2), min, max);
}
}
}
// Compare the source input with the custom RGB output.
EXPECT_TRUE(testutil::Compare(src, output, src_file_name,
testutil::GetExpectedDistortion(quality)));
// Compare the reference output with the custom RGB output.
// For kRGB10 there might be a slight difference due to rounding.
// For transparent images, the lack of clamping by alpha of
// CSPTransform::YuvToCustom() can lead to noticeable differences.
const float expected_disto =
is_opaque ? ((matrix == Matrix::kRGB10) ? 45.f : 99.f) : 35.f;
EXPECT_TRUE(
testutil::Compare(ref, output, src_file_name, expected_disto));
break;
}
case Matrix::kYCoCg:
case Matrix::kYpUV: {
// Actual luma/chroma output.
CSPTransform csp_transform; // Match Decode() behavior.
constexpr int16_t kRgbAvg[3] = {0, 0, 0};
if (matrix == Matrix::kYCoCg) {
// 1 1 -1 with 10b fixed-point precision (x 1<<10)
// 1 0 1 so that the inverse will be 14b and thus
// 1 -1 -1 after the shift of 12b, 8+2b remain
constexpr int16_t kYCoCgMatrix[] = {1024, 1024, -1024, 1024, 0,
1024, 1024, -1024, -1024};
ASSERT_TRUE(csp_transform.Init(kYCoCgMatrix, kRgbAvg));
} else {
// 1.0 0.00000 1.13983 with 9b fixed-point precision (x 1<<9)
// 1.0 -0.39465 -0.58060 so that the inverse will be 15b and thus
// 1.0 2.03211 0.00000 after the shift of 12b, 8+3b remain
constexpr int16_t kYpUVMatrix[] = {512, 0, 584, 512, -202,
-297, 512, 1040, 0};
ASSERT_TRUE(csp_transform.Init(kYpUVMatrix, kRgbAvg));
}
YUVPlane ref_yuv;
ASSERT_WP2_OK(ref_yuv.Import(ref, get_alpha, csp_transform,
/*resize_if_needed=*/true));
// SetView() plane by plane to avoid IsSubsampled() checks.
YUVPlane non_padded_yuv;
const Rectangle rect = ref.AsRect();
ASSERT_WP2_OK(non_padded_yuv.Y.SetView(custom_output.Y, rect));
ASSERT_WP2_OK(non_padded_yuv.U.SetView(custom_output.U, rect));
ASSERT_WP2_OK(non_padded_yuv.V.SetView(custom_output.V, rect));
if (!custom_output.A.IsEmpty()) {
ASSERT_WP2_OK(non_padded_yuv.A.SetView(custom_output.A, rect));
}
// Some error is expected for lossy qualities because YCoCg is no longer
// "lossless" when it has been tampered with during compression (it can
// output RGB values that are not exact multiples of 1<<kMtxShift).
// For transparent images, the lack of clamping by alpha of
// CSPTransform::YuvToCustom() can lead to noticeable differences. There
// might also be some slight rounding discrepancies.
const float expected_disto =
(quality <= kMaxLossyQuality) ? (is_opaque ? 55.f : 45.f) : 85.f;
EXPECT_TRUE(testutil::Compare(ref_yuv, non_padded_yuv,
csp_transform.GetYuvDepth(),
src_file_name, expected_disto))
<< "quality : " << quality;
break;
}
}
}
//------------------------------------------------------------------------------
INSTANTIATE_TEST_SUITE_P(
DecodeCustomCspTestInstantiation, DecodeCustomCspTest,
testing::Combine(testing::Values("source1_1x1.png", "source1_64x48.png",
"test_exif_xmp.webp"),
testing::Values(false) /* keep_alpha */,
testing::Values(5.f, 100.f) /* quality */,
testing::Values(Matrix::kRGB8, Matrix::kRGB10,
Matrix::kYCoCg, Matrix::kYpUV),
testing::Values(false, true) /* get_alpha */,
testing::Values(false, true) /* pad_y */,
testing::Values(true) /* pad_u */,
testing::Values(true) /* pad_v */,
testing::Values(true) /* pad_a */,
testing::Values(false) /* get_metadata */));
INSTANTIATE_TEST_SUITE_P(
DecodeCustomCspTestInstantiationPadding, DecodeCustomCspTest,
testing::Combine(testing::Values("source1_1x1.png"),
testing::Values(false, true) /* keep_alpha */,
testing::Values(5.f, 100.f) /* quality */,
testing::Values(Matrix::kRGB8),
testing::Values(false, true) /* get_alpha */,
testing::Values(false, true) /* pad_y */,
testing::Values(false, true) /* pad_u */,
testing::Values(false, true) /* pad_v */,
testing::Values(false, true) /* pad_a */,
testing::Values(false) /* get_metadata */));
INSTANTIATE_TEST_SUITE_P(
DecodeCustomCspTestInstantiationMetadata, DecodeCustomCspTest,
testing::Combine(
testing::Values("source1_1x1.png", "test_exif_xmp.webp"),
testing::Values(false, true) /* keep_alpha */,
testing::Values(50.f) /* quality */, testing::Values(Matrix::kYCoCg),
testing::Values(true) /* get_alpha */,
testing::Values(false) /* pad_y */, testing::Values(true) /* pad_u */,
testing::Values(false) /* pad_v */, testing::Values(true) /* pad_a */,
testing::Values(true) /* get_metadata */));
//------------------------------------------------------------------------------
static const struct BufferParam {
const char* const label;
const char* const src_file_name;
uint32_t r_step, r_buffer_size;
uint32_t g_step, g_buffer_size;
uint32_t b_step, b_buffer_size;
uint32_t a_step, a_buffer_size;
WP2Status expected_status;
} kBufferParams[] = {
{ "0-sized buffers are nullptr.",
"source1_1x1.png", 0, 0, 0, 0, 0, 0, 0, 0, WP2_STATUS_NULL_PARAMETER },
{ "allowed steps.",
"source1_1x1.png", 2, 4, 2, 4, 1, 2, 3, 6, WP2_STATUS_OK },
{ "Odd buffer sizes are allowed.",
"source1_1x1.png", 2, 5, 2, 5, 2, 7, 4, 9, WP2_STATUS_OK },
{ "buffer size are ok (reference).",
"source1_1x1.png", 8, 16, 8, 16, 10, 20, 8, 16, WP2_STATUS_OK },
{ "Smaller C0-buffer size than stride.", "source1_1x1.png",
8, 15, 8, 16, 10, 20, 8, 16, WP2_STATUS_BAD_DIMENSION },
{ "Smaller C1-buffer size than stride.", "source1_1x1.png",
8, 16, 8, 15, 10, 20, 8, 16, WP2_STATUS_BAD_DIMENSION },
{ "Smaller C2-buffer size than stride.", "source1_1x1.png",
8, 16, 8, 16, 10, 19, 8, 16, WP2_STATUS_BAD_DIMENSION },
{ "Smaller A-buffer size than stride.", "source1_1x1.png",
8, 16, 8, 16, 10, 20, 8, 15, WP2_STATUS_BAD_DIMENSION },
{ "Buffer is too small.",
"source1_32x32.png", 32, 2048, 32, 2046, 32, 2048, 0, 0,
WP2_STATUS_BAD_DIMENSION },
{ "Buffer is fine.",
"source1_32x32.png", 32, 2048, 32, 2048, 32, 2048, 0, 0, WP2_STATUS_OK },
{ "Buffer is fine.",
"source1_32x32.png", 33, 4000, 35, 3000, 34, 2887, 0, 0, WP2_STATUS_OK }
};
class StrideBufferSizeTest : public testing::TestWithParam<BufferParam> {};
TEST_P(StrideBufferSizeTest, Combination) {
const BufferParam& p = GetParam();
MemoryWriter data;
ASSERT_WP2_OK(testutil::CompressImage(p.src_file_name, &data));
Data r, g, b, a;
ASSERT_WP2_OK(r.Resize(p.r_buffer_size, /*keep_bytes=*/false));
ASSERT_WP2_OK(g.Resize(p.g_buffer_size, /*keep_bytes=*/false));
ASSERT_WP2_OK(b.Resize(p.b_buffer_size, /*keep_bytes=*/false));
ASSERT_WP2_OK(a.Resize(p.a_buffer_size, /*keep_bytes=*/false));
int16_t* const r_buffer = reinterpret_cast<int16_t*>(r.bytes);
int16_t* const g_buffer = reinterpret_cast<int16_t*>(g.bytes);
int16_t* const b_buffer = reinterpret_cast<int16_t*>(b.bytes);
int16_t* const a_buffer = reinterpret_cast<int16_t*>(a.bytes);
ASSERT_EQ(Decode(data.mem_, data.size_, kRGBToRGB8Matrix, kRGBToRGB8Shift,
r_buffer, p.r_step, p.r_buffer_size,
g_buffer, p.g_step, p.g_buffer_size,
b_buffer, p.b_step, p.b_buffer_size,
a_buffer, p.a_step, p.a_buffer_size),
p.expected_status) << p.label;
}
INSTANTIATE_TEST_SUITE_P(StrideBufferSizeTestInstantiation,
StrideBufferSizeTest,
testing::ValuesIn(kBufferParams));
//------------------------------------------------------------------------------
class YUVPlaneTest : public testing::TestWithParam<std::string> {};
TEST_P(YUVPlaneTest, LosslessUpsampling) {
const std::string& file_name = GetParam();
const std::string file_path = testutil::GetTestDataPath(file_name);
YUVPlane original;
CSPMtx ccsp_to_rgb = {};
ASSERT_WP2_OK(ReadImage(file_path.c_str(), &original, &ccsp_to_rgb));
// Ignoring the 'ccsp_to_rgb_matrix', the color space is not tested here.
ASSERT_WP2_OK(original.Downsample());
BitDepth bit_depth;
ASSERT_WP2_OK(ReadBitDepth(file_path.c_str(), &bit_depth));
YUVPlane upsampled;
// Upsampling then downsampling with no interpolation is lossless.
ASSERT_WP2_OK(upsampled.UpsampleFrom(original, SamplingTaps::kUpNearest));
ASSERT_WP2_OK(upsampled.Downsample(SamplingTaps::kDownAvg));
EXPECT_TRUE(testutil::Compare(original, upsampled, bit_depth, file_name));
// Lossy upsampling and downsampling.
ASSERT_WP2_OK(upsampled.UpsampleFrom(original, SamplingTaps::kUpSmooth));
ASSERT_WP2_OK(upsampled.Downsample(SamplingTaps::kDownSharp));
EXPECT_TRUE(
testutil::Compare(original, upsampled, bit_depth, file_name, 30.f));
}
TEST_P(YUVPlaneTest, NotTooLossyDownsampling) {
const std::string& file_name = GetParam();
const std::string file_path = testutil::GetTestDataPath(file_name);
YUVPlane original;
CSPMtx ccsp_to_rgb = {};
ASSERT_WP2_OK(ReadImage(file_path.c_str(), &original, &ccsp_to_rgb));
// Ignoring 'ccsp_to_rgb' because the color space is not tested here.
BitDepth bit_depth;
ASSERT_WP2_OK(ReadBitDepth(file_path.c_str(), &bit_depth));
YUVPlane downsampled;
// ouch
ASSERT_WP2_OK(downsampled.DownsampleFrom(original, SamplingTaps::kDownAvg));
ASSERT_WP2_OK(downsampled.Upsample(SamplingTaps::kUpNearest));
EXPECT_TRUE(
testutil::Compare(original, downsampled, bit_depth, file_name, 28.f));
// save the furniture
ASSERT_WP2_OK(downsampled.DownsampleFrom(original, SamplingTaps::kDownSharp));
ASSERT_WP2_OK(downsampled.Upsample(SamplingTaps::kUpSmooth));
EXPECT_TRUE(
testutil::Compare(original, downsampled, bit_depth, file_name, 29.f));
float disto[5];
ASSERT_WP2_OK(downsampled.GetDistortion(original, bit_depth, PSNR, disto));
EXPECT_EQ(disto[0], 99.f); // Luma and alpha are not impacted by
EXPECT_EQ(disto[1], 99.f); // downsampling, only chroma.
}
INSTANTIATE_TEST_SUITE_P(YUVPlaneTestInstantiation, YUVPlaneTest,
testing::Values("source0.ppm", "source1_64x48.png",
"source1_1x48.png", "source1_64x1.png",
"ccsp/source3_C444p12.y4m"));
//------------------------------------------------------------------------------
} // namespace
} // namespace WP2