All files / core/src/utilities/voxelSlab sampleVoxelsInShape.ts

0% Statements 0/13
0% Branches 0/8
0% Functions 0/1
0% Lines 0/13

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                                                                                                                                                                                                                     
import type { Point3 } from '../../types';
import type { VoxelsInShapeOptions } from './iterateVoxelsInShape';
import { iterateVoxelsInShape } from './iterateVoxelsInShape';
 
/** One voxel of an area annotation, with the value read from the volume. */
export interface VoxelSample {
  /** The voxel's value. */
  value: number;
  /** The voxel centre in world coordinates. Safe to retain. */
  pointLPS: Point3;
  /** The voxel index. Safe to retain. */
  pointIJK: Point3;
}
 
/** Just enough of a voxel manager to read values by index. */
export interface VoxelValueSource {
  getAtIJKPoint(ijk: Point3): number | undefined | null;
}
 
export interface VoxelsInShapeSamplingOptions extends VoxelsInShapeOptions {
  /** Where the values come from. Voxels it has no value for are skipped. */
  voxelManager: VoxelValueSource;
  /**
   * Called for every voxel that has a value, in iteration order. This is where
   * a statistics accumulator hooks in; it runs whether or not the samples are
   * being collected.
   */
  onSample?: (sample: VoxelSample) => void;
  /**
   * Whether to return the samples as well. Collecting them costs an object per
   * voxel, which an ROI over a large volume will feel, so a caller that only
   * wants statistics should leave this off and use `onSample`.
   */
  storePointData?: boolean;
}
 
/**
 * Reads the value of every voxel an area annotation contains, per Rule M of
 * https://github.com/cornerstonejs/cornerstone3D/issues/2889
 *
 * This is the whole of the accumulation half of a measurement, and it is
 * identical for every area annotation: only the shape handed in as
 * `getShapeRuns` differs between a polyline, an ellipse and a rectangle. Nothing
 * here reads a viewport, so the same annotation over the same volume samples
 * the same voxels at any zoom, pan, canvas size or slab thickness, and in any
 * orientation.
 *
 * The caller resolves the thickness, as the example below does. A planar shape
 * reports a required thickness of 0, so the `||` keeps the annotation's own
 * thickness. A shape that carries depth of its own, such as an ellipsoid,
 * reports that depth and the `||` takes it instead. The `bounds` the caller
 * passes must allow for the same value.
 *
 * ```ts
 * const shape = createPolylineShape({ volume, viewPlaneNormal, polyline });
 *
 * const samples = sampleVoxelsInShape({
 *   volume,
 *   planePoint,
 *   viewPlaneNormal,
 *   referencePlaneThickness:
 *     shape.getRequiredThickness() || referencePlaneThickness,
 *   getShapeRuns: shape.getRuns,
 *   voxelManager,
 *   onSample: statsCallback,
 *   storePointData,
 * });
 * ```
 *
 * @returns the samples when `storePointData` is set, otherwise an empty array.
 * `onSample` is called either way.
 */
export function sampleVoxelsInShape(
  options: VoxelsInShapeSamplingOptions
): VoxelSample[] {
  const { voxelManager, onSample, storePointData, ...iteration } = options;
 
  const samples: VoxelSample[] = [];
 
  if (!voxelManager) {
    return samples;
  }
 
  for (const { ijk, center } of iterateVoxelsInShape(iteration)) {
    const value = voxelManager.getAtIJKPoint(ijk);
 
    if (value === undefined || value === null) {
      continue;
    }
 
    // ijk and center are reused between iterations, so copy before retaining.
    const sample: VoxelSample = {
      value,
      pointLPS: [center[0], center[1], center[2]],
      pointIJK: [ijk[0], ijk[1], ijk[2]],
    };
 
    onSample?.(sample);
 
    if (storePointData) {
      samples.push(sample);
    }
  }
 
  return samples;
}