[OHIF-288] Rehydrating SRs is missing Labels (#1885)
* WIP * Finish OHIF-288, need to update dcmjs. * Remove unused copied resources. * Cleaned up label import code. * Update for dcmjs Findings -> Finding. * Update dcmjs * Remove debuggers * Respond to reviewer comments. * update dcmjs version Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
This commit is contained in:
parent
ce40f04f2e
commit
3eba027003
@ -35,7 +35,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "4.18.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.2",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"webpack": "^4.0.0",
|
||||
"dcmjs": "0.14.2"
|
||||
"dcmjs": "0.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6"
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0"
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.18.1",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.18.1",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "4.18.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.2",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@ -8,6 +8,7 @@ import { ViewportActionBar, useViewportGrid } from '@ohif/ui';
|
||||
import TOOL_NAMES from './constants/toolNames';
|
||||
import { adapters } from 'dcmjs';
|
||||
import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
|
||||
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
|
||||
import id from './id';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@ -376,6 +377,8 @@ function OHIFCornerstoneSRViewport({
|
||||
|
||||
const source = MeasurementService.getSource('CornerstoneTools', '4');
|
||||
|
||||
data.label = getLabelFromDCMJSImportedToolData(data);
|
||||
|
||||
MeasurementService.addRawMeasurement(
|
||||
source,
|
||||
toolType,
|
||||
|
||||
@ -25,13 +25,19 @@ const CodeNameCodeSequenceValues = {
|
||||
TrackingUniqueIdentifier: '112040',
|
||||
TrackingIdentifier: '112039',
|
||||
Finding: '121071',
|
||||
FindingSite: 'G-C0E3', // SRT
|
||||
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
|
||||
};
|
||||
|
||||
const CodingSchemeDesignators = {
|
||||
SRT: 'SRT',
|
||||
cornerstoneTools4: 'CST4',
|
||||
};
|
||||
|
||||
const RELATIONSHIP_TYPE = {
|
||||
INFERRED_FROM: 'INFERRED FROM',
|
||||
};
|
||||
|
||||
const CORNERSTONE_CODING_SCHEME_DESIGNATOR = 'CST4';
|
||||
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||
|
||||
/**
|
||||
@ -428,12 +434,20 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
);
|
||||
|
||||
const Findings = mergedContentSequence.filter(
|
||||
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: [],
|
||||
@ -442,19 +456,33 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||
};
|
||||
|
||||
if (Findings.length) {
|
||||
// TODO -> Pull in labels when we have them, just free text for now.
|
||||
const cornerstoneFreeTextFinding = Findings.find(
|
||||
Finding =>
|
||||
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||
CORNERSTONE_CODING_SCHEME_DESIGNATOR &&
|
||||
Finding.ConceptCodeSequence.CodeValue ===
|
||||
CORNERSTONE_FREETEXT_CODE_VALUE
|
||||
if (
|
||||
Finding &&
|
||||
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||
Finding.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
) {
|
||||
measurement.labels.push({
|
||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||
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 =>
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
);
|
||||
if (cornerstoneFreeTextFinding) {
|
||||
|
||||
if (cornerstoneFreeTextFindingSite) {
|
||||
measurement.labels.push({
|
||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||
value: cornerstoneFreeTextFinding.ConceptCodeSequence.CodeMeaning,
|
||||
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,10 +280,9 @@ export default class DICOMSRDisplayTool extends BaseTool {
|
||||
}
|
||||
|
||||
function _getTextBoxLinesFromLabels(labels) {
|
||||
// TODO -> max 2 for now, need a generic solution for this!
|
||||
|
||||
const labelLength = Math.min(labels.length, 2);
|
||||
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
|
||||
const labelLength = Math.min(labels.length, 3);
|
||||
const lines = [];
|
||||
|
||||
for (let i = 0; i < labelLength; i++) {
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Extracts the label from the toolData imported from dcmjs. We need to do this
|
||||
* as dcmjs does not depeend on OHIF/the MeasurementService, it just produces data for cornestoneTools.
|
||||
* This optional data is available for the consumer to process if they wish to.
|
||||
* @param {object} toolData The tooldata relating to the
|
||||
*
|
||||
* @returns {string} The extracted label.
|
||||
*/
|
||||
export default function getLabelFromDCMJSImportedToolData(toolData) {
|
||||
const { findingSites = [], finding } = toolData;
|
||||
|
||||
let freeTextLabel = findingSites.find(
|
||||
fs => fs.CodeValue === 'CORNERSTONEFREETEXT'
|
||||
);
|
||||
|
||||
if (freeTextLabel) {
|
||||
return freeTextLabel.CodeMeaning;
|
||||
}
|
||||
|
||||
if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') {
|
||||
return finding.CodeMeaning;
|
||||
}
|
||||
}
|
||||
@ -51,7 +51,7 @@ function Length(measurementData, imageId, _getValueTypeFromToolType) {
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const { handles } = measurementData;
|
||||
const { handles, label } = measurementData;
|
||||
|
||||
const points = [];
|
||||
Object.keys(handles).map(handle => {
|
||||
@ -75,6 +75,7 @@ function Length(measurementData, imageId, _getValueTypeFromToolType) {
|
||||
length: measurementData.length,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
@ -88,7 +89,7 @@ function Bidirectional(measurementData, imageId, _getValueTypeFromToolType) {
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const { handles } = measurementData;
|
||||
const { handles, label } = measurementData;
|
||||
|
||||
const longAxis = [handles.start, handles.end];
|
||||
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
||||
@ -106,6 +107,7 @@ function Bidirectional(measurementData, imageId, _getValueTypeFromToolType) {
|
||||
longestDiameter: measurementData.longestDiameter,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points: { longAxis, shortAxis },
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
@ -119,7 +121,8 @@ function EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType) {
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const { start, end } = measurementData.handles;
|
||||
const { handles, label } = measurementData;
|
||||
const { start, end } = handles;
|
||||
|
||||
const halfXLength = Math.abs(start.x - end.x) / 2;
|
||||
const halfYLength = Math.abs(start.y - end.y) / 2;
|
||||
@ -161,6 +164,7 @@ function EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType) {
|
||||
.area /* TODO: Add concept names instead (descriptor) */,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
@ -198,5 +202,6 @@ function ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType) {
|
||||
text: measurementData.text,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-tools": "4.18.1",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
|
||||
@ -18,7 +18,8 @@ async function createReportAsync(servicesManager, dataSource, measurements) {
|
||||
try {
|
||||
const naturalizedReport = await DICOMSR.storeMeasurements(
|
||||
measurements,
|
||||
dataSource
|
||||
dataSource,
|
||||
['ArrowAnnotate']
|
||||
);
|
||||
|
||||
DisplaySetService.makeDisplaySets([naturalizedReport], {
|
||||
|
||||
@ -147,8 +147,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m =>
|
||||
@ -156,8 +154,9 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
|
||||
// TODO -> local download.
|
||||
DICOMSR.downloadReport(trackedMeasurements, dataSource);
|
||||
const additionalFindings = ['ArrowAnnotate'];
|
||||
|
||||
DICOMSR.downloadReport(trackedMeasurements, additionalFindings);
|
||||
}
|
||||
|
||||
const jumpToImage = ({ id, isActive }) => {
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"cornerstone-core": "^2.3.0",
|
||||
"cornerstone-wado-image-loader": "^3.1.2",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
|
||||
@ -46,10 +46,12 @@ const retrieveMeasurements = server => {
|
||||
/**
|
||||
*
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* as opposed to Finding Sites.
|
||||
* that you wish to serialize.
|
||||
*/
|
||||
const downloadReport = measurementData => {
|
||||
const srDataset = generateReport(measurementData);
|
||||
const downloadReport = (measurementData, additionalFindingTypes = []) => {
|
||||
const srDataset = generateReport(measurementData, additionalFindingTypes);
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
@ -61,10 +63,13 @@ const downloadReport = measurementData => {
|
||||
*
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
*/
|
||||
const generateReport = measurementData => {
|
||||
const ids = measurementData.map(md => md.id);
|
||||
const filteredToolState = _getFilteredCornerstoneToolState(ids);
|
||||
const generateReport = (measurementData, additionalFindingTypes) => {
|
||||
const filteredToolState = _getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
);
|
||||
|
||||
const report = MeasurementReport.generateReport(
|
||||
filteredToolState,
|
||||
@ -79,9 +84,15 @@ const generateReport = measurementData => {
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
* @param {object} dataSource The dataSource that you wish to use to persist the data.
|
||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* as opposed to Finding Sites.
|
||||
* @return {object} The naturalized report
|
||||
*/
|
||||
const storeMeasurements = async (measurementData, dataSource) => {
|
||||
const storeMeasurements = async (
|
||||
measurementData,
|
||||
dataSource,
|
||||
additionalFindingTypes = []
|
||||
) => {
|
||||
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
|
||||
// But it is good enough for now whilst we only have cornerstone as a datasource.
|
||||
log.info('[DICOMSR] storeMeasurements');
|
||||
@ -92,7 +103,10 @@ const storeMeasurements = async (measurementData, dataSource) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const naturalizedReport = generateReport(measurementData);
|
||||
const naturalizedReport = generateReport(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
);
|
||||
const { StudyInstanceUID } = naturalizedReport;
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport);
|
||||
@ -110,7 +124,12 @@ const storeMeasurements = async (measurementData, dataSource) => {
|
||||
}
|
||||
};
|
||||
|
||||
function _getFilteredCornerstoneToolState(uidFilter) {
|
||||
function _getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
) {
|
||||
const uidFilter = measurementData.map(md => md.id);
|
||||
|
||||
const globalToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
||||
const filteredToolState = {};
|
||||
|
||||
@ -127,9 +146,38 @@ function _getFilteredCornerstoneToolState(uidFilter) {
|
||||
};
|
||||
}
|
||||
|
||||
const measurmentDataI = measurementData.find(md => md.id === toolDataI.id);
|
||||
|
||||
const toolData = imageIdSpecificToolState[toolType].data;
|
||||
|
||||
toolData.push(toolDataI);
|
||||
let finding;
|
||||
const findingSites = [];
|
||||
|
||||
// NOTE -> Any kind of freetext value abuses the DICOM standard,
|
||||
// As CodeValues should map 1:1 with CodeMeanings.
|
||||
// Ideally we would actually use SNOMED codes for this.
|
||||
if (measurmentDataI.label) {
|
||||
if (additionalFindingTypes.includes(toolType)) {
|
||||
finding = {
|
||||
CodeValue: 'CORNERSTONEFREETEXT',
|
||||
CodingSchemeDesignator: 'CST4',
|
||||
CodeMeaning: measurmentDataI.label,
|
||||
};
|
||||
} else {
|
||||
findingSites.push({
|
||||
CodeValue: 'CORNERSTONEFREETEXT',
|
||||
CodingSchemeDesignator: 'CST4',
|
||||
CodeMeaning: measurmentDataI.label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const measurement = Object.assign({}, toolDataI, {
|
||||
finding,
|
||||
findingSites,
|
||||
});
|
||||
|
||||
toolData.push(measurement);
|
||||
}
|
||||
|
||||
const uids = uidFilter.slice();
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "4.18.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.2",
|
||||
"dcmjs": "0.14.2",
|
||||
"dcmjs": "0.16.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.4.4",
|
||||
"dotenv-webpack": "^1.7.0",
|
||||
|
||||
@ -7364,10 +7364,10 @@ dateformat@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
||||
|
||||
dcmjs@0.14.2:
|
||||
version "0.14.2"
|
||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.2.tgz#bc4926c3312632ec61a5d68753c0857aebb65793"
|
||||
integrity sha512-W4/f34Ph6Nrc95QhL/7f1kPlfR2YJMbyEsbPELt3xml17SFnkEgXAeO41qHZ7KywozO/lBW5sXGOpMenuYVV2g==
|
||||
dcmjs@0.16.0:
|
||||
version "0.16.0"
|
||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.16.0.tgz#714755d006c3a49cf65ef13e627ff7a23151d35a"
|
||||
integrity sha512-41nY3Isg5i5FoSd1s67aUgUHi146r+fb8JuiRvDsxvm7MEkkdjIzMYMtxjsrVwKRW1xRH+x34nxbV9+8cc7WQw==
|
||||
dependencies:
|
||||
"@babel/polyfill" "^7.8.3"
|
||||
"@babel/runtime" "^7.8.4"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user