/*
* Erosion Territories
*
* Expected Input:
* The expected input for this script are 2D non-timelapse images in tiff/tif/ND2 format that are either 3 or 4 channel
* One channel is expect to be a DAPI or other nuclear stain
* At start of macro, user selects folder with images in (nested folders dealth with fine), sets channel order and chooses desired number of bins
*
* Description of script workflow:
* The DAPI channel is used to segment the nuclei (using gaussian blur and huang threshold and the 'watershed irregular features' from BioVoxxel - see installation notes)
* Each nucleus area is duplicated from original image to isolate nuclei for further processing
* For each nuclei image, the area is measured and the expected area of the bins is calculated based on the number requsted
* Using the expected areas of these bins and iteratively thresholding on a distance map of the nucleus ROI bins are produced
* Mean intensity of these ROIs is measured in all the channels
*
* Output:
* Each nucleus found in each image will be saved as an RGB image with the outline of created bins overlaid on the image
* A .csv file will be saved with the areas of the nuclei, the bins and the mean intensity of each bin for each channel in the image
* Bin/ territory naming in the results go from outside in so bin 1 is the outermost one.
*
* Installation Notes:
* Download FIJI here - https://imagej.net/software/fiji/downloads
* Details of the Biovoxxel Update site - https://imagej.net/plugins/biovoxxel-toolbox
* How to add an update site - https://imagej.net/update-sites/
* How to use an ImageJ macro from Github - https://github.com/IGC-Advanced-Imaging-Resource/ImageJ_Macros
* This script is hosted here with an example image - https://github.com/IGC-Advanced-Imaging-Resource/Purshouse2022_paper
*
* Written by Laura Murphy (laura.murphy@ed.ac.uk)
* IGC Advanced Imaging Resource (https://www.ed.ac.uk/institute-genetics-cancer/facilities/advanced-imaging-resource)
* First written: November 2019 Last updated January 2022
*/
//--------------------------------//-------------------------------------------------------------------------------------------------------
//-- Part 0: Preparation steps: get directories from user, set up arrays to store results that are being added to and some other setup
//--------------------------------//-------------------------------------------------------------------------------------------------------
// Get user to select folder with images for input and the output folder where they want results saved
input_folder = getDirectory("Parent directory of your image folders");
output_folder = getDirectory("Choose the folder where you want to save your results");
// Set up empty arrays that will be filled during script and displayed and saved at the end
Image_Name = newArray();
Nucleus_No = newArray();
Nucleus_Area = newArray();
Bin_No = newArray()
Bin_Area = newArray()
DAPI_MeanIntensity = newArray();
FITC_MeanIntensity = newArray();
TxRd_MeanIntensity = newArray();
Cy5_MeanIntensity = newArray();
// Magic numbers for nuclei segmentation
filter_sigma = 2; // Sigma for Gaussian blur
erosion_cycle = 1; // Erosion cycle for watershed
watershed_convexity = 0.99; // Convexity threshold for watershed
min_particle_size = 50; // Minimum particle size for Analyze Particles
max_particle_size = 700; // Maxmimum particle size for Analyze Particles
min_particle_circularity = 0.7; // Minimum circularity for Analyze Particles
max_particle_circularity = 1.00; // Maxmimum circularity for Analyze Particles
// Array of channel colours
colour_array = newArray("Blue", "Green", "Red", "Cyan");
roiManager("Reset");
// Get list of images in nested folders with certain extensions - function at bottom of macro
directory_list = newArray();
directory_list = get_file_tree(input_folder, directory_list);
//--------------------------------//--------------------------------------------------------------------------------------------
//-- Part 1: Image loop starts, each image goes through nuclear segmentation and each nuclei is duplicated from original image
//--------------------------------//--------------------------------------------------------------------------------------------
// Start looping through images in input folders
for (img = 0; img < directory_list.length; img++) {
path = directory_list[img];
run("Bio-Formats Importer", "open=[" + path + "] autoscale color_mode=Grayscale rois_import=[ROI manager] view=Hyperstack stack_order=XYCZT");
getDimensions(width, height, channels, slices, frames);
original_image = getTitle();
image_name = File.nameWithoutExtension;
// When first image is open, get user to input desired number of bins and assign channels in order of the stack
if (img == 0) {
Dialog.create("User input");
Dialog.addMessage("Enter the number of concentric territories you want the nucleus split into");
Dialog.addNumber("Bins", 4);
Dialog.addMessage("Enter the order of your channels below");
Dialog.addNumber("DAPI", 4);
Dialog.addNumber("FITC", 1);
Dialog.addNumber("TxRd", 2);
Dialog.addNumber("Cy5", 3);
Dialog.show;
//Store user input
bins = Dialog.getNumber;
channel_order = newArray(channels);
for (ch = 0; ch < channels; ch++){
channel_order[ch] = Dialog.getNumber;
}
}
// Get pixel sizing, if image isn't scaled then skip image
getVoxelSize(width, height, depth, unit);
if (unit == "pixels"){
waitForUser("Image has no scaling and will be skipped");
run("Close");
continue;
}
// Set channel colours
for (lut = 0; lut < channel_order.length; lut++){
selectWindow(original_image);
Stack.setChannel(channel_order[lut]);
run(colour_array[lut]);
}
// Duplicate image to preserve for display use
selectWindow(original_image);
run("Duplicate...", "title=Display duplicate");
// Segment nuclei in image
selectWindow(original_image);
Stack.setChannel(channel_order[0]);
run("Duplicate...", "title=Nuclear_Mask");
run("Gaussian Blur...", "sigma=" + filter_sigma);
setAutoThreshold("Huang dark");
run("Convert to Mask");
run("Watershed Irregular Features", "erosion=" + erosion_cycle + " convexity_threshold=" + watershed_convexity + " separator_size=0-Infinity");
run("Analyze Particles...", "size=" + min_particle_size + "-" + max_particle_size + " circularity=" + min_particle_circularity + "-" + max_particle_circularity + " exclude include add");
// Store number of nuclei
number_of_nuclei = roiManager("Count");
// Loop through nuclei, duplicate image of each one in image, naming them so they can be processed further
for (nuclei = 0; nuclei < number_of_nuclei; nuclei++){
selectWindow("Nuclear_Mask");
roiManager("Select", nuclei);
title = "Nuclei_" + nuclei + 1;
run("Duplicate...", "title=" + title);
selectWindow(original_image);
roiManager("Select", nuclei);
run("Duplicate...", "duplicate");
rename("Measure" + title);
selectWindow("Display");
roiManager("Select", nuclei);
run("Duplicate...", "duplicate");
rename("Display" + title);
}
//--------------------------------//--------------------------------------------------------------------------------------------
//-- Part 2: Looping through isolated nuclei image starts, creation of bins
//--------------------------------//--------------------------------------------------------------------------------------------
// Start of loop for going through the duplicated images of the solo nuclei
for (image_of_nucleus = 0; image_of_nucleus < number_of_nuclei; image_of_nucleus++){
title = "Nuclei_" + image_of_nucleus + 1;
roiManager("Reset");
selectWindow(title);
// Create distance map to use for erosion
run("Distance Map");
run("Clear Outside");
// Get area of whole nuclei and maximum value of distance map (further distance in nuclei mask from edge)
getStatistics(whole_area, mean, min, whole_max, std, histogram);
nuclear_area = whole_area;
run("Select None");
// Work out the desired area each bin should be by dividing total nuclear area by number of bins
area_fraction = whole_area/bins;
prebins = newArray(bins);
// Work out the expected areas of the shells (i.e. the smaller circles that share centroid with nucleus)
for (areas_of_prebins = 0; areas_of_prebins < prebins.length; areas_of_prebins++){
addtoArray = whole_area;
prebins[areas_of_prebins] = addtoArray;
whole_area = whole_area-area_fraction;
}
// For each of the chosen bin number, iteratively reduce threshold of distance map until area of thresholded area is lower/equal to expected
// Once that is reached, add to ROI Manager.
for (bin = 0; bin < prebins.length; bin++) {
current_area_aim = prebins[bin];
for (threshold = 1; threshold < whole_max; threshold++) {
getStatistics(area, mean, min, max, std, histogram);
run("Select None");
setThreshold(threshold, whole_max);
run("Create Selection");
getStatistics(area, mean, min, max, std, histogram);
if (area <= current_area_aim){
roiManager("Add");
break;
continue;
}
}
}
// Use prebins to create bins by using XOR function between the pre-bins in the ROI Manager
number_of_rois = roiManager("Count");
to_be_deleted = newArray(number_of_rois);
// Add current ROIs to array to delete later
for (bins = 0; bins < number_of_rois; bins++){
to_be_deleted[bins] = bins;
}
// Loop through rois, selecting pairs to get XOR area between them, add to ROI and name them.
for (roi = 0; roi < number_of_rois-1; roi++){
roiManager("select", newArray(roi, roi+1));
roiManager("XOR");
Roi.setName("BIN_" + roi+1);
roiManager("Add");
roiManager("select", newArray(roi, roi+1));
}
roiManager("select", number_of_rois-1);
Roi.setName("BIN_" + number_of_rois);
roiManager("Add");
// Delete the shells from earlier
roiManager("select", to_be_deleted);
roiManager("Delete");
resetThreshold();
//--------------------------------//--------------------------------------------------------------------------------------------
//-- Part 3: Time to save results, for each bin, the image name, nucleus and nucleus area is saved to keep arrays same length
// Then for each of the bins (also called territories), the area is measured and then the intensity of each of the channels
// All things are saved to arrays which are displayed once complete. On the last image, the arrays are saved to the output folder
//--------------------------------//--------------------------------------------------------------------------------------------
// Loop through final shapes, adding nuclei results to arrays to keep arrays even
for (territories = 0; territories < roiManager("Count"); territories++){
Image_Name = Array.concat(Image_Name, image_name);
Nucleus_No = Array.concat(Nucleus_No, image_of_nucleus+1);
Nucleus_Area = Array.concat(Nucleus_Area, nuclear_area);
Bin_No = Array.concat(Bin_No, territories+1);
selectWindow("Measure" + title);
roiManager("Select", territories);
getStatistics(bin_area, mean, min, max, std, histogram);
Bin_Area = Array.concat(Bin_Area, bin_area);
Stack.setChannel(channel_order[0]);
roiManager("Select", territories);
getStatistics(bin_area, mean_DAPI, min, max, std, histogram);
DAPI_MeanIntensity = Array.concat(DAPI_MeanIntensity, mean_DAPI);
Stack.setChannel(channel_order[1]);
roiManager("Select", territories);
getStatistics(bin_area, mean_FITC, min, max, std, histogram);
FITC_MeanIntensity = Array.concat(FITC_MeanIntensity, mean_FITC);
Stack.setChannel(channel_order[2]);
roiManager("Select", territories);
getStatistics(binArea, mean_TXRD, min, max, std, histogram);
TxRd_MeanIntensity = Array.concat(TxRd_MeanIntensity, mean_TXRD);
// If the images are 4 channel, then save the Cy5 channel
if (channel_order.length > 3){
Stack.setChannel(channel_order[3]);
roiManager("Select", territories);
getStatistics(binArea, mean_Cy5, min, max, std, histogram);
Cy5_MeanIntensity = Array.concat(Cy5_MeanIntensity, mean_Cy5);
}
}
// Take original image, convert to RGB and save with bin boundaries overlayed on image for checking.
selectWindow("Display" + title);
run("Make Composite");
run("RGB Color");
roiManager("Show all without labels");
run("Flatten");
saveAs("Tiff", output_folder + image_name + "_" + title);
}
// Set up for next image coming up by closing open images and reset ROI manager and results table
run("Close All");
roiManager("Reset");
run("Clear Results");
// Show updated array of results each time an image is completed. If image was 3 channel, the Cy5 array will be empty.
Array.show(Image_Name, Nucleus_No, Nucleus_Area, Bin_No, Bin_Area, DAPI_MeanIntensity, FITC_MeanIntensity, TxRd_MeanIntensity, Cy5_MeanIntensity);
}
// Save table of arrays to user directed output folder now all images are processed
saveAs("Results", output_folder + File.separator + "Results.csv");
run("Close");
//--------------------------------//-----------------------------------------------------------------------
//-- Part 4: Let user know the macro is complete
//--------------------------------//-----------------------------------------------------------------------
Dialog.create("Progress");
Dialog.addMessage("Macro Complete!");
Dialog.show;
//--------------------------------//-----------------------------------------------------------------------
//-- Functions
//--------------------------------//-----------------------------------------------------------------------
function get_file_tree(directory, file_tree){
file_list = getFileList(directory);
for(f = 0; f < file_list.length; f++){
if (matches(file_list[f], "(?i).*\\.(tif|tiff|nd2)$"))
file_tree = Array.concat(file_tree, directory + file_list[f]);
if(File.isDirectory(directory + File.separator + file_list[f]))
file_tree = get_file_tree(directory + file_list[f], file_tree);
}
return file_tree;