copper3d 3.2.0 → 3.2.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/Utils/segmentation/DragOperator.d.ts +2 -1
  2. package/dist/Utils/segmentation/DragOperator.js +3 -1
  3. package/dist/Utils/segmentation/DragOperator.js.map +1 -1
  4. package/dist/Utils/segmentation/NrrdTools.d.ts +3 -1
  5. package/dist/Utils/segmentation/NrrdTools.js +46 -2
  6. package/dist/Utils/segmentation/NrrdTools.js.map +1 -1
  7. package/dist/Utils/segmentation/core/GaussianSmoother.d.ts +55 -0
  8. package/dist/Utils/segmentation/core/GaussianSmoother.js +210 -0
  9. package/dist/Utils/segmentation/core/GaussianSmoother.js.map +1 -0
  10. package/dist/Utils/segmentation/core/MaskVolume.d.ts +9 -0
  11. package/dist/Utils/segmentation/core/MaskVolume.js +11 -0
  12. package/dist/Utils/segmentation/core/MaskVolume.js.map +1 -1
  13. package/dist/Utils/segmentation/tools/DragSliceTool.d.ts +0 -4
  14. package/dist/Utils/segmentation/tools/DragSliceTool.js +2 -19
  15. package/dist/Utils/segmentation/tools/DragSliceTool.js.map +1 -1
  16. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js +8 -1
  17. package/dist/Utils/segmentation/tools/SliceRenderPipeline.js.map +1 -1
  18. package/dist/Utils/segmentation/tools/ToolHost.d.ts +1 -1
  19. package/dist/bundle.esm.js +281 -25
  20. package/dist/bundle.umd.js +281 -24
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.js +3 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/types/Utils/segmentation/DragOperator.d.ts +2 -1
  25. package/dist/types/Utils/segmentation/NrrdTools.d.ts +3 -1
  26. package/dist/types/Utils/segmentation/core/GaussianSmoother.d.ts +55 -0
  27. package/dist/types/Utils/segmentation/core/MaskVolume.d.ts +9 -0
  28. package/dist/types/Utils/segmentation/tools/DragSliceTool.d.ts +0 -4
  29. package/dist/types/Utils/segmentation/tools/ToolHost.d.ts +1 -1
  30. package/dist/types/index.d.ts +3 -2
  31. package/package.json +1 -1
@@ -71395,6 +71395,17 @@ void main() {
71395
71395
  getChannels() {
71396
71396
  return this.numChannels;
71397
71397
  }
71398
+ /**
71399
+ * Return the number of bytes in one complete z-slice.
71400
+ *
71401
+ * Equal to `width × height × channels`. Useful for direct buffer
71402
+ * index arithmetic in performance-critical code paths.
71403
+ *
71404
+ * @returns Bytes per z-slice.
71405
+ */
71406
+ getBytesPerSlice() {
71407
+ return this.bytesPerSlice;
71408
+ }
71398
71409
  /**
71399
71410
  * Return total memory used by the backing buffer, in bytes.
71400
71411
  *
@@ -73212,8 +73223,8 @@ void main() {
73212
73223
  }
73213
73224
  }
73214
73225
  }
73215
- // Composite all layers to master canvas
73216
- this.compositeAllLayers();
73226
+ // Composite all layers to master canvas (uses per-layer alpha)
73227
+ this.callbacks.compositeAllLayers();
73217
73228
  }
73218
73229
  // Refresh sphere overlay from volume for the new slice
73219
73230
  if (this.ctx.gui_states.mode.sphere) {
@@ -73222,23 +73233,6 @@ void main() {
73222
73233
  view.switchSliceFlag = false;
73223
73234
  }
73224
73235
  }
73225
- /**
73226
- * Composite all visible layer canvases to the master display canvas.
73227
- */
73228
- compositeAllLayers() {
73229
- const masterCtx = this.ctx.protectedData.ctxes.drawingLayerMasterCtx;
73230
- const width = this.ctx.nrrd_states.view.changedWidth;
73231
- const height = this.ctx.nrrd_states.view.changedHeight;
73232
- masterCtx.clearRect(0, 0, width, height);
73233
- // Master stores full-alpha composite; globalAlpha applied in start() render loop.
73234
- for (const layerId of this.ctx.nrrd_states.image.layers) {
73235
- if (!this.ctx.gui_states.layerChannel.layerVisibility[layerId])
73236
- continue;
73237
- const target = this.ctx.protectedData.layerTargets.get(layerId);
73238
- if (target)
73239
- masterCtx.drawImage(target.canvas, 0, 0, width, height);
73240
- }
73241
- }
73242
73236
  // ===== Canvas Cleanup =====
73243
73237
  cleanCanvases(flag) {
73244
73238
  if (flag) {
@@ -73280,7 +73274,7 @@ void main() {
73280
73274
  }
73281
73275
 
73282
73276
  class DragOperator {
73283
- constructor(container, nrrd_sates, gui_states, protectedData, drawingPrameters, setSyncsliceNum, setIsDrawFalse, flipDisplayImageByAxis, setEmptyCanvasSize, getOrCreateSliceBuffer, renderSliceToCanvas) {
73277
+ constructor(container, nrrd_sates, gui_states, protectedData, drawingPrameters, setSyncsliceNum, setIsDrawFalse, flipDisplayImageByAxis, setEmptyCanvasSize, getOrCreateSliceBuffer, renderSliceToCanvas, compositeAllLayers) {
73284
73278
  this.dragPrameters = {
73285
73279
  move: 0,
73286
73280
  y: 0,
@@ -73316,6 +73310,7 @@ void main() {
73316
73310
  this.setEmptyCanvasSize = setEmptyCanvasSize;
73317
73311
  this.getOrCreateSliceBuffer = getOrCreateSliceBuffer;
73318
73312
  this.renderSliceToCanvas = renderSliceToCanvas;
73313
+ this.compositeAllLayers = compositeAllLayers;
73319
73314
  this.showDragNumberDiv = createShowSliceNumberDiv();
73320
73315
  this.init();
73321
73316
  }
@@ -73341,6 +73336,7 @@ void main() {
73341
73336
  setEmptyCanvasSize: (axis) => this.setEmptyCanvasSize(axis),
73342
73337
  getOrCreateSliceBuffer: (axis) => this.getOrCreateSliceBuffer(axis),
73343
73338
  renderSliceToCanvas: (layer, axis, sliceIndex, buffer, targetCtx, w, h) => this.renderSliceToCanvas(layer, axis, sliceIndex, buffer, targetCtx, w, h),
73339
+ compositeAllLayers: () => this.compositeAllLayers(),
73344
73340
  refreshSphereOverlay: () => { var _a; return (_a = this.refreshSphereOverlayCb) === null || _a === void 0 ? void 0 : _a.call(this); },
73345
73341
  }, this.showDragNumberDiv, dragEffectCanvases);
73346
73342
  }
@@ -76468,6 +76464,216 @@ void main() {
76468
76464
  }
76469
76465
  }
76470
76466
 
76467
+ /**
76468
+ * GaussianSmoother — 3D Gaussian smoothing for segmentation masks.
76469
+ *
76470
+ * Applies a separable 3D Gaussian blur to a single label channel within a
76471
+ * MaskVolume, then thresholds the result back to a binary mask. This smooths
76472
+ * jagged edges and fills small holes in segmentation annotations.
76473
+ *
76474
+ * Pure, stateless utility — no DOM/Canvas/GUI dependencies.
76475
+ *
76476
+ * Performance: uses direct typed-array access (bypassing getVoxel/setVoxel
76477
+ * boundary checks) and branch-free convolution for the interior region.
76478
+ */
76479
+ class GaussianSmoother {
76480
+ /**
76481
+ * Smooth a single label channel in-place using separable 3D Gaussian blur.
76482
+ *
76483
+ * Steps:
76484
+ * 1. Extract — create a Float32Array with 1.0 where voxel === channel, 0.0 elsewhere.
76485
+ * 2. Blur — apply separable Gaussian (X → Y → Z) on the float buffer.
76486
+ * 3. Threshold — binarize at 0.5.
76487
+ * 4. Write back — overwrite/erase voxels according to the thresholded result.
76488
+ *
76489
+ * @param volume The MaskVolume to operate on (modified in-place).
76490
+ * @param channel The label value to smooth (must be > 0).
76491
+ * @param sigma Base Gaussian sigma in voxel units (default 1.0).
76492
+ * @param spacing Optional voxel spacing [sx, sy, sz]. When provided, per-axis
76493
+ * sigma is computed as `sigma / spacing[axis]` so that the
76494
+ * physical smoothing radius is isotropic.
76495
+ */
76496
+ static gaussianSmooth3D(volume, channel, sigma = 1.0, spacing) {
76497
+ const dims = volume.getDimensions();
76498
+ const { width, height, depth } = dims;
76499
+ const totalVoxels = width * height * depth;
76500
+ // Direct access to underlying buffer — bypasses getVoxel/setVoxel
76501
+ // boundary checks for ~10x speedup in hot loops.
76502
+ const rawData = volume.getRawData();
76503
+ const channels = volume.getChannels();
76504
+ const bytesPerSlice = volume.getBytesPerSlice();
76505
+ const rowStride = width * channels;
76506
+ // Compute per-axis sigma (normalized to voxel units)
76507
+ const sigmaX = spacing ? sigma / spacing[0] : sigma;
76508
+ const sigmaY = spacing ? sigma / spacing[1] : sigma;
76509
+ const sigmaZ = spacing ? sigma / spacing[2] : sigma;
76510
+ // 1. Extract binary float buffer — direct array access
76511
+ const buffer = new Float32Array(totalVoxels);
76512
+ for (let z = 0; z < depth; z++) {
76513
+ const zOffset = z * bytesPerSlice;
76514
+ for (let y = 0; y < height; y++) {
76515
+ const zyOffset = zOffset + y * rowStride;
76516
+ for (let x = 0; x < width; x++) {
76517
+ const bufIdx = z * height * width + y * width + x;
76518
+ buffer[bufIdx] = rawData[zyOffset + x * channels] === channel ? 1.0 : 0.0;
76519
+ }
76520
+ }
76521
+ }
76522
+ // 2. Separable Gaussian blur: X → Y → Z
76523
+ const kernelX = GaussianSmoother.generateKernel1D(sigmaX);
76524
+ const kernelY = GaussianSmoother.generateKernel1D(sigmaY);
76525
+ const kernelZ = GaussianSmoother.generateKernel1D(sigmaZ);
76526
+ GaussianSmoother.convolve1D(buffer, width, height, depth, 0, kernelX); // X axis
76527
+ GaussianSmoother.convolve1D(buffer, width, height, depth, 1, kernelY); // Y axis
76528
+ GaussianSmoother.convolve1D(buffer, width, height, depth, 2, kernelZ); // Z axis
76529
+ // 3 & 4. Threshold and write back — direct array access
76530
+ for (let z = 0; z < depth; z++) {
76531
+ const zOffset = z * bytesPerSlice;
76532
+ for (let y = 0; y < height; y++) {
76533
+ const zyOffset = zOffset + y * rowStride;
76534
+ for (let x = 0; x < width; x++) {
76535
+ const bufIdx = z * height * width + y * width + x;
76536
+ const smoothed = buffer[bufIdx] >= 0.5 ? 1 : 0;
76537
+ const rawIdx = zyOffset + x * channels;
76538
+ const original = rawData[rawIdx];
76539
+ if (smoothed === 1 && original !== channel) {
76540
+ // Smoothed region expands into this voxel — overwrite
76541
+ rawData[rawIdx] = channel;
76542
+ }
76543
+ else if (smoothed === 0 && original === channel) {
76544
+ // Smoothed region retreats — erase
76545
+ rawData[rawIdx] = 0;
76546
+ }
76547
+ // Otherwise leave unchanged
76548
+ }
76549
+ }
76550
+ }
76551
+ }
76552
+ /**
76553
+ * Generate a normalized 1D Gaussian kernel truncated at ±3σ.
76554
+ *
76555
+ * @param sigma Standard deviation (in voxels). Must be > 0.
76556
+ * @returns Normalized Float32Array of odd length.
76557
+ */
76558
+ static generateKernel1D(sigma) {
76559
+ if (sigma <= 0) {
76560
+ return new Float32Array([1.0]);
76561
+ }
76562
+ const radius = Math.ceil(sigma * 3);
76563
+ const size = 2 * radius + 1;
76564
+ const kernel = new Float32Array(size);
76565
+ const twoSigmaSq = 2 * sigma * sigma;
76566
+ let sum = 0;
76567
+ for (let i = 0; i < size; i++) {
76568
+ const x = i - radius;
76569
+ kernel[i] = Math.exp(-(x * x) / twoSigmaSq);
76570
+ sum += kernel[i];
76571
+ }
76572
+ // Normalize
76573
+ for (let i = 0; i < size; i++) {
76574
+ kernel[i] /= sum;
76575
+ }
76576
+ return kernel;
76577
+ }
76578
+ /**
76579
+ * In-place single-axis convolution with zero-padding at boundaries.
76580
+ *
76581
+ * Uses a 3-segment approach: left boundary (with bounds check),
76582
+ * middle interior (branch-free, ~95% of work), right boundary
76583
+ * (with bounds check). This eliminates per-element branching
76584
+ * in the hot inner loop.
76585
+ *
76586
+ * @param data Flat float buffer (width × height × depth).
76587
+ * @param width X dimension.
76588
+ * @param height Y dimension.
76589
+ * @param depth Z dimension.
76590
+ * @param axis 0 = X, 1 = Y, 2 = Z.
76591
+ * @param kernel 1D convolution kernel (odd length).
76592
+ */
76593
+ static convolve1D(data, width, height, depth, axis, kernel) {
76594
+ const kLen = kernel.length;
76595
+ const radius = (kLen - 1) / 2;
76596
+ // Determine the dimension length along the convolution axis
76597
+ // and the stride between consecutive elements along that axis
76598
+ let axisLen;
76599
+ let stride;
76600
+ let outerCount;
76601
+ if (axis === 0) {
76602
+ // X axis: stride = 1, iterate over all (y, z) lines
76603
+ axisLen = width;
76604
+ stride = 1;
76605
+ outerCount = height * depth;
76606
+ }
76607
+ else if (axis === 1) {
76608
+ // Y axis: stride = width, iterate over all (x, z) lines
76609
+ axisLen = height;
76610
+ stride = width;
76611
+ outerCount = width * depth;
76612
+ }
76613
+ else {
76614
+ // Z axis: stride = width * height, iterate over all (x, y) lines
76615
+ axisLen = depth;
76616
+ stride = width * height;
76617
+ outerCount = width * height;
76618
+ }
76619
+ // Precompute middle segment bounds
76620
+ const midStart = radius;
76621
+ const midEnd = axisLen - radius;
76622
+ // Temporary line buffer to avoid read-after-write issues
76623
+ const line = new Float32Array(axisLen);
76624
+ for (let outer = 0; outer < outerCount; outer++) {
76625
+ // Compute the starting index for this line
76626
+ let lineStart;
76627
+ if (axis === 0) {
76628
+ lineStart = outer * width;
76629
+ }
76630
+ else if (axis === 1) {
76631
+ const z = Math.floor(outer / width);
76632
+ const x = outer % width;
76633
+ lineStart = z * width * height + x;
76634
+ }
76635
+ else {
76636
+ lineStart = outer;
76637
+ }
76638
+ // Read the line
76639
+ for (let i = 0; i < axisLen; i++) {
76640
+ line[i] = data[lineStart + i * stride];
76641
+ }
76642
+ // Left boundary segment (i = 0 .. radius-1): lower bound check
76643
+ for (let i = 0; i < midStart; i++) {
76644
+ let sum = 0;
76645
+ for (let k = 0; k < kLen; k++) {
76646
+ const j = i + k - radius;
76647
+ if (j >= 0) {
76648
+ sum += line[j] * kernel[k];
76649
+ }
76650
+ }
76651
+ data[lineStart + i * stride] = sum;
76652
+ }
76653
+ // Middle segment (i = radius .. axisLen-radius-1): NO bounds check
76654
+ for (let i = midStart; i < midEnd; i++) {
76655
+ let sum = 0;
76656
+ const lineOffset = i - radius;
76657
+ for (let k = 0; k < kLen; k++) {
76658
+ sum += line[lineOffset + k] * kernel[k];
76659
+ }
76660
+ data[lineStart + i * stride] = sum;
76661
+ }
76662
+ // Right boundary segment (i = axisLen-radius .. axisLen-1): upper bound check
76663
+ for (let i = midEnd; i < axisLen; i++) {
76664
+ let sum = 0;
76665
+ for (let k = 0; k < kLen; k++) {
76666
+ const j = i + k - radius;
76667
+ if (j < axisLen) {
76668
+ sum += line[j] * kernel[k];
76669
+ }
76670
+ }
76671
+ data[lineStart + i * stride] = sum;
76672
+ }
76673
+ }
76674
+ }
76675
+ }
76676
+
76471
76677
  /**
76472
76678
  * Manages layer/channel state: active selection, visibility, and channel colors.
76473
76679
  *
@@ -76984,6 +77190,8 @@ void main() {
76984
77190
  * Then update the changedWidth and changedHeight based on the sizeFactor.
76985
77191
  */
76986
77192
  updateOriginAndChangedWH() {
77193
+ const prevW = this.ctx.nrrd_states.image.originWidth;
77194
+ const prevH = this.ctx.nrrd_states.image.originHeight;
76987
77195
  this.ctx.nrrd_states.image.originWidth =
76988
77196
  this.ctx.protectedData.canvases.originCanvas.width;
76989
77197
  this.ctx.nrrd_states.image.originHeight =
@@ -76992,7 +77200,12 @@ void main() {
76992
77200
  // Setting them here would defeat the sizeChanged detection in resizePaintArea,
76993
77201
  // causing canvas elements to keep stale dimensions after axis switches.
76994
77202
  this.resizePaintArea(this.ctx.nrrd_states.view.sizeFactor);
76995
- this.resetPaintAreaUIPosition();
77203
+ // Only re-center when origin dimensions changed (e.g. axis switch).
77204
+ // Contrast toggle doesn't change origin size, so skip to preserve user's pan/zoom.
77205
+ if (prevW !== this.ctx.nrrd_states.image.originWidth ||
77206
+ prevH !== this.ctx.nrrd_states.image.originHeight) {
77207
+ this.resetPaintAreaUIPosition();
77208
+ }
76996
77209
  }
76997
77210
  /**
76998
77211
  * Keep all contrast slice index to same.
@@ -77206,7 +77419,7 @@ void main() {
77206
77419
  // Wire RenderingUtils' setEmptyCanvasSize callback
77207
77420
  this.drawCore.renderer.setEmptyCanvasSize = (axis) => this.setEmptyCanvasSize(axis);
77208
77421
  this.init();
77209
- this.dragOperator = new DragOperator(this.container, this.state.nrrd_states, this.state.gui_states, this.state.protectedData, this.drawCore.drawingPrameters, this.setSyncsliceNum.bind(this), this.setIsDrawFalse.bind(this), this.flipDisplayImageByAxis.bind(this), this.setEmptyCanvasSize.bind(this), this.drawCore.renderer.getOrCreateSliceBuffer.bind(this.drawCore.renderer), this.drawCore.renderer.renderSliceToCanvas.bind(this.drawCore.renderer));
77422
+ this.dragOperator = new DragOperator(this.container, this.state.nrrd_states, this.state.gui_states, this.state.protectedData, this.drawCore.drawingPrameters, this.setSyncsliceNum.bind(this), this.setIsDrawFalse.bind(this), this.flipDisplayImageByAxis.bind(this), this.setEmptyCanvasSize.bind(this), this.drawCore.renderer.getOrCreateSliceBuffer.bind(this.drawCore.renderer), this.drawCore.renderer.renderSliceToCanvas.bind(this.drawCore.renderer), this.drawCore.renderer.compositeAllLayers.bind(this.drawCore.renderer));
77210
77423
  // Inject EventRouter into DragOperator for centralized event handling
77211
77424
  if (this.drawCore.eventRouter) {
77212
77425
  this.dragOperator.setEventRouter(this.drawCore.eventRouter);
@@ -77525,7 +77738,8 @@ void main() {
77525
77738
  getPencilColor() {
77526
77739
  return this.state.gui_states.drawing.color;
77527
77740
  }
77528
- executeAction(action) {
77741
+ executeAction(action, opts) {
77742
+ var _a;
77529
77743
  switch (action) {
77530
77744
  case "undo":
77531
77745
  this.undo();
@@ -77563,6 +77777,48 @@ void main() {
77563
77777
  enableDownload(config);
77564
77778
  break;
77565
77779
  }
77780
+ case "gaussianSmooth": {
77781
+ const layerId = this.state.gui_states.layerChannel.layer;
77782
+ const channel = this.state.gui_states.layerChannel.activeChannel;
77783
+ const volume = this.drawCore.renderer.getVolumeForLayer(layerId);
77784
+ if (volume && channel > 0) {
77785
+ const dims = volume.getDimensions();
77786
+ // 1. Snapshot all Z-slices before mutation (undo support)
77787
+ const deltas = [];
77788
+ for (let z = 0; z < dims.depth; z++) {
77789
+ const sliceData = volume.getSliceUint8(z, "z").data;
77790
+ const hasChannel = sliceData.some((v) => v === channel);
77791
+ if (hasChannel) {
77792
+ deltas.push({
77793
+ layerId,
77794
+ axis: "z",
77795
+ sliceIndex: z,
77796
+ oldSlice: sliceData.slice(),
77797
+ newSlice: new Uint8Array(0), // placeholder, filled after smoothing
77798
+ });
77799
+ }
77800
+ }
77801
+ // 2. Apply smoothing with anisotropic spacing
77802
+ const spacing = this.state.nrrd_states.image.voxelSpacing;
77803
+ const spacingTuple = spacing.length >= 3 ? [spacing[0], spacing[1], spacing[2]] : undefined;
77804
+ const sigma = (_a = opts === null || opts === void 0 ? void 0 : opts.sigma) !== null && _a !== void 0 ? _a : 1.0;
77805
+ GaussianSmoother.gaussianSmooth3D(volume, channel, sigma, spacingTuple);
77806
+ // 3. Capture newSlice data after smoothing and push undo group
77807
+ for (const delta of deltas) {
77808
+ delta.newSlice = volume.getSliceUint8(delta.sliceIndex, "z").data.slice();
77809
+ }
77810
+ if (deltas.length > 0) {
77811
+ this.drawCore.undoManager.pushGroup(deltas);
77812
+ }
77813
+ // 4. Notify backend for each changed slice
77814
+ for (const delta of deltas) {
77815
+ const { data: sliceData, width, height } = volume.getSliceUint8(delta.sliceIndex, "z");
77816
+ this.state.annotationCallbacks.onMaskChanged(sliceData, layerId, channel, delta.sliceIndex, "z", width, height, false);
77817
+ }
77818
+ this.reloadMasksFromVolume();
77819
+ }
77820
+ break;
77821
+ }
77566
77822
  }
77567
77823
  }
77568
77824
  // ═══════════════════════════════════════════════════════════════════════════
@@ -78277,13 +78533,14 @@ void main() {
78277
78533
  }
78278
78534
 
78279
78535
  // import * as kiwrious from "copper3d_plugin_heart_k";
78280
- const REVISION = "v3.2.0-beta";
78536
+ const REVISION = "v3.2.1-beta";
78281
78537
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
78282
78538
 
78283
78539
  exports.CHANNEL_COLORS = CHANNEL_COLORS;
78284
78540
  exports.CHANNEL_HEX_COLORS = CHANNEL_HEX_COLORS;
78285
78541
  exports.CameraViewPoint = CameraViewPoint;
78286
78542
  exports.Copper3dTrackballControls = Copper3dTrackballControls;
78543
+ exports.GaussianSmoother = GaussianSmoother;
78287
78544
  exports.MeshNodeTool = MeshNodeTool;
78288
78545
  exports.NrrdTools = NrrdTools;
78289
78546
  exports.REVISION = REVISION;
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ import { createTexture2D_NRRD } from "./Utils/texture2d";
13
13
  import { configKiwriousHeart } from "./Utils/kiwrious/configKiwrious";
14
14
  import kiwrious from "./Utils/kiwrious/configKiwrious";
15
15
  import { NrrdTools } from "./Utils/segmentation/NrrdTools";
16
+ import { GaussianSmoother } from "./Utils/segmentation/core/GaussianSmoother";
16
17
  import { Copper3dTrackballControls } from "./Controls/Copper3dTrackballControls";
17
18
  import { MeshNodeTool } from "./Utils/MeshNodeTool";
18
19
  import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
@@ -24,6 +25,6 @@ import type { ToolMode, IAnnotationCallbacks } from "./Utils/segmentation/core/t
24
25
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
25
26
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
26
27
  import "./css/style.css";
27
- export declare const REVISION = "v3.2.0-beta";
28
- export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, };
28
+ export declare const REVISION = "v3.2.1-beta";
29
+ export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, };
29
30
  export type { positionType, screenPosType, optsType, nrrdMeshesType, nrrdSliceType, SensorDecodedValue_kiwrious, SensorReadResult_kiwrious, HeartRateResult_kiwrious, loadingBarType, IPaintImage, exportPaintImageType, IOptVTKLoader, ICommXYZ, IGUIStates, IGuiParameterSettings, INrrdStates, NrrdState, GuiState, IGuiMeta, ToolMode, IAnnotationCallbacks, LayerId, ChannelValue, };
package/dist/index.js CHANGED
@@ -14,13 +14,13 @@ import { createTexture2D_NRRD } from "./Utils/texture2d";
14
14
  import { configKiwriousHeart } from "./Utils/kiwrious/configKiwrious";
15
15
  import kiwrious from "./Utils/kiwrious/configKiwrious";
16
16
  import { NrrdTools } from "./Utils/segmentation/NrrdTools";
17
- // Phase 7: Segmentation Module - Unified exports
17
+ import { GaussianSmoother } from "./Utils/segmentation/core/GaussianSmoother";
18
18
  import { Copper3dTrackballControls } from "./Controls/Copper3dTrackballControls";
19
19
  import { MeshNodeTool } from "./Utils/MeshNodeTool";
20
20
  import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
21
21
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
22
22
  import "./css/style.css";
23
- export const REVISION = "v3.2.0-beta";
23
+ export const REVISION = "v3.2.1-beta";
24
24
  console.log(`%cCopper3D Visualisation %cBeta:${REVISION}`, "padding: 3px;color:white; background:#023047", "padding: 3px;color:white; background:#f50a25");
25
- export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, };
25
+ export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, };
26
26
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EACL,eAAe,EAGf,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAY,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,QAAQ,MAAM,iCAAiC,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,iDAAiD;AAEjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAiB5E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAG3G,OAAO,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC;AAEtC,OAAO,CAAC,GAAG,CACT,mCAAmC,QAAQ,EAAE,EAC7C,8CAA8C,EAC9C,8CAA8C,CAC/C,CAAC;AAEF,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,SAAS,EACT,OAAO,EACP,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,QAAQ,EACR,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,SAAS,EACT,SAAS,GACV,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EACL,eAAe,EAGf,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAY,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,QAAQ,MAAM,iCAAiC,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;AAE9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAiB5E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAG3G,OAAO,iBAAiB,CAAC;AAEzB,MAAM,CAAC,MAAM,QAAQ,GAAG,aAAa,CAAC;AAEtC,OAAO,CAAC,GAAG,CACT,mCAAmC,QAAQ,EAAE,EAC7C,8CAA8C,EAC9C,8CAA8C,CAC/C,CAAC;AAEF,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,SAAS,EACT,OAAO,EACP,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,QAAQ,EACR,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,SAAS,EACT,SAAS,EACT,gBAAgB,GACjB,CAAC"}
@@ -18,9 +18,10 @@ export declare class DragOperator {
18
18
  private setEmptyCanvasSize;
19
19
  private getOrCreateSliceBuffer;
20
20
  private renderSliceToCanvas;
21
+ private compositeAllLayers;
21
22
  private refreshSphereOverlayCb;
22
23
  private eventRouter;
23
- constructor(container: HTMLElement, nrrd_sates: NrrdState, gui_states: GuiState, protectedData: IProtected, drawingPrameters: IDrawingEvents, setSyncsliceNum: () => void, setIsDrawFalse: (target: number) => void, flipDisplayImageByAxis: () => void, setEmptyCanvasSize: (axis?: "x" | "y" | "z") => void, getOrCreateSliceBuffer: (axis: "x" | "y" | "z") => ImageData | null, renderSliceToCanvas: (layer: string, axis: "x" | "y" | "z", sliceIndex: number, buffer: ImageData, targetCtx: CanvasRenderingContext2D, scaledWidth: number, scaledHeight: number) => void);
24
+ constructor(container: HTMLElement, nrrd_sates: NrrdState, gui_states: GuiState, protectedData: IProtected, drawingPrameters: IDrawingEvents, setSyncsliceNum: () => void, setIsDrawFalse: (target: number) => void, flipDisplayImageByAxis: () => void, setEmptyCanvasSize: (axis?: "x" | "y" | "z") => void, getOrCreateSliceBuffer: (axis: "x" | "y" | "z") => ImageData | null, renderSliceToCanvas: (layer: string, axis: "x" | "y" | "z", sliceIndex: number, buffer: ImageData, targetCtx: CanvasRenderingContext2D, scaledWidth: number, scaledHeight: number) => void, compositeAllLayers: () => void);
24
25
  private init;
25
26
  setShowDragNumberDiv(sliceIndexContainer: HTMLDivElement): void;
26
27
  /**
@@ -88,7 +88,9 @@ export declare class NrrdTools {
88
88
  getSliderMeta(key: string): IGuiMeta | null;
89
89
  setPencilColor(hex: string): void;
90
90
  getPencilColor(): string;
91
- executeAction(action: "undo" | "redo" | "clearActiveSliceMask" | "clearActiveLayerMask" | "resetZoom" | "downloadCurrentMask"): void;
91
+ executeAction(action: "undo" | "redo" | "clearActiveSliceMask" | "clearActiveLayerMask" | "resetZoom" | "downloadCurrentMask" | "gaussianSmooth", opts?: {
92
+ sigma?: number;
93
+ }): void;
92
94
  undo(): void;
93
95
  redo(): void;
94
96
  enterKeyboardConfig(): void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GaussianSmoother — 3D Gaussian smoothing for segmentation masks.
3
+ *
4
+ * Applies a separable 3D Gaussian blur to a single label channel within a
5
+ * MaskVolume, then thresholds the result back to a binary mask. This smooths
6
+ * jagged edges and fills small holes in segmentation annotations.
7
+ *
8
+ * Pure, stateless utility — no DOM/Canvas/GUI dependencies.
9
+ *
10
+ * Performance: uses direct typed-array access (bypassing getVoxel/setVoxel
11
+ * boundary checks) and branch-free convolution for the interior region.
12
+ */
13
+ import type { MaskVolume } from './MaskVolume';
14
+ export declare class GaussianSmoother {
15
+ /**
16
+ * Smooth a single label channel in-place using separable 3D Gaussian blur.
17
+ *
18
+ * Steps:
19
+ * 1. Extract — create a Float32Array with 1.0 where voxel === channel, 0.0 elsewhere.
20
+ * 2. Blur — apply separable Gaussian (X → Y → Z) on the float buffer.
21
+ * 3. Threshold — binarize at 0.5.
22
+ * 4. Write back — overwrite/erase voxels according to the thresholded result.
23
+ *
24
+ * @param volume The MaskVolume to operate on (modified in-place).
25
+ * @param channel The label value to smooth (must be > 0).
26
+ * @param sigma Base Gaussian sigma in voxel units (default 1.0).
27
+ * @param spacing Optional voxel spacing [sx, sy, sz]. When provided, per-axis
28
+ * sigma is computed as `sigma / spacing[axis]` so that the
29
+ * physical smoothing radius is isotropic.
30
+ */
31
+ static gaussianSmooth3D(volume: MaskVolume, channel: number, sigma?: number, spacing?: [number, number, number]): void;
32
+ /**
33
+ * Generate a normalized 1D Gaussian kernel truncated at ±3σ.
34
+ *
35
+ * @param sigma Standard deviation (in voxels). Must be > 0.
36
+ * @returns Normalized Float32Array of odd length.
37
+ */
38
+ static generateKernel1D(sigma: number): Float32Array;
39
+ /**
40
+ * In-place single-axis convolution with zero-padding at boundaries.
41
+ *
42
+ * Uses a 3-segment approach: left boundary (with bounds check),
43
+ * middle interior (branch-free, ~95% of work), right boundary
44
+ * (with bounds check). This eliminates per-element branching
45
+ * in the hot inner loop.
46
+ *
47
+ * @param data Flat float buffer (width × height × depth).
48
+ * @param width X dimension.
49
+ * @param height Y dimension.
50
+ * @param depth Z dimension.
51
+ * @param axis 0 = X, 1 = Y, 2 = Z.
52
+ * @param kernel 1D convolution kernel (odd length).
53
+ */
54
+ private static convolve1D;
55
+ }
@@ -250,6 +250,15 @@ export declare class MaskVolume {
250
250
  * @returns Channel count (≥ 1).
251
251
  */
252
252
  getChannels(): number;
253
+ /**
254
+ * Return the number of bytes in one complete z-slice.
255
+ *
256
+ * Equal to `width × height × channels`. Useful for direct buffer
257
+ * index arithmetic in performance-critical code paths.
258
+ *
259
+ * @returns Bytes per z-slice.
260
+ */
261
+ getBytesPerSlice(): number;
253
262
  /**
254
263
  * Return total memory used by the backing buffer, in bytes.
255
264
  *
@@ -26,10 +26,6 @@ export declare class DragSliceTool extends BaseTool {
26
26
  setShowDragNumberDiv(div: HTMLDivElement): void;
27
27
  updateIndex(move: number): void;
28
28
  private drawDragSlice;
29
- /**
30
- * Composite all visible layer canvases to the master display canvas.
31
- */
32
- private compositeAllLayers;
33
29
  private cleanCanvases;
34
30
  updateShowNumDiv(contrastNum: number): void;
35
31
  updateCurrentContrastSlice(): any;
@@ -66,7 +66,7 @@ export type SphereHostDeps = Pick<ToolHost, 'setEmptyCanvasSize' | 'drawImageOnE
66
66
  /** DrawingTool host dependencies */
67
67
  export type DrawingHostDeps = Pick<ToolHost, 'setCurrentLayer' | 'compositeAllLayers' | 'syncLayerSliceData' | 'filterDrawedImage' | 'getVolumeForLayer' | 'pushUndoDelta' | 'getEraserUrls'>;
68
68
  /** DragSliceTool host dependencies */
69
- export type DragSliceHostDeps = Pick<ToolHost, 'setSyncsliceNum' | 'setIsDrawFalse' | 'flipDisplayImageByAxis' | 'setEmptyCanvasSize' | 'getOrCreateSliceBuffer' | 'renderSliceToCanvas' | 'refreshSphereOverlay'>;
69
+ export type DragSliceHostDeps = Pick<ToolHost, 'setSyncsliceNum' | 'setIsDrawFalse' | 'flipDisplayImageByAxis' | 'setEmptyCanvasSize' | 'getOrCreateSliceBuffer' | 'renderSliceToCanvas' | 'refreshSphereOverlay' | 'compositeAllLayers'>;
70
70
  /** LayerChannelManager host dependencies */
71
71
  export type LayerChannelHostDeps = Pick<ToolHost, 'reloadMasksFromVolume' | 'getVolumeForLayer' | 'onChannelColorChanged'>;
72
72
  /** SliceRenderPipeline host dependencies */
@@ -13,6 +13,7 @@ import { createTexture2D_NRRD } from "./Utils/texture2d";
13
13
  import { configKiwriousHeart } from "./Utils/kiwrious/configKiwrious";
14
14
  import kiwrious from "./Utils/kiwrious/configKiwrious";
15
15
  import { NrrdTools } from "./Utils/segmentation/NrrdTools";
16
+ import { GaussianSmoother } from "./Utils/segmentation/core/GaussianSmoother";
16
17
  import { Copper3dTrackballControls } from "./Controls/Copper3dTrackballControls";
17
18
  import { MeshNodeTool } from "./Utils/MeshNodeTool";
18
19
  import { removeGuiFolderChilden } from "./Utils/segmentation/coreTools/gui";
@@ -24,6 +25,6 @@ import type { ToolMode, IAnnotationCallbacks } from "./Utils/segmentation/core/t
24
25
  import { CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss } from "./Utils/segmentation/core/index";
25
26
  import type { LayerId, ChannelValue } from "./Utils/segmentation/core/index";
26
27
  import "./css/style.css";
27
- export declare const REVISION = "v3.2.0-beta";
28
- export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, };
28
+ export declare const REVISION = "v3.2.1-beta";
29
+ export { copperRenderer, copperRendererOnDemond, copperMSceneRenderer, setHDRFilePath, addLabelToScene, convert3DPostoScreenPos, convertScreenPosto3DPos, addBoxHelper, fullScreenListenner, configKiwriousHeart, copperScene, copperSceneOnDemond, copperMScene, CameraViewPoint, kiwrious, NrrdTools, loading, Copper3dTrackballControls, createTexture2D_NRRD, MeshNodeTool, throttle, removeGuiFolderChilden, CHANNEL_COLORS, CHANNEL_HEX_COLORS, rgbaToHex, rgbaToCss, GaussianSmoother, };
29
30
  export type { positionType, screenPosType, optsType, nrrdMeshesType, nrrdSliceType, SensorDecodedValue_kiwrious, SensorReadResult_kiwrious, HeartRateResult_kiwrious, loadingBarType, IPaintImage, exportPaintImageType, IOptVTKLoader, ICommXYZ, IGUIStates, IGuiParameterSettings, INrrdStates, NrrdState, GuiState, IGuiMeta, ToolMode, IAnnotationCallbacks, LayerId, ChannelValue, };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "copper3d",
3
3
  "description": "A 3d visualisation package base on threejs provides multiple scenes and Nrrd image load funtion.",
4
- "version": "3.2.0",
4
+ "version": "3.2.1",
5
5
  "main": "dist/bundle.umd.js",
6
6
  "moudle": "dist/bundle.esm.js",
7
7
  "types": "dist/types/index.d.ts",