Skip to content

Commit 71eb4e0

Browse files
Merge pull request #112 from ImagingDataCommons/feat/sr-changes
fix: Add SRPoint and SRRectangleROI sub-types
2 parents 6cce05f + f5887c0 commit 71eb4e0

16 files changed

Lines changed: 390 additions & 175 deletions

File tree

extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,10 @@ function _processTID1410Measurement(mergedContentSequence) {
511511

512512
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
513513

514+
const finding = mergedContentSequence.find(
515+
item => item.ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.Finding
516+
);
517+
514518
const { ConceptNameCodeSequence: conceptNameItem } = graphicItem;
515519
const { CodeValue: graphicValue, CodingSchemeDesignator: graphicDesignator } = conceptNameItem;
516520
const graphicCode = `${graphicDesignator}:${graphicValue}`;
@@ -547,12 +551,18 @@ function _processTID1410Measurement(mergedContentSequence) {
547551
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT
548552
);
549553
if (findingSites.length) {
554+
const siteItem = findingSites[0];
555+
const conceptName = siteItem.ConceptNameCodeSequence;
550556
measurement.labels.push({
551-
label: CodeNameCodeSequenceValues.FindingSiteSCT,
552-
value: findingSites[0].ConceptCodeSequence.CodeMeaning,
557+
label: conceptName?.CodeMeaning || 'Finding Site',
558+
value: siteItem.ConceptCodeSequence.CodeMeaning,
553559
});
554560
}
555561

562+
const measurementWithFinding = measurement as Record<string, unknown>;
563+
measurementWithFinding.srFinding = finding?.ConceptCodeSequence;
564+
measurementWithFinding.srFindingSites = findingSites.map(fs => fs.ConceptCodeSequence).filter(Boolean);
565+
556566
return measurement;
557567
}
558568

@@ -660,6 +670,10 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
660670
}
661671
});
662672

673+
const measurementWithFinding = measurement as Record<string, unknown>;
674+
measurementWithFinding.srFinding = finding?.ConceptCodeSequence;
675+
measurementWithFinding.srFindingSites = findingSites.map(fs => fs.ConceptCodeSequence).filter(Boolean);
676+
663677
return measurement;
664678
}
665679

extensions/cornerstone-dicom-sr/src/init.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
addTool,
23
AngleTool,
34
annotation,
45
ArrowAnnotateTool,
@@ -15,6 +16,8 @@ import { Types } from '@ohif/core';
1516
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
1617
import addToolInstance from './utils/addToolInstance';
1718
import toolNames from './tools/toolNames';
19+
import SRPointTool from './tools/SRPointTool';
20+
import { getSRRectangleROITextLines } from './utils/srToolGetTextLines';
1821

1922
/**
2023
* @param {object} configuration
@@ -31,17 +34,22 @@ export default function init({
3134
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
3235
addToolInstance(toolNames.SRAngle, AngleTool);
3336
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
34-
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
3537

36-
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
38+
/** SR subtypes: show label (e.g. Lesion) instead of intensity/stats */
39+
addTool(SRPointTool);
40+
addToolInstance(toolNames.SRRectangleROI, RectangleROITool, {
41+
getTextLines: getSRRectangleROITextLines,
42+
});
43+
44+
/** TODO - fix the SR display of Cobb Angle, as it joins the two lines */
3745
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
3846

39-
// Modify annotation tools to use dashed lines on SR
4047
const dashedLine = {
4148
lineDash: '4,4',
4249
};
4350
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
4451
[toolNames.DICOMSRDisplay]: dashedLine,
52+
[toolNames.SRPoint]: dashedLine,
4553
SRLength: dashedLine,
4654
SRBidirectional: dashedLine,
4755
SREllipticalROI: dashedLine,

extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,29 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
2525
_getTextBoxLinesFromLabels(labels) {
2626
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
2727

28+
if (!labels?.length) {
29+
return [];
30+
}
31+
2832
const labelLength = Math.min(labels.length, 5);
2933
const lines = [];
3034

3135
for (let i = 0; i < labelLength; i++) {
3236
const labelEntry = labels[i];
33-
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
37+
const shorthand = _labelToShorthand(labelEntry.label);
38+
const value = labelEntry.value ?? '';
39+
40+
/**
41+
* Empty shorthand (CORNERSTONEFREETEXT, Length, etc.): show value only — avoids ": text" or
42+
* "363698007: site" when label was a raw code (fixed at source for finding site).
43+
*/
44+
if (shorthand === '' && value !== '') {
45+
lines.push(String(value));
46+
} else if (shorthand === '') {
47+
continue;
48+
} else {
49+
lines.push(`${shorthand}: ${value}`);
50+
}
3451
}
3552

3653
return lines;
@@ -225,55 +242,41 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
225242
options
226243
) {
227244
const canvasCoordinates = [];
245+
const crossSize = 6;
246+
228247
renderableData.map((data, index) => {
229248
const point = data[0];
230-
// This gives us one point for arrow
249+
const [canvasX, canvasY] = viewport.worldToCanvas(point);
231250
canvasCoordinates.push(viewport.worldToCanvas(point));
232251

233252
if (data[1] !== undefined) {
234253
canvasCoordinates.push(viewport.worldToCanvas(data[1]));
254+
drawing.drawArrow(
255+
svgDrawingHelper,
256+
annotationUID,
257+
`arrow-${index}`,
258+
canvasCoordinates[canvasCoordinates.length - 1],
259+
canvasCoordinates[canvasCoordinates.length - 2],
260+
{ color: options.color, width: options.lineWidth }
261+
);
262+
} else {
263+
const cx = Number(canvasX);
264+
const cy = Number(canvasY);
265+
const crossPaths = [
266+
[[cx, cy - crossSize], [cx, cy + crossSize]],
267+
[[cx - crossSize, cy], [cx + crossSize, cy]],
268+
];
269+
drawing.drawPath(
270+
svgDrawingHelper,
271+
annotationUID,
272+
`cross-${index}`,
273+
crossPaths,
274+
{ color: options.color, lineWidth: options.lineWidth || 2 }
275+
);
235276
}
236-
else{
237-
// We get the other point for the arrow by using the image size
238-
const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
239-
240-
let xOffset = 10;
241-
let yOffset = 10;
242-
243-
if (imagePixelModule) {
244-
const { columns, rows } = imagePixelModule;
245-
xOffset = columns / 10;
246-
yOffset = rows / 10;
247-
}
248-
249-
const imagePoint = csUtils.worldToImageCoords(referencedImageId, point);
250-
const arrowEnd = csUtils.imageToWorldCoords(referencedImageId, [
251-
imagePoint[0] + xOffset,
252-
imagePoint[1] + yOffset,
253-
]);
254-
255-
canvasCoordinates.push(viewport.worldToCanvas(arrowEnd));
256-
257-
}
258-
259-
260-
const arrowUID = `${index}`;
261-
262-
// Todo: handle drawing probe as probe, currently we are drawing it as an arrow
263-
drawing.drawArrow(
264-
svgDrawingHelper,
265-
annotationUID,
266-
arrowUID,
267-
canvasCoordinates[1],
268-
canvasCoordinates[0],
269-
{
270-
color: options.color,
271-
width: options.lineWidth,
272-
}
273-
);
274277
});
275278

276-
return canvasCoordinates; // used for drawing textBox
279+
return canvasCoordinates;
277280
}
278281

279282
renderEllipse(
@@ -343,15 +346,19 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
343346
}
344347

345348
const { annotationUID, data = {} } = annotation;
346-
const { labels } = data;
349+
const { labels, label } = data;
347350
const { color } = options;
348351

349352
let adaptedCanvasCoordinates = canvasCoordinates;
350-
// adapt coordinates if there is an adapter
353+
351354
if (typeof canvasCoordinatesAdapter === 'function') {
352355
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
353356
}
354-
const textLines = this._getTextBoxLinesFromLabels(labels);
357+
358+
const textLines =
359+
typeof label === 'string' && label.length > 0
360+
? [label]
361+
: this._getTextBoxLinesFromLabels(labels);
355362
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
356363

357364
if (!annotation.data?.handles?.textBox?.worldPosition) {
@@ -374,6 +381,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
374381
{
375382
...textBoxOptions,
376383
color,
384+
padding: textBoxOptions.padding ?? 6,
377385
}
378386
);
379387

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { Types } from '@cornerstonejs/core';
2+
import { ProbeTool, drawing, annotation } from '@cornerstonejs/tools';
3+
4+
import type { SVGDrawingHelper } from '@cornerstonejs/tools';
5+
import type { StyleSpecifier } from '@cornerstonejs/tools';
6+
import type { ProbeAnnotation } from '@cornerstonejs/tools';
7+
8+
import { getSRPointTextLines } from '../utils/srToolGetTextLines';
9+
10+
const { drawPath, drawTextBox } = drawing;
11+
const { getAnnotations } = annotation.state;
12+
const { isAnnotationVisible } = annotation.visibility;
13+
14+
const CROSS_SIZE = 6;
15+
16+
/**
17+
* SRPoint: sub-type of Probe for hydrated DICOM SR SCOORD/SCOORD3D points.
18+
* Renders a cross (plus) marker per medical imaging convention;
19+
* shows label only, no intensity/coordinates.
20+
*/
21+
class SRPointTool extends ProbeTool {
22+
static toolName = 'SRPoint';
23+
24+
constructor(toolProps, defaultToolProps) {
25+
super(toolProps, {
26+
...defaultToolProps,
27+
configuration: {
28+
...defaultToolProps?.configuration,
29+
getTextLines: getSRPointTextLines,
30+
handleRadius: 6,
31+
textCanvasOffset: { x: 4, y: 4 },
32+
},
33+
});
34+
}
35+
36+
renderAnnotation = (
37+
enabledElement: Types.IEnabledElement,
38+
svgDrawingHelper: SVGDrawingHelper
39+
): boolean => {
40+
let renderStatus = false;
41+
const { viewport } = enabledElement;
42+
const { element } = viewport;
43+
44+
const annotations = getAnnotations(this.getToolName(), element) as ProbeAnnotation[];
45+
if (!annotations?.length) {
46+
return renderStatus;
47+
}
48+
49+
const filteredAnnotations = this.filterInteractableAnnotationsForElement(
50+
element,
51+
annotations
52+
);
53+
if (!filteredAnnotations?.length) {
54+
return renderStatus;
55+
}
56+
57+
const styleSpecifier: StyleSpecifier = {
58+
toolGroupId: this.toolGroupId,
59+
toolName: this.getToolName(),
60+
viewportId: enabledElement.viewport.id,
61+
};
62+
63+
for (const annotation of filteredAnnotations) {
64+
const annotationUID = annotation.annotationUID;
65+
const data = annotation.data;
66+
const point = data.handles.points[0];
67+
const [canvasX, canvasY] = viewport.worldToCanvas(point);
68+
69+
if (!isAnnotationVisible(annotationUID)) {
70+
continue;
71+
}
72+
73+
styleSpecifier.annotationUID = annotationUID;
74+
const { color, lineWidth } = this.getAnnotationStyle({
75+
annotation,
76+
styleSpecifier,
77+
});
78+
79+
const size = Number(this.configuration?.handleRadius) || CROSS_SIZE;
80+
const cx = Number(canvasX);
81+
const cy = Number(canvasY);
82+
const crossPaths: [number, number][][] = [
83+
[[cx, cy - size], [cx, cy + size]],
84+
[[cx - size, cy], [cx + size, cy]],
85+
];
86+
drawPath(svgDrawingHelper, annotationUID, 'cross', crossPaths, {
87+
color,
88+
lineWidth: lineWidth || 2,
89+
});
90+
91+
renderStatus = true;
92+
93+
const options = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
94+
if (options?.visibility !== false) {
95+
const targetId = this.getTargetId(viewport, data);
96+
const textLines = getSRPointTextLines(data, targetId);
97+
if (textLines?.length) {
98+
const textCanvasCoordinates = [
99+
cx + (this.configuration?.textCanvasOffset?.x ?? 4),
100+
cy + (this.configuration?.textCanvasOffset?.y ?? 4),
101+
];
102+
drawTextBox(
103+
svgDrawingHelper,
104+
annotationUID,
105+
'0',
106+
textLines,
107+
[textCanvasCoordinates[0], textCanvasCoordinates[1]],
108+
{ ...options, padding: options.padding ?? 4 }
109+
);
110+
}
111+
}
112+
}
113+
114+
return renderStatus;
115+
};
116+
}
117+
118+
export default SRPointTool;

extensions/cornerstone-dicom-sr/src/tools/toolNames.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const toolNames = {
99
SRCobbAngle: 'SRCobbAngle',
1010
SRRectangleROI: 'SRRectangleROI',
1111
SRPlanarFreehandROI: 'SRPlanarFreehandROI',
12+
SRPoint: 'SRPoint',
1213
};
1314

1415
export default toolNames;

0 commit comments

Comments
 (0)