| // Copyright 2020 Joe Drago. All rights reserved. |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "avif/avif.h" |
| |
| #include <inttypes.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #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"; |
| } |
| 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 |
| |
| int 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); |
| image->colorPrimaries = cicp->cp; |
| image->transferCharacteristics = cicp->tc; |
| image->matrixCoefficients = cicp->mc; |
| image->yuvRange = range; |
| avifImageAllocatePlanes(image, AVIF_PLANES_YUV); |
| |
| avifRGBImage srcRGB; |
| avifRGBImageSetDefaults(&srcRGB, image); |
| srcRGB.format = AVIF_RGB_FORMAT_RGB; |
| srcRGB.depth = rgbDepth; |
| avifRGBImageAllocatePixels(&srcRGB); |
| |
| avifRGBImage dstRGB; |
| avifRGBImageSetDefaults(&dstRGB, image); |
| dstRGB.format = AVIF_RGB_FORMAT_RGB; |
| dstRGB.depth = rgbDepth; |
| avifRGBImageAllocatePixels(&dstRGB); |
| |
| 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 { |
| printf("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); |
| |
| for (int yuvDepthIndex = 0; yuvDepthIndex < yuvDepthsCount; ++yuvDepthIndex) { |
| uint32_t yuvDepth = yuvDepths[yuvDepthIndex]; |
| |
| avifRGBImage srcRGB; |
| avifRGBImageSetDefaults(&srcRGB, image); |
| srcRGB.depth = yuvDepth; |
| avifRGBImageAllocatePixels(&srcRGB); |
| 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; |
| image->alphaRange = AVIF_RANGE_FULL; |
| avifImageRGBToYUV(image, &srcRGB); |
| |
| avifRGBImage intermediateRGB; |
| avifRGBImageSetDefaults(&intermediateRGB, image); |
| intermediateRGB.depth = rgbDepth; |
| intermediateRGB.format = rgbFormat; |
| avifRGBImageAllocatePixels(&intermediateRGB); |
| avifImageYUVToRGB(image, &intermediateRGB); |
| |
| avifImageFreePlanes(image, AVIF_PLANES_ALL); |
| avifImageRGBToYUV(image, &intermediateRGB); |
| |
| avifRGBImage dstRGB; |
| avifRGBImageSetDefaults(&dstRGB, image); |
| dstRGB.depth = yuvDepth; |
| avifRGBImageAllocatePixels(&dstRGB); |
| 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; |
| } |
| avifRGBImageAllocatePixels(&rgb); |
| |
| 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) { |
| printf("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) { |
| printf("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; |
| } |