-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
Adithyan-Dinesh-Trenser
wants to merge
1
commit into
OHIF:master
Choose a base branch
from
gradienthealth:feat/auto-flipping-of-mammogram-images-on-stack-viewport
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+372
−4
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
10
extensions/cornerstone/src/utils/isOrientationCorrectionNeeded.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.