All files / packages/core/src/utilities getCurrentVolumeViewportSlice.ts

0% Statements 0/50
0% Branches 0/6
0% Functions 0/3
0% Lines 0/48

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,
};