Skip to content

Commit

Permalink
Implement AV2 in MinimizedImageBox (#2486)
Browse files Browse the repository at this point in the history
Add avifavmminitest.
Also fix 4:2:0 decoded samples mapped to 4:0:0 output image.
  • Loading branch information
y-guyon authored Oct 24, 2024
1 parent e400780 commit 755f960
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 16 deletions.
4 changes: 3 additions & 1 deletion src/codec_avm.c
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ static avifBool avmCodecGetNextImage(struct avifCodec * codec,
return AVIF_FALSE;
}
if (codec->internal->image->monochrome) {
// avm does not handle monochrome as of research-v8.0.0.
// This should not happen.
yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
}

Expand Down Expand Up @@ -645,7 +647,7 @@ static avifResult avmCodecEncodeImage(avifCodec * codec,
cfg->g_threads = AVIF_MIN(encoder->maxThreads, 64);
}

// avm does not handle monochrome as of research-v4.0.0.
// avm does not handle monochrome as of research-v8.0.0.
// TODO(yguyon): Enable when fixed upstream
codec->internal->monochromeEnabled = AVIF_FALSE;

Expand Down
60 changes: 49 additions & 11 deletions src/read.c
Original file line number Diff line number Diff line change
Expand Up @@ -1214,13 +1214,19 @@ static avifResult avifDecoderItemValidateProperties(const avifDecoderItem * item
// This is a MinimizedImageBox ('mini').

if (item->miniBoxPixelFormat != avifCodecConfigurationBoxGetFormat(&configProp->u.av1C)) {
avifDiagnosticsPrintf(diag,
"Item ID %u format [%s] specified by MinimizedImageBox does not match %s property format [%s]",
item->id,
avifPixelFormatToString(item->miniBoxPixelFormat),
configPropName,
avifPixelFormatToString(avifCodecConfigurationBoxGetFormat(&configProp->u.av1C)));
return AVIF_RESULT_BMFF_PARSE_FAILED;
if (!memcmp(configPropName, "av2C", 4) && item->miniBoxPixelFormat == AVIF_PIXEL_FORMAT_YUV400 &&
avifCodecConfigurationBoxGetFormat(&configProp->u.av1C) == AVIF_PIXEL_FORMAT_YUV420) {
// avm does not handle monochrome as of research-v8.0.0.
// 4:2:0 is used instead.
} else {
avifDiagnosticsPrintf(diag,
"Item ID %u format [%s] specified by MinimizedImageBox does not match %s property format [%s]",
item->id,
avifPixelFormatToString(item->miniBoxPixelFormat),
configPropName,
avifPixelFormatToString(avifCodecConfigurationBoxGetFormat(&configProp->u.av1C)));
return AVIF_RESULT_BMFF_PARSE_FAILED;
}
}

if (configProp->u.av1C.chromaSamplePosition == /*CSP_UNKNOWN=*/0) {
Expand Down Expand Up @@ -3779,11 +3785,12 @@ static avifResult avifParseMinimizedImageBox(avifDecoderData * data,
AVIF_CHECKERR(avifROStreamReadBitsU8(&s, &codecConfigType[i], 8), AVIF_RESULT_BMFF_PARSE_FAILED);
}
#if defined(AVIF_CODEC_AVM)
if (!memcmp(infeType, "av02", 4) && !memcmp(codecConfigType, "av2C", 4)) {
return AVIF_RESULT_NOT_IMPLEMENTED;
}
#endif
AVIF_CHECKERR((!memcmp(infeType, "av01", 4) && !memcmp(codecConfigType, "av1C", 4)) ||
(!memcmp(infeType, "av02", 4) && !memcmp(codecConfigType, "av2C", 4)),
AVIF_RESULT_BMFF_PARSE_FAILED);
#else
AVIF_CHECKERR(!memcmp(infeType, "av01", 4) && !memcmp(codecConfigType, "av1C", 4), AVIF_RESULT_BMFF_PARSE_FAILED);
#endif
} else {
AVIF_CHECKERR(isAvifAccordingToMinorVersion, AVIF_RESULT_BMFF_PARSE_FAILED);
memcpy(infeType, "av01", 4);
Expand Down Expand Up @@ -6162,6 +6169,37 @@ static avifResult avifDecoderDecodeTiles(avifDecoder * decoder, uint32_t nextIma
}
}

#if defined(AVIF_CODEC_AVM)
avifDecoderItem * tileItem = NULL;
for (uint32_t itemIndex = 0; itemIndex < decoder->data->meta->items.count; ++itemIndex) {
avifDecoderItem * item = decoder->data->meta->items.item[itemIndex];
if (avifDecoderItemShouldBeSkipped(item)) {
continue;
}
if (item->id == sample->itemID) {
tileItem = item;
break;
}
}
if (tileItem != NULL) {
const avifProperty * prop = avifPropertyArrayFind(&tileItem->properties, "pixi");
// Match the decoded image format with the number of planes specified in 'pixi'.
if (prop != NULL && prop->u.pixi.planeCount == 1 && tile->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) {
// Codecs such as avm do not support monochrome so samples were encoded as 4:2:0.
// Ignore the UV planes at decoding.
tile->image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
if (tile->image->imageOwnsYUVPlanes) {
avifFree(tile->image->yuvPlanes[AVIF_CHAN_U]);
avifFree(tile->image->yuvPlanes[AVIF_CHAN_V]);
}
tile->image->yuvPlanes[AVIF_CHAN_U] = NULL;
tile->image->yuvRowBytes[AVIF_CHAN_U] = 0;
tile->image->yuvPlanes[AVIF_CHAN_V] = NULL;
tile->image->yuvRowBytes[AVIF_CHAN_V] = 0;
}
}
#endif

++info->decodedTileCount;

const avifBool isGrid = (info->grid.rows > 0) && (info->grid.columns > 0);
Expand Down
21 changes: 19 additions & 2 deletions src/write.c
Original file line number Diff line number Diff line change
Expand Up @@ -2596,7 +2596,19 @@ static avifResult avifEncoderWriteMiniBox(avifEncoder * encoder, avifRWStream *

const uint32_t orientationMinus1 = avifImageIrotImirToExifOrientation(image) - 1;

const avifBool hasExplicitCodecTypes = AVIF_FALSE; // 'av01' and 'av1C' known from 'avif' minor_version field of FileTypeBox.
uint8_t infeType[4];
uint8_t codecConfigType[4];
avifBool hasExplicitCodecTypes;
if (encoder->codecChoice == AVIF_CODEC_CHOICE_AVM) {
memcpy(infeType, "av02", 4);
memcpy(codecConfigType, "av2C", 4); // Same syntax as 'av1C'.
hasExplicitCodecTypes = AVIF_TRUE;
} else {
memcpy(infeType, "av01", 4);
memcpy(codecConfigType, "av1C", 4);
// 'av01' and 'av1C' are implied by 'avif' minor_version field of FileTypeBox. No need to write them.
hasExplicitCodecTypes = AVIF_FALSE;
}

uint32_t smallDimensionsFlag = image->width <= (1 << 7) && image->height <= (1 << 7);
const uint32_t codecConfigSize = 4; // 'av1C' always uses 4 bytes.
Expand Down Expand Up @@ -2676,8 +2688,13 @@ static avifResult avifEncoderWriteMiniBox(avifEncoder * encoder, avifRWStream *

if (hasExplicitCodecTypes) {
// bit(32) infe_type;
for (int i = 0; i < 4; ++i) {
AVIF_CHECKRES(avifRWStreamWriteBits(s, infeType[i], 8));
}
// bit(32) codec_config_type;
AVIF_ASSERT_OR_RETURN(AVIF_FALSE);
for (int i = 0; i < 4; ++i) {
AVIF_CHECKRES(avifRWStreamWriteBits(s, codecConfigType[i], 8));
}
}

// High Dynamic Range properties
Expand Down
3 changes: 3 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ endif()
if(AVIF_CODEC_AVM_ENABLED)
if(AVIF_ENABLE_GTEST)
add_avif_gtest(avifavmtest)
if(AVIF_ENABLE_EXPERIMENTAL_MINI)
add_avif_gtest(avifavmminitest)
endif()
endif()

if(AVIF_BUILD_APPS)
Expand Down
126 changes: 126 additions & 0 deletions tests/gtest/avifavmminitest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2024 Google LLC
// SPDX-License-Identifier: BSD-2-Clause

#include "avif/avif.h"
#include "aviftest_helpers.h"
#include "gtest/gtest.h"

using testing::Combine;
using testing::Values;

namespace avif {
namespace {

class AvmMiniTest : public testing::TestWithParam<
std::tuple</*width=*/int, /*height=*/int, /*depth=*/int,
avifPixelFormat, /*alpha=*/bool>> {};

TEST_P(AvmMiniTest, EncodeDecode) {
const int width = std::get<0>(GetParam());
const int height = std::get<1>(GetParam());
const int depth = std::get<2>(GetParam());
const avifPixelFormat format = std::get<3>(GetParam());
const bool alpha = std::get<4>(GetParam());

ImagePtr image = testutil::CreateImage(
width, height, depth, format, alpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV);
ASSERT_NE(image, nullptr);
testutil::FillImageGradient(image.get());

EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
encoder->codecChoice = AVIF_CODEC_CHOICE_AVM;
encoder->headerFormat = AVIF_HEADER_REDUCED;
testutil::AvifRwData encoded;
ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded),
AVIF_RESULT_OK);

ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
// No need to set AVIF_CODEC_CHOICE_AVM. The decoder should recognize AV2.
ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size),
AVIF_RESULT_OK);

// Verify that the input and decoded images are close.
EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0);

// Forcing an AV1 decoding codec should fail.
for (avifCodecChoice av1_codec :
{AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_CHOICE_DAV1D,
AVIF_CODEC_CHOICE_LIBGAV1}) {
decoder->codecChoice = av1_codec;
// An error is expected because av1_codec is not enabled or because we are
// trying to decode an AV2 file with an AV1 codec.
ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size),
avifCodecName(av1_codec, AVIF_CODEC_FLAG_CAN_DECODE)
? AVIF_RESULT_DECODE_COLOR_FAILED
: AVIF_RESULT_NO_CODEC_AVAILABLE);
}
}

INSTANTIATE_TEST_SUITE_P(Basic, AvmMiniTest,
Combine(/*width=*/Values(12), /*height=*/Values(34),
/*depth=*/Values(8),
Values(AVIF_PIXEL_FORMAT_YUV400,
AVIF_PIXEL_FORMAT_YUV420,
AVIF_PIXEL_FORMAT_YUV444),
/*alpha=*/Values(false, true)));

INSTANTIATE_TEST_SUITE_P(Tiny, AvmMiniTest,
Combine(/*width=*/Values(1), /*height=*/Values(1),
/*depth=*/Values(8),
Values(AVIF_PIXEL_FORMAT_YUV444),
/*alpha=*/Values(false)));

// TODO(yguyon): Implement or fix in avm then test the following combinations.
INSTANTIATE_TEST_SUITE_P(DISABLED_Broken, AvmMiniTest,
Combine(/*width=*/Values(1), /*height=*/Values(34),
/*depth=*/Values(8, 10, 12),
Values(AVIF_PIXEL_FORMAT_YUV400,
AVIF_PIXEL_FORMAT_YUV420,
AVIF_PIXEL_FORMAT_YUV444),
/*alpha=*/Values(true)));

TEST(AvmMiniTest, Av1StillWorksWhenAvmIsEnabled) {
if (!testutil::Av1EncoderAvailable() || !testutil::Av1DecoderAvailable()) {
GTEST_SKIP() << "AV1 codec unavailable, skip test.";
}
// avm is the only AV2 codec, so the default codec will be an AV1 one.

ImagePtr image =
testutil::CreateImage(/*width=*/64, /*height=*/64, /*depth=*/8,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_ALL);
ASSERT_NE(image, nullptr);
testutil::FillImageGradient(image.get());

EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
encoder->headerFormat = AVIF_HEADER_REDUCED;
testutil::AvifRwData encoded;
ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded),
AVIF_RESULT_OK);

ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size),
AVIF_RESULT_OK);

// Verify that the input and decoded images are close.
EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0);

// Forcing an AV2 decoding codec should fail.
decoder->codecChoice = AVIF_CODEC_CHOICE_AVM;
ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size),
AVIF_RESULT_DECODE_COLOR_FAILED);
}

} // namespace
} // namespace avif
5 changes: 3 additions & 2 deletions tests/gtest/avifavmtest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ TEST_P(AvmTest, EncodeDecode) {
INSTANTIATE_TEST_SUITE_P(Basic, AvmTest,
Combine(/*width=*/Values(12), /*height=*/Values(34),
/*depth=*/Values(8),
Values(AVIF_PIXEL_FORMAT_YUV420,
Values(AVIF_PIXEL_FORMAT_YUV400,
AVIF_PIXEL_FORMAT_YUV420,
AVIF_PIXEL_FORMAT_YUV444),
/*alpha=*/Values(true)));
/*alpha=*/Values(false, true)));

INSTANTIATE_TEST_SUITE_P(Tiny, AvmTest,
Combine(/*width=*/Values(1), /*height=*/Values(1),
Expand Down

0 comments on commit 755f960

Please sign in to comment.