Add avifrgbtoyuvtest to test RGB-YUV subsampling
The current tests/avifyuv.c tool only displays 4:4:4 conversions.
Adding a convenient C++ test for chroma subsampling loss evaluation
will help when working on faster alternatives to the current slow
RGB-to-YUV conversion in avifenc.
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 77aac79..a46342a 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -84,6 +84,11 @@
target_include_directories(avifmetadatatest PRIVATE ${GTEST_INCLUDE_DIRS})
add_test(NAME avifmetadatatest COMMAND avifmetadatatest)
+ add_executable(avifrgbtoyuvtest gtest/avifrgbtoyuvtest.cc)
+ target_link_libraries(avifrgbtoyuvtest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
+ target_include_directories(avifrgbtoyuvtest PRIVATE ${GTEST_INCLUDE_DIRS})
+ add_test(NAME avifrgbtoyuvtest COMMAND avifrgbtoyuvtest)
+
add_executable(avify4mtest gtest/avify4mtest.cc)
target_link_libraries(avify4mtest aviftest_helpers avif_apps ${GTEST_BOTH_LIBRARIES})
target_include_directories(avify4mtest PRIVATE ${GTEST_INCLUDE_DIRS})
diff --git a/tests/gtest/avifrgbtoyuvtest.cc b/tests/gtest/avifrgbtoyuvtest.cc
new file mode 100644
index 0000000..6fc9b78
--- /dev/null
+++ b/tests/gtest/avifrgbtoyuvtest.cc
@@ -0,0 +1,387 @@
+// Copyright 2022 Google LLC. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <algorithm>
+#include <cmath>
+#include <memory>
+#include <tuple>
+
+#include "avif/avif.h"
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+using ::testing::Combine;
+using ::testing::Values;
+using ::testing::ValuesIn;
+
+namespace libavif {
+namespace {
+
+//------------------------------------------------------------------------------
+
+// Modifies the pixel values of a channel in image by modifier[] (row-ordered).
+template <typename PixelType>
+void ModifyImageChannel(avifRGBImage* image, uint32_t channel_offset,
+ const int32_t modifier[]) {
+ const uint32_t channel_count = avifRGBFormatChannelCount(image->format);
+ assert(channel_offset < channel_count);
+ for (uint32_t y = 0, i = 0; y < image->height; ++y) {
+ PixelType* pixel =
+ reinterpret_cast<PixelType*>(image->pixels + image->rowBytes * y);
+ for (uint32_t x = 0; x < image->width; ++x, ++i) {
+ pixel[channel_offset] += modifier[i];
+ pixel += channel_count;
+ }
+ }
+}
+
+void ModifyImageChannel(avifRGBImage* image, uint32_t channel_offset,
+ const int32_t modifier[]) {
+ (image->depth <= 8)
+ ? ModifyImageChannel<uint8_t>(image, channel_offset, modifier)
+ : ModifyImageChannel<uint16_t>(image, channel_offset, modifier);
+}
+
+// Fills the image channel with the given value, and modifies the individual
+// pixel values of that channel with the modifier, if not null.
+void SetImageChannel(avifRGBImage* image, uint32_t channel_offset,
+ uint32_t value, const int32_t modifier[]) {
+ testutil::FillImageChannel(image, channel_offset, value);
+ if (modifier) {
+ ModifyImageChannel(image, channel_offset, modifier);
+ }
+}
+
+// Accumulates stats about the differences between the images a and b.
+template <typename PixelType>
+void GetDiffSumAndSqDiffSum(const avifRGBImage& a, const avifRGBImage& b,
+ int64_t* diff_sum, int64_t* abs_diff_sum,
+ int64_t* sq_diff_sum, int64_t* max_abs_diff) {
+ const uint32_t channel_count = avifRGBFormatChannelCount(a.format);
+ for (uint32_t y = 0; y < a.height; ++y) {
+ const PixelType* row_a =
+ reinterpret_cast<PixelType*>(a.pixels + a.rowBytes * y);
+ const PixelType* row_b =
+ reinterpret_cast<PixelType*>(b.pixels + b.rowBytes * y);
+ for (uint32_t x = 0; x < a.width * channel_count; ++x) {
+ const int64_t diff =
+ static_cast<int64_t>(row_b[x]) - static_cast<int64_t>(row_a[x]);
+ *diff_sum += diff;
+ *abs_diff_sum += std::abs(diff);
+ *sq_diff_sum += diff * diff;
+ *max_abs_diff = std::max(*max_abs_diff, std::abs(diff));
+ }
+ }
+}
+
+void GetDiffSumAndSqDiffSum(const avifRGBImage& a, const avifRGBImage& b,
+ int64_t* diff_sum, int64_t* abs_diff_sum,
+ int64_t* sq_diff_sum, int64_t* max_abs_diff) {
+ (a.depth <= 8) ? GetDiffSumAndSqDiffSum<uint8_t>(a, b, diff_sum, abs_diff_sum,
+ sq_diff_sum, max_abs_diff)
+ : GetDiffSumAndSqDiffSum<uint16_t>(
+ a, b, diff_sum, abs_diff_sum, sq_diff_sum, max_abs_diff);
+}
+
+// Returns the Peak Signal-to-Noise Ratio from accumulated stats.
+double GetPsnr(double sq_diff_sum, double num_diffs, double max_abs_diff) {
+ if (sq_diff_sum == 0.) {
+ return 99.; // Lossless.
+ }
+ const double distortion =
+ sq_diff_sum / (num_diffs * max_abs_diff * max_abs_diff);
+ return (distortion > 0.) ? std::min(-10 * std::log10(distortion), 98.9)
+ : 98.9; // Not lossless.
+}
+
+//------------------------------------------------------------------------------
+
+class YUVToRGBTest
+ : public testing::TestWithParam<
+ std::tuple</*rgb_depth=*/int, /*yuv_depth=*/int, avifRGBFormat,
+ avifPixelFormat, avifRange, avifMatrixCoefficients,
+ /*add_noise=*/bool, /*rgb_step=*/uint32_t,
+ /*max_abs_average_diff=*/double, /*min_psnr=*/double>> {};
+
+// Converts from RGB to YUV and back to RGB for all RGB combinations, separated
+// by a color step for reasonable timing. If add_noise is true, also applies
+// some noise to the input samples to exercise chroma subsampling.
+TEST_P(YUVToRGBTest, Convert) {
+ const int rgb_depth = std::get<0>(GetParam());
+ const int yuv_depth = std::get<1>(GetParam());
+ const avifRGBFormat rgb_format = std::get<2>(GetParam());
+ const avifPixelFormat yuv_format = std::get<3>(GetParam());
+ const avifRange yuv_range = std::get<4>(GetParam());
+ const avifMatrixCoefficients matrix_coefficients = std::get<5>(GetParam());
+ // Whether to add noise to the input RGB samples. Should only impact
+ // subsampled chroma (4:2:2 and 4:2:0).
+ const bool add_noise = std::get<6>(GetParam());
+ // Testing each RGB combination would be more accurate but results are similar
+ // with faster settings.
+ const uint32_t rgb_step = std::get<7>(GetParam());
+ // Thresholds to pass.
+ const double max_abs_average_diff = std::get<8>(GetParam());
+ const double min_psnr = std::get<9>(GetParam());
+ // Deduced constants.
+ const bool is_monochrome =
+ (yuv_format == AVIF_PIXEL_FORMAT_YUV400); // If so, only test grey input.
+ const uint32_t rgb_max = (1 << rgb_depth) - 1;
+
+ // The YUV upsampling treats the first and last rows and columns differently
+ // than the remaining pairs of rows and columns. An image of 16 pixels is used
+ // to test all these possibilities.
+ static constexpr int width = 4;
+ static constexpr int height = 4;
+ std::unique_ptr<avifImage, decltype(&avifImageDestroy)> yuv(
+ avifImageCreate(width, height, yuv_depth, yuv_format), avifImageDestroy);
+ yuv->matrixCoefficients = matrix_coefficients;
+ yuv->yuvRange = yuv_range;
+ testutil::AvifRgbImage src_rgb(yuv.get(), rgb_depth, rgb_format);
+ testutil::AvifRgbImage dst_rgb(yuv.get(), rgb_depth, rgb_format);
+ const testutil::RgbChannelOffsets offsets =
+ testutil::GetRgbChannelOffsets(rgb_format);
+
+ // Alpha values are not tested here. Keep it opaque.
+ if (avifRGBFormatHasAlpha(src_rgb.format)) {
+ testutil::FillImageChannel(&src_rgb, offsets.a, rgb_max);
+ }
+
+ // To exercise the chroma subsampling loss, the input samples must differ in
+ // each of the RGB channels. Chroma subsampling expects the input RGB channels
+ // to be correlated to minimize the quality loss.
+ static constexpr int32_t kRedNoise[] = {
+ 7, 14, 11, 5, // Random permutation of 16 values.
+ 4, 6, 8, 15, //
+ 2, 9, 13, 3, //
+ 12, 1, 10, 0};
+ static constexpr int32_t kGreenNoise[] = {
+ 3, 2, 12, 15, // Random permutation of 16 values
+ 14, 10, 7, 13, // that is somewhat close to kRedNoise.
+ 5, 1, 9, 0, //
+ 8, 4, 11, 6};
+ static constexpr int32_t kBlueNoise[] = {
+ 0, 8, 14, 9, // Random permutation of 16 values
+ 13, 12, 2, 7, // that is somewhat close to kGreenNoise.
+ 3, 1, 11, 10, //
+ 6, 15, 5, 4};
+ static constexpr int32_t* kPlainColor = nullptr;
+
+ // Estimate the loss from converting RGB values to YUV and back.
+ int64_t diff_sum = 0, abs_diff_sum = 0, sq_diff_sum = 0, max_abs_diff = 0;
+ int64_t num_diffs = 0;
+ const uint32_t max_value = rgb_max - (add_noise ? 15 : 0);
+ for (uint32_t r = 0; r < max_value + rgb_step; r += rgb_step) {
+ r = std::min(r, max_value); // Test the maximum sample value even if it is
+ // not a multiple of rgb_step.
+ SetImageChannel(&src_rgb, offsets.r, r,
+ add_noise ? kRedNoise : kPlainColor);
+
+ if (is_monochrome) {
+ // Test only greyish input when converting to a single channel.
+ SetImageChannel(&src_rgb, offsets.g, r,
+ add_noise ? kGreenNoise : kPlainColor);
+ SetImageChannel(&src_rgb, offsets.b, r,
+ add_noise ? kBlueNoise : kPlainColor);
+
+ ASSERT_EQ(avifImageRGBToYUV(yuv.get(), &src_rgb), AVIF_RESULT_OK);
+ ASSERT_EQ(avifImageYUVToRGB(yuv.get(), &dst_rgb), AVIF_RESULT_OK);
+ GetDiffSumAndSqDiffSum(src_rgb, dst_rgb, &diff_sum, &abs_diff_sum,
+ &sq_diff_sum, &max_abs_diff);
+ num_diffs += src_rgb.width * src_rgb.height * 3; // Alpha is lossless.
+ } else {
+ for (uint32_t g = 0; g < max_value + rgb_step; g += rgb_step) {
+ g = std::min(g, max_value);
+ SetImageChannel(&src_rgb, offsets.g, g,
+ add_noise ? kGreenNoise : kPlainColor);
+ for (uint32_t b = 0; b < max_value + rgb_step; b += rgb_step) {
+ b = std::min(b, max_value);
+ SetImageChannel(&src_rgb, offsets.b, b,
+ add_noise ? kBlueNoise : kPlainColor);
+
+ ASSERT_EQ(avifImageRGBToYUV(yuv.get(), &src_rgb), AVIF_RESULT_OK);
+ ASSERT_EQ(avifImageYUVToRGB(yuv.get(), &dst_rgb), AVIF_RESULT_OK);
+ GetDiffSumAndSqDiffSum(src_rgb, dst_rgb, &diff_sum, &abs_diff_sum,
+ &sq_diff_sum, &max_abs_diff);
+ num_diffs +=
+ src_rgb.width * src_rgb.height * 3; // Alpha is lossless.
+ }
+ }
+ }
+ }
+
+ // Stats and thresholds.
+ // Note: The thresholds defined in this test are calibrated for libyuv fast
+ // paths. See reformat_libyuv.c. Slower non-libyuv conversions in
+ // libavif have a higher precision (using floating point operations).
+ const double average_diff =
+ static_cast<double>(diff_sum) / static_cast<double>(num_diffs);
+ const double average_abs_diff =
+ static_cast<double>(abs_diff_sum) / static_cast<double>(num_diffs);
+ const double psnr = GetPsnr(sq_diff_sum, num_diffs, rgb_max);
+ EXPECT_LE(std::abs(average_diff), max_abs_average_diff);
+ EXPECT_GE(psnr, min_psnr);
+
+ // Print stats for convenience and easier threshold tuning.
+ static constexpr const char* kAvifRgbFormatToString[] = {
+ "RGB", "RGBA", "ARGB", "BGR", "BGRA", "ABGR"};
+ std::cout << " RGB " << rgb_depth << " bits, YUV " << yuv_depth << " bits, "
+ << kAvifRgbFormatToString[rgb_format] << ", "
+ << avifPixelFormatToString(yuv_format) << ", "
+ << (yuv_range ? "full" : "lmtd") << ", MC " << matrix_coefficients
+ << ", " << (add_noise ? "noisy" : "plain") << ", avg "
+ << average_diff << ", abs avg " << average_abs_diff << ", max "
+ << max_abs_diff << ", PSNR " << psnr << "dB" << std::endl;
+}
+
+constexpr avifRGBFormat kAllRgbFormats[] = {
+ AVIF_RGB_FORMAT_RGB, AVIF_RGB_FORMAT_RGBA, AVIF_RGB_FORMAT_ARGB,
+ AVIF_RGB_FORMAT_BGR, AVIF_RGB_FORMAT_BGRA, AVIF_RGB_FORMAT_ABGR};
+
+// This is the default avifenc setup when encoding from 8b PNG files to AVIF.
+INSTANTIATE_TEST_SUITE_P(
+ DefaultFormat, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(8),
+ /*yuv_depth=*/Values(8), Values(AVIF_RGB_FORMAT_RGBA),
+ Values(AVIF_PIXEL_FORMAT_YUV420), Values(AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(true),
+ /*rgb_step=*/Values(3),
+ /*max_abs_average_diff=*/Values(0.1), // The color drift is almost
+ // centered.
+ /*min_psnr=*/Values(36.) // Subsampling distortion is acceptable.
+ ));
+
+// Keeping RGB samples in full range and same or higher bit depth should not
+// bring any loss in the roundtrip.
+INSTANTIATE_TEST_SUITE_P(Identity8b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(8),
+ /*yuv_depth=*/Values(8, 10, 12),
+ ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV444),
+ Values(AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_IDENTITY),
+ /*add_noise=*/Values(true),
+ /*rgb_step=*/Values(31),
+ /*max_abs_average_diff=*/Values(0.),
+ /*min_psnr=*/Values(99.)));
+INSTANTIATE_TEST_SUITE_P(Identity10b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(10),
+ /*yuv_depth=*/Values(10, 12),
+ ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV444),
+ Values(AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_IDENTITY),
+ /*add_noise=*/Values(true),
+ /*rgb_step=*/Values(101),
+ /*max_abs_average_diff=*/Values(0.),
+ /*min_psnr=*/Values(99.)));
+INSTANTIATE_TEST_SUITE_P(Identity12b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(12),
+ /*yuv_depth=*/Values(12),
+ ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV444),
+ Values(AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_IDENTITY),
+ /*add_noise=*/Values(true),
+ /*rgb_step=*/Values(401),
+ /*max_abs_average_diff=*/Values(0.),
+ /*min_psnr=*/Values(99.)));
+
+// 4:4:4 and chroma subsampling have similar distortions on plain color inputs.
+INSTANTIATE_TEST_SUITE_P(
+ PlainAnySubsampling8b, YUVToRGBTest,
+ Combine(
+ /*rgb_depth=*/Values(8),
+ /*yuv_depth=*/Values(8), ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
+ AVIF_PIXEL_FORMAT_YUV420),
+ Values(AVIF_RANGE_FULL), Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(false),
+ /*rgb_step=*/Values(17),
+ /*max_abs_average_diff=*/Values(0.02), // The color drift is centered.
+ /*min_psnr=*/Values(52.) // RGB>YUV>RGB distortion is barely
+ // noticeable.
+ ));
+
+// Converting grey RGB samples to full-range monochrome of same or greater bit
+// depth should be lossless.
+INSTANTIATE_TEST_SUITE_P(MonochromeLossless8b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(8),
+ /*yuv_depth=*/Values(8, 10, 12),
+ ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV400),
+ Values(AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(false),
+ /*rgb_step=*/Values(1),
+ /*max_abs_average_diff=*/Values(0.),
+ /*min_psnr=*/Values(99.)));
+INSTANTIATE_TEST_SUITE_P(MonochromeLossless10b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(10),
+ /*yuv_depth=*/Values(10, 12),
+ ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV400),
+ Values(AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(false),
+ /*rgb_step=*/Values(1),
+ /*max_abs_average_diff=*/Values(0.),
+ /*min_psnr=*/Values(99.)));
+INSTANTIATE_TEST_SUITE_P(MonochromeLossless12b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(12),
+ /*yuv_depth=*/Values(12),
+ ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV400),
+ Values(AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(false),
+ /*rgb_step=*/Values(1),
+ /*max_abs_average_diff=*/Values(0.),
+ /*min_psnr=*/Values(99.)));
+
+// Can be used to print the drift of all RGB to YUV conversion possibilities.
+// Also used for coverage.
+INSTANTIATE_TEST_SUITE_P(
+ All8b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(8),
+ /*yuv_depth=*/Values(8, 10, 12), ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
+ AVIF_PIXEL_FORMAT_YUV420),
+ Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(false, true),
+ /*rgb_step=*/Values(61), // High or it would be too slow.
+ /*max_abs_average_diff=*/Values(1.), // Not very accurate because
+ // of high rgb_step.
+ /*min_psnr=*/Values(36.)));
+INSTANTIATE_TEST_SUITE_P(
+ All10b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(10),
+ /*yuv_depth=*/Values(8, 10, 12), ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
+ AVIF_PIXEL_FORMAT_YUV420),
+ Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(false, true),
+ /*rgb_step=*/Values(211), // High or it would be too slow.
+ /*max_abs_average_diff=*/Values(0.2), // Not very accurate because
+ // of high rgb_step.
+ /*min_psnr=*/Values(47.)));
+INSTANTIATE_TEST_SUITE_P(
+ All12b, YUVToRGBTest,
+ Combine(/*rgb_depth=*/Values(12),
+ /*yuv_depth=*/Values(8, 10, 12), ValuesIn(kAllRgbFormats),
+ Values(AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
+ AVIF_PIXEL_FORMAT_YUV420),
+ Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL),
+ Values(AVIF_MATRIX_COEFFICIENTS_BT601),
+ /*add_noise=*/Values(false, true),
+ /*rgb_step=*/Values(809), // High or it would be too slow.
+ /*max_abs_average_diff=*/Values(0.3), // Not very accurate because
+ // of high rgb_step.
+ /*min_psnr=*/Values(52.)));
+
+// TODO: Test other matrix coefficients than identity and bt.601.
+
+} // namespace
+} // namespace libavif
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index de8abae..f35775e 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -13,13 +13,41 @@
namespace {
constexpr int AVIF_CHAN_A = AVIF_CHAN_V + 1;
-constexpr int AVIF_CHANS[] = {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V,
- AVIF_CHAN_A};
} // namespace
//------------------------------------------------------------------------------
+AvifRgbImage::AvifRgbImage(const avifImage* yuv, int rgbDepth,
+ avifRGBFormat rgbFormat) {
+ avifRGBImageSetDefaults(this, yuv);
+ depth = rgbDepth;
+ format = rgbFormat;
+ avifRGBImageAllocatePixels(this);
+}
+
+//------------------------------------------------------------------------------
+
+RgbChannelOffsets GetRgbChannelOffsets(avifRGBFormat format) {
+ switch (format) {
+ case AVIF_RGB_FORMAT_RGB:
+ return {/*r=*/0, /*g=*/1, /*b=*/2, /*a=*/0};
+ case AVIF_RGB_FORMAT_RGBA:
+ return {/*r=*/0, /*g=*/1, /*b=*/2, /*a=*/3};
+ case AVIF_RGB_FORMAT_ARGB:
+ return {/*r=*/1, /*g=*/2, /*b=*/3, /*a=*/0};
+ case AVIF_RGB_FORMAT_BGR:
+ return {/*r=*/2, /*g=*/1, /*b=*/0, /*a=*/0};
+ case AVIF_RGB_FORMAT_BGRA:
+ return {/*r=*/2, /*g=*/1, /*b=*/0, /*a=*/3};
+ default:
+ assert(format == AVIF_RGB_FORMAT_ABGR);
+ return {/*r=*/3, /*g=*/2, /*b=*/1, /*a=*/0};
+ }
+}
+
+//------------------------------------------------------------------------------
+
AvifImagePtr CreateImage(int width, int height, int depth,
avifPixelFormat yuv_format, avifPlanesFlags planes,
avifRange yuv_range) {
@@ -37,7 +65,7 @@
avifPixelFormatInfo info;
avifGetPixelFormatInfo(image->yuvFormat, &info);
- for (int c : AVIF_CHANS) {
+ for (int c = 0; c < 4; c++) {
uint8_t* row = (c == AVIF_CHAN_A) ? image->alphaPlane : image->yuvPlanes[c];
if (!row) {
continue;
@@ -69,7 +97,7 @@
avifPixelFormatInfo info;
avifGetPixelFormatInfo(image->yuvFormat, &info);
- for (int c : AVIF_CHANS) {
+ for (int c = 0; c < 4; c++) {
uint8_t* row = (c == AVIF_CHAN_A) ? image->alphaPlane : image->yuvPlanes[c];
if (!row) {
continue;
@@ -99,6 +127,30 @@
}
}
+namespace {
+template <typename PixelType>
+void FillImageChannel(avifRGBImage* image, uint32_t channel_offset,
+ uint32_t value) {
+ const uint32_t channel_count = avifRGBFormatChannelCount(image->format);
+ assert(channel_offset < channel_count);
+ for (uint32_t y = 0; y < image->height; ++y) {
+ PixelType* pixel =
+ reinterpret_cast<PixelType*>(image->pixels + image->rowBytes * y);
+ for (uint32_t x = 0; x < image->width; ++x) {
+ pixel[channel_offset] = static_cast<PixelType>(value);
+ pixel += channel_count;
+ }
+ }
+}
+} // namespace
+
+void FillImageChannel(avifRGBImage* image, uint32_t channel_offset,
+ uint32_t value) {
+ (image->depth <= 8)
+ ? FillImageChannel<uint8_t>(image, channel_offset, value)
+ : FillImageChannel<uint16_t>(image, channel_offset, value);
+}
+
//------------------------------------------------------------------------------
// Returns true if image1 and image2 are identical.
@@ -113,7 +165,7 @@
avifPixelFormatInfo info;
avifGetPixelFormatInfo(image1.yuvFormat, &info);
- for (int c : AVIF_CHANS) {
+ for (int c = 0; c < 4; c++) {
uint8_t* row1 =
(c == AVIF_CHAN_A) ? image1.alphaPlane : image1.yuvPlanes[c];
uint8_t* row2 =
diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h
index 4326b2d..a0e58ba 100644
--- a/tests/gtest/aviftest_helpers.h
+++ b/tests/gtest/aviftest_helpers.h
@@ -11,6 +11,9 @@
namespace libavif {
namespace testutil {
+//------------------------------------------------------------------------------
+// Memory management
+
using AvifImagePtr = std::unique_ptr<avifImage, decltype(&avifImageDestroy)>;
using AvifEncoderPtr =
std::unique_ptr<avifEncoder, decltype(&avifEncoderDestroy)>;
@@ -23,6 +26,22 @@
~AvifRwData() { avifRWDataFree(this); }
};
+class AvifRgbImage : public avifRGBImage {
+ public:
+ AvifRgbImage(const avifImage* yuv, int rgbDepth, avifRGBFormat rgbFormat);
+ ~AvifRgbImage() { avifRGBImageFreePixels(this); }
+};
+
+//------------------------------------------------------------------------------
+// Samples and images
+
+// Contains the sample position of each channel for a given avifRGBFormat.
+// The alpha sample position is set to 0 for layouts having no alpha channel.
+struct RgbChannelOffsets {
+ uint8_t r, g, b, a;
+};
+RgbChannelOffsets GetRgbChannelOffsets(avifRGBFormat format);
+
// Creates an image. Returns null in case of memory failure.
AvifImagePtr CreateImage(int width, int height, int depth,
avifPixelFormat yuv_format, avifPlanesFlags planes,
@@ -31,10 +50,14 @@
// Set all pixels of each plane of an image.
void FillImagePlain(avifImage* image, const uint32_t yuva[4]);
void FillImageGradient(avifImage* image);
+void FillImageChannel(avifRGBImage* image, uint32_t channel_offset,
+ uint32_t value);
// Returns true if both images have the same features and pixel values.
bool AreImagesEqual(const avifImage& image1, const avifImage& image2);
+//------------------------------------------------------------------------------
+
} // namespace testutil
} // namespace libavif