macro "Erode and Prune to Fully(4)-Connected Skeleton" { /* Uses Gabriel Landini's skeletonization algorithm, different from the one in ImageJ similar to skeleton1 but 4-connected 3 Dec 2003 uses BinaryThin2_.class available from http://www.mecourse.com/landinig/software/software.html assumes a white object on a black background v171108 v180305 added option to keep white background - useful for creating skeletonized backgrounds v190325 fixed missing zero in coordinates. v200424 changed to adapt to black or white background based on mean intensity v200428 tweaked final filename v200428 added a bit more complexity to auto-file naming v200506 Just added a couple of show status updates v200512 added broken diagonals and trapped squares cleanup options v210727 changed to non-inverting. v211103 Adds median smoothing and autoSave options. v211104: Updated stripKnownExtensionFromString function 211112+220616+230505: Again. Updated functions: 5/16/2022 2:09 PM and 8/18/2022 11:06 AM + 6/7/2023 1:41 PM v220818: Auto-thresholds, no longer uses restoreExit. v240731: Pruning is now optional. "Trapped squares" now include 8-pixel trap (one corner missing). Better organization of dialogs. Restores selections and LUT condition. v250625: Fixed "tSkel" to fName. v250626: Additional options. */ macroL = "4(fully)-Cnctd_Skeleton_v250626.ijm"; if (!checkForPlugin("morphology_collection")) exit("Exiting: Gabriel Landini's morphology suite is needed to run this function."); fullFName = getInfo("image.filename"); if (fullFName == "") { fName = stripKnownExtensionFromString(getTitle()); extension = ""; } else { path = getInfo("image.directory"); fName = File.getNameWithoutExtension(path + fullFName); extension = substring(fullFName, lengthOf(fName)); } fName += "_skel"; orID = getImageID(); selType = selectionType; if (selType >= 0) run("Select None"); run("Duplicate...", "title=&fName slice"); run("8-bit"); if (!is("binary")) { /* Quick-n-dirty threshold if not previously thresholded */ getThreshold(t1, t2); if (t1 == -1) { run("Auto Threshold", "method=Default"); setOption("BlackBackground", false); run("Make Binary"); IJ.log("Default auto-threshold applied to " + fName); } } if (is("Inverting LUT")) { hadInvertedLUT = true; run("Invert LUT"); } else hadInvertedLUT = false; getStatistics(null, meanI, null, null, null, null); getDimensions(imageWidth, imageHeight, imageChannels, imageSlices, imageFrames); if (meanI < 128) blackSkel = false; else blackSkel = true; dFixes = 0; sFixes = 0; skelInt = 255; bgInt = 0; dFixes = 0; sFixes = 0; semiPixels = 0; zoomOPC = 100 * getZoom(); /* ASC message theme */ infoColor = "#006db0"; /* Honolulu blue */ instructionColor = "#798541"; /* green_dark_modern (121,133,65) AKA Wasabi */ infoWarningColor = "#ff69b4"; /* pink_modern AKA hot pink */ infoFontSize = 12; getLocationAndSize(xW, yW, widthW, heightW); Dialog.create(macroL + ": Erosion limits"); Dialog.addMessage("File: " + fullFName, infoFontSize * maxOf(0.5, minOf(1, 80 / lengthOf(fullFName))), infoColor); Dialog.addMessage("Directory: " + path, infoFontSize * maxOf(0.5, minOf(1, 80 / lengthOf(path))), infoColor); Dialog.addNumber("Denoise with median filter first:", 0, 1, 4, "radius in pixels"); Dialog.addNumber("Limit number of erosions?", -1, 0, 3, "-1 = until idempotence reached"); if (imageWidth * imageHeight > 4000000) Dialog.addMessage("The following fixes can take a long time on large images:"); if (blackSkel) Dialog.addCheckbox("Black skeleton recognized; is this true?", true); else Dialog.addCheckbox("White skeleton recognized; is this true?", true); Dialog.addCheckbox("Fix broken diagonals for full connection?", true); Dialog.addCheckbox("Fix trapped solid squares by clearing top and bottom pixels?", true); Dialog.addCheckbox("Fill open squares with single pixel diagonal disconnect?", false); Dialog.addCheckbox("Rerun erosions after diagonal and solid square fixes?", true); Dialog.addCheckbox("Prune \(performed after erosions and fixes\)?", true); Dialog.addCheckbox("Output fix report to log window?", false); Dialog.show(); medianRadius = Dialog.getNumber(); erosionN = Dialog.getNumber(); if (blackSkel){ if (!Dialog.getCheckbox()) blackSkel = false; } else if (!Dialog.getCheckbox()) blackSkel = true; dFix = Dialog.getCheckbox(); sFix = Dialog.getCheckbox(); semiPixel = Dialog.getCheckbox(); reErode = Dialog.getCheckbox(); pruneIt = Dialog.getCheckbox(); report = Dialog.getCheckbox(); xMax = imageWidth - 1; yMax = imageHeight - 1; /* Based on skeleton3.txt by G. Landini: a skeletonization algorithm, different from the one in ImageJ similar to skeleton1 but 4-connected 3 Dec 2003 uses BinaryThin2_.class available from http://www.mecourse.com/landinig/software/software.html assumes a white object on a black background */ setBatchMode(true); if (blackSkel) run("Invert"); showStatus("Creating Skeleton"); if (medianRadius > 0) run("Median...", "radius=" + medianRadius); run("BinaryThin2 ", "kernel_a='1 1 1 2 1 2 0 0 0 ' kernel_b='2 1 1 0 1 1 0 0 2 ' rotations='rotate 45' iterations=&erosionN white"); if (dFix) { for (i = 1; i < imageWidth - 1; i++) { /* start 2nd column, end penultimate column */ showStatus("Fixing broken diagonals: " + dFixes + " fixes"); showProgress(i, imageWidth - 1); for (j = bgInt; j < imageHeight - 1; j++) { /* start 1st row, end penultimate row */ if ((getPixel(i, j)) == skelInt) { if ((getPixel(i + 1, j)) == bgInt) { if ((getPixel(i, j + 1)) == bgInt) { if ((getPixel(i + 1, j + 1)) == skelInt) { setPixel(i + 1, j, skelInt); dFixes += 1; } } } /* conditions end iteration at first exception */ /* assume right and left fill-ins will balance out */ if ((getPixel(i - 1, j)) == bgInt) { if ((getPixel(i, j + 1)) == bgInt) { if ((getPixel(i - 1, j + 1)) == skelInt) { setPixel(i - 1, j, skelInt); dFixes += 1; } } } /* conditions end iteration at first exception */ } } } } if (sFix) { /* next open up trapped squares */ for (i = 1; i < imageWidth - 2; i++) { /* start 2nd column, end penultimate but one column */ showStatus("Fixing trapped squares: " + sFixes + " fixes"); showProgress(i, imageWidth - 1); for (j = 1; j < imageHeight - 3; j++) { /* start 2nd row, end penultimate but two row */ coreInt = getPixel(i, j); if (coreInt == bgInt) coreOpen = true; else coreOpen = false; skelN = 0; diagN = 0; for (k = 0; k < 3; k++) { for (l = 0; l < 3; l++) { pixInt = getPixel(i + k, j + l); if (pixInt == skelInt) skelN++; if (k != 1 && l != 1 && pixInt == bgInt) diagN++; } } if (skelN >= 8) { /* trapped square */ if ((getPixel(i + 1, j - 1)) == bgInt) { setPixel(i + 1, j, bgInt); sFixes += 1; } if ((getPixel(i + 1, j + 3)) == bgInt) { setPixel(i + 1, j + 2, bgInt); sFixes += 1; } } else if (coreOpen && semiPixel && skelN == 7 && diagN == 1) { setPixel(i, j, skelInt); semiPixels++; } } } } if ((dFix || sFix) && reErode) run("BinaryThin2 ", "kernel_a='1 1 1 2 1 2 0 0 0 ' kernel_b='2 1 1 0 1 1 0 0 2 ' rotations='rotate 45' iterations=&erosionN white"); if (blackSkel) run("Invert"); if (pruneIt) { setBatchMode("exit & display"); /* Probably not necessary if exiting gracefully but otherwise harmless */ Dialog.create("Save before pruning: Options"); Dialog.addNumber("Limit number of pruning iterations?", -1, 0, 3, "0 = No pruning, -1 = until idempotence reached"); Dialog.addCheckbox("Pin edge of image?", false); Dialog.addCheckbox("Apply to duplicate of image or active selection", false); Dialog.addRadioButtonGroup("Save before pruning?", newArray("Yes", "No", "Exit"), 1, 3, "No"); if (medianRadius > 0) fName += "_M" + medianRadius + "Smth"; fName += extension; Dialog.addString("Autosave directory:", path, lengthOf(path) + 5); Dialog.addString("Autosave filename:", fName, lengthOf(fName) + 5); Dialog.show(); pruneN = Dialog.getNumber(); pinEdge = Dialog.getCheckbox(); if (Dialog.getCheckbox()) { run("Duplicate...", " "); if (pinEdge) run("Canvas Size...", "width=" + imageWidth + 2 + " height=" imageHeight + 2 + " position=Center"); xW += 20; xY += 20; run("Set... ", "zoom=&zoomOPC x=&xW y=&xY"); } else if (pinEdge) run("Canvas Size...", "width=" + imageWidth + 2 + " height=" + imageHeight + 2 + " position=Center"); saveOpt = Dialog.getRadioButton(); path = Dialog.getString(); if (!endsWith(path, File.separator)) path += File.separator; autoPath = path + Dialog.getString; if (saveOpt == "Yes") save(autoPath); if (saveOpt == "Exit") exit("Bye bye"); /* Based on PruneAll by G. Landini: a pruning algorithm, different from the one in ImageJ. Prunes all branches of a skeleton leaving only the closed loops 8-connected 4 Dec 2003: uses BinaryThin_.class available from https://blog.bham.ac.uk/intellimic/g-landini-software assumes a white object on a black background */ setBatchMode(true); if (blackSkel) run("Invert"); showStatus("Pruning Skeleton"); run("BinaryThin ", "kernel_a='0 2 2 0 1 0 0 0 0' rotations='rotate 45' iterations=&pruneN white"); if (blackSkel) run("Invert"); } if (hadInvertedLUT) run("Invert LUT"); if (pinEdge) run("Canvas Size...", "width=&imageWidth height=&imageHeight position=Center"); if (selType >= 0) { finalID = getImageID(); selectImage(orID); run("Restore Selection"); selectImage(finalID); } setBatchMode("exit & display"); /* Probably not necessary if exiting gracefully but otherwise harmless */ beep(); wait(300); beep(); wait(300); beep(); if (report) IJ.log(dFixes + " diagonal fixes and " + sFixes + " trapped square fixes applied"); call("java.lang.System.gc"); /* force a garbage collection */ showStatus(macroL + " completed: " + dFixes + " diagonal fixes and " + sFixes + " trapped square fixes applied", "flash green"); } /* ( 8(|) ( 8(|) All ASC Functions @@@@@:-) @@@@@:-) */ function checkForPlugin(pluginName) { /* v161102 changed to true-false v180831 some cleanup v210429 Expandable array version v220510 Looks for both class and jar if no extension is given v220818 Mystery issue fixed, no longer requires restoreExit */ pluginCheck = false; if (getDirectory("plugins") == "") print("Failure to find any plugins!"); else { pluginDir = getDirectory("plugins"); if (lastIndexOf(pluginName, ".") == pluginName.length - 1) pluginName = substring(pluginName, 0, pluginName.length - 1); pExts = newArray(".jar", ".class"); knownExt = false; for (j = 0; j < lengthOf(pExts); j++) if (endsWith(pluginName, pExts[j])) knownExt = true; pluginNameO = pluginName; for (j = 0; j < lengthOf(pExts) && !pluginCheck; j++) { if (!knownExt) pluginName = pluginName + pExts[j]; if (File.exists(pluginDir + pluginName)) { pluginCheck = true; showStatus(pluginName + "found in: " + pluginDir); } else { pluginList = getFileList(pluginDir); subFolderList = newArray; for (i = 0, subFolderCount = 0; i < lengthOf(pluginList); i++) { if (endsWith(pluginList[i], "/")) { subFolderList[subFolderCount] = pluginList[i]; subFolderCount++; } } for (i = 0; i < lengthOf(subFolderList); i++) { if (File.exists(pluginDir + subFolderList[i] + "\\" + pluginName)) { pluginCheck = true; showStatus(pluginName + " found in: " + pluginDir + subFolderList[i]); i = lengthOf(subFolderList); } } } } } return pluginCheck; } function stripKnownExtensionFromString(string) { /* Note: Do not use on path as it may change the directory names v210924: Tries to make sure string stays as string. v211014: Adds some additional cleanup. v211025: fixes multiple 'known's issue. v211101: Added ".Ext_" removal. v211104: Restricts cleanup to end of string to reduce risk of corrupting path. v211112: Tries to fix trapped extension before channel listing. Adds xlsx extension. v220615: Tries to fix the fix for the trapped extensions ... v230504: Protects directory path if included in string. Only removes doubled spaces and lines. v230505: Unwanted dupes replaced by unusefulCombos. v230607: Quick fix for infinite loop on one of while statements. v230614: Added AVI. v230905: Better fix for infinite loop. v230914: Added BMP and "_transp" and rearranged */ fS = File.separator; string = "" + string; protectedPathEnd = lastIndexOf(string, fS) + 1; if (protectedPathEnd > 0) { protectedPath = substring(string, 0, protectedPathEnd); string = substring(string, protectedPathEnd); } unusefulCombos = newArray("-", "_", " "); for (i = 0; i < lengthOf(unusefulCombos); i++) { for (j = 0; j < lengthOf(unusefulCombos); j++) { combo = unusefulCombos[i] + unusefulCombos[j]; while (indexOf(string, combo) >= 0) string = replace(string, combo, unusefulCombos[i]); } } if (lastIndexOf(string, ".") > 0 || lastIndexOf(string, "_lzw") > 0) { knownExts = newArray(".avi", ".csv", ".bmp", ".dsx", ".gif", ".jpg", ".jpeg", ".jp2", ".png", ".tif", ".txt", ".xlsx"); knownExts = Array.concat(knownExts, knownExts, "_transp", "_lzw"); kEL = knownExts.length; for (i = 0; i < kEL / 2; i++) knownExts[i] = toUpperCase(knownExts[i]); chanLabels = newArray(" \(red\)", " \(green\)", " \(blue\)", "\(red\)", "\(green\)", "\(blue\)"); for (i = 0, k = 0; i < kEL; i++) { for (j = 0; j < chanLabels.length; j++) { /* Looking for channel-label-trapped extensions */ iChanLabels = lastIndexOf(string, chanLabels[j]) - 1; if (iChanLabels > 0) { preChan = substring(string, 0, iChanLabels); postChan = substring(string, iChanLabels); while (indexOf(preChan, knownExts[i]) > 0) { preChan = replace(preChan, knownExts[i], ""); string = preChan + postChan; } } } while (endsWith(string, knownExts[i])) string = "" + substring(string, 0, lastIndexOf(string, knownExts[i])); } } unwantedSuffixes = newArray(" ", "_", "-"); for (i = 0; i < unwantedSuffixes.length; i++) { while (endsWith(string, unwantedSuffixes[i])) string = substring(string, 0, string.length - lengthOf(unwantedSuffixes[i])); /* cleanup previous suffix */ } if (protectedPathEnd > 0) { if (!endsWith(protectedPath, fS)) protectedPath += fS; string = protectedPath + string; } return string; }