feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-04-09 23:57:34 -04:00
1 parent dc0b183dd4
commit d57e8bc157
200 files changed
+9589 -2178

No files matched your search

@@ -0,0 +1,31 @@
import React, { ReactElement } from 'react';
import PropTypes from 'prop-types';
import { ServicesManager } from '@ohif/core';
import { useViewportGrid } from '@ohif/ui';
import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel';
const ActiveViewportWindowLevel = ({
servicesManager,
}: {
servicesManager: ServicesManager;
}): ReactElement => {
const [viewportGrid] = useViewportGrid();
const { activeViewportId } = viewportGrid;
return (
<>
{activeViewportId && (
<ViewportWindowLevel
servicesManager={servicesManager}
viewportId={activeViewportId}
/>
)}
</>
);
};
ActiveViewportWindowLevel.propTypes = {
servicesManager: PropTypes.instanceOf(ServicesManager),
};
export default ActiveViewportWindowLevel;
@@ -0,0 +1 @@
export { default } from './ActiveViewportWindowLevel';
@@ -1,73 +1,108 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import { CinePlayer, useCine } from '@ohif/ui';
import { Enums, eventTarget } from '@cornerstonejs/core';
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
import { useAppConfig } from '@state';
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
const [{ isCineEnabled, cines }, api] = useCine();
const [{ isCineEnabled, cines }, cineService] = useCine();
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
const [dynamicInfo, setDynamicInfo] = useState(null);
const [appConfig] = useAppConfig();
const isMountedRef = useRef(null);
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const cineHandler = () => {
if (!cines || !cines[viewportId] || !enabledVPElement) {
if (!cines?.[viewportId] || !enabledVPElement) {
return;
}
const cine = cines[viewportId];
const isPlaying = cine.isPlaying || false;
const frameRate = cine.frameRate || 24;
const { isPlaying = false, frameRate = 24 } = cines[viewportId];
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
api.playClip(enabledVPElement, {
framesPerSecond: validFrameRate,
});
} else {
api.stopClip(enabledVPElement);
}
return isPlaying
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate })
: cineService.stopClip(enabledVPElement);
};
const newStackCineHandler = useCallback(() => {
const newDisplaySetHandler = useCallback(() => {
if (!enabledVPElement || !isCineEnabled) {
return;
}
const { viewports } = viewportGridService.getState();
const { displaySetInstanceUIDs } = viewports.get(viewportId);
let frameRate = 24;
let isPlaying = cines[viewportId].isPlaying;
let isPlaying = cines[viewportId]?.isPlaying || false;
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (displaySet.FrameRate) {
// displaySet.FrameRate corresponds to DICOM tag (0018,1063) which is defined as the the frame time in milliseconds
// So a bit of math to get the actual frame rate.
frameRate = Math.round(1000 / displaySet.FrameRate);
isPlaying ||= !!appConfig.autoPlayCine;
}
// check if the displaySet is dynamic and set the dynamic info
if (displaySet.isDynamicVolume) {
const { dynamicVolumeInfo } = displaySet;
const numTimePoints = dynamicVolumeInfo.timePoints.length;
const label = dynamicVolumeInfo.splittingTag;
const timePointIndex = dynamicVolumeInfo.timePointIndex || 0;
setDynamicInfo({
volumeId: displaySet.displaySetInstanceUID,
timePointIndex,
numTimePoints,
label,
});
} else {
setDynamicInfo(null);
}
});
if (isPlaying) {
api.setIsCineEnabled(isPlaying);
cineService.setIsCineEnabled(isPlaying);
}
api.setCine({ id: viewportId, isPlaying, frameRate });
cineService.setCine({ id: viewportId, isPlaying, frameRate });
setNewStackFrameRate(frameRate);
}, [displaySetService, viewportId, viewportGridService, cines]);
}, [displaySetService, viewportId, viewportGridService, cines, isCineEnabled]);
useEffect(() => {
isMountedRef.current = true;
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
newDisplaySetHandler();
return () => {
isMountedRef.current = false;
api.stopClip(enabledVPElement);
api.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
};
}, [enabledVPElement, newStackCineHandler]);
}, [isCineEnabled, newDisplaySetHandler]);
/**
* Use effect for handling new display set
*/
useEffect(() => {
if (!enabledVPElement) {
return;
}
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
// this doesn't makes sense that we are listening to this event on viewport element
enabledVPElement.addEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler
);
return () => {
cineService.setCine({ id: viewportId, isPlaying: false });
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
enabledVPElement.removeEventListener(
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
newDisplaySetHandler
);
};
}, [enabledVPElement, newDisplaySetHandler, viewportId]);
useEffect(() => {
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
@@ -77,41 +112,118 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
cineHandler();
return () => {
api.stopClip(enabledVPElement);
cineService.stopClip(enabledVPElement);
};
}, [cines, viewportId, enabledVPElement, cineHandler]);
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
if (!isCineEnabled) {
return null;
}
const cine = cines[viewportId];
const isPlaying = (cine && cine.isPlaying) || false;
const isPlaying = cine?.isPlaying || false;
return (
isCineEnabled && (
<CinePlayerComponent
className="absolute left-1/2 bottom-3 -translate-x-1/2"
frameRate={newStackFrameRate}
isPlaying={isPlaying}
onClose={() => {
// also stop the clip
api.setCine({
id: viewportId,
isPlaying: false,
});
api.setIsCineEnabled(false);
}}
onPlayPauseChange={isPlaying => {
api.setCine({
id: viewportId,
isPlaying,
});
}}
onFrameRateChange={frameRate =>
api.setCine({
id: viewportId,
frameRate,
})
}
/>
)
<RenderCinePlayer
viewportId={viewportId}
cineService={cineService}
newStackFrameRate={newStackFrameRate}
isPlaying={isPlaying}
dynamicInfo={dynamicInfo}
customizationService={customizationService}
/>
);
}
function RenderCinePlayer({
viewportId,
cineService,
newStackFrameRate,
isPlaying,
dynamicInfo: dynamicInfoProp,
customizationService,
}) {
const { component: CinePlayerComponent = CinePlayer } =
customizationService.get('cinePlayer') ?? {};
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
useEffect(() => {
setDynamicInfo(dynamicInfoProp);
}, [dynamicInfoProp]);
/**
* Use effect for handling 4D time index changed
*/
useEffect(() => {
if (!dynamicInfo) {
return;
}
const handleTimePointIndexChange = evt => {
const { volumeId, timePointIndex, numTimePoints, splittingTag } = evt.detail;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
};
eventTarget.addEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
return () => {
eventTarget.removeEventListener(
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
handleTimePointIndexChange
);
};
}, [dynamicInfo]);
useEffect(() => {
if (!dynamicInfo) {
return;
}
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
const volume = cache.getVolume(volumeId);
volume.timePointIndex = timePointIndex;
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
}, []);
const updateDynamicInfo = useCallback(props => {
const { volumeId, timePointIndex } = props;
const volume = cache.getVolume(volumeId);
volume.timePointIndex = timePointIndex;
}, []);
return (
<CinePlayerComponent
className="absolute left-1/2 bottom-3 -translate-x-1/2"
frameRate={newStackFrameRate}
isPlaying={isPlaying}
onClose={() => {
// also stop the clip
cineService.setCine({
id: viewportId,
isPlaying: false,
});
cineService.setIsCineEnabled(false);
}}
onPlayPauseChange={isPlaying => {
cineService.setCine({
id: viewportId,
isPlaying,
});
}}
onFrameRateChange={frameRate =>
cineService.setCine({
id: viewportId,
frameRate,
})
}
dynamicInfo={dynamicInfo}
updateDynamicInfo={updateDynamicInfo}
/>
);
}
@@ -0,0 +1,387 @@
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash.debounce';
import { ServicesManager } from '@ohif/core';
import { PanelSection, WindowLevel } from '@ohif/ui';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
const { Events } = Enums;
const ViewportWindowLevel = ({
servicesManager,
viewportId,
}: {
servicesManager: ServicesManager;
viewportId: string;
}): ReactElement => {
const { cornerstoneViewportService } = servicesManager.services;
const [windowLevels, setWindowLevels] = useState([]);
const [cachedHistograms, setCachedHistograms] = useState({});
/**
* Looks for all viewports that has exactly all volumeIds passed as parameter.
*/
const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports();
return viewports.filter(vp => {
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
return (
volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
);
});
},
[cornerstoneViewportService]
);
const getNodeOpacity = (volumeActor, nodeIndex) => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const nodeValue = [];
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
return nodeValue[1];
};
/**
* Checks if the opacity applied to the PET volume is something like
* [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
* constant opacity value for all x's greater than 0.1
*/
const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume) {
return false;
}
const modality = volume.metadata.Modality;
if (modality !== 'PT') {
return false;
}
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
// It must have at least two points (0 and 0.1)
if (volumeOpacity.getSize() < 2) {
return false;
}
const node1Value = [];
const node2Value = [];
volumeOpacity.getNodeValue(0, node1Value);
volumeOpacity.getNodeValue(1, node2Value);
// First node must be (x:0, y:0} and the second one {x:0.1, y:any}
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
return false;
}
const expectedOpacity = node2Value[1];
const opacitySize = volumeOpacity.getSize();
const currentNodeValue = [];
// Any point after 0.1 must have the same opacity
for (let i = 2; i < opacitySize; i++) {
volumeOpacity.getNodeValue(i, currentNodeValue);
if (currentNodeValue[1] !== expectedOpacity) {
return false;
}
}
return true;
};
/**
* Checks if the opacity function has a constance opacity value for all x's
*/
const isVolumeWithConstantOpacity = volumeActor => {
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
const opacitySize = volumeOpacity.getSize();
const firstNodeValue = [];
volumeOpacity.getNodeValue(0, firstNodeValue);
const firstNodeOpacity = firstNodeValue[1];
for (let i = 0; i < opacitySize; i++) {
const currentNodeValue = [];
volumeOpacity.getNodeValue(0, currentNodeValue);
if (currentNodeValue[1] !== firstNodeOpacity) {
return false;
}
}
return true;
};
const getVolumeOpacity = useCallback((viewport, volumeId) => {
const volumeActor = viewport.getActor(volumeId).actor;
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
// Get the opacity from the second node at 0.1
return getNodeOpacity(volumeActor, 1);
} else if (isVolumeWithConstantOpacity(volumeActor)) {
return getNodeOpacity(volumeActor, 0);
}
return undefined;
}, []);
const getWindowLevelsData = useCallback(
async (viewportId: number) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return [];
}
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const volumeIds = viewport.getActors().map(actor => actor.uid);
const viewportProperties = viewport.getProperties();
const { voiRange } = viewportProperties;
const viewportVoi = voiRange
? {
windowWidth: voiRange.upper - voiRange.lower,
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
}
: undefined;
const windowLevels = await Promise.all(
volumeIds.map(async (volumeId, volumeIndex) => {
const volume = cs3DCache.getVolume(volumeId);
if (!volume) {
return null;
}
const opacity = getVolumeOpacity(viewport, volumeId);
const { metadata, scaling } = volume;
const modality = metadata.Modality;
// TODO: find a proper way to fix the histogram
const options = {
min: modality === 'PT' ? 0.1 : -999,
max: modality === 'PT' ? 5 : 10000,
};
const histogram =
cachedHistograms[volumeId] ??
(await getViewportVolumeHistogram(viewport, volume, options));
const { voi: displaySetVOI, colormap: displaySetColormap } =
viewportInfo.displaySetOptions[volumeIndex];
let colormap;
if (displaySetColormap) {
colormap =
csUtils.colormap.getColormap(displaySetColormap.name) ??
vtkColorMaps.getPresetByName(displaySetColormap.name);
}
const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
return {
viewportId,
modality,
volumeId,
volumeIndex,
voi,
histogram,
colormap,
step: scaling?.PT ? 0.05 : 1,
opacity,
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
};
})
);
const data = windowLevels.filter(Boolean);
return data;
},
[cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
);
const updateViewportHistograms = useCallback(() => {
getWindowLevelsData(viewportId).then(windowLevels => {
setWindowLevels(windowLevels);
});
}, [viewportId, getWindowLevelsData]);
const handleCornerstoneVOIModified = useCallback(
e => {
const { detail } = e;
const { volumeId, range } = detail;
const oldWindowLevel = windowLevels.find(wl => wl.volumeId === volumeId);
if (!oldWindowLevel) {
return;
}
const oldVOI = oldWindowLevel.voi;
const windowWidth = range.upper - range.lower;
const windowCenter = range.lower + windowWidth / 2;
if (windowWidth === oldVOI.windowWidth && windowCenter === oldVOI.windowCenter) {
return;
}
const newWindowLevel = {
...oldWindowLevel,
voi: {
windowWidth,
windowCenter,
},
};
setWindowLevels(
windowLevels.map(windowLevel =>
windowLevel === oldWindowLevel ? newWindowLevel : windowLevel
)
);
},
[windowLevels]
);
const debouncedHandleCornerstoneVOIModified = useCallback(
debounce(handleCornerstoneVOIModified, 100),
[handleCornerstoneVOIModified]
);
const handleVOIChange = useCallback(
(volumeId, voi) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const newRange = {
lower: voi.windowCenter - voi.windowWidth / 2,
upper: voi.windowCenter + voi.windowWidth / 2,
};
viewport.setProperties({ voiRange: newRange }, volumeId);
viewport.render();
},
[cornerstoneViewportService, viewportId]
);
const handleOpacityChange = useCallback(
(viewportId, _volumeIndex, volumeId, opacity) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport) {
return;
}
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => {
vp.setProperties({ colormap: { opacity } }, volumeId);
vp.render();
});
},
[getViewportsWithVolumeIds, cornerstoneViewportService]
);
// Listen to windowLevels changes and caches all the new ones
useEffect(() => {
const newVolumeHistograms = windowLevels
.filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram)
.reduce((volumeHistograms, windowLevel) => {
// If the histogram exists, add it to the volumeHistograms object
if (windowLevel?.histogram) {
volumeHistograms[windowLevel.volumeId] = windowLevel.histogram;
}
return volumeHistograms;
}, {});
if (Object.keys(newVolumeHistograms).length) {
setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms }));
}
}, [windowLevels, cachedHistograms]);
// Updates the histogram when the viewport index prop has changed
useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]);
// Listen to cornerstone events on "eventTarget" and at the document level
useEffect(() => {
eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms);
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
return () => {
eventTarget.removeEventListener(
Events.IMAGE_VOLUME_LOADING_COMPLETED,
updateViewportHistograms
);
document.removeEventListener(
Events.VOI_MODIFIED,
debouncedHandleCornerstoneVOIModified,
true
);
};
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
// Updates the viewport when the context of the viewport has changed. This is
// necessary when moving across different stages because the viewport index
// may not change but the volumes loaded on it may change.
useEffect(() => {
const { unsubscribe } = cornerstoneViewportService.subscribe(
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
({ viewportInfo }) => {
if (viewportInfo.viewportId === viewportId) {
updateViewportHistograms();
}
}
);
return () => {
unsubscribe();
};
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
return (
<PanelSection title="Window Level">
{windowLevels.map((windowLevel, i) => {
if (!windowLevel.histogram) {
return null;
}
return (
<WindowLevel
key={windowLevel.volumeId}
title={`${windowLevel.modality}`}
histogram={windowLevel.histogram}
voi={windowLevel.voi}
step={windowLevel.step}
showOpacitySlider={windowLevel.showOpacitySlider}
colormap={windowLevel.colormap}
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
opacity={windowLevel.opacity}
onOpacityChange={opacity =>
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
}
/>
);
})}
</PanelSection>
);
};
ViewportWindowLevel.propTypes = {
servicesManager: PropTypes.instanceOf(ServicesManager),
viewportId: PropTypes.string.isRequired,
};
export default ViewportWindowLevel;
@@ -0,0 +1,70 @@
import { getWebWorkerManager } from '@cornerstonejs/core';
const workerManager = getWebWorkerManager();
const options = {
// maxWorkerInstances: 1,
// overwrite: false
autoTerminationOnIdle: 1000,
};
// Register the task
const workerFn = () => {
return new Worker(new URL('./histogramWorker.js', import.meta.url), {
name: 'histogram-worker', // name used by the browser to name the worker
});
};
workerManager.registerWorker('histogram-worker', workerFn, options);
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
if (!volume?.loadStatus.loaded) {
return undefined;
}
const volumeImageData = viewport.getImageData(volume.volumeId);
if (!volumeImageData) {
return undefined;
}
let scalarData = volume.scalarData;
let prevTimePoint;
if (volume.numTimePoints > 1) {
prevTimePoint = volume.timePointIndex;
const middleTimePoint = Math.round(volume.numTimePoints / 2);
volume.timePointIndex = middleTimePoint;
scalarData = volume.getScalarData(middleTimePoint);
}
const { dimensions, origin, direction, spacing } = volume;
const range = await workerManager.executeTask('histogram-worker', 'getRange', {
dimensions,
origin,
direction,
spacing,
scalarData,
});
// after we calculate the range let's reset the timePointIndex
if (volume.numTimePoints > 1) {
volume.timePointIndex = prevTimePoint;
}
const { minimum: min, maximum: max } = range;
const calcHistOptions = {
numBins: 256,
min: Math.max(min, options?.min ?? min),
max: Math.min(max, options?.max ?? max),
};
const histogram = await workerManager.executeTask('histogram-worker', 'calcHistogram', {
data: scalarData,
options: calcHistOptions,
});
return histogram;
};
export { getViewportVolumeHistogram };
@@ -0,0 +1,97 @@
import { expose } from 'comlink';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
/**
* This object simulates a heavy task by implementing a sleep function and a recursive Fibonacci function.
* It's used for testing or demonstrating purposes where a heavy or time-consuming task is needed.
*/
const obj = {
getRange: ({ dimensions, origin, direction, spacing, scalarData }) => {
const imageData = vtkImageData.newInstance();
imageData.setDimensions(dimensions);
imageData.setOrigin(origin);
imageData.setDirection(direction);
imageData.setSpacing(spacing);
const scalarArray = vtkDataArray.newInstance({
name: 'Pixels',
numberOfComponents: 1,
values: scalarData,
});
imageData.getPointData().setScalars(scalarArray);
imageData.modified();
const range = imageData.computeHistogram(imageData.getBounds());
return range;
},
calcHistogram: ({ data, options }) => {
if (options === undefined) {
options = {};
}
const histogram = {
numBins: options.numBins || 256,
range: { min: 0, max: 0 },
bins: new Int32Array(1),
maxBin: 0,
maxBinValue: 0,
};
let minToUse = options.min;
let maxToUse = options.max;
if (minToUse === undefined || maxToUse === undefined) {
let min = Infinity;
let max = -Infinity;
let index = data.length;
while (index--) {
const value = data[index];
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}
minToUse = min;
maxToUse = max;
}
histogram.range = { min: minToUse, max: maxToUse };
const bins = new Int32Array(histogram.numBins);
const binScale = histogram.numBins / (maxToUse - minToUse);
for (let index = 0; index < data.length; index++) {
const value = data[index];
if (value < minToUse) {
continue;
}
if (value > maxToUse) {
continue;
}
const bin = Math.floor((value - minToUse) * binScale);
bins[bin] += 1;
}
histogram.bins = bins;
histogram.maxBin = 0;
histogram.maxBinValue = 0;
for (let bin = 0; bin < histogram.numBins; bin++) {
if (histogram.bins[bin] > histogram.maxBinValue) {
histogram.maxBin = bin;
histogram.maxBinValue = histogram.bins[bin];
}
}
return histogram;
},
};
expose(obj);
@@ -0,0 +1 @@
export { default } from './ViewportWindowLevel';