import { ssim as bufferSsim } from 'ssim.js'; import type { Image } from '../../Image.ts'; import checkProcessable from '../../utils/validators/checkProcessable.ts'; import { validateForComparison } from '../../utils/validators/validators.ts'; export interface SsimOptions { /** * Window size for SSIM map. * @default `Math.min(11, image.width, image.height)` */ windowSize?: number; /** * Algorithm to use to compute the SSIM. * @default `'original'` */ algorithm?: 'fast' | 'original' | 'bezkrovny' | 'weber'; } export interface Ssim { /** * Mean SSIM of the whole image. It is the mean value of the SSIM map. * It is a similarity score between two images. */ mssim: number; /** * Similarity map of the two images. The dimensions of the map depend the windowSize option. * Create a GREY image based on this map to visualize the similarity of the different regions of the image. */ ssimMap: { data: number[]; width: number; height: number }; } /** * Compute the Structural Similarity (SSIM) of two RGBA or two GREY images. * "The resultant SSIM index is a decimal value between -1 and 1, * where 1 indicates perfect similarity, 0 indicates no similarity, * and -1 indicates perfect anti-correlation." * @see {@link https://en.wikipedia.org/wiki/Structural_similarity} * @param image - First image. * @param otherImage - Second image. * @param options - SSIM options. * @returns SSIM of the two images. */ export function computeSsim( image: Image, otherImage: Image, options: SsimOptions = {}, ): Ssim { let { windowSize } = options; const { algorithm = 'original' } = options; if (windowSize) { if (windowSize > image.width || windowSize > image.height) { throw new RangeError('windowSize cannot exceed image dimensions'); } } else { windowSize = Math.min(11, image.height, image.width); } checkProcessable(image, { bitDepth: [8], channels: [1, 3, 4], }); validateForComparison(image, otherImage); if (image.colorModel !== 'RGBA') { image = image.convertColor('RGBA'); otherImage = otherImage.convertColor('RGBA'); } const imageData = new Uint8ClampedArray(image.getRawImage().data); const imageBuffer = { height: image.height, width: image.width, data: imageData, }; const otherData = new Uint8ClampedArray(otherImage.getRawImage().data); const otherBuffer = { height: otherImage.height, width: otherImage.width, data: otherData, }; const ssim = bufferSsim(imageBuffer, otherBuffer, { windowSize, ssim: algorithm, }); return { mssim: ssim.mssim, ssimMap: ssim.ssim_map, }; }