@cornerstonejs/tools 5.4.11 → 5.4.13

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 (46) hide show
  1. package/dist/esm/index.d.ts +2 -2
  2. package/dist/esm/index.js +2 -2
  3. package/dist/esm/stateManagement/segmentation/helpers/isSegmentationOverlayCompatible.d.ts +3 -0
  4. package/dist/esm/stateManagement/segmentation/helpers/isSegmentationOverlayCompatible.js +87 -0
  5. package/dist/esm/stateManagement/segmentation/internalAddSegmentationRepresentation.js +5 -0
  6. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.d.ts +12 -2
  7. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapEditTransaction.js +21 -1
  8. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.d.ts +5 -0
  9. package/dist/esm/stateManagement/segmentation/labelmapModel/labelmapImageReferenceResolver.js +47 -1
  10. package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.d.ts +5 -1
  11. package/dist/esm/stateManagement/segmentation/labelmapModel/privateLabelmap.js +49 -4
  12. package/dist/esm/tools/annotation/ClickSegmentTool.d.ts +51 -0
  13. package/dist/esm/tools/annotation/ClickSegmentTool.js +748 -0
  14. package/dist/esm/tools/annotation/regionSegmentHoverCursors.d.ts +4 -0
  15. package/dist/esm/tools/annotation/regionSegmentHoverCursors.js +4 -0
  16. package/dist/esm/tools/displayTools/Labelmap/syncStackLabelmapActors.js +5 -0
  17. package/dist/esm/tools/index.d.ts +2 -1
  18. package/dist/esm/tools/index.js +2 -1
  19. package/dist/esm/tools/segmentation/BrushTool.js +7 -3
  20. package/dist/esm/tools/segmentation/strategies/BrushStrategy.js +21 -0
  21. package/dist/esm/tools/segmentation/strategies/utils/crossLayerErase.js +20 -0
  22. package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.d.ts +12 -0
  23. package/dist/esm/utilities/segmentation/commitSliceMasksToLabelmap.js +128 -0
  24. package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.d.ts +2 -0
  25. package/dist/esm/utilities/segmentation/createEnsureSliceLoadedForVolume.js +36 -0
  26. package/dist/esm/utilities/segmentation/createLabelmapMemo.d.ts +7 -0
  27. package/dist/esm/utilities/segmentation/createLabelmapMemo.js +41 -8
  28. package/dist/esm/utilities/segmentation/floodFillIslandRemoval.d.ts +20 -0
  29. package/dist/esm/utilities/segmentation/floodFillIslandRemoval.js +118 -0
  30. package/dist/esm/utilities/segmentation/floodFillSliceLazy.d.ts +33 -0
  31. package/dist/esm/utilities/segmentation/floodFillSliceLazy.js +167 -0
  32. package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.d.ts +51 -0
  33. package/dist/esm/utilities/segmentation/growCut/floodFillIntensityRangeTypes.js +0 -0
  34. package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.d.ts +10 -0
  35. package/dist/esm/utilities/segmentation/growCut/getViewportVoiMappingForVolume.js +21 -0
  36. package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.d.ts +57 -0
  37. package/dist/esm/utilities/segmentation/growCut/intensityRange/adaptiveRegionIntensityRange.js +559 -0
  38. package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.d.ts +10 -0
  39. package/dist/esm/utilities/segmentation/growCut/neighborhoodStats.js +44 -0
  40. package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.d.ts +61 -0
  41. package/dist/esm/utilities/segmentation/growCut/runFloodFillSegmentation.js +524 -0
  42. package/dist/esm/utilities/segmentation/islandRemoval.d.ts +3 -2
  43. package/dist/esm/utilities/segmentation/islandRemoval.js +3 -0
  44. package/dist/esm/version.d.ts +1 -1
  45. package/dist/esm/version.js +1 -1
  46. package/package.json +5 -5
@@ -0,0 +1,524 @@
1
+ import { utilities as csUtils, cache } from '@cornerstonejs/core';
2
+ import IslandRemoval from '../floodFillIslandRemoval.js';
3
+ import { commitSliceMasksToLabelmapVolume } from '../commitSliceMasksToLabelmap.js';
4
+ import { createEnsureSliceLoadedForVolume } from '../createEnsureSliceLoadedForVolume.js';
5
+ import { floodFill3dSliceLazy } from '../floodFillSliceLazy.js';
6
+ import { DEFAULT_NEIGHBORHOOD_RADIUS, DEFAULT_POSITIVE_STD_DEV_MULTIPLIER, } from './constants.js';
7
+ import { calculateNeighborhoodStatsVM } from './neighborhoodStats.js';
8
+ import { getViewportVoiMappingForVolume } from './getViewportVoiMappingForVolume.js';
9
+ const { transformWorldToIndex, mapScalarToViewportVoiIntensity, mapMappedBandToRawRange, } = csUtils;
10
+ const { growCutLog: log } = csUtils.logger;
11
+ const ENABLE_VERBOSE_FLOOD_FILL_LOGS = false;
12
+ const timeStart = (label) => {
13
+ if (ENABLE_VERBOSE_FLOOD_FILL_LOGS) {
14
+ console.time(label);
15
+ }
16
+ };
17
+ const timeEnd = (label) => {
18
+ if (ENABLE_VERBOSE_FLOOD_FILL_LOGS) {
19
+ console.timeEnd(label);
20
+ }
21
+ };
22
+ const FLOOD_FILL_PREP_TIMING_LABEL = 'cornerstone.tools: floodFill: prep';
23
+ const FLOOD_FILL_PREP_REF_META = 'cornerstone.tools: floodFill: prep: ref_volume_meta';
24
+ const FLOOD_FILL_RANGE_TIMING_LABEL = 'cornerstone.tools: floodFill: intensityRange';
25
+ const FLOOD_FILL_RUN_TIMING_LABEL = 'cornerstone.tools: floodFill: fillAndIslandRemoval';
26
+ const FLOOD_FILL_ISLAND_EXTERNAL_TIMING_LABEL = 'cornerstone.tools: floodFill: islandRemoval:external';
27
+ const FLOOD_FILL_ISLAND_INTERNAL_TIMING_LABEL = 'cornerstone.tools: floodFill: islandRemoval:internal';
28
+ function getDisplayVoiSnapshot(viewport, referencedVolumeId) {
29
+ const getProps = viewport.getProperties;
30
+ if (typeof getProps !== 'function') {
31
+ return null;
32
+ }
33
+ const props = referencedVolumeId
34
+ ? getProps.call(viewport, referencedVolumeId)
35
+ : getProps.call(viewport);
36
+ const voiRange = props?.voiRange;
37
+ if (!voiRange ||
38
+ typeof voiRange.lower !== 'number' ||
39
+ typeof voiRange.upper !== 'number') {
40
+ return null;
41
+ }
42
+ const { lower, upper } = voiRange;
43
+ return {
44
+ lower,
45
+ upper,
46
+ windowWidth: upper - lower,
47
+ windowCenter: (lower + upper) / 2,
48
+ };
49
+ }
50
+ function getPositiveIntensityRangeRaw(referencedVolume, worldPosition, options) {
51
+ const { dimensions, imageData: refImageData } = referencedVolume;
52
+ const [width, height, numSlices] = dimensions;
53
+ const referenceVolumeVoxelManager = referencedVolume.voxelManager;
54
+ const neighborhoodRadius = options?.initialNeighborhoodRadius ?? DEFAULT_NEIGHBORHOOD_RADIUS;
55
+ const positiveK = options?.positiveStdDevMultiplier ?? DEFAULT_POSITIVE_STD_DEV_MULTIPLIER;
56
+ const ijkStart = transformWorldToIndex(refImageData, worldPosition).map(Math.round);
57
+ if (ijkStart[0] < 0 ||
58
+ ijkStart[0] >= width ||
59
+ ijkStart[1] < 0 ||
60
+ ijkStart[1] >= height ||
61
+ ijkStart[2] < 0 ||
62
+ ijkStart[2] >= numSlices) {
63
+ log.warn('intensity range: click outside volume', {
64
+ ijkStart,
65
+ dimensions: [width, height, numSlices],
66
+ });
67
+ return null;
68
+ }
69
+ const initialStats = calculateNeighborhoodStatsVM(referenceVolumeVoxelManager, dimensions, ijkStart, neighborhoodRadius);
70
+ if (initialStats.count === 0) {
71
+ const seedScalar = Number(referenceVolumeVoxelManager.getAtIJKPoint(ijkStart));
72
+ initialStats.mean = seedScalar;
73
+ initialStats.stdDev = 0;
74
+ }
75
+ const min = initialStats.mean - positiveK * initialStats.stdDev;
76
+ const max = initialStats.mean + positiveK * initialStats.stdDev;
77
+ const startValue = referenceVolumeVoxelManager.getAtIJKPoint(ijkStart);
78
+ if (startValue < min || startValue > max) {
79
+ log.warn('intensity range: clicked voxel intensity is outside the calculated positive range');
80
+ return null;
81
+ }
82
+ return {
83
+ min,
84
+ max,
85
+ ijkStart,
86
+ diagnostics: {
87
+ neighborhoodMean: initialStats.mean,
88
+ neighborhoodStdDev: initialStats.stdDev,
89
+ clickedVoxelValue: startValue,
90
+ positiveStdDevMultiplier: positiveK,
91
+ neighborhoodRadius,
92
+ strategy: 'meanStdRaw',
93
+ },
94
+ };
95
+ }
96
+ function getPositiveIntensityRangeVoiMapped(referencedVolume, worldPosition, voiMapping, options) {
97
+ const { dimensions, imageData: refImageData } = referencedVolume;
98
+ const [width, height, numSlices] = dimensions;
99
+ const referenceVolumeVoxelManager = referencedVolume.voxelManager;
100
+ const neighborhoodRadius = options?.initialNeighborhoodRadius ?? DEFAULT_NEIGHBORHOOD_RADIUS;
101
+ const positiveK = options?.positiveStdDevMultiplier ?? DEFAULT_POSITIVE_STD_DEV_MULTIPLIER;
102
+ const ijkStart = transformWorldToIndex(refImageData, worldPosition).map(Math.round);
103
+ if (ijkStart[0] < 0 ||
104
+ ijkStart[0] >= width ||
105
+ ijkStart[1] < 0 ||
106
+ ijkStart[1] >= height ||
107
+ ijkStart[2] < 0 ||
108
+ ijkStart[2] >= numSlices) {
109
+ log.info('intensity range (VOI-mapped): click outside volume', {
110
+ ijkStart,
111
+ dimensions: [width, height, numSlices],
112
+ });
113
+ return null;
114
+ }
115
+ const mapFn = (v) => mapScalarToViewportVoiIntensity(v, voiMapping);
116
+ const initialStats = calculateNeighborhoodStatsVM(referenceVolumeVoxelManager, dimensions, ijkStart, neighborhoodRadius, mapFn);
117
+ if (initialStats.count === 0) {
118
+ const seedScalar = Number(referenceVolumeVoxelManager.getAtIJKPoint(ijkStart));
119
+ initialStats.mean = mapFn(seedScalar);
120
+ initialStats.stdDev = 0;
121
+ }
122
+ let yMin = initialStats.mean - positiveK * initialStats.stdDev;
123
+ let yMax = initialStats.mean + positiveK * initialStats.stdDev;
124
+ yMin = Math.max(0, Math.min(1, yMin));
125
+ yMax = Math.max(0, Math.min(1, yMax));
126
+ if (yMin > yMax) {
127
+ const t = yMin;
128
+ yMin = yMax;
129
+ yMax = t;
130
+ }
131
+ const { rawMin, rawMax } = mapMappedBandToRawRange(yMin, yMax, voiMapping);
132
+ const startValue = Number(referenceVolumeVoxelManager.getAtIJKPoint(ijkStart));
133
+ const bandLo = Math.min(rawMin, rawMax);
134
+ const bandHi = Math.max(rawMin, rawMax);
135
+ const min = Math.min(bandLo, startValue);
136
+ const max = Math.max(bandHi, startValue);
137
+ return {
138
+ min,
139
+ max,
140
+ ijkStart,
141
+ diagnostics: {
142
+ neighborhoodMean: initialStats.mean,
143
+ neighborhoodStdDev: initialStats.stdDev,
144
+ clickedVoxelValue: startValue,
145
+ positiveStdDevMultiplier: positiveK,
146
+ neighborhoodRadius,
147
+ strategy: 'meanStdVoiMapped',
148
+ mappedBand: { min: yMin, max: yMax },
149
+ },
150
+ };
151
+ }
152
+ function getPositiveIntensityRange(referencedVolume, worldPosition, options) {
153
+ if (options?.voiMapping) {
154
+ return getPositiveIntensityRangeVoiMapped(referencedVolume, worldPosition, options.voiMapping, options);
155
+ }
156
+ return getPositiveIntensityRangeRaw(referencedVolume, worldPosition, options);
157
+ }
158
+ function resolveIntensityRange(referencedVolume, worldPosition, viewport, referencedVolumeId, options, rangeContext) {
159
+ if (options.getIntensityRange) {
160
+ const fromGetter = options.getIntensityRange(referencedVolume, worldPosition, rangeContext);
161
+ if (!fromGetter) {
162
+ log.warn('flood fill intensity range: strategy/custom getIntensityRange returned null (e.g. missing canvasPoint/VOI for canvas-disk, click outside volume, or fixed-% rejection)', { referencedVolumeId, worldPosition });
163
+ }
164
+ return fromGetter;
165
+ }
166
+ const voiFromViewport = getViewportVoiMappingForVolume(viewport, referencedVolumeId);
167
+ const merged = {
168
+ ...rangeContext,
169
+ voiMapping: voiFromViewport ?? undefined,
170
+ };
171
+ let result = getPositiveIntensityRange(referencedVolume, worldPosition, merged);
172
+ if (!result && merged.voiMapping) {
173
+ log.info('flood fill intensity range: VOI-mapped mean±σ failed; falling back to raw mean±σ', { referencedVolumeId });
174
+ result = getPositiveIntensityRange(referencedVolume, worldPosition, {
175
+ ...merged,
176
+ voiMapping: undefined,
177
+ });
178
+ }
179
+ if (!result) {
180
+ log.warn('flood fill intensity range: default mean±σ (VOI + raw fallback) could not resolve a band', { referencedVolumeId, worldPosition });
181
+ }
182
+ return result;
183
+ }
184
+ function assertFloodFillLabelmapMatchesRef(referencedVolume, labelmapVolume) {
185
+ const [rw, rh, rd] = referencedVolume.dimensions;
186
+ const [lw, lh, ld] = labelmapVolume.dimensions;
187
+ if (rw !== lw || rh !== lh || rd !== ld) {
188
+ throw new Error(`runFloodFillSegmentation: labelmap dimensions [${lw},${lh},${ld}] must match referenced volume [${rw},${rh},${rd}]`);
189
+ }
190
+ }
191
+ function resolveFloodPaintIndices(segmentIndex, explicitPreview) {
192
+ if (explicitPreview !== undefined) {
193
+ return {
194
+ paintIndex: explicitPreview,
195
+ usePreview: explicitPreview !== segmentIndex,
196
+ };
197
+ }
198
+ if (segmentIndex === 255) {
199
+ return { paintIndex: 255, usePreview: false };
200
+ }
201
+ return { paintIndex: 255, usePreview: true };
202
+ }
203
+ function promotePreviewSegmentToFinal(readVoxelManager, writeVoxelManager, preview, final, points, numPixelsPerSlice, width) {
204
+ for (let i = 0; i < points.length; i++) {
205
+ const [x, y, z] = points[i];
206
+ const index = z * numPixelsPerSlice + y * width + x;
207
+ if (readVoxelManager.getAtIndex(index) === preview) {
208
+ writeVoxelManager.setAtIndex(index, final);
209
+ }
210
+ }
211
+ }
212
+ async function runFloodFillSegmentation({ referencedVolumeId, worldPosition, viewport, labelmapVolume, options = {}, }) {
213
+ timeStart(FLOOD_FILL_PREP_TIMING_LABEL);
214
+ const referencedVolume = cache.getVolume(referencedVolumeId);
215
+ assertFloodFillLabelmapMatchesRef(referencedVolume, labelmapVolume);
216
+ const labelmap = labelmapVolume;
217
+ const segmentIndex = options.segmentIndex ?? 1;
218
+ const { paintIndex, usePreview } = resolveFloodPaintIndices(segmentIndex, options.floodPreviewSegmentIndex);
219
+ timeStart(FLOOD_FILL_PREP_REF_META);
220
+ const [volMin, volMax] = referencedVolume.voxelManager.getRange();
221
+ const displayVoi = getDisplayVoiSnapshot(viewport, referencedVolumeId);
222
+ log.info('segmentation path: flood fill (floodfill_full)', {
223
+ referencedVolumeId,
224
+ volumeScalarRange: { min: volMin, max: volMax },
225
+ displayVoi,
226
+ floodPreview: usePreview ? paintIndex : null,
227
+ segmentIndex,
228
+ });
229
+ timeEnd(FLOOD_FILL_PREP_REF_META);
230
+ timeEnd(FLOOD_FILL_PREP_TIMING_LABEL);
231
+ const voiMapping = getViewportVoiMappingForVolume(viewport, referencedVolumeId);
232
+ const rangeContext = {
233
+ positiveStdDevMultiplier: options.positiveStdDevMultiplier,
234
+ initialNeighborhoodRadius: options.initialNeighborhoodRadius,
235
+ viewport,
236
+ element: options.element,
237
+ referencedVolumeId,
238
+ canvasPoint: options.canvasPoint,
239
+ canvasDiskRadiusPx: options.intensitySamplingDiskRadiusCanvasPx,
240
+ voiMapping: voiMapping ?? undefined,
241
+ };
242
+ timeStart(FLOOD_FILL_RANGE_TIMING_LABEL);
243
+ const rangeResult = resolveIntensityRange(referencedVolume, worldPosition, viewport, referencedVolumeId, options, rangeContext);
244
+ timeEnd(FLOOD_FILL_RANGE_TIMING_LABEL);
245
+ if (!rangeResult) {
246
+ log.warn('flood fill: aborted before fill (no intensity range)', {
247
+ referencedVolumeId,
248
+ worldPosition,
249
+ });
250
+ return null;
251
+ }
252
+ const { min: rangeMin, max: rangeMax, ijkStart, diagnostics } = rangeResult;
253
+ let clampedRangeMin = rangeMin;
254
+ let clampedRangeMax = rangeMax;
255
+ const MAX_WL_BAND_FRACTION = 0.2;
256
+ if (voiMapping && diagnostics?.mappedBand) {
257
+ const mapped = diagnostics.mappedBand;
258
+ const mappedLo = Math.max(0, Math.min(1, Math.min(mapped.min, mapped.max)));
259
+ const mappedHi = Math.max(0, Math.min(1, Math.max(mapped.min, mapped.max)));
260
+ const mappedWidth = mappedHi - mappedLo;
261
+ if (mappedWidth > MAX_WL_BAND_FRACTION) {
262
+ const seedScalarPreClamp = Number(referencedVolume.voxelManager.getAtIJKPoint(ijkStart));
263
+ const seedMapped = Math.max(0, Math.min(1, mapScalarToViewportVoiIntensity(seedScalarPreClamp, voiMapping)));
264
+ const half = MAX_WL_BAND_FRACTION / 2;
265
+ const clippedMappedLo = Math.max(0, seedMapped - half);
266
+ const clippedMappedHi = Math.min(1, seedMapped + half);
267
+ const { rawMin: clipRawMin, rawMax: clipRawMax } = mapMappedBandToRawRange(clippedMappedLo, clippedMappedHi, voiMapping);
268
+ clampedRangeMin = Math.min(clipRawMin, clipRawMax, seedScalarPreClamp);
269
+ clampedRangeMax = Math.max(clipRawMin, clipRawMax, seedScalarPreClamp);
270
+ log.info('flood fill: clipped wide WL band around seed', {
271
+ maxWlBandFraction: MAX_WL_BAND_FRACTION,
272
+ originalMappedBand: {
273
+ min: mappedLo,
274
+ max: mappedHi,
275
+ width: mappedWidth,
276
+ },
277
+ clippedMappedBand: {
278
+ min: clippedMappedLo,
279
+ max: clippedMappedHi,
280
+ width: clippedMappedHi - clippedMappedLo,
281
+ },
282
+ });
283
+ }
284
+ }
285
+ log.info('intensity tolerance band', {
286
+ toleranceMin: clampedRangeMin,
287
+ toleranceMax: clampedRangeMax,
288
+ width: clampedRangeMax - clampedRangeMin,
289
+ ...diagnostics,
290
+ });
291
+ if (ENABLE_VERBOSE_FLOOD_FILL_LOGS) {
292
+ console.info('[cornerstone-tools] flood fill intensity range', {
293
+ rawMin: clampedRangeMin,
294
+ rawMax: clampedRangeMax,
295
+ strategy: diagnostics.strategy,
296
+ mappedBand: diagnostics.mappedBand,
297
+ neighborhoodRadius: diagnostics.neighborhoodRadius,
298
+ });
299
+ }
300
+ timeStart(FLOOD_FILL_RUN_TIMING_LABEL);
301
+ try {
302
+ const { dimensions } = referencedVolume;
303
+ const [width, height, numSlices] = dimensions;
304
+ const refVoxelManager = referencedVolume.voxelManager;
305
+ const numPixelsPerSlice = width * height;
306
+ const labelmapReadVm = labelmap.voxelManager;
307
+ const labelmapWriteVm = (options.historyVoxelManager ??
308
+ labelmap.voxelManager);
309
+ let positiveMin = clampedRangeMin;
310
+ let positiveMax = clampedRangeMax;
311
+ const seedScalar = Number(refVoxelManager.getAtIJKPoint(ijkStart));
312
+ if (Number.isFinite(seedScalar)) {
313
+ if (seedScalar < positiveMin) {
314
+ log.info('flood fill: expanded tolerance min to include seed voxel', {
315
+ seedScalar,
316
+ previousMin: positiveMin,
317
+ });
318
+ positiveMin = seedScalar;
319
+ }
320
+ if (seedScalar > positiveMax) {
321
+ log.info('flood fill: expanded tolerance max to include seed voxel', {
322
+ seedScalar,
323
+ previousMax: positiveMax,
324
+ });
325
+ positiveMax = seedScalar;
326
+ }
327
+ }
328
+ if (ENABLE_VERBOSE_FLOOD_FILL_LOGS) {
329
+ console.info('[cornerstone-tools] flood fill seed + effective tolerance', {
330
+ seedScalar,
331
+ effectiveMin: positiveMin,
332
+ effectiveMax: positiveMax,
333
+ });
334
+ }
335
+ const intensityGetter = (x, y, z) => {
336
+ if (x < 0 ||
337
+ x >= width ||
338
+ y < 0 ||
339
+ y >= height ||
340
+ z < 0 ||
341
+ z >= numSlices) {
342
+ return undefined;
343
+ }
344
+ const idx = z * numPixelsPerSlice + y * width + x;
345
+ return refVoxelManager.getAtIndex(idx);
346
+ };
347
+ const inRange = (val) => val >= positiveMin && val <= positiveMax;
348
+ const ensureSliceLoaded = options.ensureSliceLoaded ??
349
+ createEnsureSliceLoadedForVolume(referencedVolume);
350
+ const planar = options.planar === true;
351
+ if (planar) {
352
+ log.info('flood fill: planar mode (fixed slice index k)', { ijkStart });
353
+ }
354
+ const { sliceMasks, voxelCount: filledVoxelCount, truncated, bbox, } = await floodFill3dSliceLazy(intensityGetter, ijkStart, {
355
+ width,
356
+ height,
357
+ depth: numSlices,
358
+ equals: (val, _startVal) => val !== undefined && typeof val === 'number' && inRange(val),
359
+ ensureSliceLoaded,
360
+ yieldEvery: options.yieldEvery ?? 500,
361
+ planar,
362
+ maxDeltaK: options.maxDeltaK,
363
+ maxDeltaIJ: options.maxDeltaIJ,
364
+ isCancelled: options.isCancelled,
365
+ maxVoxels: options.maxVoxels,
366
+ shouldContinue: options.shouldContinueRegion,
367
+ });
368
+ const finalShapeRejected = !truncated &&
369
+ bbox &&
370
+ options.shouldContinueRegion &&
371
+ !options.shouldContinueRegion({ voxelCount: filledVoxelCount, bbox });
372
+ if (truncated || finalShapeRejected) {
373
+ log.warn('flood fill: rejected — region stopped by budget or shape gate; nothing committed', {
374
+ maxVoxels: options.maxVoxels,
375
+ voxelCountAtStop: filledVoxelCount,
376
+ bbox,
377
+ ijkStart,
378
+ });
379
+ options.onRejected?.({ voxelCount: filledVoxelCount, bbox });
380
+ return null;
381
+ }
382
+ if (filledVoxelCount === 0) {
383
+ log.info('flood fill: zero voxels (range may be too tight or seed isolated)', {
384
+ ijkStart,
385
+ toleranceMin: positiveMin,
386
+ toleranceMax: positiveMax,
387
+ });
388
+ return labelmap;
389
+ }
390
+ if (options.isCancelled?.() === true) {
391
+ log.info('flood fill: cancellation requested; nothing committed', {
392
+ ijkStart,
393
+ voxelCountAtCancel: filledVoxelCount,
394
+ });
395
+ return null;
396
+ }
397
+ const { floodedPoints, voxelCount: committedVoxels } = commitSliceMasksToLabelmapVolume({
398
+ labelmapVolume: labelmap,
399
+ sliceMasks,
400
+ width,
401
+ height,
402
+ paintIndex,
403
+ historyVoxelManager: options.historyVoxelManager,
404
+ });
405
+ if (committedVoxels === 0 || floodedPoints.length === 0) {
406
+ log.warn('flood fill: commit produced no labelmap voxels', {
407
+ filledVoxelCount,
408
+ committedVoxels,
409
+ ijkStart,
410
+ });
411
+ return labelmap;
412
+ }
413
+ let committedPoints = floodedPoints;
414
+ log.info('flood fill: complete', {
415
+ voxelCount: floodedPoints.length,
416
+ ijkStart,
417
+ paintIndex,
418
+ usePreview,
419
+ });
420
+ const applyExternal = options.applyExternalIslandRemoval !== false;
421
+ const applyInternal = options.applyInternalIslandRemoval !== false && applyExternal;
422
+ if (!applyExternal && !applyInternal) {
423
+ if (usePreview) {
424
+ promotePreviewSegmentToFinal(labelmapReadVm, labelmapWriteVm, paintIndex, segmentIndex, floodedPoints, numPixelsPerSlice, width);
425
+ }
426
+ options.onCommitted?.(committedPoints);
427
+ return labelmap;
428
+ }
429
+ const islandVerbose = options.islandRemovalVerboseLogging === true;
430
+ const islandRemoval = new IslandRemoval({
431
+ maxInternalRemove: options.maxInternalRemove ?? 128,
432
+ fillInternalEdge: false,
433
+ verboseLogging: islandVerbose,
434
+ });
435
+ const ijkPoints = [ijkStart];
436
+ const initialized = islandRemoval.initialize(viewport, options.historyVoxelManager ?? labelmap.voxelManager, {
437
+ points: ijkPoints,
438
+ segmentIndex,
439
+ previewSegmentIndex: usePreview ? paintIndex : segmentIndex,
440
+ });
441
+ if (!initialized) {
442
+ log.warn('island removal: initialize failed', { segmentIndex, ijkStart });
443
+ if (usePreview) {
444
+ promotePreviewSegmentToFinal(labelmapReadVm, labelmapWriteVm, paintIndex, segmentIndex, floodedPoints, numPixelsPerSlice, width);
445
+ }
446
+ options.onCommitted?.(committedPoints);
447
+ return labelmap;
448
+ }
449
+ let islandFloodVoxels = 0;
450
+ let externalClearedVoxels = 0;
451
+ let internalSliceCount;
452
+ if (applyExternal) {
453
+ timeStart(FLOOD_FILL_ISLAND_EXTERNAL_TIMING_LABEL);
454
+ islandFloodVoxels = islandRemoval.floodFillSegmentIsland();
455
+ externalClearedVoxels = islandRemoval.removeExternalIslands();
456
+ timeEnd(FLOOD_FILL_ISLAND_EXTERNAL_TIMING_LABEL);
457
+ if (applyInternal) {
458
+ timeStart(FLOOD_FILL_ISLAND_INTERNAL_TIMING_LABEL);
459
+ const modifiedSlices = islandRemoval.removeInternalIslands();
460
+ internalSliceCount = modifiedSlices?.length;
461
+ timeEnd(FLOOD_FILL_ISLAND_INTERNAL_TIMING_LABEL);
462
+ }
463
+ }
464
+ log.info('island removal: complete', {
465
+ segmentIndex,
466
+ applyExternalIslandRemoval: applyExternal,
467
+ applyInternalIslandRemoval: applyInternal,
468
+ floodedPointsBeforeIsland: floodedPoints.length,
469
+ islandFloodVoxelsFromSegmentSet: islandFloodVoxels,
470
+ externalIslandClearVoxels: externalClearedVoxels,
471
+ internalRemovalModifiedSlices: internalSliceCount,
472
+ islandRemovalVerboseLogging: islandVerbose,
473
+ });
474
+ const internalFilledPoints = applyInternal
475
+ ? islandRemoval.getInternalFilledPoints()
476
+ : [];
477
+ committedPoints =
478
+ internalFilledPoints.length > 0
479
+ ? floodedPoints.concat(internalFilledPoints)
480
+ : floodedPoints;
481
+ if (usePreview) {
482
+ promotePreviewSegmentToFinal(labelmapReadVm, labelmapWriteVm, paintIndex, segmentIndex, committedPoints, numPixelsPerSlice, width);
483
+ }
484
+ options.onCommitted?.(committedPoints);
485
+ if (ENABLE_VERBOSE_FLOOD_FILL_LOGS && floodedPoints.length > 0) {
486
+ let finalSegmentCount = 0;
487
+ let previewCount = 0;
488
+ let zeroCount = 0;
489
+ let otherCount = 0;
490
+ for (let i = 0; i < floodedPoints.length; i++) {
491
+ const [x, y, z] = floodedPoints[i];
492
+ const index = z * numPixelsPerSlice + y * width + x;
493
+ const value = Number(labelmap.voxelManager.getAtIndex(index) ?? 0);
494
+ if (value === segmentIndex) {
495
+ finalSegmentCount += 1;
496
+ }
497
+ else if (value === paintIndex) {
498
+ previewCount += 1;
499
+ }
500
+ else if (value === 0) {
501
+ zeroCount += 1;
502
+ }
503
+ else {
504
+ otherCount += 1;
505
+ }
506
+ }
507
+ console.info('[cornerstone-tools] flood fill final voxel check', {
508
+ floodedPoints: floodedPoints.length,
509
+ segmentIndex,
510
+ paintIndex,
511
+ usePreview,
512
+ finalSegmentCount,
513
+ previewCount,
514
+ zeroCount,
515
+ otherCount,
516
+ });
517
+ }
518
+ return labelmap;
519
+ }
520
+ finally {
521
+ timeEnd(FLOOD_FILL_RUN_TIMING_LABEL);
522
+ }
523
+ }
524
+ export { runFloodFillSegmentation as default, runFloodFillSegmentation, getPositiveIntensityRange, getPositiveIntensityRangeRaw, getPositiveIntensityRangeVoiMapped, getDisplayVoiSnapshot, };
@@ -14,8 +14,8 @@ export default class IslandRemoval {
14
14
  previewVoxelManager: Types.VoxelManager<number>;
15
15
  previewSegmentIndex: number;
16
16
  selectedPoints: Types.Point3[];
17
- private fillInternalEdge;
18
- private maxInternalRemove;
17
+ protected fillInternalEdge: boolean;
18
+ protected maxInternalRemove: number;
19
19
  constructor(options?: {
20
20
  maxInternalRemove?: number;
21
21
  fillInternalEdge?: boolean;
@@ -24,5 +24,6 @@ export default class IslandRemoval {
24
24
  floodFillSegmentIsland(): number;
25
25
  removeExternalIslands(): void;
26
26
  removeInternalIslands(): number[];
27
+ protected onInternalPointFilled(_point: Types.Point3): void;
27
28
  static covers(rle: any, row: any): boolean;
28
29
  }
@@ -158,10 +158,13 @@ export default class IslandRemoval {
158
158
  for (let iPrime = rle.start; iPrime < rle.end; iPrime++) {
159
159
  const clearPoint = toIJK(segmentSet.toIJK(baseIndex + iPrime));
160
160
  previewVoxelManager.setAtIJKPoint(clearPoint, previewSegmentIndex);
161
+ this.onInternalPointFilled(clearPoint);
161
162
  }
162
163
  });
163
164
  return previewVoxelManager.getArrayOfModifiedSlices();
164
165
  }
166
+ onInternalPointFilled(_point) {
167
+ }
165
168
  static covers(rle, row) {
166
169
  if (!row) {
167
170
  return false;
@@ -1 +1 @@
1
- export declare const version = "5.4.11";
1
+ export declare const version = "5.4.13";
@@ -1 +1 @@
1
- export const version = '5.4.11';
1
+ export const version = '5.4.13';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cornerstonejs/tools",
3
- "version": "5.4.11",
3
+ "version": "5.4.13",
4
4
  "description": "Cornerstone3D Tools",
5
5
  "types": "./dist/esm/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -113,7 +113,7 @@
113
113
  "build": "pnpm run build:esm",
114
114
  "clean": "rm -rf node_modules/.cache/storybook && shx rm -rf dist",
115
115
  "clean:deep": "pnpm run clean && shx rm -rf node_modules",
116
- "dev": "tsc --project ./tsconfig.json --watch",
116
+ "dev": "tsc --project ./tsconfig.json --watch --sourceMap --inlineSources",
117
117
  "lint": "oxlint .",
118
118
  "api-check": "api-extractor --debug run ",
119
119
  "prepublishOnly": "pnpm run build",
@@ -126,11 +126,11 @@
126
126
  "lodash.get": "4.4.2"
127
127
  },
128
128
  "devDependencies": {
129
- "@cornerstonejs/core": "5.4.11",
129
+ "@cornerstonejs/core": "5.4.13",
130
130
  "canvas": "3.2.0"
131
131
  },
132
132
  "peerDependencies": {
133
- "@cornerstonejs/core": "5.4.11",
133
+ "@cornerstonejs/core": "5.4.13",
134
134
  "@kitware/vtk.js": "35.5.3",
135
135
  "@types/d3-array": "3.2.1",
136
136
  "@types/d3-interpolate": "3.0.4",
@@ -149,5 +149,5 @@
149
149
  "type": "individual",
150
150
  "url": "https://ohif.org/donate"
151
151
  },
152
- "gitHead": "b6139a8e5a81a2f5a1bf5e1d1e238605d85c6d65"
152
+ "gitHead": "9f0b0f4813368e68bc0c04f02132e06f15b6a0f8"
153
153
  }