[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:
James Petts 2020-07-10 20:11:22 +01:00 committed by GitHub
parent ce40f04f2e
commit 3eba027003
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 152 additions and 46 deletions

View File

@ -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",

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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",

View File

@ -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,

View File

@ -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,
});
}
}

View File

@ -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++) {

View File

@ -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;
}
}

View File

@ -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,
};
}

View File

@ -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",

View File

@ -18,7 +18,8 @@ async function createReportAsync(servicesManager, dataSource, measurements) {
try {
const naturalizedReport = await DICOMSR.storeMeasurements(
measurements,
dataSource
dataSource,
['ArrowAnnotate']
);
DisplaySetService.makeDisplaySets([naturalizedReport], {

View File

@ -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 }) => {

View File

@ -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",

View File

@ -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",

View File

@ -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();

View File

@ -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",

View File

@ -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"