Skip to content

Commit c898b5e

Browse files
authored
Add sato BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B (#2215)
Sample Transform derived image item support for residual bit depth extension.
1 parent 9ea9a94 commit c898b5e

File tree

5 files changed

+162
-25
lines changed

5 files changed

+162
-25
lines changed

include/avif/avif.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,16 @@ typedef enum avifSampleTransformRecipe
746746
// (ignoring the hidden image item), leading to a valid image but with
747747
// precision loss (16-bit samples truncated to the 12 most significant
748748
// bits).
749-
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B
749+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B,
750+
// Encode the 12 most significant bits of each input image sample lossily or
751+
// losslessly into a base image. The difference between the original and
752+
// decoded values of these samples is encoded as a separate 8-bit hidden
753+
// image item. The two are combined at decoding into one image with the same
754+
// bit depth as the original image. It is backward compatible in the sense
755+
// that it is possible to decode only the base image (ignoring the hidden
756+
// image item), leading to a valid image but with loss due to precision
757+
// truncation and/or compression.
758+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B
750759
} avifSampleTransformRecipe;
751760
#endif // AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM
752761

src/sampletransform.c

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,42 @@ avifResult avifSampleTransformRecipeToExpression(avifSampleTransformRecipe recip
137137
return AVIF_RESULT_OK;
138138
}
139139

140+
if (recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B) {
141+
// reference_count is two: one 12-bit input image and one 8-bit input image.
142+
// (base_sample << 4) + hidden_sample
143+
// Note: Both base_sample and hidden_sample are encoded lossily or losslessly. hidden_sample overlaps
144+
// with base_sample by 4 bits to alleviate the loss caused by the quantization of base_sample.
145+
AVIF_CHECKERR(avifArrayCreate(expression, sizeof(avifSampleTransformToken), 7), AVIF_RESULT_OUT_OF_MEMORY);
146+
147+
// The base image represents the 12 most significant bits of the reconstructed, bit-depth-extended output image.
148+
// Left shift the base image (which is also the primary item, or the auxiliary alpha item of the primary item)
149+
// by 4 bits. This is equivalent to multiplying by 2^4.
150+
AVIF_ASSERT_OR_RETURN(avifPushConstant(expression, 16));
151+
AVIF_ASSERT_OR_RETURN(avifPushInputImageItem(expression, 1));
152+
AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_PRODUCT));
153+
154+
// The second image represents the offset to apply to the shifted base image to retrieve
155+
// the original image, with some loss due to quantization.
156+
AVIF_ASSERT_OR_RETURN(avifPushInputImageItem(expression, 2));
157+
AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_SUM));
158+
159+
// The second image is offset by 128 to have unsigned values to encode.
160+
// Correct that last to always work with unsigned values in the operations above.
161+
AVIF_ASSERT_OR_RETURN(avifPushConstant(expression, 128));
162+
AVIF_ASSERT_OR_RETURN(avifPushOperator(expression, AVIF_SAMPLE_TRANSFORM_DIFFERENCE));
163+
// Sample values are clamped to [0:1<<depth[ at that point.
164+
return AVIF_RESULT_OK;
165+
}
166+
140167
return AVIF_RESULT_INVALID_ARGUMENT;
141168
}
142169

143170
avifResult avifSampleTransformExpressionToRecipe(const avifSampleTransformExpression * expression, avifSampleTransformRecipe * recipe)
144171
{
145172
*recipe = AVIF_SAMPLE_TRANSFORM_NONE;
146173
const avifSampleTransformRecipe kAllRecipes[] = { AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B,
147-
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B };
174+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B,
175+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B };
148176
for (size_t i = 0; i < sizeof(kAllRecipes) / sizeof(kAllRecipes[0]); ++i) {
149177
avifSampleTransformRecipe candidateRecipe = kAllRecipes[i];
150178
avifSampleTransformExpression candidateExpression = { 0 };

src/write.c

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,8 @@ static avifResult avifEncoderCreateBitDepthExtensionItems(avifEncoder * encoder,
12101210
uint16_t colorItemID)
12111211
{
12121212
AVIF_ASSERT_OR_RETURN(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B ||
1213-
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B);
1213+
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B ||
1214+
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B);
12141215

12151216
// There are multiple possible ISOBMFF box hierarchies for translucent images,
12161217
// using 'sato' (Sample Transform) derived image items:
@@ -1300,7 +1301,52 @@ static avifResult avifImageCreateAllocate(avifImage ** sampleTransformedImage, c
13001301
return avifImageAllocatePlanes(*sampleTransformedImage, planes);
13011302
}
13021303

1303-
static avifResult avifEncoderCreateSatoImage(const avifEncoder * encoder,
1304+
// Finds the encoded base image and decodes it. Callers of this function must free
1305+
// *codec and *decodedBaseImage if not null, whether the function succeeds or not.
1306+
static avifResult avifEncoderDecodeSatoBaseImage(avifEncoder * encoder,
1307+
const avifImage * original,
1308+
uint32_t numBits,
1309+
avifPlanesFlag planes,
1310+
avifCodec ** codec,
1311+
avifImage ** decodedBaseImage)
1312+
{
1313+
avifDecodeSample sample;
1314+
memset(&sample, 0, sizeof(sample));
1315+
sample.spatialID = AVIF_SPATIAL_ID_UNSET;
1316+
1317+
// Find the encoded bytes of the base image item.
1318+
for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
1319+
avifEncoderItem * item = &encoder->data->items.item[itemIndex];
1320+
if ((item->itemCategory != AVIF_ITEM_COLOR || planes != AVIF_PLANES_YUV) &&
1321+
(item->itemCategory != AVIF_ITEM_ALPHA || planes != AVIF_PLANES_A)) {
1322+
continue;
1323+
}
1324+
1325+
AVIF_ASSERT_OR_RETURN(item->encodeOutput != NULL); // TODO: Support grids?
1326+
AVIF_ASSERT_OR_RETURN(item->encodeOutput->samples.count == 1);
1327+
AVIF_ASSERT_OR_RETURN(item->encodeOutput->samples.sample[0].data.size != 0);
1328+
AVIF_ASSERT_OR_RETURN(sample.data.size == 0); // There should be only one base item.
1329+
sample.data.data = item->encodeOutput->samples.sample[0].data.data;
1330+
sample.data.size = item->encodeOutput->samples.sample[0].data.size;
1331+
}
1332+
AVIF_ASSERT_OR_RETURN(sample.data.size != 0); // There should be at least one base item.
1333+
1334+
// avifCodecGetNextImageFunc() uses only a few fields of its decoder argument.
1335+
avifDecoder decoder;
1336+
memset(&decoder, 0, sizeof(decoder));
1337+
decoder.maxThreads = encoder->maxThreads;
1338+
decoder.imageSizeLimit = AVIF_DEFAULT_IMAGE_SIZE_LIMIT;
1339+
1340+
AVIF_CHECKRES(avifCodecCreate(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_DECODE, codec));
1341+
(*codec)->diag = &encoder->diag;
1342+
AVIF_CHECKRES(avifImageCreateAllocate(decodedBaseImage, original, numBits, planes));
1343+
avifBool isLimitedRangeAlpha = AVIF_FALSE; // Ignored.
1344+
AVIF_CHECKERR((*codec)->getNextImage(*codec, &decoder, &sample, planes == AVIF_PLANES_A, &isLimitedRangeAlpha, *decodedBaseImage),
1345+
AVIF_RESULT_ENCODE_SAMPLE_TRANSFORM_FAILED);
1346+
return AVIF_RESULT_OK;
1347+
}
1348+
1349+
static avifResult avifEncoderCreateSatoImage(avifEncoder * encoder,
13041350
const avifEncoderItem * item,
13051351
avifBool itemWillBeEncodedLosslessly,
13061352
const avifImage * image,
@@ -1323,8 +1369,7 @@ static avifResult avifEncoderCreateSatoImage(const avifEncoder * encoder,
13231369
AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 8, planes));
13241370
AVIF_CHECKRES(avifImageApplyImgOpConst(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_AND, 255, planes));
13251371
}
1326-
} else {
1327-
AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B, AVIF_RESULT_NOT_IMPLEMENTED);
1372+
} else if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) {
13281373
if (isBase) {
13291374
AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 12, planes));
13301375
AVIF_CHECKRES(avifImageApplyImgOpConst(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_DIVIDE, 16, planes));
@@ -1351,11 +1396,49 @@ static avifResult avifEncoderCreateSatoImage(const avifEncoder * encoder,
13511396
avifImageApplyImgOpConst(*sampleTransformedImage, *sampleTransformedImage, AVIF_SAMPLE_TRANSFORM_SUM, 7, planes));
13521397
}
13531398
}
1399+
} else {
1400+
AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B,
1401+
AVIF_RESULT_NOT_IMPLEMENTED);
1402+
if (isBase) {
1403+
AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 12, planes));
1404+
AVIF_CHECKRES(avifImageApplyImgOpConst(*sampleTransformedImage, image, AVIF_SAMPLE_TRANSFORM_DIVIDE, 16, planes));
1405+
} else {
1406+
AVIF_CHECKRES(avifImageCreateAllocate(sampleTransformedImage, image, 8, planes));
1407+
avifCodec * codec = NULL;
1408+
avifImage * decodedBaseImage = NULL;
1409+
avifResult result = avifEncoderDecodeSatoBaseImage(encoder, image, 12, planes, &codec, &decodedBaseImage);
1410+
if (result == AVIF_RESULT_OK) {
1411+
// decoded = main*16+hidden-128 so hidden = clamp_8b(original-main*16+128). Postfix notation.
1412+
const avifSampleTransformToken tokens[] = { { AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, 0, /*inputImageItemIndex=*/1 },
1413+
{ AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, 0, /*inputImageItemIndex=*/2 },
1414+
{ AVIF_SAMPLE_TRANSFORM_CONSTANT, /*constant=*/16, 0 },
1415+
{ AVIF_SAMPLE_TRANSFORM_PRODUCT, 0, 0 },
1416+
{ AVIF_SAMPLE_TRANSFORM_DIFFERENCE, 0, 0 },
1417+
{ AVIF_SAMPLE_TRANSFORM_CONSTANT, /*constant=*/128, 0 },
1418+
{ AVIF_SAMPLE_TRANSFORM_SUM, 0, 0 } };
1419+
// image is "original" (index 1) and decodedBaseImage is "main" (index 2) in the formula above.
1420+
const avifImage * inputImageItems[] = { image, decodedBaseImage };
1421+
result = avifImageApplyOperations(*sampleTransformedImage,
1422+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32,
1423+
/*numTokens=*/7,
1424+
tokens,
1425+
/*numInputImageItems=*/2,
1426+
inputImageItems,
1427+
planes);
1428+
}
1429+
if (decodedBaseImage) {
1430+
avifImageDestroy(decodedBaseImage);
1431+
}
1432+
if (codec) {
1433+
avifCodecDestroy(codec);
1434+
}
1435+
AVIF_CHECKRES(result);
1436+
}
13541437
}
13551438
return AVIF_RESULT_OK;
13561439
}
13571440

1358-
static avifResult avifEncoderCreateBitDepthExtensionImage(const avifEncoder * encoder,
1441+
static avifResult avifEncoderCreateBitDepthExtensionImage(avifEncoder * encoder,
13591442
const avifEncoderItem * item,
13601443
avifBool itemWillBeEncodedLosslessly,
13611444
const avifImage * image,
@@ -1807,7 +1890,8 @@ static avifResult avifEncoderAddImageInternal(avifEncoder * encoder,
18071890

18081891
#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM)
18091892
if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B ||
1810-
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) {
1893+
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B ||
1894+
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B) {
18111895
// For now, only 16-bit depth is supported.
18121896
AVIF_ASSERT_OR_RETURN(firstCell->depth == 16);
18131897
#if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
@@ -2655,7 +2739,8 @@ static avifResult avifRWStreamWriteProperties(avifItemPropertyDedup * const dedu
26552739
uint8_t depth = (uint8_t)itemMetadata->depth;
26562740
#if defined(AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM)
26572741
if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B ||
2658-
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) {
2742+
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B ||
2743+
encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B) {
26592744
if (item->itemCategory == AVIF_ITEM_SAMPLE_TRANSFORM) {
26602745
AVIF_ASSERT_OR_RETURN(depth == 16); // Only 16-bit depth is supported for now.
26612746
} else if (encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B) {

tests/gtest/avif16bittest.cc

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,25 @@ TEST_P(SampleTransformTest, Avif16bit) {
7676
image->alphaPlane ? AVIF_PLANES_ALL : AVIF_PLANES_YUV, image->yuvRange);
7777
ASSERT_NE(image_no_sato, nullptr);
7878

79-
const uint32_t shift =
80-
recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B ? 8 : 4;
81-
const avifImage* inputImage = image.get();
82-
// Postfix notation.
83-
const avifSampleTransformToken tokens[] = {
84-
{AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, 0,
85-
/*inputImageItemIndex=*/1},
86-
{AVIF_SAMPLE_TRANSFORM_CONSTANT, 1 << shift, 0},
87-
{AVIF_SAMPLE_TRANSFORM_DIVIDE, 0, 0}};
88-
ASSERT_EQ(avifImageApplyOperations(
89-
image_no_sato.get(), AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32,
90-
/*numTokens=*/3, tokens, /*numInputImageItems=*/1, &inputImage,
91-
AVIF_PLANES_ALL),
92-
AVIF_RESULT_OK);
93-
EXPECT_TRUE(testutil::AreImagesEqual(*image_no_sato, *decoded_no_sato));
79+
if (recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B ||
80+
recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B) {
81+
// These recipes always encode the primary item losslessly. Check that.
82+
const uint32_t shift =
83+
recipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B ? 8 : 4;
84+
const avifImage* inputImage = image.get();
85+
// Postfix notation.
86+
const avifSampleTransformToken tokens[] = {
87+
{AVIF_SAMPLE_TRANSFORM_INPUT_IMAGE_ITEM_INDEX, 0,
88+
/*inputImageItemIndex=*/1},
89+
{AVIF_SAMPLE_TRANSFORM_CONSTANT, 1 << shift, 0},
90+
{AVIF_SAMPLE_TRANSFORM_DIVIDE, 0, 0}};
91+
ASSERT_EQ(avifImageApplyOperations(
92+
image_no_sato.get(), AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_32,
93+
/*numTokens=*/3, tokens, /*numInputImageItems=*/1,
94+
&inputImage, AVIF_PLANES_ALL),
95+
AVIF_RESULT_OK);
96+
EXPECT_TRUE(testutil::AreImagesEqual(*image_no_sato, *decoded_no_sato));
97+
}
9498
}
9599

96100
//------------------------------------------------------------------------------
@@ -115,6 +119,16 @@ INSTANTIATE_TEST_SUITE_P(
115119
/*quality=*/
116120
testing::Values(AVIF_QUALITY_LOSSLESS)));
117121

122+
INSTANTIATE_TEST_SUITE_P(
123+
ResidualBitDepthExtension, SampleTransformTest,
124+
testing::Combine(
125+
testing::Values(
126+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B),
127+
testing::Values(AVIF_PIXEL_FORMAT_YUV444),
128+
/*create_alpha=*/testing::Values(false),
129+
/*quality=*/
130+
testing::Values(AVIF_QUALITY_DEFAULT)));
131+
118132
INSTANTIATE_TEST_SUITE_P(
119133
Alpha, SampleTransformTest,
120134
testing::Combine(

tests/gtest/avifsampletransformtest.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ TEST(SampleTransformTest, NoRecipe) {
8585
TEST(SampleTransformTest, RecipeToExpression) {
8686
for (avifSampleTransformRecipe recipe :
8787
{AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B,
88-
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B}) {
88+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B,
89+
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B}) {
8990
AvifExpression expression;
9091
ASSERT_EQ(avifSampleTransformRecipeToExpression(recipe, &expression),
9192
AVIF_RESULT_OK);

0 commit comments

Comments
 (0)