Last active
February 5, 2021 15:32
-
-
Save agirault/1779e12ad36ae8bca7da526fbbd772a8 to your computer and use it in GitHub Desktop.
This, but in JS: https://gist.github.com/agirault/c32ccdc6755698f720d876374bebfa92
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
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, | |
}; |
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...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
They're two distinct things though L/R for
Side
is left and right of the screen. L/R forLabel
is anatomical orientations. I don't think the order matters here since there isn't a 1-1 match.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)