Merge pull request #1846 from OHIF/OHIF-200

[OHIF-200] - Add callouts to all DICOM SR read only measurements.
This commit is contained in:
Danny Brown 2020-07-01 10:24:01 -04:00 committed by GitHub
commit cc55d46b37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 316 additions and 24 deletions

View File

@ -28,7 +28,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "0.14.0",
"dcmjs": "0.14.1",
"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.16.1",
"dcmjs": "0.14.0",
"dcmjs": "0.14.1",
"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.16.1",
"dcmjs": "0.14.0",
"dcmjs": "0.14.1",
"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.16.1",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.14.0",
"dcmjs": "0.14.1",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",

View File

@ -26,12 +26,16 @@ const CodeNameCodeSequenceValues = {
ImageLibraryGroup: '126200',
TrackingUniqueIdentifier: '112040',
TrackingIdentifier: '112039',
Finding: '121071',
};
const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM',
};
const CORNERSTONE_CODING_SCHEME_DESIGNATOR = 'CST4';
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/**
* Basic SOPClassHandler:
* - For all Image types that are stackable, create
@ -68,8 +72,9 @@ function _getDisplaySetsFromSeries(
const { ConceptNameCodeSequence, ContentSequence } = instance;
if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !==
CodeNameCodeSequenceValues.ImagingMeasurementReport
CodeNameCodeSequenceValues.ImagingMeasurementReport
) {
console.warn(
'Only support Imaging Measurement Report SRs (TID1500) for now'
@ -416,6 +421,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodeNameCodeSequenceValues.TrackingIdentifier
);
const Findings = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Finding
);
const measurement = {
loaded: false,
labels: [],
@ -424,6 +435,23 @@ 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 (cornerstoneFreeTextFinding) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
value: cornerstoneFreeTextFinding.ConceptCodeSequence.CodeMeaning,
});
}
}
NUMContentItems.forEach(item => {
const {
ConceptNameCodeSequence,
@ -495,7 +523,16 @@ function _getLabelFromMeasuredValueSequence(
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
return { label: CodeMeaning, value: `${NumericValue} ${CodeValue}` }; // E.g. Long Axis: 31.0 mm
debugger;
const formatedNumericValue = NumericValue
? Number(NumericValue).toFixed(2)
: '';
return {
label: CodeMeaning,
value: `${formatedNumericValue} ${CodeValue}`,
}; // E.g. Long Axis: 31.0 mm
}
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {

View File

@ -1,4 +1,5 @@
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
import { pixelToCanvas } from 'cornerstone-core';
import TOOL_NAMES from '../constants/toolNames';
import SCOORD_TYPES from '../constants/scoordTypes';
@ -10,8 +11,10 @@ const drawJoinedLines = importInternal('drawing/drawJoinedLines');
const drawCircle = importInternal('drawing/drawCircle');
const drawEllipse = importInternal('drawing/drawEllipse');
const drawHandles = importInternal('drawing/drawHandles');
const drawArrow = importInternal('drawing/drawArrow');
const getNewContext = importInternal('drawing/getNewContext');
const BaseTool = importInternal('base/BaseTool');
const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox');
/**
* @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay).
@ -59,18 +62,20 @@ export default class DICOMSRDisplayTool extends BaseTool {
trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier)
);
let shouldRepositionTextBoxes = false;
for (let i = 0; i < filteredToolData.length; i++) {
const data = filteredToolData[i];
const { renderableData } = data;
const { renderableData, labels } = data;
const color =
data.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
? toolColors.getActiveColor()
: toolColors.getToolColor();
const lineWidth = 2;
const options = {
color,
lineWidth: 2,
lineWidth,
handleRadius: 6,
};
@ -79,8 +84,10 @@ export default class DICOMSRDisplayTool extends BaseTool {
switch (GraphicType) {
case SCOORD_TYPES.POINT:
this.renderPoint(renderableDataForGraphicType, eventData, options);
break;
case SCOORD_TYPES.MULTIPOINT:
this.renderPointOrMultipoint(
this.renderMultipoint(
renderableDataForGraphicType,
eventData,
options
@ -105,9 +112,84 @@ export default class DICOMSRDisplayTool extends BaseTool {
break;
}
});
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
if (!data.handles || !data.handles.textBox) {
const textBox = {
active: false,
hasMoved: true,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true,
};
const anchorPoints = _getTextBoxAnchorPointsForRenderableData(
renderableData,
eventData
);
textBox.anchorPoints = anchorPoints;
const bottomRight = {
x: Math.max(...anchorPoints.map(point => point.x)),
y: Math.max(...anchorPoints.map(point => point.y)),
};
textBox.x = bottomRight.x;
textBox.y = bottomRight.y;
data.handles = {};
data.handles.textBox = textBox;
shouldRepositionTextBoxes = true;
}
const text = _getTextBoxLinesFromLabels(labels);
function textBoxAnchorPoints() {
return data.handles.textBox.anchorPoints;
}
draw(context, context => {
drawLinkedTextBox(
context,
element,
data.handles.textBox,
text,
data.handles,
textBoxAnchorPoints,
color,
lineWidth,
0,
true
);
});
}
// TOOD -> text boxes may overlap with other annotations at the moment.
// To be fixed after we get requirements.
// if (shouldRepositionTextBoxes) {
// this.repositionTextBox(filteredToolData, eventData);
// }
}
// repositionTextBox(toolData, eventData) {
// const toolBoundingBoxes = [];
// for (let i = 0; i < toolData.length; i++) {
// const toolDataI = toolData[i];
// const { textBox } = toolDataI.handles;
// const { anchorPoints } = textBox;
// const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints);
// // Get the textbox bounding locations.
// // Get the tool extents.
// }
// }
renderPolyLine(renderableData, eventData, options) {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
@ -119,7 +201,7 @@ export default class DICOMSRDisplayTool extends BaseTool {
});
}
renderPointOrMultipoint(renderableData, eventData, options) {
renderMultipoint(renderableData, eventData, options) {
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(points => {
@ -129,6 +211,41 @@ export default class DICOMSRDisplayTool extends BaseTool {
});
}
renderPoint(renderableData, eventData, options) {
// Render single point as an arrow.
const { element, image } = eventData;
const { rows, columns } = image;
const context = getNewContext(eventData.canvasContext.canvas);
const { color, lineWidth } = options;
// Find a suitable length for the image size.
const xOffset = columns / 10;
const yOffset = rows / 10;
renderableData.forEach(points => {
const point = points[0]; // The SCOORD type is POINT so the array length is 1.
draw(context, context => {
// Draw the arrow
const handleStartCanvas = pixelToCanvas(element, point);
const handleEndCanvas = pixelToCanvas(element, {
x: point.x + xOffset,
y: point.y + yOffset,
});
drawArrow(
context,
handleEndCanvas,
handleStartCanvas,
color,
lineWidth,
false
);
});
});
}
renderCircle(renderableData, eventData, options) {
const { element } = eventData;
@ -161,3 +278,123 @@ 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);
const lines = [];
for (let i = 0; i < labelLength; i++) {
const labelEntry = labels[i];
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
}
return lines;
}
const SHORT_HAND_MAP = {
'Short Axis': 'S ',
'Long Axis': 'L ',
AREA: 'Area ',
Length: '',
CORNERSTONEFREETEXT: '',
};
function _labelToShorthand(label) {
const shortHand = SHORT_HAND_MAP[label];
if (shortHand !== undefined) {
return shortHand;
}
return label;
}
function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) {
let anchorPoints = [];
Object.keys(renderableData).forEach(GraphicType => {
const renderableDataForGraphicType = renderableData[GraphicType];
switch (GraphicType) {
case SCOORD_TYPES.POINT:
renderableDataForGraphicType.forEach(points => {
anchorPoints = [...anchorPoints, ...points];
// Add other arrow point based on image size.
const { image } = eventData;
const { rows, columns } = image;
const xOffset = columns / 10;
const yOffset = rows / 10;
const point = points[0];
anchorPoints.push({ x: point.x + xOffset, y: point.y + yOffset });
});
break;
case SCOORD_TYPES.MULTIPOINT:
case SCOORD_TYPES.POLYLINE:
renderableDataForGraphicType.forEach(points => {
anchorPoints = [...anchorPoints, ...points];
});
break;
case SCOORD_TYPES.CIRCLE:
renderableDataForGraphicType.forEach(circle => {
const { center, radius } = circle;
anchorPoints.push({ x: center.x + radius, y: center.y });
anchorPoints.push({ x: center.x - radius, y: center.y });
anchorPoints.push({ x: center.x, y: center.y + radius });
anchorPoints.push({ x: center.x, y: center.y - radius });
});
break;
case SCOORD_TYPES.ELLIPSE:
renderableDataForGraphicType.forEach(ellipse => {
const { corner1, corner2 } = ellipse;
const halfWidth = Math.abs(corner1.x - corner2.x) / 2;
const halfHeight = Math.abs(corner1.y - corner2.y) / 2;
const center = {
x: (corner1.x + corner2.x) / 2,
y: (corner1.y + corner2.y) / 2,
};
anchorPoints.push({ x: center.x + halfWidth, y: center.y });
anchorPoints.push({ x: center.x - halfWidth, y: center.y });
anchorPoints.push({ x: center.x, y: center.y + halfHeight });
anchorPoints.push({ x: center.x, y: center.y - halfHeight });
});
break;
}
});
return anchorPoints;
}
function _getBoundingBoxFromAnchorPoints(anchorPoints) {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
anchorPoints.forEach(point => {
const { x, y } = point;
if (x > maxX) {
maxX = x;
} else if (x < minX) {
minX = x;
}
if (y > maxX) {
maxY = y;
} else if (y < minY) {
minY = y;
}
});
}

View File

@ -14,11 +14,6 @@ export default function addMeasurement(
imageId,
displaySetInstanceUID
) {
console.log('== ADD MEASUREMENT TO CST ==');
console.log(measurement);
console.log(imageId);
console.log('============================');
// TODO -> Render rotated ellipse .
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
@ -26,6 +21,7 @@ export default function addMeasurement(
const measurementData = {
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
renderableData: {},
labels: measurement.labels,
};
measurement.coords.forEach(coord => {

View File

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

View File

@ -65,6 +65,7 @@ const machineConfiguration = {
cond: 'isNewSeries',
},
],
UNTRACK_SERIES: [
{
target: 'tracking',

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.0",
"dcmjs": "0.14.1",
"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.0",
"dcmjs": "0.14.1",
"dicomweb-client": "^0.6.0",
"immer": "6.0.2",
"isomorphic-base64": "^1.0.2",

View File

@ -69,7 +69,7 @@
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "4.16.1",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.14.0",
"dcmjs": "0.14.1",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.4.4",
"dotenv-webpack": "^1.7.0",

View File

@ -2679,6 +2679,17 @@
dependencies:
"@types/node" ">= 8"
"@ohif/extension-cornerstone@^2.4.0":
version "2.8.2"
resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-2.8.2.tgz#b8d95610b91eb43c05f84adcdd699183918a57ff"
integrity sha512-fIkPPQdDCkoVHCYTUFB4UhmdL9VJRyYawa6FxXiKXjj9OcUsrkXj+BORYgzcEkLWTLmrB+66qtIqpQ7FyXlzJQ==
dependencies:
"@babel/runtime" "^7.5.5"
classnames "^2.2.6"
lodash.merge "^4.6.2"
lodash.throttle "^4.1.1"
react-cornerstone-viewport "2.3.9"
"@ohif/extension-lesion-tracker@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@ohif/extension-lesion-tracker/-/extension-lesion-tracker-0.2.0.tgz#37fda345204041539051750fad17551846fba40f"
@ -7353,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.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.0.tgz#0dc6cb2d15ddcff759bc9002f2a9704537735d1c"
integrity sha512-VL/Ibxe5RDsc5j5SEv3aEqdlKuBXz81/bBuW59Or0cos9vgK3XnVl3rr0ct6DWXJGK8vGIUMBOguyd/NRvlN0w==
dcmjs@0.14.1:
version "0.14.1"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.1.tgz#4a4f18d1e5332d38429e2d77340a6ad0052ec71e"
integrity sha512-GGGQt4zjX7Fp0rLO5rcIQgmxZdLsEwBSZ1T74iXeJG5W32P1yiodJfBW900tNZCKj3zWamO3f5jDC1Xkv81fdg==
dependencies:
"@babel/polyfill" "^7.8.3"
"@babel/runtime" "^7.8.4"
@ -17889,6 +17900,16 @@ react-cornerstone-viewport@2.3.8:
prop-types "^15.7.2"
react-resize-detector "^4.2.1"
react-cornerstone-viewport@2.3.9:
version "2.3.9"
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.3.9.tgz#f9761da8e536f0a217137c6ca1a983f5882249f9"
integrity sha512-qrhq8CbX/jq6b93cQjV2qC/mhHOvFBpxzxcHlvHzEQZt/rmRMcaYxCqjpNaNbUmmBu61wNkxesUVsggkPTTcqg==
dependencies:
classnames "^2.2.6"
date-fns "^2.2.1"
prop-types "^15.7.2"
react-resize-detector "^4.2.1"
react-dates@21.2.1:
version "21.2.1"
resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.2.1.tgz#a979ed6876326ccfbf754a019bc95458cc061ad8"