Skip to content

Commit

Permalink
Auto flipping of disoriented Mammography images on stack viewport
Browse files Browse the repository at this point in the history
  • Loading branch information
Adithyan-Dinesh-Trenser committed Oct 3, 2024
1 parent 4f18a21 commit 7763d5b
Show file tree
Hide file tree
Showing 15 changed files with 372 additions and 4 deletions.
5 changes: 5 additions & 0 deletions extensions/cornerstone/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ const pkg = require('./package');
module.exports = {
...base,
displayName: pkg.name,
moduleNameMapper: {
...base.moduleNameMapper,
'^@ohif/(.*)$': '<rootDir>/../../platform/$1/src',
'^@cornerstonejs/tools(.*)$': '<rootDir>/../../node_modules/@cornerstonejs/tools',
},
// rootDir: "../.."
// testMatch: [
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
Expand Down
12 changes: 12 additions & 0 deletions extensions/cornerstone/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
utilities as csUtils,
Types as CoreTypes,
BaseVolumeViewport,
metaData,
} from '@cornerstonejs/core';
import {
ToolGroupManager,
Expand All @@ -21,6 +22,7 @@ import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync';
import { getImageFlips } from './utils/getImageFlips';

const toggleSyncFunctions = {
imageSlice: toggleImageSliceSync,
Expand Down Expand Up @@ -498,6 +500,16 @@ function commandsModule({
viewport.resetProperties?.();
viewport.resetCamera();

const { criteria: isOrientationCorrectionNeeded } = customizationService.get(
'orientationCorrectionCriterion'
);
const instance = metaData.get('instance', viewport.getCurrentImageId());

if ((isOrientationCorrectionNeeded as (input) => boolean)?.(instance)) {
const { hFlip, vFlip } = getImageFlips(instance);
(hFlip || vFlip) && viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });
}

viewport.render();
},
scaleViewport: ({ direction }) => {
Expand Down
3 changes: 3 additions & 0 deletions extensions/cornerstone/src/getCustomizationModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import defaultWindowLevelPresets from './components/WindowLevelActionMenu/defaul
import { colormaps } from './utils/colormaps';
import { CONSTANTS } from '@cornerstonejs/core';
import { CornerstoneOverlay } from './Viewport/Overlays/CustomizableViewportOverlay';
import isOrientationCorrectionNeeded from './utils/isOrientationCorrectionNeeded';

const DefaultColormap = 'Grayscale';
const { VIEWPORT_PRESETS } = CONSTANTS;
Expand Down Expand Up @@ -211,6 +212,8 @@ function getCustomizationModule() {
],
},
},
// TODO: Move this customization to MG specific mode when introduced in OHIF.
{ id: 'orientationCorrectionCriterion', criteria: isOrientationCorrectionNeeded },
],
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
cache,
Enums as csEnums,
BaseVolumeViewport,
metaData,
} from '@cornerstonejs/core';

import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools';
import { IViewportService } from './IViewportService';
import { RENDERING_ENGINE_ID } from './constants';
Expand All @@ -21,6 +21,7 @@ import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCa
import { LutPresentation, PositionPresentation, Presentations } from '../../types/Presentation';

import JumpPresets from '../../utils/JumpPresets';
import { getImageFlips } from '../../utils/getImageFlips';

const EVENTS = {
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
Expand Down Expand Up @@ -643,6 +644,18 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi

return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });

const { customizationService } = this.servicesManager.services;
const { criteria: isOrientationCorrectionNeeded } = customizationService.get(
'orientationCorrectionCriterion'
);
const instance = metaData.get('instance', imageIds[initialImageIndexToUse]);

if ((isOrientationCorrectionNeeded as (input) => boolean)?.(instance)) {
const { hFlip, vFlip } = getImageFlips(instance);
(hFlip || vFlip) && viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });
}

this.setPresentations(viewport.id, presentations, viewportInfo);
if (displayArea) {
viewport.setDisplayArea(displayArea);
Expand Down
159 changes: 159 additions & 0 deletions extensions/cornerstone/src/utils/getImageFlips.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { getImageFlips } from './getImageFlips';

const orientationDirectionVectorMap = {
L: [1, 0, 0], // Left
R: [-1, 0, 0], // Right
P: [0, 1, 0], // Posterior/ Back
A: [0, -1, 0], // Anterior/ Front
H: [0, 0, 1], // Head/ Superior
F: [0, 0, -1], // Feet/ Inferior
};

const getDirectionsFromPatientOrientation = patientOrientation => {
if (typeof patientOrientation === 'string') {
patientOrientation = patientOrientation.split('\\');
}

return {
rowDirection: patientOrientation[0],
columnDirection: patientOrientation[1][0],
};
};

const getOrientationStringLPS = vector => {
const sampleVectorDirectionMap = {
'1,0,0': 'L',
'-1,0,0': 'R',
'0,1,0': 'P',
'0,-1,0': 'A',
'0,0,1': 'H',
'0,0,-1': 'F',
};

return sampleVectorDirectionMap[vector.toString()];
};

jest.mock('@cornerstonejs/tools ', () => ({
utilities: { orientation: { getOrientationStringLPS } },
}));
jest.mock('@ohif/core', () => ({
defaults: { orientationDirectionVectorMap },
utils: { getDirectionsFromPatientOrientation },
}));

describe('getImageFlips', () => {
test('should return empty object if none of the parameters are provided', () => {
const flipsNeeded = getImageFlips({});
expect(flipsNeeded).toEqual({});
});

test('should return empty object if ImageOrientationPatient and PatientOrientation is not provided', () => {
const ImageLaterality = 'L';
const flipsNeeded = getImageFlips({
ImageLaterality,
});
expect(flipsNeeded).toEqual({});
});

test('should return empty object if ImageLaterality is not privided', () => {
const ImageOrientationPatient = [0, 1, 0, 0, 0, 1],
PatientOrientation = ['P', 'H'];
const flipsNeeded = getImageFlips({
ImageOrientationPatient,
PatientOrientation,
});
expect(flipsNeeded).toEqual({});
});

test('should return { hFlip: false, vFlip: false } if ImageOrientationPatient is [0, 1, 0, 0, 0, -1] and ImageLaterality is R', () => {
const ImageOrientationPatient = [0, 1, 0, 0, 0, -1],
PatientOrientation = ['P', 'F'],
ImageLaterality = 'R';
const flipsNeeded = getImageFlips({
ImageOrientationPatient,
PatientOrientation,
ImageLaterality,
});
expect(flipsNeeded).toEqual({ hFlip: false, vFlip: false });
});

test('should return { hFlip: false, vFlip: true } if ImageOrientationPatient is [0, -1, 0, 0, 0, 1] and ImageLaterality is L', () => {
const ImageOrientationPatient = [0, -1, 0, 0, 0, 1],
ImageLaterality = 'L';
const flipsNeeded = getImageFlips({
ImageOrientationPatient,
ImageLaterality,
});
expect(flipsNeeded).toEqual({ hFlip: false, vFlip: true });
});

test('should return { hFlip: true, vFlip: true } if ImageOrientationPatient is [0, -1, 0, -1, 0, 0] and ImageLaterality is R', () => {
const ImageOrientationPatient = [0, -1, 0, -1, 0, 0],
ImageLaterality = 'R';
const flipsNeeded = getImageFlips({
ImageOrientationPatient,
ImageLaterality,
});
expect(flipsNeeded).toEqual({ hFlip: true, vFlip: true });
});

test("should return { hFlip: true, vFlip: true } if ImageOrientationPatient is not present, PatientOrientation is ['P', 'H'] and ImageLaterality is L", () => {
const PatientOrientation = ['P', 'H'],
ImageLaterality = 'L';
const flipsNeeded = getImageFlips({
PatientOrientation,
ImageLaterality,
});
expect(flipsNeeded).toEqual({ hFlip: true, vFlip: true });
});

test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient is not present, PatientOrientation is ['A', 'F'] and ImageLaterality is R", () => {
const PatientOrientation = ['A', 'F'],
ImageLaterality = 'R';
const flipsNeeded = getImageFlips({
PatientOrientation,
ImageLaterality,
});
expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false });
});

test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient is not present, PatientOrientation is ['A', 'FL'] and ImageLaterality is R", () => {
const PatientOrientation = ['A', 'FL'],
ImageLaterality = 'R';
const flipsNeeded = getImageFlips({
PatientOrientation,
ImageLaterality,
});
expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false });
});

test("should return { hFlip: true, vFlip: false } if ImageOrientationPatient ans ImageLaterality is not present, PatientOrientation is ['P', 'FL'] and FrameLaterality is L", () => {
const PatientOrientation = ['P', 'FL'],
FrameLaterality = 'L';
const flipsNeeded = getImageFlips({
PatientOrientation,
FrameLaterality,
});
expect(flipsNeeded).toEqual({ hFlip: true, vFlip: false });
});

test("should return empty object if ImageOrientationPatient is not present, PatientOrientation is ['H', 'R'] and ImageLaterality is R since the orientation is rotated, not flipped", () => {
const PatientOrientation = ['H', 'R'],
ImageLaterality = 'R';
const flipsNeeded = getImageFlips({
PatientOrientation,
ImageLaterality,
});
expect(flipsNeeded).toEqual({});
});

test("should return empty object if ImageOrientationPatient is not present, PatientOrientation is ['F', 'L'] and ImageLaterality is L since the orientation is rotated, not flipped", () => {
const PatientOrientation = ['F', 'L'],
ImageLaterality = 'L';
const flipsNeeded = getImageFlips({
PatientOrientation,
ImageLaterality,
});
expect(flipsNeeded).toEqual({});
});
});
123 changes: 123 additions & 0 deletions extensions/cornerstone/src/utils/getImageFlips.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { defaults, utils } from '@ohif/core';
import { utilities } from '@cornerstonejs/tools';
import { vec3 } from 'gl-matrix';

const { orientationDirectionVectorMap } = defaults;
const { getOrientationStringLPS } = utilities.orientation;

type IOP = [number, number, number, number, number, number];
type PO = [string, string] | string;
type IL = string;
type FL = string;
type Instance = {
ImageOrientationPatient?: IOP;
PatientOrientation?: PO;
ImageLaterality?: IL;
FrameLaterality?: FL;
};

/**
* A function to get required flipping to correct the image according to Orientation and Laterality.
* This function does not handle rotated images.
* @param instance Metadata instance of the image.
* @returns vertical and horizontal flipping needed to correct the image if possible.
*/
export function getImageFlips(instance: Instance): { vFlip?: boolean; hFlip?: boolean } {
const { ImageOrientationPatient, PatientOrientation, ImageLaterality, FrameLaterality } =
instance;

if (!(ImageOrientationPatient || PatientOrientation) || !(ImageLaterality || FrameLaterality)) {
console.warn(
'Skipping image orientation correction due to missing ImageOrientationPatient/ PatientOrientation or/and ImageLaterality/ FrameLaterality'
);
return {};
}

let rowDirectionCurrent, columnDirectionCurrent, rowCosines, columnCosines;
if (ImageOrientationPatient) {
rowCosines = ImageOrientationPatient.slice(0, 3);
columnCosines = ImageOrientationPatient.slice(3, 6);
rowDirectionCurrent = getOrientationStringLPS(rowCosines);
columnDirectionCurrent = getOrientationStringLPS(columnCosines)[0];
} else {
({ rowDirection: rowDirectionCurrent, columnDirection: columnDirectionCurrent } =
utils.getDirectionsFromPatientOrientation(PatientOrientation));

rowCosines = orientationDirectionVectorMap[rowDirectionCurrent];
columnCosines = orientationDirectionVectorMap[columnDirectionCurrent];
}

const scanAxisNormal = vec3.create();
vec3.cross(scanAxisNormal, rowCosines, columnCosines);

const scanAxisDirection = getOrientationStringLPS(scanAxisNormal as [number, number, number]);

if (isImageRotated(rowDirectionCurrent, columnDirectionCurrent)) {
// TODO: Correcting images with rotation is not implemented.
console.warn('Correcting images by rotating is not implemented');
return {};
}

let rowDirectionTarget, columnDirectionTarget;
switch (scanAxisDirection[0]) {
// Sagittal
case 'L':
case 'R':
if ((ImageLaterality || FrameLaterality) === 'L') {
rowDirectionTarget = 'A';
} else {
rowDirectionTarget = 'P';
}
columnDirectionTarget = 'F';
break;
// Coronal
case 'A':
case 'P':
if ((ImageLaterality || FrameLaterality) === 'L') {
rowDirectionTarget = 'R';
} else {
rowDirectionTarget = 'L';
}
columnDirectionTarget = 'F';
break;
// Axial
case 'H':
case 'F':
if ((ImageLaterality || FrameLaterality) === 'L') {
rowDirectionTarget = 'A';
columnDirectionTarget = 'R';
} else {
rowDirectionTarget = 'P';
columnDirectionTarget = 'L';
}
break;
}

let hFlip = false,
vFlip = false;
if (rowDirectionCurrent !== rowDirectionTarget) {
hFlip = true;
}
if (columnDirectionCurrent !== columnDirectionTarget) {
vFlip = true;
}

return { hFlip, vFlip };
}

function isImageRotated(rowDirection: string, columnDirection: string): boolean {
const possibleValues: { [key: string]: [string, string] } = {
xDirection: ['L', 'R'],
yDirection: ['P', 'A'],
zDirection: ['H', 'F'],
};

if (
possibleValues.yDirection.includes(columnDirection) ||
possibleValues.zDirection.includes(rowDirection)
) {
return true;
}

return false;
}
10 changes: 10 additions & 0 deletions extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const DEFAULT_AUTO_FLIP_MODALITIES: string[] = ['MG'];

export default function isOrientationCorrectionNeeded(instance) {
const { Modality } = instance;

// Check Modality
const isModalityIncluded = DEFAULT_AUTO_FLIP_MODALITIES.includes(Modality);

return isModalityIncluded;
}
Loading

0 comments on commit 7763d5b

Please sign in to comment.