Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | import { glMatrix, mat4, vec3 } from 'gl-matrix'; import { IVolumeViewport, Point3 } from '../types'; import { transformIJKToCanvas } from './transformIJKToCanvas'; import { transformCanvasToIJK } from './transformCanvasToIJK'; /** * Get the image data for the current slice rendered on the viewport. The image * data returned is the full slice and not only the region that is visible on * the viewport. It does not work for oblique views. * @param viewport - Volume viewport * @returns Slice image dataand matrices to convert from volume * to slice and vice-versa */ function getCurrentVolumeViewportSlice(viewport: IVolumeViewport) { const { dimensions, scalarData } = viewport.getImageData(); const { width: canvasWidth, height: canvasHeight } = viewport.getCanvas(); // Get three points from the canvas to help us identify the orientation of // the slice. Using canvas width/height to get point far away for each other // because points such as (0,0), (1,0) and (0,1) may be converted to the same // ijk index when the image is zoomed in. const ijkOriginPoint = transformCanvasToIJK(viewport, [0, 0]); const ijkRowPoint = transformCanvasToIJK(viewport, [canvasWidth - 1, 0]); const ijkColPoint = transformCanvasToIJK(viewport, [0, canvasHeight - 1]); // Subtract the points to get the row and column vectors in index space const ijkRowVec = vec3.sub(vec3.create(), ijkRowPoint, ijkOriginPoint); const ijkColVec = vec3.sub(vec3.create(), ijkColPoint, ijkOriginPoint); const ijkSliceVec = vec3.cross(vec3.create(), ijkRowVec, ijkColVec); vec3.normalize(ijkRowVec, ijkRowVec); vec3.normalize(ijkColVec, ijkColVec); vec3.normalize(ijkSliceVec, ijkSliceVec); // Any unit vector parallel to IJK have one component equal to 1 and // the other two components equal to 0. If two of them are parallel // the third one is also parallel const maxIJKRowVec = Math.max( Math.abs(ijkRowVec[0]), Math.abs(ijkRowVec[1]), Math.abs(ijkRowVec[2]) ); const maxIJKColVec = Math.max( Math.abs(ijkColVec[0]), Math.abs(ijkColVec[1]), Math.abs(ijkColVec[2]) ); // Using glMatrix.equals() because the number may be not exactly equal to // 1 due to rounding issues if (!glMatrix.equals(1, maxIJKRowVec) || !glMatrix.equals(1, maxIJKColVec)) { throw new Error('Livewire is not available for rotate/oblique viewports'); } const [sx, sy, sz] = dimensions; // All eight volume corners in index space // prettier-ignore const ijkCorners: Point3[] = [ [ 0, 0, 0], // top-left-front [sx - 1, 0, 0], // top-right-front [ 0, sy - 1, 0], // bottom-left-front [sx - 1, sy - 1, 0], // bottom-right-front [ 0, 0, sz - 1], // top-left-back [sx - 1, 0, sz - 1], // top-right-back [ 0, sy - 1, sz - 1], // bottom-left-back [sx - 1, sy - 1, sz - 1], // bottom-right-back ]; // Project the volume corners onto the canvas const canvasCorners = ijkCorners.map((ijkCorner) => transformIJKToCanvas(viewport, ijkCorner) ); // Calculate the AABB from the corners project onto the canvas const canvasAABB = canvasCorners.reduce( (aabb, canvasPoint) => { aabb.minX = Math.min(aabb.minX, canvasPoint[0]); aabb.minY = Math.min(aabb.minY, canvasPoint[1]); aabb.maxX = Math.max(aabb.maxX, canvasPoint[0]); aabb.maxY = Math.max(aabb.maxY, canvasPoint[1]); return aabb; }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity } ); // Get the top-left, bottom-right and the diagonal vector of // the slice in index space const ijkTopLeft = transformCanvasToIJK(viewport, [ canvasAABB.minX, canvasAABB.minY, ]); const ijkBottomRight = transformCanvasToIJK(viewport, [ canvasAABB.maxX, canvasAABB.maxY, ]); const ijkDiagonal = vec3.sub(vec3.create(), ijkBottomRight, ijkTopLeft); // prettier-ignore const sliceToIndexMatrix = mat4.fromValues( ijkRowVec[0], ijkRowVec[1], ijkRowVec[2], 0, ijkColVec[0], ijkColVec[1], ijkColVec[2], 0, ijkSliceVec[0], ijkSliceVec[1], ijkSliceVec[2], 0, ijkTopLeft[0], ijkTopLeft[1], ijkTopLeft[2], 1 ); const indexToSliceMatrix = mat4.invert(mat4.create(), sliceToIndexMatrix); // Dot the diagonal with row/column to find the image width/height const sliceWidth = vec3.dot(ijkRowVec, ijkDiagonal) + 1; const sliceHeight = vec3.dot(ijkColVec, ijkDiagonal) + 1; // Create a TypedArray with same type from the original scalarData const TypedArray = (scalarData as any).constructor; const sliceData = new TypedArray(sliceWidth * sliceHeight); // We need to know how many pixels to jump for every change in Z direction const pixelsPerSlice = dimensions[0] * dimensions[1]; // Create two vectors to keep track of each row/column it is, reducing // the amount of vec3 instances created and simplifying the math. const ijkPixelRow = vec3.clone(ijkTopLeft); const ijkPixelCol = vec3.create(); // Use an independent index to avoid multiple (x,y) to index conversions let slicePixelIndex = 0; for (let y = 0; y < sliceHeight; y++) { vec3.copy(ijkPixelCol, ijkPixelRow); for (let x = 0; x < sliceWidth; x++) { const volumePixelIndex = ijkPixelCol[2] * pixelsPerSlice + ijkPixelCol[1] * dimensions[0] + ijkPixelCol[0]; // It may never throw any "out of bounds" error but just to be safe if (volumePixelIndex < scalarData.length) { sliceData[slicePixelIndex] = scalarData[volumePixelIndex]; } // Move to next slice pixel slicePixelIndex++; // Move to the next voxel vec3.add(ijkPixelCol, ijkPixelCol, ijkRowVec); } // Move to the next row vec3.add(ijkPixelRow, ijkPixelRow, ijkColVec); } return { width: sliceWidth, height: sliceHeight, scalarData: sliceData, sliceToIndexMatrix, indexToSliceMatrix, }; } export { getCurrentVolumeViewportSlice as default, getCurrentVolumeViewportSlice, }; |