[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-math": "^0.1.8",
"cornerstone-tools": "4.18.1", "cornerstone-tools": "4.18.1",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -34,7 +34,7 @@
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"webpack": "^4.0.0", "webpack": "^4.0.0",
"dcmjs": "0.14.2" "dcmjs": "0.16.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.7.6" "@babel/runtime": "7.7.6"

View File

@ -28,7 +28,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.11.0", "react": "^16.11.0",
"react-dom": "^16.11.0" "react-dom": "^16.11.0"

View File

@ -31,7 +31,7 @@
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.18.1", "cornerstone-tools": "4.18.1",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6" "react-dom": "^16.8.6"

View File

@ -31,7 +31,7 @@
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"cornerstone-core": "^2.2.8", "cornerstone-core": "^2.2.8",
"cornerstone-tools": "4.18.1", "cornerstone-tools": "4.18.1",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6" "react-dom": "^16.8.6"

View File

@ -35,7 +35,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.18.1", "cornerstone-tools": "4.18.1",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",

View File

@ -8,6 +8,7 @@ import { ViewportActionBar, useViewportGrid } from '@ohif/ui';
import TOOL_NAMES from './constants/toolNames'; import TOOL_NAMES from './constants/toolNames';
import { adapters } from 'dcmjs'; import { adapters } from 'dcmjs';
import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema'; import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
import id from './id'; import id from './id';
const { formatDate } = utils; const { formatDate } = utils;
@ -376,6 +377,8 @@ function OHIFCornerstoneSRViewport({
const source = MeasurementService.getSource('CornerstoneTools', '4'); const source = MeasurementService.getSource('CornerstoneTools', '4');
data.label = getLabelFromDCMJSImportedToolData(data);
MeasurementService.addRawMeasurement( MeasurementService.addRawMeasurement(
source, source,
toolType, toolType,

View File

@ -25,13 +25,19 @@ const CodeNameCodeSequenceValues = {
TrackingUniqueIdentifier: '112040', TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039', TrackingIdentifier: '112039',
Finding: '121071', Finding: '121071',
FindingSite: 'G-C0E3', // SRT
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
};
const CodingSchemeDesignators = {
SRT: 'SRT',
cornerstoneTools4: 'CST4',
}; };
const RELATIONSHIP_TYPE = { const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM', INFERRED_FROM: 'INFERRED FROM',
}; };
const CORNERSTONE_CODING_SCHEME_DESIGNATOR = 'CST4';
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT'; const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/** /**
@ -428,12 +434,20 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodeNameCodeSequenceValues.TrackingIdentifier CodeNameCodeSequenceValues.TrackingIdentifier
); );
const Findings = mergedContentSequence.filter( const Finding = mergedContentSequence.find(
item => item =>
item.ConceptNameCodeSequence.CodeValue === item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Finding CodeNameCodeSequenceValues.Finding
); );
const FindingSites = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
CodingSchemeDesignators.SRT &&
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.FindingSite
);
const measurement = { const measurement = {
loaded: false, loaded: false,
labels: [], labels: [],
@ -442,19 +456,33 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
TrackingIdentifier: TrackingIdentifierContentItem.TextValue, TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
}; };
if (Findings.length) { if (
// TODO -> Pull in labels when we have them, just free text for now. Finding &&
const cornerstoneFreeTextFinding = Findings.find( Finding.ConceptCodeSequence.CodingSchemeDesignator ===
Finding => CodingSchemeDesignators.cornerstoneTools4 &&
Finding.ConceptCodeSequence.CodingSchemeDesignator === Finding.ConceptCodeSequence.CodeValue ===
CORNERSTONE_CODING_SCHEME_DESIGNATOR && CodeNameCodeSequenceValues.CornerstoneFreeText
Finding.ConceptCodeSequence.CodeValue === ) {
CORNERSTONE_FREETEXT_CODE_VALUE 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({ measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE, 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) { function _getTextBoxLinesFromLabels(labels) {
// TODO -> max 2 for now, need a generic solution for this! // TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
const labelLength = Math.min(labels.length, 2);
const labelLength = Math.min(labels.length, 3);
const lines = []; const lines = [];
for (let i = 0; i < labelLength; i++) { 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, StudyInstanceUID,
} = instance; } = instance;
const { handles } = measurementData; const { handles, label } = measurementData;
const points = []; const points = [];
Object.keys(handles).map(handle => { Object.keys(handles).map(handle => {
@ -75,6 +75,7 @@ function Length(measurementData, imageId, _getValueTypeFromToolType) {
length: measurementData.length, length: measurementData.length,
type: _getValueTypeFromToolType(tool), type: _getValueTypeFromToolType(tool),
points, points,
label,
}; };
} }
@ -88,7 +89,7 @@ function Bidirectional(measurementData, imageId, _getValueTypeFromToolType) {
StudyInstanceUID, StudyInstanceUID,
} = instance; } = instance;
const { handles } = measurementData; const { handles, label } = measurementData;
const longAxis = [handles.start, handles.end]; const longAxis = [handles.start, handles.end];
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd]; const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
@ -106,6 +107,7 @@ function Bidirectional(measurementData, imageId, _getValueTypeFromToolType) {
longestDiameter: measurementData.longestDiameter, longestDiameter: measurementData.longestDiameter,
type: _getValueTypeFromToolType(tool), type: _getValueTypeFromToolType(tool),
points: { longAxis, shortAxis }, points: { longAxis, shortAxis },
label,
}; };
} }
@ -119,7 +121,8 @@ function EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType) {
StudyInstanceUID, StudyInstanceUID,
} = instance; } = instance;
const { start, end } = measurementData.handles; const { handles, label } = measurementData;
const { start, end } = handles;
const halfXLength = Math.abs(start.x - end.x) / 2; const halfXLength = Math.abs(start.x - end.x) / 2;
const halfYLength = Math.abs(start.y - end.y) / 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) */, .area /* TODO: Add concept names instead (descriptor) */,
type: _getValueTypeFromToolType(tool), type: _getValueTypeFromToolType(tool),
points, points,
label,
}; };
} }
@ -198,5 +202,6 @@ function ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType) {
text: measurementData.text, text: measurementData.text,
type: _getValueTypeFromToolType(tool), type: _getValueTypeFromToolType(tool),
points, points,
label,
}; };
} }

View File

@ -29,7 +29,7 @@
"peerDependencies": { "peerDependencies": {
"@ohif/core": "^0.50.0", "@ohif/core": "^0.50.0",
"cornerstone-tools": "4.18.1", "cornerstone-tools": "4.18.1",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",

View File

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

View File

@ -147,8 +147,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
} }
function exportReport() { function exportReport() {
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const measurements = MeasurementService.getMeasurements(); const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter( const trackedMeasurements = measurements.filter(
m => m =>
@ -156,8 +154,9 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
trackedSeries.includes(m.referenceSeriesUID) trackedSeries.includes(m.referenceSeriesUID)
); );
// TODO -> local download. const additionalFindings = ['ArrowAnnotate'];
DICOMSR.downloadReport(trackedMeasurements, dataSource);
DICOMSR.downloadReport(trackedMeasurements, additionalFindings);
} }
const jumpToImage = ({ id, isActive }) => { const jumpToImage = ({ id, isActive }) => {

View File

@ -33,7 +33,7 @@
"@ohif/ui": "^2.0.0", "@ohif/ui": "^2.0.0",
"cornerstone-core": "^2.3.0", "cornerstone-core": "^2.3.0",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"i18next": "^17.0.3", "i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",

View File

@ -38,7 +38,7 @@
"dependencies": { "dependencies": {
"@babel/runtime": "7.7.6", "@babel/runtime": "7.7.6",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"dicomweb-client": "^0.6.0", "dicomweb-client": "^0.6.0",
"immer": "6.0.2", "immer": "6.0.2",
"isomorphic-base64": "^1.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 {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. * that you wish to serialize.
*/ */
const downloadReport = measurementData => { const downloadReport = (measurementData, additionalFindingTypes = []) => {
const srDataset = generateReport(measurementData); const srDataset = generateReport(measurementData, additionalFindingTypes);
const reportBlob = dcmjs.data.datasetToBlob(srDataset); const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary. //Create a URL for the binary.
@ -61,10 +63,13 @@ const downloadReport = measurementData => {
* *
* @param {object[]} measurementData An array of measurements from the measurements service * @param {object[]} measurementData An array of measurements from the measurements service
* that you wish to serialize. * that you wish to serialize.
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
*/ */
const generateReport = measurementData => { const generateReport = (measurementData, additionalFindingTypes) => {
const ids = measurementData.map(md => md.id); const filteredToolState = _getFilteredCornerstoneToolState(
const filteredToolState = _getFilteredCornerstoneToolState(ids); measurementData,
additionalFindingTypes
);
const report = MeasurementReport.generateReport( const report = MeasurementReport.generateReport(
filteredToolState, filteredToolState,
@ -79,9 +84,15 @@ const generateReport = measurementData => {
* @param {object[]} measurementData An array of measurements from the measurements service * @param {object[]} measurementData An array of measurements from the measurements service
* that you wish to serialize. * that you wish to serialize.
* @param {object} dataSource The dataSource that you wish to use to persist the data. * @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 * @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, // 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. // But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements'); log.info('[DICOMSR] storeMeasurements');
@ -92,7 +103,10 @@ const storeMeasurements = async (measurementData, dataSource) => {
} }
try { try {
const naturalizedReport = generateReport(measurementData); const naturalizedReport = generateReport(
measurementData,
additionalFindingTypes
);
const { StudyInstanceUID } = naturalizedReport; const { StudyInstanceUID } = naturalizedReport;
await dataSource.store.dicom(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 globalToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
const filteredToolState = {}; const filteredToolState = {};
@ -127,9 +146,38 @@ function _getFilteredCornerstoneToolState(uidFilter) {
}; };
} }
const measurmentDataI = measurementData.find(md => md.id === toolDataI.id);
const toolData = imageIdSpecificToolState[toolType].data; 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(); const uids = uidFilter.slice();

View File

@ -69,7 +69,7 @@
"cornerstone-math": "^0.1.8", "cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.18.1", "cornerstone-tools": "4.18.1",
"cornerstone-wado-image-loader": "^3.1.2", "cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.14.2", "dcmjs": "0.16.0",
"dicom-parser": "^1.8.3", "dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4", "dicomweb-client": "^0.4.4",
"dotenv-webpack": "^1.7.0", "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" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dcmjs@0.14.2: dcmjs@0.16.0:
version "0.14.2" version "0.16.0"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.2.tgz#bc4926c3312632ec61a5d68753c0857aebb65793" resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.16.0.tgz#714755d006c3a49cf65ef13e627ff7a23151d35a"
integrity sha512-W4/f34Ph6Nrc95QhL/7f1kPlfR2YJMbyEsbPELt3xml17SFnkEgXAeO41qHZ7KywozO/lBW5sXGOpMenuYVV2g== integrity sha512-41nY3Isg5i5FoSd1s67aUgUHi146r+fb8JuiRvDsxvm7MEkkdjIzMYMtxjsrVwKRW1xRH+x34nxbV9+8cc7WQw==
dependencies: dependencies:
"@babel/polyfill" "^7.8.3" "@babel/polyfill" "^7.8.3"
"@babel/runtime" "^7.8.4" "@babel/runtime" "^7.8.4"