Skip to content

Commit

Permalink
Merge pull request #17 from gradienthealth/gradienthealth/auto_flippi…
Browse files Browse the repository at this point in the history
…ng_on_stack_viewport

Implement auto flipping of disoriented mammogram images on stack viewport
  • Loading branch information
Adithyan-Dinesh-Trenser authored Sep 5, 2024
2 parents f4191a4 + 9485743 commit 588f303
Show file tree
Hide file tree
Showing 16 changed files with 403 additions and 12 deletions.
5 changes: 5 additions & 0 deletions extensions/cornerstone/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ module.exports = {
...base,
name: pkg.name,
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
13 changes: 13 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 toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
import { getImageFlips } from './utils/getImageFlips';

function commandsModule({
servicesManager,
Expand All @@ -35,6 +37,7 @@ function commandsModule({
cornerstoneViewportService,
uiNotificationService,
measurementService,
customizationService,
} = servicesManager.services as CornerstoneServices;

const { measurementServiceSource } = this;
Expand Down Expand Up @@ -483,6 +486,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
@@ -1,6 +1,7 @@
import { Enums } from '@cornerstonejs/tools';
import { toolNames } from './initCornerstoneTools';
import DicomUpload from './components/DicomUpload/DicomUpload';
import isOrientationCorrectionNeeded from './utils/isOrientationCorrectionNeeded';

const tools = {
active: [
Expand Down Expand Up @@ -37,6 +38,8 @@ function getCustomizationModule() {
id: 'cornerstone.overlayViewportTools',
tools,
},
// TODO: Move this customization to MG specific mode when introduced in OHIF.
{ id: 'orientationCorrectionCriterion', criteria: isOrientationCorrectionNeeded },
],
},
];
Expand Down
2 changes: 2 additions & 0 deletions extensions/cornerstone/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import * as csWADOImageLoader from './initWADOImageLoader.js';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
import { getImageFlips } from './utils/getImageFlips';

const Component = React.lazy(() => {
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
Expand Down Expand Up @@ -116,6 +117,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
},
getEnabledElement,
dicomLoaderService,
getImageFlips,
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
VolumeViewport3D,
cache,
Enums as csEnums,
metaData,
eventTarget,
} 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 @@ -20,6 +21,7 @@ import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCa
import { Presentation, 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 @@ -325,6 +327,8 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
viewportInfo: ViewportInfo,
presentations: Presentations
): Promise<void> {
const { segmentationService, displaySetService, customizationService } =
this.servicesManager.services;
const displaySetOptions = viewportInfo.getDisplaySetOptions();

const { imageIds, initialImageIndex, displaySetInstanceUID } = viewportData.data;
Expand Down Expand Up @@ -354,24 +358,48 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
}
}

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

let hFlip = false,
vFlip = false;
if ((isOrientationCorrectionNeeded as (input) => boolean)?.(instance)) {
({ hFlip, vFlip } = getImageFlips(instance));
}

const segmentations = segmentationService.getSegmentations(false);
const toolgroupId = viewportInfo.getToolGroupId();
for (const segmentation of segmentations) {
const toolGroupSegmentationRepresentations =
this.servicesManager.services.segmentationService.getSegmentationRepresentationsForToolGroup(
toolgroupId
) || [];
segmentationService.getSegmentationRepresentationsForToolGroup(toolgroupId) || [];
const isSegmentationInToolGroup = toolGroupSegmentationRepresentations.find(
representation => representation.segmentationId === segmentation.id
);

const callback = evt => {
if (viewport.id !== evt.detail.viewportId) {
return;
}

eventTarget.removeEventListener(csEnums.Events.STACK_VIEWPORT_IMAGES_ADDED, callback);

const camera = presentations.positionPresentation?.camera;
if (isSegmentationInToolGroup && camera) {
viewport.setCamera(camera);
} else {
(hFlip || vFlip) && viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });
}
};

eventTarget.addEventListener(csEnums.Events.STACK_VIEWPORT_IMAGES_ADDED, callback);

if (!isSegmentationInToolGroup) {
const segDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID(
segmentation.id
);
const segDisplaySet = displaySetService.getDisplaySetByUID(segmentation.id);

segDisplaySet &&
this.servicesManager.services.segmentationService.addSegmentationRepresentationToToolGroup(
segmentationService.addSegmentationRepresentationToToolGroup(
toolgroupId,
segmentation.id,
segDisplaySet.isOverlayDisplaySet
Expand All @@ -382,6 +410,11 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
return viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
viewport.setProperties({ ...properties });
const camera = presentations.positionPresentation?.camera;

!camera &&
(hFlip || vFlip) &&
viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });

if (camera) {
viewport.setCamera(camera);
}
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({});
});
});
Loading

0 comments on commit 588f303

Please sign in to comment.