-
-
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
SideandLPSCoordinatesOfSidestart with top/bottom butLabelstarts with left/right.I also find odd that top is
[0, -1, 0]and not[0, 1, 0]