Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto flipping of disoriented Mammography images on stack viewport #4353

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really belong here - this is adding specific behaviour for certain types of images in certain cases. I would suggest adding it to the split/sort rules as part of generating the splitting criteria, and add it into the series level splitting for MG type series that have this issue. Then, the hanging protocol rules can choose to pickup those values for the orientation/position. That is a much more generic approach, and doesn't cause the viewport service to have deep knowledge about how to handle orientation changes.
You could also create a custom orientation/position metadata module as an alternative, and use that module in CS3D as the initial flip setup if it isn't currently set, but if so you need to change the viewport handling so that a viewport with flipHorizontal/flipVertical/initial rotation works correctly when you flip the viewport - that is, flipping the viewport should flip the images FROM the original location.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact I think this should also be calculated in the hanging protocol service because the same orientation isn't always shown for hanging protocols, and those should still work correctly - that is, left on right or right on left should both be supported by the hanging protocol, and NOT the viewport. Even if you create a default rule and add it into the default hanging protocol, I think doing it there is a better choice, although getting default values in the split/sort rules to apply at hte series level makes that easy to apply in the hanging protocol.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you should do is compute the desired default initial flip state in the display set object, and export it as a metadata instance and apply it here when it is set for the image id/display set uid. It should be applied as the base data, and then rotation/flip etc on TOP of that applies relative to the base rotation etc. That change could actually go into the CS3D library in the stack/volume viewports, or it can be done here as an integrated value. That way, the image "flip" isn't shown by default unless the flip is changed relative to the base flip/rotate state. That is a robust way to specify the values correctly.

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