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. */ macroL = "4(fully)-Cnctd_Skeleton_v240731.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 " + tSkel); } } 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 + ": To limit or not to limit that is the question..."); 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:"); 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(); 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=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("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(); if (Dialog.getCheckbox()){ run("Duplicate...", " "); xW +=20; xY +=20; run("Set... ", "zoom=&zoomOPC x=&xW y=&xY"); } 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 (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; j0){ protectedPath = substring(string,0,protectedPathEnd); string = substring(string,protectedPathEnd); } unusefulCombos = newArray("-", "_"," "); for (i=0; i=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; i0){ 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; i0){ if(!endsWith(protectedPath,fS)) protectedPath += fS; string = protectedPath + string; } return string; }