blob: f85a175c142c28183c4ab64a82e25c5dc8b3a778 [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.
// Encoding from custom color space test.
#include <limits>
#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/plane.h"
#include "src/utils/vector.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 kCCSPToRGBMatrices[] = {
kRGB8ToRGBMatrix, kRGB10ToRGBMatrix, kYCoCgToRGBMatrix, kYpUVToRGBMatrix};
constexpr const uint32_t kCCSPToRGBShifts[] = {
kRGB8ToRGBShift, kRGB10ToRGBShift, kYCoCgToRGBShift, kYpUVToRGBShift};
class EncodeCustomCspTest
: public testing::TestWithParam<std::tuple<std::string, float, Matrix, bool,
bool, bool, bool, bool, bool>> {
void SetUp() override { WP2CSPConverterInit(); }
};
//------------------------------------------------------------------------------
TEST_P(EncodeCustomCspTest, Comparison) {
const std::string& src_file_name = std::get<0>(GetParam());
const float quality = std::get<1>(GetParam());
const Matrix matrix = std::get<2>(GetParam());
const int16_t* const ccsp_to_rgb_matrix = kCCSPToRGBMatrices[(int)matrix];
const uint32_t ccsp_to_rgb_shift = kCCSPToRGBShifts[(int)matrix];
const bool pad_y = std::get<3>(GetParam());
const bool pad_u = std::get<4>(GetParam());
const bool pad_v = std::get<5>(GetParam());
const bool pad_a = std::get<6>(GetParam());
const bool encode_alpha = std::get<7>(GetParam());
const bool encode_metadata = std::get<8>(GetParam());
ArgbBuffer src;
MemoryWriter ref_data;
ASSERT_WP2_OK(
testutil::CompressImage(src_file_name, &ref_data, &src, quality));
YUVPlane custom_input; // Might contain RGB.
const uint32_t padded_width = Pad(src.width(), kPredWidth);
const uint32_t padded_height = Pad(src.height(), kPredWidth);
ASSERT_WP2_OK(custom_input.Y.Resize(pad_y ? padded_width : src.width(),
pad_y ? padded_height : src.height()));
ASSERT_WP2_OK(custom_input.U.Resize(pad_u ? padded_width : src.width(),
pad_u ? padded_height : src.height()));
ASSERT_WP2_OK(custom_input.V.Resize(pad_v ? padded_width : src.width(),
pad_v ? padded_height : src.height()));
if (encode_alpha) {
ASSERT_WP2_OK(custom_input.A.Resize(pad_a ? padded_width : src.width(),
pad_a ? padded_height : src.height()));
}
CSPTransform csp_transform; // Match Encode() behavior.
constexpr int16_t kRgbAvg[3] = {0, 0, 0};
if (matrix == Matrix::kRGB8 || matrix == Matrix::kRGB10) {
const int16_t v =
1u << (CSPTransform::kMtxShift - ((matrix == Matrix::kRGB8) ? 0 : 2));
const int16_t kRGBToRGBMatrix[9] = {v, 0, 0, 0, v, 0, 0, 0, v}; // Identity
ASSERT_TRUE(csp_transform.Init(kRGBToRGBMatrix, kRgbAvg));
} else 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));
}
for (uint32_t y = 0; y < padded_height; ++y) {
for (uint32_t x = 0; x < padded_width; ++x) {
if (x < src.width() && y < src.height()) {
const uint32_t px = x * WP2FormatBpp(src.format());
const uint8_t* const pixel = src.GetRow8(y) + px;
if (encode_alpha) custom_input.A.At(x, y) = pixel[0];
csp_transform.Rgb8ToYuv(
pixel[1], pixel[2], pixel[3], &custom_input.Y.At(x, y),
&custom_input.U.At(x, y), &custom_input.V.At(x, y));
}
}
}
for (Channel c : {kYChannel, kUChannel, kVChannel, kAChannel}) {
if (custom_input.GetChannel(c).IsEmpty()) continue;
ASSERT_WP2_OK(
custom_input.GetChannel(c).FillPad(src.width(), src.height()));
}
MemoryWriter data;
EncoderConfig config = EncoderConfig::kDefault;
config.quality = quality;
Metadata empty_metadata;
ASSERT_WP2_OK(Encode(
src.width(), src.height(), custom_input.Y.Row(0), custom_input.Y.Step(),
custom_input.U.Row(0), custom_input.U.Step(), custom_input.V.Row(0),
custom_input.V.Step(),
custom_input.HasAlpha() ? custom_input.A.Row(0) : nullptr,
custom_input.A.Step(), ccsp_to_rgb_matrix, ccsp_to_rgb_shift, &data,
config, encode_metadata ? src.metadata_ : empty_metadata));
// Decode the reference and the custom bitstreams.
ArgbBuffer ref, dst;
ASSERT_WP2_OK(Decode(ref_data.mem_, ref_data.size_, &ref));
ASSERT_WP2_OK(Decode(data.mem_, data.size_, &dst));
ASSERT_TRUE(testutil::Compare(src, ref, src_file_name,
testutil::GetExpectedDistortion(quality)));
ASSERT_TRUE(testutil::Compare(src, dst, src_file_name,
testutil::GetExpectedDistortion(quality)));
ASSERT_TRUE(testutil::Compare(
ref, dst, src_file_name,
(matrix == Matrix::kRGB8 || matrix == Matrix::kRGB10) ? 99.f : 50.f));
if (encode_metadata) {
EXPECT_TRUE(testutil::HasSameData(dst.metadata_.iccp, ref.metadata_.iccp));
EXPECT_TRUE(testutil::HasSameData(dst.metadata_.xmp, ref.metadata_.xmp));
EXPECT_TRUE(testutil::HasSameData(dst.metadata_.exif, ref.metadata_.exif));
}
}
//------------------------------------------------------------------------------
INSTANTIATE_TEST_SUITE_P(
EncodeCustomCspTestInstantiation, EncodeCustomCspTest,
testing::Combine(testing::Values("source1_1x1.png", "source1_64x48.png"),
testing::Values(0.f, 100.f) /* quality */,
testing::Values(Matrix::kRGB8, Matrix::kRGB10,
Matrix::kYCoCg, Matrix::kYpUV),
testing::Values(false, true) /* encode_alpha */,
testing::Values(true) /* pad_y */,
testing::Values(true) /* pad_u */,
testing::Values(true) /* pad_v */,
testing::Values(true) /* pad_a */,
testing::Values(false) /* encode_metadata */));
INSTANTIATE_TEST_SUITE_P(
EncodeCustomCspTestInstantiationPadding, EncodeCustomCspTest,
testing::Combine(testing::Values("source1_1x1.png"),
testing::Values(0.f, 100.f) /* quality */,
testing::Values(Matrix::kRGB8),
testing::Values(false, true) /* encode_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) /* encode_metadata */));
INSTANTIATE_TEST_SUITE_P(
EncodeCustomCspTestInstantiationMetadata, EncodeCustomCspTest,
testing::Combine(
testing::Values("source1_1x1.png", "test_exif_xmp.webp"),
testing::Values(50.f) /* quality */, testing::Values(Matrix::kRGB8),
testing::Values(true) /* encode_alpha */,
testing::Values(false) /* pad_y */, testing::Values(true) /* pad_u */,
testing::Values(false) /* pad_v */, testing::Values(true) /* pad_a */,
testing::Values(true) /* encode_metadata */));
//------------------------------------------------------------------------------
// Encode the bytes read from a y4m file (YCbCr).
class ReadCustomCspTest
: public testing::TestWithParam<std::tuple<const char*, float, bool>> {};
TEST_P(ReadCustomCspTest, Y4M) {
const std::string file_name = std::get<0>(GetParam());
const float quality = std::get<1>(GetParam());
const bool use_animation_encoder = std::get<2>(GetParam());
YUVPlane ccsp;
CSPMtx ccsp_to_rgb = {};
Metadata metadata;
ASSERT_WP2_OK(ReadImage(testutil::GetTestDataPath(file_name).c_str(), &ccsp,
&ccsp_to_rgb, &metadata));
// Limit canvas size to avoid a timeout.
ASSERT_WP2_OK(ccsp.SetView(ccsp, {0, 0, std::min(ccsp.GetWidth(), 255u),
std::min(ccsp.GetHeight(), 254u)}));
ArgbBuffer converted;
ASSERT_WP2_OK(
ccsp.Export(ccsp_to_rgb, /*resize_if_needed=*/true, &converted));
EncoderConfig config = EncoderConfig::kDefault;
config.quality = quality;
MemoryWriter data;
if (use_animation_encoder) {
ASSERT_WP2_OK(
Encode(ccsp.GetWidth(), ccsp.GetHeight(),
ccsp.Y.Row(0), ccsp.Y.Step(),
ccsp.U.Row(0), ccsp.U.Step(),
ccsp.V.Row(0), ccsp.V.Step(),
ccsp.HasAlpha() ? ccsp.A.Row(0) : nullptr, ccsp.A.Step(),
ccsp_to_rgb.mtx(), ccsp_to_rgb.shift, &data, config, metadata));
} else {
AnimationEncoder encoder;
for (uint32_t duration_ms : {1, 2}) {
ASSERT_WP2_OK(encoder.AddFrame(
ccsp.GetWidth(), ccsp.GetHeight(),
ccsp.Y.Row(0), ccsp.Y.Step(),
ccsp.U.Row(0), ccsp.U.Step(),
ccsp.V.Row(0), ccsp.V.Step(),
ccsp.HasAlpha() ? ccsp.A.Row(0) : nullptr, ccsp.A.Step(),
ccsp_to_rgb.mtx(), ccsp_to_rgb.shift, duration_ms));
}
ASSERT_WP2_OK(encoder.Encode(&data, config));
}
ArgbBuffer decoded;
ASSERT_WP2_OK(Decode(data.mem_, data.size_, &decoded));
EXPECT_TRUE(testutil::Compare(converted, decoded, file_name,
testutil::GetExpectedDistortion(quality)));
}
INSTANTIATE_TEST_SUITE_P(
ReadCustomCspTestInstantiation, ReadCustomCspTest,
testing::Combine(testing::Values("source1_64x48.png",
"ccsp/source3_C444p8.y4m",
"ccsp/source3_C444p12.y4m"),
testing::Values(20.f, 100.f), /* quality */
testing::Values(false, true) /* use_animation_encoder */));
//------------------------------------------------------------------------------
static struct BufferParam {
const char* label;
const char* file_name;
uint32_t steps[4];
WP2Status expected_status;
} const kBufferParams[] = {
{ "0-sized buffers are nullptr.",
"source1_1x1.png", {0, 0, 0, 0}, WP2_STATUS_NULL_PARAMETER },
{ "0-sized buffers are nullptr.",
"source1_1x1.png", {0, 2, 2, 2}, WP2_STATUS_NULL_PARAMETER },
{ "0-sized buffers are nullptr.",
"source1_1x1.png", {2, 0, 2, 2}, WP2_STATUS_NULL_PARAMETER },
{ "0-sized buffers are nullptr.",
"source1_1x1.png", {2, 2, 0, 2}, WP2_STATUS_NULL_PARAMETER },
{ "OK strides.",
"source1_1x1.png", {1, 1, 1, 2}, WP2_STATUS_OK },
{ "R-Stride is too small.",
"source1_32x32.png", {31, 32, 32, 0}, WP2_STATUS_BAD_DIMENSION },
{ "G-Stride is too small.",
"source1_32x32.png", {32, 31, 32, 0}, WP2_STATUS_BAD_DIMENSION },
{ "B-Stride is too small.",
"source1_32x32.png", {32, 32, 31, 0}, WP2_STATUS_BAD_DIMENSION },
{ "Strides are fine I.",
"source1_32x32.png", {32, 32, 32, 0}, WP2_STATUS_OK },
{ "Strides are fine II.",
"source1_32x32.png", {33, 35, 36, 0}, WP2_STATUS_OK }
};
class StrideTest : public testing::TestWithParam<BufferParam> {};
TEST_P(StrideTest, Combination) {
const BufferParam& p = GetParam();
ArgbBuffer src;
ASSERT_WP2_OK(
ReadImage(testutil::GetTestDataPath(p.file_name).c_str(), &src));
Vector_s16 rgba[4];
for (uint32_t c : {0, 1, 2, 3}) {
if (p.steps[c] == 0) continue;
ASSERT_TRUE(rgba[c].resize(p.steps[c] * src.height()));
const uint32_t max_width = std::min(src.width(), p.steps[c]);
for (uint32_t y = 0; y < src.height(); ++y) {
const uint8_t* const src_row = src.GetRow8(y);
int16_t* const dst_row = &rgba[c][p.steps[c] * y];
for (uint32_t x = 0; x < max_width; ++x) {
dst_row[x] = src_row[x * WP2FormatBpp(src.format()) + c];
}
}
// Pixels outside of the image are undefined on purpose for the sanitizers.
}
MemoryWriter writer;
ASSERT_EQ(Encode(src.width(), src.height(), rgba[0].data(), p.steps[0],
rgba[1].data(), p.steps[1], rgba[2].data(), p.steps[2],
rgba[3].data(), p.steps[3], kRGB8ToRGBMatrix,
kRGB8ToRGBShift, &writer),
p.expected_status)
<< p.label;
}
INSTANTIATE_TEST_SUITE_P(StrideTestInstantiation, StrideTest,
testing::ValuesIn(kBufferParams));
//------------------------------------------------------------------------------
// Test that WP2_STATUS_INVALID_PARAMETER is returned for bad alpha input.
TEST(CustomCspAlphaTest, Garbage) {
constexpr int16_t kGarbageAlphaValues[] = {
std::numeric_limits<int16_t>::min(), std::numeric_limits<int16_t>::max(),
-1, kAlphaMax + 1};
constexpr uint32_t kWidth = 16, kHeight = 16;
for (int16_t garbage_alpha : kGarbageAlphaValues) {
Vector_s16 rgba[4];
for (uint32_t c : {0, 1, 2, 3}) {
ASSERT_TRUE(rgba[c].resize(kWidth * kHeight));
std::fill(rgba[c].begin(), rgba[c].end(), (c == 3) ? garbage_alpha : -12);
}
MemoryWriter writer;
ASSERT_EQ(Encode(kWidth, kHeight, rgba[0].data(), kWidth, rgba[1].data(),
kWidth, rgba[2].data(), kWidth, rgba[3].data(), kWidth,
kRGB8ToRGBMatrix, kRGB8ToRGBShift, &writer),
WP2_STATUS_INVALID_PARAMETER);
}
}
// Test that given but opaque alpha input is compressed as if it was omitted.
TEST(CustomCspAlphaTest, Opaque) {
constexpr uint32_t kWidth = 16, kHeight = 16;
Vector_s16 rgba[4];
for (uint32_t c : {0, 1, 2, 3}) {
ASSERT_TRUE(rgba[c].resize(kWidth * kHeight));
std::fill(rgba[c].begin(), rgba[c].end(), (c == 3) ? kAlphaMax : -5);
}
MemoryWriter writer_opaque;
ASSERT_WP2_OK(Encode(kWidth, kHeight, rgba[0].data(), kWidth, rgba[1].data(),
kWidth, rgba[2].data(), kWidth, rgba[3].data(), kWidth,
kRGB8ToRGBMatrix, kRGB8ToRGBShift, &writer_opaque));
MemoryWriter writer_no_alpha;
ASSERT_WP2_OK(Encode(kWidth, kHeight, rgba[0].data(), kWidth, rgba[1].data(),
kWidth, rgba[2].data(), kWidth, nullptr, 0,
kRGB8ToRGBMatrix, kRGB8ToRGBShift, &writer_no_alpha));
ASSERT_EQ(writer_opaque.size_, writer_no_alpha.size_);
ASSERT_TRUE(std::equal(writer_opaque.mem_,
writer_opaque.mem_ + writer_opaque.size_,
writer_no_alpha.mem_));
}
//------------------------------------------------------------------------------
} // namespace
} // namespace WP2