macro "Images to Stack Plus" { /* Creates a stack from open images - gives you more control than built-in command. v231024: 1st version, Peter J. Lee Applied Superconductivity Center, Florida State University v231025-6: Added filters, stack alignment and Z-projection, smarter slice names. b: Added morphological filters. v231101: More filters tested and allowed with color stack. Simplified crop but added manual option. Window closing options added. Menu optimized for height. Some Prefs saved. v231102: Default common name added. For images with different sizes the gaps will set to the background color. v231103: Fixed prefs error for sort order. b: Adds manual alignment to line (scale and rotate). Better common name detection. v231106: Added a dialog to allow selection of the rotation and scale options in manual alignment. v231107: Option added to ignore black backgrounds in z-projection average. Auto-alignment now always aligns to top slice correctly. v231107b-11: Added margins option for manual align. v231113: Ignores crop if there is no selection. v231115: Fixed error on common name loop and other issues for <3 tiles. v231116: Manual alignment significantly reworked to incorporate saved lines option. v231117: Auto-alignment of pairs now working with manual ROI lines. Better auto crop. v231127f: Removed "!" from intermediate showStatus commands to allow getLine, makeLine and run("SelectNone") selectWindow to work reliably. v231128c: Simplified by removing non z-stack options. v240130: Added margin option for non-manual alignments. Removed NaN background option for RGB images. */ macroL = "Images_to_Stack_Plus_v240130.ijm"; ascPrefsKey = "asc.ImagesToStackPlus.Prefs."; close("temp_*"); /* Cleanup if crashed last time */ saveSettings(); if (nImages<2) exit("Need more than one open image to create a stack"); imageList = removeDuplicatesInArray(getList("image.titles"), true); imageN = lengthOf(imageList); minTL = imageList[0].length; for (i=1; i0 && imageN>4){ Dialog.addToSameRow(); addRow = -1; } Dialog.addChoice("slice " + i+1 + " \(" + shortCommon + "\)", imageShortList, imageShortList[i]); } } else { Dialog.addChoice("Choose top slice \(1\)", imageList, imageList[0]); Dialog.addCheckbox("Set slice label as unique name as listed above", true); } Dialog.setInsets(0, 120, 10); sliceOptions = newArray("Reverse this order", "Close originals after stacking", "Close intermediate stacks", "Apply names above as slice names", "Manually crop stack", "Diagnostic mode"); sliceOptionChecks = newArray(); sliceOptionChecks = Array.concat(sliceOptionChecks, call("ij.Prefs.get", ascPrefsKey + "reverseOrder", false)); sliceOptionChecks = Array.concat(sliceOptionChecks, call("ij.Prefs.get", ascPrefsKey + "closeSourceImages", false)); sliceOptionChecks = Array.concat(sliceOptionChecks, call("ij.Prefs.get", ascPrefsKey + "closeIntermediateStacks", false)); sliceOptionChecks = Array.concat(sliceOptionChecks, call("ij.Prefs.get", ascPrefsKey + "setSliceName", true)); sliceOptionChecks = Array.concat(sliceOptionChecks, call("ij.Prefs.get", ascPrefsKey + "manCrop", false), false); /* Extra 'false' for diagnostics default */ Dialog.addCheckboxGroup(3, 2, sliceOptions, sliceOptionChecks); barCropH = parseInt(call("ij.Prefs.get", ascPrefsKey + "barCropH", NaN)); lastImageStartWidth = parseInt(call("ij.Prefs.get", ascPrefsKey + "lastImageStartWidth", Image.width)); lastImageStartHeight = parseInt(call("ij.Prefs.get", ascPrefsKey + "lastImageStartHeight", Image.height)); if (lastImageStartWidth!=Image.width || lastImageStartHeight!=Image.height) barCropH = NaN; cols = maxOf(3, lengthOf(d2s(maxOf(Image.width, Image.height), 0))); if (barCropH>0) Dialog.addMessage("The crop of " + barCropH + " pixels from the last run has been applied below:", infoFontSize, infoWarningColor); Dialog.setInsets(5, 10, 0); Dialog.addNumber("Crop off bottom lines", barCropH, 0, cols, "pixels"); Dialog.addToSameRow(); Dialog.addMessage("Use this option to remove an annotation bar", infoFontSize, instructionColor); Dialog.setInsets(5, 10, 0); Dialog.addNumber("Linear contrast stretch", NaN, 0, cols, "% saturated"); contrastOptions = newArray("Maximize bit range", "Equalize histogram"); contrastChecks = newArray(false, false); if (bitD==8 || bitD==16){ /* Bleach correction by histogram matching \n\(Miura, K. doi:10.12688/f1000research.27171.1 */ contrastOptions = Array.concat(contrastOptions, "Histogram matching"); contrastChecks = Array.concat(contrastChecks, false); } Dialog.setInsets(10, 10, 10); Dialog.addCheckboxGroup(1, contrastOptions.length, contrastOptions, contrastChecks); Dialog.addNumber("Median filter radius", NaN, 1, 3, "pixels"); Dialog.addToSameRow(); Dialog.addMessage("Filters will be ignored if settings are blank", infoFontSize, instructionColor); Dialog.addNumber("Unsharp mask radius", NaN, 1, 3, "pixels"); Dialog.addToSameRow(); unsharpW = parseFloat(call("ij.Prefs.get", ascPrefsKey + "unsharpW", NaN)); Dialog.addNumber("Unsharp mask weight", unsharpW, 1, 3, "0.1-0.9"); if (bitD==8 || bitD==16){ morphologicalFilters = newArray("None", "Gradient", "Internal Gradient", "External Gradient", "Dilation", "Erosion", "Opening", "Closing", "White Top Hot", "Black Top Hat", "White Top Hat", "Laplacian"); Dialog.addChoice("Morphological Filters", morphologicalFilters, "None"); /* https://imagej.net/plugins/morpholibj#Morphological_filters */ morphologicalElements = newArray("Square", "Cube", "Diamond", "Octagon", "Horizontal Line", "Vertical Line", "Z-Line", "Line 45 Degrees", "Line 135 Degrees"); Dialog.addToSameRow(); morphologicalElement = call("ij.Prefs.get", ascPrefsKey + "morphologicalElement", "Square"); Dialog.addChoice("Morphological Elements", morphologicalElements, morphologicalElement); morphologicalX = parseInt(call("ij.Prefs.get", ascPrefsKey + "morphologicalX", 2)); Dialog.addNumber("X-Radius", morphologicalX, 0, 5, "voxels"); Dialog.addToSameRow(); Dialog.addMessage("Morphological Filters are 3D", infoFontSize, instructionColor); morphologicalY = parseInt(call("ij.Prefs.get", ascPrefsKey + "morphologicalY", 2)); Dialog.addNumber("Y-Radius", morphologicalY, 0, 5, "voxels"); morphologicalZ = parseInt(call("ij.Prefs.get", ascPrefsKey + "morphologicalZ", 0)); Dialog.addToSameRow(); Dialog.addNumber("Z-Radius", morphologicalZ, 0, 5, "voxels"); Dialog.addCheckbox("Find edges \(Sobel\)", false); if (imageN>=3) Dialog.addCheckbox("Create RGB image from 3-stack", false); } else Dialog.addCheckbox("Find edges \(Sobel\)", false); alignOptions = newArray("None", "Manual", "Translation", "Rigid Body", "Scaled Rotation", "Affine"); Dialog.addRadioButtonGroup("Slice alignment:_______________(manual alignment is by line selection between 2 corresponding points\)",alignOptions, 1, 5, "None"); zProjections = newArray("None", "Average Intensity", "Max Intensity", "Min Intensity", "Median"); if (bitD!=24) zProjections = Array.concat(zProjections, "Average-Ignore Black"); Dialog.addRadioButtonGroup("z-Projection:___________________", zProjections, 2, 3, "None"); Dialog.show(); tileN = Dialog.getNumber(); stackName = Dialog.getString(); tilesOrdered = newArray(); tilesShortOrdered = newArray(); if (imageN<30){ for (i=0; i0.9) unsharpW = NaN; call("ij.Prefs.set", ascPrefsKey + "unsharpW", unsharpW); morphologicalFilter = "None"; if (bitD==8 || bitD==16){ morphologicalFilter = Dialog.getChoice(); morphologicalElement = Dialog.getChoice(); morphologicalX = Dialog.getNumber; morphologicalY = Dialog.getNumber; morphologicalZ = Dialog.getNumber; if (morphologicalFilter!="None"){ call("ij.Prefs.set", ascPrefsKey + "morphologicalElement", morphologicalElement); call("ij.Prefs.set", ascPrefsKey + "morphologicalX", morphologicalX); call("ij.Prefs.set", ascPrefsKey + "morphologicalY", morphologicalY); call("ij.Prefs.set", ascPrefsKey + "morphologicalZ", morphologicalZ); } } findEdges = Dialog.getCheckbox(); if ((bitD==8 || bitD==16) && imageN>=3){ convertToRGB = Dialog.getCheckbox(); if (tileN!=3) convertToRGB = false; } else { convertToRGB = false; histMatch = false; } alignOption = Dialog.getRadioButton(); zProjection = Dialog.getRadioButton(); /* End of main dialog */ call("ij.Prefs.set", ascPrefsKey + "lastImageStartWidth", Image.width); call("ij.Prefs.set", ascPrefsKey + "lastImageStartHeight", Image.height); tileNames = newArray; for (i=0; i0 && commonL0 && commonL1){ makeLine(x1s[wLineC], y1s[wLineC], x2s[wLineC], y2s[wLineC], 1); lTxt = i + ": Drawing previous line " + wLineC + " on target " + tName; } else{ makeLine(0.1 * imWidth, 0.1 * imHt, 0.9 * imWidth, 0.9 * imHt, 1); lTxt = i + ": Drawing default diagonal line on target " + tName; } } Roi.setStrokeColor("#30ff0000"); Roi.setStrokeWidth(maxOf(3, Image.width/200)); updateDisplay(); if (diagnostics) IJ.log(lTxt); showStatus(lTxt, "flash image #00ffff 50ms"); selectWindow(tilesOrdered[i]); run("Main Window [enter]"); sName = tilesShortOrdered[i]; run("View 100%"); run("Scale to Fit"); setLocation(0.5 * screenWidth, 0.15 * screenHeight, 0.4 * screenWidth, 0.8 * screenHeight); showStatus("Source tile for alignment", "flash image #00ffff 500ms"); if (gotLines){ restoredCoords = split(savedLines[sLineC], ","); makeLine(parseInt(restoredCoords[0]), parseInt(restoredCoords[1]), parseInt(restoredCoords[2]), parseInt(restoredCoords[3]), 3); lTxt = i + ": Drawing line from file " + sLineC + " on source " + sName; sLineC++; } else { if (i>1){ makeLine(x1s[wLineC-1], y1s[wLineC-1], x2s[wLineC-1], y2s[wLineC-1], 1); lTxt = i + ": Drawing previous line " + wLineC + " on source " + sName; } else{ makeLine(0.1 * imWidth, 0.1 * imHt, 0.9 * imWidth, 0.9 * imHt, 1); lTxt = i + ": Drawing default diagonal line on source " + sName; } } Roi.setStrokeColor("#300000ff"); Roi.setStrokeWidth(maxOf(3, Image.width/200)); updateDisplay(); if (diagnostics) IJ.log(lTxt); hints = "\n \nHint 1: Use the '+' and '-' keys to zoom in and out over the cursor for better accuracy\" + "\nHint 2: You can quickly move the entire line by grabbing the center selection point" + "\nHint 3: Switch between images using Window menu on ImageJ bar"; lineInstructions = "Adjust lines on target: " + tName + "\nand source: " + sName + "\nbetween the two well-spaced features" + hints; setTool("line"); showStatus(lTxt, "flash image #00ffff 50ms"); waitForUser("Set #" + i + ": Line Align Instructions", lineInstructions); /* get target coords */ selectWindow(tilesOrdered[0]); getLine(x1s[wLineC], y1s[wLineC], x2s[wLineC], y2s[wLineC], lineWidth); run("Select None"); lineCoordString = "" + x1s[wLineC] + "," + y1s[wLineC] + "," + x2s[wLineC] + "," + y2s[wLineC]; lineCoordSet += "" + lineCoordString + "," + getTitle() + "\n"; if (diagnostics) IJ.log("Line coords for target: " + lineCoordSet); linePairs[wLineC] = lineCoordString; cXT = (x1s[wLineC] + x2s[wLineC])/2; cYT = (y1s[wLineC] + y2s[wLineC])/2; wLineC++; run("Main Window [enter]"); /* get source coords */ selectWindow(tilesOrdered[i]); getLine(x1s[wLineC], y1s[wLineC], x2s[wLineC], y2s[wLineC], lineWidth); run("Select None"); lineCoordString = "" + x1s[wLineC] + "," + y1s[wLineC] + "," + x2s[wLineC] + "," + y2s[wLineC]; lineCoordSet += "" + lineCoordString + "," + getTitle() + "\n"; if (diagnostics) IJ.log("Line coords for source: " + lineCoordSet); linePairs[wLineC] = lineCoordString; cXS = (x1s[wLineC] + x2s[wLineC])/2; cYS = (y1s[wLineC] + y2s[wLineC])/2; wLineC++; offsetX = cXT - cXS; offsetY = cYT - cYS; xImageOffsets[i] = offsetX; yImageOffsets[i] = offsetY; maxImWidth = maxOf(maxImWidth, imWidth); maxImHt = maxOf(maxImHt, imHt) - barCropH; if (diagnostics) IJ.log("Alignment " + i + ": source=" + sName + " target=" + tName + manualAlignOptionString); } } /* save line coordinates for future use */ while (endsWith(lineCoordSet,"\n")) lineCoordSet = substring(lineCoordSet, 0, lineCoordSet.length-1); title1LineCoordSet = commonName + "_LineCoordSet"; title2LineCoordSet = "["+title1LineCoordSet+"]"; f = title2LineCoordSet; if (isOpen(title1LineCoordSet)) print(f, "\\Update:"); // clears the window else run("Text Window...", "name="+title2LineCoordSet+" width=72 height=8 menu"); if (diagnostics) IJ.log("Full path tile configuration:\n" + lineCoordSet); print(f, lineCoordSet); lineCoordSetPath = dir + title1LineCoordSet +".txt"; saveAs("Text", lineCoordSetPath); /* Saved for compatibility with older versions */ run("Close"); /* Need to close window and start fresh as there is no rename function for text windows */ /* end line saves */ /* End for alignment iterations */ call("ij.Prefs.set", ascPrefsKey + "lastLinesLoaded", lineCoordSetPath); Array.getStatistics(xImageOffsets, minX, maxX, null, null); Array.getStatistics(yImageOffsets, minY, maxY, null, null); if (diagnostics){ IJ.log("Offset arrays:"); Array.print(xImageOffsets); Array.print(yImageOffsets); } } /* End for manual alignment */ if (!diagnostics) setBatchMode(true); for (i=0; i0) run("Canvas Size...", "width=" + maxWidth + marginPx * 2 + " height=" + maxHeight + marginPx * 2 + " position=Center" + marginFill); } else { if (minX<0) run("Canvas Size...", "width=" + (startW - minX) + " height=" + startH + " position=Top-Right"); if (minY<0) run("Canvas Size...", "width=" + startW + " height=" + (startH) - minY + " position=Bottom-Left"); run("Canvas Size...", "width=" + (maxX + maxWidth - minX) + " height=" + (maxY + maxHeight - minY) + " position=Top-Left"); if (manRot || manScale) run("Duplicate...", "title=temp_Target"); makeLine(x1s[0] - minX, y1s[0] - minY, x2s[0] - minX, y2s[0] - minY, 3); if (diagnostics) waitForUser(i + " line " + 0); } } else { selectWindow(tilesOrdered[i]); makeRectangle(0, 0, startW, startH); run("Duplicate...", "title=temp_Source"); run("Select None"); if (alignOption!="Manual") addImageToStack3(stackName, 0, 0); else { if (manRot || manScale) { makeLine(x1s[wLineC], y1s[wLineC], x2s[wLineC], y2s[wLineC], wLineC + 4); if (diagnostics) waitForUser(i + " line " + wLineC); run("Align Image by line ROI", "source=temp_Source target=temp_Target" + manualAlignOptionString); wLineC += 2; rename("temp_Aligned"); addImageToStack3(stackName, 0, 0); if (diagnostics) IJ.log("ROI-aligned tempSource added to 0,0 of stack: " + stackName); close("temp_Aligned*"); } else { addImageToStack3(stackName, xImageOffsets[i] - minX, yImageOffsets[i] - minY); if (diagnostics) IJ.log("Image addition to stack: " + stackName, xImageOffsets[i] - minX, yImageOffsets[i] - minY); } } close("temp_Sourc*"); } } if (manCrop){ setBatchMode("exit and display"); run("Select None"); run("Select Bounding Box (guess background color)"); setTool("rectangle"); title = "Crop Stack"; msg = "1. Select the area that you want to crop to. 2. Click on OK"; waitForUser(title, msg); setBatchMode(true); if (selectionType>=0){ run("Crop"); run("Select None"); } setBatchMode(true); } if (setSliceName){ for (i=0; i0) selectWindow(stackName); else { setBatchMode("exit and display"); if (!isOpen(stackName)) exit("Stack alignment issue: " + stackName + " not found"); if (nSlices<1) exit("Stack alignment issue: " + stackName + " not a stack"); } setSlice(1); showStatus("Stack alignment underway, this could be slow. Stack will flash green on completion", "flash image red 200ms"); run("StackReg", "transformation=[" + alignOption + "]"); IJ.log("Stack alignment applied: " + alignOption); stackName += "\+aligned" + substring(alignOption, 0, 3); rename(stackName); showStatus("Stack alignment complete", "flash image green 200ms"); } if (!isNaN(medianR)){ run("Median...", "radius=" + medianR + " stack"); IJ.log("Median filter applied: radius=" + medianR); stackName += "\+Medn" + medianR; rename(stackName); } if (!isNaN(unsharpR) && !isNaN(unsharpW)){ run("Unsharp Mask...", "radius=" + unsharpR + " mask=" + unsharpW + " stack"); stackName += "\+Unsh" + unsharpR; rename(stackName); IJ.log("Unsharp Mask applied: radius=" + unsharpR + " mask=" + unsharpW); } if (morphologicalFilter!="None"){ if (isOpen(stackName) && nSlices>0) selectWindow(stackName); else { setBatchMode("exit and display"); if (!isOpen(stackName)) exit("Morphological filter issue: " + stackName + " not found"); if (nSlices<1) exit("Morphological filter issue: " + stackName + " not a stack"); } if (bitDepth==32) run("16-bit"); /* Weird results with 32-bit */ intermID = getImageID(); morphName = stackName + "\+" + substring(morphologicalFilter, 0, 4) + "X" + morphologicalX + "Y" + morphologicalY + "Z" + morphologicalZ; run("Morphological Filters (3D)", "operation=" + morphologicalFilter + " element=" + morphologicalElement + " x-radius=" + morphologicalX + " y-radius=" + morphologicalX +" z-radius=" + morphologicalZ); rename(morphName); if (isOpen(morphName)){ stackName = morphName; IJ.log("Morphological filter applied: " +morphologicalFilter+" element="+morphologicalElement+" x-radius=" + morphologicalX +" y-radius=" + morphologicalY +" z-radius=" + morphologicalZ + "\nhttps://imagej.net/plugins/morpholibj#Morphological_filters"); if (closeIntermediateStacks){ selectImage(intermID); close(); selectWindow(stackName); } } } if (findEdges){ if (isOpen(stackName) && nSlices>0) selectWindow(stackName); else { setBatchMode("exit and display"); if (!isOpen(stackName)) exit("Find edges issue: " + stackName + " not found"); if (nSlices<1) exit("Find edges issue: " + stackName + " not a stack"); } intermID = getImageID(); run("Select None"); run("Duplicate...", "duplicate"); stackName += "\+Sobel"; rename(stackName); run("Find Edges", "stack"); IJ.log("Find edges \(Sobel filter\) applied"); if (closeIntermediateStacks){ selectImage(intermID); close(); selectWindow(stackName); } } if (convertToRGB){ /* Retains original stack */ selectWindow(stackName); run("Stack to RGB"); rename(stackName + "_RGB"); rgbID = getImageID; } if (zProjection!="None"){ /* Retains original stack */ if (isOpen(stackName) && nSlices>0) selectWindow(stackName); else { setBatchMode("exit and display"); if (!isOpen(stackName)) exit("Stack projection issue: " + stackName + " not found"); if (nSlices<1) exit("Stack projection issue: " + stackName + " not a stack"); } preProjectBD = bitDepth; if (zProjection=="Average-Ignore Black"){ /* NaN trick suggested by Tiago Ferreira: https://forum.image.sc/t/create-an-average-z-stack-projection-using-only-values-above-a-certain-level/74238/2 Nov 2022 */ setBatchMode("show"); setSlice(1); if (preProjectBD!=32 && preProjectBD!=24) run("32-bit"); setThreshold(-1, 10e30); waitForUser("Instructions for the Threshold window that pops up next to create the NaN background for averaging", "Read these instructions and then click 'OK' to continue \(do not start any of the steps before then\)\n" + "Don't worry about remembering all the steps, there will be a reminder pop-up.\n" + "1:\t Set top slider to one \(or two\) click\(s\) from full left.\n" + "2:\t Set bottom slider to full right. No other options should be necessary.\n" + " \tThe background should be black, the aligned images should be red \(or you preferred threshold color\).\n" + " \tYou may need to check all slices.\n" + "3:\t Then click on 'Apply.'\n" + "4:\t In the 'Thresholder' pop-up...click on 'Set to NaN' \(this should set all background pixels to NaN\).\n" + "5:\t In the 'Process Stack?' pop-up... click on 'Yes' to apply to all slices."); /* The full instruction are here because if shown after the Threshold window opens it will obscure the pop-ups */ run("Threshold..."); waitForUser("5th step completed?","Hints:\n1:\t Slider 1\n2:\t Slider 2\n3:\t Apply.\n4:\t 'NaN'\n5:\t All slices \(Yes\)\n6:\t 'OK' below"); /* Very short wait-ForUser to avoid completely obscuring the pop-up requesters */ resetThreshold; close("Threshold"); run("Z Project...", "projection=[Average Intensity]"); if (preProjectBD!=32 && preProjectBD!=24) run("" + preProjectBD + "-bit"); run("Select None"); } else run("Z Project...", "projection=[" + zProjection + "]"); IJ.log("Z Projection applied: " + zProjection); zProjID = getImageID(); if (marginPx>0){ getBB = true; gotBB = false; run("Select None"); if (selectionType>=0) getBB = false; else { run("Select Bounding Box (guess background color)"); getSelectionBounds(xBB, yBB, widthBB, heightBB); if (widthBB>10) gotBB = true; } if (gotBB){ if (autoCrop) run("Crop"); if (isOpen(stackName)) selectWindow(stackName); else exit(stackName + " was expected by was not found"); setBatchMode("show"); makeRectangle(xBB, yBB, widthBB, heightBB); setBatchMode("show"); updateDisplay(); if (autoCrop) run("Crop"); selectImage(zProjID); if (convertToRGB && autoCrop){ selectImage(rgbID); makeRectangle(xBB, yBB, widthBB, heightBB); setBatchMode("show"); updateDisplay(); if (autoCrop) run("Crop"); } } } } if (closeSourceImages){ for (cSi=0; cSi0){ 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; }