// Copyright 2020 Joe Drago. All rights reserved. // SPDX-License-Identifier: BSD-2-Clause #include "avif/avif.h" #include #include #include #include #include #define MAX_DRIFT 5 #define NEXTARG() \ if (((argIndex + 1) == argc) || (argv[argIndex + 1][0] == '-')) { \ fprintf(stderr, "%s requires an argument.", arg); \ return 1; \ } \ arg = argv[++argIndex] // avifyuv: // The goal here isn't to get perfect matches, as some codepoints will drift due to depth rescaling and/or YUV conversion. // The "Matches"/"NoMatches" is just there as a quick visual confirmation when scanning the results. // If you choose a more friendly starting color instead of orange (red, perhaps), you get considerably more matches, // except in the cases where it doesn't make sense (going to RGB/BGR will forget the alpha / make it opaque). static const char * rgbFormatToString(avifRGBFormat format) { switch (format) { case AVIF_RGB_FORMAT_RGB: return "RGB "; case AVIF_RGB_FORMAT_RGBA: return "RGBA"; case AVIF_RGB_FORMAT_ARGB: return "ARGB"; case AVIF_RGB_FORMAT_BGR: return "BGR "; case AVIF_RGB_FORMAT_BGRA: return "BGRA"; case AVIF_RGB_FORMAT_ABGR: return "ABGR"; case AVIF_RGB_FORMAT_RGB_565: return "RGB_565"; case AVIF_RGB_FORMAT_COUNT: break; } return "Unknown"; } typedef struct avifCICP { avifColorPrimaries cp; avifTransferCharacteristics tc; avifMatrixCoefficients mc; } avifCICP; int main(int argc, char * argv[]) { (void)argc; (void)argv; printf("avif version: %s\n", avifVersion()); int mode = 0; avifBool verbose = AVIF_FALSE; int argIndex = 1; while (argIndex < argc) { const char * arg = argv[argIndex]; if (!strcmp(arg, "-m") || !strcmp(arg, "--mode")) { NEXTARG(); if (!strcmp(arg, "limited")) { mode = 0; } else if (!strcmp(arg, "drift")) { mode = 1; } else if (!strcmp(arg, "rgb")) { mode = 2; } else if (!strcmp(arg, "premultiply")) { mode = 3; } else { mode = atoi(arg); } } else if (!strcmp(arg, "-v") || !strcmp(arg, "--verbose")) { verbose = AVIF_TRUE; } ++argIndex; } const uint32_t yuvDepths[] = { 8, 10, 12 }; const int yuvDepthsCount = (int)(sizeof(yuvDepths) / sizeof(yuvDepths[0])); const uint32_t rgbDepths[] = { 8, 10, 12 }; const int rgbDepthsCount = (int)(sizeof(rgbDepths) / sizeof(rgbDepths[0])); const avifRange ranges[2] = { AVIF_RANGE_FULL, AVIF_RANGE_LIMITED }; if (mode == 0) { // Limited to full conversion roundtripping test uint32_t depth = 8; int maxChannel = (1 << depth) - 1; for (int i = 0; i <= maxChannel; ++i) { int li = avifFullToLimitedY(depth, i); int fi = avifLimitedToFullY(depth, li); const char * prefix = "x"; if (i == fi) { prefix = "."; } printf("%s %d -> %d -> %d\n", prefix, i, li, fi); } } else if (mode == 1) { // Calculate maximum codepoint drift on different combinations of depth and CICPs const avifCICP cicpList[] = { { AVIF_COLOR_PRIMARIES_BT709, AVIF_TRANSFER_CHARACTERISTICS_SRGB, AVIF_MATRIX_COEFFICIENTS_BT709 }, { AVIF_COLOR_PRIMARIES_BT709, AVIF_TRANSFER_CHARACTERISTICS_SRGB, AVIF_MATRIX_COEFFICIENTS_BT601 }, { AVIF_COLOR_PRIMARIES_BT709, AVIF_TRANSFER_CHARACTERISTICS_SRGB, AVIF_MATRIX_COEFFICIENTS_BT2020_NCL }, { AVIF_COLOR_PRIMARIES_BT709, AVIF_TRANSFER_CHARACTERISTICS_SRGB, AVIF_MATRIX_COEFFICIENTS_IDENTITY }, { AVIF_COLOR_PRIMARIES_BT709, AVIF_TRANSFER_CHARACTERISTICS_SRGB, AVIF_MATRIX_COEFFICIENTS_YCGCO }, { AVIF_COLOR_PRIMARIES_SMPTE432, AVIF_TRANSFER_CHARACTERISTICS_SRGB, AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL }, }; const int cicpCount = (int)(sizeof(cicpList) / sizeof(cicpList[0])); for (int rgbDepthIndex = 0; rgbDepthIndex < rgbDepthsCount; ++rgbDepthIndex) { uint32_t rgbDepth = rgbDepths[rgbDepthIndex]; for (int yuvDepthIndex = 0; yuvDepthIndex < yuvDepthsCount; ++yuvDepthIndex) { uint32_t yuvDepth = yuvDepths[yuvDepthIndex]; if (yuvDepth < rgbDepth) { // skip it continue; } for (int cicpIndex = 0; cicpIndex < cicpCount; ++cicpIndex) { const avifCICP * cicp = &cicpList[cicpIndex]; for (int rangeIndex = 0; rangeIndex < 2; ++rangeIndex) { avifRange range = ranges[rangeIndex]; // YCgCo with limited range is not implemented now if (range == AVIF_RANGE_LIMITED && cicp->mc == AVIF_MATRIX_COEFFICIENTS_YCGCO) { printf(" * RGB depth: %d, YUV depth: %d, colorPrimaries: %d, transferCharas: %d, matrixCoeffs: %d, range: Limited\n" " * Skipped: currently not supported.\n", rgbDepth, yuvDepth, cicp->cp, cicp->tc, cicp->mc); continue; } int dim = 1 << rgbDepth; int maxDrift = 0; avifImage * image = avifImageCreate(dim, dim, yuvDepth, AVIF_PIXEL_FORMAT_YUV444); if (!image) { fprintf(stderr, "ERROR: Out of memory\n"); return 1; } image->colorPrimaries = cicp->cp; image->transferCharacteristics = cicp->tc; image->matrixCoefficients = cicp->mc; image->yuvRange = range; if (avifImageAllocatePlanes(image, AVIF_PLANES_YUV) != AVIF_RESULT_OK) { avifImageDestroy(image); printf("ERROR: Out of memory\n"); return 1; } avifRGBImage srcRGB; avifRGBImageSetDefaults(&srcRGB, image); srcRGB.format = AVIF_RGB_FORMAT_RGB; srcRGB.depth = rgbDepth; avifRGBImage dstRGB; avifRGBImageSetDefaults(&dstRGB, image); dstRGB.format = AVIF_RGB_FORMAT_RGB; dstRGB.depth = rgbDepth; if ((avifRGBImageAllocatePixels(&srcRGB) != AVIF_RESULT_OK)) { avifImageDestroy(image); fprintf(stderr, "ERROR: Out of memory\n"); return 1; } if ((avifRGBImageAllocatePixels(&dstRGB) != AVIF_RESULT_OK)) { avifRGBImageFreePixels(&srcRGB); avifImageDestroy(image); fprintf(stderr, "ERROR: Out of memory\n"); return 1; } uint64_t driftPixelCounts[MAX_DRIFT]; for (int i = 0; i < MAX_DRIFT; ++i) { driftPixelCounts[i] = 0; } for (int r = 0; r < dim; ++r) { if (verbose) { printf("[%4d/%4d] RGB depth: %d, YUV depth: %d, colorPrimaries: %d, transferCharas: %d, matrixCoeffs: %d, range: %s\r", r + 1, dim, rgbDepth, yuvDepth, cicp->cp, cicp->tc, cicp->mc, range == AVIF_RANGE_FULL ? "Full" : "Limited"); } for (int g = 0; g < dim; ++g) { uint8_t * row = &srcRGB.pixels[g * srcRGB.rowBytes]; for (int b = 0; b < dim; ++b) { if (rgbDepth == 8) { uint8_t * pixel = &row[b * sizeof(uint8_t) * 3]; pixel[0] = (uint8_t)r; pixel[1] = (uint8_t)g; pixel[2] = (uint8_t)b; } else { uint16_t * pixel = (uint16_t *)&row[b * sizeof(uint16_t) * 3]; pixel[0] = (uint16_t)r; pixel[1] = (uint16_t)g; pixel[2] = (uint16_t)b; } } } avifImageRGBToYUV(image, &srcRGB); avifImageYUVToRGB(image, &dstRGB); for (int y = 0; y < dim; ++y) { const uint8_t * srcRow = &srcRGB.pixels[y * srcRGB.rowBytes]; const uint8_t * dstRow = &dstRGB.pixels[y * dstRGB.rowBytes]; for (int x = 0; x < dim; ++x) { int drift = 0; if (rgbDepth == 8) { const uint8_t * srcPixel = &srcRow[x * sizeof(uint8_t) * 3]; const uint8_t * dstPixel = &dstRow[x * sizeof(uint8_t) * 3]; const int driftR = abs((int)srcPixel[0] - (int)dstPixel[0]); if (drift < driftR) { drift = driftR; } const int driftG = abs((int)srcPixel[1] - (int)dstPixel[1]); if (drift < driftG) { drift = driftG; } const int driftB = abs((int)srcPixel[2] - (int)dstPixel[2]); if (drift < driftB) { drift = driftB; } } else { const uint16_t * srcPixel = (const uint16_t *)&srcRow[x * sizeof(uint16_t) * 3]; const uint16_t * dstPixel = (const uint16_t *)&dstRow[x * sizeof(uint16_t) * 3]; const int driftR = abs((int)srcPixel[0] - (int)dstPixel[0]); if (drift < driftR) { drift = driftR; } const int driftG = abs((int)srcPixel[1] - (int)dstPixel[1]); if (drift < driftG) { drift = driftG; } const int driftB = abs((int)srcPixel[2] - (int)dstPixel[2]); if (drift < driftB) { drift = driftB; } } if (drift < MAX_DRIFT) { ++driftPixelCounts[drift]; if (maxDrift < drift) { maxDrift = drift; } } else { fprintf(stderr, "ERROR: Encountered a drift greater than or equal to MAX_DRIFT(%d): %d\n", MAX_DRIFT, drift); return 1; } } } } if (verbose) { printf("\n"); } printf(" * RGB depth: %d, YUV depth: %d, colorPrimaries: %d, transferCharas: %d, matrixCoeffs: %d, range: %s, maxDrift: %2d\n", rgbDepth, yuvDepth, cicp->cp, cicp->tc, cicp->mc, range == AVIF_RANGE_FULL ? "Full" : "Limited", maxDrift); const uint64_t totalPixelCount = (uint64_t)dim * dim * dim; for (int i = 0; i < MAX_DRIFT; ++i) { if (verbose && (driftPixelCounts[i] > 0)) { printf(" * drift: %2d -> %12" PRIu64 " / %12" PRIu64 " pixels (%.2f %%)\n", i, driftPixelCounts[i], totalPixelCount, (double)driftPixelCounts[i] * 100.0 / (double)totalPixelCount); } } avifRGBImageFreePixels(&srcRGB); avifRGBImageFreePixels(&dstRGB); avifImageDestroy(image); } } } } } else if (mode == 2) { // Stress test all RGB depths uint32_t originalWidth = 32; uint32_t originalHeight = 32; avifBool showAllResults = AVIF_TRUE; avifImage * image = avifImageCreate(originalWidth, originalHeight, 8, AVIF_PIXEL_FORMAT_YUV444); if (!image) { fprintf(stderr, "ERROR: Out of memory\n"); return 1; } for (int yuvDepthIndex = 0; yuvDepthIndex < yuvDepthsCount; ++yuvDepthIndex) { uint32_t yuvDepth = yuvDepths[yuvDepthIndex]; avifRGBImage srcRGB; avifRGBImageSetDefaults(&srcRGB, image); srcRGB.depth = yuvDepth; if (avifRGBImageAllocatePixels(&srcRGB) != AVIF_RESULT_OK) { avifImageDestroy(image); fprintf(stderr, "ERROR: Out of memory\n"); return 1; } if (yuvDepth > 8) { float maxChannelF = (float)((1 << yuvDepth) - 1); for (uint32_t j = 0; j < srcRGB.height; ++j) { for (uint32_t i = 0; i < srcRGB.width; ++i) { uint16_t * pixel = (uint16_t *)&srcRGB.pixels[(8 * i) + (srcRGB.rowBytes * j)]; pixel[0] = (uint16_t)maxChannelF; // R pixel[1] = (uint16_t)(maxChannelF * 0.5f); // G pixel[2] = 0; // B pixel[3] = (uint16_t)(maxChannelF * 0.5f); // A } } } else { for (uint32_t j = 0; j < srcRGB.height; ++j) { for (uint32_t i = 0; i < srcRGB.width; ++i) { uint8_t * pixel = &srcRGB.pixels[(4 * i) + (srcRGB.rowBytes * j)]; pixel[0] = 255; // R pixel[1] = 128; // G pixel[2] = 0; // B pixel[3] = 128; // A } } } const uint32_t depths[4] = { 8, 10, 12, 16 }; for (int depthIndex = 0; depthIndex < 4; ++depthIndex) { uint32_t rgbDepth = depths[depthIndex]; for (int rangeIndex = 0; rangeIndex < 2; ++rangeIndex) { avifRange yuvRange = ranges[rangeIndex]; const avifRGBFormat rgbFormats[6] = { AVIF_RGB_FORMAT_RGB, AVIF_RGB_FORMAT_RGBA, AVIF_RGB_FORMAT_ARGB, AVIF_RGB_FORMAT_BGR, AVIF_RGB_FORMAT_BGRA, AVIF_RGB_FORMAT_ABGR }; for (int rgbFormatIndex = 0; rgbFormatIndex < 6; ++rgbFormatIndex) { avifRGBFormat rgbFormat = rgbFormats[rgbFormatIndex]; // ---------------------------------------------------------------------- avifImageFreePlanes(image, AVIF_PLANES_ALL); image->depth = yuvDepth; image->yuvRange = yuvRange; avifImageRGBToYUV(image, &srcRGB); avifRGBImage intermediateRGB; avifRGBImageSetDefaults(&intermediateRGB, image); intermediateRGB.depth = rgbDepth; intermediateRGB.format = rgbFormat; if (avifRGBImageAllocatePixels(&intermediateRGB) != AVIF_RESULT_OK) { avifRGBImageFreePixels(&srcRGB); avifImageDestroy(image); fprintf(stderr, "ERROR: Out of memory\n"); return 1; } avifImageYUVToRGB(image, &intermediateRGB); avifImageFreePlanes(image, AVIF_PLANES_ALL); avifImageRGBToYUV(image, &intermediateRGB); avifRGBImage dstRGB; avifRGBImageSetDefaults(&dstRGB, image); dstRGB.depth = yuvDepth; if (avifRGBImageAllocatePixels(&dstRGB) != AVIF_RESULT_OK) { avifRGBImageFreePixels(&intermediateRGB); avifRGBImageFreePixels(&srcRGB); avifImageDestroy(image); fprintf(stderr, "ERROR: Out of memory\n"); return 1; } avifImageYUVToRGB(image, &dstRGB); avifBool moveOn = AVIF_FALSE; for (uint32_t j = 0; j < originalHeight; ++j) { if (moveOn) break; for (uint32_t i = 0; i < originalWidth; ++i) { if (yuvDepth > 8) { uint16_t * srcPixel = (uint16_t *)&srcRGB.pixels[(8 * i) + (srcRGB.rowBytes * j)]; uint16_t * dstPixel = (uint16_t *)&dstRGB.pixels[(8 * i) + (dstRGB.rowBytes * j)]; avifBool matches = (memcmp(srcPixel, dstPixel, 8) == 0); if (showAllResults || !matches) { printf("yuvDepth:%2d rgbFormat:%s rgbDepth:%2d yuvRange:%7s (%d,%d) [%7s] (%d, %d, %d, %d) -> (%d, %d, %d, %d)\n", yuvDepth, rgbFormatToString(rgbFormat), rgbDepth, (yuvRange == AVIF_RANGE_LIMITED) ? "Limited" : "Full", i, j, matches ? "Match" : "NoMatch", srcPixel[0], srcPixel[1], srcPixel[2], srcPixel[3], dstPixel[0], dstPixel[1], dstPixel[2], dstPixel[3]); moveOn = AVIF_TRUE; break; } } else { uint8_t * srcPixel = &srcRGB.pixels[(4 * i) + (srcRGB.rowBytes * j)]; uint8_t * dstPixel = &dstRGB.pixels[(4 * i) + (dstRGB.rowBytes * j)]; avifBool matches = (memcmp(srcPixel, dstPixel, 4) == 0); if (showAllResults || !matches) { printf("yuvDepth:%2d rgbFormat:%s rgbDepth:%2d yuvRange:%7s (%d,%d) [%7s] (%d, %d, %d, %d) -> (%d, %d, %d, %d)\n", yuvDepth, rgbFormatToString(rgbFormat), rgbDepth, (yuvRange == AVIF_RANGE_LIMITED) ? "Limited" : "Full", i, j, matches ? "Match" : "NoMatch", srcPixel[0], srcPixel[1], srcPixel[2], srcPixel[3], dstPixel[0], dstPixel[1], dstPixel[2], dstPixel[3]); moveOn = AVIF_TRUE; break; } } } } avifRGBImageFreePixels(&intermediateRGB); avifRGBImageFreePixels(&dstRGB); // ---------------------------------------------------------------------- } } } avifRGBImageFreePixels(&srcRGB); } avifImageDestroy(image); } else if (mode == 3) { // alpha premultiply roundtrip test const uint32_t depths[4] = { 8, 10, 12, 16 }; uint64_t driftPixelCounts[MAX_DRIFT]; for (int depthIndex = 0; depthIndex < 4; ++depthIndex) { uint32_t rgbDepth = depths[depthIndex]; uint32_t size = 1 << rgbDepth; avifRGBImage rgb; memset(&rgb, 0, sizeof(rgb)); rgb.alphaPremultiplied = AVIF_TRUE; rgb.pixels = NULL; rgb.format = AVIF_RGB_FORMAT_RGBA; rgb.width = size; rgb.height = 1; rgb.depth = rgbDepth; int maxDrift = 0; for (int i = 0; i < MAX_DRIFT; ++i) { driftPixelCounts[i] = 0; } if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) { fprintf(stderr, "ERROR: Out of memory\n"); return 1; } for (uint32_t a = 0; a < size; ++a) { // meaningful premultiplied RGB value can't exceed A value, so stop at R = A for (uint32_t r = 0; r <= a; ++r) { if (rgbDepth == 8) { uint8_t * pixel = &rgb.pixels[r * sizeof(uint8_t) * 4]; pixel[0] = (uint8_t)r; pixel[1] = 0; pixel[2] = 0; pixel[3] = (uint8_t)a; } else { uint16_t * pixel = (uint16_t *)&rgb.pixels[r * sizeof(uint16_t) * 4]; pixel[0] = (uint16_t)r; pixel[1] = 0; pixel[2] = 0; pixel[3] = (uint16_t)a; } } rgb.width = a + 1; avifRGBImageUnpremultiplyAlpha(&rgb); avifRGBImagePremultiplyAlpha(&rgb); for (uint32_t r = 0; r <= a; ++r) { if (rgbDepth == 8) { uint8_t * pixel = &rgb.pixels[r * sizeof(uint8_t) * 4]; int drift = abs((int)pixel[0] - (int)r); if (drift >= MAX_DRIFT) { fprintf(stderr, "ERROR: Premultiply round-trip difference greater than or equal to MAX_DRIFT(%d): RGB depth: %d, src: %d, dst: %d, alpha: %d.\n", MAX_DRIFT, rgbDepth, pixel[0], r, a); return 1; } if (maxDrift < drift) { maxDrift = drift; } ++driftPixelCounts[drift]; } else { uint16_t * pixel = (uint16_t *)&rgb.pixels[r * sizeof(uint16_t) * 4]; int drift = abs((int)pixel[0] - (int)r); if (drift >= MAX_DRIFT) { fprintf(stderr, "ERROR: Premultiply round-trip difference greater than or equal to MAX_DRIFT(%d): RGB depth: %d, src: %d, dst: %d, alpha: %d.\n", MAX_DRIFT, rgbDepth, pixel[0], r, a); return 1; } if (maxDrift < drift) { maxDrift = drift; } ++driftPixelCounts[drift]; } } if (verbose) { printf("[%5d/%5d] RGB depth: %d\r", a + 1, size, rgbDepth); } } if (verbose) { printf("\n"); } printf(" * RGB depth: %d, maxDrift: %2d\n", rgbDepth, maxDrift); avifRGBImageFreePixels(&rgb); const uint64_t totalPixelCount = (uint64_t)(size + 1) * size / 2; for (int i = 0; i < MAX_DRIFT; ++i) { if (verbose && (driftPixelCounts[i] > 0)) { printf(" * drift: %2d -> %12" PRIu64 " / %12" PRIu64 " pixels (%.2f %%)\n", i, driftPixelCounts[i], totalPixelCount, (double)driftPixelCounts[i] * 100.0 / (double)totalPixelCount); } } } } return 0; }