Render text properly, render arrow + freetext. Need to move textboxes appropriately.

This commit is contained in:
James A. Petts 2020-07-01 11:59:29 +01:00
parent d956c3008c
commit e3f2e26e3b
4 changed files with 200 additions and 67 deletions

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'
@ -134,49 +139,6 @@ function _getDisplaySetsFromSeries(
return [displaySet];
}
function _isRehydratable(displaySet, MeasurementService) {
const mappings = MeasurementService.getSourceMappings(
'CornerstoneTools',
'4'
);
if (!mappings || !mappings.length) {
return false;
}
const mappingDefinitions = mappings.map(m => m.definition);
const { measurements } = displaySet;
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
adapterKey =>
typeof cornerstoneAdapters[adapterKey]
.isValidCornerstoneTrackingIdentifier === 'function'
);
const adapters = [];
adapterKeys.forEach(key => {
if (mappingDefinitions.includes(key)) {
// Must have both a dcmjs adapter and a MeasurementService
// Definition in order to be a candidate for import.
adapters.push(cornerstoneAdapters[key]);
}
});
for (let i = 0; i < measurements.length; i++) {
const TrackingIdentifier = measurements[i].TrackingIdentifier;
const hydratable = adapters.some(adapter =>
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
);
if (hydratable) {
return true;
}
}
return false;
}
function _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet,
newDisplaySet,
@ -459,6 +421,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
CodeNameCodeSequenceValues.TrackingIdentifier
);
const Findings = mergedContentSequence.filter(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.Finding
);
const measurement = {
loaded: false,
labels: [],
@ -467,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,

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,6 +11,7 @@ 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');
@ -60,6 +62,12 @@ export default class DICOMSRDisplayTool extends BaseTool {
trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier)
);
// First: Render annotations with textboxes moved to closest side
// TODO: Render all annotations and get their locations.
// TODO: Find suitable places for textboxes that aren't covered.
let shouldRepositionTextBoxes = false;
for (let i = 0; i < filteredToolData.length; i++) {
const data = filteredToolData[i];
const { renderableData, labels } = data;
@ -80,8 +88,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
@ -110,36 +120,53 @@ export default class DICOMSRDisplayTool extends BaseTool {
const { element } = eventData;
const context = getNewContext(eventData.canvasContext.canvas);
debugger;
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);
debugger;
function textBoxAnchorPoints() {
return [{ x: 1000, y: 1000 }];
return data.handles.textBox.anchorPoints;
}
draw(context, context => {
drawLinkedTextBox(
context,
element,
{
active: false,
hasMoved: false,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true,
x: 256,
y: 256,
},
data.handles.textBox,
text,
null,
data.handles,
textBoxAnchorPoints,
color,
lineWidth,
10,
0,
true
);
});
@ -157,7 +184,7 @@ export default class DICOMSRDisplayTool extends BaseTool {
});
}
renderPointOrMultipoint(renderableData, eventData, options) {
renderMultipoint(renderableData, eventData, options) {
const context = getNewContext(eventData.canvasContext.canvas);
renderableData.forEach(points => {
@ -167,6 +194,46 @@ export default class DICOMSRDisplayTool extends BaseTool {
});
}
renderPoint(renderableData, eventData, options) {
// Render single point as an arrow.
debugger;
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;
debugger;
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,
});
debugger;
drawArrow(
context,
handleEndCanvas,
handleStartCanvas,
color,
lineWidth,
false
);
});
});
}
renderCircle(renderableData, eventData, options) {
const { element } = eventData;
@ -220,6 +287,7 @@ const SHORT_HAND_MAP = {
'Long Axis': 'L ',
AREA: 'Area ',
Length: '',
CORNERSTONEFREETEXT: '',
};
function _labelToShorthand(label) {
@ -231,3 +299,67 @@ function _labelToShorthand(label) {
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;
}

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;

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