tests_dicom_dicomImage.test.js

import {describe, test, expect, vi} from 'vitest';
import {
  getImage2DSize,
  getPixelSpacing,
  getPixelAspectRatio,
  getSpacingFromMeasure,
  getTagPixelUnit,
  getOrientationMatrix,
  getDicomMeasureItem,
  getDicomPlaneOrientationItem,
  getPhotometricInterpretation,
  isMonochrome,
  isSecondatyCapture,
  getReferencedSeriesUID
} from '../../src/dicom/dicomImage.js';
import {Spacing} from '../../src/image/spacing.js';
import {Matrix33} from '../../src/math/matrix.js';

/**
 * Tests for the 'dicom/dicomImage.js' file.
 */

describe('dicom', () => {

  /**
   * Tests for {@link getPixelSpacing}.
   *
   * @function module:tests/dicom~getpixelspacing
   */
  test('getPixelSpacing', () => {
    const TagKeys = {
      PixelSpacing: '00280030',
      ImagerPixelSpacing: '00181164',
      NominalScannedPixelSpacing: '00182010',
      DistanceSourceToDetector: '00181110',
      DistanceSourceToPatient: '00181111',
      EstimatedRadiographicMagnificationFactor: '00181114',
      PixelAspectRatio: '00280034',
      SpacingBetweenSlices: '00180088',
      PixelMeasuresSequence: '00289110',
      SharedFunctionalGroupsSequence: '52009229',
      PerFrameFunctionalGroupsSequence: '52009230'
    };
    const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    // PixelSpacing #0
    const spacing00 = [1.1];
    const elements00 = {};
    elements00[TagKeys.PixelSpacing] = {
      value: spacing00
    };
    expect(getPixelSpacing(elements00)).toEqual(undefined);

    // PixelSpacing #1
    const spacing01 = [1.1, 1.2];
    const elements01 = {};
    elements01[TagKeys.PixelSpacing] = {
      value: spacing01.toReversed()
    };
    expect(getPixelSpacing(elements01)).toStrictEqual(spacing01);

    // ImagerPixelSpacing #0
    const elements10 = {};
    elements10[TagKeys.ImagerPixelSpacing] = {
      value: spacing01.toReversed()
    };
    expect(getPixelSpacing(elements10)).toStrictEqual(spacing01);
    expect(consoleSpy).toHaveBeenLastCalledWith(
      'Got pixel spacing from raw ImagerPixelSpacing tag'
    );

    // ImagerPixelSpacing #1
    const factor11 = 2;
    const spacing11 = [
      spacing01[0] / factor11,
      spacing01[1] / factor11
    ];
    const elements11 = {};
    elements11[TagKeys.ImagerPixelSpacing] = {
      value: spacing01.toReversed()
    };
    elements11[TagKeys.EstimatedRadiographicMagnificationFactor] = {
      value: [factor11]
    };
    expect(getPixelSpacing(elements11)).toStrictEqual(spacing11);
    expect(consoleSpy).toHaveBeenLastCalledWith(
      'Got pixel spacing from corrected ImagerPixelSpacing tag'
    );

    // ImagerPixelSpacing #2
    const dstd12 = 4;
    const dstp12 = 2;
    const factor12 = dstd12 / dstp12;
    const spacing12 = [
      spacing01[0] / factor12,
      spacing01[1] / factor12
    ];
    const elements12 = {};
    elements12[TagKeys.ImagerPixelSpacing] = {
      value: spacing01.toReversed()
    };
    elements12[TagKeys.DistanceSourceToDetector] = {
      value: [dstd12]
    };
    elements12[TagKeys.DistanceSourceToPatient] = {
      value: [dstp12]
    };
    expect(getPixelSpacing(elements12)).toStrictEqual(spacing12);
    expect(consoleSpy).toHaveBeenLastCalledWith(
      'Got pixel spacing from corrected ImagerPixelSpacing tag'
    );

    // NominalScannedPixelSpacing #0
    const elements20 = {};
    elements20[TagKeys.NominalScannedPixelSpacing] = {
      value: spacing01.toReversed()
    };
    expect(getPixelSpacing(elements20)).toStrictEqual(spacing01);
    expect(consoleSpy).toHaveBeenLastCalledWith(
      'Got pixel spacing from NominalScannedPixelSpacing tag'
    );

    // PixelMeasuresSequence #0
    const elements300 = {};
    elements300[TagKeys.PixelMeasuresSequence] = {
      value: [
        elements01
      ]
    };
    const elements30 = {};
    elements30[TagKeys.SharedFunctionalGroupsSequence] = {
      value: [
        elements300
      ]
    };
    expect(getPixelSpacing(elements30)).toStrictEqual(spacing01);

    // PixelMeasuresSequence #1
    const elements31 = {};
    elements31[TagKeys.PerFrameFunctionalGroupsSequence] = {
      value: [
        elements300
      ]
    };
    expect(getPixelSpacing(elements31)).toStrictEqual(spacing01);
  });

  /**
   * Tests for {@link isMonochrome}.
   *
   * @function module:tests/dicom~ismonochrome
   */
  test('isMonochrome', () => {
    // ok
    expect(isMonochrome('MONOCHROME1')).toBeTruthy();
    expect(isMonochrome('MONOCHROME2')).toBeTruthy();
    // method tests that the string starts with MONOCHROME...
    expect(isMonochrome('MONOCHROME')).toBeTruthy();
    expect(isMonochrome('MONOCHROME123')).toBeTruthy();

    // case sensitive
    expect(isMonochrome('monochrome1')).not.toBeTruthy();

    // not ok
    expect(isMonochrome()).not.toBeTruthy();
    expect(isMonochrome('abcd')).not.toBeTruthy();
    expect(isMonochrome('RGB')).not.toBeTruthy();
    expect(isMonochrome('PALETTE COLOR')).not.toBeTruthy();
  });

  /**
   * Tests for @function getImage2DSize}.
   *
   * @function module:tests/dicom~getimage2dsize
   */
  test('getImage2DSize', () => {
    const TagKeys = {
      Rows: '00280010',
      Columns: '00280011'
    };

    // no rows
    const elements00 = {};
    elements00[TagKeys.Columns] = {value: [512]};
    expect(getImage2DSize(elements00)).toBeUndefined();

    // no columns
    const elements01 = {};
    elements01[TagKeys.Rows] = {value: [512]};
    expect(getImage2DSize(elements01)).toBeUndefined();

    // both present
    const elements02 = {};
    elements02[TagKeys.Rows] = {value: [512]};
    elements02[TagKeys.Columns] = {value: [256]};
    expect(getImage2DSize(elements02)).toEqual([256, 512]);

    // empty object
    expect(getImage2DSize({})).toBeUndefined();
  });

  /**
   * Tests for {@link getPixelAspectRatio}.
   *
   * @function module:tests/dicom~getpixelaspectratio
   */
  test('getPixelAspectRatio', () => {
    const TagKeys = {
      PixelAspectRatio: '00280034'
    };

    // single value (should be undefined)
    const elements00 = {};
    elements00[TagKeys.PixelAspectRatio] = {value: [1.0]};
    expect(getPixelAspectRatio(elements00)).toBeUndefined();

    // two values
    const elements01 = {};
    elements01[TagKeys.PixelAspectRatio] = {value: [1.0, 2.0]};
    expect(getPixelAspectRatio(elements01)).toEqual([2.0, 1.0]);

    // no pixel aspect ratio
    expect(getPixelAspectRatio({})).toBeUndefined();
  });

  /**
   * Tests for {@link getSpacingFromMeasure}.
   *
   * @function module:tests/dicom~getspacingfrommeasure
   */
  test('getSpacingFromMeasure', () => {
    const TagKeys = {
      PixelSpacing: '00280030',
      SpacingBetweenSlices: '00180088'
    };

    // no pixel spacing
    const elements00 = {};
    expect(getSpacingFromMeasure(elements00)).toBeUndefined();

    // pixel spacing only
    const elements01 = {};
    elements01[TagKeys.PixelSpacing] = {value: [1.0, 2.0]};
    const spacing01 = getSpacingFromMeasure(elements01);
    expect(spacing01).toBeDefined();
    expect(spacing01.get(0)).toBe(2.0);
    expect(spacing01.get(1)).toBe(1.0);
    expect(spacing01.get(2)).toBeUndefined();

    // pixel spacing with spacing between slices
    const elements02 = {};
    elements02[TagKeys.PixelSpacing] = {value: [1.0, 2.0]};
    elements02[TagKeys.SpacingBetweenSlices] = {value: [3.0]};
    const spacing02 = getSpacingFromMeasure(elements02);
    expect(spacing02).toBeDefined();
    expect(spacing02.get(0)).toBe(2.0);
    expect(spacing02.get(1)).toBe(1.0);
    expect(spacing02.get(2)).toBe(3.0);
  });

  /**
   * Tests for {@link getTagPixelUnit}.
   *
   * @function module:tests/dicom~gettagpixelunit
   */
  test('getTagPixelUnit', () => {
    const TagKeys = {
      RescaleType: '00281054',
      Units: '00541001',
      Modality: '00080060'
    };

    // RescaleType
    const elements00 = {};
    elements00[TagKeys.RescaleType] = {value: ['HU']};
    expect(getTagPixelUnit(elements00)).toBe('HU');

    // Units
    const elements01 = {};
    elements01[TagKeys.Units] = {value: ['Bq/mL']};
    expect(getTagPixelUnit(elements01)).toBe('Bq/mL');

    // CT modality default
    const elements02 = {};
    elements02[TagKeys.Modality] = {value: ['CT']};
    expect(getTagPixelUnit(elements02)).toBe('HU');

    // non-CT modality
    const elements03 = {};
    elements03[TagKeys.Modality] = {value: ['MR']};
    expect(getTagPixelUnit(elements03)).toBeUndefined();

    // empty
    expect(getTagPixelUnit({})).toBeUndefined();
  });

  /**
   * Tests for (@link getOrientationMatrix}.
   *
   * @function module:tests/dicom~getorientationmatrix
   */
  test('getOrientationMatrix', () => {
    const TagKeys = {
      ImageOrientationPatient: '00200037'
    };

    // no orientation
    expect(getOrientationMatrix({})).toBeUndefined();

    // identity orientation
    const elements00 = {};
    elements00[TagKeys.ImageOrientationPatient] = {
      value: ['1', '0', '0', '0', '1', '0']
    };
    const matrix00 = getOrientationMatrix(elements00);
    expect(matrix00).toBeDefined();
    expect(matrix00.get(0, 0)).toBe(1);
    expect(matrix00.get(1, 0)).toBe(0);
    expect(matrix00.get(0, 1)).toBe(0);

    // different orientation
    const elements01 = {};
    elements01[TagKeys.ImageOrientationPatient] = {
      value: ['0', '1', '0', '-1', '0', '0']
    };
    const matrix01 = getOrientationMatrix(elements01);
    expect(matrix01).toBeDefined();
  });

  /**
   * Tests for {@link getDicomMeasureItem}.
   *
   * @function module:tests/dicom~getdicommeasureitem
   */
  test('getDicomMeasureItem', () => {
    const spacing = new Spacing([1.5, 2.5, 3.5]);
    const item = getDicomMeasureItem(spacing);

    expect(item).toBeDefined();
    expect(item.PixelSpacing).toEqual([2.5, 1.5]);
    expect(item.SpacingBetweenSlices).toBe(3.5);
  });

  /**
   * Tests for {@link getDicomPlaneOrientationItem}.
   *
   * @function module:tests/dicom~getdicomplaneorientationitem
   */
  test('getDicomPlaneOrientationItem', () => {
    /* eslint-disable @stylistic/js/array-element-newline */
    const matrix = new Matrix33([
      1, 0, 0,
      0, 1, 0,
      0, 0, 1
    ]);
    /* eslint-enable @stylistic/js/array-element-newline */
    const item = getDicomPlaneOrientationItem(matrix);

    expect(item).toBeDefined();
    expect(item.ImageOrientationPatient).toEqual([1, 0, 0, 0, 1, 0]);
  });

  /**
   * Tests for {@link getPhotometricInterpretation}.
   *
   * @function module:tests/dicom~getphotometricinterpretation
   */
  test('getPhotometricInterpretation', () => {
    const TagKeys = {
      PhotometricInterpretation: '00280004',
      TransferSyntax: '00020010',
      SamplesPerPixel: '00280002'
    };

    // no elements
    expect(getPhotometricInterpretation({})).toBeUndefined();

    // monochrome
    const elements00 = {};
    elements00[TagKeys.PhotometricInterpretation] = {
      value: ['MONOCHROME2']
    };
    elements00[TagKeys.TransferSyntax] = {
      value: ['1.2.840.10008.1.2']
    };
    expect(getPhotometricInterpretation(elements00)).toBe('MONOCHROME2');

    // RGB
    const elements01 = {};
    elements01[TagKeys.PhotometricInterpretation] = {
      value: ['RGB']
    };
    elements01[TagKeys.TransferSyntax] = {
      value: ['1.2.840.10008.1.2']
    };
    elements01[TagKeys.SamplesPerPixel] = {value: [3]};
    expect(getPhotometricInterpretation(elements01)).toBe('RGB');

    // JPEG baseline
    const elements02 = {};
    elements02[TagKeys.PhotometricInterpretation] = {
      value: ['YBR_FULL_422']
    };
    elements02[TagKeys.TransferSyntax] = {
      value: ['1.2.840.10008.1.2.4.50']
    };
    elements02[TagKeys.SamplesPerPixel] = {value: [3]};
    expect(getPhotometricInterpretation(elements02)).toBe('RGB');

    // single sample RGB -> PALETTE COLOR
    const elements03 = {};
    elements03[TagKeys.PhotometricInterpretation] = {
      value: ['RGB']
    };
    elements03[TagKeys.TransferSyntax] = {
      value: ['1.2.840.10008.1.2']
    };
    elements03[TagKeys.SamplesPerPixel] = {
      value: [1]
    };
    expect(getPhotometricInterpretation(elements03)).toBe('PALETTE COLOR');
  });

  /**
   * Tests for {@link isSecondatyCapture}.
   *
   * @function module:tests/dicom~issecondatycapture
   */
  test('isSecondatyCapture', () => {
    // valid secondary capture UIDs
    expect(isSecondatyCapture('1.2.840.10008.5.1.4.1.1.7')).toBeTruthy();
    expect(isSecondatyCapture('1.2.840.10008.5.1.4.1.1.7.1')).toBeTruthy();
    expect(isSecondatyCapture('1.2.840.10008.5.1.4.1.1.7.2')).toBeTruthy();

    // invalid UIDs
    expect(isSecondatyCapture('1.2.840.10008.5.1.4.1.1.2')).not.toBeTruthy();
    expect(isSecondatyCapture('1.2.840.10008.5.1.4.1.1')).not.toBeTruthy();
    expect(isSecondatyCapture()).not.toBeTruthy();
    expect(isSecondatyCapture('invalid')).not.toBeTruthy();
  });

  /**
   * Tests for {@link getReferencedSeriesUID}.
   *
   * @function module:tests/dicom~getreferencedseriesuid
   */
  test('getReferencedSeriesUID', () => {
    const TagKeys = {
      ReferencedSeriesSequence: '00081115',
      SeriesInstanceUID: '0020000E'
    };

    // no sequence
    expect(getReferencedSeriesUID({})).toBeUndefined();

    // with sequence and UID
    const elements00 = {};
    elements00[TagKeys.ReferencedSeriesSequence] = {
      value: [
        {
          [TagKeys.SeriesInstanceUID]: {value: ['1.2.3']}
        }
      ]
    };
    expect(getReferencedSeriesUID(elements00)).toBe('1.2.3');

    // with sequence but no UID
    const elements01 = {};
    elements01[TagKeys.ReferencedSeriesSequence] = {
      value: [{}]
    };
    expect(getReferencedSeriesUID(elements01)).toBeUndefined();
  });

});