725 lines
25 KiB
TypeScript
725 lines
25 KiB
TypeScript
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
|
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
|
|
|
import addSRAnnotation from './utils/addSRAnnotation';
|
|
import isRehydratable from './utils/isRehydratable';
|
|
import {
|
|
SOPClassHandlerName,
|
|
SOPClassHandlerId,
|
|
SOPClassHandlerId3D,
|
|
SOPClassHandlerName3D,
|
|
} from './id';
|
|
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums';
|
|
|
|
const { sopClassDictionary } = utils;
|
|
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
|
|
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
|
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
|
|
|
type InstanceMetadata = Types.InstanceMetadata;
|
|
|
|
/**
|
|
* TODO
|
|
* - [ ] Add SR thumbnail
|
|
* - [ ] Make viewport
|
|
* - [ ] Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport
|
|
*/
|
|
|
|
const sopClassUids = [
|
|
sopClassDictionary.BasicTextSR,
|
|
sopClassDictionary.EnhancedSR,
|
|
sopClassDictionary.ComprehensiveSR,
|
|
];
|
|
|
|
const validateSameStudyUID = (uid: string, instances): void => {
|
|
instances.forEach(it => {
|
|
if (it.StudyInstanceUID !== uid) {
|
|
console.warn('Not all instances have the same UID', uid, it);
|
|
throw new Error(`Instances ${it.SOPInstanceUID} does not belong to ${uid}`);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Adds instances to the DICOM SR series, rather than creating a new
|
|
* series, so that as SR's are saved, they append to the series, and the
|
|
* key image display set gets updated as well, containing just the new series.
|
|
* @param instances is a list of instances from THIS series that are not
|
|
* in this DICOM SR Display Set already.
|
|
*/
|
|
function addInstances(instances: InstanceMetadata[], displaySetService: DisplaySetService) {
|
|
this.instances.push(...instances);
|
|
utils.sortStudyInstances(this.instances);
|
|
// The last instance is the newest one, so is the one most interesting.
|
|
// Eventually, the SR viewer should have the ability to choose which SR
|
|
// gets loaded, and to navigate among them.
|
|
this.instance = this.instances[this.instances.length - 1];
|
|
this.isLoaded = false;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* DICOM SR SOP Class Handler
|
|
* For all referenced images in the TID 1500/300 sections, add an image to the
|
|
* display.
|
|
* @param instances is a set of instances all from the same series
|
|
* @param servicesManager is the services that can be used for creating
|
|
* @returns The list of display sets created for the given instances object
|
|
*/
|
|
function _getDisplaySetsFromSeries(
|
|
instances,
|
|
servicesManager: AppTypes.ServicesManager,
|
|
extensionManager
|
|
) {
|
|
// If the series has no instances, stop here
|
|
if (!instances || !instances.length) {
|
|
throw new Error('No instances were provided');
|
|
}
|
|
|
|
utils.sortStudyInstances(instances);
|
|
// The last instance is the newest one, so is the one most interesting.
|
|
// Eventually, the SR viewer should have the ability to choose which SR
|
|
// gets loaded, and to navigate among them.
|
|
const instance = instances[instances.length - 1];
|
|
|
|
const {
|
|
StudyInstanceUID,
|
|
SeriesInstanceUID,
|
|
SOPInstanceUID,
|
|
SeriesDescription,
|
|
SeriesNumber,
|
|
SeriesDate,
|
|
ConceptNameCodeSequence,
|
|
SOPClassUID,
|
|
} = instance;
|
|
validateSameStudyUID(instance.StudyInstanceUID, instances);
|
|
|
|
const is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR;
|
|
|
|
const isImagingMeasurementReport =
|
|
ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport;
|
|
|
|
const displaySet = {
|
|
Modality: 'SR',
|
|
displaySetInstanceUID: utils.guid(),
|
|
SeriesDescription,
|
|
SeriesNumber,
|
|
SeriesDate,
|
|
SOPInstanceUID,
|
|
SeriesInstanceUID,
|
|
StudyInstanceUID,
|
|
SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId,
|
|
SOPClassUID,
|
|
instances,
|
|
referencedImages: null,
|
|
measurements: null,
|
|
isDerivedDisplaySet: true,
|
|
isLoaded: false,
|
|
isImagingMeasurementReport,
|
|
sopClassUids,
|
|
instance,
|
|
addInstances,
|
|
};
|
|
|
|
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
|
|
|
return [displaySet];
|
|
}
|
|
|
|
/**
|
|
* Loads the display set with the given services and extension manager.
|
|
* @param srDisplaySet - The display set to load.
|
|
* @param servicesManager - The services manager containing displaySetService and measurementService.
|
|
* @param extensionManager - The extension manager containing data sources.
|
|
*/
|
|
async function _load(
|
|
srDisplaySet: Types.DisplaySet,
|
|
servicesManager: AppTypes.ServicesManager,
|
|
extensionManager: AppTypes.ExtensionManager
|
|
) {
|
|
const { displaySetService, measurementService } = servicesManager.services;
|
|
const dataSources = extensionManager.getDataSources();
|
|
const dataSource = dataSources[0];
|
|
const { ContentSequence } = srDisplaySet.instance;
|
|
|
|
async function retrieveBulkData(obj, parentObj = null, key = null) {
|
|
for (const prop in obj) {
|
|
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
|
|
await retrieveBulkData(obj[prop], obj, prop);
|
|
} else if (Array.isArray(obj[prop])) {
|
|
await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop)));
|
|
} else if (prop === 'BulkDataURI') {
|
|
const value = await dataSource.retrieve.bulkDataURI({
|
|
BulkDataURI: obj[prop],
|
|
StudyInstanceUID: srDisplaySet.instance.StudyInstanceUID,
|
|
SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID,
|
|
SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID,
|
|
});
|
|
if (parentObj && key) {
|
|
parentObj[key] = new Float32Array(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (srDisplaySet.isLoaded !== true) {
|
|
await retrieveBulkData(ContentSequence);
|
|
}
|
|
|
|
if (srDisplaySet.isImagingMeasurementReport) {
|
|
srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
|
srDisplaySet.measurements = _getMeasurements(ContentSequence);
|
|
} else {
|
|
srDisplaySet.referencedImages = [];
|
|
srDisplaySet.measurements = [];
|
|
}
|
|
|
|
const mappings = measurementService.getSourceMappings(
|
|
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
|
);
|
|
|
|
srDisplaySet.isHydrated = false;
|
|
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
|
|
srDisplaySet.isLoaded = true;
|
|
|
|
/** Check currently added displaySets and add measurements if the sources exist */
|
|
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
|
_checkIfCanAddMeasurementsToDisplaySet(
|
|
srDisplaySet,
|
|
activeDisplaySet,
|
|
dataSource,
|
|
servicesManager
|
|
);
|
|
});
|
|
|
|
/** Subscribe to new displaySets as the source may come in after */
|
|
displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => {
|
|
const { displaySetsAdded } = data;
|
|
/**
|
|
* If there are still some measurements that have not yet been loaded into cornerstone,
|
|
* See if we can load them onto any of the new displaySets.
|
|
*/
|
|
displaySetsAdded.forEach(newDisplaySet => {
|
|
_checkIfCanAddMeasurementsToDisplaySet(
|
|
srDisplaySet,
|
|
newDisplaySet,
|
|
dataSource,
|
|
servicesManager
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks if measurements can be added to a display set.
|
|
*
|
|
* @param srDisplaySet - The source display set containing measurements.
|
|
* @param newDisplaySet - The new display set to check if measurements can be added.
|
|
* @param dataSource - The data source used to retrieve image IDs.
|
|
* @param servicesManager - The services manager.
|
|
*/
|
|
function _checkIfCanAddMeasurementsToDisplaySet(
|
|
srDisplaySet,
|
|
newDisplaySet,
|
|
dataSource,
|
|
servicesManager: AppTypes.ServicesManager
|
|
) {
|
|
const { customizationService } = servicesManager.services;
|
|
|
|
const unloadedMeasurements = srDisplaySet.measurements.filter(
|
|
measurement => measurement.loaded === false
|
|
);
|
|
|
|
if (
|
|
unloadedMeasurements.length === 0 ||
|
|
!(newDisplaySet instanceof ImageSet) ||
|
|
newDisplaySet.unsupported
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// const { sopClassUids } = newDisplaySet;
|
|
// Create a Set for faster lookups
|
|
// const sopClassUidSet = new Set(sopClassUids);
|
|
|
|
// Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number
|
|
const imageIdMap = new Map<string, string>();
|
|
const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet);
|
|
|
|
for (const imageId of imageIds) {
|
|
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
|
|
const key = `${SOPInstanceUID}:${frameNumber || 1}`;
|
|
imageIdMap.set(key, imageId);
|
|
}
|
|
|
|
if (!unloadedMeasurements?.length) {
|
|
return;
|
|
}
|
|
|
|
const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR;
|
|
|
|
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
|
let measurement = unloadedMeasurements[j];
|
|
|
|
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
|
|
'onBeforeSRAddMeasurement'
|
|
)?.value;
|
|
|
|
if (typeof onBeforeSRAddMeasurement === 'function') {
|
|
measurement = onBeforeSRAddMeasurement({
|
|
measurement,
|
|
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
|
|
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
|
|
});
|
|
}
|
|
|
|
// if it is 3d SR we can just add the SR annotation
|
|
if (is3DSR) {
|
|
addSRAnnotation(measurement, null, null);
|
|
measurement.loaded = true;
|
|
continue;
|
|
}
|
|
|
|
const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence;
|
|
if (!referencedSOPSequence) {
|
|
continue;
|
|
}
|
|
|
|
const { ReferencedSOPInstanceUID } = referencedSOPSequence;
|
|
const frame = referencedSOPSequence.ReferencedFrameNumber || 1;
|
|
const key = `${ReferencedSOPInstanceUID}:${frame}`;
|
|
const imageId = imageIdMap.get(key);
|
|
|
|
if (
|
|
imageId &&
|
|
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
|
|
) {
|
|
addSRAnnotation(measurement, imageId, frame);
|
|
|
|
// Update measurement properties
|
|
measurement.loaded = true;
|
|
measurement.imageId = imageId;
|
|
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
|
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
|
|
measurement.frameNumber = frame;
|
|
|
|
unloadedMeasurements.splice(j, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a measurement references a specific SOP Instance UID.
|
|
* @param measurement - The measurement object.
|
|
* @param SOPInstanceUID - The SOP Instance UID to check against.
|
|
* @param frameNumber - The frame number to check against (optional).
|
|
* @returns True if the measurement references the specified SOP Instance UID, false otherwise.
|
|
*/
|
|
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
|
|
const { coords } = measurement;
|
|
|
|
/**
|
|
* NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
|
|
* Standard. But for now, we will support only one ReferenceFrameNumber.
|
|
*/
|
|
const ReferencedFrameNumber =
|
|
(measurement.coords[0].ReferencedSOPSequence &&
|
|
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
|
1;
|
|
|
|
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
|
|
return false;
|
|
}
|
|
|
|
for (let j = 0; j < coords.length; j++) {
|
|
const coord = coords[j];
|
|
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
|
|
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the SOP class handler module.
|
|
*
|
|
* @param {Object} options - The options for retrieving the SOP class handler module.
|
|
* @param {Object} options.servicesManager - The services manager.
|
|
* @param {Object} options.extensionManager - The extension manager.
|
|
* @returns {Array} An array containing the SOP class handler module.
|
|
*/
|
|
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
|
const getDisplaySetsFromSeries = instances => {
|
|
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
|
};
|
|
return [
|
|
{
|
|
name: SOPClassHandlerName,
|
|
sopClassUids,
|
|
getDisplaySetsFromSeries,
|
|
},
|
|
{
|
|
name: SOPClassHandlerName3D,
|
|
sopClassUids: [sopClassDictionary.Comprehensive3DSR],
|
|
getDisplaySetsFromSeries,
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
|
|
*
|
|
* @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
|
|
* @returns {Array} - The array of measurements.
|
|
*/
|
|
function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
|
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
|
item =>
|
|
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
|
|
);
|
|
|
|
if (!ImagingMeasurements) {
|
|
return [];
|
|
}
|
|
|
|
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
|
|
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
|
|
);
|
|
|
|
const mergedContentSequencesByTrackingUniqueIdentifiers =
|
|
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
|
|
const measurements = [];
|
|
|
|
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
|
trackingUniqueIdentifier => {
|
|
const mergedContentSequence =
|
|
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
|
|
|
|
const measurement = _processMeasurement(mergedContentSequence);
|
|
if (measurement) {
|
|
measurements.push(measurement);
|
|
}
|
|
}
|
|
);
|
|
|
|
return measurements;
|
|
}
|
|
|
|
/**
|
|
* Retrieves merged content sequences by tracking unique identifiers.
|
|
*
|
|
* @param {Array} MeasurementGroups - The measurement groups.
|
|
* @returns {Object} - The merged content sequences by tracking unique identifiers.
|
|
*/
|
|
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
|
|
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
|
|
|
MeasurementGroups.forEach(MeasurementGroup => {
|
|
const ContentSequence = _getSequenceAsArray(MeasurementGroup.ContentSequence);
|
|
|
|
const TrackingUniqueIdentifierItem = ContentSequence.find(
|
|
item =>
|
|
item.ConceptNameCodeSequence.CodeValue ===
|
|
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
|
);
|
|
if (!TrackingUniqueIdentifierItem) {
|
|
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
|
|
}
|
|
|
|
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
|
|
|
|
if (mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] === undefined) {
|
|
// Add the full ContentSequence
|
|
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] = [
|
|
...ContentSequence,
|
|
];
|
|
} else {
|
|
// Add the ContentSequence minus the tracking identifier, as we have this
|
|
// Information in the merged ContentSequence anyway.
|
|
ContentSequence.forEach(item => {
|
|
if (
|
|
item.ConceptNameCodeSequence.CodeValue !==
|
|
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
|
) {
|
|
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return mergedContentSequencesByTrackingUniqueIdentifiers;
|
|
}
|
|
|
|
/**
|
|
* Processes the measurement based on the merged content sequence.
|
|
* If the merged content sequence contains SCOORD or SCOORD3D value types,
|
|
* it calls the _processTID1410Measurement function.
|
|
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
|
|
*
|
|
* @param {Array<Object>} mergedContentSequence - The merged content sequence to process.
|
|
* @returns {any} - The processed measurement result.
|
|
*/
|
|
function _processMeasurement(mergedContentSequence) {
|
|
if (
|
|
mergedContentSequence.some(
|
|
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
|
)
|
|
) {
|
|
return _processTID1410Measurement(mergedContentSequence);
|
|
}
|
|
|
|
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
|
|
}
|
|
|
|
/**
|
|
* Processes TID 1410 style measurements from the mergedContentSequence.
|
|
* TID 1410 style measurements have a SCOORD or SCOORD3D at the top level,
|
|
* and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D.
|
|
*
|
|
* @param mergedContentSequence - The merged content sequence containing the measurements.
|
|
* @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
|
|
*/
|
|
function _processTID1410Measurement(mergedContentSequence) {
|
|
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
|
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
|
|
|
const graphicItem = mergedContentSequence.find(
|
|
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
|
);
|
|
|
|
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
|
|
|
|
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
|
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
|
|
);
|
|
|
|
if (!graphicItem) {
|
|
console.warn(
|
|
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
|
|
|
|
const measurement = {
|
|
loaded: false,
|
|
labels: [],
|
|
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
|
|
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
|
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
|
};
|
|
|
|
NUMContentItems.forEach(item => {
|
|
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
|
|
if (MeasuredValueSequence) {
|
|
measurement.labels.push(
|
|
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
|
|
);
|
|
}
|
|
});
|
|
|
|
const findingSites = mergedContentSequence.filter(
|
|
item =>
|
|
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT &&
|
|
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT
|
|
);
|
|
if (findingSites.length) {
|
|
measurement.labels.push({
|
|
label: CodeNameCodeSequenceValues.FindingSiteSCT,
|
|
value: findingSites[0].ConceptCodeSequence.CodeMeaning,
|
|
});
|
|
}
|
|
|
|
return measurement;
|
|
}
|
|
|
|
/**
|
|
* Processes the non-geometrically defined measurement from the merged content sequence.
|
|
*
|
|
* @param mergedContentSequence The merged content sequence containing the measurement data.
|
|
* @returns The processed measurement object.
|
|
*/
|
|
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
|
|
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
|
|
|
|
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
|
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
|
|
);
|
|
|
|
const finding = mergedContentSequence.find(
|
|
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.Finding
|
|
);
|
|
|
|
const findingSites = mergedContentSequence.filter(
|
|
item =>
|
|
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SRT &&
|
|
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
|
|
);
|
|
|
|
const measurement = {
|
|
loaded: false,
|
|
labels: [],
|
|
coords: [],
|
|
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
|
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
|
};
|
|
|
|
if (
|
|
finding &&
|
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
|
finding.ConceptCodeSequence.CodingSchemeDesignator
|
|
) &&
|
|
finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
|
|
) {
|
|
measurement.labels.push({
|
|
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
|
|
value: finding.ConceptCodeSequence.CodeMeaning,
|
|
});
|
|
}
|
|
|
|
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
|
|
if (findingSites.length) {
|
|
const cornerstoneFreeTextFindingSite = findingSites.find(
|
|
FindingSite =>
|
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
|
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
|
) &&
|
|
FindingSite.ConceptCodeSequence.CodeValue ===
|
|
Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT
|
|
);
|
|
|
|
if (cornerstoneFreeTextFindingSite) {
|
|
measurement.labels.push({
|
|
label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT,
|
|
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
|
|
});
|
|
}
|
|
}
|
|
|
|
NUMContentItems.forEach(item => {
|
|
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
|
|
|
|
const { ValueType } = ContentSequence;
|
|
if (!ValueType === 'SCOORD') {
|
|
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
|
|
return;
|
|
}
|
|
|
|
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
|
|
if (coords) {
|
|
measurement.coords.push(coords);
|
|
}
|
|
|
|
if (MeasuredValueSequence) {
|
|
measurement.labels.push(
|
|
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
|
|
);
|
|
}
|
|
});
|
|
|
|
return measurement;
|
|
}
|
|
|
|
/**
|
|
* Extracts coordinates from a graphic item of type SCOORD or SCOORD3D.
|
|
* @param {object} graphicItem - The graphic item containing the coordinates.
|
|
* @returns {object} - The extracted coordinates.
|
|
*/
|
|
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
|
|
const { ValueType, GraphicType, GraphicData } = graphicItem;
|
|
const coords = { ValueType, GraphicType, GraphicData };
|
|
coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence;
|
|
coords.ReferencedFrameOfReferenceSequence =
|
|
graphicItem.ReferencedFrameOfReferenceUID ||
|
|
graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence;
|
|
return coords;
|
|
};
|
|
|
|
/**
|
|
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
|
|
* @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object.
|
|
* @param {Object} MeasuredValueSequence - The MeasuredValueSequence object.
|
|
* @returns {Object} - An object containing the label and value.
|
|
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
|
|
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
|
|
* Example: { label: 'Long Axis', value: '31.00 mm' }
|
|
*/
|
|
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
|
|
const { CodeMeaning } = ConceptNameCodeSequence;
|
|
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
|
|
const { CodeValue } = MeasurementUnitsCodeSequence;
|
|
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
|
|
return {
|
|
label: CodeMeaning,
|
|
value: `${formatedNumericValue} ${CodeValue}`,
|
|
}; // E.g. Long Axis: 31.0 mm
|
|
}
|
|
|
|
/**
|
|
* Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence.
|
|
*
|
|
* @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
|
|
* @returns {Array} - The list of referenced images.
|
|
*/
|
|
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
|
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
|
|
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
|
|
);
|
|
|
|
if (!ImageLibrary) {
|
|
return [];
|
|
}
|
|
|
|
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
|
|
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
|
|
);
|
|
if (!ImageLibraryGroup) {
|
|
return [];
|
|
}
|
|
|
|
const referencedImages = [];
|
|
|
|
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
|
const { ReferencedSOPSequence } = item;
|
|
if (!ReferencedSOPSequence) {
|
|
return;
|
|
}
|
|
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
|
|
if (ref.ReferencedSOPClassUID) {
|
|
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
|
|
|
|
referencedImages.push({
|
|
ReferencedSOPClassUID,
|
|
ReferencedSOPInstanceUID,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return referencedImages;
|
|
}
|
|
|
|
/**
|
|
* Converts a DICOM sequence to an array.
|
|
* If the sequence is null or undefined, an empty array is returned.
|
|
* If the sequence is already an array, it is returned as is.
|
|
* Otherwise, the sequence is wrapped in an array and returned.
|
|
*
|
|
* @param {any} sequence - The DICOM sequence to convert.
|
|
* @returns {any[]} - The converted array.
|
|
*/
|
|
function _getSequenceAsArray(sequence) {
|
|
if (!sequence) {
|
|
return [];
|
|
}
|
|
return Array.isArray(sequence) ? sequence : [sequence];
|
|
}
|
|
|
|
export default getSopClassHandlerModule;
|