All files / tools/src/utilities/segmentation commitSliceMasksToLabelmap.ts

1.04% Statements 1/96
0% Branches 0/48
0% Functions 0/3
1.14% Lines 1/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192      128x                                                                                                                                                                                                                                                                                                                                                                                        
import { cache, utilities, type Types } from '@cornerstonejs/core';
import { FLOOD_SLICE_FLAG_VISITED } from './floodFillSliceLazy';
 
const { VoxelManager } = utilities;
 
type WritableScalarSlice = {
  length: number;
  fill: (value: number, start: number, end: number) => void;
};
 
/** Structural view of labelmap / per-slice voxel managers (avoids intersecting with private `scalarData`). */
type LabelmapCommitVm = {
  scalarData?: WritableScalarSlice | null;
  modifiedSlices: Set<number>;
  boundsIJK: Types.BoundsIJK;
  setAtIndex: (index: number, v: number) => unknown;
};
 
/**
 * Writes slice visit masks into the labelmap using **row runs** (`TypedArray.fill` per run)
 * when slice `scalarData` is available; otherwise falls back to `setAtIndex` per voxel.
 *
 * Also builds `floodedPoints` for downstream preview promotion / island removal.
 *
 * When `historyVoxelManager` is provided (an RLE history wrapper around the
 * labelmap's voxel manager), every write goes through it per-voxel so the
 * original values are recorded for undo — the dense `scalarData.fill` fast
 * paths are skipped because they would bypass the history layer.
 */
export function commitSliceMasksToLabelmapVolume({
  labelmapVolume,
  sliceMasks,
  width: w,
  height: h,
  paintIndex,
  historyVoxelManager,
}: {
  labelmapVolume: Types.IImageVolume;
  sliceMasks: Map<number, Uint8Array>;
  width: number;
  height: number;
  paintIndex: number;
  historyVoxelManager?: Types.IVoxelManager<number>;
}): { floodedPoints: Types.Point3[]; voxelCount: number } {
  const floodedPoints: Types.Point3[] = [];
  let voxelCount = 0;
 
  const vm = labelmapVolume.voxelManager as unknown as LabelmapCommitVm;
  const historyWriter = historyVoxelManager as unknown as
    | LabelmapCommitVm
    | undefined;
  const [labelmapWidth, labelmapHeight, depth] = labelmapVolume.dimensions;
  if (labelmapWidth !== w || labelmapHeight !== h) {
    throw new Error(
      `commitSliceMasksToLabelmapVolume: labelmap in-plane dimensions ` +
        `${labelmapWidth}x${labelmapHeight} do not match mask dimensions ${w}x${h}`
    );
  }
  const frameSize = w * h;
  const expectedLen = frameSize * depth;
 
  const zs = Array.from(sliceMasks.keys()).sort((a, b) => a - b);
 
  for (let zi = 0; zi < zs.length; zi++) {
    const z = zs[zi];
    if (z < 0 || z >= depth) {
      continue;
    }
    const flags = sliceMasks.get(z);
    if (!flags || flags.length !== frameSize) {
      continue;
    }
 
    let sliceTouched = false;
    let minX = Infinity;
    let minY = Infinity;
    let maxX = -Infinity;
    let maxY = -Infinity;
 
    const appendRunFlooded = (y: number, x0: number, x1: number) => {
      for (let xi = x0; xi < x1; xi++) {
        floodedPoints.push([xi, y, z]);
      }
      voxelCount += x1 - x0;
      sliceTouched = true;
      minX = Math.min(minX, x0);
      maxX = Math.max(maxX, x1 - 1);
      minY = Math.min(minY, y);
      maxY = Math.max(maxY, y);
    };
 
    // History recording requires per-voxel writes through the wrapper; the
    // run-fill fast paths write straight into scalar buffers and would leave
    // the undo map empty.
    const denseScalar =
      !historyWriter &&
      vm.scalarData &&
      vm.scalarData.length >= expectedLen &&
      typeof vm.scalarData.fill === 'function';
 
    if (denseScalar) {
      const base = z * frameSize;
      const data = vm.scalarData as WritableScalarSlice;
      for (let y = 0; y < h; y++) {
        const row = y * w;
        for (let x = 0; x < w; ) {
          const li = row + x;
          if (!(flags[li] & FLOOD_SLICE_FLAG_VISITED)) {
            x++;
            continue;
          }
          const x0 = x;
          while (x < w && flags[row + x] & FLOOD_SLICE_FLAG_VISITED) {
            x++;
          }
          data.fill(paintIndex, base + row + x0, base + row + x);
          appendRunFlooded(y, x0, x);
        }
      }
    } else {
      const imageIds = labelmapVolume.imageIds;
      const image =
        imageIds?.length && z < imageIds.length
          ? cache.getImage(imageIds[z])
          : null;
      const svm = image?.voxelManager as unknown as
        | LabelmapCommitVm
        | undefined;
 
      if (
        !historyWriter &&
        svm?.scalarData &&
        svm.scalarData.length >= frameSize &&
        typeof svm.scalarData.fill === 'function'
      ) {
        const data = svm.scalarData as WritableScalarSlice;
        for (let y = 0; y < h; y++) {
          const row = y * w;
          for (let x = 0; x < w; ) {
            const li = row + x;
            if (!(flags[li] & FLOOD_SLICE_FLAG_VISITED)) {
              x++;
              continue;
            }
            const x0 = x;
            while (x < w && flags[row + x] & FLOOD_SLICE_FLAG_VISITED) {
              x++;
            }
            data.fill(paintIndex, row + x0, row + x);
            appendRunFlooded(y, x0, x);
          }
        }
        svm.modifiedSlices.add(z);
      } else {
        const writer = historyWriter ?? vm;
        for (let y = 0; y < h; y++) {
          const row = y * w;
          for (let x = 0; x < w; x++) {
            if (!(flags[row + x] & FLOOD_SLICE_FLAG_VISITED)) {
              continue;
            }
            const index = z * frameSize + row + x;
            writer.setAtIndex(index, paintIndex);
            floodedPoints.push([x, y, z]);
            voxelCount++;
            sliceTouched = true;
            minX = Math.min(minX, x);
            maxX = Math.max(maxX, x);
            minY = Math.min(minY, y);
            maxY = Math.max(maxY, y);
          }
        }
      }
    }
 
    if (sliceTouched) {
      vm.modifiedSlices.add(z);
      if (
        Number.isFinite(minX) &&
        Number.isFinite(minY) &&
        Number.isFinite(maxX) &&
        Number.isFinite(maxY)
      ) {
        VoxelManager.addBounds(vm.boundsIJK, [minX, minY, z]);
        VoxelManager.addBounds(vm.boundsIJK, [maxX, maxY, z]);
      }
    }
  }
 
  return { floodedPoints, voxelCount };
}