@cornerstonejs/tools 1.62.0 → 1.63.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/index.d.ts +2 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tools/index.d.ts +2 -1
- package/dist/cjs/tools/index.js +4 -1
- package/dist/cjs/tools/index.js.map +1 -1
- package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +63 -0
- package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js +347 -0
- package/dist/cjs/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -0
- package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +3 -0
- package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js +73 -0
- package/dist/cjs/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
- package/dist/cjs/types/ToolSpecificAnnotationTypes.d.ts +34 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tools/index.js +2 -1
- package/dist/esm/tools/index.js.map +1 -1
- package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js +342 -0
- package/dist/esm/tools/segmentation/CircleROIStartEndThresholdTool.js.map +1 -0
- package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js +75 -2
- package/dist/esm/tools/segmentation/RectangleROIStartEndThresholdTool.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/tools/index.d.ts +2 -1
- package/dist/types/tools/index.d.ts.map +1 -1
- package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts +64 -0
- package/dist/types/tools/segmentation/CircleROIStartEndThresholdTool.d.ts.map +1 -0
- package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts +3 -0
- package/dist/types/tools/segmentation/RectangleROIStartEndThresholdTool.d.ts.map +1 -1
- package/dist/types/types/ToolSpecificAnnotationTypes.d.ts +34 -1
- package/dist/types/types/ToolSpecificAnnotationTypes.d.ts.map +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/segmentation/CircleROIStartEndThresholdTool.ts +677 -0
- package/src/tools/segmentation/RectangleROIStartEndThresholdTool.ts +134 -2
- package/src/types/ToolSpecificAnnotationTypes.ts +43 -8
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StackViewport,
|
|
3
|
+
Types,
|
|
4
|
+
cache,
|
|
5
|
+
getEnabledElement,
|
|
6
|
+
utilities as csUtils,
|
|
7
|
+
metaData,
|
|
8
|
+
triggerEvent,
|
|
9
|
+
eventTarget,
|
|
10
|
+
} from '@cornerstonejs/core';
|
|
11
|
+
|
|
12
|
+
import { vec3 } from 'gl-matrix';
|
|
13
|
+
import { Events } from '../../enums';
|
|
14
|
+
import {
|
|
15
|
+
addAnnotation,
|
|
16
|
+
removeAnnotation,
|
|
17
|
+
getAnnotations,
|
|
18
|
+
} from '../../stateManagement/annotation/annotationState';
|
|
19
|
+
import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking';
|
|
20
|
+
import {
|
|
21
|
+
drawCircle as drawCircleSvg,
|
|
22
|
+
drawHandles as drawHandlesSvg,
|
|
23
|
+
} from '../../drawingSvg';
|
|
24
|
+
import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
|
|
25
|
+
import throttle from '../../utilities/throttle';
|
|
26
|
+
import { AnnotationModifiedEventDetail } from '../../types/EventTypes';
|
|
27
|
+
import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
|
|
28
|
+
import {
|
|
29
|
+
hideElementCursor,
|
|
30
|
+
resetElementCursor,
|
|
31
|
+
} from '../../cursors/elementCursor';
|
|
32
|
+
import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds';
|
|
33
|
+
import { triggerAnnotationCompleted } from '../../stateManagement/annotation/helpers/state';
|
|
34
|
+
import {
|
|
35
|
+
PublicToolProps,
|
|
36
|
+
ToolProps,
|
|
37
|
+
EventTypes,
|
|
38
|
+
SVGDrawingHelper,
|
|
39
|
+
} from '../../types';
|
|
40
|
+
import { CircleROIStartEndThresholdAnnotation } from '../../types/ToolSpecificAnnotationTypes';
|
|
41
|
+
import CircleROITool from '../annotation/CircleROITool';
|
|
42
|
+
import { StyleSpecifier } from '../../types/AnnotationStyle';
|
|
43
|
+
import {
|
|
44
|
+
getCanvasCircleCorners,
|
|
45
|
+
getCanvasCircleRadius,
|
|
46
|
+
} from '../../utilities/math/circle';
|
|
47
|
+
import { pointInEllipse } from '../../utilities/math/ellipse';
|
|
48
|
+
import { pointInShapeCallback } from '../../utilities';
|
|
49
|
+
|
|
50
|
+
const { transformWorldToIndex } = csUtils;
|
|
51
|
+
|
|
52
|
+
class CircleROIStartEndThresholdTool extends CircleROITool {
|
|
53
|
+
static toolName;
|
|
54
|
+
|
|
55
|
+
touchDragCallback: any;
|
|
56
|
+
mouseDragCallback: any;
|
|
57
|
+
_throttledCalculateCachedStats: any;
|
|
58
|
+
editData: {
|
|
59
|
+
annotation: any;
|
|
60
|
+
viewportIdsToRender: Array<string>;
|
|
61
|
+
handleIndex?: number;
|
|
62
|
+
newAnnotation?: boolean;
|
|
63
|
+
hasMoved?: boolean;
|
|
64
|
+
} | null;
|
|
65
|
+
isDrawing: boolean;
|
|
66
|
+
isHandleOutsideImage = false;
|
|
67
|
+
|
|
68
|
+
constructor(
|
|
69
|
+
toolProps: PublicToolProps = {},
|
|
70
|
+
defaultToolProps: ToolProps = {
|
|
71
|
+
supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
72
|
+
configuration: {
|
|
73
|
+
numSlicesToPropagate: 10,
|
|
74
|
+
calculatePointsInsideVolume: false,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
) {
|
|
78
|
+
super(toolProps, defaultToolProps);
|
|
79
|
+
|
|
80
|
+
this._throttledCalculateCachedStats = throttle(
|
|
81
|
+
this._calculateCachedStatsTool,
|
|
82
|
+
100,
|
|
83
|
+
{ trailing: true }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Based on the current position of the mouse and the current imageId to create
|
|
89
|
+
* a CircleROI Annotation and stores it in the annotationManager
|
|
90
|
+
*
|
|
91
|
+
* @param evt - EventTypes.NormalizedMouseEventType
|
|
92
|
+
* @returns The annotation object.
|
|
93
|
+
*
|
|
94
|
+
*/
|
|
95
|
+
addNewAnnotation = (evt: EventTypes.InteractionEventType) => {
|
|
96
|
+
const eventDetail = evt.detail;
|
|
97
|
+
const { currentPoints, element } = eventDetail;
|
|
98
|
+
const worldPos = currentPoints.world;
|
|
99
|
+
const canvasPos = currentPoints.canvas;
|
|
100
|
+
|
|
101
|
+
const enabledElement = getEnabledElement(element);
|
|
102
|
+
const { viewport, renderingEngine } = enabledElement;
|
|
103
|
+
|
|
104
|
+
this.isDrawing = true;
|
|
105
|
+
|
|
106
|
+
const camera = viewport.getCamera();
|
|
107
|
+
const { viewPlaneNormal, viewUp } = camera;
|
|
108
|
+
|
|
109
|
+
let referencedImageId, imageVolume, volumeId;
|
|
110
|
+
if (viewport instanceof StackViewport) {
|
|
111
|
+
throw new Error('Stack Viewport Not implemented');
|
|
112
|
+
} else {
|
|
113
|
+
const targetId = this.getTargetId(viewport);
|
|
114
|
+
volumeId = targetId.split(/volumeId:|\?/)[1];
|
|
115
|
+
imageVolume = cache.getVolume(volumeId);
|
|
116
|
+
|
|
117
|
+
referencedImageId = csUtils.getClosestImageId(
|
|
118
|
+
imageVolume,
|
|
119
|
+
worldPos,
|
|
120
|
+
viewPlaneNormal
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// if (!referencedImageId) {
|
|
125
|
+
// throw new Error('This tool does not work on non-acquisition planes');
|
|
126
|
+
// }
|
|
127
|
+
|
|
128
|
+
const spacingInNormal = csUtils.getSpacingInNormalDirection(
|
|
129
|
+
imageVolume,
|
|
130
|
+
viewPlaneNormal
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const newStartIndex = this._getStartSliceIndex(
|
|
134
|
+
imageVolume,
|
|
135
|
+
worldPos,
|
|
136
|
+
spacingInNormal,
|
|
137
|
+
viewPlaneNormal
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// We cannot newStartIndex add numSlicesToPropagate to startIndex because
|
|
141
|
+
// the order of imageIds can be from top to bottom or bottom to top and
|
|
142
|
+
// we want to make sure it is always propagated in the direction of the
|
|
143
|
+
// view and also to make sure we don't go out of bounds.
|
|
144
|
+
const endIndex = this._getEndSliceIndex(
|
|
145
|
+
imageVolume,
|
|
146
|
+
worldPos,
|
|
147
|
+
spacingInNormal,
|
|
148
|
+
viewPlaneNormal
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
|
|
152
|
+
|
|
153
|
+
const annotation = {
|
|
154
|
+
highlighted: true,
|
|
155
|
+
invalidated: true,
|
|
156
|
+
metadata: {
|
|
157
|
+
toolName: this.getToolName(),
|
|
158
|
+
viewPlaneNormal: <Types.Point3>[...viewPlaneNormal],
|
|
159
|
+
viewUp: <Types.Point3>[...viewUp],
|
|
160
|
+
FrameOfReferenceUID,
|
|
161
|
+
referencedImageId,
|
|
162
|
+
volumeId,
|
|
163
|
+
spacingInNormal,
|
|
164
|
+
enabledElement,
|
|
165
|
+
},
|
|
166
|
+
data: {
|
|
167
|
+
label: '',
|
|
168
|
+
startSlice: newStartIndex,
|
|
169
|
+
endSlice: endIndex,
|
|
170
|
+
|
|
171
|
+
handles: {
|
|
172
|
+
textBox: {
|
|
173
|
+
hasMoved: false,
|
|
174
|
+
worldPosition: null,
|
|
175
|
+
worldBoundingBox: null,
|
|
176
|
+
},
|
|
177
|
+
points: [[...worldPos], [...worldPos]] as [
|
|
178
|
+
Types.Point3, // center
|
|
179
|
+
Types.Point3 // end
|
|
180
|
+
],
|
|
181
|
+
activeHandleIndex: null,
|
|
182
|
+
},
|
|
183
|
+
cachedStats: {
|
|
184
|
+
pointsInVolume: [],
|
|
185
|
+
projectionPoints: [],
|
|
186
|
+
},
|
|
187
|
+
labelmapUID: null,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
addAnnotation(annotation, element);
|
|
192
|
+
|
|
193
|
+
const viewportIdsToRender = getViewportIdsWithToolToRender(
|
|
194
|
+
element,
|
|
195
|
+
this.getToolName()
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
this.editData = {
|
|
199
|
+
annotation,
|
|
200
|
+
viewportIdsToRender,
|
|
201
|
+
newAnnotation: true,
|
|
202
|
+
hasMoved: false,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
this._activateDraw(element);
|
|
206
|
+
hideElementCursor(element);
|
|
207
|
+
|
|
208
|
+
evt.preventDefault();
|
|
209
|
+
|
|
210
|
+
triggerAnnotationRenderForViewportIds(renderingEngine, viewportIdsToRender);
|
|
211
|
+
|
|
212
|
+
return annotation;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
_endCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
216
|
+
const eventDetail = evt.detail;
|
|
217
|
+
const { element } = eventDetail;
|
|
218
|
+
|
|
219
|
+
const { annotation, viewportIdsToRender, newAnnotation, hasMoved } =
|
|
220
|
+
this.editData;
|
|
221
|
+
const { data } = annotation;
|
|
222
|
+
|
|
223
|
+
if (newAnnotation && !hasMoved) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Circle ROI tool should reset its highlight to false on mouse up (as opposed
|
|
228
|
+
// to other tools that keep it highlighted until the user moves. The reason
|
|
229
|
+
// is that we use top-left and bottom-right handles to define the circle,
|
|
230
|
+
// and they are by definition not in the circle on mouse up.
|
|
231
|
+
annotation.highlighted = false;
|
|
232
|
+
data.handles.activeHandleIndex = null;
|
|
233
|
+
|
|
234
|
+
this._deactivateModify(element);
|
|
235
|
+
this._deactivateDraw(element);
|
|
236
|
+
|
|
237
|
+
resetElementCursor(element);
|
|
238
|
+
|
|
239
|
+
const enabledElement = getEnabledElement(element);
|
|
240
|
+
|
|
241
|
+
this.editData = null;
|
|
242
|
+
this.isDrawing = false;
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
this.isHandleOutsideImage &&
|
|
246
|
+
this.configuration.preventHandleOutsideImage
|
|
247
|
+
) {
|
|
248
|
+
removeAnnotation(annotation.annotationUID);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const targetId = this.getTargetId(enabledElement.viewport);
|
|
252
|
+
const imageVolume = cache.getVolume(targetId.split(/volumeId:|\?/)[1]);
|
|
253
|
+
|
|
254
|
+
if (this.configuration.calculatePointsInsideVolume) {
|
|
255
|
+
this._computePointsInsideVolume(annotation, imageVolume, enabledElement);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
triggerAnnotationRenderForViewportIds(
|
|
259
|
+
enabledElement.renderingEngine,
|
|
260
|
+
viewportIdsToRender
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (newAnnotation) {
|
|
264
|
+
triggerAnnotationCompleted(annotation);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* it is used to draw the circleROI annotation in each
|
|
270
|
+
* request animation frame. It calculates the updated cached statistics if
|
|
271
|
+
* data is invalidated and cache it.
|
|
272
|
+
*
|
|
273
|
+
* @param enabledElement - The Cornerstone's enabledElement.
|
|
274
|
+
* @param svgDrawingHelper - The svgDrawingHelper providing the context for drawing.
|
|
275
|
+
*/
|
|
276
|
+
renderAnnotation = (
|
|
277
|
+
enabledElement: Types.IEnabledElement,
|
|
278
|
+
svgDrawingHelper: SVGDrawingHelper
|
|
279
|
+
): boolean => {
|
|
280
|
+
let renderStatus = false;
|
|
281
|
+
const { viewport } = enabledElement;
|
|
282
|
+
|
|
283
|
+
const annotations = getAnnotations(this.getToolName(), viewport.element);
|
|
284
|
+
|
|
285
|
+
if (!annotations?.length) {
|
|
286
|
+
return renderStatus;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const sliceIndex = viewport.getCurrentImageIdIndex();
|
|
290
|
+
|
|
291
|
+
const styleSpecifier: StyleSpecifier = {
|
|
292
|
+
toolGroupId: this.toolGroupId,
|
|
293
|
+
toolName: this.getToolName(),
|
|
294
|
+
viewportId: enabledElement.viewport.id,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < annotations.length; i++) {
|
|
298
|
+
const annotation = annotations[i] as CircleROIStartEndThresholdAnnotation;
|
|
299
|
+
const { annotationUID, data } = annotation;
|
|
300
|
+
const { startSlice, endSlice } = data;
|
|
301
|
+
const { points, activeHandleIndex } = data.handles;
|
|
302
|
+
|
|
303
|
+
styleSpecifier.annotationUID = annotationUID;
|
|
304
|
+
|
|
305
|
+
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
|
306
|
+
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
|
307
|
+
const color = this.getStyle('color', styleSpecifier, annotation);
|
|
308
|
+
|
|
309
|
+
const canvasCoordinates = points.map((p) =>
|
|
310
|
+
viewport.worldToCanvas(p)
|
|
311
|
+
) as [Types.Point2, Types.Point2];
|
|
312
|
+
const center = canvasCoordinates[0];
|
|
313
|
+
|
|
314
|
+
const radius = getCanvasCircleRadius(canvasCoordinates);
|
|
315
|
+
const { centerPointRadius } = this.configuration;
|
|
316
|
+
|
|
317
|
+
// range of slices to render based on the start and end slice, like
|
|
318
|
+
// np.arange
|
|
319
|
+
|
|
320
|
+
// if indexIJK is outside the start/end slice, we don't render
|
|
321
|
+
if (
|
|
322
|
+
sliceIndex < Math.min(startSlice, endSlice) ||
|
|
323
|
+
sliceIndex > Math.max(startSlice, endSlice)
|
|
324
|
+
) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// WE HAVE TO CACHE STATS BEFORE FETCHING TEXT
|
|
329
|
+
|
|
330
|
+
if (annotation.invalidated) {
|
|
331
|
+
this._throttledCalculateCachedStats(annotation, enabledElement);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const middleSlice = Math.round((startSlice + endSlice) / 2);
|
|
335
|
+
// if it is inside the start/end slice, but not exactly the first or
|
|
336
|
+
// last slice, we render the line in dash, but not the handles
|
|
337
|
+
|
|
338
|
+
let isMiddleSlice = false;
|
|
339
|
+
if (sliceIndex === middleSlice) {
|
|
340
|
+
isMiddleSlice = true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// If rendering engine has been destroyed while rendering
|
|
344
|
+
if (!viewport.getRenderingEngine()) {
|
|
345
|
+
console.warn('Rendering Engine has been destroyed');
|
|
346
|
+
return renderStatus;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let activeHandleCanvasCoords;
|
|
350
|
+
|
|
351
|
+
if (!isAnnotationVisible(annotationUID)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
!isAnnotationLocked(annotation) &&
|
|
357
|
+
!this.editData &&
|
|
358
|
+
activeHandleIndex !== null &&
|
|
359
|
+
isMiddleSlice
|
|
360
|
+
) {
|
|
361
|
+
// Not locked or creating and hovering over handle, so render handle.
|
|
362
|
+
activeHandleCanvasCoords = [canvasCoordinates[activeHandleIndex]];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (activeHandleCanvasCoords) {
|
|
366
|
+
const handleGroupUID = '0';
|
|
367
|
+
|
|
368
|
+
drawHandlesSvg(
|
|
369
|
+
svgDrawingHelper,
|
|
370
|
+
annotationUID,
|
|
371
|
+
handleGroupUID,
|
|
372
|
+
activeHandleCanvasCoords,
|
|
373
|
+
{
|
|
374
|
+
color,
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let lineWidthToUse = lineWidth;
|
|
380
|
+
|
|
381
|
+
if (isMiddleSlice) {
|
|
382
|
+
lineWidthToUse = 3;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const circleUID = '0';
|
|
386
|
+
drawCircleSvg(
|
|
387
|
+
svgDrawingHelper,
|
|
388
|
+
annotationUID,
|
|
389
|
+
circleUID,
|
|
390
|
+
center,
|
|
391
|
+
radius,
|
|
392
|
+
{
|
|
393
|
+
color,
|
|
394
|
+
lineDash,
|
|
395
|
+
lineWidth: lineWidthToUse,
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// draw center point, if "centerPointRadius" configuration is valid.
|
|
400
|
+
if (centerPointRadius > 0) {
|
|
401
|
+
if (radius > 3 * centerPointRadius) {
|
|
402
|
+
drawCircleSvg(
|
|
403
|
+
svgDrawingHelper,
|
|
404
|
+
annotationUID,
|
|
405
|
+
`${circleUID}-center`,
|
|
406
|
+
center,
|
|
407
|
+
centerPointRadius,
|
|
408
|
+
{
|
|
409
|
+
color,
|
|
410
|
+
lineDash,
|
|
411
|
+
lineWidth,
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
renderStatus = true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return renderStatus;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// Todo: make it work for planes other than acquisition planes
|
|
424
|
+
_computeProjectionPoints(
|
|
425
|
+
annotation: CircleROIStartEndThresholdAnnotation,
|
|
426
|
+
imageVolume: Types.IImageVolume
|
|
427
|
+
): void {
|
|
428
|
+
const { data, metadata } = annotation;
|
|
429
|
+
const { viewPlaneNormal, spacingInNormal } = metadata;
|
|
430
|
+
const { imageData } = imageVolume;
|
|
431
|
+
const { startSlice, endSlice } = data;
|
|
432
|
+
const { points } = data.handles;
|
|
433
|
+
|
|
434
|
+
const startIJK = transformWorldToIndex(imageData, points[0]);
|
|
435
|
+
startIJK[2] = startSlice;
|
|
436
|
+
|
|
437
|
+
if (startIJK[2] !== startSlice) {
|
|
438
|
+
throw new Error('Start slice does not match');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// substitute the end slice index 2 with startIJK index 2
|
|
442
|
+
const endIJK = vec3.fromValues(startIJK[0], startIJK[1], endSlice);
|
|
443
|
+
|
|
444
|
+
const startWorld = vec3.create();
|
|
445
|
+
imageData.indexToWorldVec3(startIJK, startWorld);
|
|
446
|
+
|
|
447
|
+
const endWorld = vec3.create();
|
|
448
|
+
imageData.indexToWorldVec3(endIJK, endWorld);
|
|
449
|
+
|
|
450
|
+
// distance between start and end slice in the world coordinate
|
|
451
|
+
const distance = vec3.distance(startWorld, endWorld);
|
|
452
|
+
|
|
453
|
+
// for each point inside points, navigate in the direction of the viewPlaneNormal
|
|
454
|
+
// with amount of spacingInNormal, and calculate the next slice until we reach the distance
|
|
455
|
+
const newProjectionPoints = [];
|
|
456
|
+
for (let dist = 0; dist < distance; dist += spacingInNormal) {
|
|
457
|
+
newProjectionPoints.push(
|
|
458
|
+
points.map((point) => {
|
|
459
|
+
const newPoint = vec3.create();
|
|
460
|
+
//@ts-ignore
|
|
461
|
+
vec3.scaleAndAdd(newPoint, point, viewPlaneNormal, dist);
|
|
462
|
+
return Array.from(newPoint);
|
|
463
|
+
})
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
data.cachedStats.projectionPoints = newProjectionPoints;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
_computePointsInsideVolume(annotation, imageVolume, enabledElement) {
|
|
471
|
+
const { data } = annotation;
|
|
472
|
+
const { viewport } = enabledElement;
|
|
473
|
+
const projectionPoints = data.cachedStats.projectionPoints;
|
|
474
|
+
|
|
475
|
+
const pointsInsideVolume: Types.Point3[][] = [[]];
|
|
476
|
+
|
|
477
|
+
for (let i = 0; i < projectionPoints.length; i++) {
|
|
478
|
+
// If image does not exists for the targetId, skip. This can be due
|
|
479
|
+
// to various reasons such as if the target was a volumeViewport, and
|
|
480
|
+
// the volumeViewport has been decached in the meantime.
|
|
481
|
+
if (!imageVolume) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const centerWorld = projectionPoints[i][0];
|
|
486
|
+
const canvasCoordinates = projectionPoints[i].map((p) =>
|
|
487
|
+
viewport.worldToCanvas(p)
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const [topLeftCanvas, bottomRightCanvas] = <Array<Types.Point2>>(
|
|
491
|
+
getCanvasCircleCorners(canvasCoordinates)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const topLeftWorld = viewport.canvasToWorld(topLeftCanvas);
|
|
495
|
+
const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas);
|
|
496
|
+
|
|
497
|
+
const worldPos1 = topLeftWorld;
|
|
498
|
+
const worldPos2 = bottomRightWorld;
|
|
499
|
+
|
|
500
|
+
const { dimensions, imageData } = imageVolume;
|
|
501
|
+
|
|
502
|
+
const worldPos1Index = transformWorldToIndex(imageData, worldPos1);
|
|
503
|
+
const worldCenterIndex = transformWorldToIndex(imageData, centerWorld);
|
|
504
|
+
|
|
505
|
+
worldPos1Index[0] = Math.floor(worldPos1Index[0]);
|
|
506
|
+
worldPos1Index[1] = Math.floor(worldPos1Index[1]);
|
|
507
|
+
worldPos1Index[2] = Math.floor(worldCenterIndex[2]);
|
|
508
|
+
|
|
509
|
+
const worldPos2Index = transformWorldToIndex(imageData, worldPos2);
|
|
510
|
+
|
|
511
|
+
worldPos2Index[0] = Math.floor(worldPos2Index[0]);
|
|
512
|
+
worldPos2Index[1] = Math.floor(worldPos2Index[1]);
|
|
513
|
+
worldPos2Index[2] = Math.floor(worldCenterIndex[2]);
|
|
514
|
+
|
|
515
|
+
// Check if one of the indexes are inside the volume, this then gives us
|
|
516
|
+
// Some area to do stats over.
|
|
517
|
+
|
|
518
|
+
if (this._isInsideVolume(worldPos1Index, worldPos2Index, dimensions)) {
|
|
519
|
+
const iMin = Math.min(worldPos1Index[0], worldPos2Index[0]);
|
|
520
|
+
const iMax = Math.max(worldPos1Index[0], worldPos2Index[0]);
|
|
521
|
+
|
|
522
|
+
const jMin = Math.min(worldPos1Index[1], worldPos2Index[1]);
|
|
523
|
+
const jMax = Math.max(worldPos1Index[1], worldPos2Index[1]);
|
|
524
|
+
|
|
525
|
+
const kMin = Math.min(worldPos1Index[2], worldPos2Index[2]);
|
|
526
|
+
const kMax = Math.max(worldPos1Index[2], worldPos2Index[2]);
|
|
527
|
+
|
|
528
|
+
const boundsIJK = [
|
|
529
|
+
[iMin, iMax],
|
|
530
|
+
[jMin, jMax],
|
|
531
|
+
[kMin, kMax],
|
|
532
|
+
] as [Types.Point2, Types.Point2, Types.Point2];
|
|
533
|
+
|
|
534
|
+
const center = centerWorld as Types.Point3;
|
|
535
|
+
|
|
536
|
+
const ellipseObj = {
|
|
537
|
+
center,
|
|
538
|
+
xRadius: Math.abs(topLeftWorld[0] - bottomRightWorld[0]) / 2,
|
|
539
|
+
yRadius: Math.abs(topLeftWorld[1] - bottomRightWorld[1]) / 2,
|
|
540
|
+
zRadius: Math.abs(topLeftWorld[2] - bottomRightWorld[2]) / 2,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const pointsInShape = pointInShapeCallback(
|
|
544
|
+
imageData,
|
|
545
|
+
//@ts-ignore
|
|
546
|
+
(pointLPS) => pointInEllipse(ellipseObj, pointLPS),
|
|
547
|
+
null,
|
|
548
|
+
boundsIJK
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
//@ts-ignore
|
|
552
|
+
pointsInsideVolume.push(pointsInShape);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
data.cachedStats.pointsInVolume = pointsInsideVolume;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_calculateCachedStatsTool(annotation, enabledElement) {
|
|
559
|
+
const data = annotation.data;
|
|
560
|
+
const { viewportId, renderingEngineId, viewport } = enabledElement;
|
|
561
|
+
|
|
562
|
+
const { cachedStats } = data;
|
|
563
|
+
const targetId = this.getTargetId(viewport);
|
|
564
|
+
const imageVolume = cache.getVolume(targetId.split(/volumeId:|\?/)[1]);
|
|
565
|
+
|
|
566
|
+
// Todo: this shouldn't be here, this is a performance issue
|
|
567
|
+
// Since we are extending the RectangleROI class, we need to
|
|
568
|
+
// bring the logic for handle to some cachedStats calculation
|
|
569
|
+
this._computeProjectionPoints(annotation, imageVolume);
|
|
570
|
+
|
|
571
|
+
annotation.invalidated = false;
|
|
572
|
+
|
|
573
|
+
// Dispatching annotation modified
|
|
574
|
+
const eventType = Events.ANNOTATION_MODIFIED;
|
|
575
|
+
|
|
576
|
+
const eventDetail: AnnotationModifiedEventDetail = {
|
|
577
|
+
annotation,
|
|
578
|
+
viewportId,
|
|
579
|
+
renderingEngineId,
|
|
580
|
+
};
|
|
581
|
+
triggerEvent(eventTarget, eventType, eventDetail);
|
|
582
|
+
|
|
583
|
+
return cachedStats;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
_getStartSliceIndex(
|
|
587
|
+
imageVolume: Types.IImageVolume,
|
|
588
|
+
worldPos: Types.Point3,
|
|
589
|
+
spacingInNormal: number,
|
|
590
|
+
viewPlaneNormal: Types.Point3
|
|
591
|
+
): number | undefined {
|
|
592
|
+
const numSlicesToPropagate = this.configuration.numSlicesToPropagate;
|
|
593
|
+
|
|
594
|
+
const numSlicesToPropagateFromStart = Math.round(numSlicesToPropagate / 2);
|
|
595
|
+
// get end position by moving from worldPos in the direction of viewplaneNormal
|
|
596
|
+
// with amount of numSlicesToPropagate * spacingInNormal
|
|
597
|
+
const startPos = vec3.create();
|
|
598
|
+
vec3.scaleAndAdd(
|
|
599
|
+
startPos,
|
|
600
|
+
worldPos,
|
|
601
|
+
viewPlaneNormal,
|
|
602
|
+
numSlicesToPropagateFromStart * -spacingInNormal
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const imageIdIndex = this._getImageIdIndex(
|
|
606
|
+
imageVolume,
|
|
607
|
+
startPos,
|
|
608
|
+
spacingInNormal,
|
|
609
|
+
viewPlaneNormal
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
return imageIdIndex;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
_getEndSliceIndex(
|
|
616
|
+
imageVolume: Types.IImageVolume,
|
|
617
|
+
worldPos: Types.Point3,
|
|
618
|
+
spacingInNormal: number,
|
|
619
|
+
viewPlaneNormal: Types.Point3
|
|
620
|
+
): number | undefined {
|
|
621
|
+
const numSlicesToPropagate = this.configuration.numSlicesToPropagate;
|
|
622
|
+
const numSlicesToPropagateFromStart = Math.round(numSlicesToPropagate / 2);
|
|
623
|
+
|
|
624
|
+
// get end position by moving from worldPos in the direction of viewplaneNormal
|
|
625
|
+
// with amount of numSlicesToPropagate * spacingInNormal
|
|
626
|
+
const endPos = vec3.create();
|
|
627
|
+
vec3.scaleAndAdd(
|
|
628
|
+
endPos,
|
|
629
|
+
worldPos,
|
|
630
|
+
viewPlaneNormal,
|
|
631
|
+
numSlicesToPropagateFromStart * spacingInNormal
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
const imageIdIndex = this._getImageIdIndex(
|
|
635
|
+
imageVolume,
|
|
636
|
+
endPos,
|
|
637
|
+
spacingInNormal,
|
|
638
|
+
viewPlaneNormal
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return imageIdIndex;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
_getImageIdIndex(
|
|
645
|
+
imageVolume: Types.IImageVolume,
|
|
646
|
+
pos: vec3,
|
|
647
|
+
spacingInNormal: number,
|
|
648
|
+
viewPlaneNormal: Types.Point3
|
|
649
|
+
): number | undefined {
|
|
650
|
+
const halfSpacingInNormalDirection = spacingInNormal / 2;
|
|
651
|
+
// Loop through imageIds of the imageVolume and find the one that is closest to endPos
|
|
652
|
+
const { imageIds } = imageVolume;
|
|
653
|
+
let imageIdIndex;
|
|
654
|
+
for (let i = 0; i < imageIds.length; i++) {
|
|
655
|
+
const imageId = imageIds[i];
|
|
656
|
+
|
|
657
|
+
const { imagePositionPatient } = metaData.get(
|
|
658
|
+
'imagePlaneModule',
|
|
659
|
+
imageId
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
const dir = vec3.create();
|
|
663
|
+
vec3.sub(dir, pos, imagePositionPatient);
|
|
664
|
+
|
|
665
|
+
const dot = vec3.dot(dir, viewPlaneNormal);
|
|
666
|
+
|
|
667
|
+
if (Math.abs(dot) < halfSpacingInNormalDirection) {
|
|
668
|
+
imageIdIndex = i;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return imageIdIndex;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
CircleROIStartEndThresholdTool.toolName = 'CircleROIStartEndThreshold';
|
|
677
|
+
export default CircleROIStartEndThresholdTool;
|