regions/thresholding.js

import * as cornerstone from 'cornerstone-core';
import { addToolState, getToolState } from '../stateManagement/toolState';

// UNUSED const toolType = 'thresholding';

let LASTELEMENT = null;

export function getLastElement () {
  return LASTELEMENT;
}

const LABEL_SIZE_BYTES = 1;

let configuration = {
  historySize: 4,
  historyPosition: 0,
  toolRegionValue: 2,
  calciumThresholdHu: 130,
  layersAbove: 0,
  layersBelow: 0,
  drawAlpha: 1,
  regionColorsRGB: [
    [255, 0, 255],
    [246, 193, 91],
    [237, 148, 69],
    [230, 103, 49],
    [184, 74, 41],
    [106, 58, 45]
  ],
  kvpToMultiplier: {
    140: 1.04,
    130: 1.02,
    120: 1,
    110: 0.98,
    100: 0.96,
    90: 0.93,
    80: 0.89,
    // add dummy for 70
    70: 0.86,
  },
  growIterationsPerChunk: 2
};

/**
 * Perform the thresholding on a stack
 */
function performThresholding (stack, afterwards) {
  let width, height;
  const imageIds = stack.imageIds;
  const slices = imageIds.length;

  // Get slope and intercept
  return cornerstone.loadImage(imageIds[0]).then(function (image) {
    width = image.width;
    height = image.height;

    const length = width * height * slices * LABEL_SIZE_BYTES;
    const buffer = new ArrayBuffer(length);
    const view = new Uint8Array(buffer);

    // Thresholding promises
    const promises = imageIds.map(function (imageId, imageIdx) {

      return cornerstone.loadImage(imageId).then(function (image) {
        const slope = image.slope;
        const intercept = image.intercept;
        const pixelData = image.getPixelData();
        const n = width * height;

        for (let i = 0; i < n; i++) {
          const pixel = pixelData[i];
          const hu = (pixel * slope) + intercept;
          const label = (hu >= configuration.calciumThresholdHu) ? 1 : 0;
          const viewIdx = (imageIdx) * n + i;

          view[viewIdx] = label;
        }
      });
    });

    // Callback with buffer
    return Promise.all(promises).then(function () {
      const result = {
        buffer,
        width,
        height
      };

      if (afterwards) {
        afterwards(result);
      }

      return result;
    });

  });
}

let imgdata = null;

/**
 * Draw regions on image
 */
function onImageRendered (e, eventData) {
  const element = eventData.element;
  const stackData = getToolState(element, 'stack');
  const thresholdingData = getToolState(element, 'regions');

  if (!thresholdingData || !thresholdingData.data || !thresholdingData.data.length) {
    return;
  }

  const slice = stackData.data[0].currentImageIdIndex;
  const buffer = thresholdingData.data[0].buffer;
  const context = eventData.canvasContext;
  const enabledElement = eventData.enabledElement;
  const image = eventData.image;
  const width = image.width;
  const height = image.height;

  const doubleBuffer = document.createElement('canvas');
  const doubleBufferContext = doubleBuffer.getContext('2d');

  doubleBuffer.width = width;
  doubleBuffer.height = height;
  imgdata = imgdata || doubleBufferContext.createImageData(width, height);

  const pixels = imgdata.data;
  const sliceSize = width * height;
  const sliceOffset = slice * sliceSize;
  const view = new Uint8Array(buffer, sliceOffset, sliceSize);

  for (let i = 0; i < view.length; i += 1) {
    const label = view[i];
    const pi = i * 4;

    if (label) {
      const color = configuration.regionColorsRGB[label - 1];

      pixels[pi + 0] = color[0];
      pixels[pi + 1] = color[1];
      pixels[pi + 2] = color[2];
      pixels[pi + 3] = configuration.drawAlpha * 255;
    } else {
      pixels[pi + 3] = 0;
    }
  }
  doubleBufferContext.putImageData(imgdata, 0, 0);

  cornerstone.setToPixelCoordinateSystem(enabledElement, context);
  context.drawImage(doubleBuffer, 0, 0);
}

function enable (element) {
  LASTELEMENT = element;
  // First check that there is stack data available
  const stackData = getToolState(element, 'stack');

  if (!stackData || !stackData.data || !stackData.data.length) {
    return;
  }

  const initialThresholdingData = {
    enabled: 1,
    buffer: null,
    width: null,
    height: null,
    history: []
  };

  addToolState(element, 'regions', initialThresholdingData);

  const stack = stackData.data[0];

  performThresholding(stack, function (regions) {
    // Add threshold data to tool state
    const thresholdingData = getToolState(element, 'regions');

    thresholdingData.data[0].buffer = regions.buffer;
    thresholdingData.data[0].width = regions.width;
    thresholdingData.data[0].height = regions.height;
    // Draw regions on image
    $(element).on('CornerstoneImageRendered', onImageRendered);

    // Update the element to apply the viewport and tool changes
    cornerstone.updateImage(element);
  });
}

function disable (element) {
  const thresholdingData = getToolState(element, 'regions');

  // If there is actually something to disable, disable it
  if (thresholdingData && thresholdingData.data.length) {
    thresholdingData.data[0].enabled = false;
  }
}

export function createUndoStep (element) {
  const thresholdingData = getToolState(element, 'regions');

  const state = thresholdingData.data[0];
  // Make a copy using .slice()
  const current = state.buffer.slice();

  // Put at end of history
  state.history.push(current);
  // Remove oldest if too much history
  if (state.history.length > configuration.historySize) {
    state.history.shift();
  }

}

export function getConfiguration () {
  return configuration;
}

export function setConfiguration (config) {
  configuration = config;
}

// Module/private exports
export default {
  activate: enable,
  deactivate: disable,
  enable,
  disable,
  getConfiguration,
  setConfiguration
};