Fork me on GitHub

Source: img_metrics.js

/* Image Metrics
 * ===========================================================
 * derived from image-mse.js (https://github.com/darosh/image-mse-js)
 * MIT License
 * Copyright (c) 2023 Joachim de Fourestier (Joe DF)
 */

/* exported NRMSE */

/**
 * The NRMSE (normalized root mean square error) - reference-based image metric.
 * @namespace NRMSE
 */
const NRMSE = {
	/** The maximum value of a pixel component. 255 for 8 bit depth */
	pixelMaxValue: 255,
	/** The allowed size difference when comparing images.
	 * The leftover extra pixels of the larger image will be ignored. */
	defaultTolerance: 0.01,
	/**
	 * Compares two images.
	 * @param {*} image1 Preferably the ground truth / reference (convention, shouldnt matter otherwise)
	 * @param {*} image2 An image to compare
	 * @param {*} tolerance The allowed size ratio difference, default is {@link NRMSE.defaultTolerance}.
	 * @returns An object with the calculate metric values of MSE, PSNR, RMSE, NRMSE,
	 * Inverted NRMSE, NMSE, and Inverted NMSE.
	 */
	compare: function(image1, image2, tolerance) {
		'use strict';

		// size check and tolerance
		if (image1.data.length != image2.data.length) {
			let dl1 = image1.data.length, dl2 = image2.data.length;
			var errmsg = "The given images have different data length ("
				+image1.width+"x"+image1.height+" vs "+image2.width+"x"+image2.height
				+") or sizes ("+dl1+" vs "+dl2+").";
			
			// get the tolerance or use default if not provided
			tolerance = (typeof tolerance !== 'undefined') ? tolerance : this.defaultTolerance;
			let relDiff = Math.abs(dl1 - dl2) / Math.max(dl1, dl2);
			if (relDiff <= tolerance) {
				// eslint-disable-next-line no-magic-numbers
				console.warn(errmsg + " Tolerance = "+tolerance+" relDiff = "+relDiff.toFixed(6));
			} else {
				throw errmsg;
			}
		}

		const n_channels = 4; // assume grayscale RGBA flat array
		// max pixel value for the bit depth. At 8 bit-depth, this is 255.
		const p_max = this.pixelMaxValue;
		
		// Do sum of squared difference
		var sum = 0, len = image1.data.length;
		for (var i = 0; i < len; i += n_channels) {
			let diff = image1.data[i] - image2.data[i];

			// check and allow for tolerance of diff image sizes
			if (isNaN(diff)) {
				if (i >= image2.data.length) {
					console.warn("Calculation loop ended early as i = "+i);
					break;
				} else {
					throw "Error: untolerated or invalid calculation!";
				}
			}

			sum += Math.pow(diff, 2);
		}

		// number of pixels, each having multiple "channels",
		// or color components (e.g., R, G, B, A)...
		var pc = len / n_channels;

		// final step ("average error over all entries/pixels"),
		// to obtain MSE (Mean Squared Error)
		var mse = sum / pc;

		// Maximum possible mse value
		// var max_mse = (Math.pow(p_max - 0, 2) * pc) / pc;
		// which can be reduced to:
		const max_mse = Math.pow(p_max, 2);

		// Normalized MSE (NMSE)
		var nmse = mse / max_mse;

		// Compute RMSE (Root MSE)
		var rmse = Math.sqrt(mse);
		
		// Normalized RMSE (NRMSE) or RMS-percentage-E (RMSPE)
		// many ways to normalize:
		// https://cirpwiki.info/wiki/Statistics#Normalization
		// https://stats.stackexchange.com/a/413257/37910
		// We just normalize by the max value possible for our RMSE
		// which is equal to Math.sqrt(max_mse);
		// which can be reduce to just: p_max
		var nrmse = rmse / p_max;

		return {
			mse: mse,
			psnr: this.psnr(mse),
			rmse: rmse,
			nrmse: nrmse,
			// "inverted" ("iNRMSE") since we want 1 = good, 0 = bad match
			inrmse: 1 - nrmse,
			nmse: nmse,
			// inverted NMSE ("iNMSE")
			inmse: 1 - nmse,
		};
	},
	/**
	 * Calculates PSNR (Peak Signal-to-Noise Ratio).
	 * @param {*} mse a Mean Squared Error (MSE) value
	 * @param {*} max the maximum value of a pixel component, default is {@link NRMSE.pixelMaxValue}
	 * @returns the calculated value
	 */
	psnr: function(mse, max) {
		if (max === void 0) { max = this.pixelMaxValue; }
		// eslint-disable-next-line no-magic-numbers
		return 10 * this.log10((max * max) / mse);
	},
	/** Utility function that performs a Log base 10 calculation. */
	log10: function(value) {
		return Math.log(value) / Math.LN10;
	}
};

↑ Top