perf(segmentation): make segmentation read 3x faster (#3577)

This commit is contained in:
Alireza 2023-08-08 14:39:31 -04:00 committed by GitHub
parent ec76ea24dd
commit 156c1ba60b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 187 additions and 315 deletions

View File

@ -1,11 +1,14 @@
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import {
metaData,
cache,
triggerEvent,
eventTarget,
} from '@cornerstonejs/core';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id'; import { SOPClassHandlerId } from './id';
import dcmjs from 'dcmjs'; import { dicomlabToRGB } from './utils/dicomlabToRGB';
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4']; const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
@ -122,13 +125,12 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
!segDisplaySet.segments || !segDisplaySet.segments ||
Object.keys(segDisplaySet.segments).length === 0 Object.keys(segDisplaySet.segments).length === 0
) { ) {
const segments = await _loadSegments( await _loadSegments({
extensionManager, extensionManager,
servicesManager,
segDisplaySet, segDisplaySet,
headers headers,
); });
segDisplaySet.segments = segments;
} }
const suppressEvents = true; const suppressEvents = true;
@ -147,28 +149,65 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
return loadPromises[SOPInstanceUID]; return loadPromises[SOPInstanceUID];
} }
async function _loadSegments(extensionManager, segDisplaySet, headers) { async function _loadSegments({
extensionManager,
servicesManager,
segDisplaySet,
headers,
}) {
const utilityModule = extensionManager.getModuleEntry( const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common' '@ohif/extension-cornerstone.utilityModule.common'
); );
const { segmentationService } = servicesManager.services;
const { dicomLoaderService } = utilityModule.exports; const { dicomLoaderService } = utilityModule.exports;
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise( const arrayBuffer = await dicomLoaderService.findDicomDataPromise(
segDisplaySet, segDisplaySet,
null, null,
headers headers
); );
const dicomData = DicomMessage.readFile(segArrayBuffer); const cachedReferencedVolume = cache.getVolume(
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict); segDisplaySet.referencedVolumeId
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta); );
if (!Array.isArray(dataset.SegmentSequence)) { if (!cachedReferencedVolume) {
dataset.SegmentSequence = [dataset.SegmentSequence]; throw new Error(
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
);
} }
const segments = _getSegments(dataset); const { imageIds } = cachedReferencedVolume;
return segments;
// Todo: what should be defaults here
const tolerance = 0.001;
const skipOverlapping = true;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
{
percentComplete,
}
);
});
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
imageIds,
arrayBuffer,
metaData,
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
);
results.segMetadata.data.forEach((data, i) => {
if (i > 0) {
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
}
});
Object.assign(segDisplaySet, results);
} }
function _segmentationExists(segDisplaySet, segmentationService) { function _segmentationExists(segDisplaySet, segmentationService) {
@ -178,114 +217,6 @@ function _segmentationExists(segDisplaySet, segmentationService) {
); );
} }
function _getPixelData(dataset, segments) {
let frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
let nextOffset = 0;
Object.keys(segments).forEach(segmentKey => {
const segment = segments[segmentKey];
segment.numberOfFrames = segment.functionalGroups.length;
segment.size = segment.numberOfFrames * frameSize;
segment.offset = nextOffset;
nextOffset = segment.offset + segment.size;
const packedSegment = dataset.PixelData[0].slice(
segment.offset,
nextOffset
);
segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
segment.geometry = geometryFromFunctionalGroups(
dataset,
segment.functionalGroups
);
});
return segments;
}
function geometryFromFunctionalGroups(dataset, perFrame) {
let pixelMeasures =
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
let planeOrientation =
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;
let planePosition = perFrame[0].PlanePositionSequence; // TODO: assume sorted frames!
const geometry = {};
// NB: DICOM PixelSpacing is defined as Row then Column,
// unlike ImageOrientationPatient
let spacingBetweenSlices = pixelMeasures.SpacingBetweenSlices;
if (!spacingBetweenSlices) {
if (pixelMeasures.SliceThickness) {
console.log('Using SliceThickness as SpacingBetweenSlices');
spacingBetweenSlices = pixelMeasures.SliceThickness;
}
}
geometry.spacing = [
pixelMeasures.PixelSpacing[1],
pixelMeasures.PixelSpacing[0],
spacingBetweenSlices,
].map(Number);
geometry.dimensions = [dataset.Columns, dataset.Rows, perFrame.length].map(
Number
);
let orientation = planeOrientation.ImageOrientationPatient.map(Number);
const columnStepToPatient = orientation.slice(0, 3);
const rowStepToPatient = orientation.slice(3, 6);
geometry.planeNormal = [];
vtkMath.cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);
let firstPosition = perFrame[0].PlanePositionSequence.ImagePositionPatient.map(
Number
);
let lastPosition = perFrame[
perFrame.length - 1
].PlanePositionSequence.ImagePositionPatient.map(Number);
geometry.sliceStep = [];
vtkMath.subtract(lastPosition, firstPosition, geometry.sliceStep);
vtkMath.normalize(geometry.sliceStep);
geometry.direction = columnStepToPatient
.concat(rowStepToPatient)
.concat(geometry.sliceStep);
geometry.origin = planePosition.ImagePositionPatient.map(Number);
return geometry;
}
function _getSegments(dataset) {
const segments = {};
dataset.SegmentSequence.forEach(segment => {
const cielab = segment.RecommendedDisplayCIELabValue;
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
rgba.push(255);
const segmentNumber = segment.SegmentNumber;
segments[segmentNumber] = {
color: rgba,
functionalGroups: [],
offset: null,
size: null,
pixelData: null,
label: segment.SegmentLabel,
};
});
// make a list of functional groups per segment
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
const segmentNumber =
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
segments[segmentNumber].functionalGroups.push(functionalGroup);
});
return _getPixelData(dataset, segments);
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) { function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => { const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries( return _getDisplaySetsFromSeries(

View File

@ -0,0 +1,16 @@
import dcmjs from 'dcmjs';
/**
* Converts a CIELAB color to an RGB color using the dcmjs library.
* @param cielab - The CIELAB color to convert.
* @returns The RGB color as an array of three integers between 0 and 255.
*/
function dicomlabToRGB(cielab: number[]): number[] {
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
Math.round(x * 255)
);
return rgb;
}
export { dicomlabToRGB };

View File

@ -317,7 +317,6 @@ function OHIFCornerstoneSEGViewport(props) {
setIsHydrated(isHydrated); setIsHydrated(isHydrated);
}; };
return ( return (
<> <>
<ViewportActionBar <ViewportActionBar
@ -344,10 +343,13 @@ function OHIFCornerstoneSEGViewport(props) {
patientSex: PatientSex || '', patientSex: PatientSex || '',
patientAge: PatientAge || '', patientAge: PatientAge || '',
MRN: PatientID || '', MRN: PatientID || '',
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '', thickness: SliceThickness
? utils.roundNumber(SliceThickness, 2)
: '',
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
spacing: spacing:
SpacingBetweenSlices !== undefined SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm` ? utils.roundNumber(SpacingBetweenSlices, 2)
: '', : '',
scanner: ManufacturerModelName || '', scanner: ManufacturerModelName || '',
}, },

View File

@ -44,9 +44,9 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.7.1", "@cornerstonejs/adapters": "^1.9.3",
"@cornerstonejs/core": "^1.7.1", "@cornerstonejs/core": "^1.9.3",
"@cornerstonejs/tools": "^1.7.1", "@cornerstonejs/tools": "^1.9.3",
"classnames": "^2.3.2" "classnames": "^2.3.2"
} }
} }

View File

@ -36,7 +36,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2", "@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.7.1", "@cornerstonejs/dicom-image-loader": "^1.9.3",
"@ohif/core": "3.7.0-beta.42", "@ohif/core": "3.7.0-beta.42",
"@ohif/ui": "3.7.0-beta.42", "@ohif/ui": "3.7.0-beta.42",
"dcmjs": "^0.29.6", "dcmjs": "^0.29.6",
@ -52,10 +52,10 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.13", "@babel/runtime": "^7.20.13",
"@cornerstonejs/adapters": "^1.7.1", "@cornerstonejs/adapters": "^1.9.3",
"@cornerstonejs/core": "^1.7.1", "@cornerstonejs/core": "^1.9.3",
"@cornerstonejs/streaming-image-volume-loader": "^1.7.1", "@cornerstonejs/streaming-image-volume-loader": "^1.9.3",
"@cornerstonejs/tools": "^1.7.1", "@cornerstonejs/tools": "^1.9.3",
"@kitware/vtk.js": "27.3.1", "@kitware/vtk.js": "27.3.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",

View File

@ -563,11 +563,11 @@ class SegmentationService extends PubSubService {
); );
} }
const { segments, referencedVolumeId } = segDisplaySet; const { labelmapBufferArray, referencedVolumeId } = segDisplaySet;
if (!segments || !referencedVolumeId) { if (!labelmapBufferArray || !referencedVolumeId) {
throw new Error( throw new Error(
'To create the segmentation from SEG displaySet, the displaySet should be loaded first, you can perform segDisplaySet.load() before calling this method.' 'No labelmapBufferArray or referencedVolumeId found for the SEG displaySet'
); );
} }
@ -593,88 +593,27 @@ class SegmentationService extends PubSubService {
}, },
} }
); );
const [rows, columns] = derivedVolume.dimensions;
const derivedVolumeScalarData = derivedVolume.getScalarData(); const derivedVolumeScalarData = derivedVolume.getScalarData();
const { imageIds } = referencedVolume; const segmentsInfo = segDisplaySet.segMetadata.data;
const sopUIDImageIdIndexMap = imageIds.reduce((acc, imageId, index) => { derivedVolumeScalarData.set(new Uint8Array(labelmapBufferArray[0]));
const { sopInstanceUid } = metaData.get('generalImageModule', imageId);
acc[sopInstanceUid] = index;
return acc;
}, {} as { [sopUID: string]: number });
const numSegments = Object.keys(segments).length; segmentation.segments = segmentsInfo.map((segmentInfo, segmentIndex) => {
// Note: ideally we could use the TypedArray set method, but since each if (segmentIndex === 0) {
// slice can have multiple segments, we need to loop over each slice and return;
// set the segment value for each segment.
let overlappingSegments = false;
const _segmentInfoUpdate = (segmentInfo, segmentIndex) => {
const { pixelData: segPixelData } = segmentInfo;
let segmentX = 0;
let segmentY = 0;
let segmentZ = 0;
let count = 0;
for (const [
functionalGroupIndex,
functionalGroup,
] of segmentInfo.functionalGroups.entries()) {
const {
ReferencedSOPInstanceUID,
} = functionalGroup.DerivationImageSequence.SourceImageSequence;
const imageIdIndex = sopUIDImageIdIndexMap[ReferencedSOPInstanceUID];
if (imageIdIndex === -1) {
return;
}
const step = rows * columns;
// we need a faster way to get the pixel data for the current
// functional group, which we use typed array view
const functionGroupPixelData = new Uint8Array(
segPixelData.buffer,
functionalGroupIndex * step,
step
);
const functionalGroupStartIndex = imageIdIndex * step;
const functionalGroupEndIndex = (imageIdIndex + 1) * step;
// Note: this for loop is not optimized, since DICOM SEG stores
// each segment as a separate labelmap so if there is a slice
// that has multiple segments, we will have to loop over each
// segment and we cannot use the TypedArray set method.
for (
let i = functionalGroupStartIndex, j = 0;
i < functionalGroupEndIndex;
i++, j++
) {
if (functionGroupPixelData[j] !== 0) {
if (derivedVolumeScalarData[i] !== 0) {
overlappingSegments = true;
}
derivedVolumeScalarData[i] = segmentIndex;
// centroid calculations
segmentX += i % columns;
segmentY += Math.floor(i / columns) % rows;
segmentZ += Math.floor(i / (columns * rows));
count++;
}
}
} }
// centroid calculations const {
const x = Math.floor(segmentX / count); SegmentedPropertyCategoryCodeSequence,
const y = Math.floor(segmentY / count); SegmentNumber,
const z = Math.floor(segmentZ / count); SegmentLabel,
SegmentAlgorithmType,
SegmentAlgorithmName,
SegmentedPropertyTypeCodeSequence,
rgba,
} = segmentInfo;
const { x, y, z } = segDisplaySet.centroids.get(segmentIndex);
const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]); const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]);
segmentation.cachedStats = { segmentation.cachedStats = {
@ -691,65 +630,31 @@ class SegmentationService extends PubSubService {
}, },
}; };
const numInitialized = Object.keys(segmentation.cachedStats.segmentCenter) return {
.length; label: SegmentLabel || `Segment ${SegmentNumber}`,
segmentIndex: Number(SegmentNumber),
// Calculate percentage completed category: SegmentedPropertyCategoryCodeSequence
const percentComplete = Math.round((numInitialized / numSegments) * 100); ? SegmentedPropertyCategoryCodeSequence.CodeMeaning
: '',
this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, { type: SegmentedPropertyTypeCodeSequence
percentComplete, ? SegmentedPropertyTypeCodeSequence.CodeMeaning
numSegments: numSegments, : '',
}); algorithmType: SegmentAlgorithmType,
}; algorithmName: SegmentAlgorithmName,
color: rgba,
const promiseArray = []; opacity: 255,
for (const segmentIndex in segments) {
const segmentInfo = segments[segmentIndex];
// Important: we need a non-blocking way to update the segmentation
// state, otherwise the UI will freeze and the user will not be able
// to interact with the app or progress bars will not be updated.
const promise = new Promise<void>((resolve, reject) => {
setTimeout(() => {
_segmentInfoUpdate(segmentInfo, segmentIndex);
resolve();
}, 0);
});
promiseArray.push(promise);
}
await Promise.all(promiseArray);
segmentation.segmentCount = Object.keys(segments).length;
segmentation.segments = [null]; // segment 0
Object.keys(segments).forEach(segmentIndex => {
const segmentInfo = segments[segmentIndex];
const segIndex = Number(segmentIndex);
segmentation.segments[segIndex] = {
label: segmentInfo.label || `Segment ${segIndex}`,
segmentIndex: Number(segmentIndex),
color: [
segmentInfo.color[0],
segmentInfo.color[1],
segmentInfo.color[2],
],
opacity: segmentInfo.color[3],
isVisible: true, isVisible: true,
isLocked: false, isLocked: false,
}; };
}); });
segmentation.segmentCount = segmentsInfo.length - 1;
segDisplaySet.isLoaded = true; segDisplaySet.isLoaded = true;
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
segmentationId, segmentationId,
segDisplaySet, segDisplaySet,
overlappingSegments,
}); });
return this.addOrUpdateSegmentation(segmentation, suppressEvents); return this.addOrUpdateSegmentation(segmentation, suppressEvents);

View File

@ -30,8 +30,8 @@
"start": "yarn run dev" "start": "yarn run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@cornerstonejs/core": "^1.7.1", "@cornerstonejs/core": "^1.9.3",
"@cornerstonejs/tools": "^1.7.1", "@cornerstonejs/tools": "^1.9.3",
"@ohif/core": "3.7.0-beta.42", "@ohif/core": "3.7.0-beta.42",
"@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.42", "@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.42",
"@ohif/ui": "3.7.0-beta.42", "@ohif/ui": "3.7.0-beta.42",

View File

@ -30,7 +30,6 @@
"dev:dcm4chee": "cross-env NODE_ENV=development APP_CONFIG=config/local_dcm4chee.js webpack serve --config .webpack/webpack.pwa.js", "dev:dcm4chee": "cross-env NODE_ENV=development APP_CONFIG=config/local_dcm4chee.js webpack serve --config .webpack/webpack.pwa.js",
"dev:static": "cross-env NODE_ENV=development APP_CONFIG=config/local_static.js webpack serve --config .webpack/webpack.pwa.js", "dev:static": "cross-env NODE_ENV=development APP_CONFIG=config/local_static.js webpack serve --config .webpack/webpack.pwa.js",
"dev:viewer": "yarn run dev", "dev:viewer": "yarn run dev",
"preinstall": "node preinstall.js",
"start": "yarn run dev", "start": "yarn run dev",
"test:e2e": "cypress open", "test:e2e": "cypress open",
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'", "test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
@ -51,7 +50,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2", "@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.7.1", "@cornerstonejs/dicom-image-loader": "^1.9.3",
"@ohif/core": "3.7.0-beta.42", "@ohif/core": "3.7.0-beta.42",
"@ohif/extension-cornerstone": "3.7.0-beta.42", "@ohif/extension-cornerstone": "3.7.0-beta.42",
"@ohif/extension-cornerstone-dicom-rt": "3.7.0-beta.42", "@ohif/extension-cornerstone-dicom-rt": "3.7.0-beta.42",

View File

@ -35,7 +35,7 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
"@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2",
"@cornerstonejs/codec-openjph": "^2.4.2", "@cornerstonejs/codec-openjph": "^2.4.2",
"@cornerstonejs/dicom-image-loader": "^1.7.1", "@cornerstonejs/dicom-image-loader": "^1.9.3",
"@ohif/ui": "3.7.0-beta.42", "@ohif/ui": "3.7.0-beta.42",
"cornerstone-math": "0.1.9", "cornerstone-math": "0.1.9",
"dicom-parser": "^1.8.21" "dicom-parser": "^1.8.21"

View File

@ -70,6 +70,11 @@ class MetadataProvider {
SeriesInstanceUID, SeriesInstanceUID,
SOPInstanceUID SOPInstanceUID
); );
if (!instance) {
return;
}
return ( return (
(frameNumber && combineFrameInstance(frameNumber, instance)) || instance (frameNumber && combineFrameInstance(frameNumber, instance)) || instance
); );

View File

@ -27,6 +27,7 @@ export type ExtensionConfiguration = Record<string, unknown>;
*/ */
export interface ExtensionParams extends ExtensionConstructor { export interface ExtensionParams extends ExtensionConstructor {
extensionManager: ExtensionManager; extensionManager: ExtensionManager;
servicesManager: ServicesManager;
configuration?: ExtensionConfiguration; configuration?: ExtensionConfiguration;
} }
@ -43,6 +44,7 @@ export interface Extension {
getViewportModule?: (p: ExtensionParams) => unknown; getViewportModule?: (p: ExtensionParams) => unknown;
getUtilityModule?: (p: ExtensionParams) => unknown; getUtilityModule?: (p: ExtensionParams) => unknown;
getCustomizationModule?: (p: ExtensionParams) => unknown; getCustomizationModule?: (p: ExtensionParams) => unknown;
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
onModeEnter?: () => void; onModeEnter?: () => void;
onModeExit?: () => void; onModeExit?: () => void;
} }

View File

@ -1,3 +1,4 @@
import { ExtensionManager } from '../../extensions';
import { InstanceMetadata } from '../../types'; import { InstanceMetadata } from '../../types';
import { PubSubService } from '../_shared/pubSubServiceInterface'; import { PubSubService } from '../_shared/pubSubServiceInterface';
import EVENTS from './EVENTS'; import EVENTS from './EVENTS';
@ -47,6 +48,7 @@ export default class DisplaySetService extends PubSubService {
}; };
public activeDisplaySets = []; public activeDisplaySets = [];
extensionManager: ExtensionManager;
protected activeDisplaySetsMap = new Map<string, DisplaySet>(); protected activeDisplaySetsMap = new Map<string, DisplaySet>();

View File

@ -161,3 +161,9 @@ devServer: {
} }
} }
``` ```
:::tip
Take a look at how other people have integrated OHIF in their react app
example1: https://github.com/OHIF/Viewers/issues/3371#issuecomment-1630405255
:::

View File

@ -21,26 +21,23 @@ function LoadingIndicatorTotalPercent({
loadingText = 'Loading...', loadingText = 'Loading...',
targetText = 'segments', targetText = 'segments',
}: Props): JSX.Element { }: Props): JSX.Element {
percentComplete = percentComplete !== null ? percentComplete : null; const progress = percentComplete;
const progress = percentComplete !== null ? percentComplete : null;
const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : ''; const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : '';
const numTargetsLoadedText = const numTargetsLoadedText =
percentComplete !== null percentComplete !== null
? Math.floor((percentComplete * totalNumbers) / 100) ? Math.floor((percentComplete * totalNumbers) / 100)
: ''; : '';
const textBlock = !totalNumbers ? ( const textBlock =
<div className="text-white text-sm">{loadingText}</div> !totalNumbers && percentComplete === null ? (
) : ( <div className="text-white text-sm">{loadingText}</div>
<div className="text-white text-sm flex items-baseline space-x-1"> ) : !totalNumbers && percentComplete !== null ? (
<div>Loaded</div> <div className="text-white text-sm">Loaded {percentComplete}%</div>
<div>{numTargetsLoadedText}</div> ) : (
<div>of</div> <div className="text-white text-sm">
<div>{totalNumbersText}</div> Loaded {numTargetsLoadedText} of {totalNumbersText} {targetText}
<div>{targetText}</div> </div>
</div> );
);
return ( return (
<LoadingIndicatorProgress <LoadingIndicatorProgress

View File

@ -19,6 +19,7 @@ function PatientInfo({
patientAge, patientAge,
MRN, MRN,
thickness, thickness,
thicknessUnits,
spacing, spacing,
scanner, scanner,
isOpen, isOpen,
@ -29,7 +30,6 @@ function PatientInfo({
while (patientAge.charAt(0) === '0') { while (patientAge.charAt(0) === '0') {
patientAge = patientAge.substr(1); patientAge = patientAge.substr(1);
} }
return ( return (
<div ref={showPatientInfoRef}> <div ref={showPatientInfoRef}>
<Tooltip <Tooltip
@ -90,7 +90,9 @@ function PatientInfo({
className={classnames(classes.infoText)} className={classnames(classes.infoText)}
title={thickness} title={thickness}
> >
{thickness} {thicknessUnits
? `${thickness}${thicknessUnits}`
: `${thickness}`}
</span> </span>
</div> </div>
<div className={classnames(classes.row)}> <div className={classnames(classes.row)}>
@ -136,6 +138,7 @@ PatientInfo.propTypes = {
patientAge: PropTypes.string, patientAge: PropTypes.string,
MRN: PropTypes.string, MRN: PropTypes.string,
thickness: PropTypes.string, thickness: PropTypes.string,
thicknessUnits: PropTypes.string,
spacing: PropTypes.string, spacing: PropTypes.string,
scanner: PropTypes.string, scanner: PropTypes.string,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,

View File

@ -34,6 +34,7 @@ const ViewportActionBar = ({
patientAge, patientAge,
MRN, MRN,
thickness, thickness,
thicknessUnits,
spacing, spacing,
scanner, scanner,
} = patientInformation; } = patientInformation;
@ -169,6 +170,7 @@ const ViewportActionBar = ({
patientAge={patientAge} patientAge={patientAge}
MRN={MRN} MRN={MRN}
thickness={thickness} thickness={thickness}
thicknessUnits={thicknessUnits}
spacing={spacing} spacing={spacing}
scanner={scanner} scanner={scanner}
/> />
@ -192,6 +194,7 @@ ViewportActionBar.propTypes = {
patientAge: PropTypes.string.isRequired, patientAge: PropTypes.string.isRequired,
MRN: PropTypes.string.isRequired, MRN: PropTypes.string.isRequired,
thickness: PropTypes.string.isRequired, thickness: PropTypes.string.isRequired,
thicknessUnits: PropTypes.string.isRequired,
spacing: PropTypes.string.isRequired, spacing: PropTypes.string.isRequired,
scanner: PropTypes.string.isRequired, scanner: PropTypes.string.isRequired,
}), }),

View File

@ -1321,9 +1321,9 @@
regenerator-runtime "^0.13.11" regenerator-runtime "^0.13.11"
"@babel/runtime@7.17.9", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": "@babel/runtime@7.17.9", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.22.6" version "7.22.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
dependencies: dependencies:
regenerator-runtime "^0.13.11" regenerator-runtime "^0.13.11"
@ -1376,13 +1376,14 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cornerstonejs/adapters@^1.7.1": "@cornerstonejs/adapters@^1.9.3":
version "1.7.1" version "1.9.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.7.1.tgz#5ae8b510791f5817420ade4b5637f9e84a8ada52" resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.9.3.tgz#b1031e26af175ac15c3c975aff89f51c214ec092"
integrity sha512-iwowMUVoNKWZ3PTmYSjnZ2vDThR+OUkvIsSB913A1YhPCl+FVv2Y0JLdueFCYKlTFLnzFa8FOlFwzAOI+AAB+Q== integrity sha512-bjpUq78yupE3+z5qNAS7j27OgCn1BAJQKwcQNY+PBUzTojwg2KpMpG69I+CucSCAxHKDv4waQy4hEyNaiI5JAA==
dependencies: dependencies:
"@babel/runtime-corejs2" "^7.17.8" "@babel/runtime-corejs2" "^7.17.8"
dcmjs "^0.29.5" buffer "^6.0.3"
dcmjs "^0.29.8"
gl-matrix "^3.4.3" gl-matrix "^3.4.3"
lodash.clonedeep "^4.5.0" lodash.clonedeep "^4.5.0"
ndarray "^1.0.19" ndarray "^1.0.19"
@ -1427,43 +1428,43 @@
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81" resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81"
integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng== integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng==
"@cornerstonejs/core@^1.7.1": "@cornerstonejs/core@^1.9.3":
version "1.7.1" version "1.9.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.7.1.tgz#38c2fa24cc166b15088a0f8d220ae33e9a80cb7e" resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.9.3.tgz#43c04b5c2a929e1f2ffc71d71aa2311dea2b1e7d"
integrity sha512-XS44HcRwXJ81QqEnOjbpstLCrhApzNIvBuV4UM1+qhvUn0fg6LrXEkR/cT4xQ0RhHB9oq+0LPsfm9/X2xxWZAA== integrity sha512-TMgxFe23Rur0k3ISDh0JGkQ6x2xCYFnqON68A+Eovxc7g/sHUZ0tbzXiTd+IaVpuRsaDxspb0fwklPISKTJDuA==
dependencies: dependencies:
"@kitware/vtk.js" "27.3.1" "@kitware/vtk.js" "27.3.1"
detect-gpu "^5.0.22" detect-gpu "^5.0.22"
gl-matrix "^3.4.3" gl-matrix "^3.4.3"
lodash.clonedeep "4.5.0" lodash.clonedeep "4.5.0"
"@cornerstonejs/dicom-image-loader@^1.7.1": "@cornerstonejs/dicom-image-loader@^1.9.3":
version "1.7.1" version "1.9.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.7.1.tgz#c5ee643bd1c846c1ca7c536f6db3ae26050353fd" resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.9.3.tgz#6ec07e8f22ba20c78cab930a1396effc1f86885e"
integrity sha512-U15NsCdjKkU6OKKWvRs6IwBxRqAudE2vZh+fwAWB9tmyjZCHnRs0KgscOVoYQvR5EV7f1gz7777Iz/u8tnSoFQ== integrity sha512-+JRiI5hb9Cuo7yRGg3DzqgXTmNUMn9DAI1ASh721mbqJcPymHBDs+QTfF2bXyAnh4Ijxr+WfCEUSBm4PRPvrAw==
dependencies: dependencies:
"@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-charls" "^1.2.3"
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
"@cornerstonejs/codec-openjpeg" "^1.2.2" "@cornerstonejs/codec-openjpeg" "^1.2.2"
"@cornerstonejs/codec-openjph" "^2.4.2" "@cornerstonejs/codec-openjph" "^2.4.2"
"@cornerstonejs/core" "^1.7.1" "@cornerstonejs/core" "^1.9.3"
dicom-parser "^1.8.9" dicom-parser "^1.8.9"
pako "^2.0.4" pako "^2.0.4"
uuid "^9.0.0" uuid "^9.0.0"
"@cornerstonejs/streaming-image-volume-loader@^1.7.1": "@cornerstonejs/streaming-image-volume-loader@^1.9.3":
version "1.7.1" version "1.9.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.7.1.tgz#1102cde7136f4804c21b665b6429210545a79087" resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.9.3.tgz#1772273bfd59f7f470ba254d9463c2d1f9c22359"
integrity sha512-eIILqRpWodUVX4HgJbWlZhVebsz4gLxZaOtErOvL/knSK3zyb/D+Rsz8RjWbMJsixHp+gQ/Q51q/FYab/RvJrw== integrity sha512-xD1+ZNrYa1FQTgieG11d6jYnuZFH2zpDzg5eigFeZx9e8Vsu9rO1lpkwVWUw1Cf3qhqmsG9PA+BxHQm3ACjwTg==
dependencies: dependencies:
"@cornerstonejs/core" "^1.7.1" "@cornerstonejs/core" "^1.9.3"
"@cornerstonejs/tools@^1.7.1": "@cornerstonejs/tools@^1.9.3":
version "1.7.1" version "1.9.3"
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.7.1.tgz#7cd589db556ba8a2139fcdaf01b84e03d4b78d2e" resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.9.3.tgz#40e967c4bc275b4bf4d38f6c26d3a979779a632b"
integrity sha512-iulzSXInFlcsLTWsn4xCF+dpZEwSWf/UjZBLmVNfTA262bncmE083cn57VEB9mRuFN+doA50/8UJi7uNew+Q5Q== integrity sha512-mesMJi8L0JHebYy7F3+VE+6zRwkv9ZaYw4cwplguTgOq7s7xmINVHomIVkffHUQeUPhwPVmxbjIqeW6O5MkXeg==
dependencies: dependencies:
"@cornerstonejs/core" "^1.7.1" "@cornerstonejs/core" "^1.9.3"
lodash.clonedeep "4.5.0" lodash.clonedeep "4.5.0"
lodash.get "^4.4.2" lodash.get "^4.4.2"
@ -8219,7 +8220,7 @@ dcmjs@^0.27:
loglevelnext "^3.0.1" loglevelnext "^3.0.1"
ndarray "^1.0.19" ndarray "^1.0.19"
dcmjs@^0.29.5: dcmjs@^0.29.5, dcmjs@^0.29.8:
version "0.29.8" version "0.29.8"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.8.tgz#3daa5224f8e75b2e5b069590bef26a272741fa44" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.8.tgz#3daa5224f8e75b2e5b069590bef26a272741fa44"
integrity sha512-Y0/KZAmT1siVo7eH3KK4ZflEbNi61soUpD0N7lsXMVVJQ6IZkHlaSzb9DtqnEpMs7RJDfvZGr1uXpv1vBBIypQ== integrity sha512-Y0/KZAmT1siVo7eH3KK4ZflEbNi61soUpD0N7lsXMVVJQ6IZkHlaSzb9DtqnEpMs7RJDfvZGr1uXpv1vBBIypQ==