ohif-viewer/extensions/cornerstone-dicom-rt/src/loadRTStruct.js
Alireza c7bcf1134d
fix(staticwado): SM and RT and update the server with new data (#3422)
* fix bugs for the RT for the new demo

* fix SM with the static-wado server

* remove thumbnail from tmtv

* migration guide

* fix stability for the rt struct

* apply review comments

* add loading indicator to SM

* pdf works

* try to fix relative bulkData

* fix the rest

* fix preflight for the SM

* fix typo

* apply review comments

* yarn lock
2023-05-29 09:04:49 -04:00

332 lines
9.5 KiB
JavaScript

import dcmjs from 'dcmjs';
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
async function checkAndLoadContourData(instance, datasource) {
if (!instance || !instance.ROIContourSequence) {
return Promise.reject('Invalid instance object or ROIContourSequence');
}
const promisesMap = new Map();
for (const ROIContour of instance.ROIContourSequence) {
const referencedROINumber = ROIContour.ReferencedROINumber;
if (!ROIContour || !ROIContour.ContourSequence) {
promisesMap.set(referencedROINumber, [Promise.resolve([])]);
continue;
}
for (const Contour of ROIContour.ContourSequence) {
if (!Contour || !Contour.ContourData) {
return Promise.reject('Invalid Contour or ContourData');
}
const contourData = Contour.ContourData;
if (Array.isArray(contourData)) {
promisesMap.has(referencedROINumber)
? promisesMap
.get(referencedROINumber)
.push(Promise.resolve(contourData))
: promisesMap.set(referencedROINumber, [
Promise.resolve(contourData),
]);
} else if (contourData && contourData.BulkDataURI) {
const bulkDataURI = contourData.BulkDataURI;
if (
!datasource ||
!datasource.retrieve ||
!datasource.retrieve.bulkDataURI
) {
return Promise.reject(
'Invalid datasource object or retrieve function'
);
}
const bulkDataPromise = datasource.retrieve.bulkDataURI({
BulkDataURI: bulkDataURI,
StudyInstanceUID: instance.StudyInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
SOPInstanceUID: instance.SOPInstanceUID,
});
promisesMap.has(referencedROINumber)
? promisesMap.get(referencedROINumber).push(bulkDataPromise)
: promisesMap.set(referencedROINumber, [bulkDataPromise]);
} else {
return Promise.reject(`Invalid ContourData: ${contourData}`);
}
}
}
const resolvedPromisesMap = new Map();
for (const [key, promiseArray] of promisesMap.entries()) {
resolvedPromisesMap.set(key, await Promise.allSettled(promiseArray));
}
instance.ROIContourSequence.forEach(ROIContour => {
try {
const referencedROINumber = ROIContour.ReferencedROINumber;
const resolvedPromises = resolvedPromisesMap.get(referencedROINumber);
if (ROIContour.ContourSequence) {
ROIContour.ContourSequence.forEach((Contour, index) => {
const promise = resolvedPromises[index];
if (promise.status === 'fulfilled') {
if (
Array.isArray(promise.value) &&
promise.value.every(Number.isFinite)
) {
// If promise.value is already an array of numbers, use it directly
Contour.ContourData = promise.value;
} else {
// If the resolved promise value is a byte array (Blob), it needs to be decoded
const uint8Array = new Uint8Array(promise.value);
const textDecoder = new TextDecoder();
const dataUint8Array = textDecoder.decode(uint8Array);
if (
typeof dataUint8Array === 'string' &&
dataUint8Array.includes('\\')
) {
Contour.ContourData = dataUint8Array
.split('\\')
.map(parseFloat);
} else {
Contour.ContourData = [];
}
}
} else {
console.error(promise.reason);
}
});
}
} catch (error) {
console.error(error);
}
});
}
export default async function loadRTStruct(
extensionManager,
rtStructDisplaySet,
referencedDisplaySet,
headers
) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const dataSource = extensionManager.getActiveDataSource()[0];
const { bulkDataURI } = dataSource.getConfig?.() || {};
const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
referencedDisplaySet
);
// Set here is loading is asynchronous.
// If this function throws its set back to false.
rtStructDisplaySet.isLoaded = true;
let instance = rtStructDisplaySet.instance;
if (!bulkDataURI || !bulkDataURI.enabled) {
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
rtStructDisplaySet,
null,
headers
);
const dicomData = DicomMessage.readFile(segArrayBuffer);
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
instance = rtStructDataset;
} else {
await checkAndLoadContourData(instance, dataSource);
}
const {
StructureSetROISequence,
ROIContourSequence,
RTROIObservationsSequence,
} = instance;
// Define our structure set entry and add it to the rtstruct module state.
const structureSet = {
StructureSetLabel: instance.StructureSetLabel,
SeriesInstanceUID: instance.SeriesInstanceUID,
ROIContours: [],
visible: true,
};
for (let i = 0; i < ROIContourSequence.length; i++) {
const ROIContour = ROIContourSequence[i];
const { ContourSequence } = ROIContour;
if (!ContourSequence) {
continue;
}
const isSupported = false;
const ContourSequenceArray = _toArray(ContourSequence);
const contourPoints = [];
for (let c = 0; c < ContourSequenceArray.length; c++) {
const {
ContourImageSequence,
ContourData,
NumberOfContourPoints,
ContourGeometricType,
} = ContourSequenceArray[c];
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
if (!imageId) {
continue;
}
let isSupported = false;
const points = [];
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
points.push({
x: ContourData[p],
y: ContourData[p + 1],
z: ContourData[p + 2],
});
}
switch (ContourGeometricType) {
case 'CLOSED_PLANAR':
case 'OPEN_PLANAR':
case 'POINT':
isSupported = true;
break;
default:
continue;
}
contourPoints.push({
numberOfPoints: NumberOfContourPoints,
points,
type: ContourGeometricType,
isSupported,
});
}
_setROIContourMetadata(
structureSet,
StructureSetROISequence,
RTROIObservationsSequence,
ROIContour,
contourPoints,
isSupported
);
}
return structureSet;
}
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
imageIdSopInstanceUidPairsEntry =>
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
);
return imageIdSopInstanceUidPairsEntry
? imageIdSopInstanceUidPairsEntry.imageId
: null;
};
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
return referencedDisplaySet.images.map(image => {
return {
imageId: image.imageId,
sopInstanceUID: image.SOPInstanceUID,
};
});
}
function _setROIContourMetadata(
structureSet,
StructureSetROISequence,
RTROIObservationsSequence,
ROIContour,
contourPoints,
isSupported
) {
const StructureSetROI = StructureSetROISequence.find(
structureSetROI =>
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
);
const ROIContourData = {
ROINumber: StructureSetROI.ROINumber,
ROIName: StructureSetROI.ROIName,
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
ROIDescription: StructureSetROI.ROIDescription,
isSupported,
contourPoints,
visible: true,
};
_setROIContourDataColor(ROIContour, ROIContourData);
if (RTROIObservationsSequence) {
// If present, add additional RTROIObservations metadata.
_setROIContourRTROIObservations(
ROIContourData,
RTROIObservationsSequence,
ROIContour.ReferencedROINumber
);
}
structureSet.ROIContours.push(ROIContourData);
}
function _setROIContourDataColor(ROIContour, ROIContourData) {
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
}
if (ROIDisplayColor) {
ROIContourData.colorArray = [...ROIDisplayColor];
}
}
function _setROIContourRTROIObservations(
ROIContourData,
RTROIObservationsSequence,
ROINumber
) {
const RTROIObservations = RTROIObservationsSequence.find(
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
);
if (RTROIObservations) {
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
const {
ObservationNumber,
ROIObservationDescription,
RTROIInterpretedType,
ROIInterpreter,
} = RTROIObservations;
ROIContourData.RTROIObservations = {
ObservationNumber,
ROIObservationDescription,
RTROIInterpretedType,
ROIInterpreter,
};
}
}
function _toArray(objOrArray) {
return Array.isArray(objOrArray) ? objOrArray : [objOrArray];
}