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 | 128x | import VOILUTFunctionType from '../enums/VOILUTFunctionType';
import { logit } from './logit';
import * as windowLevelUtil from './windowLevel';
const Y_EPS = 1e-6;
export type ViewportVoiMappingProps = {
voiRange: { lower: number; upper: number };
VOILUTFunction?: string | VOILUTFunctionType;
/**
* When true the viewport renders the VOI inverted (e.g. PET AC), so the
* displayed intensity is `1 − mapped`. Both the forward and inverse maps must
* honor this or sampled display luma will inverse-map to the wrong raw end.
*/
invert?: boolean;
};
/**
* Maps a stored scalar to a normalized display intensity in [0, 1] using the same
* convention as VTK RGB transfer functions (linear) or DICOM sigmoid (sampled).
*/
export function mapScalarToViewportVoiIntensity(
value: number,
props: ViewportVoiMappingProps
): number {
const { lower, upper } = props.voiRange;
const span = upper - lower;
const fn = props.VOILUTFunction as string | undefined;
const applyInvert = (y: number) => (props.invert === true ? 1 - y : y);
if (fn === VOILUTFunctionType.SAMPLED_SIGMOID || fn === 'SIGMOID') {
const { windowCenter, windowWidth } = windowLevelUtil.toWindowLevel(
lower,
upper
);
const w = Math.max(Math.abs(windowWidth), 1e-12);
return applyInvert(1 / (1 + Math.exp((-4 * (value - windowCenter)) / w)));
}
if (span === 0 || !Number.isFinite(span)) {
return applyInvert(0);
}
return applyInvert(clamp01((value - lower) / span));
}
/**
* Inverse map: normalized intensity Y in (0, 1) back to stored scalar.
* Endpoints Y=0 and Y=1 map to lower and upper for linear modes.
*/
export function mapViewportVoiIntensityToScalar(
mapped01: number,
props: ViewportVoiMappingProps
): number {
const { lower, upper } = props.voiRange;
const fn = props.VOILUTFunction as string | undefined;
// Undo display inversion before mapping back to a stored scalar so the
// round-trip with mapScalarToViewportVoiIntensity is exact.
const y =
props.invert === true ? clamp01(1 - clamp01(mapped01)) : clamp01(mapped01);
if (fn === VOILUTFunctionType.SAMPLED_SIGMOID || fn === 'SIGMOID') {
const { windowCenter, windowWidth } = windowLevelUtil.toWindowLevel(
lower,
upper
);
const yy = clamp(y, Y_EPS, 1 - Y_EPS);
return logit(yy, windowCenter, windowWidth);
}
const span = upper - lower;
if (span === 0 || !Number.isFinite(span)) {
return lower;
}
return lower + y * span;
}
/**
* Converts a tolerance band in mapped [0,1] space to raw [rawMin, rawMax] (ordered).
*/
export function mapMappedBandToRawRange(
mappedMin: number,
mappedMax: number,
props: ViewportVoiMappingProps
): { rawMin: number; rawMax: number } {
const a = Math.min(mappedMin, mappedMax);
const b = Math.max(mappedMin, mappedMax);
const rawAtA = mapViewportVoiIntensityToScalar(a, props);
const rawAtB = mapViewportVoiIntensityToScalar(b, props);
return {
rawMin: Math.min(rawAtA, rawAtB),
rawMax: Math.max(rawAtA, rawAtB),
};
}
function clamp01(x: number): number {
if (!Number.isFinite(x)) {
return 0;
}
return clamp(x, 0, 1);
}
function clamp(x: number, lo: number, hi: number): number {
return Math.min(hi, Math.max(lo, x));
}
|