@cornerstonejs/tools 3.6.0 → 3.7.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.
Files changed (40) hide show
  1. package/dist/esm/tools/annotation/AngleTool.d.ts +4 -0
  2. package/dist/esm/tools/annotation/AngleTool.js +1 -5
  3. package/dist/esm/tools/annotation/ArrowAnnotateTool.d.ts +4 -0
  4. package/dist/esm/tools/annotation/ArrowAnnotateTool.js +1 -5
  5. package/dist/esm/tools/annotation/BidirectionalTool.d.ts +4 -0
  6. package/dist/esm/tools/annotation/BidirectionalTool.js +13 -5
  7. package/dist/esm/tools/annotation/CircleROITool.d.ts +4 -0
  8. package/dist/esm/tools/annotation/CircleROITool.js +1 -5
  9. package/dist/esm/tools/annotation/EllipticalROITool.d.ts +4 -0
  10. package/dist/esm/tools/annotation/EllipticalROITool.js +1 -5
  11. package/dist/esm/tools/annotation/LengthTool.d.ts +4 -0
  12. package/dist/esm/tools/annotation/LengthTool.js +1 -5
  13. package/dist/esm/tools/annotation/ProbeTool.d.ts +4 -0
  14. package/dist/esm/tools/annotation/ProbeTool.js +1 -5
  15. package/dist/esm/tools/annotation/RectangleROITool.d.ts +4 -0
  16. package/dist/esm/tools/annotation/RectangleROITool.js +1 -5
  17. package/dist/esm/tools/annotation/SplineROITool.d.ts +4 -0
  18. package/dist/esm/tools/annotation/SplineROITool.js +1 -10
  19. package/dist/esm/tools/base/AnnotationTool.d.ts +15 -0
  20. package/dist/esm/tools/base/AnnotationTool.js +42 -1
  21. package/dist/esm/tools/segmentation/SegmentBidirectionalTool.d.ts +4 -0
  22. package/dist/esm/tools/segmentation/SegmentBidirectionalTool.js +12 -4
  23. package/dist/esm/tools/segmentation/strategies/compositions/ensureImageVolume.js +5 -8
  24. package/dist/esm/types/CalculatorTypes.d.ts +6 -0
  25. package/dist/esm/utilities/math/basic/BasicStatsCalculator.js +21 -9
  26. package/dist/esm/utilities/segmentation/VolumetricCalculator.js +11 -2
  27. package/dist/esm/utilities/segmentation/computeMetabolicStats.d.ts +8 -0
  28. package/dist/esm/utilities/segmentation/computeMetabolicStats.js +58 -0
  29. package/dist/esm/utilities/segmentation/createMergedLabelmapForIndex.js +10 -2
  30. package/dist/esm/utilities/segmentation/getOrCreateImageVolume.d.ts +3 -0
  31. package/dist/esm/utilities/segmentation/getOrCreateImageVolume.js +18 -0
  32. package/dist/esm/utilities/segmentation/getOrCreateSegmentationVolume.d.ts +2 -1
  33. package/dist/esm/utilities/segmentation/getOrCreateSegmentationVolume.js +1 -1
  34. package/dist/esm/utilities/segmentation/getReferenceVolumeForSegmentation.d.ts +1 -0
  35. package/dist/esm/utilities/segmentation/getReferenceVolumeForSegmentation.js +34 -0
  36. package/dist/esm/utilities/segmentation/getStatistics.js +8 -1
  37. package/dist/esm/utilities/segmentation/index.d.ts +3 -1
  38. package/dist/esm/utilities/segmentation/index.js +3 -1
  39. package/dist/esm/workers/computeWorker.js +148 -94
  40. package/package.json +4 -4
@@ -19,6 +19,10 @@ declare class AngleTool extends AnnotationTool {
19
19
  constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
20
20
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
21
21
  annotationUID?: string;
22
+ toolInstance?: AngleTool;
23
+ referencedImageId?: string;
24
+ viewplaneNormal?: Types.Point3;
25
+ viewUp?: Types.Point3;
22
26
  }) => AngleAnnotation;
23
27
  addNewAnnotation: (evt: EventTypes.InteractionEventType) => AngleAnnotation;
24
28
  isPointNearTool: (element: HTMLDivElement, annotation: AngleAnnotation, canvasCoords: Types.Point2, proximity: number) => boolean;
@@ -390,11 +390,7 @@ class AngleTool extends AnnotationTool {
390
390
  if (!enabledElement) {
391
391
  return;
392
392
  }
393
- const { viewport } = enabledElement;
394
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
395
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
396
- const instance = new this();
397
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
393
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(AngleTool, enabledElement, points, options);
398
394
  const annotation = {
399
395
  annotationUID: options?.annotationUID || csUtils.uuidv4(),
400
396
  data: {
@@ -18,6 +18,10 @@ declare class ArrowAnnotateTool extends AnnotationTool {
18
18
  constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
19
19
  static hydrate: (viewportId: string, points: Types.Point3[], text?: string, options?: {
20
20
  annotationUID?: string;
21
+ toolInstance?: ArrowAnnotateTool;
22
+ referencedImageId?: string;
23
+ viewplaneNormal?: Types.Point3;
24
+ viewUp?: Types.Point3;
21
25
  }) => ArrowAnnotation;
22
26
  addNewAnnotation: (evt: EventTypes.InteractionEventType) => ArrowAnnotation;
23
27
  isPointNearTool: (element: HTMLDivElement, annotation: ArrowAnnotation, canvasCoords: Types.Point2, proximity: number) => boolean;
@@ -392,11 +392,7 @@ class ArrowAnnotateTool extends AnnotationTool {
392
392
  if (!enabledElement) {
393
393
  return;
394
394
  }
395
- const { viewport } = enabledElement;
396
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
397
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
398
- const instance = new this();
399
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
395
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(ArrowAnnotateTool, enabledElement, points, options);
400
396
  const annotation = {
401
397
  annotationUID: options?.annotationUID || csUtils.uuidv4(),
402
398
  data: {
@@ -20,6 +20,10 @@ declare class BidirectionalTool extends AnnotationTool {
20
20
  addNewAnnotation(evt: EventTypes.InteractionEventType): BidirectionalAnnotation;
21
21
  static hydrate: (viewportId: string, axis: [[Types.Point3, Types.Point3], [Types.Point3, Types.Point3]], options?: {
22
22
  annotationUID?: string;
23
+ toolInstance?: BidirectionalTool;
24
+ referencedImageId?: string;
25
+ viewplaneNormal?: Types.Point3;
26
+ viewUp?: Types.Point3;
23
27
  }) => BidirectionalAnnotation;
24
28
  isPointNearTool: (element: HTMLDivElement, annotation: BidirectionalAnnotation, canvasCoords: Types.Point2, proximity: number) => boolean;
25
29
  toolSelectedCallback: (evt: EventTypes.InteractionEventType, annotation: BidirectionalAnnotation) => void;
@@ -714,22 +714,29 @@ class BidirectionalTool extends AnnotationTool {
714
714
  if (!enabledElement) {
715
715
  return;
716
716
  }
717
- const { viewport } = enabledElement;
718
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
719
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
720
- const instance = new this();
717
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(BidirectionalTool, enabledElement, [], options);
721
718
  const [majorAxis, minorAxis] = axis;
722
719
  const [major0, major1] = majorAxis;
723
720
  const [minor0, minor1] = minorAxis;
724
721
  const points = [major0, major1, minor0, minor1];
725
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
726
722
  const annotation = {
727
723
  annotationUID: options?.annotationUID || utilities.uuidv4(),
728
724
  data: {
729
725
  handles: {
730
726
  points,
731
727
  activeHandleIndex: null,
728
+ textBox: {
729
+ hasMoved: false,
730
+ worldPosition: [0, 0, 0],
731
+ worldBoundingBox: {
732
+ topLeft: [0, 0, 0],
733
+ topRight: [0, 0, 0],
734
+ bottomLeft: [0, 0, 0],
735
+ bottomRight: [0, 0, 0],
736
+ },
737
+ },
732
738
  },
739
+ cachedStats: {},
733
740
  },
734
741
  highlighted: false,
735
742
  autoGenerated: false,
@@ -746,6 +753,7 @@ class BidirectionalTool extends AnnotationTool {
746
753
  };
747
754
  addAnnotation(annotation, viewport.element);
748
755
  triggerAnnotationRenderForViewportIds([viewport.id]);
756
+ return annotation;
749
757
  }; }
750
758
  _calculateLength(pos1, pos2) {
751
759
  const dx = pos1[0] - pos2[0];
@@ -34,6 +34,10 @@ declare class CircleROITool extends AnnotationTool {
34
34
  _isInsideVolume: (index1: any, index2: any, dimensions: any) => boolean;
35
35
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
36
36
  annotationUID?: string;
37
+ toolInstance?: CircleROITool;
38
+ referencedImageId?: string;
39
+ viewplaneNormal?: Types.Point3;
40
+ viewUp?: Types.Point3;
37
41
  }) => CircleROIAnnotation;
38
42
  }
39
43
  export default CircleROITool;
@@ -570,11 +570,7 @@ class CircleROITool extends AnnotationTool {
570
570
  if (!enabledElement) {
571
571
  return;
572
572
  }
573
- const { viewport } = enabledElement;
574
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
575
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
576
- const instance = new this();
577
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
573
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(CircleROITool, enabledElement, points, options);
578
574
  const annotation = {
579
575
  annotationUID: options?.annotationUID || csUtils.uuidv4(),
580
576
  data: {
@@ -22,6 +22,10 @@ declare class EllipticalROITool extends AnnotationTool {
22
22
  constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
23
23
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
24
24
  annotationUID?: string;
25
+ toolInstance?: EllipticalROITool;
26
+ referencedImageId?: string;
27
+ viewplaneNormal?: Types.Point3;
28
+ viewUp?: Types.Point3;
25
29
  }) => EllipticalROIAnnotation;
26
30
  addNewAnnotation: (evt: EventTypes.InteractionEventType) => EllipticalROIAnnotation;
27
31
  isPointNearTool: (element: HTMLDivElement, annotation: EllipticalROIAnnotation, canvasCoords: Types.Point2, proximity: number) => boolean;
@@ -636,11 +636,7 @@ class EllipticalROITool extends AnnotationTool {
636
636
  if (!enabledElement) {
637
637
  return;
638
638
  }
639
- const { viewport } = enabledElement;
640
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
641
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
642
- const instance = new this();
643
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
639
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(EllipticalROITool, enabledElement, points, options);
644
640
  const annotation = {
645
641
  annotationUID: options?.annotationUID || csUtils.uuidv4(),
646
642
  data: {
@@ -18,6 +18,10 @@ declare class LengthTool extends AnnotationTool {
18
18
  constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
19
19
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
20
20
  annotationUID?: string;
21
+ toolInstance?: LengthTool;
22
+ referencedImageId?: string;
23
+ viewplaneNormal?: Types.Point3;
24
+ viewUp?: Types.Point3;
21
25
  }) => LengthAnnotation;
22
26
  addNewAnnotation: (evt: EventTypes.InteractionEventType) => LengthAnnotation;
23
27
  isPointNearTool: (element: HTMLDivElement, annotation: LengthAnnotation, canvasCoords: Types.Point2, proximity: number) => boolean;
@@ -361,11 +361,7 @@ class LengthTool extends AnnotationTool {
361
361
  if (!enabledElement) {
362
362
  return;
363
363
  }
364
- const { viewport } = enabledElement;
365
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
366
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
367
- const instance = new this();
368
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
364
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(LengthTool, enabledElement, points, options);
369
365
  const annotation = {
370
366
  annotationUID: options?.annotationUID || utilities.uuidv4(),
371
367
  data: {
@@ -19,6 +19,10 @@ declare class ProbeTool extends AnnotationTool {
19
19
  toolSelectedCallback(): void;
20
20
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
21
21
  annotationUID?: string;
22
+ toolInstance?: ProbeTool;
23
+ referencedImageId?: string;
24
+ viewplaneNormal?: Types.Point3;
25
+ viewUp?: Types.Point3;
22
26
  }) => ProbeAnnotation;
23
27
  addNewAnnotation: (evt: EventTypes.InteractionEventType) => ProbeAnnotation;
24
28
  getHandleNearImagePoint(element: HTMLDivElement, annotation: ProbeAnnotation, canvasCoords: Types.Point2, proximity: number): ToolHandle | undefined;
@@ -224,11 +224,7 @@ class ProbeTool extends AnnotationTool {
224
224
  if (!enabledElement) {
225
225
  return;
226
226
  }
227
- const { viewport } = enabledElement;
228
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
229
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
230
- const instance = new this();
231
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
227
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, viewUp, instance, viewport, } = this.hydrateBase(ProbeTool, enabledElement, points, options);
232
228
  const annotation = {
233
229
  annotationUID: options?.annotationUID || csUtils.uuidv4(),
234
230
  data: {
@@ -38,6 +38,10 @@ declare class RectangleROITool extends AnnotationTool {
38
38
  _isInsideVolume: (index1: any, index2: any, dimensions: any) => boolean;
39
39
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
40
40
  annotationUID?: string;
41
+ toolInstance?: RectangleROITool;
42
+ referencedImageId?: string;
43
+ viewplaneNormal?: Types.Point3;
44
+ viewUp?: Types.Point3;
41
45
  }) => RectangleROIAnnotation;
42
46
  }
43
47
  export default RectangleROITool;
@@ -521,11 +521,7 @@ class RectangleROITool extends AnnotationTool {
521
521
  if (!enabledElement) {
522
522
  return;
523
523
  }
524
- const { viewport } = enabledElement;
525
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
526
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
527
- const instance = new this();
528
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
524
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, viewport, } = this.hydrateBase(RectangleROITool, enabledElement, points, options);
529
525
  const annotation = {
530
526
  annotationUID: options?.annotationUID || csUtils.uuidv4(),
531
527
  data: {
@@ -68,6 +68,10 @@ declare class SplineROITool extends ContourSegmentationBaseTool {
68
68
  static hydrate: (viewportId: string, points: Types.Point3[], options?: {
69
69
  annotationUID?: string;
70
70
  splineType?: SplineTypesEnum;
71
+ toolInstance?: SplineROITool;
72
+ referencedImageId?: string;
73
+ viewplaneNormal?: Types.Point3;
74
+ viewUp?: Types.Point3;
71
75
  }) => SplineROIAnnotation;
72
76
  }
73
77
  export default SplineROITool;
@@ -726,19 +726,11 @@ class SplineROITool extends ContourSegmentationBaseTool {
726
726
  console.warn('Spline requires at least 3 control points');
727
727
  return;
728
728
  }
729
- const { viewport } = enabledElement;
730
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
731
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
732
- const instance = new this();
733
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
729
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, viewUp, instance, viewport, } = this.hydrateBase(SplineROITool, enabledElement, points, options);
734
730
  const splineType = options?.splineType || SplineTypesEnum.CatmullRom;
735
731
  const splineConfig = instance._getSplineConfig(splineType);
736
732
  const SplineClass = splineConfig.Class;
737
733
  const splineInstance = new SplineClass();
738
- const canvasPoints = points.map((point) => viewport.worldToCanvas(point));
739
- splineInstance.setControlPoints(canvasPoints);
740
- const splinePolylineCanvas = splineInstance.getPolylinePoints();
741
- const splinePolylineWorld = splinePolylineCanvas.map((point) => viewport.canvasToWorld(point));
742
734
  const annotation = {
743
735
  annotationUID: options?.annotationUID || utilities.uuidv4(),
744
736
  data: {
@@ -753,7 +745,6 @@ class SplineROITool extends ContourSegmentationBaseTool {
753
745
  },
754
746
  contour: {
755
747
  closed: true,
756
- polyline: splinePolylineWorld,
757
748
  },
758
749
  },
759
750
  highlighted: false,
@@ -1,3 +1,4 @@
1
+ import { StackViewport } from '@cornerstonejs/core';
1
2
  import type { Types } from '@cornerstonejs/core';
2
3
  import AnnotationDisplayTool from './AnnotationDisplayTool';
3
4
  import type { Annotation, Annotations, EventTypes, ToolHandle, InteractionTypes, ToolProps, PublicToolProps } from '../../types';
@@ -54,5 +55,19 @@ declare abstract class AnnotationTool extends AnnotationDisplayTool {
54
55
  restoreMemo: () => void;
55
56
  };
56
57
  protected createMemo(element: any, annotation: any, options?: any): void;
58
+ protected static hydrateBase<T extends AnnotationTool>(ToolClass: new () => T, enabledElement: Types.IEnabledElement, points: Types.Point3[], options?: {
59
+ annotationUID?: string;
60
+ toolInstance?: T;
61
+ referencedImageId?: string;
62
+ viewplaneNormal?: Types.Point3;
63
+ viewUp?: Types.Point3;
64
+ }): {
65
+ FrameOfReferenceUID: string;
66
+ referencedImageId: any;
67
+ viewPlaneNormal: Types.Point3;
68
+ viewUp: Types.Point3;
69
+ instance: T;
70
+ viewport: StackViewport | import("@cornerstonejs/core").VolumeViewport;
71
+ };
57
72
  }
58
73
  export default AnnotationTool;
@@ -1,4 +1,4 @@
1
- import { BaseVolumeViewport, cache, getEnabledElement, metaData, utilities as csUtils, } from '@cornerstonejs/core';
1
+ import { BaseVolumeViewport, cache, getEnabledElement, metaData, utilities as csUtils, StackViewport, } from '@cornerstonejs/core';
2
2
  import { vec2 } from 'gl-matrix';
3
3
  import AnnotationDisplayTool from './AnnotationDisplayTool';
4
4
  import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking';
@@ -266,6 +266,47 @@ class AnnotationTool extends AnnotationDisplayTool {
266
266
  createMemo(element, annotation, options) {
267
267
  this.memo ||= AnnotationTool.createAnnotationMemo(element, annotation, options);
268
268
  }
269
+ static hydrateBase(ToolClass, enabledElement, points, options = {}) {
270
+ if (!enabledElement) {
271
+ return null;
272
+ }
273
+ const { viewport } = enabledElement;
274
+ const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
275
+ const camera = viewport.getCamera();
276
+ const viewPlaneNormal = options.viewplaneNormal ?? camera.viewPlaneNormal;
277
+ const viewUp = options.viewUp ?? camera.viewUp;
278
+ const instance = options.toolInstance || new ToolClass();
279
+ let referencedImageId;
280
+ let finalViewPlaneNormal = viewPlaneNormal;
281
+ let finalViewUp = viewUp;
282
+ if (options.referencedImageId) {
283
+ referencedImageId = options.referencedImageId;
284
+ finalViewPlaneNormal = undefined;
285
+ finalViewUp = undefined;
286
+ }
287
+ else {
288
+ if (viewport instanceof StackViewport) {
289
+ const closestImageIndex = csUtils.getClosestStackImageIndexForPoint(points[0], viewport);
290
+ if (closestImageIndex) {
291
+ referencedImageId = viewport.getImageIds()[closestImageIndex];
292
+ }
293
+ }
294
+ else if (viewport instanceof BaseVolumeViewport) {
295
+ referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
296
+ }
297
+ else {
298
+ throw new Error('Unsupported viewport type');
299
+ }
300
+ }
301
+ return {
302
+ FrameOfReferenceUID,
303
+ referencedImageId,
304
+ viewPlaneNormal: finalViewPlaneNormal,
305
+ viewUp: finalViewUp,
306
+ instance,
307
+ viewport,
308
+ };
309
+ }
269
310
  }
270
311
  AnnotationTool.toolName = 'AnnotationTool';
271
312
  export default AnnotationTool;
@@ -21,6 +21,10 @@ declare class SegmentBidirectionalTool extends BidirectionalTool {
21
21
  segmentIndex?: number;
22
22
  segmentationId?: string;
23
23
  annotationUID?: string;
24
+ toolInstance?: SegmentBidirectionalTool;
25
+ referencedImageId?: string;
26
+ viewplaneNormal?: Types.Point3;
27
+ viewUp?: Types.Point3;
24
28
  }) => SegmentBidirectionalAnnotation;
25
29
  renderAnnotation: (enabledElement: Types.IEnabledElement, svgDrawingHelper: SVGDrawingHelper) => boolean;
26
30
  }
@@ -201,8 +201,6 @@ class SegmentBidirectionalTool extends BidirectionalTool {
201
201
  return;
202
202
  }
203
203
  const { viewport } = enabledElement;
204
- const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
205
- const { viewPlaneNormal, viewUp } = viewport.getCamera();
206
204
  const existingAnnotations = getAllAnnotations();
207
205
  const toolAnnotations = existingAnnotations.filter((annotation) => annotation.metadata.toolName === 'SegmentBidirectional');
208
206
  const existingAnnotation = toolAnnotations.find((annotation) => {
@@ -216,19 +214,29 @@ class SegmentBidirectionalTool extends BidirectionalTool {
216
214
  if (existingAnnotation) {
217
215
  removeAnnotation(existingAnnotation.annotationUID);
218
216
  }
219
- const instance = new this();
217
+ const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, } = this.hydrateBase(SegmentBidirectionalTool, enabledElement, axis[0], options);
220
218
  const [majorAxis, minorAxis] = axis;
221
219
  const [major0, major1] = majorAxis;
222
220
  const [minor0, minor1] = minorAxis;
223
221
  const points = [major0, major1, minor0, minor1];
224
- const referencedImageId = instance.getReferencedImageId(viewport, points[0], viewPlaneNormal, viewUp);
225
222
  const annotation = {
226
223
  annotationUID: options?.annotationUID || utilities.uuidv4(),
227
224
  data: {
228
225
  handles: {
229
226
  points,
230
227
  activeHandleIndex: null,
228
+ textBox: {
229
+ hasMoved: false,
230
+ worldPosition: [0, 0, 0],
231
+ worldBoundingBox: {
232
+ topLeft: [0, 0, 0],
233
+ topRight: [0, 0, 0],
234
+ bottomLeft: [0, 0, 0],
235
+ bottomRight: [0, 0, 0],
236
+ },
237
+ },
231
238
  },
239
+ cachedStats: {},
232
240
  },
233
241
  highlighted: false,
234
242
  autoGenerated: false,
@@ -1,6 +1,7 @@
1
- import { cache, utilities as csUtils, volumeLoader } from '@cornerstonejs/core';
1
+ import { cache, utilities as csUtils } from '@cornerstonejs/core';
2
2
  import StrategyCallbacks from '../../../../enums/StrategyCallbacks';
3
3
  import { getSegmentation } from '../../../../stateManagement/segmentation/getSegmentation';
4
+ import getOrCreateImageVolume from '../../../../utilities/segmentation/getOrCreateImageVolume';
4
5
  export default {
5
6
  [StrategyCallbacks.EnsureImageVolumeFor3DManipulation]: (data) => {
6
7
  const { operationData, viewport } = data;
@@ -21,14 +22,10 @@ export default {
21
22
  return image.referencedImageId;
22
23
  });
23
24
  }
24
- const volumeId = cache.generateVolumeId(referencedImageIds);
25
- let imageVolume = cache.getVolume(volumeId);
26
- if (imageVolume) {
27
- operationData.imageVoxelManager = imageVolume.voxelManager;
28
- operationData.imageData = imageVolume.imageData;
29
- return;
25
+ const imageVolume = getOrCreateImageVolume(referencedImageIds);
26
+ if (!imageVolume) {
27
+ throw new Error('Failed to create or get image volume');
30
28
  }
31
- imageVolume = volumeLoader.createAndCacheVolumeFromImagesSync(volumeId, referencedImageIds);
32
29
  operationData.imageVoxelManager = imageVolume.voxelManager;
33
30
  operationData.imageData = imageVolume.imageData;
34
31
  },
@@ -45,6 +45,12 @@ type NamedStatistics = {
45
45
  lesionGlycolysis?: Statistics & {
46
46
  name: 'lesionGlycolysis';
47
47
  };
48
+ maxLPS?: Statistics & {
49
+ name: 'maxLPS';
50
+ };
51
+ minLPS?: Statistics & {
52
+ name: 'minLPS';
53
+ };
48
54
  maxIJKs?: Array<{
49
55
  value: number;
50
56
  pointIJK: Types.Point3;
@@ -55,15 +55,15 @@ function basicStatsCallback(state, newValue, pointLPS = null, pointIJK = null) {
55
55
  if (value < state.min[idx]) {
56
56
  state.min[idx] = value;
57
57
  if (idx === 0) {
58
- state.minIJK = pointIJK;
59
- state.minLPS = pointLPS;
58
+ state.minIJK = pointIJK ? [...pointIJK] : null;
59
+ state.minLPS = pointLPS ? [...pointLPS] : null;
60
60
  }
61
61
  }
62
62
  if (value > state.max[idx]) {
63
63
  state.max[idx] = value;
64
64
  if (idx === 0) {
65
- state.maxIJK = pointIJK;
66
- state.maxLPS = pointLPS;
65
+ state.maxIJK = pointIJK ? [...pointIJK] : null;
66
+ state.maxLPS = pointLPS ? [...pointLPS] : null;
67
67
  }
68
68
  }
69
69
  });
@@ -105,16 +105,16 @@ function basicGetStatistics(state, unit) {
105
105
  label: 'Max Pixel',
106
106
  value: state.max.length === 1 ? state.max[0] : state.max,
107
107
  unit,
108
- pointIJK: state.maxIJK,
109
- pointLPS: state.maxLPS,
108
+ pointIJK: state.maxIJK ? [...state.maxIJK] : null,
109
+ pointLPS: state.maxLPS ? [...state.maxLPS] : null,
110
110
  },
111
111
  min: {
112
112
  name: 'min',
113
113
  label: 'Min Pixel',
114
114
  value: state.min.length === 1 ? state.min[0] : state.min,
115
115
  unit,
116
- pointIJK: state.minIJK,
117
- pointLPS: state.minLPS,
116
+ pointIJK: state.minIJK ? [...state.minIJK] : null,
117
+ pointLPS: state.minLPS ? [...state.minLPS] : null,
118
118
  },
119
119
  mean: {
120
120
  name: 'mean',
@@ -152,10 +152,22 @@ function basicGetStatistics(state, unit) {
152
152
  value: kurtosis.length === 1 ? kurtosis[0] : kurtosis,
153
153
  unit: null,
154
154
  },
155
+ maxLPS: {
156
+ name: 'maxLPS',
157
+ label: 'Max LPS',
158
+ value: state.maxLPS ? Array.from(state.maxLPS) : null,
159
+ unit: null,
160
+ },
161
+ minLPS: {
162
+ name: 'minLPS',
163
+ label: 'Min LPS',
164
+ value: state.minLPS ? Array.from(state.minLPS) : null,
165
+ unit: null,
166
+ },
155
167
  pointsInShape: state.pointsInShape,
156
168
  array: [],
157
169
  };
158
- named.array.push(named.max, named.mean, named.stdDev, named.median, named.skewness, named.kurtosis, named.count);
170
+ named.array.push(named.min, named.max, named.mean, named.stdDev, named.median, named.skewness, named.kurtosis, named.count, named.maxLPS, named.minLPS);
159
171
  const store = state.pointsInShape !== null;
160
172
  const freshState = createBasicStatsState(store);
161
173
  state.max = freshState.max;
@@ -14,13 +14,22 @@ function volumetricStatsCallback(state, data) {
14
14
  (length >= TEST_MAX_LOCATIONS && value < maxIJKs[0].value)) {
15
15
  return;
16
16
  }
17
+ const dataCopy = {
18
+ value: data.value,
19
+ pointLPS: data.pointLPS
20
+ ? [data.pointLPS[0], data.pointLPS[1], data.pointLPS[2]]
21
+ : undefined,
22
+ pointIJK: data.pointIJK
23
+ ? [data.pointIJK[0], data.pointIJK[1], data.pointIJK[2]]
24
+ : undefined,
25
+ };
17
26
  if (!length || value >= maxIJKs[length - 1].value) {
18
- maxIJKs.push(data);
27
+ maxIJKs.push(dataCopy);
19
28
  }
20
29
  else {
21
30
  for (let i = 0; i < length; i++) {
22
31
  if (value <= maxIJKs[i].value) {
23
- maxIJKs.splice(i, 0, data);
32
+ maxIJKs.splice(i, 0, dataCopy);
24
33
  break;
25
34
  }
26
35
  }
@@ -0,0 +1,8 @@
1
+ import type { NamedStatistics } from '../../types';
2
+ declare function computeMetabolicStats({ segmentationIds, segmentIndex, }: {
3
+ segmentationIds: string[];
4
+ segmentIndex: number;
5
+ }): Promise<NamedStatistics | {
6
+ [segmentIndex: number]: NamedStatistics;
7
+ }>;
8
+ export { computeMetabolicStats };
@@ -0,0 +1,58 @@
1
+ import { utilities, getWebWorkerManager } from '@cornerstonejs/core';
2
+ import { triggerWorkerProgress } from './utilsForWorker';
3
+ import { WorkerTypes } from '../../enums';
4
+ import { registerComputeWorker } from '../registerComputeWorker';
5
+ import createMergedLabelmapForIndex from './createMergedLabelmapForIndex';
6
+ import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation';
7
+ import getOrCreateSegmentationVolume from './getOrCreateSegmentationVolume';
8
+ import { getReferenceVolumeForSegmentation } from './getReferenceVolumeForSegmentation';
9
+ async function computeMetabolicStats({ segmentationIds, segmentIndex, }) {
10
+ registerComputeWorker();
11
+ triggerWorkerProgress(WorkerTypes.COMPUTE_STATISTICS, 0);
12
+ const segmentation = getSegmentation(segmentationIds[0]);
13
+ const { imageIds: segImageIds } = segmentation.representationData
14
+ .Labelmap;
15
+ const isValidVolume = utilities.isValidVolume(segImageIds);
16
+ if (!isValidVolume) {
17
+ throw new Error('Invalid volume - TMTV cannot be calculated');
18
+ }
19
+ const stats = await calculateForVolume({
20
+ segmentationIds,
21
+ segmentIndex,
22
+ });
23
+ return stats;
24
+ }
25
+ async function calculateForVolume({ segmentationIds, segmentIndex }) {
26
+ const labelmapVolumes = segmentationIds.map((id) => {
27
+ return getOrCreateSegmentationVolume(id);
28
+ });
29
+ const mergedLabelmap = createMergedLabelmapForIndex(labelmapVolumes, segmentIndex);
30
+ if (!mergedLabelmap) {
31
+ throw new Error('Invalid volume - TMTV cannot be calculated');
32
+ }
33
+ const { imageData, dimensions, direction, origin, voxelManager } = mergedLabelmap;
34
+ const spacing = imageData.getSpacing();
35
+ const segmentationScalarData = voxelManager.getCompleteScalarDataArray();
36
+ const segmentationInfo = {
37
+ scalarData: segmentationScalarData,
38
+ dimensions,
39
+ spacing,
40
+ origin,
41
+ direction,
42
+ };
43
+ const referenceVolume = getReferenceVolumeForSegmentation(segmentationIds[0]);
44
+ const imageInfo = {
45
+ dimensions: referenceVolume.dimensions,
46
+ spacing: referenceVolume.spacing,
47
+ origin: referenceVolume.origin,
48
+ direction: referenceVolume.direction,
49
+ scalarData: referenceVolume.voxelManager.getCompleteScalarDataArray(),
50
+ };
51
+ const stats = await getWebWorkerManager().executeTask('compute', 'computeMetabolicStats', {
52
+ segmentationInfo,
53
+ imageInfo,
54
+ });
55
+ triggerWorkerProgress(WorkerTypes.COMPUTE_STATISTICS, 100);
56
+ return stats;
57
+ }
58
+ export { computeMetabolicStats };
@@ -1,4 +1,4 @@
1
- import { volumeLoader, utilities as csUtils } from '@cornerstonejs/core';
1
+ import { volumeLoader, utilities as csUtils, cache } from '@cornerstonejs/core';
2
2
  function createMergedLabelmapForIndex(labelmaps, segmentIndex = 1, volumeId = 'mergedLabelmap') {
3
3
  labelmaps.forEach(({ direction, dimensions, origin, spacing }) => {
4
4
  if (!csUtils.isEqual(dimensions, labelmaps[0].dimensions) ||
@@ -28,7 +28,15 @@ function createMergedLabelmapForIndex(labelmaps, segmentIndex = 1, volumeId = 'm
28
28
  direction: labelmap.direction,
29
29
  dimensions: labelmap.dimensions,
30
30
  };
31
- const mergedVolume = volumeLoader.createLocalVolume(volumeId, options);
31
+ const cachedVolume = cache.getVolume(volumeId);
32
+ let mergedVolume;
33
+ if (cachedVolume) {
34
+ mergedVolume = cachedVolume;
35
+ mergedVolume.voxelManager.setCompleteScalarDataArray(outputData);
36
+ }
37
+ else {
38
+ mergedVolume = volumeLoader.createLocalVolume(volumeId, options);
39
+ }
32
40
  return mergedVolume;
33
41
  }
34
42
  export default createMergedLabelmapForIndex;
@@ -0,0 +1,3 @@
1
+ import { type Types } from '@cornerstonejs/core';
2
+ declare function getOrCreateImageVolume(referencedImageIds: string[]): Types.IImageVolume | undefined;
3
+ export default getOrCreateImageVolume;
@@ -0,0 +1,18 @@
1
+ import { cache, volumeLoader, utilities as csUtils, } from '@cornerstonejs/core';
2
+ function getOrCreateImageVolume(referencedImageIds) {
3
+ if (!referencedImageIds || referencedImageIds.length <= 1) {
4
+ return;
5
+ }
6
+ const isValidVolume = csUtils.isValidVolume(referencedImageIds);
7
+ if (!isValidVolume) {
8
+ return;
9
+ }
10
+ const volumeId = cache.generateVolumeId(referencedImageIds);
11
+ let imageVolume = cache.getVolume(volumeId);
12
+ if (imageVolume) {
13
+ return imageVolume;
14
+ }
15
+ imageVolume = volumeLoader.createAndCacheVolumeFromImagesSync(volumeId, referencedImageIds);
16
+ return imageVolume;
17
+ }
18
+ export default getOrCreateImageVolume;
@@ -1,2 +1,3 @@
1
- declare function getOrCreateSegmentationVolume(segmentationId: any): any;
1
+ import { type Types } from '@cornerstonejs/core';
2
+ declare function getOrCreateSegmentationVolume(segmentationId: any): Types.IImageVolume | undefined;
2
3
  export default getOrCreateSegmentationVolume;
@@ -1,4 +1,4 @@
1
- import { cache, volumeLoader, utilities } from '@cornerstonejs/core';
1
+ import { cache, volumeLoader, utilities, } from '@cornerstonejs/core';
2
2
  import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation';
3
3
  function getOrCreateSegmentationVolume(segmentationId) {
4
4
  const { representationData } = getSegmentation(segmentationId);
@@ -0,0 +1 @@
1
+ export declare function getReferenceVolumeForSegmentation(segmentationId: string): import("@cornerstonejs/core").ImageVolume;
@@ -0,0 +1,34 @@
1
+ import { cache } from '@cornerstonejs/core';
2
+ import { getSegmentation } from '../../stateManagement/segmentation/getSegmentation';
3
+ import getOrCreateImageVolume from './getOrCreateImageVolume';
4
+ export function getReferenceVolumeForSegmentation(segmentationId) {
5
+ const segmentation = getSegmentation(segmentationId);
6
+ if (!segmentation) {
7
+ return null;
8
+ }
9
+ let referenceImageIds;
10
+ const labelmap = segmentation.representationData.Labelmap;
11
+ if ('imageIds' in labelmap) {
12
+ const { imageIds } = labelmap;
13
+ const firstImage = cache.getImage(imageIds[0]);
14
+ const volumeInfo = cache.getVolumeContainingImageId(firstImage.referencedImageId);
15
+ if (volumeInfo?.volume) {
16
+ return volumeInfo.volume;
17
+ }
18
+ referenceImageIds = imageIds.map((imageId) => cache.getImage(imageId).referencedImageId);
19
+ }
20
+ else if ('volumeId' in labelmap) {
21
+ const { volumeId, referencedVolumeId } = labelmap;
22
+ if (referencedVolumeId) {
23
+ const refVolume = cache.getVolume(referencedVolumeId);
24
+ if (refVolume) {
25
+ return refVolume;
26
+ }
27
+ }
28
+ const segVolume = cache.getVolume(volumeId);
29
+ if (segVolume) {
30
+ referenceImageIds = segVolume.imageIds.map((imageId) => cache.getImage(imageId).referencedImageId);
31
+ }
32
+ }
33
+ return getOrCreateImageVolume(referenceImageIds);
34
+ }
@@ -116,7 +116,14 @@ const processSegmentationStatistics = ({ stats, unit, spacing, segmentationImage
116
116
  value: mean.value,
117
117
  unit,
118
118
  };
119
+ stats.peakPoint = {
120
+ name: 'peakLPS',
121
+ label: 'Peak SUV Point',
122
+ value: testMax.pointLPS ? [...testMax.pointLPS] : null,
123
+ unit: null,
124
+ };
119
125
  updateStatsArray(stats, stats.peakValue);
126
+ updateStatsArray(stats, stats.peakPoint);
120
127
  }
121
128
  }
122
129
  if (stats.volume && stats.mean) {
@@ -169,7 +176,7 @@ async function calculateStackStatistics({ segImageIds, indices, unit, mode }) {
169
176
  }
170
177
  }
171
178
  function getSphereStats(testMax, radiusIJK, segData, imageVoxels, spacing) {
172
- const { pointIJK: centerIJK } = testMax;
179
+ const { pointIJK: centerIJK, pointLPS: centerLPS } = testMax;
173
180
  if (!centerIJK) {
174
181
  return;
175
182
  }
@@ -22,10 +22,12 @@ import * as growCut from './growCut';
22
22
  import * as LabelmapMemo from './createLabelmapMemo';
23
23
  import IslandRemoval from './islandRemoval';
24
24
  import getOrCreateSegmentationVolume from './getOrCreateSegmentationVolume';
25
+ import getOrCreateImageVolume from './getOrCreateImageVolume';
25
26
  import getStatistics from './getStatistics';
26
27
  import * as validateLabelmap from './validateLabelmap';
27
28
  import { computeStackLabelmapFromVolume } from '../../stateManagement/segmentation/helpers/computeStackLabelmapFromVolume';
28
29
  import { computeVolumeLabelmapFromStack } from '../../stateManagement/segmentation/helpers/computeVolumeLabelmapFromStack';
29
30
  import { getReferenceVolumeForSegmentationVolume } from './getReferenceVolumeForSegmentationVolume';
30
31
  import { getSegmentLargestBidirectional } from './getSegmentLargestBidirectional';
31
- export { thresholdVolumeByRange, createMergedLabelmapForIndex, createLabelmapVolumeForViewport, rectangleROIThresholdVolumeByRange, triggerSegmentationRender, triggerSegmentationRenderBySegmentationId, floodFill, getBrushSizeForToolGroup, setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, VolumetricCalculator, SegmentStatsCalculator, thresholdSegmentationByRange, contourAndFindLargestBidirectional, createBidirectionalToolData, segmentContourAction, invalidateBrushCursor, getUniqueSegmentIndices, getSegmentIndexAtWorldPoint, getSegmentIndexAtLabelmapBorder, getHoveredContourSegmentationAnnotation, getBrushToolInstances, growCut, LabelmapMemo, IslandRemoval, getOrCreateSegmentationVolume, getStatistics, validateLabelmap, computeStackLabelmapFromVolume, computeVolumeLabelmapFromStack, getReferenceVolumeForSegmentationVolume, getSegmentLargestBidirectional, };
32
+ import { computeMetabolicStats } from './computeMetabolicStats';
33
+ export { thresholdVolumeByRange, createMergedLabelmapForIndex, createLabelmapVolumeForViewport, rectangleROIThresholdVolumeByRange, triggerSegmentationRender, triggerSegmentationRenderBySegmentationId, floodFill, getBrushSizeForToolGroup, setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, VolumetricCalculator, SegmentStatsCalculator, thresholdSegmentationByRange, contourAndFindLargestBidirectional, createBidirectionalToolData, segmentContourAction, invalidateBrushCursor, getUniqueSegmentIndices, getSegmentIndexAtWorldPoint, getSegmentIndexAtLabelmapBorder, getHoveredContourSegmentationAnnotation, getBrushToolInstances, growCut, LabelmapMemo, IslandRemoval, getOrCreateSegmentationVolume, getOrCreateImageVolume, getStatistics, validateLabelmap, computeStackLabelmapFromVolume, computeVolumeLabelmapFromStack, getReferenceVolumeForSegmentationVolume, getSegmentLargestBidirectional, computeMetabolicStats, };
@@ -22,10 +22,12 @@ import * as growCut from './growCut';
22
22
  import * as LabelmapMemo from './createLabelmapMemo';
23
23
  import IslandRemoval from './islandRemoval';
24
24
  import getOrCreateSegmentationVolume from './getOrCreateSegmentationVolume';
25
+ import getOrCreateImageVolume from './getOrCreateImageVolume';
25
26
  import getStatistics from './getStatistics';
26
27
  import * as validateLabelmap from './validateLabelmap';
27
28
  import { computeStackLabelmapFromVolume } from '../../stateManagement/segmentation/helpers/computeStackLabelmapFromVolume';
28
29
  import { computeVolumeLabelmapFromStack } from '../../stateManagement/segmentation/helpers/computeVolumeLabelmapFromStack';
29
30
  import { getReferenceVolumeForSegmentationVolume } from './getReferenceVolumeForSegmentationVolume';
30
31
  import { getSegmentLargestBidirectional } from './getSegmentLargestBidirectional';
31
- export { thresholdVolumeByRange, createMergedLabelmapForIndex, createLabelmapVolumeForViewport, rectangleROIThresholdVolumeByRange, triggerSegmentationRender, triggerSegmentationRenderBySegmentationId, floodFill, getBrushSizeForToolGroup, setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, VolumetricCalculator, SegmentStatsCalculator, thresholdSegmentationByRange, contourAndFindLargestBidirectional, createBidirectionalToolData, segmentContourAction, invalidateBrushCursor, getUniqueSegmentIndices, getSegmentIndexAtWorldPoint, getSegmentIndexAtLabelmapBorder, getHoveredContourSegmentationAnnotation, getBrushToolInstances, growCut, LabelmapMemo, IslandRemoval, getOrCreateSegmentationVolume, getStatistics, validateLabelmap, computeStackLabelmapFromVolume, computeVolumeLabelmapFromStack, getReferenceVolumeForSegmentationVolume, getSegmentLargestBidirectional, };
32
+ import { computeMetabolicStats } from './computeMetabolicStats';
33
+ export { thresholdVolumeByRange, createMergedLabelmapForIndex, createLabelmapVolumeForViewport, rectangleROIThresholdVolumeByRange, triggerSegmentationRender, triggerSegmentationRenderBySegmentationId, floodFill, getBrushSizeForToolGroup, setBrushSizeForToolGroup, getBrushThresholdForToolGroup, setBrushThresholdForToolGroup, VolumetricCalculator, SegmentStatsCalculator, thresholdSegmentationByRange, contourAndFindLargestBidirectional, createBidirectionalToolData, segmentContourAction, invalidateBrushCursor, getUniqueSegmentIndices, getSegmentIndexAtWorldPoint, getSegmentIndexAtLabelmapBorder, getHoveredContourSegmentationAnnotation, getBrushToolInstances, growCut, LabelmapMemo, IslandRemoval, getOrCreateSegmentationVolume, getOrCreateImageVolume, getStatistics, validateLabelmap, computeStackLabelmapFromVolume, computeVolumeLabelmapFromStack, getReferenceVolumeForSegmentationVolume, getSegmentLargestBidirectional, computeMetabolicStats, };
@@ -29,15 +29,24 @@ const computeWorker = {
29
29
  scalarData,
30
30
  };
31
31
  },
32
- createVTKImageData: (dimensions, origin, direction, spacing) => {
32
+ createVTKImageData: (dimensions, origin, direction, spacing, scalarData) => {
33
33
  const imageData = vtkImageData.newInstance();
34
34
  imageData.setDimensions(dimensions);
35
35
  imageData.setOrigin(origin);
36
36
  imageData.setDirection(direction);
37
37
  imageData.setSpacing(spacing);
38
+ if (!scalarData) {
39
+ return imageData;
40
+ }
41
+ const scalarArray = vtkDataArray.newInstance({
42
+ name: 'Scalars',
43
+ numberOfComponents: 1,
44
+ values: scalarData,
45
+ });
46
+ imageData.getPointData().setScalars(scalarArray);
38
47
  return imageData;
39
48
  },
40
- processSegmentStatistics: (segVoxelManager, imageVoxelManager, indices, bounds) => {
49
+ processSegmentStatistics: ({ segVoxelManager, imageVoxelManager, indices, bounds, imageData, }) => {
41
50
  segVoxelManager.forEach(({ value, pointIJK, pointLPS, index }) => {
42
51
  if (indices.indexOf(value) === -1) {
43
52
  return;
@@ -51,6 +60,7 @@ const computeWorker = {
51
60
  });
52
61
  }, {
53
62
  boundsIJK: bounds || imageVoxelManager.getDefaultBounds(),
63
+ imageData,
54
64
  });
55
65
  },
56
66
  performMarchingSquares: (imageData, sliceIndex = null, slicingMode = null) => {
@@ -99,8 +109,14 @@ const computeWorker = {
99
109
  const { segmentation, image } = computeWorker.getArgsFromInfo(args);
100
110
  const { voxelManager: segVoxelManager, spacing: segmentationSpacing } = segmentation;
101
111
  const { voxelManager: imageVoxelManager } = image;
112
+ const imageData = computeWorker.createVTKImageData(segmentation.dimensions, segmentation.origin, segmentation.direction, segmentation.spacing);
102
113
  SegmentStatsCalculator.statsInit({ storePointData: false, indices, mode });
103
- computeWorker.processSegmentStatistics(segVoxelManager, imageVoxelManager, indices);
114
+ computeWorker.processSegmentStatistics({
115
+ segVoxelManager,
116
+ imageVoxelManager,
117
+ indices,
118
+ imageData,
119
+ });
104
120
  const stats = SegmentStatsCalculator.getStatistics({
105
121
  spacing: segmentationSpacing,
106
122
  unit: 'mm',
@@ -108,6 +124,33 @@ const computeWorker = {
108
124
  });
109
125
  return stats;
110
126
  },
127
+ computeMetabolicStats({ segmentationInfo, imageInfo }) {
128
+ const { scalarData, dimensions, spacing, origin, direction } = segmentationInfo;
129
+ const { spacing: imageSpacing, dimensions: imageDimensions, direction: imageDirection, origin: imageOrigin, scalarData: imageScalarData, } = imageInfo;
130
+ const segVoxelManager = computeWorker.createVoxelManager(segmentationInfo.dimensions, segmentationInfo.scalarData);
131
+ const refVoxelManager = computeWorker.createVoxelManager(imageDimensions, imageScalarData);
132
+ let suv = 0;
133
+ let numVoxels = 0;
134
+ const scalarDataLength = segVoxelManager.getScalarDataLength();
135
+ for (let i = 0; i < scalarDataLength; i++) {
136
+ if (segVoxelManager.getAtIndex(i) !== 0) {
137
+ suv += refVoxelManager.getAtIndex(i);
138
+ numVoxels++;
139
+ }
140
+ }
141
+ const tmtv = 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
142
+ const averageSuv = numVoxels > 0 ? suv / numVoxels : 0;
143
+ const tlg = averageSuv *
144
+ numVoxels *
145
+ imageSpacing[0] *
146
+ imageSpacing[1] *
147
+ imageSpacing[2] *
148
+ 1e-3;
149
+ return {
150
+ tmtv,
151
+ tlg,
152
+ };
153
+ },
111
154
  calculateSegmentsStatisticsStack: (args) => {
112
155
  const { segmentationInfo, imageInfo, indices, mode } = args;
113
156
  SegmentStatsCalculator.statsInit({ storePointData: true, indices, mode });
@@ -121,7 +164,13 @@ const computeWorker = {
121
164
  ];
122
165
  const segVoxelManager = computeWorker.createVoxelManager(segDimensions, segInfo.scalarData);
123
166
  const imageVoxelManager = computeWorker.createVoxelManager(segDimensions, imgInfo.scalarData);
124
- computeWorker.processSegmentStatistics(segVoxelManager, imageVoxelManager, indices);
167
+ const imageData = computeWorker.createVTKImageData(segDimensions, segInfo.origin, segInfo.direction, segInfo.spacing);
168
+ computeWorker.processSegmentStatistics({
169
+ segVoxelManager,
170
+ imageVoxelManager,
171
+ indices,
172
+ imageData,
173
+ });
125
174
  }
126
175
  const spacing = segmentationInfo[0].spacing;
127
176
  const stats = SegmentStatsCalculator.getStatistics({
@@ -141,58 +190,119 @@ const computeWorker = {
141
190
  segmentationInfo: segmentationInfo[0],
142
191
  }));
143
192
  }
144
- const { voxelManager, dimensions, origin, direction, spacing } = segmentation;
145
- const stackDimensions = isStack
146
- ? [dimensions[0], dimensions[1], 2]
147
- : dimensions;
148
- const stackSpacing = isStack ? [spacing[0], spacing[1], 1] : spacing;
149
- const imageData = computeWorker.createVTKImageData(stackDimensions, origin, direction, stackSpacing);
150
- let contourSets;
151
- if (!isStack) {
152
- contourSets = computeWorker.generateContourSetsFromLabelmapVolume({
153
- segmentation,
193
+ return isStack
194
+ ? computeWorker.calculateBidirectionalStack({
195
+ segmentationInfo,
154
196
  indices,
155
- imageData,
156
197
  mode,
157
- });
158
- }
159
- else {
160
- contourSets = computeWorker.generateContourSetsFromLabelmapStack({
161
- segmentationInfo,
198
+ })
199
+ : computeWorker.calculateVolumetricBidirectional({
200
+ segmentation,
162
201
  indices,
163
202
  mode,
164
203
  });
204
+ },
205
+ findLargestBidirectionalFromContours: (contours, isInSegment, segmentIndex) => {
206
+ let maxBidirectional;
207
+ for (const sliceContour of contours) {
208
+ const bidirectional = createBidirectionalForSlice(sliceContour, isInSegment, maxBidirectional);
209
+ if (!bidirectional) {
210
+ continue;
211
+ }
212
+ maxBidirectional = bidirectional;
165
213
  }
166
- const bidirectionalData = [];
214
+ if (maxBidirectional) {
215
+ return {
216
+ segmentIndex,
217
+ majorAxis: maxBidirectional.majorAxis,
218
+ minorAxis: maxBidirectional.minorAxis,
219
+ maxMajor: maxBidirectional.maxMajor,
220
+ maxMinor: maxBidirectional.maxMinor,
221
+ };
222
+ }
223
+ return null;
224
+ },
225
+ calculateBidirectionalStack: ({ segmentationInfo, indices, mode }) => {
226
+ const segments = computeWorker.createSegmentsFromIndices(indices);
227
+ let bidirectionalResults = [];
228
+ for (let i = 0; i < segmentationInfo.length; i++) {
229
+ const segInfo = segmentationInfo[i];
230
+ const dimensions = segInfo.dimensions;
231
+ const segScalarData = segInfo.scalarData;
232
+ const { spacing, direction, origin } = segInfo;
233
+ const voxelManager = computeWorker.createVoxelManager(dimensions, segScalarData);
234
+ const pixelsPerSlice = dimensions[0] * dimensions[1];
235
+ for (let segIndex = 1; segIndex < segments.length; segIndex++) {
236
+ const segment = segments[segIndex];
237
+ if (!segment) {
238
+ continue;
239
+ }
240
+ const segmentIndex = segment.segmentIndex;
241
+ if (computeWorker.isSliceEmptyForSegmentVolume(0, segScalarData, pixelsPerSlice, segmentIndex)) {
242
+ continue;
243
+ }
244
+ const sliceContours = [];
245
+ const filteredData = new Uint8Array(segScalarData.length);
246
+ for (let i = 0; i < segScalarData.length; i++) {
247
+ filteredData[i] = segScalarData[i] === segmentIndex ? 1 : 0;
248
+ }
249
+ const scalarArray = vtkDataArray.newInstance({
250
+ name: 'Pixels',
251
+ numberOfComponents: 1,
252
+ values: filteredData,
253
+ });
254
+ const imageData = computeWorker.createVTKImageData(dimensions, origin, direction, [spacing[0], spacing[1], 1]);
255
+ imageData.getPointData().setScalars(scalarArray);
256
+ try {
257
+ const msOutput = computeWorker.performMarchingSquares(imageData, null, 2);
258
+ const contourData = computeWorker.createContoursFromPolyData(msOutput);
259
+ if (contourData) {
260
+ sliceContours.push(contourData);
261
+ }
262
+ }
263
+ catch (e) {
264
+ console.warn(e);
265
+ }
266
+ const isInSegment = createIsInSegmentMetadata({
267
+ dimensions,
268
+ imageData,
269
+ voxelManager,
270
+ segmentIndex,
271
+ });
272
+ const bidirectionalResult = computeWorker.findLargestBidirectionalFromContours(sliceContours, isInSegment, segmentIndex);
273
+ if (bidirectionalResult) {
274
+ bidirectionalResults.push(bidirectionalResult);
275
+ }
276
+ }
277
+ }
278
+ return bidirectionalResults;
279
+ },
280
+ calculateVolumetricBidirectional: ({ segmentation, indices, mode }) => {
281
+ const { voxelManager, dimensions, origin, direction, spacing } = segmentation;
282
+ const imageData = computeWorker.createVTKImageData(dimensions, origin, direction, spacing);
283
+ const contourSets = computeWorker.generateContourSetsFromLabelmapVolume({
284
+ segmentation,
285
+ indices,
286
+ imageData,
287
+ mode,
288
+ });
289
+ const bidirectionalResults = [];
167
290
  for (let i = 0; i < contourSets.length; i++) {
168
291
  const contourSet = contourSets[i];
169
292
  const { segmentIndex } = contourSet.segment;
170
293
  const contours = contourSet.sliceContours;
171
- let maxBidirectional;
172
294
  const isInSegment = createIsInSegmentMetadata({
173
295
  dimensions,
174
296
  imageData,
175
297
  voxelManager,
176
298
  segmentIndex,
177
299
  });
178
- for (const sliceContour of contours) {
179
- const bidirectional = createBidirectionalForSlice(sliceContour, isInSegment, maxBidirectional);
180
- if (!bidirectional) {
181
- continue;
182
- }
183
- maxBidirectional = bidirectional;
184
- }
185
- if (maxBidirectional) {
186
- bidirectionalData.push({
187
- segmentIndex,
188
- majorAxis: maxBidirectional.majorAxis,
189
- minorAxis: maxBidirectional.minorAxis,
190
- maxMajor: maxBidirectional.maxMajor,
191
- maxMinor: maxBidirectional.maxMinor,
192
- });
300
+ const bidirectionalResult = computeWorker.findLargestBidirectionalFromContours(contours, isInSegment, segmentIndex);
301
+ if (bidirectionalResult) {
302
+ bidirectionalResults.push(bidirectionalResult);
193
303
  }
194
304
  }
195
- return bidirectionalData;
305
+ return bidirectionalResults;
196
306
  },
197
307
  generateContourSetsFromLabelmapVolume: (args) => {
198
308
  const { segmentation, indices, imageData } = args;
@@ -259,62 +369,6 @@ const computeWorker = {
259
369
  }
260
370
  return ContourSets;
261
371
  },
262
- generateContourSetsFromLabelmapStack: (args) => {
263
- const { segmentationInfo, indices } = args;
264
- let ContourSets = [];
265
- for (let i = 0; i < segmentationInfo.length; i++) {
266
- const segInfo = segmentationInfo[i];
267
- const dimensions = segInfo.dimensions;
268
- const segScalarData = segInfo.scalarData;
269
- const { spacing, direction, origin } = segInfo;
270
- const segments = computeWorker.createSegmentsFromIndices(indices);
271
- const pixelsPerSlice = dimensions[0] * dimensions[1];
272
- const numSegments = segments.length;
273
- for (let segIndex = 0; segIndex < numSegments; segIndex++) {
274
- const segment = segments[segIndex];
275
- if (!segment) {
276
- continue;
277
- }
278
- const segmentIndex = segment.segmentIndex;
279
- if (computeWorker.isSliceEmptyForSegmentVolume(0, segScalarData, pixelsPerSlice, segmentIndex)) {
280
- continue;
281
- }
282
- const sliceContours = [];
283
- const filteredData = new Uint8Array(segScalarData.length);
284
- for (let i = 0; i < segScalarData.length; i++) {
285
- if (segScalarData[i] === segmentIndex) {
286
- filteredData[i] = 1;
287
- }
288
- else {
289
- filteredData[i] = 0;
290
- }
291
- }
292
- const scalarArray = vtkDataArray.newInstance({
293
- name: 'Pixels',
294
- numberOfComponents: 1,
295
- values: filteredData,
296
- });
297
- const imageData = computeWorker.createVTKImageData(dimensions, origin, direction, [spacing[0], spacing[1], 1]);
298
- imageData.getPointData().setScalars(scalarArray);
299
- try {
300
- const msOutput = computeWorker.performMarchingSquares(imageData, null, 2);
301
- const contourData = computeWorker.createContoursFromPolyData(msOutput);
302
- if (contourData) {
303
- sliceContours.push(contourData);
304
- }
305
- }
306
- catch (e) {
307
- console.warn(e);
308
- }
309
- const ContourSet = {
310
- sliceContours,
311
- segment,
312
- };
313
- ContourSets.push(ContourSet);
314
- }
315
- }
316
- return ContourSets;
317
- },
318
372
  isSliceEmptyForSegmentVolume: (sliceIndex, segData, pixelsPerSlice, segIndex) => {
319
373
  const startIdx = sliceIndex * pixelsPerSlice;
320
374
  const endIdx = startIdx + pixelsPerSlice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/tools",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "Cornerstone3D Tools",
5
5
  "types": "./dist/esm/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -103,8 +103,8 @@
103
103
  "canvas": "^2.11.2"
104
104
  },
105
105
  "peerDependencies": {
106
- "@cornerstonejs/core": "^3.6.0",
107
- "@kitware/vtk.js": "32.12.0",
106
+ "@cornerstonejs/core": "^3.7.0",
107
+ "@kitware/vtk.js": "32.12.1",
108
108
  "@types/d3-array": "^3.0.4",
109
109
  "@types/d3-interpolate": "^3.0.1",
110
110
  "d3-array": "^3.2.3",
@@ -122,5 +122,5 @@
122
122
  "type": "individual",
123
123
  "url": "https://ohif.org/donate"
124
124
  },
125
- "gitHead": "480e48c2a25ce79a4a90be6fe180d1bcdd1b831e"
125
+ "gitHead": "177fce2dd8c2278d3c8473f15df795e757d40b7e"
126
126
  }