@cornerstonejs/tools 3.0.5 → 3.1.1

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.
Files changed (31) hide show
  1. package/dist/esm/enums/ChangeTypes.d.ts +2 -1
  2. package/dist/esm/enums/ChangeTypes.js +1 -0
  3. package/dist/esm/tools/SculptorTool.js +2 -2
  4. package/dist/esm/tools/annotation/AngleTool.js +5 -2
  5. package/dist/esm/tools/annotation/ArrowAnnotateTool.js +6 -4
  6. package/dist/esm/tools/annotation/BidirectionalTool.js +6 -2
  7. package/dist/esm/tools/annotation/CircleROITool.js +6 -2
  8. package/dist/esm/tools/annotation/CobbAngleTool.js +5 -2
  9. package/dist/esm/tools/annotation/EllipticalROITool.js +6 -2
  10. package/dist/esm/tools/annotation/LengthTool.js +5 -2
  11. package/dist/esm/tools/annotation/PlanarFreehandROITool.js +2 -2
  12. package/dist/esm/tools/annotation/RectangleROITool.js +5 -2
  13. package/dist/esm/tools/annotation/RegionSegmentPlusTool.js +8 -3
  14. package/dist/esm/tools/annotation/RegionSegmentTool.js +1 -0
  15. package/dist/esm/tools/annotation/WholeBodySegmentTool.js +1 -0
  16. package/dist/esm/tools/annotation/planarFreehandROITool/drawLoop.js +6 -2
  17. package/dist/esm/tools/base/AnnotationTool.d.ts +2 -15
  18. package/dist/esm/tools/base/GrowCutBaseTool.d.ts +2 -0
  19. package/dist/esm/tools/base/GrowCutBaseTool.js +77 -5
  20. package/dist/esm/types/AnnotationTypes.d.ts +18 -16
  21. package/dist/esm/types/ToolSpecificAnnotationTypes.d.ts +1 -0
  22. package/dist/esm/utilities/index.d.ts +2 -1
  23. package/dist/esm/utilities/index.js +2 -1
  24. package/dist/esm/utilities/segmentation/growCut/growCutShader.d.ts +1 -1
  25. package/dist/esm/utilities/segmentation/growCut/growCutShader.js +43 -2
  26. package/dist/esm/utilities/segmentation/growCut/runGrowCut.js +52 -5
  27. package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.d.ts +7 -1
  28. package/dist/esm/utilities/segmentation/growCut/runOneClickGrowCut.js +62 -130
  29. package/dist/esm/utilities/setAnnotationLabel.d.ts +2 -0
  30. package/dist/esm/utilities/setAnnotationLabel.js +6 -0
  31. package/package.json +3 -3
@@ -6,6 +6,7 @@ declare enum ChangeTypes {
6
6
  Completed = "Completed",
7
7
  InterpolationUpdated = "InterpolationUpdated",
8
8
  History = "History",
9
- MetadataReferenceModified = "MetadataReferenceModified"
9
+ MetadataReferenceModified = "MetadataReferenceModified",
10
+ LabelChange = "LabelChange"
10
11
  }
11
12
  export default ChangeTypes;
@@ -8,5 +8,6 @@ var ChangeTypes;
8
8
  ChangeTypes["InterpolationUpdated"] = "InterpolationUpdated";
9
9
  ChangeTypes["History"] = "History";
10
10
  ChangeTypes["MetadataReferenceModified"] = "MetadataReferenceModified";
11
+ ChangeTypes["LabelChange"] = "LabelChange";
11
12
  })(ChangeTypes || (ChangeTypes = {}));
12
13
  export default ChangeTypes;
@@ -2,7 +2,7 @@ import { getEnabledElement } from '@cornerstonejs/core';
2
2
  import { BaseTool } from './base';
3
3
  import { getAnnotations } from '../stateManagement';
4
4
  import { point } from '../utilities/math';
5
- import { Events, ToolModes, AnnotationStyleStates } from '../enums';
5
+ import { Events, ToolModes, AnnotationStyleStates, ChangeTypes, } from '../enums';
6
6
  import { triggerAnnotationRenderForViewportIds } from '../utilities/triggerAnnotationRenderForViewportIds';
7
7
  import { hideElementCursor, resetElementCursor, } from '../cursors/elementCursor';
8
8
  import { getStyleProperty } from '../stateManagement/annotation/config/helpers';
@@ -70,7 +70,7 @@ class SculptorTool extends BaseTool {
70
70
  if (toolInstance.configuration.calculateStats) {
71
71
  activeAnnotation.invalidated = true;
72
72
  }
73
- triggerAnnotationModified(activeAnnotation, element);
73
+ triggerAnnotationModified(activeAnnotation, element, ChangeTypes.HandlesUpdated);
74
74
  };
75
75
  this.dragCallback = (evt) => {
76
76
  const eventData = evt.detail;
@@ -1,4 +1,4 @@
1
- import { Events } from '../../enums';
1
+ import { ChangeTypes, Events } from '../../enums';
2
2
  import { getEnabledElement, utilities as csUtils, getEnabledElementByViewportId, } from '@cornerstonejs/core';
3
3
  import { AnnotationTool } from '../base';
4
4
  import throttle from '../../utilities/throttle';
@@ -210,6 +210,9 @@ class AngleTool extends AnnotationTool {
210
210
  const enabledElement = getEnabledElement(element);
211
211
  const { renderingEngine } = enabledElement;
212
212
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
213
+ if (annotation.invalidated) {
214
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
215
+ }
213
216
  };
214
217
  this.cancel = (element) => {
215
218
  if (this.isDrawing) {
@@ -465,7 +468,7 @@ class AngleTool extends AnnotationTool {
465
468
  };
466
469
  }
467
470
  annotation.invalidated = false;
468
- triggerAnnotationModified(annotation, element);
471
+ triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
469
472
  return cachedStats;
470
473
  }
471
474
  }
@@ -1,4 +1,4 @@
1
- import { Events } from '../../enums';
1
+ import { ChangeTypes, Events } from '../../enums';
2
2
  import { getEnabledElement, utilities as csUtils, getEnabledElementByViewportId, } from '@cornerstonejs/core';
3
3
  import { AnnotationTool } from '../base';
4
4
  import { addAnnotation, getAnnotations, removeAnnotation, } from '../../stateManagement/annotation/annotationState';
@@ -11,6 +11,7 @@ import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnota
11
11
  import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
12
12
  import { resetElementCursor, hideElementCursor, } from '../../cursors/elementCursor';
13
13
  import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
14
+ import { setAnnotationLabel } from '../../utilities';
14
15
  class ArrowAnnotateTool extends AnnotationTool {
15
16
  static { this.toolName = 'ArrowAnnotate'; }
16
17
  constructor(toolProps = {}, defaultToolProps = {
@@ -127,7 +128,7 @@ class ArrowAnnotateTool extends AnnotationTool {
127
128
  this._endCallback = (evt) => {
128
129
  const eventDetail = evt.detail;
129
130
  const { element } = eventDetail;
130
- const { annotation, viewportIdsToRender, newAnnotation, hasMoved } = this.editData;
131
+ const { annotation, viewportIdsToRender, newAnnotation, hasMoved, movingTextBox, } = this.editData;
131
132
  const { data } = annotation;
132
133
  if (newAnnotation && !hasMoved) {
133
134
  return;
@@ -152,11 +153,12 @@ class ArrowAnnotateTool extends AnnotationTool {
152
153
  annotation.data.text = text;
153
154
  triggerAnnotationCompleted(annotation);
154
155
  this.createMemo(element, annotation, { newAnnotation: !!this.memo });
156
+ setAnnotationLabel(annotation, element, text);
155
157
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
156
158
  });
157
159
  }
158
- else {
159
- triggerAnnotationModified(annotation, element);
160
+ else if (!movingTextBox) {
161
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
160
162
  }
161
163
  this.doneEditMemo();
162
164
  this.editData = null;
@@ -9,7 +9,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
9
9
  import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
10
10
  import { drawLine as drawLineSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
11
11
  import { state } from '../../store/state';
12
- import { Events } from '../../enums';
12
+ import { ChangeTypes, Events } from '../../enums';
13
13
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
14
14
  import * as lineSegment from '../../utilities/math/line';
15
15
  import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
@@ -219,6 +219,7 @@ class BidirectionalTool extends AnnotationTool {
219
219
  data.handles.points[3] = viewport.canvasToWorld([endX, endY]);
220
220
  annotation.invalidated = true;
221
221
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
222
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
222
223
  this.editData.hasMoved = true;
223
224
  };
224
225
  this._dragModifyCallback = (evt) => {
@@ -254,6 +255,9 @@ class BidirectionalTool extends AnnotationTool {
254
255
  annotation.invalidated = true;
255
256
  }
256
257
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
258
+ if (annotation.invalidated) {
259
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
260
+ }
257
261
  };
258
262
  this._dragModifyHandle = (evt) => {
259
263
  const eventDetail = evt.detail;
@@ -629,7 +633,7 @@ class BidirectionalTool extends AnnotationTool {
629
633
  };
630
634
  }
631
635
  annotation.invalidated = false;
632
- triggerAnnotationModified(annotation, element);
636
+ triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
633
637
  return cachedStats;
634
638
  };
635
639
  this._isInsideVolume = (index1, index2, index3, index4, dimensions) => {
@@ -8,7 +8,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
8
8
  import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
9
9
  import { drawCircle as drawCircleSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
10
10
  import { state } from '../../store/state';
11
- import { Events } from '../../enums';
11
+ import { ChangeTypes, Events } from '../../enums';
12
12
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
13
13
  import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
14
14
  import getWorldWidthAndHeightFromTwoPoints from '../../utilities/planar/getWorldWidthAndHeightFromTwoPoints';
@@ -196,6 +196,7 @@ class CircleROITool extends AnnotationTool {
196
196
  annotation.invalidated = true;
197
197
  this.editData.hasMoved = true;
198
198
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
199
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
199
200
  };
200
201
  this._dragModifyCallback = (evt) => {
201
202
  this.isDrawing = true;
@@ -232,6 +233,9 @@ class CircleROITool extends AnnotationTool {
232
233
  const enabledElement = getEnabledElement(element);
233
234
  const { renderingEngine } = enabledElement;
234
235
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
236
+ if (annotation.invalidated) {
237
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
238
+ }
235
239
  };
236
240
  this._dragHandle = (evt) => {
237
241
  const eventDetail = evt.detail;
@@ -552,7 +556,7 @@ class CircleROITool extends AnnotationTool {
552
556
  }
553
557
  }
554
558
  annotation.invalidated = false;
555
- triggerAnnotationModified(annotation, element);
559
+ triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
556
560
  return cachedStats;
557
561
  };
558
562
  this._isInsideVolume = (index1, index2, dimensions) => {
@@ -1,5 +1,5 @@
1
1
  import { vec3 } from 'gl-matrix';
2
- import { Events } from '../../enums';
2
+ import { ChangeTypes, Events } from '../../enums';
3
3
  import { getEnabledElement } from '@cornerstonejs/core';
4
4
  import { AnnotationTool } from '../base';
5
5
  import throttle from '../../utilities/throttle';
@@ -237,6 +237,9 @@ class CobbAngleTool extends AnnotationTool {
237
237
  const enabledElement = getEnabledElement(element);
238
238
  const { renderingEngine } = enabledElement;
239
239
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
240
+ if (annotation.invalidated) {
241
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
242
+ }
240
243
  };
241
244
  this.cancel = (element) => {
242
245
  if (!this.isDrawing) {
@@ -672,7 +675,7 @@ class CobbAngleTool extends AnnotationTool {
672
675
  };
673
676
  }
674
677
  annotation.invalidated = false;
675
- triggerAnnotationModified(annotation, element);
678
+ triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
676
679
  return cachedStats;
677
680
  }
678
681
  }
@@ -8,7 +8,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
8
8
  import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
9
9
  import { drawCircle as drawCircleSvg, drawEllipseByCoordinates as drawEllipseSvg, drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, } from '../../drawingSvg';
10
10
  import { state } from '../../store/state';
11
- import { Events } from '../../enums';
11
+ import { ChangeTypes, Events } from '../../enums';
12
12
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
13
13
  import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
14
14
  import getWorldWidthAndHeightFromTwoPoints from '../../utilities/planar/getWorldWidthAndHeightFromTwoPoints';
@@ -240,6 +240,7 @@ class EllipticalROITool extends AnnotationTool {
240
240
  annotation.invalidated = true;
241
241
  this.editData.hasMoved = true;
242
242
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
243
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
243
244
  };
244
245
  this._dragModifyCallback = (evt) => {
245
246
  this.isDrawing = true;
@@ -276,6 +277,9 @@ class EllipticalROITool extends AnnotationTool {
276
277
  const enabledElement = getEnabledElement(element);
277
278
  const { renderingEngine } = enabledElement;
278
279
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
280
+ if (annotation.invalidated) {
281
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
282
+ }
279
283
  };
280
284
  this._dragHandle = (evt) => {
281
285
  const eventDetail = evt.detail;
@@ -618,7 +622,7 @@ class EllipticalROITool extends AnnotationTool {
618
622
  };
619
623
  }
620
624
  annotation.invalidated = false;
621
- triggerAnnotationModified(annotation, element);
625
+ triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
622
626
  return cachedStats;
623
627
  };
624
628
  this._isInsideVolume = (index1, index2, dimensions) => {
@@ -1,4 +1,4 @@
1
- import { Events } from '../../enums';
1
+ import { Events, ChangeTypes } from '../../enums';
2
2
  import { getEnabledElement, utilities as csUtils, utilities, getEnabledElementByViewportId, } from '@cornerstonejs/core';
3
3
  import { getCalibratedLengthUnitsAndScale } from '../../utilities/getCalibratedUnits';
4
4
  import { AnnotationTool } from '../base';
@@ -187,6 +187,9 @@ class LengthTool extends AnnotationTool {
187
187
  }
188
188
  this.editData.hasMoved = true;
189
189
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
190
+ if (annotation.invalidated) {
191
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
192
+ }
190
193
  };
191
194
  this.cancel = (element) => {
192
195
  if (this.isDrawing) {
@@ -448,7 +451,7 @@ class LengthTool extends AnnotationTool {
448
451
  };
449
452
  }
450
453
  annotation.invalidated = false;
451
- triggerAnnotationModified(annotation, element);
454
+ triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
452
455
  return cachedStats;
453
456
  }
454
457
  _isInsideVolume(index1, index2, dimensions) {
@@ -365,8 +365,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool {
365
365
  }
366
366
  if (!this.commonData?.movingTextBox) {
367
367
  const { data } = annotation;
368
- if (!data.cachedStats[targetId] ||
369
- data.cachedStats[targetId].areaUnit == null) {
368
+ if (!data.cachedStats[targetId]?.unit) {
370
369
  data.cachedStats[targetId] = {
371
370
  Modality: null,
372
371
  area: null,
@@ -374,6 +373,7 @@ class PlanarFreehandROITool extends ContourSegmentationBaseTool {
374
373
  mean: null,
375
374
  stdDev: null,
376
375
  areaUnit: null,
376
+ unit: null,
377
377
  };
378
378
  this._calculateCachedStats(annotation, viewport, renderingEngine, enabledElement);
379
379
  }
@@ -8,7 +8,7 @@ import { isAnnotationVisible } from '../../stateManagement/annotation/annotation
8
8
  import { triggerAnnotationCompleted, triggerAnnotationModified, } from '../../stateManagement/annotation/helpers/state';
9
9
  import { drawHandles as drawHandlesSvg, drawLinkedTextBox as drawLinkedTextBoxSvg, drawRectByCoordinates as drawRectSvg, } from '../../drawingSvg';
10
10
  import { state } from '../../store/state';
11
- import { Events } from '../../enums';
11
+ import { ChangeTypes, Events } from '../../enums';
12
12
  import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
13
13
  import * as rectangle from '../../utilities/math/rectangle';
14
14
  import { getTextBoxCoordsCanvas } from '../../utilities/drawing';
@@ -242,6 +242,9 @@ class RectangleROITool extends AnnotationTool {
242
242
  this.editData.hasMoved = true;
243
243
  const enabledElement = getEnabledElement(element);
244
244
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
245
+ if (annotation.invalidated) {
246
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
247
+ }
245
248
  };
246
249
  this.cancel = (element) => {
247
250
  if (this.isDrawing) {
@@ -504,7 +507,7 @@ class RectangleROITool extends AnnotationTool {
504
507
  }
505
508
  }
506
509
  annotation.invalidated = false;
507
- triggerAnnotationModified(annotation, element);
510
+ triggerAnnotationModified(annotation, element, ChangeTypes.StatsUpdated);
508
511
  return cachedStats;
509
512
  };
510
513
  this._isInsideVolume = (index1, index2, dimensions) => {
@@ -6,11 +6,12 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
6
6
  constructor(toolProps = {}, defaultToolProps = {
7
7
  supportedInteractionTypes: ['Mouse', 'Touch'],
8
8
  configuration: {
9
+ isPartialVolume: false,
9
10
  positiveSeedVariance: 0.4,
10
11
  negativeSeedVariance: 0.9,
11
12
  subVolumePaddingPercentage: 0.1,
12
13
  islandRemoval: {
13
- enabled: true,
14
+ enabled: false,
14
15
  },
15
16
  },
16
17
  }) {
@@ -41,7 +42,7 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
41
42
  };
42
43
  }
43
44
  async getGrowCutLabelmap(growCutData) {
44
- const { segmentation: { referencedVolumeId }, renderingEngineId, viewportId, worldPoint, options, } = growCutData;
45
+ const { segmentation: { referencedVolumeId, labelmapVolumeId }, renderingEngineId, viewportId, worldPoint, options, } = growCutData;
45
46
  const renderingEngine = getRenderingEngine(renderingEngineId);
46
47
  const viewport = renderingEngine.getViewport(viewportId);
47
48
  const { subVolumePaddingPercentage } = this.configuration;
@@ -49,7 +50,11 @@ class RegionSegmentPlusTool extends GrowCutBaseTool {
49
50
  ...options,
50
51
  subVolumePaddingPercentage,
51
52
  };
52
- return growCut.runOneClickGrowCut(referencedVolumeId, worldPoint, viewport, mergedOptions);
53
+ return growCut.runOneClickGrowCut({
54
+ referencedVolumeId,
55
+ worldPosition: worldPoint,
56
+ options: mergedOptions,
57
+ });
53
58
  }
54
59
  }
55
60
  export default RegionSegmentPlusTool;
@@ -11,6 +11,7 @@ class RegionSegmentTool extends GrowCutBaseTool {
11
11
  constructor(toolProps = {}, defaultToolProps = {
12
12
  supportedInteractionTypes: ['Mouse', 'Touch'],
13
13
  configuration: {
14
+ isPartialVolume: true,
14
15
  positiveSeedVariance: 0.5,
15
16
  negativeSeedVariance: 0.9,
16
17
  },
@@ -14,6 +14,7 @@ class WholeBodySegmentTool extends GrowCutBaseTool {
14
14
  constructor(toolProps = {}, defaultToolProps = {
15
15
  supportedInteractionTypes: ['Mouse', 'Touch'],
16
16
  configuration: {
17
+ isPartialVolume: true,
17
18
  positivePixelRange: POSITIVE_PIXEL_RANGE,
18
19
  negativePixelRange: NEGATIVE_PIXEL_RANGE,
19
20
  islandRemoval: {
@@ -1,12 +1,12 @@
1
1
  import { getEnabledElement, utilities } from '@cornerstonejs/core';
2
2
  import { resetElementCursor, hideElementCursor, } from '../../../cursors/elementCursor';
3
- import { Events } from '../../../enums';
3
+ import { ChangeTypes, Events } from '../../../enums';
4
4
  import { state } from '../../../store/state';
5
5
  import { vec3 } from 'gl-matrix';
6
6
  import { shouldSmooth, getInterpolatedPoints, } from '../../../utilities/planarFreehandROITool/smoothPoints';
7
7
  import getMouseModifierKey from '../../../eventDispatchers/shared/getMouseModifier';
8
8
  import triggerAnnotationRenderForViewportIds from '../../../utilities/triggerAnnotationRenderForViewportIds';
9
- import { triggerContourAnnotationCompleted } from '../../../stateManagement/annotation/helpers/state';
9
+ import { triggerAnnotationModified, triggerContourAnnotationCompleted, } from '../../../stateManagement/annotation/helpers/state';
10
10
  import findOpenUShapedContourVectorToPeak from './findOpenUShapedContourVectorToPeak';
11
11
  import { polyline } from '../../../utilities/math';
12
12
  import { removeAnnotation } from '../../../stateManagement/annotation/annotationState';
@@ -97,8 +97,12 @@ function mouseDragDrawCallback(evt) {
97
97
  const numPointsAdded = addCanvasPointsToArray(element, canvasPoints, canvasPos, this.commonData);
98
98
  this.drawData.polylineIndex = polylineIndex + numPointsAdded;
99
99
  }
100
+ annotation.invalidated = true;
100
101
  }
101
102
  triggerAnnotationRenderForViewportIds(viewportIdsToRender);
103
+ if (annotation.invalidated) {
104
+ triggerAnnotationModified(annotation, element, ChangeTypes.HandlesUpdated);
105
+ }
102
106
  }
103
107
  function mouseUpDrawCallback(evt) {
104
108
  const { allowOpenContours } = this.configuration;
@@ -41,22 +41,9 @@ declare abstract class AnnotationTool extends AnnotationDisplayTool {
41
41
  annotationUID: string;
42
42
  data: {
43
43
  [key: string]: unknown;
44
- handles?: {
45
- points?: Types.Point3[];
46
- activeHandleIndex?: number | null;
47
- textBox?: {
48
- hasMoved?: boolean;
49
- worldPosition?: Types.Point3;
50
- worldBoundingBox?: {
51
- topLeft: Types.Point3;
52
- topRight: Types.Point3;
53
- bottomLeft: Types.Point3;
54
- bottomRight: Types.Point3;
55
- };
56
- };
57
- [key: string]: unknown;
58
- };
44
+ handles?: import("../../types/AnnotationTypes").Handles;
59
45
  cachedStats?: Record<string, unknown>;
46
+ label?: string;
60
47
  };
61
48
  deleting: boolean;
62
49
  };
@@ -33,6 +33,7 @@ declare class GrowCutBaseTool extends BaseTool {
33
33
  refresh(): void;
34
34
  protected getGrowCutLabelmap(_growCutData: GrowCutToolData): Promise<Types.IImageVolume>;
35
35
  protected runGrowCut(): Promise<void>;
36
+ protected applyPartialGrowCutLabelmap(segmentationId: string, segmentIndex: number, targetLabelmap: Types.IImageVolume, sourceLabelmap: Types.IImageVolume): void;
36
37
  protected applyGrowCutLabelmap(segmentationId: string, segmentIndex: number, targetLabelmap: Types.IImageVolume, sourceLabelmap: Types.IImageVolume): void;
37
38
  private _runLastCommand;
38
39
  protected getLabelmapSegmentationData(viewport: Types.IViewport): Promise<{
@@ -41,6 +42,7 @@ declare class GrowCutBaseTool extends BaseTool {
41
42
  labelmapVolumeId: string;
42
43
  referencedVolumeId: string;
43
44
  }>;
45
+ private _createFakeVolume;
44
46
  protected _isOrthogonalView(viewport: Types.IViewport, referencedVolumeId: string): boolean;
45
47
  protected getRemoveIslandData(_growCutData: GrowCutToolData): RemoveIslandData;
46
48
  private _removeIslands;
@@ -1,10 +1,12 @@
1
- import { getEnabledElement, utilities as csUtils, cache, getRenderingEngine, StackViewport, } from '@cornerstonejs/core';
1
+ import { getEnabledElement, utilities as csUtils, cache, getRenderingEngine, volumeLoader, imageLoader, ImageVolume, } from '@cornerstonejs/core';
2
2
  import { BaseTool } from '../base';
3
3
  import { SegmentationRepresentations } from '../../enums';
4
4
  import { segmentIndex as segmentIndexController, state as segmentationState, activeSegmentation, } from '../../stateManagement/segmentation';
5
5
  import { triggerSegmentationDataModified } from '../../stateManagement/segmentation/triggerSegmentationEvents';
6
6
  import { getSVGStyleForSegment } from '../../utilities/segmentation/getSVGStyleForSegment';
7
7
  import IslandRemoval from '../../utilities/segmentation/islandRemoval';
8
+ import { getOrCreateSegmentationVolume } from '../../utilities/segmentation';
9
+ import { getCurrentLabelmapImageIdForViewport } from '../../stateManagement/segmentation/getCurrentLabelmapImageIdForViewport';
8
10
  const { transformWorldToIndex, transformIndexToWorld } = csUtils;
9
11
  class GrowCutBaseTool extends BaseTool {
10
12
  static { this.lastGrowCutCommand = null; }
@@ -90,7 +92,11 @@ class GrowCutBaseTool extends BaseTool {
90
92
  },
91
93
  });
92
94
  const growcutLabelmap = await this.getGrowCutLabelmap(updatedGrowCutData);
93
- this.applyGrowCutLabelmap(segmentationId, segmentIndex, labelmap, growcutLabelmap);
95
+ const { isPartialVolume } = config;
96
+ const fn = isPartialVolume
97
+ ? this.applyPartialGrowCutLabelmap
98
+ : this.applyGrowCutLabelmap;
99
+ fn(segmentationId, segmentIndex, labelmap, growcutLabelmap);
94
100
  this._removeIslands(growCutData);
95
101
  };
96
102
  await growCutCommand();
@@ -99,7 +105,7 @@ class GrowCutBaseTool extends BaseTool {
99
105
  }
100
106
  this.growCutData = null;
101
107
  }
102
- applyGrowCutLabelmap(segmentationId, segmentIndex, targetLabelmap, sourceLabelmap) {
108
+ applyPartialGrowCutLabelmap(segmentationId, segmentIndex, targetLabelmap, sourceLabelmap) {
103
109
  const srcLabelmapData = sourceLabelmap.voxelManager.getCompleteScalarDataArray();
104
110
  const tgtVoxelManager = targetLabelmap.voxelManager;
105
111
  const [srcColumns, srcRows, srcNumSlices] = sourceLabelmap.dimensions;
@@ -124,6 +130,16 @@ class GrowCutBaseTool extends BaseTool {
124
130
  }
125
131
  triggerSegmentationDataModified(segmentationId);
126
132
  }
133
+ applyGrowCutLabelmap(segmentationId, segmentIndex, targetLabelmap, sourceLabelmap) {
134
+ const tgtVoxelManager = targetLabelmap.voxelManager;
135
+ const srcVoxelManager = sourceLabelmap.voxelManager;
136
+ srcVoxelManager.forEach(({ value, index }) => {
137
+ if (value === segmentIndex) {
138
+ tgtVoxelManager.setAtIndex(index, value);
139
+ }
140
+ });
141
+ triggerSegmentationDataModified(segmentationId);
142
+ }
127
143
  _runLastCommand({ shrinkExpandAmount = 0 } = {}) {
128
144
  const cmd = GrowCutBaseTool.lastGrowCutCommand;
129
145
  if (cmd) {
@@ -139,9 +155,39 @@ class GrowCutBaseTool extends BaseTool {
139
155
  const segmentIndex = segmentIndexController.getActiveSegmentIndex(segmentationId);
140
156
  const { representationData } = segmentationState.getSegmentation(segmentationId);
141
157
  const labelmapData = representationData[SegmentationRepresentations.Labelmap];
142
- const { volumeId: labelmapVolumeId, referencedVolumeId } = labelmapData;
158
+ let { volumeId: labelmapVolumeId, referencedVolumeId } = labelmapData;
143
159
  if (!labelmapVolumeId) {
144
- throw new Error('Labelmap volume id not found - not implemented');
160
+ const referencedImageIds = viewport.getImageIds();
161
+ if (!csUtils.isValidVolume(referencedImageIds)) {
162
+ const currentImageId = viewport.getCurrentImageId();
163
+ const currentImage = cache.getImage(currentImageId);
164
+ const fakeImage = imageLoader.createAndCacheDerivedImage(currentImageId);
165
+ const fakeVolume = this._createFakeVolume([
166
+ currentImage.imageId,
167
+ fakeImage.imageId,
168
+ ]);
169
+ referencedVolumeId = fakeVolume.volumeId;
170
+ const currentLabelmapImageId = getCurrentLabelmapImageIdForViewport(viewport.id, segmentationId);
171
+ const fakeDerivedImage = imageLoader.createAndCacheDerivedImage(currentLabelmapImageId);
172
+ const fakeLabelmapVolume = this._createFakeVolume([
173
+ currentLabelmapImageId,
174
+ fakeDerivedImage.imageId,
175
+ ]);
176
+ labelmapVolumeId = fakeLabelmapVolume.volumeId;
177
+ }
178
+ else {
179
+ const segVolume = getOrCreateSegmentationVolume(segmentationId);
180
+ labelmapVolumeId = segVolume.volumeId;
181
+ }
182
+ }
183
+ if (!referencedVolumeId) {
184
+ const { imageIds: segImageIds } = labelmapData;
185
+ const referencedImageIds = segImageIds.map((imageId) => cache.getImage(imageId).referencedImageId);
186
+ const volumeId = cache.generateVolumeId(referencedImageIds);
187
+ const imageVolume = cache.getVolume(volumeId);
188
+ referencedVolumeId = imageVolume
189
+ ? imageVolume.volumeId
190
+ : (await volumeLoader.createAndCacheVolumeFromImagesSync(volumeId, referencedImageIds)).volumeId;
145
191
  }
146
192
  return {
147
193
  segmentationId,
@@ -150,6 +196,32 @@ class GrowCutBaseTool extends BaseTool {
150
196
  referencedVolumeId,
151
197
  };
152
198
  }
199
+ _createFakeVolume(imageIds) {
200
+ const volumeId = cache.generateVolumeId(imageIds);
201
+ const cachedVolume = cache.getVolume(volumeId);
202
+ if (cachedVolume) {
203
+ return cachedVolume;
204
+ }
205
+ const volumeProps = csUtils.generateVolumePropsFromImageIds(imageIds, volumeId);
206
+ const spacing = volumeProps.spacing;
207
+ if (spacing[2] === 0) {
208
+ spacing[2] = 1;
209
+ }
210
+ const derivedVolume = new ImageVolume({
211
+ volumeId,
212
+ dataType: volumeProps.dataType,
213
+ metadata: structuredClone(volumeProps.metadata),
214
+ dimensions: volumeProps.dimensions,
215
+ spacing: volumeProps.spacing,
216
+ origin: volumeProps.origin,
217
+ direction: volumeProps.direction,
218
+ referencedVolumeId: volumeProps.referencedVolumeId,
219
+ imageIds: volumeProps.imageIds,
220
+ referencedImageIds: volumeProps.referencedImageIds,
221
+ });
222
+ cache.putVolumeSync(volumeId, derivedVolume);
223
+ return derivedVolume;
224
+ }
153
225
  _isOrthogonalView(viewport, referencedVolumeId) {
154
226
  const volume = cache.getVolume(referencedVolumeId);
155
227
  const volumeImageData = volume.imageData;
@@ -16,23 +16,10 @@ type Annotation = {
16
16
  viewUp?: Types.Point3;
17
17
  };
18
18
  data: {
19
- handles?: {
20
- points?: Types.Point3[];
21
- activeHandleIndex?: number | null;
22
- textBox?: {
23
- hasMoved?: boolean;
24
- worldPosition?: Types.Point3;
25
- worldBoundingBox?: {
26
- topLeft: Types.Point3;
27
- topRight: Types.Point3;
28
- bottomLeft: Types.Point3;
29
- bottomRight: Types.Point3;
30
- };
31
- };
32
- [key: string]: unknown;
33
- };
19
+ handles?: Handles;
34
20
  [key: string]: unknown;
35
21
  cachedStats?: Record<string, unknown>;
22
+ label?: string;
36
23
  };
37
24
  };
38
25
  type Annotations = Array<Annotation>;
@@ -42,4 +29,19 @@ type GroupSpecificAnnotations = {
42
29
  type AnnotationState = {
43
30
  [key: string]: GroupSpecificAnnotations;
44
31
  };
45
- export type { Annotation, Annotations, GroupSpecificAnnotations, AnnotationState, };
32
+ type Handles = {
33
+ points?: Types.Point3[];
34
+ activeHandleIndex?: number | null;
35
+ textBox?: {
36
+ hasMoved?: boolean;
37
+ worldPosition?: Types.Point3;
38
+ worldBoundingBox?: {
39
+ topLeft: Types.Point3;
40
+ topRight: Types.Point3;
41
+ bottomLeft: Types.Point3;
42
+ bottomRight: Types.Point3;
43
+ };
44
+ };
45
+ [key: string]: unknown;
46
+ };
47
+ export type { Annotation, Annotations, GroupSpecificAnnotations, AnnotationState, Handles, };
@@ -11,6 +11,7 @@ export interface ROICachedStats {
11
11
  max: number;
12
12
  mean: number;
13
13
  stdDev: number;
14
+ unit?: number;
14
15
  };
15
16
  }
16
17
  export interface RectangleROIAnnotation extends Annotation {
@@ -37,4 +37,5 @@ import normalizeViewportPlane from './normalizeViewportPlane';
37
37
  import IslandRemoval from './segmentation/islandRemoval';
38
38
  import { getPixelValueUnits, getPixelValueUnitsImageId } from './getPixelValueUnits';
39
39
  import * as geometricSurfaceUtils from './geometricSurfaceUtils';
40
- export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, };
40
+ import setAnnotationLabel from './setAnnotationLabel';
41
+ export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, setAnnotationLabel, };
@@ -37,4 +37,5 @@ import normalizeViewportPlane from './normalizeViewportPlane';
37
37
  import IslandRemoval from './segmentation/islandRemoval';
38
38
  import { getPixelValueUnits, getPixelValueUnitsImageId, } from './getPixelValueUnits';
39
39
  import * as geometricSurfaceUtils from './geometricSurfaceUtils';
40
- export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, };
40
+ import setAnnotationLabel from './setAnnotationLabel';
41
+ export { math, planar, viewportFilters, drawing, debounce, dynamicVolume, throttle, orientation, isObject, touch, triggerEvent, calibrateImageSpacing, getCalibratedLengthUnitsAndScale, getCalibratedProbeUnitsAndValue, getCalibratedAspect, getPixelValueUnits, getPixelValueUnitsImageId, segmentation, contours, triggerAnnotationRenderForViewportIds, triggerAnnotationRenderForToolGroupIds, triggerAnnotationRender, getSphereBoundsInfo, getAnnotationNearPoint, getViewportForAnnotation, getAnnotationNearPointOnEnabledElement, viewport, cine, boundingBox, rectangleROITool, planarFreehandROITool, stackPrefetch, stackContextPrefetch, roundNumber, pointToString, polyDataUtils, voi, AnnotationMultiSlice, contourSegmentation, annotationHydration, getClosestImageIdForStackViewport, pointInSurroundingSphereCallback, normalizeViewportPlane, IslandRemoval, geometricSurfaceUtils, setAnnotationLabel, };
@@ -1,2 +1,2 @@
1
- declare const shader = "\nconst MAX_STRENGTH = 65535f;\n\n// Workgroup soze - X*Y*Z must be multiple of 32 for better performance\n// otherwise warps are sub allocated and some threads will not process anything\noverride workGroupSizeX = 1u;\noverride workGroupSizeY = 1u;\noverride workGroupSizeZ = 1u;\n\n// Compare the current voxel to neighbors using a 9x9x9 window\noverride windowSize = 9i;\n\nstruct Params {\n size: vec3u,\n iteration: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage> volumePixelData: array<f32>;\n@group(0) @binding(2) var<storage, read_write> labelmap: array<u32>;\n@group(0) @binding(3) var<storage, read_write> strengthData: array<f32>;\n@group(0) @binding(4) var<storage> prevLabelmap: array<u32>;\n@group(0) @binding(5) var<storage> prevStrengthData: array<f32>;\n@group(0) @binding(6) var<storage, read_write> updatedVoxelsCounter: array<atomic<u32>>;\n\nfn getPixelIndex(ijkPos: vec3u) -> u32 {\n let numPixelsPerSlice = params.size.x * params.size.y;\n return ijkPos.x + ijkPos.y * params.size.x + ijkPos.z * numPixelsPerSlice;\n}\n\n@compute @workgroup_size(workGroupSizeX, workGroupSizeY, workGroupSizeZ)\nfn main(\n @builtin(global_invocation_id) globalId: vec3u,\n) {\n // Make sure it will not get out of bounds for volume with sizes that\n // are not multiple of workGroupSize\n if (\n globalId.x >= params.size.x ||\n globalId.y >= params.size.y ||\n globalId.z >= params.size.z\n ) {\n return;\n }\n\n let currentCoord = vec3i(globalId);\n let currentPixelIndex = getPixelIndex(globalId);\n\n let numPixels = arrayLength(&volumePixelData);\n let currentPixelValue = volumePixelData[currentPixelIndex];\n\n if (params.iteration == 0) {\n // All non-zero initial labels are given maximum strength\n strengthData[currentPixelIndex] = select(MAX_STRENGTH, 0., labelmap[currentPixelIndex] == 0);\n return;\n }\n\n // It should at least copy the values from previous state\n var newLabel = prevLabelmap[currentPixelIndex];\n var newStrength = prevStrengthData[currentPixelIndex];\n\n let window = i32(ceil(f32(windowSize - 1) * .5));\n let minWindow = -1i * window;\n let maxWindow = 1i * window;\n\n for (var k = minWindow; k <= maxWindow; k++) {\n for (var j = minWindow; j <= maxWindow; j++) {\n for (var i = minWindow; i <= maxWindow; i++) {\n // Skip current voxel\n if (i == 0 && j == 0 && k == 0) {\n continue;\n }\n\n let neighborCoord = currentCoord + vec3i(i, j, k);\n\n // Boundary conditions. Do not grow outside of the volume\n if (\n neighborCoord.x < 0i || neighborCoord.x >= i32(params.size.x) ||\n neighborCoord.y < 0i || neighborCoord.y >= i32(params.size.y) ||\n neighborCoord.z < 0i || neighborCoord.z >= i32(params.size.z)\n ) {\n continue;\n }\n\n let neighborIndex = getPixelIndex(vec3u(neighborCoord));\n let neighborPixelValue = volumePixelData[neighborIndex];\n let prevNeighborStrength = prevStrengthData[neighborIndex];\n let strengthCost = abs(neighborPixelValue - currentPixelValue);\n let takeoverStrength = prevNeighborStrength - strengthCost;\n\n if (takeoverStrength > newStrength) {\n newLabel = prevLabelmap[neighborIndex];\n newStrength = takeoverStrength;\n }\n }\n }\n }\n\n if (labelmap[currentPixelIndex] != newLabel) {\n atomicAdd(&updatedVoxelsCounter[params.iteration], 1u);\n }\n\n labelmap[currentPixelIndex] = newLabel;\n strengthData[currentPixelIndex] = newStrength;\n}\n";
1
+ declare const shader = "\nconst MAX_STRENGTH = 65535f;\n\n// Workgroup size - X*Y*Z must be multiple of 32 for better performance\noverride workGroupSizeX = 1u;\noverride workGroupSizeY = 1u;\noverride workGroupSizeZ = 1u;\n\n// Compare the current voxel to neighbors using a 9x9x9 window\noverride windowSize = 9i;\n\nstruct Params {\n size: vec3u,\n iteration: u32,\n}\n\n// New structure to track bounds of modified voxels\nstruct Bounds {\n minX: atomic<i32>,\n minY: atomic<i32>,\n minZ: atomic<i32>,\n maxX: atomic<i32>,\n maxY: atomic<i32>,\n maxZ: atomic<i32>,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage> volumePixelData: array<f32>;\n@group(0) @binding(2) var<storage, read_write> labelmap: array<u32>;\n@group(0) @binding(3) var<storage, read_write> strengthData: array<f32>;\n@group(0) @binding(4) var<storage> prevLabelmap: array<u32>;\n@group(0) @binding(5) var<storage> prevStrengthData: array<f32>;\n@group(0) @binding(6) var<storage, read_write> updatedVoxelsCounter: array<atomic<u32>>;\n@group(0) @binding(7) var<storage, read_write> modifiedBounds: Bounds;\n\nfn getPixelIndex(ijkPos: vec3u) -> u32 {\n let numPixelsPerSlice = params.size.x * params.size.y;\n return ijkPos.x + ijkPos.y * params.size.x + ijkPos.z * numPixelsPerSlice;\n}\n\nfn updateBounds(position: vec3i) {\n // Atomically update min bounds (use min operation)\n let oldMinX = atomicMin(&modifiedBounds.minX, position.x);\n let oldMinY = atomicMin(&modifiedBounds.minY, position.y);\n let oldMinZ = atomicMin(&modifiedBounds.minZ, position.z);\n\n // Atomically update max bounds (use max operation)\n let oldMaxX = atomicMax(&modifiedBounds.maxX, position.x);\n let oldMaxY = atomicMax(&modifiedBounds.maxY, position.y);\n let oldMaxZ = atomicMax(&modifiedBounds.maxZ, position.z);\n}\n\n@compute @workgroup_size(workGroupSizeX, workGroupSizeY, workGroupSizeZ)\nfn main(\n @builtin(global_invocation_id) globalId: vec3u,\n) {\n // Make sure it will not get out of bounds for volume with sizes that\n // are not multiple of workGroupSize\n if (\n globalId.x >= params.size.x ||\n globalId.y >= params.size.y ||\n globalId.z >= params.size.z\n ) {\n return;\n }\n\n // Initialize bounds for the first iteration\n if (params.iteration == 0 && globalId.x == 0 && globalId.y == 0 && globalId.z == 0) {\n // Initialize to opposite extremes to ensure any update will improve the bounds\n atomicStore(&modifiedBounds.minX, i32(params.size.x));\n atomicStore(&modifiedBounds.minY, i32(params.size.y));\n atomicStore(&modifiedBounds.minZ, i32(params.size.z));\n atomicStore(&modifiedBounds.maxX, -1);\n atomicStore(&modifiedBounds.maxY, -1);\n atomicStore(&modifiedBounds.maxZ, -1);\n }\n\n let currentCoord = vec3i(globalId);\n let currentPixelIndex = getPixelIndex(globalId);\n\n let numPixels = arrayLength(&volumePixelData);\n let currentPixelValue = volumePixelData[currentPixelIndex];\n\n if (params.iteration == 0) {\n // All non-zero initial labels are given maximum strength\n strengthData[currentPixelIndex] = select(MAX_STRENGTH, 0., labelmap[currentPixelIndex] == 0);\n\n // Update bounds for non-zero initial labels\n if (labelmap[currentPixelIndex] != 0) {\n updateBounds(currentCoord);\n }\n return;\n }\n\n // It should at least copy the values from previous state\n var newLabel = prevLabelmap[currentPixelIndex];\n var newStrength = prevStrengthData[currentPixelIndex];\n\n let window = i32(ceil(f32(windowSize - 1) * .5));\n let minWindow = -1i * window;\n let maxWindow = 1i * window;\n\n for (var k = minWindow; k <= maxWindow; k++) {\n for (var j = minWindow; j <= maxWindow; j++) {\n for (var i = minWindow; i <= maxWindow; i++) {\n // Skip current voxel\n if (i == 0 && j == 0 && k == 0) {\n continue;\n }\n\n let neighborCoord = currentCoord + vec3i(i, j, k);\n\n // Boundary conditions. Do not grow outside of the volume\n if (\n neighborCoord.x < 0i || neighborCoord.x >= i32(params.size.x) ||\n neighborCoord.y < 0i || neighborCoord.y >= i32(params.size.y) ||\n neighborCoord.z < 0i || neighborCoord.z >= i32(params.size.z)\n ) {\n continue;\n }\n\n let neighborIndex = getPixelIndex(vec3u(neighborCoord));\n let neighborPixelValue = volumePixelData[neighborIndex];\n let prevNeighborStrength = prevStrengthData[neighborIndex];\n let strengthCost = abs(neighborPixelValue - currentPixelValue);\n let takeoverStrength = prevNeighborStrength - strengthCost;\n\n if (takeoverStrength > newStrength) {\n newLabel = prevLabelmap[neighborIndex];\n newStrength = takeoverStrength;\n }\n }\n }\n }\n\n if (labelmap[currentPixelIndex] != newLabel) {\n atomicAdd(&updatedVoxelsCounter[params.iteration], 1u);\n\n // Update bounds for modified voxels\n updateBounds(currentCoord);\n }\n\n labelmap[currentPixelIndex] = newLabel;\n strengthData[currentPixelIndex] = newStrength;\n}\n";
2
2
  export default shader;
@@ -1,8 +1,7 @@
1
1
  const shader = `
2
2
  const MAX_STRENGTH = 65535f;
3
3
 
4
- // Workgroup soze - X*Y*Z must be multiple of 32 for better performance
5
- // otherwise warps are sub allocated and some threads will not process anything
4
+ // Workgroup size - X*Y*Z must be multiple of 32 for better performance
6
5
  override workGroupSizeX = 1u;
7
6
  override workGroupSizeY = 1u;
8
7
  override workGroupSizeZ = 1u;
@@ -15,6 +14,16 @@ struct Params {
15
14
  iteration: u32,
16
15
  }
17
16
 
17
+ // New structure to track bounds of modified voxels
18
+ struct Bounds {
19
+ minX: atomic<i32>,
20
+ minY: atomic<i32>,
21
+ minZ: atomic<i32>,
22
+ maxX: atomic<i32>,
23
+ maxY: atomic<i32>,
24
+ maxZ: atomic<i32>,
25
+ }
26
+
18
27
  @group(0) @binding(0) var<uniform> params: Params;
19
28
  @group(0) @binding(1) var<storage> volumePixelData: array<f32>;
20
29
  @group(0) @binding(2) var<storage, read_write> labelmap: array<u32>;
@@ -22,12 +31,25 @@ struct Params {
22
31
  @group(0) @binding(4) var<storage> prevLabelmap: array<u32>;
23
32
  @group(0) @binding(5) var<storage> prevStrengthData: array<f32>;
24
33
  @group(0) @binding(6) var<storage, read_write> updatedVoxelsCounter: array<atomic<u32>>;
34
+ @group(0) @binding(7) var<storage, read_write> modifiedBounds: Bounds;
25
35
 
26
36
  fn getPixelIndex(ijkPos: vec3u) -> u32 {
27
37
  let numPixelsPerSlice = params.size.x * params.size.y;
28
38
  return ijkPos.x + ijkPos.y * params.size.x + ijkPos.z * numPixelsPerSlice;
29
39
  }
30
40
 
41
+ fn updateBounds(position: vec3i) {
42
+ // Atomically update min bounds (use min operation)
43
+ let oldMinX = atomicMin(&modifiedBounds.minX, position.x);
44
+ let oldMinY = atomicMin(&modifiedBounds.minY, position.y);
45
+ let oldMinZ = atomicMin(&modifiedBounds.minZ, position.z);
46
+
47
+ // Atomically update max bounds (use max operation)
48
+ let oldMaxX = atomicMax(&modifiedBounds.maxX, position.x);
49
+ let oldMaxY = atomicMax(&modifiedBounds.maxY, position.y);
50
+ let oldMaxZ = atomicMax(&modifiedBounds.maxZ, position.z);
51
+ }
52
+
31
53
  @compute @workgroup_size(workGroupSizeX, workGroupSizeY, workGroupSizeZ)
32
54
  fn main(
33
55
  @builtin(global_invocation_id) globalId: vec3u,
@@ -42,6 +64,17 @@ fn main(
42
64
  return;
43
65
  }
44
66
 
67
+ // Initialize bounds for the first iteration
68
+ if (params.iteration == 0 && globalId.x == 0 && globalId.y == 0 && globalId.z == 0) {
69
+ // Initialize to opposite extremes to ensure any update will improve the bounds
70
+ atomicStore(&modifiedBounds.minX, i32(params.size.x));
71
+ atomicStore(&modifiedBounds.minY, i32(params.size.y));
72
+ atomicStore(&modifiedBounds.minZ, i32(params.size.z));
73
+ atomicStore(&modifiedBounds.maxX, -1);
74
+ atomicStore(&modifiedBounds.maxY, -1);
75
+ atomicStore(&modifiedBounds.maxZ, -1);
76
+ }
77
+
45
78
  let currentCoord = vec3i(globalId);
46
79
  let currentPixelIndex = getPixelIndex(globalId);
47
80
 
@@ -51,6 +84,11 @@ fn main(
51
84
  if (params.iteration == 0) {
52
85
  // All non-zero initial labels are given maximum strength
53
86
  strengthData[currentPixelIndex] = select(MAX_STRENGTH, 0., labelmap[currentPixelIndex] == 0);
87
+
88
+ // Update bounds for non-zero initial labels
89
+ if (labelmap[currentPixelIndex] != 0) {
90
+ updateBounds(currentCoord);
91
+ }
54
92
  return;
55
93
  }
56
94
 
@@ -97,6 +135,9 @@ fn main(
97
135
 
98
136
  if (labelmap[currentPixelIndex] != newLabel) {
99
137
  atomicAdd(&updatedVoxelsCounter[params.iteration], 1u);
138
+
139
+ // Update bounds for modified voxels
140
+ updateBounds(currentCoord);
100
141
  }
101
142
 
102
143
  labelmap[currentPixelIndex] = newLabel;
@@ -23,7 +23,8 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
23
23
  labelmap.dimensions[2] !== numSlices) {
24
24
  throw new Error('Volume and labelmap must have the same size');
25
25
  }
26
- const numIterations = Math.floor(Math.sqrt(rows ** 2 + columns ** 2 + numSlices ** 2) / 2);
26
+ let numIterations = Math.floor(Math.sqrt(rows ** 2 + columns ** 2 + numSlices ** 2) / 2);
27
+ numIterations = Math.min(numIterations, 500);
27
28
  const labelmapData = labelmap.voxelManager.getCompleteScalarDataArray();
28
29
  let volumePixelData = volume.voxelManager.getCompleteScalarDataArray();
29
30
  if (!(volumePixelData instanceof Float32Array)) {
@@ -37,6 +38,7 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
37
38
  const device = await adapter.requestDevice({ requiredLimits });
38
39
  const BUFFER_SIZE = volumePixelData.byteLength;
39
40
  const UPDATED_VOXELS_COUNTER_BUFFER_SIZE = numIterations * Uint32Array.BYTES_PER_ELEMENT;
41
+ const BOUNDS_BUFFER_SIZE = 6 * Int32Array.BYTES_PER_ELEMENT;
40
42
  const shaderModule = device.createShaderModule({
41
43
  code: shaderCode,
42
44
  });
@@ -78,6 +80,21 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
78
80
  GPUBufferUsage.COPY_SRC |
79
81
  GPUBufferUsage.COPY_DST,
80
82
  });
83
+ const gpuBoundsBuffer = device.createBuffer({
84
+ size: BOUNDS_BUFFER_SIZE,
85
+ usage: GPUBufferUsage.STORAGE |
86
+ GPUBufferUsage.COPY_SRC |
87
+ GPUBufferUsage.COPY_DST,
88
+ });
89
+ const initialBounds = new Int32Array([
90
+ columns,
91
+ rows,
92
+ numSlices,
93
+ -1,
94
+ -1,
95
+ -1,
96
+ ]);
97
+ device.queue.writeBuffer(gpuBoundsBuffer, 0, initialBounds);
81
98
  const bindGroupLayout = device.createBindGroupLayout({
82
99
  entries: [
83
100
  {
@@ -129,6 +146,13 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
129
146
  type: 'storage',
130
147
  },
131
148
  },
149
+ {
150
+ binding: 7,
151
+ visibility: GPUShaderStage.COMPUTE,
152
+ buffer: {
153
+ type: 'storage',
154
+ },
155
+ },
132
156
  ],
133
157
  });
134
158
  const bindGroups = [0, 1].map((i) => {
@@ -181,6 +205,12 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
181
205
  buffer: gpuCounterBuffer,
182
206
  },
183
207
  },
208
+ {
209
+ binding: 7,
210
+ resource: {
211
+ buffer: gpuBoundsBuffer,
212
+ },
213
+ },
184
214
  ],
185
215
  });
186
216
  });
@@ -208,10 +238,6 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
208
238
  size: UPDATED_VOXELS_COUNTER_BUFFER_SIZE,
209
239
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
210
240
  });
211
- const labelmapStagingBufferTemp = device.createBuffer({
212
- size: BUFFER_SIZE,
213
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
214
- });
215
241
  const limitProcessingTime = maxProcessingTime
216
242
  ? performance.now() + maxProcessingTime
217
243
  : 0;
@@ -257,13 +283,34 @@ async function runGrowCut(referenceVolumeId, labelmapVolumeId, options = DEFAULT
257
283
  size: BUFFER_SIZE,
258
284
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
259
285
  });
286
+ const boundsStagingBuffer = device.createBuffer({
287
+ size: BOUNDS_BUFFER_SIZE,
288
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
289
+ });
260
290
  commandEncoder.copyBufferToBuffer(gpuLabelmapBuffers[outputLabelmapBufferIndex], 0, labelmapStagingBuffer, 0, BUFFER_SIZE);
291
+ commandEncoder.copyBufferToBuffer(gpuBoundsBuffer, 0, boundsStagingBuffer, 0, BOUNDS_BUFFER_SIZE);
261
292
  device.queue.submit([commandEncoder.finish()]);
262
293
  await labelmapStagingBuffer.mapAsync(GPUMapMode.READ, 0, BUFFER_SIZE);
263
294
  const labelmapResultBuffer = labelmapStagingBuffer.getMappedRange(0, BUFFER_SIZE);
264
295
  const labelmapResult = new Uint32Array(labelmapResultBuffer);
265
296
  labelmapData.set(labelmapResult);
266
297
  labelmapStagingBuffer.unmap();
298
+ await boundsStagingBuffer.mapAsync(GPUMapMode.READ, 0, BOUNDS_BUFFER_SIZE);
299
+ const boundsResultBuffer = boundsStagingBuffer.getMappedRange(0, BOUNDS_BUFFER_SIZE);
300
+ const boundsResult = new Int32Array(boundsResultBuffer.slice(0));
301
+ boundsStagingBuffer.unmap();
302
+ const minX = boundsResult[0];
303
+ const minY = boundsResult[1];
304
+ const minZ = boundsResult[2];
305
+ const maxX = boundsResult[3];
306
+ const maxY = boundsResult[4];
307
+ const maxZ = boundsResult[5];
267
308
  labelmap.voxelManager.setCompleteScalarDataArray(labelmapData);
309
+ labelmap.voxelManager.clearBounds();
310
+ labelmap.voxelManager.setBounds([
311
+ [minX, maxX],
312
+ [minY, maxY],
313
+ [minZ, maxZ],
314
+ ]);
268
315
  }
269
316
  export { runGrowCut as default, runGrowCut as run };
@@ -3,7 +3,13 @@ import type { GrowCutOptions } from './runGrowCut';
3
3
  type GrowCutOneClickOptions = GrowCutOptions & {
4
4
  subVolumePaddingPercentage?: number | [number, number, number];
5
5
  subVolumeMinPadding?: number | [number, number, number];
6
+ negativeSeedMargin?: number;
7
+ negativeSeedsCount?: number;
6
8
  };
7
- declare function runOneClickGrowCut(referencedVolumeId: string, worldPosition: Types.Point3, viewport: Types.IViewport, options?: GrowCutOneClickOptions): Promise<Types.IImageVolume>;
9
+ declare function runOneClickGrowCut({ referencedVolumeId, worldPosition, options, }: {
10
+ referencedVolumeId: string;
11
+ worldPosition: Types.Point3;
12
+ options?: GrowCutOneClickOptions;
13
+ }): Promise<Types.IImageVolume>;
8
14
  export { runOneClickGrowCut as default, runOneClickGrowCut };
9
15
  export type { GrowCutOneClickOptions };
@@ -1,4 +1,3 @@
1
- import { vec3 } from 'gl-matrix';
2
1
  import { utilities as csUtils, cache, volumeLoader } from '@cornerstonejs/core';
3
2
  import { run } from './runGrowCut';
4
3
  const { transformWorldToIndex, transformIndexToWorld } = csUtils;
@@ -6,63 +5,25 @@ const POSITIVE_SEED_VALUE = 254;
6
5
  const NEGATIVE_SEED_VALUE = 255;
7
6
  const POSITIVE_SEED_VARIANCE = 0.1;
8
7
  const NEGATIVE_SEED_VARIANCE = 0.8;
9
- const SUBVOLUME_PADDING_PERCENTAGE = 0.2;
10
- const SUBVOLUME_MIN_PADDING = 5;
11
- function _createSubVolume(referencedVolume, positiveRegionData, options) {
12
- const { dimensions } = referencedVolume;
13
- const positiveRegionSize = vec3.sub(vec3.create(), positiveRegionData.boundingBox.bottomRight, positiveRegionData.boundingBox.topLeft);
14
- let subVolumePaddingPercentage = options?.subVolumePaddingPercentage ?? SUBVOLUME_PADDING_PERCENTAGE;
15
- let subVolumeMinPadding = options?.subVolumeMinPadding ?? SUBVOLUME_MIN_PADDING;
16
- if (typeof subVolumePaddingPercentage === 'number') {
17
- subVolumePaddingPercentage = [
18
- subVolumePaddingPercentage,
19
- subVolumePaddingPercentage,
20
- subVolumePaddingPercentage,
21
- ];
22
- }
23
- if (typeof subVolumeMinPadding === 'number') {
24
- subVolumeMinPadding = [
25
- subVolumeMinPadding,
26
- subVolumeMinPadding,
27
- subVolumeMinPadding,
28
- ];
29
- }
30
- const padding = vec3.mul(vec3.create(), positiveRegionSize, subVolumePaddingPercentage);
31
- vec3.round(padding, padding);
32
- vec3.max(padding, padding, subVolumeMinPadding);
33
- const subVolumeSize = vec3.scaleAndAdd(vec3.create(), positiveRegionSize, padding, 2);
34
- const ijkTopLeft = vec3.sub(vec3.create(), positiveRegionData.boundingBox.topLeft, padding);
35
- const ijkBottomRight = vec3.add(vec3.create(), ijkTopLeft, subVolumeSize);
36
- vec3.max(ijkTopLeft, ijkTopLeft, [0, 0, 0]);
37
- vec3.min(ijkTopLeft, ijkTopLeft, dimensions);
38
- vec3.max(ijkBottomRight, ijkBottomRight, [0, 0, 0]);
39
- vec3.min(ijkBottomRight, ijkBottomRight, dimensions);
40
- const subVolumeBoundsIJK = {
41
- minX: ijkTopLeft[0],
42
- maxX: ijkBottomRight[0],
43
- minY: ijkTopLeft[1],
44
- maxY: ijkBottomRight[1],
45
- minZ: ijkTopLeft[2],
46
- maxZ: ijkBottomRight[2],
47
- };
48
- return csUtils.createSubVolume(referencedVolume.volumeId, subVolumeBoundsIJK, {
49
- targetBuffer: {
50
- type: 'Float32Array',
51
- },
52
- });
53
- }
54
- function _getPositiveRegionData(referencedVolume, worldPosition, options) {
8
+ function _generateSeeds(referencedVolume, labelmap, worldPosition, options) {
55
9
  const [width, height, numSlices] = referencedVolume.dimensions;
56
10
  const subVolPixelData = referencedVolume.voxelManager.getCompleteScalarDataArray();
57
11
  const numPixelsPerSlice = width * height;
58
12
  const ijkStartPosition = transformWorldToIndex(referencedVolume.imageData, worldPosition);
59
- const referencePixelValue = subVolPixelData[ijkStartPosition[2] * numPixelsPerSlice +
13
+ const startIndex = ijkStartPosition[2] * numPixelsPerSlice +
60
14
  ijkStartPosition[1] * width +
61
- ijkStartPosition[0]];
15
+ ijkStartPosition[0];
16
+ const referencePixelValue = subVolPixelData[startIndex];
62
17
  const positiveSeedVariance = options.positiveSeedVariance ?? POSITIVE_SEED_VARIANCE;
63
18
  const positiveSeedVarianceValue = Math.abs(referencePixelValue * positiveSeedVariance);
64
19
  const minPositivePixelValue = referencePixelValue - positiveSeedVarianceValue;
65
20
  const maxPositivePixelValue = referencePixelValue + positiveSeedVarianceValue;
21
+ const negativeSeedVariance = options.negativeSeedVariance ?? NEGATIVE_SEED_VARIANCE;
22
+ const negativeSeedVarianceValue = Math.abs(referencePixelValue * negativeSeedVariance);
23
+ const minNegativePixelValue = referencePixelValue - negativeSeedVarianceValue;
24
+ const maxNegativePixelValue = referencePixelValue + negativeSeedVarianceValue;
25
+ const positiveSeedValue = options.positiveSeedValue ?? POSITIVE_SEED_VALUE;
26
+ const negativeSeedValue = options.negativeSeedValue ?? NEGATIVE_SEED_VALUE;
66
27
  const neighborsCoordDelta = [
67
28
  [-1, 0, 0],
68
29
  [1, 0, 0],
@@ -71,32 +32,23 @@ function _getPositiveRegionData(referencedVolume, worldPosition, options) {
71
32
  [0, 0, -1],
72
33
  [0, 0, 1],
73
34
  ];
74
- let minX = Infinity;
75
- let minY = Infinity;
76
- let minZ = Infinity;
77
- let maxX = -Infinity;
78
- let maxY = -Infinity;
79
- let maxZ = -Infinity;
80
- const startVoxelIndex = ijkStartPosition[2] * numPixelsPerSlice +
81
- ijkStartPosition[1] * width +
82
- ijkStartPosition[0];
83
- const voxelIndexesSet = new Set([startVoxelIndex]);
84
- const worldVoxelSet = new Set([worldPosition]);
35
+ let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
36
+ const voxelIndexesSet = new Set([startIndex]);
37
+ labelmap.voxelManager.setAtIndex(startIndex, positiveSeedValue);
85
38
  const queue = [ijkStartPosition];
86
39
  while (queue.length) {
87
- const ijkVoxel = queue.shift();
88
- const [x, y, z] = ijkVoxel;
89
- minX = ijkVoxel[0] < minX ? ijkVoxel[0] : minX;
90
- minY = ijkVoxel[1] < minY ? ijkVoxel[1] : minY;
91
- minZ = ijkVoxel[2] < minZ ? ijkVoxel[2] : minZ;
92
- maxX = ijkVoxel[0] > maxX ? ijkVoxel[0] : maxX;
93
- maxY = ijkVoxel[1] > maxY ? ijkVoxel[1] : maxY;
94
- maxZ = ijkVoxel[2] > maxZ ? ijkVoxel[2] : maxZ;
95
- for (let i = 0, len = neighborsCoordDelta.length; i < len; i++) {
96
- const neighborCoordDelta = neighborsCoordDelta[i];
97
- const nx = x + neighborCoordDelta[0];
98
- const ny = y + neighborCoordDelta[1];
99
- const nz = z + neighborCoordDelta[2];
40
+ const [x, y, z] = queue.shift();
41
+ minX = x < minX ? x : minX;
42
+ minY = y < minY ? y : minY;
43
+ minZ = z < minZ ? z : minZ;
44
+ maxX = x > maxX ? x : maxX;
45
+ maxY = y > maxY ? y : maxY;
46
+ maxZ = z > maxZ ? z : maxZ;
47
+ for (let i = 0; i < neighborsCoordDelta.length; i++) {
48
+ const [dx, dy, dz] = neighborsCoordDelta[i];
49
+ const nx = x + dx;
50
+ const ny = y + dy;
51
+ const nz = z + dz;
100
52
  if (nx < 0 ||
101
53
  nx >= width ||
102
54
  ny < 0 ||
@@ -106,71 +58,51 @@ function _getPositiveRegionData(referencedVolume, worldPosition, options) {
106
58
  continue;
107
59
  }
108
60
  const neighborVoxelIndex = nz * numPixelsPerSlice + ny * width + nx;
109
- const neighborPixelValue = subVolPixelData[neighborVoxelIndex];
110
- if (voxelIndexesSet.has(neighborVoxelIndex) ||
111
- neighborPixelValue < minPositivePixelValue ||
112
- neighborPixelValue > maxPositivePixelValue) {
61
+ if (voxelIndexesSet.has(neighborVoxelIndex)) {
113
62
  continue;
114
63
  }
115
- const ijkVoxel = [nx, ny, nz];
116
- const worldVoxel = transformIndexToWorld(referencedVolume.imageData, ijkVoxel);
117
- voxelIndexesSet.add(neighborVoxelIndex);
118
- worldVoxelSet.add(worldVoxel);
119
- queue.push(ijkVoxel);
64
+ const neighborPixelValue = subVolPixelData[neighborVoxelIndex];
65
+ if (neighborPixelValue >= minPositivePixelValue &&
66
+ neighborPixelValue <= maxPositivePixelValue) {
67
+ labelmap.voxelManager.setAtIndex(neighborVoxelIndex, positiveSeedValue);
68
+ voxelIndexesSet.add(neighborVoxelIndex);
69
+ queue.push([nx, ny, nz]);
70
+ }
120
71
  }
121
72
  }
122
- return {
123
- worldVoxels: Array.from(worldVoxelSet),
124
- boundingBox: {
125
- topLeft: [minX, minY, minZ],
126
- bottomRight: [maxX, maxY, maxZ],
127
- },
128
- };
129
- }
130
- function _setPositiveSeedValues(labelmap, positiveRegionData, options) {
131
- const { dimensions } = labelmap;
132
- const [width, height] = dimensions;
133
- const numPixelsPerSlice = width * height;
134
- const positiveSeedValue = options.positiveSeedValue ?? POSITIVE_SEED_VALUE;
135
- const { worldVoxels } = positiveRegionData;
136
- for (let i = 0, len = worldVoxels.length; i < len; i++) {
137
- const worldVoxel = worldVoxels[i];
138
- const ijkVoxel = transformWorldToIndex(labelmap.imageData, worldVoxel);
139
- const voxelIndex = ijkVoxel[2] * numPixelsPerSlice + ijkVoxel[1] * width + ijkVoxel[0];
140
- labelmap.voxelManager.setAtIndex(voxelIndex, positiveSeedValue);
141
- }
142
- }
143
- function _setNegativeSeedValues(subVolume, labelmap, worldPosition, options) {
144
- const [width, height] = subVolume.dimensions;
145
- const subVolPixelData = subVolume.voxelManager.getCompleteScalarDataArray();
146
- const labelmapData = labelmap.voxelManager.getCompleteScalarDataArray();
147
- const ijkPosition = transformWorldToIndex(subVolume.imageData, worldPosition);
148
- const referencePixelValue = subVolPixelData[ijkPosition[2] * width * height + ijkPosition[1] * width + ijkPosition[0]];
149
- const negativeSeedVariance = options.negativeSeedVariance ?? NEGATIVE_SEED_VARIANCE;
150
- const negativeSeedValue = options.negativeSeedValue ?? NEGATIVE_SEED_VALUE;
151
- const negativeSeedVarianceValue = Math.abs(referencePixelValue * negativeSeedVariance);
152
- const minNegativePixelValue = referencePixelValue - negativeSeedVarianceValue;
153
- const maxNegativePixelValue = referencePixelValue + negativeSeedVarianceValue;
154
- for (let i = 0, len = subVolPixelData.length; i < len; i++) {
155
- const pixelValue = subVolPixelData[i];
156
- if (!labelmapData[i] &&
157
- (pixelValue < minNegativePixelValue || pixelValue > maxNegativePixelValue)) {
158
- labelmap.voxelManager.setAtIndex(i, negativeSeedValue);
73
+ const margin = options.negativeSeedMargin ?? 30;
74
+ const minXwMargin = Math.max(0, minX - margin);
75
+ const minYwMargin = Math.max(0, minY - margin);
76
+ const minZwMargin = Math.max(0, minZ - margin);
77
+ const maxXwMargin = Math.min(width - 1, maxX + margin);
78
+ const maxYwMargin = Math.min(height - 1, maxY + margin);
79
+ const maxZwMargin = Math.min(numSlices - 1, maxZ + margin);
80
+ const negativeSeedsCount = options.negativeSeedsCount ?? 70;
81
+ const negativeIndexesSet = new Set();
82
+ let attempts = 0;
83
+ while (negativeIndexesSet.size < negativeSeedsCount &&
84
+ attempts < negativeSeedsCount * 50) {
85
+ attempts++;
86
+ const rx = Math.floor(Math.random() * (maxXwMargin - minXwMargin + 1) + minXwMargin);
87
+ const ry = Math.floor(Math.random() * (maxYwMargin - minYwMargin + 1) + minYwMargin);
88
+ const rz = Math.floor(Math.random() * (maxZwMargin - minZwMargin + 1) + minZwMargin);
89
+ const randomIndex = rz * numPixelsPerSlice + ry * width + rx;
90
+ if (voxelIndexesSet.has(randomIndex)) {
91
+ continue;
92
+ }
93
+ const randomVal = subVolPixelData[randomIndex];
94
+ if (randomVal >= minNegativePixelValue &&
95
+ randomVal <= maxNegativePixelValue) {
96
+ labelmap.voxelManager.setAtIndex(randomIndex, negativeSeedValue);
97
+ negativeIndexesSet.add(randomIndex);
159
98
  }
160
99
  }
161
100
  }
162
- async function _createAndCacheSegmentation(subVolume, positiveRegionData, worldPosition, options) {
163
- const labelmap = volumeLoader.createAndCacheDerivedLabelmapVolume(subVolume.volumeId);
164
- _setPositiveSeedValues(labelmap, positiveRegionData, options);
165
- _setNegativeSeedValues(subVolume, labelmap, worldPosition, options);
166
- return labelmap;
167
- }
168
- async function runOneClickGrowCut(referencedVolumeId, worldPosition, viewport, options) {
101
+ async function runOneClickGrowCut({ referencedVolumeId, worldPosition, options, }) {
169
102
  const referencedVolume = cache.getVolume(referencedVolumeId);
170
- const positiveRegionData = _getPositiveRegionData(referencedVolume, worldPosition, options);
171
- const subVolume = _createSubVolume(referencedVolume, positiveRegionData, options);
172
- const labelmap = await _createAndCacheSegmentation(subVolume, positiveRegionData, worldPosition, options);
173
- await run(subVolume.volumeId, labelmap.volumeId);
103
+ const labelmap = volumeLoader.createAndCacheDerivedLabelmapVolume(referencedVolumeId);
104
+ _generateSeeds(referencedVolume, labelmap, worldPosition, options);
105
+ await run(referencedVolumeId, labelmap.volumeId);
174
106
  return labelmap;
175
107
  }
176
108
  export { runOneClickGrowCut as default, runOneClickGrowCut };
@@ -0,0 +1,2 @@
1
+ import type { Annotation } from '../types/AnnotationTypes';
2
+ export default function setAnnotationLabel(annotation: Annotation, element: HTMLDivElement, updatedLabel: string): void;
@@ -0,0 +1,6 @@
1
+ import { triggerAnnotationModified } from '../stateManagement/annotation/helpers/state';
2
+ import { ChangeTypes } from '../enums';
3
+ export default function setAnnotationLabel(annotation, element, updatedLabel) {
4
+ annotation.data.label = updatedLabel;
5
+ triggerAnnotationModified(annotation, element, ChangeTypes.LabelChange);
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/tools",
3
- "version": "3.0.5",
3
+ "version": "3.1.1",
4
4
  "description": "Cornerstone3D Tools",
5
5
  "types": "./dist/esm/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -103,7 +103,7 @@
103
103
  "canvas": "^2.11.2"
104
104
  },
105
105
  "peerDependencies": {
106
- "@cornerstonejs/core": "^3.0.5",
106
+ "@cornerstonejs/core": "^3.1.1",
107
107
  "@kitware/vtk.js": "32.9.0",
108
108
  "@types/d3-array": "^3.0.4",
109
109
  "@types/d3-interpolate": "^3.0.1",
@@ -122,5 +122,5 @@
122
122
  "type": "individual",
123
123
  "url": "https://ohif.org/donate"
124
124
  },
125
- "gitHead": "0b67f91a41eb069e0ef212d4be7acdcb4c68c542"
125
+ "gitHead": "91a215bf75b76f6cbd017a811b5dfcff60f7f060"
126
126
  }