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,27 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
import { measurementTools } from './measurementTools';
|
||||
import { retrieveMeasurements, storeMeasurements, retrieveTimepoints, storeTimepoints, removeTimepoint, updateTimepoint, disassociateStudy } from './dataExchange';
|
||||
|
||||
export const configureApis = () => {
|
||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
||||
measurementTools,
|
||||
dataExchange: {
|
||||
retrieve: retrieveMeasurements,
|
||||
store: storeMeasurements
|
||||
},
|
||||
dataValidation: {
|
||||
validation: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
OHIF.measurements.TimepointApi.setConfiguration({
|
||||
dataExchange: {
|
||||
retrieve: retrieveTimepoints,
|
||||
store: storeTimepoints,
|
||||
remove: removeTimepoint,
|
||||
update: updateTimepoint,
|
||||
disassociate: disassociateStudy
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { handleSR, getLatestSRSeries } from "../utils/handleSR";
|
||||
|
||||
export const retrieveMeasurements = (patientId, timepointIds) => {
|
||||
OHIF.log.info('retrieveMeasurements');
|
||||
|
||||
const latestSeries = getLatestSRSeries();
|
||||
|
||||
if(!latestSeries) return Promise.resolve({});
|
||||
|
||||
return handleSR(latestSeries).then((value) => {
|
||||
return {
|
||||
length: value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const storeMeasurements = (measurementData, timepointIds) => {
|
||||
OHIF.log.info('storeMeasurements');
|
||||
|
||||
// Here is where we should do any required data transformation and API calls
|
||||
|
||||
// TODO: Write SR, STOW back to PACS
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const retrieveTimepoints = filter => {
|
||||
const studyInstanceUids = OHIF.viewer.StudyMetadataList.all().map(study => study.getStudyInstanceUID());
|
||||
OHIF.log.info('retrieveTimepoints');
|
||||
|
||||
return Promise.resolve([{
|
||||
timepointType: 'baseline',
|
||||
timepointId: 'TimepointId',
|
||||
studyInstanceUids,
|
||||
patientId: filter.patientId,
|
||||
earliestDate: new Date(),
|
||||
latestDate: new Date(),
|
||||
isLocked: false
|
||||
}]);
|
||||
};
|
||||
|
||||
export const storeTimepoints = (timepointData) => {
|
||||
OHIF.log.info('storeTimepoints');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const updateTimepoint = (timepointData, query) => {
|
||||
OHIF.log.info('updateTimepoint');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const removeTimepoint = timepointId => {
|
||||
OHIF.log.info('removeTimepoint');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const disassociateStudy = (timepointIds, studyInstanceUid) => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ToolGroupBaseSchema } from '../schema/toolGroupSchema';
|
||||
import { length } from '../schema/length';
|
||||
import { ellipticalRoi } from '../schema/ellipticalRoi';
|
||||
import { rectangleRoi } from '../schema/rectangleRoi';
|
||||
|
||||
const trackedTools = [
|
||||
length,
|
||||
ellipticalRoi,
|
||||
rectangleRoi
|
||||
];
|
||||
|
||||
export const measurementTools = [{
|
||||
id: 'allTools',
|
||||
name: 'Measurements',
|
||||
childTools: trackedTools,
|
||||
schema: ToolGroupBaseSchema
|
||||
}];
|
||||
@@ -0,0 +1,125 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Session } from 'meteor/session';
|
||||
import { configureApis } from './configuration/configuration'
|
||||
import { $ } from 'meteor/jquery';
|
||||
|
||||
class MeasurementTable {
|
||||
constructor() {
|
||||
configureApis();
|
||||
|
||||
Session.set('TimepointsReady', false);
|
||||
Session.set('MeasurementsReady', false);
|
||||
|
||||
this.firstMeasurementActivated = false;
|
||||
}
|
||||
|
||||
async onCreated(instance) {
|
||||
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
||||
|
||||
OHIF.viewer.data.currentTimepointId = 'TimepointId';
|
||||
|
||||
const timepointApi = new TimepointApi(OHIF.viewer.data.currentTimepointId);
|
||||
const measurementApi = new MeasurementApi(timepointApi);
|
||||
const apis = {
|
||||
timepointApi,
|
||||
measurementApi
|
||||
};
|
||||
|
||||
Object.assign(OHIF.viewer, apis);
|
||||
Object.assign(instance.data, apis);
|
||||
|
||||
const patientId = instance.data.studies[0].patientId;
|
||||
|
||||
await timepointApi.retrieveTimepoints({ patientId });
|
||||
Session.set('TimepointsReady', true);
|
||||
|
||||
await measurementApi.retrieveMeasurements(patientId, [OHIF.viewer.data.currentTimepointId]);
|
||||
Session.set('MeasurementsReady', true);
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
instance.autorun(() => {
|
||||
if (!Session.get('TimepointsReady') ||
|
||||
!Session.get('MeasurementsReady') ||
|
||||
!Session.get('ViewerReady') ||
|
||||
this.firstMeasurementActivated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jumpToFirstMeasurement();
|
||||
|
||||
});
|
||||
|
||||
instance.measurementModifiedHandler = _.throttle((event, instance) => {
|
||||
OHIF.measurements.MeasurementHandlers.onModified(event, instance);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
onDestroyed() {
|
||||
Session.set('TimepointsReady', false);
|
||||
Session.set('MeasurementsReady', false);
|
||||
}
|
||||
|
||||
jumpToFirstMeasurement() {
|
||||
// Find and activate the first measurement by Lesion Number
|
||||
// NOTE: This is inefficient, we should be using a hanging protocol
|
||||
// to hang the first measurement's imageId immediately, rather
|
||||
// than changing images after initial loading...
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const tools = config.measurementTools[0].childTools;
|
||||
const firstTool = tools[Object.keys(tools)[0]];
|
||||
const measurementTypeId = firstTool.id;
|
||||
|
||||
const collection = OHIF.viewer.measurementApi.tools[measurementTypeId];
|
||||
const sorting = {
|
||||
sort: {
|
||||
measurementNumber: -1
|
||||
}
|
||||
};
|
||||
|
||||
const data = collection.find({}, sorting).fetch();
|
||||
|
||||
// TODO: Clean this up, it's probably an inefficient way to get what we need
|
||||
const groupObject = _.groupBy(data, m => m.measurementNumber);
|
||||
|
||||
// Reformat the data
|
||||
const rows = Object.keys(groupObject).map(key => ({
|
||||
measurementTypeId: measurementTypeId,
|
||||
measurementNumber: key,
|
||||
entries: groupObject[key]
|
||||
}));
|
||||
|
||||
const rowItem = rows[0];
|
||||
const timepoints = [
|
||||
OHIF.viewer.timepointApi.current()
|
||||
];
|
||||
|
||||
if (rowItem) {
|
||||
OHIF.measurements.jumpToRowItem(rowItem, timepoints);
|
||||
}
|
||||
|
||||
this.firstMeasurementActivated = true;
|
||||
}
|
||||
};
|
||||
|
||||
const measurementEvents = {
|
||||
'cornerstonetoolsmeasurementadded .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
OHIF.measurements.MeasurementHandlers.onAdded(originalEvent, instance);
|
||||
},
|
||||
|
||||
'cornerstonetoolsmeasurementmodified .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
instance.measurementModifiedHandler(originalEvent, instance);
|
||||
},
|
||||
|
||||
'cornerstonemeasurementremoved .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved(originalEvent, instance);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
MeasurementTable,
|
||||
measurementEvents
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements';
|
||||
|
||||
const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema;
|
||||
|
||||
const EllipticalRoiHandlesSchema = new SimpleSchema({
|
||||
start: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Start'
|
||||
},
|
||||
end: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'End'
|
||||
},
|
||||
textBox: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Text Box'
|
||||
},
|
||||
});
|
||||
|
||||
const MeanStdDevSchema = new SimpleSchema({
|
||||
count: {
|
||||
type: Number,
|
||||
label: 'count',
|
||||
decimal: true
|
||||
},
|
||||
mean: {
|
||||
type: Number,
|
||||
label: 'mean',
|
||||
decimal: true
|
||||
},
|
||||
stdDev: {
|
||||
type: Number,
|
||||
label: 'stdDev',
|
||||
decimal: true
|
||||
},
|
||||
variance: {
|
||||
type: Number,
|
||||
label: 'variance',
|
||||
decimal: true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const EllipticalRoiSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, {
|
||||
handles: {
|
||||
type: EllipticalRoiHandlesSchema,
|
||||
label: 'Handles'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
},
|
||||
toolType: {
|
||||
type: String,
|
||||
label: 'Measurement Tool Type',
|
||||
defaultValue: 'ellipticalRoi'
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
label: 'Location',
|
||||
optional: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
label: 'Description',
|
||||
optional: true
|
||||
},
|
||||
area: {
|
||||
type: Number,
|
||||
label: 'Ellipse Area value',
|
||||
decimal: true,
|
||||
optional: true
|
||||
},
|
||||
meanStdDev: {
|
||||
type: MeanStdDevSchema,
|
||||
label: 'MeanStd Values',
|
||||
optional: true
|
||||
}
|
||||
}]);
|
||||
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
if (data.meanStdDev && data.meanStdDev.mean) {
|
||||
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
// let meanValue = data.meanStdDev && data.meanStdDev.mean || 0;
|
||||
// return numberWithCommas(meanValue).toFixed(2) + ' HU';
|
||||
//return data.meanStdDev.mean.toFixed(2);
|
||||
};
|
||||
|
||||
export const ellipticalRoi = {
|
||||
id: 'ellipticalRoi',
|
||||
name: 'Ellipse',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'ellipticalRoi',
|
||||
schema: EllipticalRoiSchema,
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements';
|
||||
|
||||
const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema;
|
||||
|
||||
const LengthHandlesSchema = new SimpleSchema({
|
||||
start: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Start'
|
||||
},
|
||||
end: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'End'
|
||||
},
|
||||
textBox: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Text Box'
|
||||
}
|
||||
});
|
||||
|
||||
const LengthSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, {
|
||||
handles: {
|
||||
type: LengthHandlesSchema,
|
||||
label: 'Handles'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
label: 'Location',
|
||||
optional: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
label: 'Description',
|
||||
optional: true
|
||||
},
|
||||
toolType: {
|
||||
type: String,
|
||||
label: 'Measurement Tool Type',
|
||||
defaultValue: 'length'
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
label: 'Length',
|
||||
optional: true,
|
||||
decimal: true
|
||||
}
|
||||
}]);
|
||||
|
||||
const displayFunction = data => {
|
||||
let lengthValue = '';
|
||||
if (data.length) {
|
||||
lengthValue = data.length.toFixed(2) + ' mm';
|
||||
}
|
||||
return lengthValue;
|
||||
};
|
||||
|
||||
export const length = {
|
||||
id: 'length',
|
||||
name: 'Length',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'length',
|
||||
schema: LengthSchema,
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements';
|
||||
|
||||
const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema;
|
||||
|
||||
const RectangleRoiHandlesSchema = new SimpleSchema({
|
||||
start: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Start'
|
||||
},
|
||||
end: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'End'
|
||||
},
|
||||
textBox: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Text Box'
|
||||
},
|
||||
});
|
||||
|
||||
const MeanStdDevSchema = new SimpleSchema({
|
||||
count: {
|
||||
type: Number,
|
||||
label: 'count',
|
||||
decimal: true
|
||||
},
|
||||
mean: {
|
||||
type: Number,
|
||||
label: 'mean',
|
||||
decimal: true
|
||||
},
|
||||
stdDev: {
|
||||
type: Number,
|
||||
label: 'stdDev',
|
||||
decimal: true
|
||||
},
|
||||
variance: {
|
||||
type: Number,
|
||||
label: 'variance',
|
||||
decimal: true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const RectangleRoiSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, {
|
||||
handles: {
|
||||
type: RectangleRoiHandlesSchema,
|
||||
label: 'Handles'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
},
|
||||
toolType: {
|
||||
type: String,
|
||||
label: 'Measurement Tool Type',
|
||||
defaultValue: 'rectangleRoi'
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
label: 'Location',
|
||||
optional: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
label: 'Description',
|
||||
optional: true
|
||||
},
|
||||
area: {
|
||||
type: Number,
|
||||
label: 'Rectangle Area value',
|
||||
decimal: true,
|
||||
optional: true
|
||||
},
|
||||
meanStdDev: {
|
||||
type: MeanStdDevSchema,
|
||||
label: 'MeanStd Values',
|
||||
optional: true
|
||||
}
|
||||
}]);
|
||||
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
if (data.meanStdDev && data.meanStdDev.mean) {
|
||||
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
};
|
||||
|
||||
export const rectangleRoi = {
|
||||
id: 'rectangleRoi',
|
||||
name: 'Rectangle',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'rectangleRoi',
|
||||
schema: RectangleRoiSchema,
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
|
||||
export const ToolGroupBaseSchema = new SimpleSchema({
|
||||
toolId: {
|
||||
type: String,
|
||||
label: 'Tool ID',
|
||||
optional: true
|
||||
},
|
||||
toolItemId: {
|
||||
type: String,
|
||||
label: 'Tool Item ID',
|
||||
optional: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date
|
||||
},
|
||||
studyInstanceUid: {
|
||||
type: String,
|
||||
label: 'Study Instance UID'
|
||||
},
|
||||
timepointId: {
|
||||
type: String,
|
||||
label: 'Timepoint ID'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
}
|
||||
});
|
||||
@@ -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