src_dicom_dicomSegment.js

import {
  isEqualRgb,
  cielabToSrgb,
  uintLabToLab,
  labToUintLab,
  srgbToCielab
} from '../utils/colour.js';
import {
  getCode,
  getDicomCodeItem
} from './dicomCode.js';
import {logger} from '../utils/logger.js';

// doc imports
/* eslint-disable no-unused-vars */
import {RGB} from '../utils/colour.js';
import {DataElement} from './dataElement.js';
import {DicomCode} from './dicomCode.js';
/* eslint-enable no-unused-vars */

/**
 * Related DICOM tag keys.
 */
const TagKeys = {
  SegmentNumber: '00620004',
  SegmentLabel: '00620005',
  SegmentAlgorithmType: '00620008',
  SegmentAlgorithmName: '00620009',
  RecommendedDisplayGrayscaleValue: '0062000C',
  RecommendedDisplayCIELabValue: '0062000D',
  SegmentedPropertyCategoryCodeSequence: '00620003',
  SegmentedPropertyTypeCodeSequence: '0062000F',
  TrackingID: '00620020',
  TrackingUID: '00620021'
};

/**
 * Get a default RGB colour for a segment.
 *
 * @param {number} segmentNumber The segment number.
 * @returns {RGB} A colour.
 */
function getDefaultColour(segmentNumber) {
  // ITK snap colours
  const colours = [
    new RGB(0, 0, 0),
    new RGB(255, 0, 0),
    new RGB(0, 255, 0),
    new RGB(0, 0, 255),
    new RGB(255, 255, 0),
    new RGB(0, 255, 255),
    new RGB(255, 0, 255),
    new RGB(255, 239, 213),
    new RGB(0, 0, 205),
    new RGB(205, 133, 63),
    new RGB(210, 180, 140),
    new RGB(102, 205, 170),
    new RGB(0, 0, 128),
    new RGB(0, 139, 139),
    new RGB(46, 139, 87),
    new RGB(255, 228, 225)
  ];
  let colour;
  if (segmentNumber < colours.length) {
    colour = colours[segmentNumber];
  } else {
    colour = new RGB(
      Math.random() * 255,
      Math.random() * 255,
      Math.random() * 255
    );
  }
  return colour;
};

/**
 * DICOM (mask) segment: item of a SegmentSequence (0062,0002).
 *
 * Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.8.20.4.html}.
 */
export class MaskSegment {
  /**
   * Segment number (0062,0004).
   *
   * @type {number}
   */
  number;
  /**
   * Segment label (0062,0005).
   *
   * @type {string}
   */
  label;
  /**
   * Segment algorithm type (0062,0008).
   *
   * @type {string}
   */
  algorithmType;
  /**
   * Segment algorithm name (0062,0009).
   *
   * @type {string|undefined}
   */
  algorithmName;
  /**
   * Segment display value as simple value.
   *
   * @type {number|undefined}
   */
  displayValue;
  /**
   * Segment display value as RGB colour ({r,g,b}).
   *
   * @type {RGB|undefined}
   */
  displayRGBValue;
  /**
   * Segment property code: specific property
   * the segment represents (0062,000F).
   *
   * @type {DicomCode|undefined}
   */
  propertyTypeCode;
  /**
   * Segment property category code: general category
   * of the property the segment represents (0062,0003).
   *
   * @type {DicomCode|undefined}
   */
  propertyCategoryCode;
  /**
   * Segment tracking UID (0062,0021).
   *
   * @type {string|undefined}
   */
  trackingUid;
  /**
   * Segment tracking id: text label for the UID (0062,0020).
   *
   * @type {string|undefined}
   */
  trackingId;

  /**
   * @param {number} number The segment number.
   * @param {string} label The segment label.
   * @param {string} algorithmType The segment number.
   */
  constructor(number, label, algorithmType) {
    this.number = number;
    this.label = label;
    this.algorithmType = algorithmType;
  }
}

/**
 * Get a segment object from a dicom element.
 *
 * @param {Object<string, DataElement>} dataElements The dicom element.
 * @returns {MaskSegment} A segment object.
 */
export function getSegment(dataElements) {
  // number -> SegmentNumber (type1)
  // label -> SegmentLabel (type1)
  // algorithmType -> SegmentAlgorithmType (type1)
  const segment = new MaskSegment(
    dataElements[TagKeys.SegmentNumber].value[0],
    dataElements[TagKeys.SegmentLabel]
      ? dataElements[TagKeys.SegmentLabel].value[0] : 'n/a',
    dataElements[TagKeys.SegmentAlgorithmType].value[0]
  );
  // algorithmName -> SegmentAlgorithmName (type1C)
  if (typeof dataElements[TagKeys.SegmentAlgorithmName] !== 'undefined') {
    segment.algorithmName = dataElements[TagKeys.SegmentAlgorithmName].value[0];
  }
  // // required if type is not MANUAL
  // if (segment.algorithmType !== 'MANUAL' &&
  //   (typeof segment.algorithmName === 'undefined' ||
  //   segment.algorithmName.length === 0)) {
  //   throw new Error('Empty algorithm name for non MANUAL algorithm type.');
  // }
  // displayValue ->
  // - RecommendedDisplayGrayscaleValue
  // - RecommendedDisplayCIELabValue converted to RGB
  if (typeof dataElements[TagKeys.RecommendedDisplayGrayscaleValue] !==
    'undefined') {
    segment.displayValue =
      dataElements[TagKeys.RecommendedDisplayGrayscaleValue].value[0];
  } else if (typeof dataElements[TagKeys.RecommendedDisplayCIELabValue] !==
    'undefined') {
    const cielabElement =
      dataElements[TagKeys.RecommendedDisplayCIELabValue].value;
    const rgb = cielabToSrgb(uintLabToLab({
      l: cielabElement[0],
      a: cielabElement[1],
      b: cielabElement[2]
    }));
    segment.displayRGBValue = rgb;
  } else {
    logger.warn('No recommended colour for segment, using default');
    segment.displayRGBValue = getDefaultColour(segment.number);
  }
  // Segmented Property Category Code Sequence (type1, only one)
  if (typeof dataElements[TagKeys.SegmentedPropertyCategoryCodeSequence] !==
    'undefined') {
    segment.propertyCategoryCode =
      getCode(
        dataElements[TagKeys.SegmentedPropertyCategoryCodeSequence].value[0]
      );
  } else {
    throw new Error('Missing Segmented Property Category Code Sequence.');
  }
  // Segmented Property Type Code Sequence (type1)
  if (typeof dataElements[TagKeys.SegmentedPropertyTypeCodeSequence] !==
    'undefined') {
    segment.propertyTypeCode =
      getCode(dataElements[TagKeys.SegmentedPropertyTypeCodeSequence].value[0]);
  } else {
    throw new Error('Missing Segmented Property Type Code Sequence.');
  }
  // tracking Id and UID (type1C)
  if (typeof dataElements[TagKeys.TrackingID] !== 'undefined') {
    segment.trackingId = dataElements[TagKeys.TrackingID].value[0];
    segment.trackingUid = dataElements[TagKeys.TrackingUID].value[0];
  }

  return segment;
}

/**
 * Check if two segment objects are equal.
 *
 * @param {MaskSegment} seg1 The first segment.
 * @param {MaskSegment} seg2 The second segment.
 * @returns {boolean} True if both segment are equal.
 */
export function isEqualSegment(seg1, seg2) {
  // basics
  if (typeof seg1 === 'undefined' ||
    typeof seg2 === 'undefined' ||
    seg1 === null ||
    seg2 === null) {
    return false;
  }
  let isEqual = seg1.number === seg2.number &&
    seg1.label === seg2.label &&
    seg1.algorithmType === seg2.algorithmType;
  // display value
  if (typeof seg1.displayRGBValue !== 'undefined' &&
    typeof seg2.displayRGBValue !== 'undefined') {
    isEqual = isEqual &&
      isEqualRgb(seg1.displayRGBValue, seg2.displayRGBValue);
  } else if (typeof seg1.displayValue !== 'undefined' &&
    typeof seg2.displayValue !== 'undefined') {
    isEqual = isEqual &&
      seg1.displayValue === seg2.displayValue;
  } else {
    isEqual = false;
  }
  // algorithmName
  if (typeof seg1.algorithmName !== 'undefined') {
    if (typeof seg2.algorithmName === 'undefined') {
      isEqual = false;
    } else {
      isEqual = isEqual &&
        seg1.algorithmName === seg2.algorithmName;
    }
  }

  return isEqual;
}

/**
 * Check if two segment objects are similar: either the
 * number or the displayValue are equal.
 *
 * @param {MaskSegment} seg1 The first segment.
 * @param {MaskSegment} seg2 The second segment.
 * @returns {boolean} True if both segment are similar.
 */
export function isSimilarSegment(seg1, seg2) {
  // basics
  if (typeof seg1 === 'undefined' ||
    typeof seg2 === 'undefined' ||
    seg1 === null ||
    seg2 === null) {
    return false;
  }
  let isSimilar = seg1.number === seg2.number;
  // display value
  if (typeof seg1.displayRGBValue !== 'undefined' &&
    typeof seg2.displayRGBValue !== 'undefined') {
    isSimilar = isSimilar ||
      isEqualRgb(seg1.displayRGBValue, seg2.displayRGBValue);
  } else if (typeof seg1.displayValue !== 'undefined' &&
    typeof seg2.displayValue !== 'undefined') {
    isSimilar = isSimilar ||
      seg1.displayValue === seg2.displayValue;
  } else {
    isSimilar = false;
  }

  return isSimilar;
}

/**
 * Get a dicom simple tag from a segment object.
 *
 * @param {MaskSegment} segment The segment object.
 * @returns {Object<string, any>} The item as a list of (key, value) pairs.
 */
export function getDicomSegmentItem(segment) {
  let algoType = segment.algorithmType;
  if (algoType === undefined) {
    algoType = 'MANUAL';
  }
  // dicom item (tags are in group/element order)
  const segmentItem = {
    SegmentNumber: segment.number,
    SegmentLabel: segment.label,
    SegmentAlgorithmType: algoType
  };
  // SegmentAlgorithmName
  if (algoType !== 'MANUAL' && segment.algorithmName !== undefined) {
    segmentItem.SegmentAlgorithmName = segment.algorithmName;
  }
  // RecommendedDisplay value
  if (segment.displayRGBValue) {
    const cieLab = labToUintLab(srgbToCielab(segment.displayRGBValue));
    segmentItem.RecommendedDisplayCIELabValue = [
      Math.round(cieLab.l),
      Math.round(cieLab.a),
      Math.round(cieLab.b)
    ];
  } else {
    segmentItem.RecommendedDisplayGrayscaleValue = segment.displayValue;
  }
  // SegmentedPropertyCategoryCodeSequence
  if (segment.propertyCategoryCode) {
    segmentItem.SegmentedPropertyCategoryCodeSequence = {
      value: [getDicomCodeItem(segment.propertyCategoryCode)]
    };
  }
  // SegmentedPropertyTypeCodeSequence
  if (segment.propertyTypeCode) {
    segmentItem.SegmentedPropertyTypeCodeSequence = {
      value: [getDicomCodeItem(segment.propertyTypeCode)]
    };
  }
  // tracking
  if (segment.trackingId) {
    segmentItem.TrackingID = segment.trackingId;
    segmentItem.TrackingUID = segment.trackingUid;
  }
  // return
  return segmentItem;
}