init
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
|
||||
import { adaptersPMAP } from '@cornerstonejs/adapters';
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import { dicomLoaderService } from '@ohif/extension-cornerstone';
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.30'];
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SOPClassUID,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
wadoUriRoot,
|
||||
} = instance;
|
||||
|
||||
const displaySet = {
|
||||
// Parametric map use to have the same modality as its referenced volume but
|
||||
// "PMAP" is used in the viewer even though this is not a valid DICOM modality
|
||||
Modality: 'PMAP',
|
||||
isReconstructable: true, // by default for now
|
||||
displaySetInstanceUID: `pmap.${utils.guid()}`,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
referencedImages: null,
|
||||
referencedSeriesInstanceUID: null,
|
||||
referencedDisplaySetInstanceUID: null,
|
||||
referencedVolumeURI: null,
|
||||
referencedVolumeId: null,
|
||||
isDerivedDisplaySet: true,
|
||||
loadStatus: {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
},
|
||||
sopClassUids,
|
||||
instance,
|
||||
instances: [instance],
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
isOverlayDisplaySet: true,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
|
||||
if (!referencedSeriesSequence) {
|
||||
console.error('ReferencedSeriesSequence is missing for the parametric map');
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
|
||||
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
// Does not get the referenced displaySet during parametric displaySet creation
|
||||
// because it is still not available (getDisplaySetByUID returns `undefined`).
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
|
||||
if (displaySet.referencedDisplaySetInstanceUID) {
|
||||
return displaySetService.getDisplaySetByUID(displaySet.referencedDisplaySetInstanceUID);
|
||||
}
|
||||
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
throw new Error('Referenced displaySet is missing for the parametric map');
|
||||
}
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
|
||||
// Does not get the referenced volumeId during parametric displaySet creation because the
|
||||
// referenced displaySet is still not available (getDisplaySetByUID returns `undefined`).
|
||||
displaySet.getReferencedVolumeId = () => {
|
||||
if (displaySet.referencedVolumeId) {
|
||||
return displaySet.referencedVolumeId;
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySet.getReferenceDisplaySet();
|
||||
const referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
||||
const referencedVolumeId = `${VOLUME_LOADER_SCHEME}:${referencedVolumeURI}`;
|
||||
|
||||
displaySet.referencedVolumeURI = referencedVolumeURI;
|
||||
displaySet.referencedVolumeId = referencedVolumeId;
|
||||
|
||||
return referencedVolumeId;
|
||||
};
|
||||
|
||||
displaySet.load = async ({ headers }) =>
|
||||
await _load(displaySet, servicesManager, extensionManager, headers);
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
const getRangeFromPixelData = (pixelData: Float32Array) => {
|
||||
let lowest = pixelData[0];
|
||||
let highest = pixelData[0];
|
||||
|
||||
for (let i = 1; i < pixelData.length; i++) {
|
||||
if (pixelData[i] < lowest) {
|
||||
lowest = pixelData[i];
|
||||
}
|
||||
if (pixelData[i] > highest) {
|
||||
highest = pixelData[i];
|
||||
}
|
||||
}
|
||||
|
||||
return [lowest, highest];
|
||||
};
|
||||
|
||||
async function _load(
|
||||
displaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager,
|
||||
headers
|
||||
) {
|
||||
const volumeId = `${VOLUME_LOADER_SCHEME}:${displaySet.displaySetInstanceUID}`;
|
||||
const volumeLoadObject = cache.getVolumeLoadObject(volumeId);
|
||||
|
||||
if (volumeLoadObject) {
|
||||
return volumeLoadObject.promise;
|
||||
}
|
||||
|
||||
displaySet.loading = true;
|
||||
displaySet.isLoaded = false;
|
||||
|
||||
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||
// and also return the same promise to any other callers.
|
||||
const promise = _loadParametricMap({
|
||||
extensionManager,
|
||||
displaySet,
|
||||
headers,
|
||||
});
|
||||
|
||||
cache.putVolumeLoadObject(volumeId, { promise }).catch(err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
promise
|
||||
.then(() => {
|
||||
displaySet.loading = false;
|
||||
displaySet.isLoaded = true;
|
||||
// Broadcast that loading is complete
|
||||
servicesManager.services.segmentationService._broadcastEvent(
|
||||
servicesManager.services.segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
{
|
||||
pmapDisplaySet: displaySet,
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
displaySet.loading = false;
|
||||
displaySet.isLoaded = false;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function _loadParametricMap({ displaySet, headers }: withAppTypes) {
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(displaySet, null, headers);
|
||||
const referencedVolumeId = displaySet.getReferencedVolumeId();
|
||||
const cachedReferencedVolume = cache.getVolume(referencedVolumeId);
|
||||
|
||||
// Parametric map can be loaded only if its referenced volume exists otherwise it will fail
|
||||
if (!cachedReferencedVolume) {
|
||||
throw new Error(
|
||||
'Referenced Volume is missing for the PMAP, and stack viewport PMAP is not supported yet'
|
||||
);
|
||||
}
|
||||
|
||||
const { imageIds } = cachedReferencedVolume;
|
||||
const results = await adaptersPMAP.Cornerstone3D.ParametricMap.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
metaData
|
||||
);
|
||||
const { pixelData } = results;
|
||||
const TypedArrayConstructor = pixelData.constructor;
|
||||
const paramMapId = displaySet.displaySetInstanceUID;
|
||||
|
||||
const derivedVolume = await volumeLoader.createAndCacheDerivedVolume(referencedVolumeId, {
|
||||
volumeId: paramMapId,
|
||||
targetBuffer: {
|
||||
type: TypedArrayConstructor.name,
|
||||
},
|
||||
});
|
||||
|
||||
const newPixelData = new TypedArrayConstructor(pixelData.length);
|
||||
for (let i = 0; i < pixelData.length; i++) {
|
||||
newPixelData[i] = pixelData[i] * 100;
|
||||
}
|
||||
derivedVolume.voxelManager.setCompleteScalarDataArray(newPixelData);
|
||||
const range = getRangeFromPixelData(newPixelData);
|
||||
const windowLevel = csUtils.windowLevel.toWindowLevel(range[0], range[1]);
|
||||
|
||||
derivedVolume.metadata.voiLut = [windowLevel];
|
||||
derivedVolume.loadStatus = { loaded: true };
|
||||
|
||||
return derivedVolume;
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dicom-pmap',
|
||||
sopClassUids,
|
||||
getDisplaySetsFromSeries,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getSopClassHandlerModule;
|
||||
7
extensions/cornerstone-dicom-pmap/src/id.js
Normal file
7
extensions/cornerstone-dicom-pmap/src/id.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
const SOPClassHandlerName = 'dicom-pmap';
|
||||
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||
|
||||
export { id, SOPClassHandlerId, SOPClassHandlerName };
|
||||
39
extensions/cornerstone-dicom-pmap/src/index.tsx
Normal file
39
extensions/cornerstone-dicom-pmap/src/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { id } from './id';
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstonePMAPViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstonePMAPViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* You can remove any of the following modules if you don't need them.
|
||||
*/
|
||||
const extension = {
|
||||
id,
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstonePMAPViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstonePMAPViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
commandsManager={commandsManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [{ name: 'dicom-pmap', component: ExtendedOHIFCornerstonePMAPViewport }];
|
||||
},
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
export default extension;
|
||||
@@ -0,0 +1,205 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
|
||||
function OHIFCornerstonePMAPViewport(props: withAppTypes) {
|
||||
const {
|
||||
displaySets,
|
||||
children,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
const { displaySetService, segmentationService, uiNotificationService } =
|
||||
servicesManager.services;
|
||||
|
||||
// PMAP viewport will always have a single display set
|
||||
if (displaySets.length !== 1) {
|
||||
throw new Error('PMAP viewport must have a single display set');
|
||||
}
|
||||
|
||||
const pmapDisplaySet = displaySets[0];
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
const referencedDisplaySet = pmapDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
referencedDisplaySet,
|
||||
pmapDisplaySet
|
||||
);
|
||||
|
||||
referencedDisplaySetRef.current = {
|
||||
displaySet: referencedDisplaySet,
|
||||
metadata: referencedDisplaySetMetadata,
|
||||
};
|
||||
|
||||
const [pmapIsLoading, setPmapIsLoading] = useState(!pmapDisplaySet.isLoaded);
|
||||
|
||||
// Add effect to listen for loading complete
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (evt.pmapDisplaySet?.displaySetInstanceUID === pmapDisplaySet.displaySetInstanceUID) {
|
||||
setPmapIsLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [pmapDisplaySet]);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
displaySetOptions.unshift({});
|
||||
const [pmapDisplaySetOptions] = displaySetOptions;
|
||||
|
||||
// Make sure `options` exists
|
||||
pmapDisplaySetOptions.options = pmapDisplaySetOptions.options ?? {};
|
||||
|
||||
Object.assign(pmapDisplaySetOptions.options, {
|
||||
colormap: {
|
||||
name: 'rainbow_2',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.25, opacity: 0.25 },
|
||||
{ value: 0.5, opacity: 0.5 },
|
||||
{ value: 0.75, opacity: 0.75 },
|
||||
{ value: 0.9, opacity: 0.99 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
windowCenter: 50,
|
||||
windowWidth: 100,
|
||||
},
|
||||
});
|
||||
|
||||
uiNotificationService.show({
|
||||
title: 'Parametric Map',
|
||||
type: 'warning',
|
||||
message: 'The values are multiplied by 100 in the viewport for better visibility',
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
// Referenced + PMAP displaySets must be passed as parameter in this order
|
||||
displaySets={[referencedDisplaySet, pmapDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
displaySetOptions={[{}, pmapDisplaySetOptions]}
|
||||
></Component>
|
||||
);
|
||||
}, [
|
||||
extensionManager,
|
||||
displaySetOptions,
|
||||
props,
|
||||
pmapDisplaySet,
|
||||
viewportOptions.orientation,
|
||||
viewportOptions.viewportId,
|
||||
]);
|
||||
|
||||
// Cleanup the PMAP viewport when the viewport is destroyed
|
||||
useEffect(() => {
|
||||
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||
({ displaySetInstanceUIDs }) => {
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
onDisplaySetsRemovedSubscription.unsubscribe();
|
||||
};
|
||||
}, [activeViewportId, displaySetService, viewportGridService, viewports]);
|
||||
|
||||
let childrenWithProps = null;
|
||||
|
||||
if (children && children.length) {
|
||||
childrenWithProps = children.map((child, index) => {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{pmapIsLoading && (
|
||||
<LoadingIndicatorTotalPercent
|
||||
className="h-full w-full"
|
||||
totalNumbers={null}
|
||||
percentComplete={null}
|
||||
loadingText="Loading Parametric Map..."
|
||||
/>
|
||||
)}
|
||||
{getCornerstoneViewport()}
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstonePMAPViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet, pmapDisplaySet) {
|
||||
const { SharedFunctionalGroupsSequence } = pmapDisplaySet.instance;
|
||||
|
||||
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
|
||||
? SharedFunctionalGroupsSequence[0]
|
||||
: SharedFunctionalGroupsSequence;
|
||||
|
||||
const { PixelMeasuresSequence } = SharedFunctionalGroup;
|
||||
|
||||
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
|
||||
? PixelMeasuresSequence[0]
|
||||
: PixelMeasuresSequence;
|
||||
|
||||
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
|
||||
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
const referencedDisplaySetMetadata = {
|
||||
PatientID: image0.PatientID,
|
||||
PatientName: image0.PatientName,
|
||||
PatientSex: image0.PatientSex,
|
||||
PatientAge: image0.PatientAge,
|
||||
SliceThickness: image0.SliceThickness || SliceThickness,
|
||||
StudyDate: image0.StudyDate,
|
||||
SeriesDescription: image0.SeriesDescription,
|
||||
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||
SeriesNumber: image0.SeriesNumber,
|
||||
ManufacturerModelName: image0.ManufacturerModelName,
|
||||
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
|
||||
};
|
||||
|
||||
return referencedDisplaySetMetadata;
|
||||
}
|
||||
|
||||
export default OHIFCornerstonePMAPViewport;
|
||||
Reference in New Issue
Block a user