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