src_image_maskSegmentHelper.js

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

// doc imports
/* eslint-disable no-unused-vars */
import {Image} from './image.js';
import {MaskSegment} from '../dicom/dicomSegment.js';
/* eslint-enable no-unused-vars */

/**
 * Mask segment helper: helps handling the segments list,
 *   but does *NOT* update the associated mask (use special commands
 *   for that such as DeleteSegmentCommand, ChangeSegmentColourCommand...).
 */
export class MaskSegmentHelper {

  /**
   * The associated mask.
   *
   * @type {Image}
   */
  #mask;

  /**
   * The segments: array of segment description.
   *
   * @type {MaskSegment[]}
   */
  #segments;

  /**
   * @param {Image} mask The associated mask image.
   */
  constructor(mask) {
    this.#mask = mask;
    // check segments in meta
    const meta = mask.getMeta();
    if (typeof meta.custom === 'undefined') {
      meta.custom = {};
    }
    if (typeof meta.custom.segments === 'undefined') {
      meta.custom.segments = [];
    }
    this.#segments = meta.custom.segments;
  }

  /**
   * Get the associated mask image.
   *
   * @returns {Image} The mask image.
   */
  getMask() {
    return this.#mask;
  }

  /**
   * Find the index of a segment in the segments list.
   *
   * @param {number} segmentNumber The number to find.
   * @returns {number} The index in the segments list, -1 if not found.
   */
  #findSegmentIndex(segmentNumber) {
    return this.#segments.findIndex(function (item) {
      return item.number === segmentNumber;
    });
  }

  /**
   * Check if a segment is part of the segments list.
   *
   * @param {number} segmentNumber The segment number.
   * @returns {boolean} True if the segment is included.
   */
  hasSegment(segmentNumber) {
    return this.#findSegmentIndex(segmentNumber) !== -1;
  }

  /**
   * Get the number of segments of the segmentation.
   *
   * @returns {number} The number of segments.
   */
  getNumberOfSegments() {
    return this.#segments.length;
  }

  /**
   * Check if a segment is present in a mask image.
   *
   * @param {number[]} numbers Array of segment numbers.
   * @returns {boolean[]} Array of boolean set to true
   *   if the segment is present in the mask.
   */
  maskHasSegments(numbers) {
    // create values using displayValue
    const values = [];
    const unknowns = [];
    for (let i = 0; i < numbers.length; ++i) {
      const segment = this.getSegment(numbers[i]);
      if (typeof segment !== 'undefined') {
        if (typeof segment.displayValue !== 'undefined') {
          values.push(segment.displayValue);
        } else {
          values.push(segment.number);
        }
      } else {
        logger.warn('Unknown segment in maskHasSegments: ' + numbers[i]);
        unknowns.push(i);
      }
    }
    const res = this.#mask.hasValues(values);
    // insert unknowns as false in result
    for (let j = 0; j < unknowns.length; ++j) {
      res.splice(unknowns[j], 0, false);
    }
    return res;
  }

  /**
   * Get a segment from the inner segment list.
   *
   * @param {number} segmentNumber The segment number.
   * @returns {MaskSegment|undefined} The segment or undefined if not found.
   */
  getSegment(segmentNumber) {
    let segment;
    const index = this.#findSegmentIndex(segmentNumber);
    if (index !== -1) {
      segment = this.#segments[index];
    }
    return segment;
  }

  /**
   * Add a segment to the segments list.
   *
   * @param {MaskSegment} segment The segment to add.
   */
  addSegment(segment) {
    const index = this.#findSegmentIndex(segment.number);
    if (index === -1) {
      this.#segments.push(segment);
      // update palette colour map
      if (typeof segment.displayRGBValue !== 'undefined') {
        this.#mask.updatePaletteColourMap(
          segment.number, segment.displayRGBValue);
      }
    } else {
      logger.warn(
        'Not adding segment, it is allready in the segments list: ' +
        segment.number);
    }
  }

  /**
   * Remove a segment from the segments list.
   *
   * @param {number} segmentNumber The segment number.
   */
  removeSegment(segmentNumber) {
    const index = this.#findSegmentIndex(segmentNumber);
    if (index !== -1) {
      this.#segments.splice(index, 1);
    } else {
      logger.warn(
        'Cannot remove segment, it is not in the segments list: ' +
        segmentNumber);
    }
  }

  /**
   * Update a segment of the segments list.
   *
   * @param {MaskSegment} segment The segment to update.
   */
  updateSegment(segment) {
    const index = this.#findSegmentIndex(segment.number);
    if (index !== -1) {
      this.#segments[index] = segment;
    } else {
      logger.warn(
        'Cannot update segment, it is not in the segments list: ' +
        segment.number);
    }
  }

  /**
   * The overlap count of a single segment with another segment.
   *
   * @typedef OverlapCount
   * @property {string} label The segment label.
   * @property {number} count The number of overlapping voxels.
   * @property {number} percentage The overlap percentage between 0 and 100.
   */

  /**
   * The count of overlapping voxels of a single segment with a set of
   * different segments.
   *
   * @typedef Overlap
   * @property {string} label The segment label.
   * @property {Object.<number, OverlapCount>} overlap A Dictionary
   *  containing the counts. The key is the segment number of the overlapping
   *  segment.
   * @property {number} count The voxel volume of this segment.
   */

  /**
   * An Dictionary containing the count of overlapping voxels between two
   * sets of segments.
   * The key is the segment number of a segment in the first set.
   * The value is a Dictionary of all of the segments in the second set
   * that overlap with the key segment.
   *
   * @typedef {Object.<number, Overlap>} OverlapMap
   */

  /**
   * Find the overlap for each segment between two segmentation masks.
   * It is assumed these images have the same orientation.
   *
   * @param {MaskSegmentHelper} rhs The helper of the
   *   segmentation image to find overlap with.
   * @returns {OverlapMap} The overlapping voxel counts. First level is the
   *  segments from this image, second level is the compare image segments.
   */
  findOverlap(rhs) {
    // Find the overlapping slices
    const thisGeometry = this.#mask.getGeometry();
    const thisSize = thisGeometry.getSize();
    const compareMask = rhs.getMask();
    const compareGeometry = compareMask.getGeometry();
    const compareSize = compareGeometry.getSize();

    /**
     * @type {OverlapMap}
     */
    const overlap = {};

    const thisTotalSize = thisSize.getTotalSize();
    for (let i = 0; i < thisTotalSize; i++) {
      const thisValue = this.#mask.getValueAtOffset(i);
      if (thisValue !== 0) {
        if (typeof overlap[thisValue] !== 'undefined') {
          overlap[thisValue].count += 1;
        } else {
          const thisSegment = this.getSegment(thisValue);
          overlap[thisValue] = {
            label: thisSegment.label,
            overlap: {},
            count: 1
          };
        }

        const thisIndex = thisSize.offsetToIndex(i);
        const thisWorld = thisGeometry.indexToWorld(thisIndex);
        const compareIndex = compareGeometry.worldToIndex(thisWorld);
        const compareOffset = compareSize.indexToOffset(compareIndex);
        const compareValue = compareMask.getValueAtOffset(compareOffset);

        if (typeof compareValue !== 'undefined' && compareValue !== 0) {
          if (typeof overlap[thisValue].overlap[compareValue] !== 'undefined') {
            overlap[thisValue].overlap[compareValue].count += 1;
          } else {
            const compareSegment = rhs.getSegment(compareValue);
            overlap[thisValue].overlap[compareValue] = {
              label: compareSegment.label,
              count: 1,
              percentage: 0
            };
          }
        }
      }
    }

    for (const thisOverlap of Object.values(overlap)) {
      for (const overlapCount of Object.values(thisOverlap.overlap)) {
        overlapCount.percentage = overlapCount.count * 100 / thisOverlap.count;
      }
    }

    return overlap;
  }

} // class MaskSegmentHelper