src_image_imageFactory.js

import {Size} from './size';
import {Geometry} from './geometry';
import {RescaleSlopeAndIntercept} from './rsi';
import {WindowCenterAndWidth} from './windowCenterAndWidth';
import {Image} from './image';
import {
  isJpeg2000TransferSyntax,
  isJpegBaselineTransferSyntax,
  isJpegLosslessTransferSyntax
} from '../dicom/dicomParser';
import {
  getImage2DSize,
  getPixelSpacing,
  getPixelUnit,
  TagValueExtractor
} from '../dicom/dicomElementsWrapper';
import {Vector3D} from '../math/vector';
import {Matrix33} from '../math/matrix';
import {Point3D} from '../math/point';
import {logger} from '../utils/logger';

// doc imports
/* eslint-disable no-unused-vars */
import {DataElement} from '../dicom/dataElement';
/* eslint-enable no-unused-vars */

/**
 * @typedef {Object<string, DataElement>} DataElements
 */

/**
 * {@link Image} factory.
 */
export class ImageFactory {

  /**
   * Check dicom elements. Throws an error if not suitable.
   *
   * @param {DataElements} dataElements The DICOM data elements.
   * @returns {object|undefined} A possible warning.
   */
  checkElements(dataElements) {
    // will throw if columns or rows is not defined
    getImage2DSize(dataElements);
  }

  /**
   * Get an {@link Image} object from the read DICOM file.
   *
   * @param {DataElements} dataElements The DICOM tags.
   * @param {Uint8Array | Int8Array |
   *   Uint16Array | Int16Array |
   *   Uint32Array | Int32Array} pixelBuffer The pixel buffer.
   * @param {number} numberOfFiles The input number of files.
   * @returns {Image} A new Image.
   */
  create(dataElements, pixelBuffer, numberOfFiles) {
    const size2D = getImage2DSize(dataElements);
    const sizeValues = [size2D[0], size2D[1], 1];

    // frames
    const frames = dataElements['00280008'];
    if (frames) {
      sizeValues.push(frames.value[0]);
    }

    // image size
    const size = new Size(sizeValues);

    // image spacing
    const spacing = getPixelSpacing(dataElements);

    // TransferSyntaxUID
    const syntax = dataElements['00020010'].value[0];
    const jpeg2000 = isJpeg2000TransferSyntax(syntax);
    const jpegBase = isJpegBaselineTransferSyntax(syntax);
    const jpegLoss = isJpegLosslessTransferSyntax(syntax);

    // ImagePositionPatient
    const imagePositionPatient = dataElements['00200032'];
    // slice position
    let slicePosition = new Array(0, 0, 0);
    if (typeof imagePositionPatient !== 'undefined') {
      slicePosition = [
        parseFloat(imagePositionPatient.value[0]),
        parseFloat(imagePositionPatient.value[1]),
        parseFloat(imagePositionPatient.value[2])
      ];
    }

    // slice orientation (cosines are matrices' columns)
    // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1
    const imageOrientationPatient = dataElements['00200037'];
    let orientationMatrix;
    if (typeof imageOrientationPatient !== 'undefined') {
      const rowCosines = new Vector3D(
        parseFloat(imageOrientationPatient.value[0]),
        parseFloat(imageOrientationPatient.value[1]),
        parseFloat(imageOrientationPatient.value[2]));
      const colCosines = new Vector3D(
        parseFloat(imageOrientationPatient.value[3]),
        parseFloat(imageOrientationPatient.value[4]),
        parseFloat(imageOrientationPatient.value[5]));
      const normal = rowCosines.crossProduct(colCosines);
      /* eslint-disable array-element-newline */
      orientationMatrix = new Matrix33([
        rowCosines.getX(), colCosines.getX(), normal.getX(),
        rowCosines.getY(), colCosines.getY(), normal.getY(),
        rowCosines.getZ(), colCosines.getZ(), normal.getZ()
      ]);
      /* eslint-enable array-element-newline */
    }

    // geometry
    const origin = new Point3D(
      slicePosition[0], slicePosition[1], slicePosition[2]);
    const extractor = new TagValueExtractor();
    const time = extractor.getTime(dataElements);
    const geometry = new Geometry(
      origin, size, spacing, orientationMatrix, time);

    // SOP Instance UID
    let sopInstanceUid;
    const siu = dataElements['00080018'];
    if (typeof siu !== 'undefined') {
      sopInstanceUid = siu.value[0];
    }

    // Sample per pixels
    let samplesPerPixel = 1;
    const spp = dataElements['00280002'];
    if (typeof spp !== 'undefined') {
      samplesPerPixel = spp.value[0];
    }

    // check buffer size
    const bufferSize = size.getTotalSize() * samplesPerPixel;
    if (bufferSize !== pixelBuffer.length) {
      logger.warn('Badly sized pixel buffer: ' +
        pixelBuffer.length + ' != ' + bufferSize);
      if (bufferSize < pixelBuffer.length) {
        pixelBuffer = pixelBuffer.slice(0, size.getTotalSize());
      } else {
        throw new Error('Underestimated buffer size, can\'t fix it...');
      }
    }

    // image
    const image = new Image(geometry, pixelBuffer, [sopInstanceUid]);
    // PhotometricInterpretation
    const photometricInterpretation = dataElements['00280004'];
    if (typeof photometricInterpretation !== 'undefined') {
      let photo = photometricInterpretation.value[0].toUpperCase();
      // jpeg decoders output RGB data
      if ((jpeg2000 || jpegBase || jpegLoss) &&
        (photo !== 'MONOCHROME1' && photo !== 'MONOCHROME2')) {
        photo = 'RGB';
      }
      // check samples per pixels
      if (photo === 'RGB' && samplesPerPixel === 1) {
        photo = 'PALETTE COLOR';
      }
      image.setPhotometricInterpretation(photo);
    }
    // PlanarConfiguration
    const planarConfiguration = dataElements['00280006'];
    if (typeof planarConfiguration !== 'undefined') {
      image.setPlanarConfiguration(planarConfiguration.value[0]);
    }

    // rescale slope and intercept
    let slope = 1;
    // RescaleSlope
    const rescaleSlope = dataElements['00281053'];
    if (typeof rescaleSlope !== 'undefined') {
      const value = parseFloat(rescaleSlope.value[0]);
      if (!isNaN(value)) {
        slope = value;
      }
    }
    let intercept = 0;
    // RescaleIntercept
    const rescaleIntercept = dataElements['00281052'];
    if (typeof rescaleIntercept !== 'undefined') {
      const value = parseFloat(rescaleIntercept.value[0]);
      if (!isNaN(value)) {
        intercept = value;
      }
    }
    const rsi = new RescaleSlopeAndIntercept(slope, intercept);
    image.setRescaleSlopeAndIntercept(rsi);

    // meta information
    const meta = {
      numberOfFiles: numberOfFiles
    };
    const modality = dataElements['00080060'];
    if (typeof modality !== 'undefined') {
      meta.Modality = modality.value[0];
    }
    const sopClassUID = dataElements['00080016'];
    if (typeof sopClassUID !== 'undefined') {
      meta.SOPClassUID = sopClassUID.value[0];
    }
    const studyUID = dataElements['0020000D'];
    if (typeof studyUID !== 'undefined') {
      meta.StudyInstanceUID = studyUID.value[0];
    }
    const seriesUID = dataElements['0020000E'];
    if (typeof seriesUID !== 'undefined') {
      meta.SeriesInstanceUID = seriesUID.value[0];
    }
    const bits = dataElements['00280101'];
    if (typeof bits !== 'undefined') {
      meta.BitsStored = bits.value[0];
    }
    const pixelRep = dataElements['00280103'];
    if (typeof pixelRep !== 'undefined') {
      meta.PixelRepresentation = pixelRep.value[0];
    }
    // PixelRepresentation -> is signed
    meta.IsSigned = meta.PixelRepresentation === 1;
    // local pixel unit
    const pixelUnit = getPixelUnit(dataElements);
    if (typeof pixelUnit !== 'undefined') {
      meta.pixelUnit = pixelUnit;
    }
    // FrameOfReferenceUID (optional)
    const frameOfReferenceUID = dataElements['00200052'];
    if (typeof frameOfReferenceUID !== 'undefined') {
      meta.FrameOfReferenceUID = frameOfReferenceUID.value[0];
    }
    // window level presets
    const windowPresets = {};
    const windowCenter = dataElements['00281050'];
    const windowWidth = dataElements['00281051'];
    const windowCWExplanation = dataElements['00281055'];
    if (typeof windowCenter !== 'undefined' &&
      typeof windowWidth !== 'undefined') {
      let name;
      for (let j = 0; j < windowCenter.value.length; ++j) {
        const center = parseFloat(windowCenter.value[j]);
        const width = parseFloat(windowWidth.value[j]);
        if (center && width && width !== 0) {
          name = '';
          if (typeof windowCWExplanation !== 'undefined') {
            name = windowCWExplanation.value[j];
          }
          if (name === '') {
            name = 'Default' + j;
          }
          windowPresets[name] = {
            wl: [new WindowCenterAndWidth(center, width)],
            name: name
          };
        }
        if (width === 0) {
          logger.warn('Zero window width found in DICOM.');
        }
      }
    }
    meta.windowPresets = windowPresets;

    // PALETTE COLOR luts
    if (image.getPhotometricInterpretation() === 'PALETTE COLOR') {
      const redLutElement = dataElements['00281201'];
      const greenLutElement = dataElements['00281202'];
      const blueLutElement = dataElements['00281203'];
      let redLut;
      let greenLut;
      let blueLut;
      // check red palette descriptor (should all be equal)
      const descriptor = dataElements['00281101'];
      if (typeof descriptor !== 'undefined' &&
        descriptor.value.length === 3) {
        if (descriptor.value[2] === 16) {
          let doScale = false;
          // (C.7.6.3.1.5 Palette Color Lookup Table Descriptor)
          // Some implementations have encoded 8 bit entries with 16 bits
          // allocated, padding the high bits;
          let descSize = descriptor.value[0];
          // (C.7.6.3.1.5 Palette Color Lookup Table Descriptor)
          // The first Palette Color Lookup Table Descriptor value is the
          // number of entries in the lookup table. When the number of table
          // entries is equal to 216 then this value shall be 0.
          if (descSize === 0) {
            descSize = 65536;
          }
          // red palette VL
          const vlSize = redLutElement.vl;
          // check double size
          if (vlSize !== 2 * descSize) {
            doScale = true;
            logger.info('16bits lut but size is not double. desc: ' +
              descSize + ' vl: ' + vlSize);
          }
          // (C.7.6.3.1.6 Palette Color Lookup Table Data)
          // Palette color values must always be scaled across the full
          // range of available intensities
          const bitsAllocated = parseInt(
            dataElements['00280100'].value[0], 10);
          if (bitsAllocated === 8) {
            doScale = true;
            logger.info(
              'Scaling 16bits color lut since bits allocated is 8.');
          }

          if (doScale) {
            const scaleTo8 = function (value) {
              return value >> 8;
            };

            redLut = redLutElement.value.map(scaleTo8);
            greenLut = greenLutElement.value.map(scaleTo8);
            blueLut = blueLutElement.value.map(scaleTo8);
          }
        } else if (descriptor.value[2] === 8) {
          // lut with vr=OW was read as Uint16, convert it to Uint8
          logger.info(
            'Scaling 16bits color lut since the lut descriptor is 8.');
          let clone = redLutElement.value.slice(0);
          // @ts-expect-error
          redLut = new Uint8Array(clone.buffer);
          clone = greenLutElement.value.slice(0);
          // @ts-expect-error
          greenLut = new Uint8Array(clone.buffer);
          clone = blueLutElement.value.slice(0);
          // @ts-expect-error
          blueLut = new Uint8Array(clone.buffer);
        }
      }
      // set the palette
      meta.paletteLut = {
        red: redLut,
        green: greenLut,
        blue: blueLut
      };
    }

    // RecommendedDisplayFrameRate
    const recommendedDisplayFrameRate = dataElements['00082144'];
    if (typeof recommendedDisplayFrameRate !== 'undefined') {
      meta.RecommendedDisplayFrameRate = parseInt(
        recommendedDisplayFrameRate.value[0], 10);
    }

    // store the meta data
    image.setMeta(meta);

    return image;
  }

}