// SPDX-License-Identifier: Apache-2.0 // ---------------------------------------------------------------------------- // Copyright 2011-2022 Arm Limited // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at: // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // ---------------------------------------------------------------------------- /** * @brief Functions for computing image error metrics. */ #include #include #include "astcenccli_internal.h" /** * @brief An accumulator for errors. */ class error_accum4 { public: /** @brief The running sum. */ double sum_r { 0.0 }; double sum_g { 0.0 }; double sum_b { 0.0 }; double sum_a { 0.0 }; }; /** * @brief Incremental addition operator for error accumulators. * * @param val The accumulator to increment * @param inc The increment to apply * * @return The updated accumulator */ static error_accum4& operator+=( error_accum4 &val, vfloat4 inc ) { val.sum_r += static_cast(inc.lane<0>()); val.sum_g += static_cast(inc.lane<1>()); val.sum_b += static_cast(inc.lane<2>()); val.sum_a += static_cast(inc.lane<3>()); return val; } /** * @brief mPSNR tone-mapping operator for HDR images. * * @param val The color value to tone map * @param fstop The exposure fstop; should be in range [-125, 125] * * @return The mapped color value in [0.0f, 255.0f] range */ static float mpsnr_operator( float val, int fstop ) { if32 p; p.u = 0x3f800000 + (fstop << 23); // 0x3f800000 is 1.0f val *= p.f; val = powf(val, (1.0f / 2.2f)); val *= 255.0f; return astc::clamp(val, 0.0f, 255.0f); } /** * @brief mPSNR difference between two values. * * Differences are given as "val1 - val2". * * @param val1 The first color value * @param val2 The second color value * @param fstop_lo The low exposure fstop; should be in range [-125, 125] * @param fstop_hi The high exposure fstop; should be in range [-125, 125] * * @return The summed mPSNR difference across all active fstop levels */ static float mpsnr_sumdiff( float val1, float val2, int fstop_lo, int fstop_hi ) { float summa = 0.0f; for (int i = fstop_lo; i <= fstop_hi; i++) { float mval1 = mpsnr_operator(val1, i); float mval2 = mpsnr_operator(val2, i); float mdiff = mval1 - mval2; summa += mdiff * mdiff; } return summa; } /* See header for documentation */ void compute_error_metrics( bool compute_hdr_metrics, bool compute_normal_metrics, int input_components, const astcenc_image* img1, const astcenc_image* img2, int fstop_lo, int fstop_hi ) { static const int componentmasks[5] { 0x00, 0x07, 0x0C, 0x07, 0x0F }; int componentmask = componentmasks[input_components]; error_accum4 errorsum; error_accum4 alpha_scaled_errorsum; error_accum4 log_errorsum; error_accum4 mpsnr_errorsum; double mean_angular_errorsum = 0.0; double worst_angular_errorsum = 0.0; unsigned int dim_x = astc::min(img1->dim_x, img2->dim_x); unsigned int dim_y = astc::min(img1->dim_y, img2->dim_y); unsigned int dim_z = astc::min(img1->dim_z, img2->dim_z); if (img1->dim_x != img2->dim_x || img1->dim_y != img2->dim_y || img1->dim_z != img2->dim_z) { printf("WARNING: Only intersection of images will be compared:\n" " Image 1: %dx%dx%d\n" " Image 2: %dx%dx%d\n", img1->dim_x, img1->dim_y, img1->dim_z, img2->dim_x, img2->dim_y, img2->dim_z); } double rgb_peak = 0.0; unsigned int xsize1 = img1->dim_x; unsigned int xsize2 = img2->dim_x; for (unsigned int z = 0; z < dim_z; z++) { for (unsigned int y = 0; y < dim_y; y++) { for (unsigned int x = 0; x < dim_x; x++) { vfloat4 color1; vfloat4 color2; if (img1->data_type == ASTCENC_TYPE_U8) { uint8_t* data8 = static_cast(img1->data[z]); color1 = vfloat4( data8[(4 * xsize1 * y) + (4 * x )], data8[(4 * xsize1 * y) + (4 * x + 1)], data8[(4 * xsize1 * y) + (4 * x + 2)], data8[(4 * xsize1 * y) + (4 * x + 3)]); color1 = color1 / 255.0f; } else if (img1->data_type == ASTCENC_TYPE_F16) { uint16_t* data16 = static_cast(img1->data[z]); vint4 color1i = vint4( data16[(4 * xsize1 * y) + (4 * x )], data16[(4 * xsize1 * y) + (4 * x + 1)], data16[(4 * xsize1 * y) + (4 * x + 2)], data16[(4 * xsize1 * y) + (4 * x + 3)]); color1 = float16_to_float(color1i); color1 = clamp(0, 65504.0f, color1); } else // if (img1->data_type == ASTCENC_TYPE_F32) { assert(img1->data_type == ASTCENC_TYPE_F32); float* data32 = static_cast(img1->data[z]); color1 = vfloat4( data32[(4 * xsize1 * y) + (4 * x )], data32[(4 * xsize1 * y) + (4 * x + 1)], data32[(4 * xsize1 * y) + (4 * x + 2)], data32[(4 * xsize1 * y) + (4 * x + 3)]); color1 = clamp(0, 65504.0f, color1); } if (img2->data_type == ASTCENC_TYPE_U8) { uint8_t* data8 = static_cast(img2->data[z]); color2 = vfloat4( data8[(4 * xsize2 * y) + (4 * x )], data8[(4 * xsize2 * y) + (4 * x + 1)], data8[(4 * xsize2 * y) + (4 * x + 2)], data8[(4 * xsize2 * y) + (4 * x + 3)]); color2 = color2 / 255.0f; } else if (img2->data_type == ASTCENC_TYPE_F16) { uint16_t* data16 = static_cast(img2->data[z]); vint4 color2i = vint4( data16[(4 * xsize2 * y) + (4 * x )], data16[(4 * xsize2 * y) + (4 * x + 1)], data16[(4 * xsize2 * y) + (4 * x + 2)], data16[(4 * xsize2 * y) + (4 * x + 3)]); color2 = float16_to_float(color2i); color2 = clamp(0, 65504.0f, color2); } else // if (img2->data_type == ASTCENC_TYPE_F32) { assert(img2->data_type == ASTCENC_TYPE_F32); float* data32 = static_cast(img2->data[z]); color2 = vfloat4( data32[(4 * xsize2 * y) + (4 * x )], data32[(4 * xsize2 * y) + (4 * x + 1)], data32[(4 * xsize2 * y) + (4 * x + 2)], data32[(4 * xsize2 * y) + (4 * x + 3)]); color2 = clamp(0, 65504.0f, color2); } rgb_peak = astc::max(static_cast(color1.lane<0>()), static_cast(color1.lane<1>()), static_cast(color1.lane<2>()), rgb_peak); vfloat4 diffcolor = color1 - color2; vfloat4 diffcolor_sq = diffcolor * diffcolor; errorsum += diffcolor_sq; vfloat4 alpha_scaled_diffcolor = vfloat4( diffcolor.lane<0>() * color1.lane<3>(), diffcolor.lane<1>() * color1.lane<3>(), diffcolor.lane<2>() * color1.lane<3>(), diffcolor.lane<3>()); vfloat4 alpha_scaled_diffcolor_sq = alpha_scaled_diffcolor * alpha_scaled_diffcolor; alpha_scaled_errorsum += alpha_scaled_diffcolor_sq; if (compute_hdr_metrics) { vfloat4 log_input_color1 = log2(color1); vfloat4 log_input_color2 = log2(color2); vfloat4 log_diffcolor = log_input_color1 - log_input_color2; log_errorsum += log_diffcolor * log_diffcolor; vfloat4 mpsnr_error = vfloat4( mpsnr_sumdiff(color1.lane<0>(), color2.lane<0>(), fstop_lo, fstop_hi), mpsnr_sumdiff(color1.lane<1>(), color2.lane<1>(), fstop_lo, fstop_hi), mpsnr_sumdiff(color1.lane<2>(), color2.lane<2>(), fstop_lo, fstop_hi), mpsnr_sumdiff(color1.lane<3>(), color2.lane<3>(), fstop_lo, fstop_hi)); mpsnr_errorsum += mpsnr_error; } if (compute_normal_metrics) { // Decode the normal vector vfloat4 normal1 = (color1 - 0.5f) * 2.0f; normal1 = normalize_safe(normal1.swz<0, 1, 2>(), unit3()); vfloat4 normal2 = (color2 - 0.5f) * 2.0f; normal2 = normalize_safe(normal2.swz<0, 1, 2>(), unit3()); // Float error can push this outside of valid range for acos, so clamp to avoid NaN issues float normal_cos = clamp(-1.0f, 1.0f, dot3(normal1, normal2)).lane<0>(); float rad_to_degrees = 180.0f / astc::PI; double error_degrees = std::acos(static_cast(normal_cos)) * static_cast(rad_to_degrees); mean_angular_errorsum += error_degrees / (dim_x * dim_y * dim_z); worst_angular_errorsum = astc::max(worst_angular_errorsum, error_degrees); } } } } double pixels = static_cast(dim_x * dim_y * dim_z); double samples = 0.0; double num = 0.0; double alpha_num = 0.0; double log_num = 0.0; double mpsnr_num = 0.0; if (componentmask & 1) { num += errorsum.sum_r; alpha_num += alpha_scaled_errorsum.sum_r; log_num += log_errorsum.sum_r; mpsnr_num += mpsnr_errorsum.sum_r; samples += pixels; } if (componentmask & 2) { num += errorsum.sum_g; alpha_num += alpha_scaled_errorsum.sum_g; log_num += log_errorsum.sum_g; mpsnr_num += mpsnr_errorsum.sum_g; samples += pixels; } if (componentmask & 4) { num += errorsum.sum_b; alpha_num += alpha_scaled_errorsum.sum_b; log_num += log_errorsum.sum_b; mpsnr_num += mpsnr_errorsum.sum_b; samples += pixels; } if (componentmask & 8) { num += errorsum.sum_a; alpha_num += alpha_scaled_errorsum.sum_a; samples += pixels; } double denom = samples; double stopcount = static_cast(fstop_hi - fstop_lo + 1); double mpsnr_denom = pixels * 3.0 * stopcount * 255.0 * 255.0; double psnr; if (num == 0.0) { psnr = 999.0; } else { psnr = 10.0 * log10(denom / num); } double rgb_psnr = psnr; printf("Quality metrics\n"); printf("===============\n\n"); if (componentmask & 8) { printf(" PSNR (LDR-RGBA): %9.4f dB\n", psnr); double alpha_psnr; if (alpha_num == 0.0) { alpha_psnr = 999.0; } else { alpha_psnr = 10.0 * log10(denom / alpha_num); } printf(" Alpha-weighted PSNR: %9.4f dB\n", alpha_psnr); double rgb_num = errorsum.sum_r + errorsum.sum_g + errorsum.sum_b; if (rgb_num == 0.0) { rgb_psnr = 999.0; } else { rgb_psnr = 10.0 * log10(pixels * 3.0 / rgb_num); } printf(" PSNR (LDR-RGB): %9.4f dB\n", rgb_psnr); } else { printf(" PSNR (LDR-RGB): %9.4f dB\n", psnr); } if (compute_hdr_metrics) { printf(" PSNR (RGB norm to peak): %9.4f dB (peak %f)\n", rgb_psnr + 20.0 * log10(rgb_peak), rgb_peak); double mpsnr; if (mpsnr_num == 0.0) { mpsnr = 999.0; } else { mpsnr = 10.0 * log10(mpsnr_denom / mpsnr_num); } printf(" mPSNR (RGB): %9.4f dB (fstops %+d to %+d)\n", mpsnr, fstop_lo, fstop_hi); double logrmse = sqrt(log_num / pixels); printf(" LogRMSE (RGB): %9.4f\n", logrmse); } if (compute_normal_metrics) { printf(" Mean Angular Error: %9.4f degrees\n", mean_angular_errorsum); printf(" Worst Angular Error: %9.4f degrees\n", worst_angular_errorsum); } printf("\n"); }