From cb77498106e243f10d648e3ad2df2bc4e2db961d Mon Sep 17 00:00:00 2001 From: Kai Schleicher <kai.schleicher@unibas.ch> Date: Thu, 9 Jan 2020 15:53:27 +0100 Subject: [PATCH] feature: adds support for 11bit images fixes bead segmentation method, adds rescaling factor and mean Noise value. --- .../Plugins/NoiSee/NoiSee_Bead_Analysis.ijm | 2028 +++++++++-------- 1 file changed, 1019 insertions(+), 1009 deletions(-) diff --git a/src/main/resources/scripts/Plugins/NoiSee/NoiSee_Bead_Analysis.ijm b/src/main/resources/scripts/Plugins/NoiSee/NoiSee_Bead_Analysis.ijm index e69f6b8..ab5b1aa 100644 --- a/src/main/resources/scripts/Plugins/NoiSee/NoiSee_Bead_Analysis.ijm +++ b/src/main/resources/scripts/Plugins/NoiSee/NoiSee_Bead_Analysis.ijm @@ -1,1009 +1,1019 @@ -//////////// NoiSee Beads Analysis /////////////////////////////////////////////////////// -// Signal-to-Noise-Ratio evaluation suite -// written by Kai Schleicher, Niko Ehrenfeuchter, IMCF Basel -// licence: GPLv3 - - -/// TODOs: -// - consider using lacan's LibInstaller: https://github.com/lacan/LibInstaller - -/// Naming conventions -// - "imgs_" - image IDs of stacks -// - "img_" - image IDs of 2D images -// - "rgb_" - image IDs of 2D RGB images -// - "roi_" - index number of a ROI manager entry - - -//////// Script Parameters, see https://imagej.net/Script_Parameters for details -#@ String (label="<html><img src=\"https://drive.switch.ch/index.php/s/zyKSwuCMKMs6ZQ1/download\"></html>",value="<html><img src=\"https://drive.switch.ch/index.php/s/54H9s84wYou3uBd/download\"></html>",visibility=MESSAGE,persist=false) msg_logo -#@ String(visibility=MESSAGE, label="NoiSee -",value="Beads SNR analysis",persist=false) msg_title -#@ File(label="Beads time-series image",description="2D time-lapse acquisition of fluorescent beads") beadsimage -#@ Integer(label="Beads diameter (in pixels)",description="approximate bead diameter (in pixels)",value=15) beads_diameter -#@ Integer(label="Find Maxima noise tolerance",description="typical values: [PMT=50] [HyD (photon counting)=10] [Camera=500]",value=50) beads_noisetolerance -#@ Boolean(label="Create Kymographs",description="visual indicator for drift and bleaching",value="true") make_kymographs -#@ Boolean(label="Save additional measurements",description="store 'StdDev', 'SNR', 'Mean' and 'bleaching' measurements",value="false") save_measurements -#@ Boolean(label="Save results as PDF",description="generate a PDF with images and plots",value="true") save_pdf -#@ Boolean(label="Keep ROI images open",description="if disabled ROI visualizations will only be added to PDF",value="false") keep_roiimages -#@ String(visibility=MESSAGE,label="Note:",value="all currently open images will be closed",persist=false) msg_note_close - - -// valid log levels: 0 (quiet), 1 (info messages), 2 (debug messages) -LOGLEVEL=0; - -// save contents of the 'Log' window in a text file: -save_log=false; - - -////////////////// function definitions /////////////////////////////// - -function duplicateImage(id_orig, new_name, single){ - // duplicate an image (given by ID), give it a new name and return the new ID - selectImage(id_orig); - run("Select None"); // clear selection to avoid messing up the duplication - if (single == true) { - run("Duplicate...", "use"); // duplicate single slice only - } else { - run("Duplicate...", "duplicate"); // duplicate the entire stack - } - rename(new_name); - return getImageID(); -} - -function duplicateAndClose(id_orig) { - // helper function for re-ordering images when creating a PDF - // the PDF exporter places images one per page in the order when they were created, so the only - // way to change that order is to duplicate an existing image into a new one, closing the old... - selectImage(id_orig); - new_id = duplicateImage(id_orig, getTitle(), false); - closeImage(id_orig); - return new_id; -} - -function plotResult(Title, XLabel, YLabel, col) { - // create a high-res plot of the values in column "col" of the results table - // using the given title and labels - // returns the ID of the newly created plot image - if (nResults == 0) { - print("Unable to create plot [" + Title + "], results table empty!"); - return; - } - print("Creating plot [" + Title + "] for " + nResults + " results."); - Yvalues=newArray(nResults); - Ymin = getResult(col, 0); - Ymax = Ymin; - - // store Y-values in an array, get min and max: - for (i=0; i < nResults; i++) { - Yvalues[i] = getResult(col, i); - Ymax = maxOf(Ymax, Yvalues[i]); - Ymin = minOf(Ymin, Yvalues[i]); - } - // make sure not to over-emphasize data that has only little variance by - // setting the plot limits to min/max +/- 10: - Ymin = round(Ymin - 10.5); - Ymax = round(Ymax + 10.5); - print("Plot Y-axis range: " + Ymin + " - " + Ymax); - - - temp_plot_name = "NoiSee temporary plot window"; - Plot.create(temp_plot_name, XLabel, YLabel); - Plot.setLimits(0, nResults, Ymin, Ymax); - Plot.setLineWidth(2); - Plot.setColor("#6688dd"); - Plot.add("line",Yvalues); - Plot.setColor("red"); - Plot.add("circles", Yvalues); - Plot.show(); - Plot.makeHighResolution("plot of " + col, 2); - closeImage(temp_plot_name); - selectWindow("plot of " + col); - plot_id = getImageID(); - addTextToImage(plot_id, true, "Center", 48, Title); - addTextToImage(plot_id, true, "Center", 24, ""); - return plot_id; -} - -function erode(image_id, num_iterations) { - // run the "erode" operation multiple times on an image / stack - selectImage(image_id); - logd("Eroding current image by " + num_iterations + " iterations..."); - for (i=0; i < num_iterations; i++) { - run("Erode", "stack"); - } -} - -function binaryToSelection(image_id, roi_name, savepath) { - // create a selection from a binary image, add it to the ROI Manager with the given name and - // return the ID of the newly created ROI - selectImage(image_id); - if (is("binary") == false) - exit("Image " + getTitle() + " is not binary, not creating a selection!"); - // "Create Selection" requires a threshold to be set, see the source code in - // https://imagej.nih.gov/ij/developer/source/ij/plugin/filter/ThresholdToSelection.java.html - // particularly for method selected(x, y) as the documentation is not very clear about this! - setThreshold(255, 255); - run("Create Selection"); - return selectionToROI(roi_name, savepath); -} - -function selectionToROI(roi_name, savepath) { - // add an existing selection as a new ROI with the given name and return the ID - roiManager("Add"); - roi_id = roiManager("count") - 1; // ROIs are appended, hence "count" == newest ID + 1 - roiManager("Select", roi_id); - roiManager("Rename", roi_name); - roiManager("Show None"); // clean up display behavior of the ROI manager - if (savepath != "") - roiManager("save selected", savepath + "/roi-" + roi_name + ".zip"); - return roi_id; -} - -function pickResults(count) { - // generate a list of index numbers creating a (more or less) equally spaced subset of the - // current results - if (nResults < count) { - setBatchMode("exit and display"); // exit batch mode and show images - exit("less than " + count + " results found, check your image!"); - } - indexes = newArray(count); - halfstep = nResults / (count * 2); - for (i=0; i<count; i++) { - row = round((i*2+1) * halfstep); - indexes[i] = row; - } - return indexes; -} - -function createCenterLines(image_id, res, len, namepfx) { - // create line selections of length "2*len" for the center of mass of a given subset of the - // current results ("res" is an array with the index numbers of results to be used) - roiManager("Set Color", "magenta"); - run("Select None"); - roiManager("reset"); // make sure the ROI Manager only contains our newly created line ROIs - selectImage(image_id); - for (i=0; i<res.length; i++) { - index = res[i]; - cx = getResult("XM", index - 1); - cy = getResult("YM", index - 1); - logd("draw line for result " + index + " (c-o-m: " + cx + " / " + cy + ")"); - makeLine(cx - len, cy, cx + len, cy); - roiManager("add"); - roiManager("select", roiManager("count") - 1); - roiManager("rename", namepfx + index); - } - roiManager("Show all without labels"); - roiManager("Set Line Width", 3); -} - -function lineKymograph(image_id) { - // expects a number of "line" ROIs in the ROI Manager, creates a Kymograph (stack or orthogonal - // profile) for each of them, returning the newly created image IDs as an array - num_rois = roiManager("count"); - ortho_imgs = newArray(num_rois); - for (i=0; i<num_rois; i++) { - selectImage(image_id); - roiManager("select", i); - roi_name = call("ij.plugin.frame.RoiManager.getName", i); - run("Reslice [/]...", "output=1.000 slice_count=1"); - ortho_imgs[i] = getImageID(); - rename("kymograph-" + roi_name); - } - return ortho_imgs; -} - -function makeKymographMontage(imgs_beadmask, imgs_beads, diameter, basepath) { - // calculate the expected bead area to set the size parameter for "Analyze Particles" below: - beads_area = pow(diameter / 2, 2) * 3.14; - lo = beads_area * 0.6; // lower limit - hi = beads_area * 1.4; // upper limit - logi("range of expected beads areas: " + lo + " - " + hi); - - img_tmp = duplicateImage(imgs_beadmask, "beadmask-T0", true); - run("Set Measurements...", "area center"); // center-of-mass gives us the bead location - run("Analyze Particles...", "size=" + lo + "-" + hi+ " display clear add"); - resetThreshold(); - closeImage(img_tmp); - saveAs("Results", basepath + "/individual-beads.txt"); - logi("found " + nResults + " individual beads (excluding clustered / clumped ones)"); - - indexes = pickResults(4); // select a subset of four beads from the results - imgs_tmp = duplicateImage(imgs_beads, "tmp-stack", false); - createCenterLines(imgs_tmp, indexes, diameter, "bead-"); - lineKymograph(imgs_tmp); - closeImage(imgs_tmp); - run("Images to Stack", "name=[kymograph-stack] title=[kymograph-] use"); - imgs_tmp = getImageID(); - run("Scale...", "x=16 y=16 z=1.0 depth=4 interpolation=None process create"); - imgs_tmp2 = getImageID(); - closeImage(imgs_tmp); - run("Make Montage...", "columns=2 rows=2 scale=1 border=40 font=16 label use"); - closeImage(imgs_tmp2); - rename("Kymographs Montage"); - run("cool"); - return getImageID(); -} - -function findROI(roi_name) { - // scan for an ROI with the given name and return its ID - // WARNING: will terminate the macro if no ROI with the name exists! - for (roi_id=0; roi_id<roiManager("count"); roi_id++) { - cur_name = call("ij.plugin.frame.RoiManager.getName", roi_id); - if (cur_name == roi_name) - return roi_id; - } - exit("ERROR: ROI with name '" + roi_name + "' doesn't exist!"); -} - -function makeFilledROIImage(image_id, roi_name, inverse) { - roi_id = findROI(roi_name); - new_id = duplicateImage(image_id, "ROI - " + roi_name, true); - logd("creating filled ROI image for [" + getTitle() + "] and ROI [" + roi_name + "]"); - run("RGB Color"); - setForegroundColor(255, 57, 0); - roiManager("Select", roi_id); - if (inverse == true) { - run("Make Inverse"); - } - // roiManager("Fill"); // WARNING: this command doesn't respect the "make inverse" from above! - run("Fill", "slice"); - return new_id; -} - -function drawROIsOnImage(image_id, namepfx, color) { - // use a given image (or stack) to create an RGB image where all ROIs starting with the given - // prefix are rendered in the specified color - // returns the ID of the new RGB image - tmp_id = duplicateImage(image_id, "tmp_image", true); - run("RGB Color"); - // scan the ROI manager for the given prefix and assemble an array with corresponding IDs: - num_rois = roiManager("count"); - roi_ids = newArray(); - for (i=0; i<num_rois; i++) { - roiManager("select", i); - roi_name = call("ij.plugin.frame.RoiManager.getName", i); - if (startsWith(roi_name, namepfx)) { - roi_ids = Array.concat(roi_ids, i); - } - } - // Array.print(roi_ids); - - // now select the IDs, combine them and flatten them into the new image: - roiManager("Select", roi_ids); - roiManager("Combine"); - roiManager("Add", "00ff00", 0); - // the new ROI-ID equals "num_rois": - roiManager("Select", num_rois); - roiManager("Set Fill Color", color); - run("Flatten"); - new_id = getImageID(); - rename("Kymographs Selections"); - roiManager("Delete"); - closeImage(tmp_id); - return new_id; -} - -function measureSelection(image_id, title, basepath, multi, savetxt) { - // measure (using the existing ROIs) on a given image and save the results - // in a text file at the given path if requested - - selectImage(image_id); - run("Restore Selection"); - run("Clear Results"); - if (multi) { - roiManager("Multi Measure"); // measure on all slices of a stack - } else { - // run("Measure"); // is there any difference to roiManager("Measure") ?? - roiManager("Measure"); - } - - if (savetxt == false) - return; - - File.makeDirectory(basepath); - if (File.isDirectory(basepath) == false) - exit("ERROR creating directory [" + basepath + "], stopping."); - - saveAs("Results", basepath + "/" + title + ".txt"); -} - -function flattenOverlay(image_id) { - selectImage(image_id); - title = getTitle(); - run("Flatten"); - new_id = getImageID(); - closeImage(image_id); - selectImage(new_id); - rename(title); - return new_id; -} - -function closeImage(image_id) { - if (isOpen(image_id) == false) - return; - selectImage(image_id); - close(); -} - -function dressImage(image_id, lut_name, enhance, desc) { - // applies a given LUT to an image, applies the "Enhance contrast" command to it if the - // "enhance" parameter is non-negative, adds a calibration bar (non-overlay) and optionally - // adds a text description below the image - // returns the ID of the new image, closing the old one - selectImage(image_id); - logi("dressImage(" + getTitle() + ", " + lut_name + ", " + enhance + ")"); - run("Select None"); - logd("applying LUT " + lut_name); - run(lut_name); // WARNING: applying a LUT changes the selected image!! - selectImage(image_id); - if (enhance >= 0) { - logd("enhancing contrast (saturated=" + enhance + ")"); - run("Enhance Contrast", "saturated=" + enhance); - } - // sometimes calibration bar doesn't work depending on when / where the image window appears on - // the screen, so make sure the right window is selected / active: - selectImage(image_id); - logd("adding calibration bar"); - title = getTitle(); - run("Calibration Bar...","location=[Upper Right] fill=None " + - "label=White number=5 decimal=0 font=12 zoom=3.3 bold"); - new_id = getImageID(); - rename(title); - if (desc != "") - addTextToImage(new_id, false, "Center", 22, desc); - closeImage(image_id); - return new_id; -} - -function createTable(title, pairs, fname) { - // create a new 2-column table with the given title, using the key-value tuples from the array - // given in "pairs" to fill the cells, saving it as to the text file "fname" - titleb = "[" + title + "]"; - if (isOpen(title)) - print(titleb, "\\Clear"); - else - run("Table...", "name=" + titleb + " width=320 height=340"); - - print(titleb, "\\Headings:Key\tValue"); - for (i = 0; i < pairs.length; i = i + 2) { - print(titleb, pairs[i] + "\t" + pairs[i+1]); - } - selectWindow(title); - if (fname != "") - saveAs("Results", fname); -} - -function createTableImage(title, pairs) { - // create an image with a 2-column table from the given array of pairs - // returns the ID of the new image - newImage("tmp_table_col1", "RGB white", 1, 1, 1); - img_col1 = getImageID(); - newImage("tmp_table_col2", "RGB white", 1, 1, 1); - img_col2 = getImageID(); - - for (i = 0; i < pairs.length; i = i + 2) { - addTextToImage(img_col1, false, "Left", 28, pairs[i]); - addTextToImage(img_col2, false, "Left", 28, pairs[i+1]); - } - selectImage(img_col2); - col2h = getHeight(); - col2w = getWidth() + 28; - run("Canvas Size...", "width=" + col2w + " height=" + col2h + " position=Top-Right"); - run("Combine...", "stack1=tmp_table_col1 stack2=tmp_table_col2"); - img_table = getImageID(); - rename(title); - - addTextToImage(img_table, true, "Center", 18, ""); - addTextToImage(img_table, true, "Center", 36, title); - - return img_table; -} - -function addTextToImage(image_id, above, justify, font_size, text) { - selectImage(image_id); - logd("addTextToImage(" + getTitle() + ", ''" + text + "'')"); - run("Select None"); - - curw = getWidth(); - neww = curw; - curh = getHeight(); - newh = curh + font_size + 2; - setFont("SansSerif", font_size); - textw = getStringWidth(text); - if (textw > curw) - neww = textw; - - if (above == true) { - canvpos = "Bottom-" + justify; - textpos = font_size + 2; - } else { - canvpos = "Top-" + justify; - textpos = newh; - } - - setForegroundColor(30, 30, 30); - setBackgroundColor(255, 255, 255); - run("Canvas Size...", "width=" + neww + " height=" + newh + " position=" + canvpos); - drawString(text, 0, textpos); -} - -function arrangeImages(ids, y_coord) { - // try to place the image windows given in the "ids" array horizontally on the screen, - // arranging them in equal distance and adjusting the zoom level if necessary - selectImage(ids[0]); - getLocationAndSize(_, _, window_width, window_height); - px_width = getWidth(); - px_height = getHeight(); - border_x = window_width - px_width; - border_y = window_height - px_height; - // try varius zoom levels until the windows fit on the screen and calculate the spacing: - for (s=0; s<4; s++) { - zoom = 1 - 0.25 * s; - width = px_width * zoom + border_x; - height = px_height * zoom + border_y; - total_width = ids.length * width; - logd("width " + width + " (zoom " + zoom + ", total width " + total_width + ")"); - remaining = screenWidth - total_width; - if (remaining > 0) { - spacing = remaining / (ids.length + 1); - logd("zoom / width / spacing: " + zoom + " / " + width + " / " + spacing); - break; - } else { - continue; - } - // if we reach this point we can't seem to calculate a zoom factor, so we give up without - // placing / relocating the image windows (doesn't make sense): - return; - } - - // now place the images: - for (i=0; i<ids.length; i++) { - selectImage(ids[i]); - x_coord = width * i + spacing * (i+1); - logd("placing image at " + x_coord + " / " + y_coord); - setLocation(x_coord, y_coord, width, height); - getLocationAndSize(_, _, window_width, window_height); - if (window_width != width) { - delta = (width - window_width) / 2; - setLocation(x_coord + delta, y_coord, width, height); - } - } -} - -function stripOmeSuffix(orig) { - if (endsWith(orig, ".ome")) { - index = lastIndexOf(orig, ".ome"); - orig = substring(orig, 0, index); - } - return orig; -} - -function detectValueRange() { - // detect the *actual* value range bit depth of a 16-bit image, i.e. if the - // image contains pixels in a range from 5 to 3978 the value range depth is - // 12 bits (possible values from 0 to 4095) - - // define the value range bit depths to test for (8 to 16): - possible_depths = Array.getSequence(17); - possible_depths = Array.slice(possible_depths, 8, 17); - - Stack.getStatistics(_, _, _, max, _, _); - logi("maximum pixel value in stack: " + max); - - saturated = false; - actual_depth = 16; - // traverse the possible depths array backwards: - for (i = lengthOf(possible_depths) - 1; i > 0; i--) { - sat = pow(2, possible_depths[i]) - 1; - logd("checking " + possible_depths[i] + " bit range (0-" + sat + ")"); - - if (max == sat) { - saturated = true; - actual_depth = possible_depths[i]; - logd("saturated " + actual_depth + "-bit image detected!"); - } else { - next_lower = pow(2, possible_depths[i-1]); - if (max < sat && max >= next_lower) { - actual_depth = possible_depths[i]; - logd("non-saturated " + actual_depth + "-bit image detected!"); - } - } - } - - msg_sat = "(not saturated)"; - if (saturated) - msg_sat = "(SATURATED!)"; - logd("\ndetected value range: " + actual_depth + " bit " + msg_sat); - - res = newArray(actual_depth, saturated); - return res; -} - -function mapTo8bitPreservingSaturation(effectiveBits) { - // convert an image with 12 or 16 bit effective value range depth to 8 bit - // preserving saturation in the sense that only saturated pixels (65535 for - // 16 bits and 4095 for 12 bits) are mapped to the 8 bit saturation value - // (255), avoiding the usual binning that would happen if simply the display - // range was adjusted to the min/max values of the image (resulting in - // over-saturation of wrongly mapped pixels) - if (effectiveBits == 16) { - divisor = 257.50; - } else if (effectiveBits == 12) { - divisor = 16.09; - } else { - print("Unsupported VALUE range detected: " + effectiveBits + " bits"); - print("Input image needs to have a value range of 8, 12 or 16 bits!"); - exit_show(); - } - msg = "mapping " + effectiveBits + " bit value range to 8 bit"; - msg = msg + " (dividing pixel values by " + divisor + ")"; - logd(msg); - run("Divide...", "value=" + divisor + " stack"); - setMinAndMax(0, 255); - run("8-bit"); -} - -function clear_workspace() { - /* - * Ensure a clean workspace, i.e. - * - no open image windows - * - an empty Log window - * - no ROIs - * - no results - */ - - // clear the Log window - print("\\Clear"); - - // close all open images - if (nImages > 0) { - run("Close All"); - } - - // make sure the ROI manager is empty - roiManager("reset"); - - // empty the results table - run("Clear Results"); -} - -function reset_ij_options() { - /* - * Make sure to set all relevant ImageJ options to a useful state for being - * able to provide consistent results independent of what has been - * configured or done by the user before. - * - * IMPORTANT: the order of the commands is highly relevant, on changes - * careful tests need to be done to ensure correct behavior. - */ - - // disable inverting LUT - run("Appearance...", " menu=0 16-bit=Automatic"); - - // set foreground color to be white, background black - run("Colors...", "foreground=white background=black selection=red"); - - // pad edges when eroding a binary image - run("Options...", "pad"); - - // set saving format to .txt files - run("Input/Output...", "file=.txt"); - - - // ============= WARNING WARNING WARNING =============// - // the commands below this marker *MUST NOT* be moved - // upwards as they seem to be overridden by some of the - // "run(...)" calls otherwise, turning them useless! - - // set "Black Background" in "Binary Options" - setOption("black background", true); -} - -function exit_show() { - setBatchMode("exit and display"); - exit(); -} - -function log_formatter(message) { - print('image "' + getTitle() + "'' [type=" + bitDepth() + "] [id=" + - getImageID() + "]: " + message); -} - -function logi(message) { - if (LOGLEVEL < 1) - return; - log_formatter(message); -} - -function logd(message) { - if (LOGLEVEL < 2) - return; - log_formatter(message); -} - - -//////////////// set the user environment ///////////////////////////////////////////////////// - -clear_workspace(); -reset_ij_options(); - -print("============================================"); -print("NoiSee is published in Ferrand, Schleicher & Biehlmaier et al. 2018"); -print("============================================"); -print("running on ImageJ version " + getVersion); - - -process_beads(); - - - - - - -//////////////// bead method ////////////////////////////////////////////////////////////////// -function process_beads() { - setBatchMode(true); - ////////// open the image stack and prepare processing ////////// ////////// ////////// - bfopts = " color_mode=Default"; - bfopts += " view=Hyperstack"; - bfopts += " stack_order=XYCZT"; - run("Bio-Formats Importer", "open=[" + beadsimage + "]" + bfopts); - imgs_orig = getImageID(); - fpath = File.getParent(beadsimage); // path only - fname = File.getName(beadsimage); // filename only - fname_nosuffix = stripOmeSuffix(File.nameWithoutExtension); // filename without extension - respath = fpath + "/" + fname_nosuffix + "_NoiSee-Bead-Analysis"; // path for additional results - File.makeDirectory(respath); - print("processing image: " + fname + " (location: [" + fpath + "])"); - - // check if image dimensions meet our expectations (z=1, t>1) - getDimensions(_, _, _, slices, frames); - if (slices > 1 || frames == 1) { - print("Input data needs to be a time-lapse with a single slice only!"); - print("Found " + slices + " slices, expected 1!"); - print("Found " + frames + " frames, expected > 1!"); - exit_show(); - } - - // neither floating point nor RGB images make sense for this, so check: - if (bitDepth() != 8 && bitDepth() != 16) { - print("Only images with a bit depth of 8 or 16 are supported!"); - exit_show(); - } - - if (bitDepth() == 16) { - print("16 bit image type detected, converting to 8 bit..."); - valueRange = detectValueRange(); - mapTo8bitPreservingSaturation(valueRange[0]); - } - - // remove the scaling so all units (measurements, coordinates, ...) are pixel-based: - run("Set Scale...", "pixel=1 unit=pixel"); - // make sure to reset any color LUT: - run("Grays"); - - getDimensions(width, height, channels, slices, frames); - area_full = width * height; - logi("dimensions: " + width + " x " + height + " (" + area_full + ")"); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// find beads and create a mask (binary image) ////////// ////////// ////////// - imgs_beadmask = duplicateImage(imgs_orig, "beadmask", false); - run("Convert to Mask", "method=IsoData background=Dark calculate black"); - roi_beads = binaryToSelection(imgs_beadmask, "beads", respath); - // now the background is 0 and beads are 255 - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// create kymographs for a number of beads ////////// ////////// ////////// - if (make_kymographs) { - rgb_kymo = makeKymographMontage(imgs_beadmask, imgs_orig, beads_diameter, respath); - addTextToImage(rgb_kymo, false, "Center", 22, - "Kymographs of selected beads. Each of them should be well-aligned from top to bottom."); - addTextToImage(rgb_kymo, false, "Center", 14, "Bleaching or variations in excitation " + - "power can be identified from changes in the intensities,"); - addTextToImage(rgb_kymo, false, "Center", 14, "drifts in X result in a // or \\\\ shape " + - "of the Kymograph, drifts in Y will show as /\\ or \\/ shape."); - } - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// identify background and create a background-subtracted image ////////// ////////// - imgs_bg = duplicateImage(imgs_beadmask, "background", false); - run("Invert", "stack"); // we want the background, not the beads... - erode(imgs_bg, 3); // erode the bead mask by 3 pixels to segment only real background - - /// transfer selection to original image, measure mean and standard-deviation: - roi_bg = binaryToSelection(imgs_bg, "background", respath); - selectImage(imgs_orig); - run("Restore Selection"); - getStatistics(area_bg, mean_bg, _, _, std_bg); - areapct_full_bg = area_bg / area_full * 100; - print("background stats: mean=" + mean_bg + " std=" + std_bg + " (" + - areapct_full_bg + "% of full area)"); - - /// create the background-subtracted image - imgs_bgsub = duplicateImage(imgs_orig, "background-subtracted", false); - run("Subtract...", "value=" + mean_bg); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// calculate StdDev, Mean and SNR images ////////// ////////// ////////// ////////// - selectImage(imgs_bgsub); - run("Z Project...", "projection=[Standard Deviation]"); - img_std = getImageID(); - rename("Standard Deviation (StdDev)"); - - selectImage(imgs_bgsub); - run("Z Project...", "projection=[Average Intensity]"); - img_avg = getImageID(); - rename("Average Intensity (Mean)"); - - // calculate the SNR as an image: - imageCalculator("Divide create 32-bit", img_avg, img_std); - img_snr = getImageID(); - rename("Mean divided by StdDev (SNR)"); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// find saturated beads and exclude them ////////// ////////// ////////// ////////// - /// TODO: discuss whether looking for saturation at this point makes sense, - /// or if this should be done on the original image (before the - /// background is subtracted and the time-series is averaged) - img_satmask = duplicateImage(img_avg, "saturation-mask", false); - - run("8-bit"); // make sure the approach also works if input is not 8-bit - setThreshold(0, 253); - run("Convert to Mask", "method=Default background=Default"); - // check if any close-to-saturation pixels exist at all: - getStatistics(_, _, min_satmask); - avg_has_saturated_pixels = true; - if (min_satmask == 255) { - print("No saturated pixels found in Average Intensity image, no need to exclude beads."); - avg_has_saturated_pixels = false; - } else { - print("Average Intensity image has saturated pixels, using a mask to exclude those beads!"); - // erode by half of the given bead diameter, adding 30% safety margin: - erode(img_satmask, round((beads_diameter/2)*1.3)); - - roi_satmask = binaryToSelection(img_satmask, "saturation_mask", respath); - run("Divide...", "value=255 stack"); // create a binary mask with 1 and 0 - setMinAndMax(0, 1); - // now apply the mask to the mean/average image: - imageCalculator("Multiply", img_avg, img_satmask); - } - closeImage(img_satmask); // the saturation mask is not required any more - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// find local maxima to determine locations for SNR evaluation ////////// ////////// - // prepare measurements: - run("Set Measurements...", "min redirect=None decimal=2"); - - /// Find Maxima - noise parameter: Maxima are ignored if they do not stand out from the - /// surroundings by more than this value (calibrated units for calibrated images). In other - /// words, a threshold is set at the maximum value minus noise tolerance and the contiguous - /// area around the maximum above the threshold is analyzed. For accepting a maximum, this - /// area must not contain any point with a value higher at than the maximum. Only one maximum - /// within this area is accepted. - selectImage(img_avg); - run("Find Maxima...", "noise=" + beads_noisetolerance + " output=[Point Selection]"); - roi_peaks = selectionToROI("peaks_from_mean", respath); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// measure (evaulate) using the above determined points ////////// ////////// ////////// - // DISCUSS: should this be normalized (bit depth) to allow comparing different sensors? - measureSelection(img_std, "StdDev", respath, false, save_measurements); - measureSelection(img_snr, "SNR", respath, false, save_measurements); - getStatistics(_, mean_snr, _, _, std_snr); // mean and std from the SNR - print("SNR stats: mean=" + mean_snr + " std=" + std_snr); - - measureSelection(img_avg, "mean", respath, false, save_measurements); - getStatistics(_, mean_sig, _, _, std_sig); // mean and std from the signal - print("signal stats: mean=" + mean_sig + " std=" + std_sig); - - mean_sbr = mean_sig / mean_bg; // calculate signal-to-background (SBR) - print("SBR (signal-to-background): " + mean_sbr); - - // NOTE about error-propagation: all components contributing to the SBR contain an error and - // thus the SBR standard-deviation should be taken into consideration when further analyzing / - // using the mean SBR value! - - nBeads = nResults; - print("number of beads detected / measured: " + nBeads); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// measure bleaching ////////// ////////// ////////// ////////// ////////// ////////// - // prepare measurements: area/mean/stddev to allow calculating the SEM = StdDev / sqrt(npixels) - run("Set Measurements...", "area mean standard redirect=None decimal=2"); - - imgs_bleach = duplicateImage(imgs_beadmask, "bleaching", false); - - // erode the mask a bit to compensate for a possible xy-drift: - erode(imgs_bleach, 3); - - // NOTE: if a scatter plot of stddev vs. time reveals an increasing trend this might indicate - // an xy-drift. try dilating the bead mask more in that case, if that does not help the - // time-series should be stabilized (e.g. using stackreg) before analyzing (make sure NOT to - // have sub-pixel correction as this will change the SNR!!) - - roi_bleach = binaryToSelection(imgs_bleach, "bead_intensity_vs_time", respath); - closeImage(imgs_bleach); // we have the regions in the ROI manager, no need to keep the image - - measureSelection(imgs_orig, "bead_intensity_vs_time", respath, true, save_measurements); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - /////////// create images showing detected ROIs //////////// ////////// ////////// ////////// - rgb_roi_bg = makeFilledROIImage(imgs_orig, "background", false); - addTextToImage(rgb_roi_bg, false, "Center", 22, - "Red is representing the area detected as background."); - if (avg_has_saturated_pixels) { - rgb_roi_satmask = makeFilledROIImage(imgs_orig, "saturation_mask", true); - addTextToImage(rgb_roi_satmask, false, "Center", 22, - "Red areas contain saturation and are being ignored."); - } else { - rgb_roi_satmask = duplicateImage(imgs_orig, "ROI - saturation_mask", true); - addTextToImage(rgb_roi_satmask, false, "Center", 22, - "No saturated pixels found, no need to exclude any beads from analysis."); - run("RGB Color"); - } - rgb_roi_intensity = makeFilledROIImage(imgs_orig, "bead_intensity_vs_time", false); - addTextToImage(rgb_roi_intensity, false, "Center", 22, - "Red pixels used for Mean / StdDev plots over time."); - - if (make_kymographs) { - rgb_kymo_lines = drawROIsOnImage(imgs_orig, "bead-", "green"); - addTextToImage(rgb_kymo_lines, false, "Center", 22, - "Green lines showing selections used to create Kymographs."); - } - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - /////////// decorate images, add calibration bars //////////// ////////// ////////// ////////// - rgb_t0 = dressImage(imgs_orig, "Glow", -1, "First timepoint (T0) of the original data."); - rgb_avg = dressImage(img_avg, "Fire", -1, "Average projection of the background-subtracted data."); - rgb_std = dressImage(img_std, "Fire", -1, "StdDev projection of the background-subtracted data."); - rgb_snr = dressImage(img_snr, "physics", 0.35, "Signal-To-Noise ratio for all pixels."); - - // adjust name for original image (now showing the first time point): - addTextToImage(rgb_t0, false, "Center", 12, "Original file name:"); - addTextToImage(rgb_t0, false, "Center", 12, fname); - rename("Original Data"); - - // clean up image windows: - closeImage(imgs_bgsub); - closeImage(imgs_bg); - closeImage(imgs_beadmask); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// create plots from bleaching measurements ////////// ////////// ////////// ////////// - // linear decay = z-drift, exponential decay = bleaching - col_mean = "Mean(bead_intensity_vs_time)"; - plot_mean = plotResult("Mean bead intensity over time", "Frame", "bead intensity mean", col_mean); - addTextToImage(plot_mean, false, "Center", 22, - "A linear decay is an indicator for a z-drift, an exponential decay for bleaching."); - addTextToImage(plot_mean, false, "Center", 96, ""); - - // linear increase = xy-drift - // this reveals only drift that is relvant for the regions that measure bleaching or z-drift. - // could in general be used to reveal xy-drift as it also interferes with SNR - col_std = "StdDev(bead_intensity_vs_time)"; - plot_std = plotResult("Standard deviation of bead intensity over time", "Frame", - "bead intensity StdDev", col_std); - addTextToImage(plot_std, false, "Center", 22, - "Evolution of noise over time."); - - run("Combine...", "stack1=[plot of " + col_mean + "] stack2=[plot of " + col_std + "] combine"); - rename("Plots of intensity vs. time"); - rgb_plots = getImageID(); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - /////////// write summary of results and settings into a new table //////////// ////////// - title = "NoiSee Results Summary"; - - pairs = newArray(); - pairs = Array.concat(pairs, "SNR mean", mean_snr); - pairs = Array.concat(pairs, "SNR StdDev", std_snr); - pairs = Array.concat(pairs, "SBR mean", mean_sbr); - pairs = Array.concat(pairs, "Signal mean", mean_sig); - pairs = Array.concat(pairs, "Signal StdDev (noise)", std_sig); - pairs = Array.concat(pairs, "Background mean", mean_bg); - pairs = Array.concat(pairs, "Background StdDev", std_bg); - pairs = Array.concat(pairs, "Background area", area_bg); - pairs = Array.concat(pairs, "Background area %", areapct_full_bg); - pairs = Array.concat(pairs, "Number of beads", nBeads); - pairs = Array.concat(pairs, "", ""); - pairs = Array.concat(pairs, "-- Settings --", ""); - pairs = Array.concat(pairs, "Beads diameter", beads_diameter); - pairs = Array.concat(pairs, "Noise tolerance", beads_noisetolerance); - pairs = Array.concat(pairs, "", ""); - pairs = Array.concat(pairs, "-- Input --", ""); - pairs = Array.concat(pairs, "Filename", fname); - - createTable(title, pairs, respath + "/" + "NoiSee-summary.txt"); - img_summary = createTableImage(title, pairs); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// adjust order of the images and create a PDF ////////// ////////// ////////// - - // the PDF exporter of ImageJ simply concatenates all open images in ascending order, - // starting with the oldest one first, therefore we need to duplicate all images into - // new ones (closing the old ones) in the order which is desired in the resulting PDF: - rgb_t0 = duplicateAndClose(rgb_t0); - rgb_avg = duplicateAndClose(rgb_avg); - rgb_std = duplicateAndClose(rgb_std); - rgb_snr = duplicateAndClose(rgb_snr); - if (make_kymographs){ - rgb_kymo = duplicateAndClose(rgb_kymo); - rgb_kymo_lines = duplicateAndClose(rgb_kymo_lines); - } - rgb_roi_bg = duplicateAndClose(rgb_roi_bg); - rgb_roi_satmask = duplicateAndClose(rgb_roi_satmask); - rgb_roi_intensity = duplicateAndClose(rgb_roi_intensity); - - rgb_plots = duplicateAndClose(rgb_plots); - img_summary = duplicateAndClose(img_summary); - - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// - - - - ////////// save the log, create PDF and arrange the windows on the screen ////////// ////////// - if (save_log) { - selectWindow("Log"); - saveAs("Text", respath + "/Log.txt"); - } - - setBatchMode("exit and display"); // exit batch mode and show images - // wait(100); // give the OS some time to display all image windows - - if (save_pdf) { - fname_pdf = respath + "/" + "NoiSee-report.pdf"; - print("Creating PDF from images: " + fname_pdf); - run("PDF ... ", "show show scale save one save=[" + fname_pdf + "]"); - } - closeImage(img_summary); - - row_upper = newArray(rgb_t0, rgb_avg, rgb_plots); - row_lower = newArray(rgb_std, rgb_snr); - - if (make_kymographs) { - row_lower = Array.concat(row_lower, rgb_kymo); - } - - if (keep_roiimages) { - row_upper = Array.concat(row_upper, rgb_roi_intensity, rgb_roi_bg); - if (make_kymographs) { - row_lower = Array.concat(row_lower, rgb_kymo_lines); - } - row_lower = Array.concat(row_lower, rgb_roi_satmask); - } else { - closeImage(rgb_kymo_lines); - closeImage(rgb_roi_bg); - closeImage(rgb_roi_satmask); - closeImage(rgb_roi_intensity); - } - - arrangeImages(row_upper, screenHeight * 0.1); - arrangeImages(row_lower, screenHeight * 0.5); - ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// -} +//////////// NoiSee Beads Analysis /////////////////////////////////////////////////////// +// Signal-to-Noise-Ratio evaluation suite +// written by Kai Schleicher, Niko Ehrenfeuchter, IMCF Basel +// licence: GPLv3 + + +/// TODOs: +// - consider using lacan's LibInstaller: https://github.com/lacan/LibInstaller + +/// Naming conventions +// - "imgs_" - image IDs of stacks +// - "img_" - image IDs of 2D images +// - "rgb_" - image IDs of 2D RGB images +// - "roi_" - index number of a ROI manager entry + + +//////// Script Parameters, see https://imagej.net/Script_Parameters for details +#@ String (label="<html><img src=\"https://drive.switch.ch/index.php/s/zyKSwuCMKMs6ZQ1/download\"></html>",value="<html><img src=\"https://drive.switch.ch/index.php/s/54H9s84wYou3uBd/download\"></html>",visibility=MESSAGE,persist=false) msg_logo +#@ String(visibility=MESSAGE, label="NoiSee -",value="Beads SNR analysis",persist=false) msg_title +#@ File(label="Beads time-series image",description="2D time-lapse acquisition of fluorescent beads") beadsimage +#@ Integer(label="Beads diameter (in pixels)",description="approximate bead diameter (in pixels)",value=15) beads_diameter +#@ Integer(label="Find Maxima noise tolerance",description="typical values: [PMT=50] [HyD (photon counting)=10] [Camera=500]",value=50) beads_noisetolerance +#@ Boolean(label="Create Kymographs",description="visual indicator for drift and bleaching",value="true") make_kymographs +#@ Boolean(label="Save additional measurements",description="store 'StdDev', 'SNR', 'Mean' and 'bleaching' measurements",value="false") save_measurements +#@ Boolean(label="Save results as PDF",description="generate a PDF with images and plots",value="true") save_pdf +#@ Boolean(label="Keep ROI images open",description="if disabled ROI visualizations will only be added to PDF",value="false") keep_roiimages +#@ String(visibility=MESSAGE,label="Note:",value="all currently open images will be closed",persist=false) msg_note_close + + +// valid log levels: 0 (quiet), 1 (info messages), 2 (debug messages) +LOGLEVEL=2; + +// save contents of the 'Log' window in a text file: +save_log=false; + + +////////////////// function definitions /////////////////////////////// + +function duplicateImage(id_orig, new_name, single){ + // duplicate an image (given by ID), give it a new name and return the new ID + selectImage(id_orig); + run("Select None"); // clear selection to avoid messing up the duplication + if (single == true) { + run("Duplicate...", "use"); // duplicate single slice only + } else { + run("Duplicate...", "duplicate"); // duplicate the entire stack + } + rename(new_name); + return getImageID(); +} + +function duplicateAndClose(id_orig) { + // helper function for re-ordering images when creating a PDF + // the PDF exporter places images one per page in the order when they were created, so the only + // way to change that order is to duplicate an existing image into a new one, closing the old... + selectImage(id_orig); + new_id = duplicateImage(id_orig, getTitle(), false); + closeImage(id_orig); + return new_id; +} + +function plotResult(Title, XLabel, YLabel, col) { + // create a high-res plot of the values in column "col" of the results table + // using the given title and labels + // returns the ID of the newly created plot image + if (nResults == 0) { + print("Unable to create plot [" + Title + "], results table empty!"); + return; + } + print("Creating plot [" + Title + "] for " + nResults + " results."); + Yvalues=newArray(nResults); + Ymin = getResult(col, 0); + Ymax = Ymin; + + // store Y-values in an array, get min and max: + for (i=0; i < nResults; i++) { + Yvalues[i] = getResult(col, i); + Ymax = maxOf(Ymax, Yvalues[i]); + Ymin = minOf(Ymin, Yvalues[i]); + } + // make sure not to over-emphasize data that has only little variance by + // setting the plot limits to min/max +/- 10: + Ymin = round(Ymin - 10.5); + Ymax = round(Ymax + 10.5); + print("Plot Y-axis range: " + Ymin + " - " + Ymax); + + + temp_plot_name = "NoiSee temporary plot window"; + Plot.create(temp_plot_name, XLabel, YLabel); + Plot.setLimits(0, nResults, Ymin, Ymax); + Plot.setLineWidth(2); + Plot.setColor("#6688dd"); + Plot.add("line",Yvalues); + Plot.setColor("red"); + Plot.add("circles", Yvalues); + Plot.show(); + Plot.makeHighResolution("plot of " + col, 2); + closeImage(temp_plot_name); + selectWindow("plot of " + col); + plot_id = getImageID(); + addTextToImage(plot_id, true, "Center", 48, Title); + addTextToImage(plot_id, true, "Center", 24, ""); + return plot_id; +} + +function erode(image_id, num_iterations) { + // run the "erode" operation multiple times on an image / stack + selectImage(image_id); + logd("Eroding current image by " + num_iterations + " iterations..."); + for (i=0; i < num_iterations; i++) { + run("Erode", "stack"); + } +} + +function binaryToSelection(image_id, roi_name, savepath) { + // create a selection from a binary image, add it to the ROI Manager with the given name and + // return the ID of the newly created ROI + selectImage(image_id); + if (is("binary") == false) + exit("Image " + getTitle() + " is not binary, not creating a selection!"); + // "Create Selection" requires a threshold to be set, see the source code in + // https://imagej.nih.gov/ij/developer/source/ij/plugin/filter/ThresholdToSelection.java.html + // particularly for method selected(x, y) as the documentation is not very clear about this! + setThreshold(255, 255); + run("Create Selection"); + return selectionToROI(roi_name, savepath); +} + +function selectionToROI(roi_name, savepath) { + // add an existing selection as a new ROI with the given name and return the ID + roiManager("Add"); + roi_id = roiManager("count") - 1; // ROIs are appended, hence "count" == newest ID + 1 + roiManager("Select", roi_id); + roiManager("Rename", roi_name); + roiManager("Show None"); // clean up display behavior of the ROI manager + if (savepath != "") + roiManager("save selected", savepath + "/roi-" + roi_name + ".zip"); + return roi_id; +} + +function pickResults(count) { + // generate a list of index numbers creating a (more or less) equally spaced subset of the + // current results + if (nResults < count) { + setBatchMode("exit and display"); // exit batch mode and show images + exit("less than " + count + " results found, check your image!"); + } + indexes = newArray(count); + halfstep = nResults / (count * 2); + for (i=0; i<count; i++) { + row = round((i*2+1) * halfstep); + indexes[i] = row; + } + return indexes; +} + +function createCenterLines(image_id, res, len, namepfx) { + // create line selections of length "2*len" for the center of mass of a given subset of the + // current results ("res" is an array with the index numbers of results to be used) + roiManager("Set Color", "magenta"); + run("Select None"); + roiManager("reset"); // make sure the ROI Manager only contains our newly created line ROIs + selectImage(image_id); + for (i=0; i<res.length; i++) { + index = res[i]; + cx = getResult("XM", index - 1); + cy = getResult("YM", index - 1); + logd("draw line for result " + index + " (c-o-m: " + cx + " / " + cy + ")"); + makeLine(cx - len, cy, cx + len, cy); + roiManager("add"); + roiManager("select", roiManager("count") - 1); + roiManager("rename", namepfx + index); + } + roiManager("Show all without labels"); + roiManager("Set Line Width", 3); +} + +function lineKymograph(image_id) { + // expects a number of "line" ROIs in the ROI Manager, creates a Kymograph (stack or orthogonal + // profile) for each of them, returning the newly created image IDs as an array + num_rois = roiManager("count"); + ortho_imgs = newArray(num_rois); + for (i=0; i<num_rois; i++) { + selectImage(image_id); + roiManager("select", i); + roi_name = call("ij.plugin.frame.RoiManager.getName", i); + run("Reslice [/]...", "output=1.000 slice_count=1"); + ortho_imgs[i] = getImageID(); + rename("kymograph-" + roi_name); + } + return ortho_imgs; +} + +function makeKymographMontage(imgs_beadmask, imgs_beads, diameter, basepath) { + // calculate the expected bead area to set the size parameter for "Analyze Particles" below: + beads_area = pow(diameter / 2, 2) * 3.14; + lo = beads_area * 0.6; // lower limit + hi = beads_area * 1.4; // upper limit + logi("range of expected beads areas: " + lo + " - " + hi); + + img_tmp = duplicateImage(imgs_beadmask, "beadmask-T0", true); + run("Set Measurements...", "area center"); // center-of-mass gives us the bead location + run("Analyze Particles...", "size=" + lo + "-" + hi+ " display clear add"); + resetThreshold(); + closeImage(img_tmp); + saveAs("Results", basepath + "/individual-beads.txt"); + logi("found " + nResults + " individual beads (excluding clustered / clumped ones)"); + + indexes = pickResults(4); // select a subset of four beads from the results + imgs_tmp = duplicateImage(imgs_beads, "tmp-stack", false); + createCenterLines(imgs_tmp, indexes, diameter, "bead-"); + lineKymograph(imgs_tmp); + closeImage(imgs_tmp); + run("Images to Stack", "name=[kymograph-stack] title=[kymograph-] use"); + imgs_tmp = getImageID(); + run("Scale...", "x=16 y=16 z=1.0 depth=4 interpolation=None process create"); + imgs_tmp2 = getImageID(); + closeImage(imgs_tmp); + run("Make Montage...", "columns=2 rows=2 scale=1 border=40 font=16 label use"); + closeImage(imgs_tmp2); + rename("Kymographs Montage"); + run("cool"); + return getImageID(); +} + +function findROI(roi_name) { + // scan for an ROI with the given name and return its ID + // WARNING: will terminate the macro if no ROI with the name exists! + for (roi_id=0; roi_id<roiManager("count"); roi_id++) { + cur_name = call("ij.plugin.frame.RoiManager.getName", roi_id); + if (cur_name == roi_name) + return roi_id; + } + exit("ERROR: ROI with name '" + roi_name + "' doesn't exist!"); +} + +function makeFilledROIImage(image_id, roi_name, inverse) { + roi_id = findROI(roi_name); + new_id = duplicateImage(image_id, "ROI - " + roi_name, true); + logd("creating filled ROI image for [" + getTitle() + "] and ROI [" + roi_name + "]"); + run("RGB Color"); + setForegroundColor(255, 57, 0); + roiManager("Select", roi_id); + if (inverse == true) { + run("Make Inverse"); + } + // roiManager("Fill"); // WARNING: this command doesn't respect the "make inverse" from above! + run("Fill", "slice"); + return new_id; +} + +function drawROIsOnImage(image_id, namepfx, color) { + // use a given image (or stack) to create an RGB image where all ROIs starting with the given + // prefix are rendered in the specified color + // returns the ID of the new RGB image + tmp_id = duplicateImage(image_id, "tmp_image", true); + run("RGB Color"); + // scan the ROI manager for the given prefix and assemble an array with corresponding IDs: + num_rois = roiManager("count"); + roi_ids = newArray(); + for (i=0; i<num_rois; i++) { + roiManager("select", i); + roi_name = call("ij.plugin.frame.RoiManager.getName", i); + if (startsWith(roi_name, namepfx)) { + roi_ids = Array.concat(roi_ids, i); + } + } + // Array.print(roi_ids); + + // now select the IDs, combine them and flatten them into the new image: + roiManager("Select", roi_ids); + roiManager("Combine"); + roiManager("Add", "00ff00", 0); + // the new ROI-ID equals "num_rois": + roiManager("Select", num_rois); + roiManager("Set Fill Color", color); + run("Flatten"); + new_id = getImageID(); + rename("Kymographs Selections"); + roiManager("Delete"); + closeImage(tmp_id); + return new_id; +} + +function measureSelection(image_id, title, basepath, multi, savetxt) { + // measure (using the existing ROIs) on a given image and save the results + // in a text file at the given path if requested + + selectImage(image_id); + run("Restore Selection"); + run("Clear Results"); + if (multi) { + roiManager("Multi Measure"); // measure on all slices of a stack + } else { + // run("Measure"); // is there any difference to roiManager("Measure") ?? + roiManager("Measure"); + } + + if (savetxt == false) + return; + + File.makeDirectory(basepath); + if (File.isDirectory(basepath) == false) + exit("ERROR creating directory [" + basepath + "], stopping."); + + saveAs("Results", basepath + "/" + title + ".txt"); +} + +function flattenOverlay(image_id) { + selectImage(image_id); + title = getTitle(); + run("Flatten"); + new_id = getImageID(); + closeImage(image_id); + selectImage(new_id); + rename(title); + return new_id; +} + +function closeImage(image_id) { + if (isOpen(image_id) == false) + return; + selectImage(image_id); + close(); +} + +function dressImage(image_id, lut_name, enhance, desc) { + // applies a given LUT to an image, applies the "Enhance contrast" command to it if the + // "enhance" parameter is non-negative, adds a calibration bar (non-overlay) and optionally + // adds a text description below the image + // returns the ID of the new image, closing the old one + selectImage(image_id); + logi("dressImage(" + getTitle() + ", " + lut_name + ", " + enhance + ")"); + run("Select None"); + logd("applying LUT " + lut_name); + run(lut_name); // WARNING: applying a LUT changes the selected image!! + selectImage(image_id); + if (enhance >= 0) { + logd("enhancing contrast (saturated=" + enhance + ")"); + run("Enhance Contrast", "saturated=" + enhance); + } + // sometimes calibration bar doesn't work depending on when / where the image window appears on + // the screen, so make sure the right window is selected / active: + selectImage(image_id); + logd("adding calibration bar"); + title = getTitle(); + run("Calibration Bar...","location=[Upper Right] fill=None " + + "label=White number=5 decimal=0 font=12 zoom=3.3 bold"); + new_id = getImageID(); + rename(title); + if (desc != "") + addTextToImage(new_id, false, "Center", 22, desc); + closeImage(image_id); + return new_id; +} + +function createTable(title, pairs, fname) { + // create a new 2-column table with the given title, using the key-value tuples from the array + // given in "pairs" to fill the cells, saving it as to the text file "fname" + titleb = "[" + title + "]"; + if (isOpen(title)) + print(titleb, "\\Clear"); + else + run("Table...", "name=" + titleb + " width=320 height=340"); + + print(titleb, "\\Headings:Key\tValue"); + for (i = 0; i < pairs.length; i = i + 2) { + print(titleb, pairs[i] + "\t" + pairs[i+1]); + } + selectWindow(title); + if (fname != "") + saveAs("Results", fname); +} + +function createTableImage(title, pairs) { + // create an image with a 2-column table from the given array of pairs + // returns the ID of the new image + newImage("tmp_table_col1", "RGB white", 1, 1, 1); + img_col1 = getImageID(); + newImage("tmp_table_col2", "RGB white", 1, 1, 1); + img_col2 = getImageID(); + + for (i = 0; i < pairs.length; i = i + 2) { + addTextToImage(img_col1, false, "Left", 28, pairs[i]); + addTextToImage(img_col2, false, "Left", 28, pairs[i+1]); + } + selectImage(img_col2); + col2h = getHeight(); + col2w = getWidth() + 28; + run("Canvas Size...", "width=" + col2w + " height=" + col2h + " position=Top-Right"); + run("Combine...", "stack1=tmp_table_col1 stack2=tmp_table_col2"); + img_table = getImageID(); + rename(title); + + addTextToImage(img_table, true, "Center", 18, ""); + addTextToImage(img_table, true, "Center", 36, title); + + return img_table; +} + +function addTextToImage(image_id, above, justify, font_size, text) { + selectImage(image_id); + logd("addTextToImage(" + getTitle() + ", ''" + text + "'')"); + run("Select None"); + + curw = getWidth(); + neww = curw; + curh = getHeight(); + newh = curh + font_size + 2; + setFont("SansSerif", font_size); + textw = getStringWidth(text); + if (textw > curw) + neww = textw; + + if (above == true) { + canvpos = "Bottom-" + justify; + textpos = font_size + 2; + } else { + canvpos = "Top-" + justify; + textpos = newh; + } + + setForegroundColor(30, 30, 30); + setBackgroundColor(255, 255, 255); + run("Canvas Size...", "width=" + neww + " height=" + newh + " position=" + canvpos); + drawString(text, 0, textpos); +} + +function arrangeImages(ids, y_coord) { + // try to place the image windows given in the "ids" array horizontally on the screen, + // arranging them in equal distance and adjusting the zoom level if necessary + selectImage(ids[0]); + getLocationAndSize(_, _, window_width, window_height); + px_width = getWidth(); + px_height = getHeight(); + border_x = window_width - px_width; + border_y = window_height - px_height; + // try varius zoom levels until the windows fit on the screen and calculate the spacing: + for (s=0; s<4; s++) { + zoom = 1 - 0.25 * s; + width = px_width * zoom + border_x; + height = px_height * zoom + border_y; + total_width = ids.length * width; + logd("width " + width + " (zoom " + zoom + ", total width " + total_width + ")"); + remaining = screenWidth - total_width; + if (remaining > 0) { + spacing = remaining / (ids.length + 1); + logd("zoom / width / spacing: " + zoom + " / " + width + " / " + spacing); + break; + } else { + continue; + } + // if we reach this point we can't seem to calculate a zoom factor, so we give up without + // placing / relocating the image windows (doesn't make sense): + return; + } + + // now place the images: + for (i=0; i<ids.length; i++) { + selectImage(ids[i]); + x_coord = width * i + spacing * (i+1); + logd("placing image at " + x_coord + " / " + y_coord); + setLocation(x_coord, y_coord, width, height); + getLocationAndSize(_, _, window_width, window_height); + if (window_width != width) { + delta = (width - window_width) / 2; + setLocation(x_coord + delta, y_coord, width, height); + } + } +} + +function stripOmeSuffix(orig) { + if (endsWith(orig, ".ome")) { + index = lastIndexOf(orig, ".ome"); + orig = substring(orig, 0, index); + } + return orig; +} + +function detectValueRange() { + // detect the *actual* value range bit depth of a 16-bit image, i.e. if the + // image contains pixels in a range from 5 to 3978 the value range depth is + // 12 bits (possible values from 0 to 4095) + + // define the value range bit depths to test for (8 to 16): + possible_depths = Array.getSequence(17); + possible_depths = Array.slice(possible_depths, 8, 17); + + Stack.getStatistics(_, _, _, max, _, _); + logi("maximum pixel value in stack: " + max); + + saturated = false; + actual_depth = 16; + // traverse the possible depths array backwards: + for (i = lengthOf(possible_depths) - 1; i > 0; i--) { + sat = pow(2, possible_depths[i]) - 1; + logd("checking " + possible_depths[i] + " bit range (0-" + sat + ")"); + + if (max == sat) { + saturated = true; + actual_depth = possible_depths[i]; + logd("saturated " + actual_depth + "-bit image detected!"); + } else { + next_lower = pow(2, possible_depths[i-1]); + if (max < sat && max >= next_lower) { + actual_depth = possible_depths[i]; + logd("non-saturated " + actual_depth + "-bit image detected!"); + } + } + } + + msg_sat = "(not saturated)"; + if (saturated) + msg_sat = "(SATURATED!)"; + logd("\ndetected value range: " + actual_depth + " bit " + msg_sat); + + res = newArray(actual_depth, saturated); + return res; +} + +function mapTo8bitPreservingSaturation(effectiveBits) { + // convert an image with 12 or 16 bit effective value range depth to 8 bit + // preserving saturation in the sense that only saturated pixels (65535 for + // 16 bits and 4095 for 12 bits) are mapped to the 8 bit saturation value + // (255), avoiding the usual binning that would happen if simply the display + // range was adjusted to the min/max values of the image (resulting in + // over-saturation of wrongly mapped pixels) + if (effectiveBits == 16) { + divisor = 257.50; + } else if (effectiveBits == 12) { + divisor = 16.09; + } else if (effectiveBits == 11) { + divisor = 8.03; + } else { + print("Unsupported VALUE range detected: " + effectiveBits + " bits"); + print("Input image needs to have a value range of 8, 12 or 16 bits!"); + exit_show(); + } + msg = "mapping " + effectiveBits + " bit value range to 8 bit"; + msg = msg + " (dividing pixel values by " + divisor + ")"; + logd(msg); + run("Divide...", "value=" + divisor + " stack"); + setMinAndMax(0, 255); + run("8-bit"); + + return divisor; +} + +function clear_workspace() { + /* + * Ensure a clean workspace, i.e. + * - no open image windows + * - an empty Log window + * - no ROIs + * - no results + */ + + // clear the Log window + print("\\Clear"); + + // close all open images + if (nImages > 0) { + run("Close All"); + } + + // make sure the ROI manager is empty + roiManager("reset"); + + // empty the results table + run("Clear Results"); +} + +function reset_ij_options() { + /* + * Make sure to set all relevant ImageJ options to a useful state for being + * able to provide consistent results independent of what has been + * configured or done by the user before. + * + * IMPORTANT: the order of the commands is highly relevant, on changes + * careful tests need to be done to ensure correct behavior. + */ + + // disable inverting LUT + run("Appearance...", " menu=0 16-bit=Automatic"); + + // set foreground color to be white, background black + run("Colors...", "foreground=white background=black selection=red"); + + // pad edges when eroding a binary image + run("Options...", "pad"); + + // set saving format to .txt files + run("Input/Output...", "file=.txt"); + + + // ============= WARNING WARNING WARNING =============// + // the commands below this marker *MUST NOT* be moved + // upwards as they seem to be overridden by some of the + // "run(...)" calls otherwise, turning them useless! + + // set "Black Background" in "Binary Options" + setOption("black background", true); +} + +function exit_show() { + setBatchMode("exit and display"); + exit(); +} + +function log_formatter(message) { + print('image "' + getTitle() + "'' [type=" + bitDepth() + "] [id=" + + getImageID() + "]: " + message); +} + +function logi(message) { + if (LOGLEVEL < 1) + return; + log_formatter(message); +} + +function logd(message) { + if (LOGLEVEL < 2) + return; + log_formatter(message); +} + + +//////////////// set the user environment ///////////////////////////////////////////////////// + +clear_workspace(); +reset_ij_options(); + +print("============================================"); +print("NoiSee is published in Ferrand, Schleicher & Biehlmaier et al. 2018"); +print("============================================"); +print("running on ImageJ version " + getVersion); + + +process_beads(); + + + + + + +//////////////// bead method ////////////////////////////////////////////////////////////////// +function process_beads() { + setBatchMode(true); + ////////// open the image stack and prepare processing ////////// ////////// ////////// + bfopts = " color_mode=Default"; + bfopts += " view=Hyperstack"; + bfopts += " stack_order=XYCZT"; + run("Bio-Formats Importer", "open=[" + beadsimage + "]" + bfopts); + imgs_orig = getImageID(); + fpath = File.getParent(beadsimage); // path only + fname = File.getName(beadsimage); // filename only + fname_nosuffix = stripOmeSuffix(File.nameWithoutExtension); // filename without extension + respath = fpath + "/" + fname_nosuffix + "_NoiSee-Bead-Analysis"; // path for additional results + File.makeDirectory(respath); + print("processing image: " + fname + " (location: [" + fpath + "])"); + + // check if image dimensions meet our expectations (z=1, t>1) + getDimensions(_, _, _, slices, frames); + if (slices > 1 || frames == 1) { + print("Input data needs to be a time-lapse with a single slice only!"); + print("Found " + slices + " slices, expected 1!"); + print("Found " + frames + " frames, expected > 1!"); + exit_show(); + } + + // neither floating point nor RGB images make sense for this, so check: + if (bitDepth() != 8 && bitDepth() != 16) { + print("Only images with a bit depth of 8, 11 or 16 are supported!"); + exit_show(); + } + + if (bitDepth() == 16) { + print("16 bit image type detected, converting to 8 bit..."); + valueRange = detectValueRange(); + divisor = mapTo8bitPreservingSaturation(valueRange[0]); + } + + // remove the scaling so all units (measurements, coordinates, ...) are pixel-based: + run("Set Scale...", "pixel=1 unit=pixel"); + // make sure to reset any color LUT: + run("Grays"); + + getDimensions(width, height, channels, slices, frames); + area_full = width * height; + logi("dimensions: " + width + " x " + height + " (" + area_full + ")"); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// find beads and create a mask (binary image) ////////// ////////// ////////// + imgs_beadmask = duplicateImage(imgs_orig, "beadmask", false); + // run("Convert to Mask", "method=IsoData background=Dark calculate black"); // does not work anymore somehow + run("Auto Threshold", "method=MaxEntropy white stack use_stack_histogram"); + roi_beads = binaryToSelection(imgs_beadmask, "beads", respath); + // now the background is 0 and beads are 255 + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// create kymographs for a number of beads ////////// ////////// ////////// + if (make_kymographs) { + rgb_kymo = makeKymographMontage(imgs_beadmask, imgs_orig, beads_diameter, respath); + addTextToImage(rgb_kymo, false, "Center", 22, + "Kymographs of selected beads. Each of them should be well-aligned from top to bottom."); + addTextToImage(rgb_kymo, false, "Center", 14, "Bleaching or variations in excitation " + + "power can be identified from changes in the intensities,"); + addTextToImage(rgb_kymo, false, "Center", 14, "drifts in X result in a // or \\\\ shape " + + "of the Kymograph, drifts in Y will show as /\\ or \\/ shape."); + } + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// identify background and create a background-subtracted image ////////// ////////// + imgs_bg = duplicateImage(imgs_beadmask, "background", false); + run("Invert", "stack"); // we want the background, not the beads... + erode(imgs_bg, 3); // erode the bead mask by 3 pixels to segment only real background + + /// transfer selection to original image, measure mean and standard-deviation: + roi_bg = binaryToSelection(imgs_bg, "background", respath); + selectImage(imgs_orig); + run("Restore Selection"); + getStatistics(area_bg, mean_bg, _, _, std_bg); + areapct_full_bg = area_bg / area_full * 100; + print("background stats: mean=" + mean_bg + " std=" + std_bg + " (" + + areapct_full_bg + "% of full area)"); + + /// create the background-subtracted image + imgs_bgsub = duplicateImage(imgs_orig, "background-subtracted", false); + run("Subtract...", "value=" + mean_bg); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// calculate StdDev, Mean and SNR images ////////// ////////// ////////// ////////// + selectImage(imgs_bgsub); + run("Z Project...", "projection=[Standard Deviation]"); + img_std = getImageID(); + rename("Standard Deviation (StdDev)"); + + selectImage(imgs_bgsub); + run("Z Project...", "projection=[Average Intensity]"); + img_avg = getImageID(); + rename("Average Intensity (Mean)"); + + // calculate the SNR as an image: + imageCalculator("Divide create 32-bit", img_avg, img_std); + img_snr = getImageID(); + rename("Mean divided by StdDev (SNR)"); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// find saturated beads and exclude them ////////// ////////// ////////// ////////// + /// TODO: discuss whether looking for saturation at this point makes sense, + /// or if this should be done on the original image (before the + /// background is subtracted and the time-series is averaged) + img_satmask = duplicateImage(img_avg, "saturation-mask", false); + + run("8-bit"); // make sure the approach also works if input is not 8-bit + setThreshold(0, 253); + run("Convert to Mask", "method=Default background=Default"); + // check if any close-to-saturation pixels exist at all: + getStatistics(_, _, min_satmask); + avg_has_saturated_pixels = true; + if (min_satmask == 255) { + print("No saturated pixels found in Average Intensity image, no need to exclude beads."); + avg_has_saturated_pixels = false; + } else { + print("Average Intensity image has saturated pixels, using a mask to exclude those beads!"); + // erode by half of the given bead diameter, adding 30% safety margin: + erode(img_satmask, round((beads_diameter/2)*1.3)); + + roi_satmask = binaryToSelection(img_satmask, "saturation_mask", respath); + run("Divide...", "value=255 stack"); // create a binary mask with 1 and 0 + setMinAndMax(0, 1); + // now apply the mask to the mean/average image: + imageCalculator("Multiply", img_avg, img_satmask); + } + closeImage(img_satmask); // the saturation mask is not required any more + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// find local maxima to determine locations for SNR evaluation ////////// ////////// + // prepare measurements: + run("Set Measurements...", "min redirect=None decimal=2"); + + /// Find Maxima - noise parameter: Maxima are ignored if they do not stand out from the + /// surroundings by more than this value (calibrated units for calibrated images). In other + /// words, a threshold is set at the maximum value minus noise tolerance and the contiguous + /// area around the maximum above the threshold is analyzed. For accepting a maximum, this + /// area must not contain any point with a value higher at than the maximum. Only one maximum + /// within this area is accepted. + selectImage(img_avg); + run("Find Maxima...", "noise=" + beads_noisetolerance + " output=[Point Selection]"); + roi_peaks = selectionToROI("peaks_from_mean", respath); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// measure (evaulate) using the above determined points ////////// ////////// ////////// + // DISCUSS: should this be normalized (bit depth) to allow comparing different sensors? + measureSelection(img_std, "StdDev", respath, false, save_measurements); + getStatistics(_, mean_std, _, _, std_std); // mean and std from the Noise + print("StdDev stats: mean=" + mean_std + " std=" + std_std); + + measureSelection(img_snr, "SNR", respath, false, save_measurements); + getStatistics(_, mean_snr, _, _, std_snr); // mean and std from the SNR + print("SNR stats: mean=" + mean_snr + " std=" + std_snr); + + measureSelection(img_avg, "mean", respath, false, save_measurements); + getStatistics(_, mean_sig, _, _, std_sig); // mean and std from the signal + print("signal stats: mean=" + mean_sig + " std=" + std_sig); + + mean_sbr = mean_sig / mean_bg; // calculate signal-to-background (SBR) + print("SBR (signal-to-background): " + mean_sbr); + + // NOTE about error-propagation: all components contributing to the SBR contain an error and + // thus the SBR standard-deviation should be taken into consideration when further analyzing / + // using the mean SBR value! + + nBeads = nResults; + print("number of beads detected / measured: " + nBeads); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// measure bleaching ////////// ////////// ////////// ////////// ////////// ////////// + // prepare measurements: area/mean/stddev to allow calculating the SEM = StdDev / sqrt(npixels) + run("Set Measurements...", "area mean standard redirect=None decimal=2"); + + imgs_bleach = duplicateImage(imgs_beadmask, "bleaching", false); + + // erode the mask a bit to compensate for a possible xy-drift: + erode(imgs_bleach, 3); + + // NOTE: if a scatter plot of stddev vs. time reveals an increasing trend this might indicate + // an xy-drift. try dilating the bead mask more in that case, if that does not help the + // time-series should be stabilized (e.g. using stackreg) before analyzing (make sure NOT to + // have sub-pixel correction as this will change the SNR!!) + + roi_bleach = binaryToSelection(imgs_bleach, "bead_intensity_vs_time", respath); + closeImage(imgs_bleach); // we have the regions in the ROI manager, no need to keep the image + + measureSelection(imgs_orig, "bead_intensity_vs_time", respath, true, save_measurements); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + /////////// create images showing detected ROIs //////////// ////////// ////////// ////////// + rgb_roi_bg = makeFilledROIImage(imgs_orig, "background", false); + addTextToImage(rgb_roi_bg, false, "Center", 22, + "Red is representing the area detected as background."); + if (avg_has_saturated_pixels) { + rgb_roi_satmask = makeFilledROIImage(imgs_orig, "saturation_mask", true); + addTextToImage(rgb_roi_satmask, false, "Center", 22, + "Red areas contain saturation and are being ignored."); + } else { + rgb_roi_satmask = duplicateImage(imgs_orig, "ROI - saturation_mask", true); + addTextToImage(rgb_roi_satmask, false, "Center", 22, + "No saturated pixels found, no need to exclude any beads from analysis."); + run("RGB Color"); + } + rgb_roi_intensity = makeFilledROIImage(imgs_orig, "bead_intensity_vs_time", false); + addTextToImage(rgb_roi_intensity, false, "Center", 22, + "Red pixels used for Mean / StdDev plots over time."); + + if (make_kymographs) { + rgb_kymo_lines = drawROIsOnImage(imgs_orig, "bead-", "green"); + addTextToImage(rgb_kymo_lines, false, "Center", 22, + "Green lines showing selections used to create Kymographs."); + } + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + /////////// decorate images, add calibration bars //////////// ////////// ////////// ////////// + rgb_t0 = dressImage(imgs_orig, "Glow", -1, "First timepoint (T0) of the original data."); + rgb_avg = dressImage(img_avg, "Fire", -1, "Average projection of the background-subtracted data."); + rgb_std = dressImage(img_std, "Fire", -1, "StdDev projection of the background-subtracted data."); + rgb_snr = dressImage(img_snr, "physics", 0.35, "Signal-To-Noise ratio for all pixels."); + + // adjust name for original image (now showing the first time point): + addTextToImage(rgb_t0, false, "Center", 12, "Original file name:"); + addTextToImage(rgb_t0, false, "Center", 12, fname); + rename("Original Data"); + + // clean up image windows: + closeImage(imgs_bgsub); + closeImage(imgs_bg); + closeImage(imgs_beadmask); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// create plots from bleaching measurements ////////// ////////// ////////// ////////// + // linear decay = z-drift, exponential decay = bleaching + col_mean = "Mean(bead_intensity_vs_time)"; + plot_mean = plotResult("Mean bead intensity over time", "Frame", "bead intensity mean", col_mean); + addTextToImage(plot_mean, false, "Center", 22, + "A linear decay is an indicator for a z-drift, an exponential decay for bleaching."); + addTextToImage(plot_mean, false, "Center", 96, ""); + + // linear increase = xy-drift + // this reveals only drift that is relvant for the regions that measure bleaching or z-drift. + // could in general be used to reveal xy-drift as it also interferes with SNR + col_std = "StdDev(bead_intensity_vs_time)"; + plot_std = plotResult("Standard deviation of bead intensity over time", "Frame", + "bead intensity StdDev", col_std); + addTextToImage(plot_std, false, "Center", 22, + "Evolution of noise over time."); + + run("Combine...", "stack1=[plot of " + col_mean + "] stack2=[plot of " + col_std + "] combine"); + rename("Plots of intensity vs. time"); + rgb_plots = getImageID(); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + /////////// write summary of results and settings into a new table //////////// ////////// + title = "NoiSee Results Summary"; + + pairs = newArray(); + pairs = Array.concat(pairs, "SNR mean", mean_snr); + pairs = Array.concat(pairs, "SNR StdDev", std_snr); + pairs = Array.concat(pairs, "SBR mean", mean_sbr); + pairs = Array.concat(pairs, "Signal mean", mean_sig); + pairs = Array.concat(pairs, "Noise mean", mean_std); + pairs = Array.concat(pairs, "Background mean", mean_bg); + pairs = Array.concat(pairs, "Background StdDev", std_bg); + pairs = Array.concat(pairs, "Background area", area_bg); + pairs = Array.concat(pairs, "Background area %", areapct_full_bg); + pairs = Array.concat(pairs, "Number of beads", nBeads); + pairs = Array.concat(pairs, "", ""); + pairs = Array.concat(pairs, "-- Settings --", ""); + pairs = Array.concat(pairs, "Beads diameter", beads_diameter); + pairs = Array.concat(pairs, "Noise tolerance", beads_noisetolerance); + pairs = Array.concat(pairs, "", ""); + pairs = Array.concat(pairs, "-- Input --", ""); + pairs = Array.concat(pairs, "Filename", fname); + pairs = Array.concat(pairs, "Rescaling divisor used", divisor); + + + createTable(title, pairs, respath + "/" + "NoiSee-summary.txt"); + img_summary = createTableImage(title, pairs); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// adjust order of the images and create a PDF ////////// ////////// ////////// + + // the PDF exporter of ImageJ simply concatenates all open images in ascending order, + // starting with the oldest one first, therefore we need to duplicate all images into + // new ones (closing the old ones) in the order which is desired in the resulting PDF: + rgb_t0 = duplicateAndClose(rgb_t0); + rgb_avg = duplicateAndClose(rgb_avg); + rgb_std = duplicateAndClose(rgb_std); + rgb_snr = duplicateAndClose(rgb_snr); + if (make_kymographs){ + rgb_kymo = duplicateAndClose(rgb_kymo); + rgb_kymo_lines = duplicateAndClose(rgb_kymo_lines); + } + rgb_roi_bg = duplicateAndClose(rgb_roi_bg); + rgb_roi_satmask = duplicateAndClose(rgb_roi_satmask); + rgb_roi_intensity = duplicateAndClose(rgb_roi_intensity); + + rgb_plots = duplicateAndClose(rgb_plots); + img_summary = duplicateAndClose(img_summary); + + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// + + + + ////////// save the log, create PDF and arrange the windows on the screen ////////// ////////// + if (save_log) { + selectWindow("Log"); + saveAs("Text", respath + "/Log.txt"); + } + + setBatchMode("exit and display"); // exit batch mode and show images + // wait(100); // give the OS some time to display all image windows + + if (save_pdf) { + fname_pdf = respath + "/" + "NoiSee-report.pdf"; + print("Creating PDF from images: " + fname_pdf); + run("PDF ... ", "show show scale save one save=[" + fname_pdf + "]"); + } + closeImage(img_summary); + + row_upper = newArray(rgb_t0, rgb_avg, rgb_plots); + row_lower = newArray(rgb_std, rgb_snr); + + if (make_kymographs) { + row_lower = Array.concat(row_lower, rgb_kymo); + } + + if (keep_roiimages) { + row_upper = Array.concat(row_upper, rgb_roi_intensity, rgb_roi_bg); + if (make_kymographs) { + row_lower = Array.concat(row_lower, rgb_kymo_lines); + } + row_lower = Array.concat(row_lower, rgb_roi_satmask); + } else { + closeImage(rgb_kymo_lines); + closeImage(rgb_roi_bg); + closeImage(rgb_roi_satmask); + closeImage(rgb_roi_intensity); + } + + arrangeImages(row_upper, screenHeight * 0.1); + arrangeImages(row_lower, screenHeight * 0.5); + ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// +} -- GitLab