obartra/ssim

View on GitHub
src/ssim.ts

Summary

Maintainability
B
4 hrs
Test Coverage
A
100%
/**
 * Implements all ssim-specific logic.
 *
 * Reproduces the original SSIM matlab scripts. For a direct comparison you may want to check the
 * scripts contained within `/assets`
 *
 * @namespace ssim
 */
import {
  add2d,
  divide2d,
  multiply2d,
  square2d,
  subtract2d,
  sum2d,
} from './math'
import { conv2, normpdf, transpose } from './matlab'
import { Options, Matrix } from './types'

/**
 * Generates a SSIM map based on two input image matrices. For images greater than 512 pixels, it
 * will downsample by default (unless `options.downsample` is set to falsy).
 *
 * Images must be a 2-Dimensional grayscale image
 *
 * This method produces the same exact output than `assets/ssim.m` when running on Matlab. It's
 * mathematically equivalent but it is not a line-by-line port. If you want a line-by-line port
 * check `originalSsim`. Several performance optimizations have been made here to achieve greater
 * performance.
 *
 * @method ssim
 * @param {Matrix} pixels1 - The reference matrix
 * @param {Matrix} pixels2 - The second matrix to compare against
 * @param {Options} options - The input options parameter
 * @returns {Matrix} ssim_map - A matrix containing the map of computed SSIMs
 * @public
 * @memberOf ssim
 */
export function ssim(
  pixels1: Matrix,
  pixels2: Matrix,
  options: Options
): Matrix {
  let w = normpdf(getRange(options.windowSize), 0, 1.5)
  const L = 2 ** options.bitDepth - 1
  const c1 = (options.k1 * L) ** 2
  const c2 = (options.k2 * L) ** 2

  w = divide2d(w, sum2d(w))
  const wt = transpose(w)
  const μ1 = conv2(pixels1, w, wt, 'valid')
  const μ2 = conv2(pixels2, w, wt, 'valid')
  const μ1Sq = square2d(μ1)
  const μ2Sq = square2d(μ2)
  const μ12 = multiply2d(μ1, μ2)
  const pixels1Sq = square2d(pixels1)
  const pixels2Sq = square2d(pixels2)
  const σ1Sq = subtract2d(conv2(pixels1Sq, w, wt, 'valid'), μ1Sq)
  const σ2Sq = subtract2d(conv2(pixels2Sq, w, wt, 'valid'), μ2Sq)
  const σ12 = subtract2d(
    conv2(multiply2d(pixels1, pixels2), w, wt, 'valid'),
    μ12
  )

  if (c1 > 0 && c2 > 0) {
    return genSSIM(μ12, σ12, μ1Sq, μ2Sq, σ1Sq, σ2Sq, c1, c2)
  }
  return genUQI(μ12, σ12, μ1Sq, μ2Sq, σ1Sq, σ2Sq)
}

/**
 * Generates a range of distances of size `2n+1` with increments of 1 and centered at 0.
 *
 * @example `getRange(2) => [2 1 0 1 2]
 * @method getRange
 * @param {Number} size - The maximum distance from the center
 * @returns {Matrix} out - The generated vector
 * @private
 * @memberOf ssim
 */
function getRange(size: number): Matrix {
  const offset = Math.floor(size / 2)
  const data = new Array(offset * 2 + 1)

  for (let x = -offset; x <= offset; x++) {
    data[x + offset] = Math.abs(x)
  }

  return {
    data,
    width: data.length,
    height: 1,
  }
}

/**
 * Generates the ssim_map based on the intermediate values of the convolutions of the input with the
 * gaussian filter.
 *
 * These methods apply when K1 or K2 are not 0 (non UQI)
 *
 * @method genSSIM
 * @param {Matrix} μ12 - The cell-by cell multiplication of both images convolved
 * with the gaussian filter
 * @param {Matrix} σ12 - The convolution of cell-by cell multiplication of both
 * images minus μ12
 * @param {Matrix} μ1Sq - The convolution of image1 with the gaussian filter squared
 * @param {Matrix} μ2Sq - The convolution of image2 with the gaussian filter squared
 * @param {Matrix} σ1Sq - The convolution of image1^2, minus μ1Sq
 * @param {Matrix} σ2Sq - The convolution of image2^2, minus μ2Sq
 * @param {Number} c1 - The first stability constant
 * @param {Number} c2 - The second stability constant
 * @returns {Matrix} ssim_map - The generated map of SSIM values at each window
 * @private
 * @memberOf ssim
 */
function genSSIM(
  μ12: Matrix,
  σ12: Matrix,
  μ1Sq: Matrix,
  μ2Sq: Matrix,
  σ1Sq: Matrix,
  σ2Sq: Matrix,
  c1: number,
  c2: number
): Matrix {
  const num1 = add2d(multiply2d(μ12, 2), c1)
  const num2 = add2d(multiply2d(σ12, 2), c2)
  const denom1 = add2d(add2d(μ1Sq, μ2Sq), c1)
  const denom2 = add2d(add2d(σ1Sq, σ2Sq), c2)

  return divide2d(multiply2d(num1, num2), multiply2d(denom1, denom2))
}

/**
 * Generates the Universal Quality Index (UQI) ssim_map based on the intermediate values of the
 * convolutions of the input with the gaussian filter.
 *
 * These methods apply when K1 or K2 are 0 (UQI)
 *
 * @method genUQI
 * @param {Matrix} μ12 - The cell-by cell multiplication of both images convolved
 * with the gaussian filter
 * @param {Matrix} σ12 - The convolution of cell-by cell multiplication of both
 * images minus μ12
 * @param {Matrix} μ1Sq - The convolution of image1 with the gaussian filter squared
 * @param {Matrix} μ2Sq - The convolution of image2 with the gaussian filter squared
 * @param {Matrix} σ1Sq - The convolution of image1^2, minus μ1Sq
 * @param {Matrix} σ2Sq - The convolution of image2^2, minus μ2Sq
 * @returns {Matrix} ssim_map - The generated map of SSIM values at each window
 * @private
 * @memberOf ssim
 */
function genUQI(
  μ12: Matrix,
  σ12: Matrix,
  μ1Sq: Matrix,
  μ2Sq: Matrix,
  σ1Sq: Matrix,
  σ2Sq: Matrix
): Matrix {
  const numerator1 = multiply2d(μ12, 2)
  const numerator2 = multiply2d(σ12, 2)
  const denominator1 = add2d(μ1Sq, μ2Sq)
  const denominator2 = add2d(σ1Sq, σ2Sq)

  return divide2d(
    multiply2d(numerator1, numerator2),
    multiply2d(denominator1, denominator2)
  )
}