src_image_voiLut.js

import {logger} from '../utils/logger.js';

// doc imports
/* eslint-disable no-unused-vars */
import {WindowLevel} from './windowLevel.js';
/* eslint-enable no-unused-vars */

export const VoiLutFunctionNames = {
  linear: 'LINEAR',
  linear_exact: 'LINEAR_EXACT',
  sigmoid: 'SIGMOID'
};

/**
 * VOI LUT linear function.
 *
 * Can be default linear or linear exact.
 *
 * ```
 * if (x <= c - 0.5 - (w-1)/2) then y = ymin
 * else if (x > c - 0.5 + (w-1)/2) then y = ymax
 * else y = ((x - (c - 0.5)) / (w-1) + 0.5) * (ymax - ymin) + ymin
 * ```
 *
 * ```
 * if (x <= c - w/2), then y = ymin
 * else if (x > c + w/2), then y = ymax
 * else y = ((x - c) / w + 0.5) * (ymax- ymin) + ymin
 * ```
 *
 * Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.2}.
 */
export class VoiLutLinearFunction {
  /**
   * Input value minimum.
   *
   * @type {number}
   */
  #xmin;

  /**
   * Input value maximum.
   *
   * @type {number}
   */
  #xmax;

  /**
   * Output value minimum. Defaults to 0.
   *
   * @type {number}
   */
  #ymin = 0;

  /**
   * Output value maximum. Defaults to 255.
   *
   * @type {number}
   */
  #ymax = 255;

  /**
   * Function slope.
   *
   * @type {number}
   */
  #slope;

  /**
   * Function intercept.
   *
   * @type {number}
   */
  #intercept;

  /**
   * @param {number} center The window level center.
   * @param {number} width The window level width.
   * @param {boolean} [isExact] Is exact flag, defaults to false.
   */
  constructor(center, width, isExact) {
    if (typeof isExact === 'undefined') {
      isExact = false;
    }
    // from the standard
    let c = center;
    let w = width;
    if (!isExact) {
      c -= 0.5;
      w -= 1;
    }
    this.#xmin = c - (w / 2);
    this.#xmax = c + (w / 2);
    // pre-calculate slope and intercept
    this.#slope = (this.#ymax - this.#ymin) / w;
    this.#intercept = (-c / w + 0.5) *
      (this.#ymax - this.#ymin) + this.#ymin;
  }

  /**
   * Get the value of the function at a given number.
   *
   * @param {number} x The input value.
   * @returns {number} The value of the function at x.
   */
  getY(x) {
    let res;
    if (x <= this.#xmin) {
      res = this.#ymin;
    } else if (x > this.#xmax) {
      res = this.#ymax;
    } else {
      res = (x * this.#slope) + this.#intercept;
    }
    return res;
  }
}

/**
 * VOI LUT sigmoid function.
 *
 * ```
 * y = (ymax − ymin) / (1 + exp(−4 * (x − c) / w)) + ymin
 * ```
 *
 * Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.11.2.html#sect_C.11.2.1.2}.
 */
export class VoiLutSigmoidFunction {
  /**
   * Output value minimum. Defaults to 0.
   *
   * @type {number}
   */
  #ymin = 0;

  /**
   * Output value maximum. Defaults to 255.
   *
   * @type {number}
   */
  #ymax = 255;

  /**
   * Window level center.
   *
   * @type {number}
   */
  #center;

  /**
   * Window level width.
   *
   * @type {number}
   */
  #width;

  /**
   * @param {number} center The window level center.
   * @param {number} width The window level width.
   */
  constructor(center, width) {
    this.#center = center;
    this.#width = width;
  }

  /**
   * Get the value of the function at a given number.
   *
   * @param {number} x The input value.
   * @returns {number} The value of the function at x.
   */
  getY(x) {
    return ((this.#ymax - this.#ymin) /
      (1 + Math.exp(-4 * (x - this.#center) / this.#width))) +
      this.#ymin;
  }
}

/**
 * VOI (Values of Interest) LUT class: apply window centre and width
 * using a VOI LUT function.
 */
export class VoiLut {

  /**
   * The window and level.
   *
   * @type {WindowLevel}
   */
  #windowLevel;

  /**
   * Signed data offset. Defaults to 0.
   *
   * @type {number}
   */
  #signedOffset = 0;

  /**
   * VOI LUT function.
   *
   * @type {VoiLutLinearFunction|VoiLutSigmoidFunction}
   */
  #voiLutFunction;

  /**
   * VOI LUT function name.
   *
   * @type {string}
   */
  #voiLutFunctionName = VoiLutFunctionNames.linear;

  /**
   * @param {WindowLevel} wl The window center and width.
   * @param {string} [voiLutFunctionName] The name of the VOI LUT function,
   *   defaults to 'LINEAR'.
   */
  constructor(wl, voiLutFunctionName) {
    this.#windowLevel = wl;

    if (typeof voiLutFunctionName !== 'undefined') {
      // valid name check
      const names = [];
      for (const key in VoiLutFunctionNames) {
        names.push(VoiLutFunctionNames[key]);
      }
      if (names.includes(voiLutFunctionName)) {
        this.#voiLutFunctionName = voiLutFunctionName;
      } else {
        logger.debug('Unknown VOI LUT function: ' + voiLutFunctionName);
      }
    }

    this.#init();
  }

  /**
   * Get the window and level.
   *
   * @returns {WindowLevel} The window center and width.
   */
  getWindowLevel() {
    return this.#windowLevel;
  }

  /**
   * Initialise members. Called at construction.
   */
  #init() {
    const center = this.#windowLevel.center;
    const width = this.#windowLevel.width;
    const c = center + this.#signedOffset;

    if (this.#voiLutFunctionName === VoiLutFunctionNames.linear_exact) {
      this.#voiLutFunction = new VoiLutLinearFunction(c, width, true);
    } else if (this.#voiLutFunctionName === VoiLutFunctionNames.sigmoid) {
      this.#voiLutFunction = new VoiLutSigmoidFunction(c, width);
    } else {
      // default case
      this.#voiLutFunction = new VoiLutLinearFunction(c, width);
    }
  }

  /**
   * Set the signed offset.
   *
   * @param {number} offset The signed data offset,
   *   typically: slope * ( size / 2).
   */
  setSignedOffset(offset) {
    this.#signedOffset = offset;
    // re-initialise
    this.#init();
  }

  /**
   * Apply the window level on an input value.
   *
   * @param {number} value The value to rescale as an integer.
   * @returns {number} The leveled value, in the [0,255] range.
   */
  apply(value) {
    return this.#voiLutFunction.getY(value);
  }

} // class VoiLut