feat(measurement-table) Display support for measurements from DICOM Structured Reports (#225)
This commit is contained in:
1 parent
e9832f62f7
commit
d94f0eec16
64 files changed
+1797
-78
No files matched your search
@@ -0,0 +1,95 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
function getInstanceMetadata (displaySets, sopInstanceUid) {
|
||||
let instance;
|
||||
|
||||
// Use Array.some so that this loop stops when the internal loop
|
||||
// has found the correct instance
|
||||
displaySets.some(displaySet => {
|
||||
// Search the display set to find the instance metadata for
|
||||
return displaySet.images.find(instanceMetadata => {
|
||||
if (instanceMetadata._sopInstanceUID === sopInstanceUid) {
|
||||
instance = instanceMetadata;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export default function getLengthMeasurementData(lengthMeasurementContent, displaySets) {
|
||||
let lengthStates = [];
|
||||
|
||||
lengthMeasurementContent.forEach(groupItemContent => {
|
||||
const lengthContent = groupItemContent.ContentSequence;
|
||||
const reference = lengthContent.ContentSequence.ReferencedSOPSequence;
|
||||
const lengthState = {};
|
||||
|
||||
lengthState.measuredValue = groupItemContent.MeasuredValueSequence.NumericValue;
|
||||
lengthState.handles = {start: {}, end: {}};
|
||||
[lengthState.handles.start.x,
|
||||
lengthState.handles.start.y,
|
||||
lengthState.handles.end.x,
|
||||
lengthState.handles.end.y] = lengthContent.GraphicData;
|
||||
|
||||
// TODO: Save textbox position in GraphicData?
|
||||
lengthState.handles.textBox = {
|
||||
hasMoved: false,
|
||||
movesIndependently: false,
|
||||
drawnIndependently: true,
|
||||
allowedOutsideImage: true,
|
||||
hasBoundingBox: true
|
||||
}
|
||||
|
||||
lengthState.ReferencedInstanceUID = reference.ReferencedSOPInstanceUID;
|
||||
if (reference.ReferencedFrameNumber && reference.ReferencedFrameNumber !== 'NaN') {
|
||||
lengthState.ReferencedFrameNumber = reference.ReferencedFrameNumber;
|
||||
} else {
|
||||
lengthState.ReferencedFrameNumber = 0;
|
||||
}
|
||||
|
||||
lengthStates.push(lengthState);
|
||||
});
|
||||
|
||||
const lengthMeasurementData = [];
|
||||
|
||||
let measurementNumber = 0;
|
||||
lengthStates.forEach(lengthState => {
|
||||
const sopInstanceUid = lengthState.ReferencedInstanceUID;
|
||||
const instanceMetadata = getInstanceMetadata(displaySets, sopInstanceUid);
|
||||
const imageId = OHIF.viewerbase.getImageId(instanceMetadata);
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const studyInstanceUid = cornerstone.metaData.get('study', imageId).studyInstanceUid;
|
||||
const seriesInstanceUid = cornerstone.metaData.get('series', imageId).seriesInstanceUid;
|
||||
const patientId = instanceMetadata._study.patientId;
|
||||
const frameIndex = lengthState.ReferencedFrameNumber;
|
||||
const imagePath = [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join('_');
|
||||
const measurement = {
|
||||
handles: lengthState.handles,
|
||||
length: lengthState.measuredValue,
|
||||
imageId,
|
||||
imagePath,
|
||||
sopInstanceUid,
|
||||
seriesInstanceUid,
|
||||
studyInstanceUid,
|
||||
patientId,
|
||||
frameIndex,
|
||||
measurementNumber: ++measurementNumber,
|
||||
userId: 'UserID',
|
||||
timepointId: OHIF.viewer.data.currentTimepointId,
|
||||
toolType: 'length',
|
||||
_id: imageId + measurementNumber,
|
||||
};
|
||||
|
||||
lengthMeasurementData.push(measurement);
|
||||
});
|
||||
|
||||
|
||||
return lengthMeasurementData;
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { dcmjs } from 'meteor/ohif:cornerstone';
|
||||
import getLengthMeasurementData from './getLengthMeasurementData';
|
||||
|
||||
const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22'];
|
||||
|
||||
const toArray = function(x) {
|
||||
return (x.constructor.name === "Array" ? x : [x]);
|
||||
};
|
||||
|
||||
const codeMeaningEquals = (codeMeaningName) => {
|
||||
return (contentItem) => {
|
||||
return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName;
|
||||
};
|
||||
};
|
||||
|
||||
const imagingMeasurementsToMeasurementData = (dataset, displaySets) => {
|
||||
// Identify the Imaging Measurements
|
||||
const imagingMeasurementContent = toArray(dataset.ContentSequence).find(codeMeaningEquals("Imaging Measurements"));
|
||||
|
||||
// Retrieve the Measurements themselves
|
||||
const measurementGroupContent = toArray(imagingMeasurementContent.ContentSequence).find(codeMeaningEquals("Measurement Group"));
|
||||
|
||||
// For now, bail out if the dataset is not a TID1500 SR with length measurements
|
||||
// TODO: generalize to the various kinds of report
|
||||
// TODO: generalize to the kinds of measurements the Viewer supports
|
||||
if (dataset.ContentTemplateSequence.TemplateIdentifier !== "1500") {
|
||||
OHIF.log.warn("This package can currently only interpret DICOM SR TID 1500");
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Filter to find Length measurements in the Structured Report
|
||||
const lengthMeasurementContent = toArray(measurementGroupContent.ContentSequence).filter(codeMeaningEquals("Length"));
|
||||
|
||||
// Retrieve Length Measurement Data
|
||||
return getLengthMeasurementData(lengthMeasurementContent, displaySets);
|
||||
};
|
||||
|
||||
const retrieveDataFromSR = (Part10SRArrayBuffer) => {
|
||||
const allStudies = OHIF.viewer.Studies.all();
|
||||
let allDisplaySets = [];
|
||||
|
||||
allStudies.forEach(study => {
|
||||
allDisplaySets = allDisplaySets.concat(study.displaySets);
|
||||
});
|
||||
|
||||
// get the dicom data as an Object
|
||||
let dicomData = dcmjs.data.DicomMessage.readFile(Part10SRArrayBuffer);
|
||||
let dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
|
||||
// Convert the SR into the kind of object the Measurements package is expecting
|
||||
return imagingMeasurementsToMeasurementData(dataset, allDisplaySets);
|
||||
};
|
||||
|
||||
export const getLatestSRSeries = () => {
|
||||
const allStudies = OHIF.viewer.StudyMetadataList.all();
|
||||
let latestSeries;
|
||||
|
||||
allStudies.forEach(study => {
|
||||
study.getSeries().forEach(series => {
|
||||
const firstInstance = series.getFirstInstance();
|
||||
const sopClassUid = firstInstance._instance.sopClassUid;
|
||||
|
||||
if (supportedSopClassUIDs.includes(sopClassUid)) {
|
||||
if(!latestSeries) {
|
||||
latestSeries = series;
|
||||
} else if (series._data.seriesDate > latestSeries._data.seriesDate ||
|
||||
(series._data.seriesDate === latestSeries._data.seriesDate && series._data.seriesTime > latestSeries._data.seriesTime)) {
|
||||
latestSeries = series;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return latestSeries;
|
||||
};
|
||||
|
||||
export const handleSR = (series) => {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.responseType = 'arraybuffer';
|
||||
request.open('GET', instance.getDataProperty('wadouri'));
|
||||
|
||||
request.onload = function (progressEvent) {
|
||||
const data = retrieveDataFromSR(progressEvent.currentTarget.response);
|
||||
|
||||
resolve(data);
|
||||
};
|
||||
|
||||
request.onerror = function(error) {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
request.send();
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user