--- a
+++ b/tensor-utils.js
@@ -0,0 +1,613 @@
+import * as tf from '@tensorflow/tfjs'
+import { BWLabeler } from './bwlabels.js'
+
+export async function addZeroPaddingTo3dTensor(tensor3d, rowPadArr = [1, 1], colPadArr = [1, 1], depthPadArr = [1, 1]) {
+  if (tensor3d.rank !== 3) {
+    throw new Error('Tensor must be 3D')
+  }
+  return tensor3d.pad([rowPadArr, colPadArr, depthPadArr])
+}
+
+export async function applyMriThreshold(tensor, percentage) {
+  // Perform asynchronous operations outside of tf.tidy
+  const maxTensor = tensor.max()
+  const thresholdTensor = maxTensor.mul(percentage)
+  const threshold = await thresholdTensor.data() // Extracts the threshold value
+
+  // Dispose tensors not needed anymore
+  maxTensor.dispose()
+  thresholdTensor.dispose()
+
+  // Use tf.tidy for synchronous operations
+  return tf.tidy(() => {
+    const dataForProcessing = tensor.clone()
+
+    // Thresholding (assuming background has very low values compared to the head)
+    const mask = dataForProcessing.greater(threshold[0])
+    // -- const denoisedMriData = dataForProcessing.mul(mask)
+
+    // No need to  manually dispose dataForProcessing and mask, as tf.tidy() will dispose them auto.
+    return mask
+  })
+
+  // -- return denoisedMriData
+}
+
+export async function binarizeVolumeDataTensor(volumeDataTensor) {
+  const alpha = 0
+  // element-wise: (x > 0 ? 1 : alpha * x );  e.g. Tenosr [0, 0.9, 0.8, -3] => Tensor [0, 1, 1, 0]
+  return volumeDataTensor.step(alpha)
+}
+
+async function calculateQuantiles(tensor, lowerQuantile = 0.01, upperQuantile = 0.99) {
+  // Flatten the tensor
+  const flatTensor = tensor.flatten()
+
+  // Convert the flattened tensor to an array to sort it
+  const flatArray = await flatTensor.array()
+  flatArray.sort((a, b) => a - b) // Sort the array in ascending order
+
+  // Convert the sorted array back to a tensor
+  const sortedTensor = tf.tensor1d(flatArray)
+
+  // Calculate the indices for the quantiles
+  const numElements = sortedTensor.shape[0]
+  const lowIndex = Math.floor(numElements * lowerQuantile)
+  const highIndex = Math.ceil(numElements * upperQuantile) - 1 // Subtract 1 because indices are 0-based
+
+  // Slice the sorted tensor to get qmin and qmax
+  const qmin = sortedTensor.slice(lowIndex, 1) // Get the value at the low index
+  const qmax = sortedTensor.slice(highIndex, 1) // Get the value at the high index
+
+  // Get the actual values from the tensors
+  const qminValue = (await qmin.array())[0]
+  const qmaxValue = (await qmax.array())[0]
+
+  // Clean up tensors to free memory
+  flatTensor.dispose()
+  sortedTensor.dispose()
+  qmin.dispose()
+  qmax.dispose()
+
+  return { qmin: qminValue, qmax: qmaxValue }
+}
+
+export async function convByOutputChannelAndInputSlicing(input, filter, biases, stride, pad, dilationRate, sliceSize) {
+  const inChannels = input.shape[4]
+  const outChannels = filter.shape[4]
+
+  // Create an empty array to hold the output channels
+  let outputChannels = null
+
+  // Slice the input tensor and process one output channel at a time
+  for (let channel = 0; channel < outChannels; channel++) {
+    const numSlices = Math.ceil(inChannels / sliceSize)
+    const biasesSlice = biases.slice([channel], [1])
+    let outputChannel = null
+
+    for (let i = 0; i < numSlices; i++) {
+      const startChannel = i * sliceSize
+      const endChannel = Math.min((i + 1) * sliceSize, inChannels)
+
+      // Only proceed if there are channels to process
+      if (startChannel < inChannels) {
+        const resultSlice = tf.tidy(() => {
+          const inputSlice = input.slice([0, 0, 0, 0, startChannel], [-1, -1, -1, -1, endChannel - startChannel])
+          const filterSlice = filter.slice([0, 0, 0, startChannel, channel], [-1, -1, -1, endChannel - startChannel, 1])
+          // Perform the convolution for the current slice and output channel
+          return tf.conv3d(inputSlice, filterSlice, stride, pad, 'NDHWC', dilationRate)
+        })
+
+        if (outputChannel === null) {
+          outputChannel = resultSlice
+        } else {
+          const updatedOutputChannel = outputChannel.add(resultSlice)
+          outputChannel.dispose()
+          resultSlice.dispose()
+          outputChannel = updatedOutputChannel
+        }
+      }
+    }
+
+    // Add the biases to the accumulated convolutions for this channel
+    const biasedOutputChannel = outputChannel.add(biasesSlice)
+    outputChannel.dispose()
+    biasesSlice.dispose()
+
+    // Accumulate the channel to the output array
+    if (outputChannels == null) {
+      outputChannels = biasedOutputChannel
+    } else {
+      const updatedOutputChannels = await tf.concat([outputChannels, biasedOutputChannel], 4)
+      biasedOutputChannel.dispose()
+      outputChannels.dispose()
+      outputChannels = updatedOutputChannels
+    }
+  }
+
+  return outputChannels
+}
+
+export async function draw3dObjBoundingVolume(unstackOutVolumeTensor, opts, modelEntry, callbackImg) {
+  const allOutputSlices3DCC = []
+
+  // dataSync() using to flatten array. Takes around 1.5 s
+  for (let sliceTensorIdx = 0; sliceTensorIdx < unstackOutVolumeTensor.length; sliceTensorIdx++) {
+    allOutputSlices3DCC[sliceTensorIdx] = Array.from(unstackOutVolumeTensor[sliceTensorIdx].dataSync())
+  }
+
+  // Use this conversion to download output slices as nii file. Takes around 30 ms
+  // does not use `push` to avoid stack overflows. In future: consider .set() with typed arrays
+  const allOutputSlices3DCC1DimArray = new Array(allOutputSlices3DCC[0].length * allOutputSlices3DCC.length)
+  let index = 0
+  for (let sliceIdx = 0; sliceIdx < allOutputSlices3DCC.length; sliceIdx++) {
+    for (let i = 0; i < allOutputSlices3DCC[sliceIdx].length; i++) {
+      allOutputSlices3DCC1DimArray[index++] = allOutputSlices3DCC[sliceIdx][i]
+    }
+  }
+  console.log('Done with allOutputSlices3DCC1DimArray ')
+  const brainMaskTensor1d = await binarizeVolumeDataTensor(tf.tensor1d(allOutputSlices3DCC1DimArray))
+  const brainOut = Array.from(brainMaskTensor1d.dataSync())
+  callbackImg(brainOut, opts, modelEntry)
+}
+// return first and last non-zero voxel in row (dim = 0), column (1) or slice (2) dimension
+async function firstLastNonZero(tensor3D, dim = 0) {
+  let mxs = []
+  if (dim === 0) {
+    mxs = await tensor3D.max(2).max(1).arraySync()
+  } else if (dim === 1) {
+    mxs = await tensor3D.max(2).max(0).arraySync()
+  } else {
+    mxs = await tensor3D.max(1).max(0).arraySync()
+  }
+  let mn = mxs.length
+  let mx = 0
+  for (let i = 0; i < mxs.length; i++) {
+    if (mxs[i] > 0) {
+      mn = i
+      break
+    }
+  }
+  for (let i = mxs.length - 1; i >= 0; i--) {
+    if (mxs[i] > 0) {
+      mx = i
+      break
+    }
+  }
+  return [mn, mx]
+}
+
+export async function firstLastNonZero3D(tensor3D) {
+  const [row_min, row_max] = await firstLastNonZero(tensor3D, 0)
+  const [col_min, col_max] = await firstLastNonZero(tensor3D, 1)
+  const [depth_min, depth_max] = await firstLastNonZero(tensor3D, 2)
+  console.log('row min and max  :', row_min, row_max)
+  console.log('col min and max  :', col_min, col_max)
+  console.log('depth min and max  :', depth_min, depth_max)
+  return [row_min, row_max, col_min, col_max, depth_min, depth_max]
+}
+
+/*
+//simpler function, but x4 slower
+export async function firstLastNonZero3D(tensor3D) {
+  const coords = await tf.whereAsync(tensor3D)
+  const row_min = coords.min(0).arraySync()[0]
+  const row_max = coords.max(0).arraySync()[0]
+  const col_min = coords.min(0).arraySync()[1]
+  const col_max = coords.max(0).arraySync()[1]
+  const depth_min = coords.min(0).arraySync()[2]
+  const depth_max = coords.max(0).arraySync()[2]
+  coords.dispose()
+  return [row_min, row_max, col_min, col_max, depth_min, depth_max]
+}
+*/
+
+export async function generateBrainMask(
+  unstackOutVolumeTensor,
+  num_of_slices,
+  slice_height,
+  slice_width,
+  modelEntry,
+  opts,
+  callbackUI,
+  callbackImg,
+  isFinalImage = true
+) {
+  if (unstackOutVolumeTensor[0].dtype !== 'int32') {
+    callbackUI('', -1, 'generateBrainMask assumes int32')
+  }
+  if (modelEntry.preModelPostProcess) {
+    callbackUI('', -1, 'generateBrainMask assumes BWLabeler instead of preModelPostProcess')
+  }
+  const numSlices = unstackOutVolumeTensor.length
+  const numPixels2D = unstackOutVolumeTensor[0].size
+  const numVox3D = numSlices * numPixels2D
+  // preallocate to reduce heap usage
+  const brainOut = new Int32Array(numVox3D)
+  let offset = 0
+  for (let i = 0; i < numSlices; i++) {
+    brainOut.set(unstackOutVolumeTensor[i].dataSync(), offset)
+    offset += numPixels2D
+  }
+  for (let i = 0; i < numVox3D; i++) {
+    brainOut[i] = brainOut[i] !== 0 ? 1 : 0
+  }
+  if (isFinalImage || opts.showPhase1Output) {
+    // all done
+    callbackImg(brainOut, opts, modelEntry)
+    callbackUI('Segmentation finished', 0)
+  }
+  return tf.tensor(brainOut, [num_of_slices, slice_height, slice_width])
+}
+
+export async function generateOutputSlicesV2(
+  img,
+  OutVolumeTensorShape,
+  OutVolumeTensorType,
+  num_of_slices,
+  numSegClasses,
+  slice_height,
+  slice_width,
+  modelEntry,
+  opts,
+  niftiImage
+) {
+  // Convert all slices into 1 Dim array
+  if (opts.isPostProcessEnable) {
+    const BWInstance = new BWLabeler()
+    const dim = new Uint32Array(OutVolumeTensorShape)
+    const conn = 26 // Example connectivity
+    const binarize = true
+    const onlyLargestClusterPerClass = true
+    const [_labelCount, labeledImage] = BWInstance.bwlabel(img, dim, conn, binarize, onlyLargestClusterPerClass)
+    for (let i = 0; i < img.length; i++) {
+      img[i] *= labeledImage[i]
+    }
+  } // if isPostProcessEnable
+  const typedArrayConstructor = {
+    float32: Float32Array,
+    int32: Int32Array
+    // Add other cases as needed for different dtypes
+  }[OutVolumeTensorType]
+  // Create a new TypedArray from img with the same type as outLabelVolume
+  const allOutputSlices3DCC1DimArray = new Uint8Array(img)
+  switch (modelEntry.type) {
+    case 'Brain_Masking': {
+      const brainMask = new Uint8Array(allOutputSlices3DCC1DimArray.length)
+      for (let i = 0; i < allOutputSlices3DCC1DimArray.length; i++) {
+        brainMask[i] = allOutputSlices3DCC1DimArray[i] !== 0 ? 1 : 0
+      }
+      return brainMask
+    }
+    case 'Brain_Extraction': {
+      const maskedData = new Uint8Array(allOutputSlices3DCC1DimArray.length)
+      for (let i = 0; i < allOutputSlices3DCC1DimArray.length; i++) {
+        // Create the mask - 1 where the value is non-zero, 0 where it is zero.
+        const maskValue = allOutputSlices3DCC1DimArray[i] !== 0 ? 1 : 0
+        // Apply the mask to the data - multiply by the mask value.
+        maskedData[i] = niftiImage[i] * maskValue
+      }
+      return maskedData
+    }
+  }
+  return img
+}
+
+export async function getAllSlicesDataAsTF3D(num_of_slices, niftiHeader, niftiImage) {
+  // Get nifti dimensions
+  const cols = niftiHeader.dims[1] // Slice width
+  const rows = niftiHeader.dims[2] // Slice height
+  let typedData
+  if (niftiHeader.datatypeCode === 2) {
+    // enum from nvimage/utils DT_UINT8 = 2
+    typedData = new Uint8Array(niftiImage)
+  } else if (niftiHeader.datatypeCode === 4) {
+    // DT_INT16 = 4
+    typedData = new Int16Array(niftiImage)
+  } else if (niftiHeader.datatypeCode === 8) {
+    // DT_INT32 = 8
+    typedData = new Int32Array(niftiImage)
+  } else if (niftiHeader.datatypeCode === 16) {
+    // DT_FLOAT32 = 16
+    typedData = new Float32Array(niftiImage)
+  } else if (niftiHeader.datatypeCode === 64) {
+    // DT_FLOAT64 = 64
+    typedData = new Float64Array(niftiImage)
+  } else if (niftiHeader.datatypeCode === 256) {
+    // DT_INT8 = 256
+    typedData = new Int8Array(niftiImage)
+  } else if (niftiHeader.datatypeCode === 512) {
+    // DT_UINT16 = 512
+    typedData = new Uint16Array(niftiImage)
+  } else if (niftiHeader.datatypeCode === 768) {
+    // DT_UINT32 = 768
+    typedData = new Uint32Array(niftiImage)
+  } else {
+    return
+  }
+  const allSlices_2D = []
+  let offset3D = 0
+  // Draw pixels
+  for (let slice = 0; slice < num_of_slices; slice++) {
+    const slice = new Array(rows * cols)
+    let offset2D = 0
+    for (let row = 0; row < rows; row++) {
+      for (let col = 0; col < cols; col++) {
+        const value = typedData[offset3D++]
+        // Create 1Dim Array of pixel value, this 1 dim represents one channel
+        slice[offset2D++] = value & 0xff
+      }
+    }
+    allSlices_2D.push(tf.tensor(slice, [rows, cols])) // slice_height, slice_width
+  }
+  const allSlices_3D = tf.stack(allSlices_2D)
+  tf.dispose(allSlices_2D)
+  return allSlices_3D
+}
+
+export async function getModelNumLayers(modelObj) {
+  return modelObj.layers.length
+}
+
+export async function getModelNumParameters(modelObj) {
+  let numParameters = 0
+  for (let layerIdx = 0; layerIdx < modelObj.layers.length; layerIdx++) {
+    numParameters += modelObj.layers[layerIdx].countParams()
+  }
+  return numParameters
+}
+
+export async function isModelChnlLast(modelObj) {
+  for (let layerIdx = 0; layerIdx < modelObj.layers.length; layerIdx++) {
+    if (modelObj.layersByDepth[layerIdx][0].dataFormat) {
+      return modelObj.layersByDepth[layerIdx][0].dataFormat === 'channelsLast'
+    }
+  }
+}
+
+export async function load_model(modelUrl) {
+  return await tf.loadLayersModel(modelUrl)
+}
+
+export async function minMaxNormalizeVolumeData(volumeData) {
+  // Normalize the data to the range 0 - 1 using min-max scaling
+  const volumeData_Max = volumeData.max()
+  const volumeData_Min = volumeData.min()
+  const normalizedSlices_3d = await volumeData.sub(volumeData_Min).div(volumeData_Max.sub(volumeData_Min))
+  return normalizedSlices_3d
+}
+
+function processTensorInChunks(inputTensor, filterWeights, chunkSize) {
+  // Assuming inputTensor's shape: [batch, depth, height, width, inChannels]
+  // and filterWeights's shape: [filterDepth, filterHeight, filterWidth, inChannels, outChannels]
+  const stride = 1
+  const pad = 0
+  const dilationRate = 1
+  const inChannels = inputTensor.shape[4]
+  const numSlices = Math.ceil(inChannels / chunkSize)
+
+  let accumulatedResult = null
+  for (let i = 0; i < numSlices; i++) {
+    const startChannel = i * chunkSize
+    const endChannel = Math.min((i + 1) * chunkSize, inChannels)
+    const channels = endChannel - startChannel
+
+    const inputSlice = tf.tidy(() => {
+      // Slice the input tensor to get the current chunk
+      return inputTensor.slice([0, 0, 0, 0, startChannel], [-1, -1, -1, -1, channels])
+    })
+    const filterSlice = tf.tidy(() => {
+      // Slice the filter weights to match the input tensor's current chunk
+      return filterWeights.slice([0, 0, 0, startChannel, 0], [-1, -1, -1, channels, -1])
+    })
+
+    const resultSlice = tf.conv3d(inputSlice, filterSlice, stride, pad, 'NDHWC', dilationRate)
+    // Clean up the slices to free memory
+    inputSlice.dispose()
+    filterSlice.dispose()
+
+    // Squeeze the result slice to remove dimensions of size 1
+    const squeezedResultSlice = tf.squeeze(resultSlice)
+    resultSlice.dispose() // Dispose of the original resultSlice after squeezing
+
+    if (accumulatedResult === null) {
+      accumulatedResult = squeezedResultSlice
+    } else {
+      // Accumulate the result by adding the new result slice to it
+      const newAccumulatedResult = accumulatedResult.add(squeezedResultSlice)
+
+      // Dispose of the previous accumulatedResult and squeezedResultSlice
+      accumulatedResult.dispose()
+      // Dispose of squeezedResultSlice only if it wasn't assigned to accumulatedResult
+      if (accumulatedResult !== squeezedResultSlice) {
+        squeezedResultSlice.dispose()
+      }
+      // Update accumulatedResult with the new result
+      accumulatedResult = newAccumulatedResult
+    }
+
+    tf.tidy(() => {
+      tf.matMul(tf.zeros([1, 1]), tf.zeros([1, 1]))
+    })
+  }
+
+  return accumulatedResult
+}
+
+export async function quantileNormalizeVolumeData(tensor, lowerQuantile = 0.05, upperQuantile = 0.95) {
+  // Call calculateQuantiles and wait for the result
+  const { qmin, qmax } = await calculateQuantiles(tensor, lowerQuantile, upperQuantile)
+
+  // Convert qmin and qmax back to scalars
+  const qminScalar = tf.scalar(qmin)
+  const qmaxScalar = tf.scalar(qmax)
+
+  // Perform the operation: (tensor - qmin) / (qmax - qmin)
+  const resultTensor = tensor.sub(qminScalar).div(qmaxScalar.sub(qminScalar))
+
+  // Dispose of the created scalars to free memory
+  qminScalar.dispose()
+  qmaxScalar.dispose()
+
+  // Return the resulting tensor
+  return resultTensor
+}
+
+export async function removeZeroPaddingFrom3dTensor(tensor3d, rowPad = 1, colPad = 1, depthPad = 1) {
+  if (tensor3d.rank !== 3) {
+    throw new Error('Tensor must be 3D')
+  }
+  const [h, w, d] = tensor3d.shape
+  return tensor3d.slice([rowPad, colPad, depthPad], [h - 2 * rowPad, w - 2 * colPad, d - 2 * depthPad])
+}
+
+export async function resizeWithZeroPadding(croppedTensor3d, newDepth, newHeight, newWidth, refVoxel, boundVolSizeArr) {
+  const row_pad_befor = refVoxel[0]
+  const col_pad_befor = refVoxel[1]
+  const depth_pad_befor = refVoxel[2]
+  // last and lower volume voxel
+  const row_max = row_pad_befor + boundVolSizeArr[0] - 1 // size [2, 2, 2] means 2 voxels total in each dim
+  const col_max = col_pad_befor + boundVolSizeArr[1] - 1
+  const depth_max = depth_pad_befor + boundVolSizeArr[2] - 1
+
+  const row_pad_after = newHeight - row_max - 1 > 0 ? newHeight - row_max - 1 : 0
+  const col_pad_after = newWidth - col_max - 1 > 0 ? newWidth - col_max - 1 : 0
+  const depth_pad_after = newDepth - depth_max - 1 > 0 ? newDepth - depth_max - 1 : 0
+
+  return croppedTensor3d.pad([
+    [row_pad_befor, row_pad_after],
+    [col_pad_befor, col_pad_after],
+    [depth_pad_befor, depth_pad_after]
+  ])
+}
+
+export class SequentialConvLayer {
+  constructor(model, chunkSize, isChannelLast, callbackUI, isWebWorker = true) {
+    this.model = model
+    this.outChannels = model.outputLayers[0].kernel.shape[4]
+    this.chunkSize = chunkSize
+    this.isChannelLast = isChannelLast
+    this.callbackUI = callbackUI
+    this.isWebWorker = isWebWorker
+  }
+
+  /**
+   * Apply sequential convolution layer
+   * @since 3.0.0
+   * @member SequentialConvLayer
+   * @param {tf.Tensor}  inputTensor  e.g.  [ 1, 256, 256, 256, 5 ]
+   * @return {outC}
+   */
+
+  async apply(inputTensor) {
+    const oldDeleteTextureThreshold = tf.ENV.get('WEBGL_DELETE_TEXTURE_THRESHOLD')
+    tf.ENV.set('WEBGL_DELETE_TEXTURE_THRESHOLD', 0)
+
+    // eslint-disable-next-line @typescript-eslint/no-this-alias
+    const self = this
+    // Important to avoid "undefined" class var members inside the timer.
+    // "this" has another meaning inside the timer.
+
+    // document.getElementById("progressBarChild").parentElement.style.visibility = "visible"
+    const startTime = performance.now()
+
+    const convLayer = self.model.layers[self.model.layers.length - 1]
+    const weights = convLayer.getWeights()[0] //
+    const biases = convLayer.getWeights()[1]
+    const outputShape = self.isChannelLast ? inputTensor.shape.slice(1, -1) : inputTensor.shape.slice(2)
+    // -- e.g.  outputShape : [256,256,256] or cropped Dim
+    // -- if inputTensor [ 1, D, H, W, 50 ], channelLast true ->   outputShape : outputShape [D, H, W]
+    // -- if inputTensor [ 1, 50, D, H, W ], channelLast false ->   outputShape : outputShape [D, H, W]
+
+    let outB = tf.mul(tf.ones(outputShape), -10000)
+    // -- e.g. outB.shape  [256,256,256]
+    let outC = tf.zeros(outputShape)
+    // -- e.g. outC.shape  [256,256,256]
+    let chIdx = 0
+
+    // console.log("---------------------------------------------------------")
+    console.log(' channel loop')
+
+    while (true) {
+      tf.engine().startScope() // Start TensorFlow.js scope
+      /* console.log('=======================')
+      const memoryInfo0 = await tf.memory()
+      console.log(`| Number of Tensors: ${memoryInfo0.numTensors}`)
+      console.log(`| Number of Data Buffers: ${memoryInfo0.numDataBuffers}`) */
+
+      const result = await tf.tidy(() => {
+        const filterWeights = weights.slice([0, 0, 0, 0, chIdx], [-1, -1, -1, -1, 1])
+        // -- e.g. filterWeights.shape [ 1, 1, 1, 5, 1 ]
+        const filterBiases = biases.slice([chIdx], [1])
+        // -- e.g. filterBiases.shape [1] -> Tensor  [-0.7850812]
+        const outA = processTensorInChunks(inputTensor, filterWeights, Math.min(self.chunkSize, self.outChannels)).add(
+          filterBiases
+        )
+        const greater = tf.greater(outA, outB)
+        const newoutB = tf.where(greater, outA, outB)
+        const newoutC = tf.where(greater, tf.fill(outC.shape, chIdx), outC)
+        // Dispose the old tensors before reassigning
+        tf.dispose([outB, outC, filterWeights, filterBiases, outA, greater])
+        // Dummy operation to trigger cleanup
+        tf.tidy(() => tf.matMul(tf.ones([1, 1]), tf.ones([1, 1])))
+        return [newoutC, newoutB]
+      })
+      console.log('=======================')
+      self.callbackUI(`Iteration ${chIdx}`, chIdx / self.outChannels)
+      if (!self.isWebWorker) {
+        // allow user interface to refresh
+        await new Promise((resolve) => setTimeout(resolve, 17))
+      }
+      const memoryInfo = await tf.memory()
+      console.log(`Number of Tensors: ${memoryInfo.numTensors}`)
+      console.log(`Number of Data Buffers: ${memoryInfo.numDataBuffers}`)
+      console.log(`Megabytes In Use: ${(memoryInfo.numBytes / 1048576).toFixed(3)} MB`)
+      if (memoryInfo.unreliable) {
+        console.log(`Unreliable: ${memoryInfo.unreliable}`)
+      }
+      // Dispose of previous values before assigning new tensors to outC and outB
+      if (typeof outC !== 'undefined') {
+        outC.dispose()
+      }
+      if (typeof outB !== 'undefined') {
+        outB.dispose()
+      }
+      // Assign the new values to outC and outB
+      outC = tf.keep(result[0])
+      outB = tf.keep(result[1])
+      // // Assign the new values to outC and outB
+      // outC = result[0]
+      // outB = result[1]
+      tf.engine().endScope()
+
+      if (chIdx === self.outChannels - 1) {
+        // document.getElementById("progressBarChild").style.width = 0 + "%"
+        tf.dispose(outB)
+        const endTime = performance.now()
+        const executionTime = endTime - startTime
+        console.log(`Execution time for output layer: ${executionTime} milliseconds`)
+        tf.ENV.set('WEBGL_DELETE_TEXTURE_THRESHOLD', oldDeleteTextureThreshold)
+        return outC
+      } else {
+        chIdx++
+
+        // the seemingly strange sequence of operations
+        // below prevents tfjs from uncontrolably
+        // grabbing buffers, even when all tensors have
+        // already been disposed
+
+        const outCShape = outC.shape
+        const outCdata = outC.dataSync()
+        const outBShape = outC.shape
+        const outBdata = outB.dataSync()
+        outC.dispose()
+        outB.dispose()
+        // tf.disposeVariables()
+        outC = tf.tensor(outCdata, outCShape)
+        outB = tf.tensor(outBdata, outBShape)
+
+        // document.getElementById("progressBarChild").style.width = (chIdx + 1) * 100 / self.outChannels + "%"
+      }
+    }
+  }
+} // <<<< End of class