import { ImageSet } from './classes/ImageSet'; const sortBySeriesNumberAndAcquisition = (a, b) => { if (a.seriesNumber && b.seriesNumber) { if (a.seriesNumber !== b.seriesNumber) { return a.seriesNumber - b.seriesNumber; } } if (a.acquisitionDateTime && b.acquisitionDateTime) { return a.acquisitionDateTime.localeCompare(b.acquisitionDateTime); } }; const makeImageSet = sopInstances => { const imageSet = new ImageSet(sopInstances); const sopInstance = sopInstances[0]; // list of attributes to be appended to ImageSet instance... const attributes = { seriesInstanceUid: sopInstance.getRawValue('x0020000e'), seriesNumber: sopInstance.getIntValue('x00200011'), seriesDescription: sopInstance.getRawValue('x0008103e', ''), acquisitionDateTime: sopInstance.getRawValue('x00080022', '') + sopInstance.getRawValue('x00080032', ''), numImageFrames: sopInstances.length, frameRate: sopInstance.getFloatValue('x00181063', 0, 0), studyInstanceUid: sopInstance.getRawValue('x0020000d'), displaySetInstanceUid: imageSet.uid // create an alias for the imageSet UID }; const frameTime = sopInstance.getFloatValue('x00181063', 0, 0); const recommendedDisplayFrameRate = sopInstance.getFloatValue('x00082144', 0, 0); if (frameTime) { // FrameTime is in milliseconds, so we use 1000/frameTime to calculate // the number of frames per second. attributes.frameRate = 1000 / frameTime; attributes.isClip = true; } else if (recommendedDisplayFrameRate) { // Recommended Display FrameRate is specified in frames per second as an integer attributes.frameRate = recommendedDisplayFrameRate; attributes.isClip = true; } imageSet.setAttributes(attributes); // Sort the images in this series imageSet.sortBy( (a, b) => { const aInstanceNumber = a.getFloatValue('x00200013'); const bInstanceNumber = b.getFloatValue('x00200013'); if (aInstanceNumber && bInstanceNumber) { if (aInstanceNumber !== bInstanceNumber) { return aInstanceNumber - bInstanceNumber; } } const aAcquisitionNumber = a.getFloatValue('x00200012'); const bAcquisitionNumber = b.getFloatValue('x00200012'); if (aAcquisitionNumber && bAcquisitionNumber) { if (aAcquisitionNumber !== bAcquisitionNumber) { return aAcquisitionNumber - bAcquisitionNumber; } } const aAcquisitionDateTime = a.getRawValue('x00080022', '') + a.getRawValue('x00080032', ''); const bAcquisitionDateTime = b.getRawValue('x00080022', '') + b.getRawValue('x00080032', ''); if (aAcquisitionDateTime && bAcquisitionDateTime) { return aAcquisitionDateTime.localeCompare(bAcquisitionDateTime); } }); return imageSet; }; const isMultiFrame = instanceView => { const numFrames = instanceView.getIntValue('x00280008'); return (numFrames > 1); }; const isSingleImageModality = modality => { const singleImageModalities = ['CR', 'MG', 'DX', 'RG', 'PX', 'XA', 'XR'] return singleImageModalities.indexOf(modality) > -1; }; const seriesSplittingRules = [ { tag: 'x00180010', // ContrastBolusAgent }, { tag: 'x00180086', // Echo Number }, // --- Some other potential examples of tags to split on --- // /*{ tag: 'x00180020', // Scanning Sequence }, { tag: 'x00180050', // Slice Thickness },*/ /*{ tag: 'x00180081', // Echo Time }, { tag: 'x00181210', // Convolution Kernel },*/ /*{ tag: 'x00200012', // Acquisition Number }*/ ]; const stackOfInstancesToDisplaySets = (sopInstances, seriesView) => { // If no instances are provided, return an empty array if (!sopInstances.length) { return []; } // Create an empty array to store our display sets in const displaySets = []; // Use Underscore's groupBy function to group our sopInstances // by a key. In our case, the key is a combined string which represents // the value of the DICOM tags listed in the seriesSplittingRules array. // Currently this uses tags for EchoNumber and ContrastBolusAgent. const groups = _.groupBy(sopInstances, sopInstance => { // Create an empty string to store our key let result = ''; const instanceView = sopInstance.getData(); // For each rule, find out if the tag exists seriesSplittingRules.forEach(rule => { // Include a delimitation character between each tag value result += ';' // If the tag exists, concatenate its value to our key if (instanceView.exists(rule.tag)) { result += instanceView.raw(rule.tag); } }); // Return the key to be used for grouping. return result; }) const seriesInstanceCount = seriesView.getInstanceCount(); Object.keys(groups).forEach(groupKey => { // For each group, obtain the instances in the stack const stackInstances = groups[groupKey]; // Create a new display set from the images in this stack const displaySet = makeImageSet(stackInstances); displaySet.setAttribute('seriesInstanceCount', seriesInstanceCount); displaySets.push(displaySet); }); return displaySets; }; /** * This function extracts and produces sorted lists of image sets and non-viewable instance * sets from a studyMetadata {StudyMetadata instance}. * * If an optional series number is specified, only the image set with this series number * will be returned (as the sole element in an array). * * The function returns an object with two fields, imageSets and nonViewable, both of which * are arrays. imageSets is an array of sorted image sets and nonViewable is an array of * non-viewable sopInstances. * * @param studyMetadata * @param {number} [seriesNumber] One series number. If specified, the function only returns the imageSet with this seriesNumber * * @returns {{imageSets: Array, nonViewable: Array}} */ getSortedData = function(studyMetadata, seriesNumber) { let imageSets = []; let nonViewable = []; studyMetadata.forEachSeries((series, seriesNum) => { const firstInstance = series.getInstanceByIndex(0); // If a specific series number is given, skip all series except this one if (seriesNumber !== undefined && firstInstance.getIntValue('x00200011', '') !== seriesNumber) { return; } let sopInstances = []; let imageSet; const seriesInstanceCount = series.getInstanceCount(); series.forEachInstance(instanceMetadata => { // Skip non image types (an image must have the 'rows' tag) const rows = instanceMetadata.tagExists('x00280010'); if (!rows) { nonViewable.push(instanceMetadata); return; } const modality = firstInstance.getRawValue('x00080060', ''); const singleImageModality = isSingleImageModality(modality); // If we detect a multi-frame instance if (isMultiFrame(instanceMetadata)) { // First, sort all sopInstances currently in our stack into display sets using // our series splitting rules const displaySets = stackOfInstancesToDisplaySets(sopInstances, series); imageSets = imageSets.concat(displaySets); // Next, make another display set for the multi-frame instance imageSet = makeImageSet([ instanceMetadata ]); imageSet.setAttributes({ seriesInstanceCount: seriesInstanceCount, numImageFrames: instanceMetadata.getIntValue('x00280008', 0) }); imageSets.push(imageSet); // Reset our stack of sopInstances sopInstances = []; } else if (singleImageModality) { imageSet = makeImageSet([ instanceMetadata ]); imageSet.setAttribute('seriesInstanceCount', seriesInstanceCount); imageSets.push(imageSet); } else { // If no multi-frame instances have been discovered sopInstances.push(instanceMetadata); } }); // Create display sets from any remaining sopInstances in the series const displaySets = stackOfInstancesToDisplaySets(sopInstances, series); imageSets = imageSets.concat(displaySets); }); // sort imageSets imageSets.sort(sortBySeriesNumberAndAcquisition); nonViewable.sort(sortBySeriesNumberAndAcquisition); return { imageSets, nonViewable }; }; export { getSortedData };