// Macro Stitch_Mosaic by Christophe Leterrier // Require S. Preibisch "Grid/Collection Stitching" plugin (included in Fiji). // Takes an input folder containing single or multi-channel tifs that have been taken as "mosaics" by hand (following an axon for example). // If using the sorting step, the input folder can contain several mosaics, and the elements for each must be named by adding a TileID string (such as "WF" followed by "a", "b", "c": WFa, WFb, WFc etc.): // myfirstmosaic_WFa.tif, myfirstmosaic_WFb.tif, myfirstmosaic_WFc.tif, myothermosaic_WFa.tif, myothermosaic_WFb.tif, etc. // The folder can contain single images (non mosaic) that should be named with the TileID ("WF") without a,b,c and followed by "." or "_": mysingleimage_WF.tif, myotherimage_WF.tif) // If only using the stitching step, tiles from each mosaic should be in a separate subfolder of the input folder with no naming constrain. // v1.0 05/05/2016 // v2.0 17/09/2020 added stage position from metadata for Nikon images // v2.1 18/09/2020 preserves scale for mosaics // v3 23/04/2025 Multichannel 3D with options for global or sequential pairwise stitching and use of Nikon metadata (optional) (version named Stitch Mosaic 3 in WIP) // v4 26/11/2025 Maintained for NSTORM, separated image sorting from mosaic stitching (which now includes stage coordinate retrieval and image ehancement) // v5 27/11/2025 Make sure it can be run on any group of folders, add SIM compatibility, sort options macro "Stitch_Mosaic" { print("\n\n\n***** Stitch Mosaic Log *****"); print("\n*** Stitch Mosaic has started ***"); // Main parameters sort_DEF = false; // Perform images sorting and tiles configuration extraction TileID_DEF = "WF"; mos_DEF = true; // Perofrm alignment and stitching batch_DEF = true; // Use batch mode MICRO_ARRAY = newArray("NSTORM (512, 0.16, 0.25)", "SoRA (2304, 0.027, 0.15)", "N-SIM (2048, 0.0325, 0.12)"); MICRO_DEF = MICRO_ARRAY[2]; OVERW_DEF = false; fullWidth_DEF = 512; OVERPIX_DEF = false; pixSize_DEF = 0.25; OVERZ_DEF = false; ZSize_DEF = 0.16; posdata_DEF = true; // use stage position in image metadata (use if global stitching of 3+ images) // Use sequential pairwise rather than global, good for a single pair of images (with posdata=false) STITCH_ARRAY = newArray("Global", "Sequential pairwise"); STITCH_DEF = STITCH_ARRAY[0]; // Define merging mode (maximum intensity or linear blending, preferred specially for sequential pairwise stitching) MERGE_ARRAY = newArray("Max. Intensity", "Linear Blending"); MERGE_DEF = MERGE_ARRAY[0]; enh_DEF = true; // enhance mosaic after generation (see parameters below) closeOut_DEF = false; // close mosaics after generation // Process in 2D, 3D or 2.5D (3D with no preset Z position, better for 3D pairwise and 3D global stitching) DIM_ARRAY = newArray("2", "2.5", "3"); DIM_DEF = DIM_ARRAY[0]; pixSize_DEF = 0.16; ZSize_DEF = 0.15; // Parameters for the optional image enhancement at the end Enhance = false; // Enhance images in addition to fusing them (background subtraction, unsharp mask, contrast enhancement) rb_diam = 50; // diameter of the rolling ball for background substraction (50 for a 512x512 image is OK) um_mask = 0.3; // weight of the mask for unsharp mark (closer to 1 = sharper) setOption("ExpandableArrays", true); // Creation of the dialog box Dialog.create("Stitch Mosaic Options"); Dialog.addMessage("--- General parameters ---"); Dialog.addCheckbox("Perform image sorting (step 1)", sort_DEF); Dialog.addString("ID for tile (followed by a,b,c…)", TileID_DEF); Dialog.addCheckbox("Perform mosaic alignment and stitching (step 2)", mos_DEF); Dialog.addCheckbox("Use silent batch mode", batch_DEF); Dialog.addMessage("--- Hardware parameters ---"); Dialog.addChoice("Microscope", MICRO_ARRAY, MICRO_DEF); Dialog.addCheckbox("Override full sensor width", OVERW_DEF); Dialog.addNumber("Custom full sensor width (pixels)", fullWidth_DEF); Dialog.addCheckbox("Override pixel size", OVERPIX_DEF); Dialog.addNumber("Custom pixel size (µm)", pixSize_DEF); Dialog.addCheckbox("Override Z spacing", OVERZ_DEF); Dialog.addNumber("Custom Z spacing (µm)", ZSize_DEF); Dialog.addCheckbox("Use Nikon stage coordinates", posdata_DEF); Dialog.addMessage("--- Stitching parameters ---"); Dialog.addChoice("Dimensions", DIM_ARRAY, DIM_DEF); Dialog.addChoice("Stitching mode", STITCH_ARRAY, STITCH_DEF); Dialog.addChoice("Merging mode", MERGE_ARRAY, MERGE_DEF); Dialog.addCheckbox("Enhance images", enh_DEF); Dialog.addCheckbox("Close mosaic after generation", closeOut_DEF); Dialog.show(); // Feeding variables from dialog choices sort = Dialog.getCheckbox(); TileID = Dialog.getString(); mos = Dialog.getCheckbox(); batch = Dialog.getCheckbox(); MICRO = Dialog.getChoice(); OVERW = Dialog.getCheckbox(); fullWidthCustom = Dialog.getNumber(); OVERPIX = Dialog.getCheckbox(); pixSizeCustom = Dialog.getNumber(); OVERZ = Dialog.getCheckbox(); ZSizeCustom = Dialog.getNumber(); posdata = Dialog.getCheckbox(); ndim = Dialog.getChoice(); STITCH = Dialog.getChoice(); mergeMode = Dialog.getChoice(); Enhance = Dialog.getCheckbox(); closeOut = Dialog.getCheckbox(); // Get the input folder name inDir = getDirectory("Select a directory full of tif images"); print("Input folder:" + inDir); if (batch == true) setBatchMode(true); // Get name of input folder, parent folder, short name of input folder (before first space in name) parDir = File.getParent(inDir); inName = File.getName(inDir); // Default microscope parameters: pixel size, voxel height, camera full sensor size... if (MICRO == "NSTORM (512, 0.16, 0.25)") { fullWidth = 512; pixSize = 0.16; ZSize = 0.25; Xfactor = 1; Yfactor = -1; Zfactor = 1; } else if (MICRO == "SoRA (2304, 0.027, 0.15)"){ fullWidth = 2304; pixSize = 0.027; ZSize = 0.15; Xfactor = -1; Yfactor = 1; Zfactor = 1; } else if (MICRO == "N-SIM (2048, 0.0325, 0.12)"){ fullWidth = 2048; pixSize = 0.0325; ZSize = 0.12; Xfactor = -1; Yfactor = 1; Zfactor = 1; } // Override values if custom if (OVERW == true) { fullWidth = fullWidthCustom; } if (OVERPIX == true) { pixSize = pixSizeCustom; } if (OVERZ == true) { ZSize = ZSizeCustom; } if (STITCH == "Sequential pairwise") pairwise = true; else pairwise = false; if (sort == true) { print("\n*** Image sorting (step 1) ***"); // Count the number of files ending with ".tif" as the number of images imN = countTif(inDir); print("Number of .tif images in input folder:" + imN); // Store the images names in an array allNames = getFileList(inDir); imNames = newArray(imN); for (i = 0; i < allNames.length; i++) { nLength = lengthOf(allNames[i]); if (substring(allNames[i], nLength-4, nLength) == ".tif") { imNames[i] = allNames[i]; } } // Create a new folder that will have each mosaic element (individual multi-channel stacks) inside its own subfolder (necessary for the fusion plugin) // Mosaic elements name must contain WFa, WFb, WFc etc. preceded by the SAME string. // If some images are single (not part of a mosaic) they must contain "WF_" or "WF." in their name. // Make temp folder where individual folders will contain mosaic elements (single multi-channel stacks) sortDir = parDir + File.separator + inName + " folders" + File.separator; print("Sorted folder: " + sortDir); if (File.isDirectory(sortDir) == false) { File.makeDirectory(sortDir); } mosCount = 0; // counter of mosaic number // Loop on images names for (i = 0; i < imN; i++) { // If image is first in a mosaic series (name has WFa, followers have WFb, WFc etc...) WFi = indexOf(imNames[i], TileID + "a"); WFi2 = indexOf(imNames[i], TileID + "A"); // If image is isolated (not part of mosaic) it has "WF_" or "WF." in its name WFn = indexOf(imNames[i], TileID + "."); WFc = indexOf(imNames[i], TileID + "_"); // Store mosaic elements / individual images in separated folders if (WFi > -1 || WFi2 > -1 || WFn > -1 || WFc > -1) { // Create a subfolder of temp folder subName = substring(imNames[i], 0, WFi + WFi2 + WFn + WFc + 5); // make the subfolder fDir = sortDir + subName + File.separator; if (File.isDirectory(fDir) == false) { File.makeDirectory(fDir); } // Loop on all images in input folder to find all parts of the mosaic (including the "WFa" or "WF_" / "WF." one) // save the mosaic elements flag2 = 0; // flag for (j = 0; j < imN; j++) { if (indexOf(imNames[j], subName) > -1) { // only for images of this mosaic // open image open(inDir + imNames[j]); // if first image from mosaic, get scale and store in array flag2++; // flag to get scale at first image of the mosaic if (flag2 == 1) { mosCount++; // mosaic counter } if (Enhance == true) { getDimensions(w, h, ch, sl, fr); for (k = 0; k < ch; k++) { Stack.setChannel(k+1); run("Subtract Background...", "rolling=" + rb_diam); } } save(fDir + imNames[j]); close(); } } print("Mosaic #" + mosCount + " (" + flag2 + " tiles" + "): " + fDir); } } print("Number of mosaics: " + mosCount); } if (mos == true) { // Perform stiching using S. Preibisch's "Grid/Collection Stitching" plugin // part of Fiji see http://imagej.net/Image_Stitching#Grid.2FCollection_Stitching print("\n*** Mosaic alignment and stitching (step 2) ***"); // Useful if macro just makes alignment form existing sorted folder if (sort == false) { sortDir = inDir; print ("Sorted tiles folder: " + sortDir); } // Get folder names and test of they contain a single image foldNames = getFileList(sortDir); for (i = 0; i < foldNames.length; i++) { // Detect if single image TIF_NUMBER = countTif(sortDir + foldNames[i]); if (TIF_NUMBER == 1) isSingle = 1; else isSingle = 0; } // Subtract background on individual tiles if "Enhance" is checked if (Enhance == true) { // Create enhanced tiles folder enhDir = parDir + File.separator + inName + " enh" + File.separator; print("Enhanced tiles folder: " + enhDir); if (File.isDirectory(enhDir) == false) { File.makeDirectory(enhDir); } //Loop on temp subfolders to process mosaic tiles print(" Enhancing tiles"); for (i = 0; i < foldNames.length; i++) { // create folder in the Enhanced dir folder if (File.isDirectory(enhDir + foldNames[i]) == false) { File.makeDirectory(enhDir + foldNames[i]); } // call the function to subtract background on all tiles in the folder and save in the enhanced dir folder batchSB(sortDir + foldNames[i], enhDir + foldNames[i]); } // Replace sortDir by Enhanced directory for the rest of the macro sortDir = enhDir; } // Generate TileConfiguration.txt file from image metadata if option checked if (posdata == true) { //Loop on temp subfolders to process mosaic tiles for (i = 0; i < foldNames.length; i++) { if (isSingle == 0) { // only if not single image // call the function to make the configuration file from stage position metadata in images (Nikon version) makeTileConfiguration(sortDir + foldNames[i]); } } } // Create mosaics folder mosDir = parDir + File.separator + inName + " mosaics" + File.separator; print("\n Mosaic folder: " + mosDir); if (File.isDirectory(mosDir) == false) { File.makeDirectory(mosDir); } //Loop on temp subfolders to process mosaics for (i = 0; i < foldNames.length; i++) { // Stitch all images in subfolder if (isSingle == 0) { // open first image to get spatial scale tileNames = getFileList(sortDir + foldNames[i]); tileNames = Array.sort(tileNames); open (sortDir + foldNames[i] + tileNames[0]); getPixelSize(scaleUnit, pixelWidth, pixelHeight); close(); // if using stage positions in images metadata if (posdata == true) { // call the function to make the configuration file from stage position metadata in images (Nikon version) if (pairwise == false) { // global stitching print("--- Running global stitching with preset positions on " + foldNames[i]); run("Grid/Collection stitching", "type=[Positions from file] order=[Defined by TileConfiguration] directory=[" + sortDir + foldNames[i] +"] layout_file=TileConfiguration.txt fusion_method=[" + mergeMode + "] regression_threshold=0.30 max/avg_displacement_threshold=2.50 absolute_displacement_threshold=3.50 compute_overlap subpixel_accuracy computation_parameters=[Save computation time (but use more RAM)] image_output=[Fuse and display]"); } else { // pairwise stitching print("--- Running sequential pairwise stitching with preset positions on " + foldNames[i]); seqPair(sortDir + foldNames[i], 1); } } // Cas without stage positions else { if (pairwise == false) { // global stitching print("--- Running global stitching without preset positions on " + foldNames[i]); run("Grid/Collection stitching", "type=[Unknown position] order=[All files in directory] directory=[" + sortDir + foldNames[i] +"] output_textfile_name=TileConfiguration.txt fusion_method=[" + mergeMode + "] regression_threshold=0.30 max/avg_displacement_threshold=2.50 absolute_displacement_threshold=3.50 ignore_z_stage subpixel_accuracy computation_parameters=[Save computation time (but use more RAM)] image_output=[Fuse and display]"); } else { // pairwise stitching print("--- Running sequential pairwise stitching without preset positions on " + foldNames[i]); seqPair(sortDir + foldNames[i], 0); } } } // unless it's a single image else { print("--- No stitching for single image " + foldNames[i]); insideNames = getFileList(sortDir + foldNames[i]); open(insideNames[0]); if (nSlices > 1) Stack.setDisplayMode("composite"); } // Enhance fused image (contrast enhancement looping on channels) if (Enhance == true) { print(" Enhancing fused image"); getDimensions(w, h, ch, sl, fr); if (sl==1) { for (j = 0; j < ch; j++) { Stack.setChannel(j+1); run("Unsharp Mask...", "radius=1 mask=0.3"); getMinAndMax(min, max); setMinAndMax(0, max); } } else { run("Unsharp Mask...", "radius=1 mask=0.3"); for (j = 0; j < ch; j++) { Stack.setChannel(j+1); getMinAndMax(min, max); setMinAndMax(0, max); } } } // If no Enhance just reset the contrast else { getDimensions(w, h, ch, sl, fr); for (j = 0; j < ch; j++) { if (nSlices > 1) Stack.setChannel(j+1); getMinAndMax(min, max); setMinAndMax(0, max); } } // Apply stored scale and set to first channel run("Set Scale...", "distance=1 known=" + pixelWidth + " pixel=1 unit=" + scaleUnit); Stack.setChannel(1); // Save resulting image fusedName = substring(foldNames[i], 0, lengthOf(foldNames[i]) - 1) + "_fused.tif"; rename(fusedName); print(" Saved fused image as " + mosDir + fusedName + "\n"); save(mosDir + fusedName); if (closeOut == true) close(); } } if (batch == true) setBatchMode("exit and display"); print("\n*** Stitch Mosaic has finished ***"); } function makeTileConfiguration(InputDir) { // Get all file names TAllNames = getFileList(InputDir); Array.sort(TAllNames); TnL = TAllNames.length; TAllExt = newArray(TnL); // Create extensions array and test if tif Tntif = 0; for (Tk = 0; Tk < TnL; Tk++) { TAllNamesParts = getFileExtension(TAllNames[Tk]); TAllExt[Tk] = TAllNamesParts[1]; if (indexOf(toLowerCase(TAllExt[Tk]), ".tif") > -1) Tntif++; // condition to catch .tif, .tiff, .TIF, .TIFF... } // Define arrays for metadata of each tile NamesArray = newArray(Tntif); XPosArray = newArray(Tntif); YPosArray = newArray(Tntif); leftArray = newArray(Tntif); topArray = newArray(Tntif); sizeXArray = newArray(Tntif); sizeYArray = newArray(Tntif); if (ndim == "3") ZPosArray = newArray(Tntif); // Get the coordinates for each tile from the metadata for (Tl = 0; Tl -1) { // open only tif files open(InputDir + TAllNames[Tl]); // get X and Y stage position in µm, top and left coordinates of the sensor crop in pixels, size of image in pixels, Z coordinate in µm (Nikon metadata) if (ndim != "3") XYCoor = getPropValues("dXpos,dYPos,left,top,uiWidth,uiHeight"); else XYCoor = getPropValues("dXpos,dYPos,left,top,uiWidth,uiHeight,dZPos"); // Assign name of the image, X and Y stage position, left/top coordinates, image size, Z coordinate in arrays NamesArray[Tl] = TAllNames[Tl]; XPosArray[Tl] = XYCoor[0]; YPosArray[Tl] = XYCoor[1]; leftArray[Tl] = XYCoor[2]; topArray[Tl] = XYCoor[3]; sizeXArray[Tl] = XYCoor[4]; sizeYArray[Tl] = XYCoor[5]; if (ndim == "3") ZPosArray[Tl] = XYCoor[6]; print("NameArray[" + Tl + "]: " + NamesArray[Tl]); print(" XPosArray[" + Tl + "]=" + XPosArray[Tl] + ", YPosArray[" + Tl + "]=" + YPosArray[Tl]); print (" leftArray[" + Tl + "]=" + leftArray[Tl] + ", topArray[" + Tl + "]=" + topArray[Tl] + ", sizeXArray[" + Tl + "]=" + sizeXArray[Tl] + ", sizeYArray[" + Tl + "]=" + sizeYArray[Tl]); if (ndim == "3") print (" ZPosArray[" + Tl + "]=" + ZPosArray[Tl]); // close image close(); } } // Define intermediate and output array Xcorner = newArray(Tntif); Ycorner = newArray(Tntif); Xcoor = newArray(Tntif); Ycoor = newArray(Tntif); XArrayT = newArray(Tntif); YArrayT = newArray(Tntif); Zcoor = newArray(Tntif); ZArrayT = newArray(Tntif); // Caclulate the coordinates of the top left corner of first tile Xcorner[0] = parseFloat(Xfactor*XPosArray[0])/pixSize + (parseFloat(leftArray[0])-(fullWidth/2)); Ycorner[0] = parseFloat(Yfactor*YPosArray[0])/pixSize + (parseFloat(topArray[0])-(fullWidth/2)); // Calculate the coordinates of the top left corner of each tile for (Tm = 1; Tm -1) { // open only tif files open(InputDir + BAllNames[Bl]); getDimensions(w, h, ch, sl, fr); if (sl == 1) { for (Bm = 0; Bm < ch; Bm++) { Stack.setChannel(Bm+1); run("Subtract Background...", "rolling=" + rb_diam); } } else run("Subtract Background...", "rolling=" + rb_diam); save(OutputDir + BAllNames[Bl]); close(); } } return; } function getFileExtension(Name) { nameparts = split(Name, "."); shortname = nameparts[0]; if (nameparts.length > 2) { for (n = 1; n < nameparts.length - 1; n++) { shortname += "." + nameparts[n]; } } extname = "." + nameparts[nameparts.length - 1]; namearray = newArray(shortname, extname); return namearray; } function getPropValues(keys) { // get values of image properties defined by a "keys" string with property names separated by commas // return an array of values correspoonding to each key // define delimiters between a property and its value, with a parenthesed version for the split del1 = " = "; del2 = "; "; // get properties Info = Property.getInfo(); // split by lines InfoArray = split(Info, "\n"); pL = InfoArray.length; PropArray = newArray(pL); // array of property names ValArray = newArray(pL); // array of property values // fill the property names and values arrays for (o = 0; o < pL; o++) { // split each line according to delimiters defined above (del1, del2) or assign NaN if no delimiter detected if (indexOf(InfoArray[o], del1) > -1) LineSplit = split(InfoArray[o], "(" + del1 + ")"); // the split string is parenthesed to work as regular expression else if (indexOf(InfoArray[o], del2) > -1) LineSplit = split(InfoArray[o], "(" + del2 + ")"); else LineSplit = newArray(NaN, NaN); PropArray[o] = LineSplit[0]; ValArray[o] = LineSplit[1]; } // split keys (asked property names) KeyArray = split(keys, ","); kL = KeyArray.length; KeyValArray = newArray(kL); // for each key of KeyArray, look in the PropArray property names array, and when name is found, assign corresponding value from ValArray to the corresponding KeyValArray slot for (p = 0; p < kL; p++) { flag = 0; for (q = 0; q < pL; q++) { if (PropArray[q] == KeyArray [p]) { flag = 1; KeyValArray[p] = ValArray[q]; } } if (flag == 0) KeyValArray[p] = NaN; // if key not found, assign NaN for that slot } // return the key values array return KeyValArray; } function writeTileConfiguration(nameA, xA, yA, zA, fp) { // beginning of the file if (ndim == "2") FileString = "# Define the number of dimensions we are working on\ndim = 2\n\n# Define the image coordinates\n"; else FileString = "# Define the number of dimensions we are working on\ndim = 3\n\n# Define the image coordinates\n"; // lines for each image and its coordinates for (r = 0; r < nameA.length; r++) { if (ndim == "2") FileString += nameA[r] + "; ; (" + xA[r] + ", " + yA[r] + ")\n"; else if (ndim == "2.5") FileString += nameA[r] + "; ; (" + xA[r] + ", " + yA[r] + ", 0.0)\n"; else FileString += nameA[r] + "; ; (" + xA[r] + ", " + yA[r] + ", " + zA[r] + ")\n"; } // save file File.saveString(FileString, fp + "TileConfiguration.txt"); return; } function seqPair(ip, po) { // takes the folder path and a position option (1=preset positions, 0=no preset positions) // Make an array of tif images names in folder pairFileNames = getFileList(ip); pairFiles = newArray; t = 0; for (s=0; s -1) { pairFiles[t] = pairFileNames[s]; t++; } } if (po == 1) { presCoords = getPresetPos(ip); presX = newArray(presCoords.length); presY = newArray(presCoords.length); if (ndim != "2") presZ = newArray(presCoords.length); for (w=0; w -1) Fntif++; // condition to catch .tif, .tiff, .TIF, .TIFF... } return Fntif; } function getPresetPos(inp) { presetString = File.openAsString(inp + "TileConfiguration.txt"); presetLines = split(presetString, "\n"); pairCoords = newArray(presetLines.length-4); for (v=4; v