Skip to content

Commit

Permalink
Do not allow for monochrome + identity matrix
Browse files Browse the repository at this point in the history
  • Loading branch information
vrabaud committed Mar 6, 2025
1 parent ad49114 commit 04e3a07
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 76 deletions.
7 changes: 7 additions & 0 deletions src/write.c
Original file line number Diff line number Diff line change
Expand Up @@ -1657,6 +1657,13 @@ static avifResult avifValidateGrid(uint32_t gridCols,
return AVIF_RESULT_INVALID_IMAGE_GRID;
}

// AV1 (Version 1.0.0 with Errata 1), Section 6.4.2. Color config semantics
// If matrix coefficients is equal to MC_IDENTITY, it is a requirement of bitstream conformance that subsampling_x is equal to 0 and subsampling_y is equal to 0.
if (cellImage->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY && cellImage->yuvFormat != AVIF_PIXEL_FORMAT_YUV444) {
avifDiagnosticsPrintf(diag, "subsampling must be 0 with identity matrix coefficients");
return AVIF_RESULT_INVALID_ARGUMENT;
}

if (!cellImage->yuvPlanes[AVIF_CHAN_Y]) {
return AVIF_RESULT_NO_CONTENT;
}
Expand Down
195 changes: 119 additions & 76 deletions tests/gtest/aviflosslesstest.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2023 Google LLC
// SPDX-License-Identifier: BSD-2-Clause

#include <tuple>

#include "avif/avif.h"
#include "aviftest_helpers.h"
#include "avifutil.h"
Expand All @@ -12,86 +14,127 @@ namespace {
// Used to pass the data folder path to the GoogleTest suites.
const char* data_path = nullptr;

// Read image losslessly, using identiy MC.
ImagePtr ReadImageLossless(const std::string& path,
avifPixelFormat requested_format,
avifBool ignore_icc) {
ImagePtr image(avifImageCreateEmpty());
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
if (!image ||
avifReadImage(path.c_str(), requested_format, /*requested_depth=*/0,
/*chroma_downsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
ignore_icc, /*ignore_exif=*/false, /*ignore_xmp=*/false,
/*allow_changing_cicp=*/false, /*ignore_gain_map=*/false,
AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(),
/*outDepth=*/nullptr, /*sourceTiming=*/nullptr,
/*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) {
return nullptr;
}
return image;
}

class LosslessTest
: public testing::TestWithParam<std::tuple<std::string, int, int>> {};

// Tests encode/decode round trips under different matrix coefficients.
TEST(BasicTest, EncodeDecodeMatrixCoefficients) {
for (const auto& file_name :
{"paris_icc_exif_xmp.png", "paris_exif_xmp_icc.jpg"}) {
// Read a ground truth image with default matrix coefficients.
const std::string file_path = std::string(data_path) + file_name;
const ImagePtr ground_truth_image =
testutil::ReadImage(data_path, file_name);

for (auto matrix_coefficient :
{AVIF_MATRIX_COEFFICIENTS_IDENTITY, AVIF_MATRIX_COEFFICIENTS_YCGCO,
AVIF_MATRIX_COEFFICIENTS_YCGCO_RE,
AVIF_MATRIX_COEFFICIENTS_YCGCO_RO}) {
// Read a ground truth image but ask for certain matrix coefficients.
ImagePtr image(avifImageCreateEmpty());
ASSERT_NE(image, nullptr);
image->matrixCoefficients = (avifMatrixCoefficients)matrix_coefficient;
const avifAppFileFormat file_format = avifReadImage(
file_path.c_str(),
/*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE,
/*requestedDepth=*/0,
/*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
/*ignoreColorProfile=*/false, /*ignoreExif=*/false,
/*ignoreXMP=*/false, /*allowChangingCicp=*/true,
/*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(),
/*outDepth=*/nullptr, /*sourceTiming=*/nullptr,
/*frameIter=*/nullptr);
if (matrix_coefficient == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
// AVIF_MATRIX_COEFFICIENTS_YCGCO_RO does not work because the input
// depth is not odd.
ASSERT_EQ(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
continue;
}
ASSERT_NE(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);

// Encode.
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
encoder->speed = AVIF_SPEED_FASTEST;
encoder->quality = AVIF_QUALITY_LOSSLESS;
encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
testutil::AvifRwData encoded;
avifResult result =
avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);

// Decode to RAM.
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);

// Convert to a temporary PNG and read back, to have default matrix
// coefficients.
const std::string temp_dir = testing::TempDir();
const std::string temp_file = "decoded_default.png";
const std::string tmp_path = temp_dir + temp_file;
ASSERT_TRUE(testutil::WriteImage(decoded.get(), tmp_path.c_str()));
const ImagePtr decoded_default =
testutil::ReadImage(temp_dir.c_str(), temp_file.c_str());

// Verify that the ground truth and decoded images are the same.
const bool are_images_equal =
testutil::AreImagesEqual(*ground_truth_image, *decoded_default);

if (matrix_coefficient == AVIF_MATRIX_COEFFICIENTS_IDENTITY ||
matrix_coefficient == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) {
ASSERT_TRUE(are_images_equal);
} else {
// AVIF_MATRIX_COEFFICIENTS_YCGCO is not lossless because it does not
// expand the input bit range for YCgCo values.
ASSERT_FALSE(are_images_equal);
}
}
TEST_P(LosslessTest, EncodeDecodeMatrixCoefficients) {
const std::string& file_name = std::get<0>(GetParam());
const avifMatrixCoefficients matrix_coefficients =
static_cast<avifMatrixCoefficients>(std::get<1>(GetParam()));
const avifPixelFormat pixel_format =
static_cast<avifPixelFormat>(std::get<2>(GetParam()));
// Ignore ICC when going from RGB to gray.
const avifBool ignore_icc =
pixel_format == AVIF_PIXEL_FORMAT_YUV400 ? AVIF_TRUE : AVIF_FALSE;

// Read a ground truth image but ask for certain matrix coefficients.
const std::string file_path = std::string(data_path) + file_name;
ImagePtr image(avifImageCreateEmpty());
ASSERT_NE(image, nullptr);
image->matrixCoefficients = matrix_coefficients;
const avifAppFileFormat file_format = avifReadImage(
file_path.c_str(),
/*requestedFormat=*/pixel_format,
/*requestedDepth=*/0,
/*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
/*ignoreColorProfile=*/ignore_icc, /*ignoreExif=*/false,
/*ignoreXMP=*/false, /*allowChangingCicp=*/true,
/*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(),
/*outDepth=*/nullptr, /*sourceTiming=*/nullptr,
/*frameIter=*/nullptr);
if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
// AVIF_MATRIX_COEFFICIENTS_YCGCO_RO does not work because the input
// depth is not odd.
ASSERT_EQ(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);
return;
}
ASSERT_NE(file_format, AVIF_APP_FILE_FORMAT_UNKNOWN);

// Encode.
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
encoder->speed = AVIF_SPEED_FASTEST;
encoder->quality = AVIF_QUALITY_LOSSLESS;
encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);

if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
pixel_format != AVIF_PIXEL_FORMAT_NONE &&
pixel_format != AVIF_PIXEL_FORMAT_YUV444) {
// The AV1 spec does not allow identity with subsampling.
ASSERT_EQ(result, AVIF_RESULT_INVALID_ARGUMENT);
return;
}
ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);

// Decode to RAM.
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK) << avifResultToString(result);

// What we read should be what we encoded
ASSERT_TRUE(testutil::AreImagesEqual(*image, *decoded));

// Convert to a temporary PNG and read back, to have default matrix
// coefficients.
const std::string tmp_path = testing::TempDir() + "decoded_default.png";
ASSERT_TRUE(testutil::WriteImage(decoded.get(), tmp_path.c_str()));
const ImagePtr decoded_lossless =
ReadImageLossless(tmp_path, pixel_format, ignore_icc);

// Verify that the ground truth and decoded images are the same.
const ImagePtr ground_truth_lossless =
ReadImageLossless(file_path, pixel_format, ignore_icc);
const bool are_images_equal =
testutil::AreImagesEqual(*ground_truth_lossless, *decoded_lossless);

if (matrix_coefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO) {
// AVIF_MATRIX_COEFFICIENTS_YCGCO is not a lossless transform.
ASSERT_FALSE(are_images_equal);
} else if (pixel_format == AVIF_PIXEL_FORMAT_YUV400) {
// For gray images, information is lost.
ASSERT_FALSE(are_images_equal);
} else {
ASSERT_TRUE(are_images_equal);
}
}

INSTANTIATE_TEST_SUITE_P(
LosslessTestInstantiation, LosslessTest,
testing::Combine(
testing::Values("paris_icc_exif_xmp.png", "paris_exif_xmp_icc.jpg"),
testing::Values(static_cast<int>(AVIF_MATRIX_COEFFICIENTS_IDENTITY),
static_cast<int>(AVIF_MATRIX_COEFFICIENTS_YCGCO),
static_cast<int>(AVIF_MATRIX_COEFFICIENTS_YCGCO_RE),
static_cast<int>(AVIF_MATRIX_COEFFICIENTS_YCGCO_RO)),
testing::Values(static_cast<int>(AVIF_PIXEL_FORMAT_NONE),
static_cast<int>(AVIF_PIXEL_FORMAT_YUV400))));

} // namespace
} // namespace avif

Expand Down

0 comments on commit 04e3a07

Please sign in to comment.