perf(segmentation): make segmentation read 3x faster (#3577)
This commit is contained in:
parent
ec76ea24dd
commit
156c1ba60b
@ -1,11 +1,14 @@
|
||||
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
import {
|
||||
metaData,
|
||||
cache,
|
||||
triggerEvent,
|
||||
eventTarget,
|
||||
} from '@cornerstonejs/core';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
|
||||
import { dicomlabToRGB } from './utils/dicomlabToRGB';
|
||||
|
||||
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 ||
|
||||
Object.keys(segDisplaySet.segments).length === 0
|
||||
) {
|
||||
const segments = await _loadSegments(
|
||||
await _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers
|
||||
);
|
||||
|
||||
segDisplaySet.segments = segments;
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
@ -147,28 +149,65 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
|
||||
async function _loadSegments(extensionManager, segDisplaySet, headers) {
|
||||
async function _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers,
|
||||
}) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(
|
||||
segDisplaySet,
|
||||
null,
|
||||
headers
|
||||
);
|
||||
|
||||
const dicomData = DicomMessage.readFile(segArrayBuffer);
|
||||
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
const cachedReferencedVolume = cache.getVolume(
|
||||
segDisplaySet.referencedVolumeId
|
||||
);
|
||||
|
||||
if (!Array.isArray(dataset.SegmentSequence)) {
|
||||
dataset.SegmentSequence = [dataset.SegmentSequence];
|
||||
if (!cachedReferencedVolume) {
|
||||
throw new Error(
|
||||
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
|
||||
);
|
||||
}
|
||||
|
||||
const segments = _getSegments(dataset);
|
||||
return segments;
|
||||
const { imageIds } = cachedReferencedVolume;
|
||||
|
||||
// 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) {
|
||||
@ -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 }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
|
||||
16
extensions/cornerstone-dicom-seg/src/utils/dicomlabToRGB.ts
Normal file
16
extensions/cornerstone-dicom-seg/src/utils/dicomlabToRGB.ts
Normal 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 };
|
||||
@ -317,7 +317,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
@ -344,10 +343,13 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${parseFloat(SliceThickness).toFixed(2)}mm` : '',
|
||||
thickness: SliceThickness
|
||||
? utils.roundNumber(SliceThickness, 2)
|
||||
: '',
|
||||
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined
|
||||
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
|
||||
? utils.roundNumber(SpacingBetweenSlices, 2)
|
||||
: '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
|
||||
@ -44,9 +44,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.7.1",
|
||||
"@cornerstonejs/core": "^1.7.1",
|
||||
"@cornerstonejs/tools": "^1.7.1",
|
||||
"@cornerstonejs/adapters": "^1.9.3",
|
||||
"@cornerstonejs/core": "^1.9.3",
|
||||
"@cornerstonejs/tools": "^1.9.3",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.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/ui": "3.7.0-beta.42",
|
||||
"dcmjs": "^0.29.6",
|
||||
@ -52,10 +52,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.7.1",
|
||||
"@cornerstonejs/core": "^1.7.1",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.7.1",
|
||||
"@cornerstonejs/tools": "^1.7.1",
|
||||
"@cornerstonejs/adapters": "^1.9.3",
|
||||
"@cornerstonejs/core": "^1.9.3",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.9.3",
|
||||
"@cornerstonejs/tools": "^1.9.3",
|
||||
"@kitware/vtk.js": "27.3.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
|
||||
@ -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(
|
||||
'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 { imageIds } = referencedVolume;
|
||||
const sopUIDImageIdIndexMap = imageIds.reduce((acc, imageId, index) => {
|
||||
const { sopInstanceUid } = metaData.get('generalImageModule', imageId);
|
||||
acc[sopInstanceUid] = index;
|
||||
return acc;
|
||||
}, {} as { [sopUID: string]: number });
|
||||
const segmentsInfo = segDisplaySet.segMetadata.data;
|
||||
derivedVolumeScalarData.set(new Uint8Array(labelmapBufferArray[0]));
|
||||
|
||||
const numSegments = Object.keys(segments).length;
|
||||
// Note: ideally we could use the TypedArray set method, but since each
|
||||
// slice can have multiple segments, we need to loop over each slice and
|
||||
// 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++;
|
||||
}
|
||||
}
|
||||
segmentation.segments = segmentsInfo.map((segmentInfo, segmentIndex) => {
|
||||
if (segmentIndex === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// centroid calculations
|
||||
const x = Math.floor(segmentX / count);
|
||||
const y = Math.floor(segmentY / count);
|
||||
const z = Math.floor(segmentZ / count);
|
||||
const {
|
||||
SegmentedPropertyCategoryCodeSequence,
|
||||
SegmentNumber,
|
||||
SegmentLabel,
|
||||
SegmentAlgorithmType,
|
||||
SegmentAlgorithmName,
|
||||
SegmentedPropertyTypeCodeSequence,
|
||||
rgba,
|
||||
} = segmentInfo;
|
||||
|
||||
const { x, y, z } = segDisplaySet.centroids.get(segmentIndex);
|
||||
const centerWorld = derivedVolume.imageData.indexToWorld([x, y, z]);
|
||||
|
||||
segmentation.cachedStats = {
|
||||
@ -691,65 +630,31 @@ class SegmentationService extends PubSubService {
|
||||
},
|
||||
};
|
||||
|
||||
const numInitialized = Object.keys(segmentation.cachedStats.segmentCenter)
|
||||
.length;
|
||||
|
||||
// Calculate percentage completed
|
||||
const percentComplete = Math.round((numInitialized / numSegments) * 100);
|
||||
|
||||
this._broadcastEvent(EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||
percentComplete,
|
||||
numSegments: numSegments,
|
||||
});
|
||||
};
|
||||
|
||||
const promiseArray = [];
|
||||
|
||||
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],
|
||||
return {
|
||||
label: SegmentLabel || `Segment ${SegmentNumber}`,
|
||||
segmentIndex: Number(SegmentNumber),
|
||||
category: SegmentedPropertyCategoryCodeSequence
|
||||
? SegmentedPropertyCategoryCodeSequence.CodeMeaning
|
||||
: '',
|
||||
type: SegmentedPropertyTypeCodeSequence
|
||||
? SegmentedPropertyTypeCodeSequence.CodeMeaning
|
||||
: '',
|
||||
algorithmType: SegmentAlgorithmType,
|
||||
algorithmName: SegmentAlgorithmName,
|
||||
color: rgba,
|
||||
opacity: 255,
|
||||
isVisible: true,
|
||||
isLocked: false,
|
||||
};
|
||||
});
|
||||
|
||||
segmentation.segmentCount = segmentsInfo.length - 1;
|
||||
|
||||
segDisplaySet.isLoaded = true;
|
||||
|
||||
this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, {
|
||||
segmentationId,
|
||||
segDisplaySet,
|
||||
overlappingSegments,
|
||||
});
|
||||
|
||||
return this.addOrUpdateSegmentation(segmentation, suppressEvents);
|
||||
|
||||
@ -30,8 +30,8 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@cornerstonejs/core": "^1.7.1",
|
||||
"@cornerstonejs/tools": "^1.7.1",
|
||||
"@cornerstonejs/core": "^1.9.3",
|
||||
"@cornerstonejs/tools": "^1.9.3",
|
||||
"@ohif/core": "3.7.0-beta.42",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "3.7.0-beta.42",
|
||||
"@ohif/ui": "3.7.0-beta.42",
|
||||
|
||||
@ -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:static": "cross-env NODE_ENV=development APP_CONFIG=config/local_static.js webpack serve --config .webpack/webpack.pwa.js",
|
||||
"dev:viewer": "yarn run dev",
|
||||
"preinstall": "node preinstall.js",
|
||||
"start": "yarn run dev",
|
||||
"test:e2e": "cypress open",
|
||||
"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-openjpeg": "^1.2.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/extension-cornerstone": "3.7.0-beta.42",
|
||||
"@ohif/extension-cornerstone-dicom-rt": "3.7.0-beta.42",
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.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",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@ -70,6 +70,11 @@ class MetadataProvider {
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
(frameNumber && combineFrameInstance(frameNumber, instance)) || instance
|
||||
);
|
||||
|
||||
@ -27,6 +27,7 @@ export type ExtensionConfiguration = Record<string, unknown>;
|
||||
*/
|
||||
export interface ExtensionParams extends ExtensionConstructor {
|
||||
extensionManager: ExtensionManager;
|
||||
servicesManager: ServicesManager;
|
||||
configuration?: ExtensionConfiguration;
|
||||
}
|
||||
|
||||
@ -43,6 +44,7 @@ export interface Extension {
|
||||
getViewportModule?: (p: ExtensionParams) => unknown;
|
||||
getUtilityModule?: (p: ExtensionParams) => unknown;
|
||||
getCustomizationModule?: (p: ExtensionParams) => unknown;
|
||||
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
|
||||
onModeEnter?: () => void;
|
||||
onModeExit?: () => void;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { InstanceMetadata } from '../../types';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import EVENTS from './EVENTS';
|
||||
@ -47,6 +48,7 @@ export default class DisplaySetService extends PubSubService {
|
||||
};
|
||||
|
||||
public activeDisplaySets = [];
|
||||
extensionManager: ExtensionManager;
|
||||
|
||||
protected activeDisplaySetsMap = new Map<string, DisplaySet>();
|
||||
|
||||
|
||||
@ -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
|
||||
:::
|
||||
|
||||
@ -21,26 +21,23 @@ function LoadingIndicatorTotalPercent({
|
||||
loadingText = 'Loading...',
|
||||
targetText = 'segments',
|
||||
}: Props): JSX.Element {
|
||||
percentComplete = percentComplete !== null ? percentComplete : null;
|
||||
|
||||
const progress = percentComplete !== null ? percentComplete : null;
|
||||
const progress = percentComplete;
|
||||
const totalNumbersText = totalNumbers !== null ? `${totalNumbers}` : '';
|
||||
const numTargetsLoadedText =
|
||||
percentComplete !== null
|
||||
? Math.floor((percentComplete * totalNumbers) / 100)
|
||||
: '';
|
||||
|
||||
const textBlock = !totalNumbers ? (
|
||||
<div className="text-white text-sm">{loadingText}</div>
|
||||
) : (
|
||||
<div className="text-white text-sm flex items-baseline space-x-1">
|
||||
<div>Loaded</div>
|
||||
<div>{numTargetsLoadedText}</div>
|
||||
<div>of</div>
|
||||
<div>{totalNumbersText}</div>
|
||||
<div>{targetText}</div>
|
||||
</div>
|
||||
);
|
||||
const textBlock =
|
||||
!totalNumbers && percentComplete === null ? (
|
||||
<div className="text-white text-sm">{loadingText}</div>
|
||||
) : !totalNumbers && percentComplete !== null ? (
|
||||
<div className="text-white text-sm">Loaded {percentComplete}%</div>
|
||||
) : (
|
||||
<div className="text-white text-sm">
|
||||
Loaded {numTargetsLoadedText} of {totalNumbersText} {targetText}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<LoadingIndicatorProgress
|
||||
|
||||
@ -19,6 +19,7 @@ function PatientInfo({
|
||||
patientAge,
|
||||
MRN,
|
||||
thickness,
|
||||
thicknessUnits,
|
||||
spacing,
|
||||
scanner,
|
||||
isOpen,
|
||||
@ -29,7 +30,6 @@ function PatientInfo({
|
||||
while (patientAge.charAt(0) === '0') {
|
||||
patientAge = patientAge.substr(1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={showPatientInfoRef}>
|
||||
<Tooltip
|
||||
@ -90,7 +90,9 @@ function PatientInfo({
|
||||
className={classnames(classes.infoText)}
|
||||
title={thickness}
|
||||
>
|
||||
{thickness}
|
||||
{thicknessUnits
|
||||
? `${thickness}${thicknessUnits}`
|
||||
: `${thickness}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
@ -136,6 +138,7 @@ PatientInfo.propTypes = {
|
||||
patientAge: PropTypes.string,
|
||||
MRN: PropTypes.string,
|
||||
thickness: PropTypes.string,
|
||||
thicknessUnits: PropTypes.string,
|
||||
spacing: PropTypes.string,
|
||||
scanner: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
|
||||
@ -34,6 +34,7 @@ const ViewportActionBar = ({
|
||||
patientAge,
|
||||
MRN,
|
||||
thickness,
|
||||
thicknessUnits,
|
||||
spacing,
|
||||
scanner,
|
||||
} = patientInformation;
|
||||
@ -169,6 +170,7 @@ const ViewportActionBar = ({
|
||||
patientAge={patientAge}
|
||||
MRN={MRN}
|
||||
thickness={thickness}
|
||||
thicknessUnits={thicknessUnits}
|
||||
spacing={spacing}
|
||||
scanner={scanner}
|
||||
/>
|
||||
@ -192,6 +194,7 @@ ViewportActionBar.propTypes = {
|
||||
patientAge: PropTypes.string.isRequired,
|
||||
MRN: PropTypes.string.isRequired,
|
||||
thickness: PropTypes.string.isRequired,
|
||||
thicknessUnits: PropTypes.string.isRequired,
|
||||
spacing: PropTypes.string.isRequired,
|
||||
scanner: PropTypes.string.isRequired,
|
||||
}),
|
||||
|
||||
57
yarn.lock
57
yarn.lock
@ -1321,9 +1321,9 @@
|
||||
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":
|
||||
version "7.22.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438"
|
||||
integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==
|
||||
version "7.22.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
|
||||
integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
@ -1376,13 +1376,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@cornerstonejs/adapters@^1.7.1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.7.1.tgz#5ae8b510791f5817420ade4b5637f9e84a8ada52"
|
||||
integrity sha512-iwowMUVoNKWZ3PTmYSjnZ2vDThR+OUkvIsSB913A1YhPCl+FVv2Y0JLdueFCYKlTFLnzFa8FOlFwzAOI+AAB+Q==
|
||||
"@cornerstonejs/adapters@^1.9.3":
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/adapters/-/adapters-1.9.3.tgz#b1031e26af175ac15c3c975aff89f51c214ec092"
|
||||
integrity sha512-bjpUq78yupE3+z5qNAS7j27OgCn1BAJQKwcQNY+PBUzTojwg2KpMpG69I+CucSCAxHKDv4waQy4hEyNaiI5JAA==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs2" "^7.17.8"
|
||||
dcmjs "^0.29.5"
|
||||
buffer "^6.0.3"
|
||||
dcmjs "^0.29.8"
|
||||
gl-matrix "^3.4.3"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
ndarray "^1.0.19"
|
||||
@ -1427,43 +1428,43 @@
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/codec-openjph/-/codec-openjph-2.4.2.tgz#e96721d56f6ec96f7f95c16321d88cc8467d8d81"
|
||||
integrity sha512-lgdvBvvNezleY+4pIe2ceUsJzlZe/0PipdeubQ3vZZOz3xxtHHMR1XFCl4fgd8gosR8COHuD7h6q+MwgrwBsng==
|
||||
|
||||
"@cornerstonejs/core@^1.7.1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.7.1.tgz#38c2fa24cc166b15088a0f8d220ae33e9a80cb7e"
|
||||
integrity sha512-XS44HcRwXJ81QqEnOjbpstLCrhApzNIvBuV4UM1+qhvUn0fg6LrXEkR/cT4xQ0RhHB9oq+0LPsfm9/X2xxWZAA==
|
||||
"@cornerstonejs/core@^1.9.3":
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-1.9.3.tgz#43c04b5c2a929e1f2ffc71d71aa2311dea2b1e7d"
|
||||
integrity sha512-TMgxFe23Rur0k3ISDh0JGkQ6x2xCYFnqON68A+Eovxc7g/sHUZ0tbzXiTd+IaVpuRsaDxspb0fwklPISKTJDuA==
|
||||
dependencies:
|
||||
"@kitware/vtk.js" "27.3.1"
|
||||
detect-gpu "^5.0.22"
|
||||
gl-matrix "^3.4.3"
|
||||
lodash.clonedeep "4.5.0"
|
||||
|
||||
"@cornerstonejs/dicom-image-loader@^1.7.1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.7.1.tgz#c5ee643bd1c846c1ca7c536f6db3ae26050353fd"
|
||||
integrity sha512-U15NsCdjKkU6OKKWvRs6IwBxRqAudE2vZh+fwAWB9tmyjZCHnRs0KgscOVoYQvR5EV7f1gz7777Iz/u8tnSoFQ==
|
||||
"@cornerstonejs/dicom-image-loader@^1.9.3":
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/dicom-image-loader/-/dicom-image-loader-1.9.3.tgz#6ec07e8f22ba20c78cab930a1396effc1f86885e"
|
||||
integrity sha512-+JRiI5hb9Cuo7yRGg3DzqgXTmNUMn9DAI1ASh721mbqJcPymHBDs+QTfF2bXyAnh4Ijxr+WfCEUSBm4PRPvrAw==
|
||||
dependencies:
|
||||
"@cornerstonejs/codec-charls" "^1.2.3"
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"
|
||||
"@cornerstonejs/codec-openjpeg" "^1.2.2"
|
||||
"@cornerstonejs/codec-openjph" "^2.4.2"
|
||||
"@cornerstonejs/core" "^1.7.1"
|
||||
"@cornerstonejs/core" "^1.9.3"
|
||||
dicom-parser "^1.8.9"
|
||||
pako "^2.0.4"
|
||||
uuid "^9.0.0"
|
||||
|
||||
"@cornerstonejs/streaming-image-volume-loader@^1.7.1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.7.1.tgz#1102cde7136f4804c21b665b6429210545a79087"
|
||||
integrity sha512-eIILqRpWodUVX4HgJbWlZhVebsz4gLxZaOtErOvL/knSK3zyb/D+Rsz8RjWbMJsixHp+gQ/Q51q/FYab/RvJrw==
|
||||
"@cornerstonejs/streaming-image-volume-loader@^1.9.3":
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-1.9.3.tgz#1772273bfd59f7f470ba254d9463c2d1f9c22359"
|
||||
integrity sha512-xD1+ZNrYa1FQTgieG11d6jYnuZFH2zpDzg5eigFeZx9e8Vsu9rO1lpkwVWUw1Cf3qhqmsG9PA+BxHQm3ACjwTg==
|
||||
dependencies:
|
||||
"@cornerstonejs/core" "^1.7.1"
|
||||
"@cornerstonejs/core" "^1.9.3"
|
||||
|
||||
"@cornerstonejs/tools@^1.7.1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.7.1.tgz#7cd589db556ba8a2139fcdaf01b84e03d4b78d2e"
|
||||
integrity sha512-iulzSXInFlcsLTWsn4xCF+dpZEwSWf/UjZBLmVNfTA262bncmE083cn57VEB9mRuFN+doA50/8UJi7uNew+Q5Q==
|
||||
"@cornerstonejs/tools@^1.9.3":
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-1.9.3.tgz#40e967c4bc275b4bf4d38f6c26d3a979779a632b"
|
||||
integrity sha512-mesMJi8L0JHebYy7F3+VE+6zRwkv9ZaYw4cwplguTgOq7s7xmINVHomIVkffHUQeUPhwPVmxbjIqeW6O5MkXeg==
|
||||
dependencies:
|
||||
"@cornerstonejs/core" "^1.7.1"
|
||||
"@cornerstonejs/core" "^1.9.3"
|
||||
lodash.clonedeep "4.5.0"
|
||||
lodash.get "^4.4.2"
|
||||
|
||||
@ -8219,7 +8220,7 @@ dcmjs@^0.27:
|
||||
loglevelnext "^3.0.1"
|
||||
ndarray "^1.0.19"
|
||||
|
||||
dcmjs@^0.29.5:
|
||||
dcmjs@^0.29.5, dcmjs@^0.29.8:
|
||||
version "0.29.8"
|
||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.29.8.tgz#3daa5224f8e75b2e5b069590bef26a272741fa44"
|
||||
integrity sha512-Y0/KZAmT1siVo7eH3KK4ZflEbNi61soUpD0N7lsXMVVJQ6IZkHlaSzb9DtqnEpMs7RJDfvZGr1uXpv1vBBIypQ==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user