-
-
Save agirault/1779e12ad36ae8bca7da526fbbd772a8 to your computer and use it in GitHub Desktop.
import vtkImageMapper from 'vtk.js/Sources/Rendering/Core/ImageMapper'; | |
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; | |
const { SlicingMode } = vtkImageMapper; | |
const ScreenSide = { | |
top: Symbol('top'), | |
bottom: Symbol('bottom'), | |
left: Symbol('left'), | |
right: Symbol('right'), | |
close: Symbol('close'), | |
far: Symbol('far'), | |
}; | |
function LPSCoordinatesOfScreenSide(screenSide) { | |
switch (screenSide) { | |
case ScreenSide.top: return [0, -1, 0]; | |
case ScreenSide.bottom: return [0, 1, 0]; | |
case ScreenSide.left: return [-1, 0, 0]; | |
case ScreenSide.right: return [1, 0, 0]; | |
case ScreenSide.close: return [0, 0, -1]; | |
case ScreenSide.far: return [0, 0, 1]; | |
default: return []; | |
} | |
} | |
const Label = { | |
left: Symbol('Left'), | |
right: Symbol('Right'), | |
posterior: Symbol('Posterior'), | |
anterior: Symbol('Anterior'), | |
superior: Symbol('Superior'), | |
inferior: Symbol('Inferior'), | |
}; | |
function LPSCoordinatesOfLabel(label) { | |
switch (label) { | |
case Label.left: return [1, 0, 0]; | |
case Label.right: return [-1, 0, 0]; | |
case Label.posterior: return [0, 1, 0]; | |
case Label.anterior: return [0, -1, 0]; | |
case Label.superior: return [0, 0, 1]; | |
case Label.inferior: return [0, 0, -1]; | |
default: return []; | |
} | |
} | |
function labelFor(screenSide, mapper) { | |
// Method for image space slicing (I, J, K) | |
function labelForImageSpaceSlicing(mat3x3) { | |
// Create a 3x3 matrix transformation from the image space | |
// (IJK, JKI or KIJ) to world space (LPS) | |
const dir9 = mapper.getInputData().getDirection(); | |
const dir3x3 = [ | |
[dir9[0], dir9[3], dir9[6]], | |
[dir9[1], dir9[4], dir9[7]], | |
[dir9[2], dir9[5], dir9[8]], | |
]; | |
vtkMath.multiply3x3_mat3(dir3x3, mat3x3, mat3x3); | |
// Apply the transformation to the coordinate of the side | |
let outVec = [0, 0, 0]; | |
const inVec = LPSCoordinatesOfScreenSide(screenSide); | |
vtkMath.multiply3x3_vect3(mat3x3, inVec, outVec); | |
// Restrict to the unit vectors | |
outVec = outVec.map((val) => { | |
const threshold = Math.sqrt(0.5); | |
if (val < -threshold) { | |
return -1; | |
} | |
if (val > threshold) { | |
return 1; | |
} | |
return 0; | |
}); | |
// Find matching label | |
const labels = Object.values(Label); | |
return labels.find((label) => vtkMath.areMatricesEqual(outVec, LPSCoordinatesOfLabel(label))); | |
} | |
switch (mapper.getSlicingMode()) { | |
case SlicingMode.I: { | |
// JKI -> IJK | |
const mat3x3 = [[0, 1, 0], [0, 0, -1], [-1, 0, 0]]; | |
return labelForImageSpaceSlicing(mat3x3); | |
} | |
case SlicingMode.J: { | |
// KIJ -> IJK | |
const mat3x3 = [[1, 0, 0], [0, 0, -1], [0, 1, 0]]; | |
return labelForImageSpaceSlicing(mat3x3); | |
} | |
case SlicingMode.K: { | |
// IJK -> IJK | |
const mat3x3 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; | |
return labelForImageSpaceSlicing(mat3x3); | |
} | |
case SlicingMode.X: | |
switch (screenSide) { | |
case ScreenSide.top: return Label.superior; | |
case ScreenSide.bottom: return Label.inferior; | |
case ScreenSide.left: return Label.anterior; | |
case ScreenSide.right: return Label.posterior; | |
case ScreenSide.close: return Label.left; | |
case ScreenSide.far: return Label.right; | |
default: return null; | |
} | |
case SlicingMode.Y: | |
switch (side) { | |
case ScreenSide.top: return Label.superior; | |
case ScreenSide.bottom: return Label.inferior; | |
case ScreenSide.left: return Label.right; | |
case ScreenSide.right: return Label.left; | |
case ScreenSide.close: return Label.anterior; | |
case ScreenSide.far: return Label.posterior; | |
default: return null; | |
} | |
case SlicingMode.Z: | |
switch (screenSide) { | |
case ScreenSide.top: return Label.anterior; | |
case ScreenSide.bottom: return Label.posterior; | |
case ScreenSide.left: return Label.right; | |
case ScreenSide.right: return Label.left; | |
case ScreenSide.close: return Label.inferior; | |
case ScreenSide.far: return Label.superior; | |
default: return null; | |
} | |
default: return null; | |
} | |
} | |
export default { | |
ScreenSide, | |
labelFor, | |
}; |
I find it odd that Side and LPSCoordinatesOfSide start with top/bottom but Label starts with left/right.
They're two distinct things though L/R for Side
is left and right of the screen. L/R for Label
is anatomical orientations. I don't think the order matters here since there isn't a 1-1 match.
I also find odd that top is [0, -1, 0] and not [0, 1, 0]
So, the logic here is a bit hard to explain (meaning I could do a better job/it should be refactored another way for clarity), but I'll try to show my thinking here: if you take a DICOM scan (IJ coordinates, slices along K) that is axial (direction cosines -> identity in LPS), then the top of the screen points to "Anterior" which is [0, -1, 0]
in LPS.
It does work (I've tested with data acquired in any arbitrary direction), but I am pretty sure there is a better way to explain/implement the conversion from screen coordinates (Side) to anatomical coordinates (Label)
Not sure to fully understand, but it seems that you know what you are doing :-)
As a side note, I did not realize that Side
was for screen positions, maybe ScreenSide
would be more meaningful...
I find it odd that
Side
andLPSCoordinatesOfSide
start with top/bottom butLabel
starts with left/right.I also find odd that top is
[0, -1, 0]
and not[0, 1, 0]