246 lines
7.4 KiB
TypeScript
246 lines
7.4 KiB
TypeScript
import { utils, Types as OhifTypes } from '@ohif/core';
|
|
import i18n from '@ohif/i18n';
|
|
import { metaData, cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
|
|
import { adaptersPMAP } from '@cornerstonejs/adapters';
|
|
import { SOPClassHandlerId } from './id';
|
|
import { dicomLoaderService, VOLUME_LOADER_SCHEME } from '@ohif/extension-cornerstone';
|
|
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,
|
|
supportsWindowLevel: true,
|
|
label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('PMAP')}`,
|
|
};
|
|
|
|
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(params: OhifTypes.Extensions.ExtensionParams) {
|
|
const { servicesManager, extensionManager } = params;
|
|
const getDisplaySetsFromSeries = instances => {
|
|
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
|
};
|
|
|
|
return [
|
|
{
|
|
name: 'dicom-pmap',
|
|
sopClassUids,
|
|
getDisplaySetsFromSeries,
|
|
},
|
|
];
|
|
}
|
|
|
|
export default getSopClassHandlerModule;
|