Diff of /main.js [000000] .. [b86468]

Switch to side-by-side view

--- a
+++ b/main.js
@@ -0,0 +1,384 @@
+import { Niivue } from "@niivue/niivue";
+import { runInference } from "./brainchop-mainthread.js";
+import { inferenceModelsList, brainChopOpts } from "./brainchop-parameters.js";
+import { isChrome, localSystemDetails } from "./brainchop-diagnostics.js";
+import MyWorker from "./brainchop-webworker.js?worker";
+
+async function main() {
+  dragMode.onchange = async function () {
+    nv1.opts.dragMode = this.selectedIndex;
+  };
+  drawDrop.onchange = async function () {
+    if (nv1.volumes.length < 2) {
+      window.alert("No segmentation open (use the Segmentation pull down)");
+      drawDrop.selectedIndex = -1;
+      return;
+    }
+    if (!nv1.drawBitmap) {
+      window.alert("No drawing (hint: use the Draw pull down to select a pen)");
+      drawDrop.selectedIndex = -1;
+      return;
+    }
+    const mode = parseInt(this.value);
+    if (mode === 0) {
+      nv1.drawUndo();
+      drawDrop.selectedIndex = -1;
+      return;
+    }
+    let img = nv1.volumes[1].img;
+    let draw = await nv1.saveImage({ filename: "", isSaveDrawing: true });
+    const niiHdrBytes = 352;
+    const nvox = draw.length;
+    if (mode === 1) {
+      //append
+      for (let i = 0; i < nvox; i++) if (draw[niiHdrBytes + i] > 0) img[i] = 1;
+    }
+    if (mode === 2) {
+      //delete
+      for (let i = 0; i < nvox; i++) if (draw[niiHdrBytes + i] > 0) img[i] = 0;
+    }
+    nv1.closeDrawing();
+    nv1.updateGLVolume();
+    nv1.setDrawingEnabled(false);
+    penDrop.selectedIndex = -1;
+    drawDrop.selectedIndex = -1;
+  };
+  penDrop.onchange = async function () {
+    const mode = parseInt(this.value);
+    nv1.setDrawingEnabled(mode >= 0);
+    if (mode >= 0) nv1.setPenValue(mode & 7, mode > 7);
+  };
+  aboutBtn.onclick = function () {
+    window.alert(
+      "Drag and drop NIfTI images. Use pulldown menu to choose brainchop model",
+    );
+  };
+  diagnosticsBtn.onclick = function () {
+    if (diagnosticsString.length < 1) {
+      window.alert(
+        "No diagnostic string generated: run a model to create diagnostics",
+      );
+      return;
+    }
+    missingLabelStatus = missingLabelStatus.slice(0, -2);
+    if (missingLabelStatus !== "") {
+      if (diagnosticsString.includes('Status: OK')) {
+        diagnosticsString = diagnosticsString.replace('Status: OK', `Status: ${missingLabelStatus}`);
+      }
+    }
+    missingLabelStatus = ""
+    navigator.clipboard.writeText(diagnosticsString);
+    window.alert("Diagnostics copied to clipboard\n" + diagnosticsString);
+  };
+  opacitySlider0.oninput = function () {
+    nv1.setOpacity(0, opacitySlider0.value / 255);
+    nv1.updateGLVolume();
+  };
+  opacitySlider1.oninput = function () {
+    nv1.setOpacity(1, opacitySlider1.value / 255);
+  };
+  async function ensureConformed() {
+    const nii = nv1.volumes[0];
+    let isConformed =
+      nii.dims[1] === 256 && nii.dims[2] === 256 && nii.dims[3] === 256;
+    if (
+      nii.permRAS[0] !== -1 ||
+      nii.permRAS[1] !== 3 ||
+      nii.permRAS[2] !== -2
+    ) {
+      isConformed = false;
+    }
+    if (isConformed) {
+      return;
+    }
+    const nii2 = await nv1.conform(nii, false);
+    await nv1.removeVolume(nv1.volumes[0]);
+    await nv1.addVolume(nii2);
+  }
+  async function closeAllOverlays() {
+    while (nv1.volumes.length > 1) {
+      await nv1.removeVolume(nv1.volumes[1]);
+    }
+  }
+  modelSelect.onchange = async function () {
+    if (this.selectedIndex < 0) {
+      modelSelect.selectedIndex = 11;
+    }
+    await closeAllOverlays();
+    await ensureConformed();
+    const model = inferenceModelsList[this.selectedIndex];
+    const opts = brainChopOpts;
+    // opts.rootURL should be the url without the query string
+    const urlParams = new URL(window.location.href);
+    // remove the query string
+    opts.rootURL = urlParams.origin + urlParams.pathname;
+    const isLocalhost = Boolean(
+      window.location.hostname === "localhost" ||
+        // [::1] is the IPv6 localhost address.
+        window.location.hostname === "[::1]" ||
+        // 127.0.0.1/8 is considered localhost for IPv4.
+        window.location.hostname.match(
+          /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
+        ),
+    );
+    if (isLocalhost) {
+      opts.rootURL = location.protocol + "//" + location.host;
+    }
+    if (workerCheck.checked) {
+      if (typeof chopWorker !== "undefined") {
+        console.log(
+          "Unable to start new segmentation: previous call has not completed",
+        );
+        return;
+      }
+      chopWorker = await new MyWorker({ type: "module" });
+      const hdr = {
+        datatypeCode: nv1.volumes[0].hdr.datatypeCode,
+        dims: nv1.volumes[0].hdr.dims,
+      };
+      const msg = {
+        opts,
+        modelEntry: model,
+        niftiHeader: hdr,
+        niftiImage: nv1.volumes[0].img,
+      };
+      chopWorker.postMessage(msg);
+      chopWorker.onmessage = function (event) {
+        const cmd = event.data.cmd;
+        if (cmd === "ui") {
+          if (event.data.modalMessage !== "") {
+            chopWorker.terminate();
+            chopWorker = undefined;
+          }
+          callbackUI(
+            event.data.message,
+            event.data.progressFrac,
+            event.data.modalMessage,
+            event.data.statData,
+          );
+        }
+        if (cmd === "img") {
+          chopWorker.terminate();
+          chopWorker = undefined;
+          callbackImg(event.data.img, event.data.opts, event.data.modelEntry);
+        }
+      };
+    } else {
+      runInference(
+        opts,
+        model,
+        nv1.volumes[0].hdr,
+        nv1.volumes[0].img,
+        callbackImg,
+        callbackUI,
+      );
+    }
+  };
+  saveImgBtn.onclick = function () {
+    nv1.volumes[1].saveToDisk("segmentaion.nii.gz");
+  };
+  saveSceneBtn.onclick = function () {
+    nv1.saveDocument("brainchop.nvd");
+  };
+  workerCheck.onchange = function () {
+    modelSelect.onchange();
+  };
+  clipCheck.onchange = function () {
+    if (clipCheck.checked) {
+      nv1.setClipPlane([0, 0, 90]);
+    } else {
+      nv1.setClipPlane([2, 0, 90]);
+    }
+  };
+  function doLoadImage() {
+    opacitySlider0.oninput();
+  }
+  async function fetchJSON(fnm) {
+    const response = await fetch(fnm);
+    const js = await response.json();
+    return js;
+  }
+  async function getUniqueValuesAndCounts(uint8Array) {
+    // Use a Map to count occurrences
+    const countsMap = new Map();
+
+    for (let i = 0; i < uint8Array.length; i++) {
+      const value = uint8Array[i];
+      if (countsMap.has(value)) {
+        countsMap.set(value, countsMap.get(value) + 1);
+      } else {
+        countsMap.set(value, 1);
+      }
+    }
+
+    // Convert the Map to an array of objects
+    const result = Array.from(countsMap, ([value, count]) => ({
+      value,
+      count,
+    }));
+
+    return result;
+  }
+  async function createLabeledCounts(uniqueValuesAndCounts, labelStrings) {
+    if (uniqueValuesAndCounts.length !== labelStrings.length) {
+      missingLabelStatus = "Failed to Predict Labels - "
+      console.error(
+        "Mismatch in lengths: uniqueValuesAndCounts has",
+        uniqueValuesAndCounts.length,
+        "items, but labelStrings has",
+        labelStrings.length,
+        "items.",
+      );
+    }
+
+    return labelStrings.map((label, index) => {
+      // Find the entry matching the current label index
+      const entry = uniqueValuesAndCounts.find(item => item.value === index);
+
+      // If an entry is found, append the count value with 'mm3', otherwise show 'Missing'
+      const countText = entry ? `${entry.count} mm3` : "Missing";
+
+      countText === "Missing"
+      ? missingLabelStatus += `${label}, ` : null;
+
+      return `${label}   ${countText}`;
+    });
+  }
+  async function callbackImg(img, opts, modelEntry) {
+    closeAllOverlays();
+    const overlayVolume = await nv1.volumes[0].clone();
+    overlayVolume.zeroImage();
+    overlayVolume.hdr.scl_inter = 0;
+    overlayVolume.hdr.scl_slope = 1;
+    overlayVolume.img = new Uint8Array(img);
+    const roiVolumes = await getUniqueValuesAndCounts(overlayVolume.img);
+    console.log(roiVolumes);
+    if (modelEntry.colormapPath) {
+      const cmap = await fetchJSON(modelEntry.colormapPath);
+      const newLabels = await createLabeledCounts(roiVolumes, cmap["labels"]);
+      console.log(newLabels);
+      overlayVolume.setColormapLabel({
+        R: cmap["R"],
+        G: cmap["G"],
+        B: cmap["B"],
+        labels: newLabels,
+      });
+      // n.b. most models create indexed labels, but those without colormap mask scalar input
+      overlayVolume.hdr.intent_code = 1002; // NIFTI_INTENT_LABEL
+    } else {
+      let colormap = opts.atlasSelectedColorTable.toLowerCase();
+      const cmaps = nv1.colormaps();
+      if (!cmaps.includes(colormap)) {
+        colormap = "actc";
+      }
+      overlayVolume.colormap = colormap;
+    }
+    overlayVolume.opacity = opacitySlider1.value / 255;
+    await nv1.addVolume(overlayVolume);
+  }
+  async function reportTelemetry(statData) {
+    if (typeof statData === "string" || statData instanceof String) {
+      function strToArray(str) {
+        const list = JSON.parse(str);
+        const array = [];
+        for (const key in list) {
+          array[key] = list[key];
+        }
+        return array;
+      }
+      statData = strToArray(statData);
+    }
+    statData = await localSystemDetails(statData, nv1.gl);
+    diagnosticsString =
+      ":: Diagnostics can help resolve issues https://github.com/neuroneural/brainchop/issues ::\n";
+    for (const key in statData) {
+      diagnosticsString += key + ": " + statData[key] + "\n";
+    }
+  }
+  function callbackUI(
+    message = "",
+    progressFrac = -1,
+    modalMessage = "",
+    statData = [],
+  ) {
+    if (message !== "") {
+      console.log(message);
+      document.getElementById("location").innerHTML = message;
+    }
+    if (isNaN(progressFrac)) {
+      // memory issue
+      memstatus.style.color = "red";
+      memstatus.innerHTML = "Memory Issue";
+    } else if (progressFrac >= 0) {
+      modelProgress.value = progressFrac * modelProgress.max;
+    }
+    if (modalMessage !== "") {
+      window.alert(modalMessage);
+    }
+    if (Object.keys(statData).length > 0) {
+      reportTelemetry(statData);
+    }
+  }
+  function handleLocationChange(data) {
+    document.getElementById("location").innerHTML = data.string
+      .split("   ")
+      .map((value) => `<p style="font-size: 14px;margin:0px;">${value}</p>`)
+      .join("");
+  }
+  const defaults = {
+    backColor: [0.4, 0.4, 0.4, 1],
+    show3Dcrosshair: true,
+    onLocationChange: handleLocationChange,
+  };
+  let diagnosticsString = "";
+  let missingLabelStatus = ""
+  let chopWorker;
+  const nv1 = new Niivue(defaults);
+  nv1.attachToCanvas(gl1);
+  nv1.opts.dragMode = nv1.dragModes.pan;
+  nv1.opts.multiplanarForceRender = true;
+  nv1.opts.yoke3Dto2DZoom = true;
+  nv1.opts.crosshairGap = 11;
+  nv1.setInterpolation(true);
+  await nv1.loadVolumes([{ url: "./t1_crop.nii.gz" }]);
+  for (let i = 0; i < inferenceModelsList.length; i++) {
+    const option = document.createElement("option");
+    option.text = inferenceModelsList[i].modelName;
+    option.value = inferenceModelsList[i].id.toString();
+    modelSelect.appendChild(option);
+  }
+  nv1.onImageLoaded = doLoadImage;
+  modelSelect.selectedIndex = -1;
+  drawDrop.selectedIndex = -1;
+  workerCheck.checked = await isChrome(); // TODO: Safari does not yet support WebGL TFJS webworkers, test FireFox
+  // uncomment next two lines to automatically run segmentation when web page is loaded
+  // modelSelect.selectedIndex = 11
+  // modelSelect.onchange()
+
+  // get the query string parameter model.
+  // if set, select the model from the dropdown list and call the modelSelect.onchange() function
+  const urlParams = new URLSearchParams(window.location.search);
+  const modelParam = urlParams.get("model");
+  if (modelParam) {
+    // make sure the model index is a number
+    modelSelect.selectedIndex = Number(modelParam);
+    modelSelect.onchange();
+  }
+}
+
+async function updateStarCount() {
+  try {
+    const response = await fetch(
+      `https://api.github.com/repos/neuroneural/brainchop`,
+    );
+    const data = await response.json();
+    document.getElementById("star-count").textContent = data.stargazers_count;
+  } catch (error) {
+    console.error("Error fetching star count:", error);
+  }
+}
+(async function () {
+  await main();
+  updateStarCount();
+})();
+