@cornerstonejs/tools 1.41.0 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/eventDispatchers/keyboardEventHandlers/keyDown.js +2 -1
- package/dist/cjs/eventDispatchers/keyboardEventHandlers/keyDown.js.map +1 -1
- package/dist/cjs/stateManagement/annotation/annotationState.js +3 -0
- package/dist/cjs/stateManagement/annotation/annotationState.js.map +1 -1
- package/dist/cjs/tools/annotation/BidirectionalTool.js +7 -6
- package/dist/cjs/tools/annotation/BidirectionalTool.js.map +1 -1
- package/dist/cjs/types/index.d.ts +2 -1
- package/dist/cjs/utilities/contours/AnnotationToPointData.d.ts +11 -0
- package/dist/cjs/utilities/contours/AnnotationToPointData.js +44 -0
- package/dist/cjs/utilities/contours/AnnotationToPointData.js.map +1 -0
- package/dist/cjs/utilities/contours/RectangleROIStartEndThreshold.d.ts +6 -0
- package/dist/cjs/utilities/contours/RectangleROIStartEndThreshold.js +43 -0
- package/dist/cjs/utilities/contours/RectangleROIStartEndThreshold.js.map +1 -0
- package/dist/cjs/utilities/contours/contourFinder.d.ts +7 -0
- package/dist/cjs/utilities/contours/contourFinder.js +68 -0
- package/dist/cjs/utilities/contours/contourFinder.js.map +1 -0
- package/dist/cjs/utilities/contours/detectContourHoles.d.ts +5 -0
- package/dist/cjs/utilities/contours/detectContourHoles.js +78 -0
- package/dist/cjs/utilities/contours/detectContourHoles.js.map +1 -0
- package/dist/cjs/utilities/contours/generateContourSetsFromLabelmap.d.ts +4 -0
- package/dist/cjs/utilities/contours/generateContourSetsFromLabelmap.js +124 -0
- package/dist/cjs/utilities/contours/generateContourSetsFromLabelmap.js.map +1 -0
- package/dist/cjs/utilities/contours/index.d.ts +6 -0
- package/dist/cjs/utilities/contours/index.js +17 -0
- package/dist/cjs/utilities/contours/index.js.map +1 -0
- package/dist/cjs/utilities/contours/mergePoints.d.ts +8 -0
- package/dist/cjs/utilities/contours/mergePoints.js +77 -0
- package/dist/cjs/utilities/contours/mergePoints.js.map +1 -0
- package/dist/cjs/utilities/index.d.ts +2 -1
- package/dist/cjs/utilities/index.js +3 -1
- package/dist/cjs/utilities/index.js.map +1 -1
- package/dist/cjs/utilities/segmentation/contourAndFindLargestBidirectional.d.ts +1 -0
- package/dist/cjs/utilities/segmentation/contourAndFindLargestBidirectional.js +31 -0
- package/dist/cjs/utilities/segmentation/contourAndFindLargestBidirectional.js.map +1 -0
- package/dist/cjs/utilities/segmentation/createBidirectionalToolData.d.ts +14 -0
- package/dist/cjs/utilities/segmentation/createBidirectionalToolData.js +43 -0
- package/dist/cjs/utilities/segmentation/createBidirectionalToolData.js.map +1 -0
- package/dist/cjs/utilities/segmentation/findLargestBidirectional.d.ts +1 -0
- package/dist/cjs/utilities/segmentation/findLargestBidirectional.js +94 -0
- package/dist/cjs/utilities/segmentation/findLargestBidirectional.js.map +1 -0
- package/dist/cjs/utilities/segmentation/index.d.ts +4 -1
- package/dist/cjs/utilities/segmentation/index.js +7 -1
- package/dist/cjs/utilities/segmentation/index.js.map +1 -1
- package/dist/cjs/utilities/segmentation/isLineInSegment.d.ts +9 -0
- package/dist/cjs/utilities/segmentation/isLineInSegment.js +55 -0
- package/dist/cjs/utilities/segmentation/isLineInSegment.js.map +1 -0
- package/dist/cjs/utilities/segmentation/segmentContourAction.d.ts +17 -0
- package/dist/cjs/utilities/segmentation/segmentContourAction.js +122 -0
- package/dist/cjs/utilities/segmentation/segmentContourAction.js.map +1 -0
- package/dist/esm/eventDispatchers/keyboardEventHandlers/keyDown.js +2 -1
- package/dist/esm/eventDispatchers/keyboardEventHandlers/keyDown.js.map +1 -1
- package/dist/esm/stateManagement/annotation/annotationState.js +3 -0
- package/dist/esm/stateManagement/annotation/annotationState.js.map +1 -1
- package/dist/esm/tools/annotation/BidirectionalTool.js +7 -6
- package/dist/esm/tools/annotation/BidirectionalTool.js.map +1 -1
- package/dist/esm/utilities/contours/AnnotationToPointData.js +39 -0
- package/dist/esm/utilities/contours/AnnotationToPointData.js.map +1 -0
- package/dist/esm/utilities/contours/RectangleROIStartEndThreshold.js +41 -0
- package/dist/esm/utilities/contours/RectangleROIStartEndThreshold.js.map +1 -0
- package/dist/esm/utilities/contours/contourFinder.js +63 -0
- package/dist/esm/utilities/contours/contourFinder.js.map +1 -0
- package/dist/esm/utilities/contours/detectContourHoles.js +74 -0
- package/dist/esm/utilities/contours/detectContourHoles.js.map +1 -0
- package/dist/esm/utilities/contours/generateContourSetsFromLabelmap.js +117 -0
- package/dist/esm/utilities/contours/generateContourSetsFromLabelmap.js.map +1 -0
- package/dist/esm/utilities/contours/index.js +7 -0
- package/dist/esm/utilities/contours/index.js.map +1 -0
- package/dist/esm/utilities/contours/mergePoints.js +73 -0
- package/dist/esm/utilities/contours/mergePoints.js.map +1 -0
- package/dist/esm/utilities/index.js +2 -1
- package/dist/esm/utilities/index.js.map +1 -1
- package/dist/esm/utilities/segmentation/contourAndFindLargestBidirectional.js +25 -0
- package/dist/esm/utilities/segmentation/contourAndFindLargestBidirectional.js.map +1 -0
- package/dist/esm/utilities/segmentation/createBidirectionalToolData.js +40 -0
- package/dist/esm/utilities/segmentation/createBidirectionalToolData.js.map +1 -0
- package/dist/esm/utilities/segmentation/findLargestBidirectional.js +96 -0
- package/dist/esm/utilities/segmentation/findLargestBidirectional.js.map +1 -0
- package/dist/esm/utilities/segmentation/index.js +4 -1
- package/dist/esm/utilities/segmentation/index.js.map +1 -1
- package/dist/esm/utilities/segmentation/isLineInSegment.js +50 -0
- package/dist/esm/utilities/segmentation/isLineInSegment.js.map +1 -0
- package/dist/esm/utilities/segmentation/segmentContourAction.js +98 -0
- package/dist/esm/utilities/segmentation/segmentContourAction.js.map +1 -0
- package/dist/types/eventDispatchers/keyboardEventHandlers/keyDown.d.ts.map +1 -1
- package/dist/types/stateManagement/annotation/annotationState.d.ts.map +1 -1
- package/dist/types/tools/annotation/BidirectionalTool.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +2 -1
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/utilities/contours/AnnotationToPointData.d.ts +12 -0
- package/dist/types/utilities/contours/AnnotationToPointData.d.ts.map +1 -0
- package/dist/types/utilities/contours/RectangleROIStartEndThreshold.d.ts +7 -0
- package/dist/types/utilities/contours/RectangleROIStartEndThreshold.d.ts.map +1 -0
- package/dist/types/utilities/contours/contourFinder.d.ts +8 -0
- package/dist/types/utilities/contours/contourFinder.d.ts.map +1 -0
- package/dist/types/utilities/contours/detectContourHoles.d.ts +6 -0
- package/dist/types/utilities/contours/detectContourHoles.d.ts.map +1 -0
- package/dist/types/utilities/contours/generateContourSetsFromLabelmap.d.ts +5 -0
- package/dist/types/utilities/contours/generateContourSetsFromLabelmap.d.ts.map +1 -0
- package/dist/types/utilities/contours/index.d.ts +7 -0
- package/dist/types/utilities/contours/index.d.ts.map +1 -0
- package/dist/types/utilities/contours/mergePoints.d.ts +9 -0
- package/dist/types/utilities/contours/mergePoints.d.ts.map +1 -0
- package/dist/types/utilities/index.d.ts +2 -1
- package/dist/types/utilities/index.d.ts.map +1 -1
- package/dist/types/utilities/segmentation/contourAndFindLargestBidirectional.d.ts +2 -0
- package/dist/types/utilities/segmentation/contourAndFindLargestBidirectional.d.ts.map +1 -0
- package/dist/types/utilities/segmentation/createBidirectionalToolData.d.ts +15 -0
- package/dist/types/utilities/segmentation/createBidirectionalToolData.d.ts.map +1 -0
- package/dist/types/utilities/segmentation/findLargestBidirectional.d.ts +2 -0
- package/dist/types/utilities/segmentation/findLargestBidirectional.d.ts.map +1 -0
- package/dist/types/utilities/segmentation/index.d.ts +4 -1
- package/dist/types/utilities/segmentation/index.d.ts.map +1 -1
- package/dist/types/utilities/segmentation/isLineInSegment.d.ts +10 -0
- package/dist/types/utilities/segmentation/isLineInSegment.d.ts.map +1 -0
- package/dist/types/utilities/segmentation/segmentContourAction.d.ts +18 -0
- package/dist/types/utilities/segmentation/segmentContourAction.d.ts.map +1 -0
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +3 -3
- package/src/eventDispatchers/keyboardEventHandlers/keyDown.ts +7 -1
- package/src/stateManagement/annotation/annotationState.ts +3 -0
- package/src/tools/annotation/BidirectionalTool.ts +9 -5
- package/src/types/index.ts +2 -0
- package/src/utilities/contours/AnnotationToPointData.ts +61 -0
- package/src/utilities/contours/RectangleROIStartEndThreshold.ts +60 -0
- package/src/utilities/contours/contourFinder.ts +78 -0
- package/src/utilities/contours/detectContourHoles.ts +147 -0
- package/src/utilities/contours/generateContourSetsFromLabelmap.ts +160 -0
- package/src/utilities/contours/index.ts +14 -0
- package/src/utilities/contours/mergePoints.ts +108 -0
- package/src/utilities/index.ts +2 -0
- package/src/utilities/segmentation/contourAndFindLargestBidirectional.ts +46 -0
- package/src/utilities/segmentation/createBidirectionalToolData.ts +68 -0
- package/src/utilities/segmentation/findLargestBidirectional.ts +159 -0
- package/src/utilities/segmentation/index.ts +6 -0
- package/src/utilities/segmentation/isLineInSegment.ts +84 -0
- package/src/utilities/segmentation/segmentContourAction.ts +169 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if point is within array
|
|
3
|
+
* @param {*} array
|
|
4
|
+
* @param {*} pt
|
|
5
|
+
* @returns
|
|
6
|
+
*/
|
|
7
|
+
function ptInArray(array, pt) {
|
|
8
|
+
let index = -1;
|
|
9
|
+
for (let i = 0; i < array.length; i++) {
|
|
10
|
+
if (isSamePoint(pt, array[i])) {
|
|
11
|
+
index = i;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if point A and point B contain same values
|
|
19
|
+
* @param {*} ptA
|
|
20
|
+
* @param {*} ptB
|
|
21
|
+
* @returns
|
|
22
|
+
*/
|
|
23
|
+
function isSamePoint(ptA, ptB) {
|
|
24
|
+
if (ptA[0] == ptB[0] && ptA[1] == ptB[1] && ptA[2] == ptB[2]) {
|
|
25
|
+
return true;
|
|
26
|
+
} else {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Goes through linesArray and replaces all references of old index with new index
|
|
33
|
+
* @param {*} linesArray
|
|
34
|
+
* @param {*} oldIndex
|
|
35
|
+
* @param {*} newIndex
|
|
36
|
+
*/
|
|
37
|
+
function replacePointIndexReferences(linesArray, oldIndex, newIndex) {
|
|
38
|
+
for (let i = 0; i < linesArray.length; i++) {
|
|
39
|
+
const line = linesArray[i];
|
|
40
|
+
if (line.a == oldIndex) {
|
|
41
|
+
line.a = newIndex;
|
|
42
|
+
} else if (line.b == oldIndex) {
|
|
43
|
+
line.b = newIndex;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Iterate through polyData from vtkjs and merge any points that are the same
|
|
50
|
+
* then update merged point references within lines array
|
|
51
|
+
* @param {*} polyData
|
|
52
|
+
* @param {*} bypass
|
|
53
|
+
* @returns
|
|
54
|
+
*/
|
|
55
|
+
export function removeDuplicatePoints(polyData, bypass = false) {
|
|
56
|
+
const points = polyData.getPoints();
|
|
57
|
+
const lines = polyData.getLines();
|
|
58
|
+
|
|
59
|
+
const pointsArray = [];
|
|
60
|
+
for (let i = 0; i < points.getNumberOfPoints(); i++) {
|
|
61
|
+
const pt = points.getPoint(i).slice();
|
|
62
|
+
pointsArray.push(pt);
|
|
63
|
+
}
|
|
64
|
+
const linesArray = [];
|
|
65
|
+
for (let i = 0; i < lines.getNumberOfCells(); i++) {
|
|
66
|
+
const cell = lines.getCell(i * 3).slice();
|
|
67
|
+
//console.log(JSON.stringify(cell));
|
|
68
|
+
const a = cell[0];
|
|
69
|
+
const b = cell[1];
|
|
70
|
+
const line = {
|
|
71
|
+
a,
|
|
72
|
+
b,
|
|
73
|
+
};
|
|
74
|
+
linesArray.push(line);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (bypass) {
|
|
78
|
+
return { points: pointsArray, lines: linesArray };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Iterate through points and replace any duplicates
|
|
82
|
+
const newPoints = [];
|
|
83
|
+
for (let i = 0; i < pointsArray.length; i++) {
|
|
84
|
+
const pt = pointsArray[i];
|
|
85
|
+
let index = ptInArray(newPoints, pt);
|
|
86
|
+
|
|
87
|
+
if (index >= 0) {
|
|
88
|
+
// Duplicate Point -> replace references in lines
|
|
89
|
+
replacePointIndexReferences(linesArray, i, index);
|
|
90
|
+
} else {
|
|
91
|
+
index = newPoints.length;
|
|
92
|
+
newPoints.push(pt);
|
|
93
|
+
replacePointIndexReferences(linesArray, i, index);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Final pass through lines, remove any that refer to exact same point
|
|
98
|
+
const newLines = [];
|
|
99
|
+
linesArray.forEach((line) => {
|
|
100
|
+
if (line.a != line.b) {
|
|
101
|
+
newLines.push(line);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { points: newPoints, lines: newLines };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default { removeDuplicatePoints };
|
package/src/utilities/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { pointToString } from './pointToString';
|
|
|
26
26
|
import annotationFrameRange from './annotationFrameRange';
|
|
27
27
|
|
|
28
28
|
// name spaces
|
|
29
|
+
import * as contours from './contours';
|
|
29
30
|
import * as segmentation from './segmentation';
|
|
30
31
|
import * as drawing from './drawing';
|
|
31
32
|
import * as math from './math';
|
|
@@ -63,6 +64,7 @@ export {
|
|
|
63
64
|
getCalibratedAreaUnits,
|
|
64
65
|
getCalibratedScale,
|
|
65
66
|
segmentation,
|
|
67
|
+
contours,
|
|
66
68
|
triggerAnnotationRenderForViewportIds,
|
|
67
69
|
triggerAnnotationRender,
|
|
68
70
|
pointInShapeCallback,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { generateContourSetsFromLabelmap } from '../contours';
|
|
2
|
+
import SegmentationRepresentations from '../../enums/SegmentationRepresentations';
|
|
3
|
+
import findLargestBidirectional from './findLargestBidirectional';
|
|
4
|
+
|
|
5
|
+
const EPSILON = 1e-2;
|
|
6
|
+
const { Labelmap } = SegmentationRepresentations;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates a contour object over the segment, and then uses the contouring to
|
|
10
|
+
* find the largest bidirectional object that can be applied within the acquisition
|
|
11
|
+
* plane that is within the segment index, or the contained segment indices.
|
|
12
|
+
*
|
|
13
|
+
* @param segmentation.segments - a list of segments to apply the contour to.
|
|
14
|
+
* @param segmentation.segments.containedSegmentIndices - a set of segment indexes equivalent to the primary segment
|
|
15
|
+
* @param segmentation.segments.label - the label for the segment
|
|
16
|
+
* @param segmentation.segments.color - the color to use for the segment label
|
|
17
|
+
*/
|
|
18
|
+
export default function contourAndFindLargestBidirectional(segmentation) {
|
|
19
|
+
const contours = generateContourSetsFromLabelmap({
|
|
20
|
+
segmentations: segmentation,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!contours?.length || !contours[0].sliceContours.length) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
representationData,
|
|
29
|
+
segments = [
|
|
30
|
+
null,
|
|
31
|
+
{ label: 'Unspecified', color: null, containedSegmentIndices: null },
|
|
32
|
+
],
|
|
33
|
+
} = segmentation;
|
|
34
|
+
const { volumeId: segVolumeId } = representationData[Labelmap];
|
|
35
|
+
|
|
36
|
+
const segmentIndex = segments.findIndex((it) => !!it);
|
|
37
|
+
if (segmentIndex === -1) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
segments[segmentIndex].segmentIndex = segmentIndex;
|
|
41
|
+
return findLargestBidirectional(
|
|
42
|
+
contours[0],
|
|
43
|
+
segVolumeId,
|
|
44
|
+
segments[segmentIndex]
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import type { Annotation } from '../../types/AnnotationTypes';
|
|
3
|
+
|
|
4
|
+
export type BidirectionalData = {
|
|
5
|
+
majorAxis: [Types.Point3, Types.Point3];
|
|
6
|
+
minorAxis: [Types.Point3, Types.Point3];
|
|
7
|
+
maxMajor: number;
|
|
8
|
+
maxMinor: number;
|
|
9
|
+
segmentIndex: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
color?: string | number[];
|
|
12
|
+
referencedImageId: string;
|
|
13
|
+
FrameOfReferenceUID: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates data suitable for the BidirectionalTool from the basic bidirectional
|
|
18
|
+
* data object.
|
|
19
|
+
*/
|
|
20
|
+
export default function createBidirectionalToolData(
|
|
21
|
+
bidirectionalData: BidirectionalData,
|
|
22
|
+
viewport
|
|
23
|
+
): Annotation {
|
|
24
|
+
const {
|
|
25
|
+
majorAxis,
|
|
26
|
+
minorAxis,
|
|
27
|
+
label = '',
|
|
28
|
+
FrameOfReferenceUID,
|
|
29
|
+
referencedImageId,
|
|
30
|
+
} = bidirectionalData;
|
|
31
|
+
const [major0, major1] = majorAxis;
|
|
32
|
+
const [minor0, minor1] = minorAxis;
|
|
33
|
+
|
|
34
|
+
const { viewUp, viewPlaneNormal } = viewport.getCamera();
|
|
35
|
+
const points = [major0, major1, minor0, minor1];
|
|
36
|
+
const bidirectionalToolData = {
|
|
37
|
+
highlighted: true,
|
|
38
|
+
invalidated: true,
|
|
39
|
+
metadata: {
|
|
40
|
+
toolName: 'Bidirectional',
|
|
41
|
+
viewPlaneNormal,
|
|
42
|
+
viewUp,
|
|
43
|
+
FrameOfReferenceUID,
|
|
44
|
+
referencedImageId,
|
|
45
|
+
},
|
|
46
|
+
data: {
|
|
47
|
+
handles: {
|
|
48
|
+
points,
|
|
49
|
+
textBox: {
|
|
50
|
+
hasMoved: false,
|
|
51
|
+
worldPosition: [0, 0, 0] as Types.Point3,
|
|
52
|
+
worldBoundingBox: {
|
|
53
|
+
topLeft: [0, 0, 0] as Types.Point3,
|
|
54
|
+
topRight: [0, 0, 0] as Types.Point3,
|
|
55
|
+
bottomLeft: [0, 0, 0] as Types.Point3,
|
|
56
|
+
bottomRight: [0, 0, 0] as Types.Point3,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
activeHandleIndex: null,
|
|
60
|
+
},
|
|
61
|
+
label,
|
|
62
|
+
cachedStats: {},
|
|
63
|
+
},
|
|
64
|
+
isLocked: false,
|
|
65
|
+
isVisible: true,
|
|
66
|
+
};
|
|
67
|
+
return bidirectionalToolData;
|
|
68
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { vec3 } from 'gl-matrix';
|
|
2
|
+
|
|
3
|
+
import { createIsInSegment, isLineInSegment } from './isLineInSegment';
|
|
4
|
+
import type { BidirectionalData } from './createBidirectionalToolData';
|
|
5
|
+
|
|
6
|
+
const EPSILON = 1e-2;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Search in the contours for the given segment to find the largest bidirectional
|
|
10
|
+
* that will fit entirely within the slice contours inside the contours object.
|
|
11
|
+
* Assumptions/implementation details:
|
|
12
|
+
*
|
|
13
|
+
* 1. The major and minor bidirectional lines must not cross the contour
|
|
14
|
+
* 2. The center point for both major and minor bidirectional lines must be
|
|
15
|
+
* within the segment, or the contained segment index.
|
|
16
|
+
* 3. The major/minor axis must be orthogonal
|
|
17
|
+
*
|
|
18
|
+
* Note this does NOT test that the major/minor axis intersect. Normally they will, but
|
|
19
|
+
* it isn't a hard requirement.
|
|
20
|
+
*
|
|
21
|
+
* The way that islands within the contours are handled is to allow the island to be
|
|
22
|
+
* coloured with something that is contained - that way both open and closed islands
|
|
23
|
+
* can be handled correctly for finding the bidirectional (an open island is a section
|
|
24
|
+
* inside the segment that is open to the outside - this can happen at bone endpoints or when
|
|
25
|
+
* one region flows into another)
|
|
26
|
+
*/
|
|
27
|
+
export default function findLargestBidirectional(
|
|
28
|
+
contours,
|
|
29
|
+
segVolumeId: string,
|
|
30
|
+
segment
|
|
31
|
+
) {
|
|
32
|
+
const { sliceContours } = contours;
|
|
33
|
+
const { segmentIndex, containedSegmentIndices } = segment;
|
|
34
|
+
let maxBidirectional;
|
|
35
|
+
const isInSegment = createIsInSegment(
|
|
36
|
+
segVolumeId,
|
|
37
|
+
segmentIndex,
|
|
38
|
+
containedSegmentIndices
|
|
39
|
+
);
|
|
40
|
+
for (const sliceContour of sliceContours) {
|
|
41
|
+
const bidirectional = createBidirectionalForSlice(
|
|
42
|
+
sliceContour,
|
|
43
|
+
isInSegment,
|
|
44
|
+
maxBidirectional
|
|
45
|
+
);
|
|
46
|
+
if (!bidirectional) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
maxBidirectional = bidirectional;
|
|
50
|
+
}
|
|
51
|
+
if (maxBidirectional) {
|
|
52
|
+
Object.assign(maxBidirectional, segment);
|
|
53
|
+
}
|
|
54
|
+
return maxBidirectional;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This function creates a bidirectional data object for the given slice and
|
|
59
|
+
* slice contour, only when the major distance is larger than currentMax, or
|
|
60
|
+
* equal to current max and the minor is larger than currentMax's minor.
|
|
61
|
+
* It does this by looking at every pair of distances in sliceCountour to find
|
|
62
|
+
* those larger than the currentMax, and then finds the minor distance for those
|
|
63
|
+
* major distances.
|
|
64
|
+
*
|
|
65
|
+
*/
|
|
66
|
+
function createBidirectionalForSlice(
|
|
67
|
+
sliceContour,
|
|
68
|
+
isInSegment,
|
|
69
|
+
currentMax = { maxMajor: 0, maxMinor: 0 }
|
|
70
|
+
) {
|
|
71
|
+
const { points } = sliceContour.polyData;
|
|
72
|
+
const { maxMinor: currentMaxMinor, maxMajor: currentMaxMajor } = currentMax;
|
|
73
|
+
let maxMajor = currentMaxMajor * currentMaxMajor;
|
|
74
|
+
let maxMinor = currentMaxMinor * currentMaxMinor;
|
|
75
|
+
let maxMajorPoints;
|
|
76
|
+
for (let index1 = 0; index1 < points.length; index1++) {
|
|
77
|
+
for (let index2 = index1 + 1; index2 < points.length; index2++) {
|
|
78
|
+
const point1 = points[index1];
|
|
79
|
+
const point2 = points[index2];
|
|
80
|
+
const distance2 = vec3.sqrDist(point1, point2);
|
|
81
|
+
if (distance2 < maxMajor) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (distance2 - EPSILON < maxMajor + EPSILON && maxMajorPoints) {
|
|
85
|
+
// Consider adding to the set of points rather than continuing here
|
|
86
|
+
// so that all minor axis can be tested
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!isInSegment.testCenter(point1, point2)) {
|
|
90
|
+
// Center between the two points has to be in the segment, otherwise
|
|
91
|
+
// this is out of bounds.
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!isLineInSegment(point1, point2, isInSegment)) {
|
|
95
|
+
// If the line intersects the segment boundary, then skip it
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
maxMajor = distance2 - EPSILON;
|
|
99
|
+
maxMajorPoints = [index1, index2];
|
|
100
|
+
maxMinor = 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!maxMajorPoints) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
maxMajor = Math.sqrt(maxMajor + EPSILON);
|
|
108
|
+
const handle0 = points[maxMajorPoints[0]];
|
|
109
|
+
const handle1 = points[maxMajorPoints[1]];
|
|
110
|
+
const unitMajor = vec3.sub(vec3.create(), handle0, handle1);
|
|
111
|
+
vec3.scale(unitMajor, unitMajor, 1 / maxMajor);
|
|
112
|
+
|
|
113
|
+
let maxMinorPoints;
|
|
114
|
+
|
|
115
|
+
for (let index1 = 0; index1 < points.length; index1++) {
|
|
116
|
+
for (let index2 = index1 + 1; index2 < points.length; index2++) {
|
|
117
|
+
const point1 = points[index1];
|
|
118
|
+
const point2 = points[index2];
|
|
119
|
+
const distance2 = vec3.sqrDist(point1, point2);
|
|
120
|
+
if (distance2 <= maxMinor) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const delta = vec3.sub(vec3.create(), point1, point2);
|
|
124
|
+
|
|
125
|
+
const dot = Math.abs(vec3.dot(delta, unitMajor)) / Math.sqrt(distance2);
|
|
126
|
+
if (dot > EPSILON) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!isInSegment.testCenter(point1, point2)) {
|
|
131
|
+
// Center between the two points has to be in the segment, otherwise
|
|
132
|
+
// this is out of bounds.
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!isLineInSegment(point1, point2, isInSegment)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
maxMinor = distance2;
|
|
139
|
+
maxMinorPoints = [index1, index2];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!maxMinorPoints) {
|
|
144
|
+
// Didn't find a larger minor distance
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
maxMinor = Math.sqrt(maxMinor);
|
|
148
|
+
const handle2 = points[maxMinorPoints[0]];
|
|
149
|
+
const handle3 = points[maxMinorPoints[1]];
|
|
150
|
+
|
|
151
|
+
const bidirectional = {
|
|
152
|
+
majorAxis: [handle0, handle1],
|
|
153
|
+
minorAxis: [handle2, handle3],
|
|
154
|
+
maxMajor,
|
|
155
|
+
maxMinor,
|
|
156
|
+
...sliceContour,
|
|
157
|
+
} as BidirectionalData;
|
|
158
|
+
return bidirectional;
|
|
159
|
+
}
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
setBrushThresholdForToolGroup,
|
|
16
16
|
} from './brushThresholdForToolGroup';
|
|
17
17
|
import thresholdSegmentationByRange from './thresholdSegmentationByRange';
|
|
18
|
+
import contourAndFindLargestBidirectional from './contourAndFindLargestBidirectional';
|
|
19
|
+
import createBidirectionalToolData from './createBidirectionalToolData';
|
|
20
|
+
import segmentContourAction from './segmentContourAction';
|
|
18
21
|
|
|
19
22
|
export {
|
|
20
23
|
thresholdVolumeByRange,
|
|
@@ -30,4 +33,7 @@ export {
|
|
|
30
33
|
getBrushThresholdForToolGroup,
|
|
31
34
|
setBrushThresholdForToolGroup,
|
|
32
35
|
thresholdSegmentationByRange,
|
|
36
|
+
contourAndFindLargestBidirectional,
|
|
37
|
+
createBidirectionalToolData,
|
|
38
|
+
segmentContourAction,
|
|
33
39
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Types } from '@cornerstonejs/core';
|
|
2
|
+
import { cache } from '@cornerstonejs/core';
|
|
3
|
+
import { vec3 } from 'gl-matrix';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Determines if there is a point between point1 and point2 which is not
|
|
7
|
+
* contained in the segmentation
|
|
8
|
+
*/
|
|
9
|
+
export default function isLineInSegment(
|
|
10
|
+
point1: Types.Point3,
|
|
11
|
+
point2: Types.Point3,
|
|
12
|
+
isInSegment
|
|
13
|
+
) {
|
|
14
|
+
const ijk1 = isInSegment.toIJK(point1);
|
|
15
|
+
const ijk2 = isInSegment.toIJK(point2);
|
|
16
|
+
const testPoint = vec3.create();
|
|
17
|
+
const { testIJK } = isInSegment;
|
|
18
|
+
const delta = vec3.sub(vec3.create(), ijk1, ijk2);
|
|
19
|
+
|
|
20
|
+
// Test once for index value between the two points, so the max of the
|
|
21
|
+
// difference in IJK values
|
|
22
|
+
const testSize = Math.round(Math.max(...delta.map(Math.abs)));
|
|
23
|
+
if (testSize < 2) {
|
|
24
|
+
// No need to test when there are only two points
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const unitDelta = vec3.scale(vec3.create(), delta, 1 / testSize);
|
|
28
|
+
|
|
29
|
+
for (let i = 1; i < testSize; i++) {
|
|
30
|
+
vec3.scaleAndAdd(testPoint, ijk2, unitDelta, i);
|
|
31
|
+
if (!testIJK(testPoint)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a function that tests to see if the provided line segment, specified
|
|
40
|
+
* in LPS space (as endpoints) is contained in the segment
|
|
41
|
+
*/
|
|
42
|
+
function createIsInSegment(
|
|
43
|
+
segVolumeId: string,
|
|
44
|
+
segmentIndex: number,
|
|
45
|
+
containedSegmentIndices?: Set<number>
|
|
46
|
+
) {
|
|
47
|
+
// Get segmentation volume
|
|
48
|
+
const vol = cache.getVolume(segVolumeId);
|
|
49
|
+
if (!vol) {
|
|
50
|
+
console.warn(`No volume found for ${segVolumeId}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const segData = vol.imageData.getPointData().getScalars().getData();
|
|
55
|
+
const width = vol.dimensions[0];
|
|
56
|
+
const pixelsPerSlice = width * vol.dimensions[1];
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
/**
|
|
60
|
+
* Find the center point between point1 and point2, convert it to IJK space
|
|
61
|
+
* and test if the value at that location is in the segment
|
|
62
|
+
*/
|
|
63
|
+
testCenter: (point1, point2) => {
|
|
64
|
+
const point = vec3.add(vec3.create(), point1, point2).map((it) => it / 2);
|
|
65
|
+
const ijk = vol.imageData.worldToIndex(point as vec3).map(Math.round);
|
|
66
|
+
const [i, j, k] = ijk;
|
|
67
|
+
const index = i + j * width + k * pixelsPerSlice;
|
|
68
|
+
const value = segData[index];
|
|
69
|
+
return value === segmentIndex || containedSegmentIndices?.has(value);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
toIJK: (point) => vol.imageData.worldToIndex(point as vec3),
|
|
73
|
+
|
|
74
|
+
testIJK: (ijk) => {
|
|
75
|
+
const [i, j, k] = ijk;
|
|
76
|
+
const index =
|
|
77
|
+
Math.round(i) + Math.round(j) * width + Math.round(k) * pixelsPerSlice;
|
|
78
|
+
const value = segData[index];
|
|
79
|
+
return value === segmentIndex || containedSegmentIndices?.has(value);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { createIsInSegment, isLineInSegment };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { getEnabledElement, type Types } from '@cornerstonejs/core';
|
|
2
|
+
|
|
3
|
+
import type { Annotation } from '../../types/AnnotationTypes';
|
|
4
|
+
import * as segmentation from '../../stateManagement/segmentation';
|
|
5
|
+
import {
|
|
6
|
+
state as annotationState,
|
|
7
|
+
config as annotationConfig,
|
|
8
|
+
} from '../../stateManagement/annotation';
|
|
9
|
+
import { jumpToSlice } from '../viewport';
|
|
10
|
+
import contourAndFindLargestBidirectional from './contourAndFindLargestBidirectional';
|
|
11
|
+
import createBidirectionalToolData from './createBidirectionalToolData';
|
|
12
|
+
import BidirectionalTool from '../../tools/annotation/BidirectionalTool';
|
|
13
|
+
|
|
14
|
+
export type Segment = {
|
|
15
|
+
segmentationId: string;
|
|
16
|
+
segmentIndex: number;
|
|
17
|
+
label: string;
|
|
18
|
+
|
|
19
|
+
style?: any;
|
|
20
|
+
containedSegmentIndices?: (number) => boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SegmentContourActionConfiguration = {
|
|
24
|
+
getSegment?: (
|
|
25
|
+
enabledElement: Types.IEnabledElement,
|
|
26
|
+
configuration: SegmentContourActionConfiguration
|
|
27
|
+
) => Segment;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional map for data about each segment
|
|
31
|
+
*/
|
|
32
|
+
segmentationId?: string;
|
|
33
|
+
segmentIndex?: number;
|
|
34
|
+
segmentData?: Map<number, Segment>;
|
|
35
|
+
toolGroupId?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default function segmentContourAction(
|
|
39
|
+
element: HTMLDivElement,
|
|
40
|
+
configuration
|
|
41
|
+
) {
|
|
42
|
+
const { data: configurationData } = configuration;
|
|
43
|
+
const enabledElement = getEnabledElement(element);
|
|
44
|
+
const segment = (configurationData.getSegment || defaultGetSegment)(
|
|
45
|
+
enabledElement,
|
|
46
|
+
configurationData
|
|
47
|
+
);
|
|
48
|
+
if (!segment) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const FrameOfReferenceUID = enabledElement.viewport.getFrameOfReferenceUID();
|
|
52
|
+
const segmentationsList = segmentation.state.getSegmentations();
|
|
53
|
+
const { segmentIndex, segmentationId } = segment;
|
|
54
|
+
const bidirectionals = annotationState.getAnnotations(
|
|
55
|
+
this.toolName || BidirectionalTool.toolName,
|
|
56
|
+
FrameOfReferenceUID
|
|
57
|
+
);
|
|
58
|
+
let hasExistingActiveSegment = false;
|
|
59
|
+
const existingLargestBidirectionals = bidirectionals.filter(
|
|
60
|
+
(existingBidirectionalItem) => {
|
|
61
|
+
const { segment } = existingBidirectionalItem.data;
|
|
62
|
+
if (!segment) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
segment.segmentationId === segmentationId &&
|
|
67
|
+
segment.segmentIndex === segmentIndex
|
|
68
|
+
) {
|
|
69
|
+
hasExistingActiveSegment = true;
|
|
70
|
+
existingBidirectionalItem.data.segment = segment;
|
|
71
|
+
}
|
|
72
|
+
return !!segment;
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
if (!hasExistingActiveSegment) {
|
|
76
|
+
// Just create a dummy annotation object containing just enough information
|
|
77
|
+
// to create a real one.
|
|
78
|
+
existingLargestBidirectionals.push({
|
|
79
|
+
data: { segment },
|
|
80
|
+
} as unknown as Annotation);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let newBidirectional;
|
|
84
|
+
existingLargestBidirectionals.forEach((existingLargestBidirectional) => {
|
|
85
|
+
const segments = [];
|
|
86
|
+
const { segment: updateSegment } = existingLargestBidirectional.data;
|
|
87
|
+
const { segmentIndex, segmentationId } = updateSegment;
|
|
88
|
+
segments[segmentIndex] = updateSegment;
|
|
89
|
+
annotationState.removeAnnotation(
|
|
90
|
+
existingLargestBidirectional.annotationUID
|
|
91
|
+
);
|
|
92
|
+
const bidirectionalData = contourAndFindLargestBidirectional({
|
|
93
|
+
...segmentationsList.find(
|
|
94
|
+
(segmentation) => segmentation.segmentationId === segmentationId
|
|
95
|
+
),
|
|
96
|
+
segments,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!bidirectionalData) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const bidirectionalToolData = createBidirectionalToolData(
|
|
103
|
+
bidirectionalData,
|
|
104
|
+
enabledElement.viewport
|
|
105
|
+
);
|
|
106
|
+
bidirectionalToolData.annotationUID =
|
|
107
|
+
existingLargestBidirectional.annotationUID;
|
|
108
|
+
bidirectionalToolData.data.segment = updateSegment;
|
|
109
|
+
|
|
110
|
+
const annotationUID = annotationState.addAnnotation(
|
|
111
|
+
bidirectionalToolData,
|
|
112
|
+
FrameOfReferenceUID
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
updateSegment.segmentIndex === segment.segmentIndex &&
|
|
117
|
+
updateSegment.segmentationId === segment.segmentationId
|
|
118
|
+
) {
|
|
119
|
+
newBidirectional = bidirectionalData;
|
|
120
|
+
const { style } = segment;
|
|
121
|
+
if (style) {
|
|
122
|
+
annotationConfig.style.setAnnotationStyles(annotationUID, style);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (newBidirectional) {
|
|
128
|
+
const { referencedImageId } = newBidirectional;
|
|
129
|
+
const imageIds = enabledElement.viewport.getImageIds();
|
|
130
|
+
const imageIndex = imageIds.findIndex(
|
|
131
|
+
(imageId) => imageId === referencedImageId
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// TODO - figure out why this is reversed
|
|
135
|
+
jumpToSlice(element, {
|
|
136
|
+
imageIndex: imageIds.length - 1 - imageIndex,
|
|
137
|
+
});
|
|
138
|
+
enabledElement.viewport.render();
|
|
139
|
+
} else {
|
|
140
|
+
console.warn('No bidirectional found');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return newBidirectional;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function defaultGetSegment(
|
|
147
|
+
enabledElement: Types.IEnabledElement,
|
|
148
|
+
configuration: SegmentContourActionConfiguration
|
|
149
|
+
): Segment {
|
|
150
|
+
const segmentationsList = segmentation.state.getSegmentations();
|
|
151
|
+
if (!segmentationsList.length) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const segmentationId =
|
|
155
|
+
configuration.segmentationId || segmentationsList[0].segmentationId;
|
|
156
|
+
const segmentIndex =
|
|
157
|
+
configuration.segmentIndex ??
|
|
158
|
+
segmentation.segmentIndex.getActiveSegmentIndex(segmentationId);
|
|
159
|
+
if (!segmentIndex) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const segmentData = configuration.segmentData?.get(segmentIndex);
|
|
163
|
+
return {
|
|
164
|
+
label: `Segment ${segmentIndex}`,
|
|
165
|
+
segmentIndex,
|
|
166
|
+
segmentationId,
|
|
167
|
+
...segmentData,
|
|
168
|
+
};
|
|
169
|
+
}
|