Measuring cardiomyocytes

Hey people,

I tried my best to find an existing post before writing here. However there are so many helpful and related posts, but until now i was unable to solve my problem with their help.

I took a lot of photos of isolated atrial cardiomyocytes to compare their length and width. My aim is to measure cell length and width with Fiji/ImageJ, if possible with a little support of automation.
The problem is that the cells are nearly transparent and sometimes there are artifacts just as erythrocytes and tissue rests remaining from the isolation. I tried out to threshold, fill holes,…, but naturally the more transparent parts of the cell always get lost as background. Here is an example of a cropped image as a jpeg (it was too big otherwise):

Does anybody of you have experience or a good idea how to fix this? Or maybe even somebody even knows a solution/a script to bring a little automation to >30GB of photos i want to analyze. (luckily i even have acces to a HPC-Cluster for this project)

I’m very happy to hear from you!
Peter

Hello and welcome to this forum.

It would be really helpful to post a .png or zipped .tif instead of .jpg.

I think in your case an edge detection could yield a more robust result.
Process > Find Edges…

Then do a threshold:
Image > Adjust > Threshold…

and process the binary image a bit (https://imagej.nih.gov/ij/docs/guide/146-29.html#sub:Binary):
Process > Binary > …

You can then filter out smaller or roundish objects using the particle analyzer (https://imagej.nih.gov/ij/docs/guide/146-30.html#sub:Analyze-Particles…):
Analyze > Analyze Particles …

With these tools one can clean up the mask like so:

This then allows to create different kinds of measurements. Either with the ROIs themselves via the ROI Manager and the included Measurements: https://imagej.nih.gov/ij/docs/guide/146-30.html#sub:Set-Measurements…

or then via a skeleton analysis:

I can post a small script including the settings if this is going into a useful direction for you.

1 Like

Hello Mr Schmied!

Amazing how fast you did this! Thank you very much!
This is very close to what i tried before, so i assume i’m a little to inexperienced… I don’t get even close to merge the white parts of each cell after thresholding into one. The image you sent looks exactly like my imagination when i started to try different processing steps… how did you do that?

Please find below a short macro for a segmentation prototype.
Just be aware that this now optimized for this specific example.
Particulary the size and circularity settings in the Analyze Particles command are depending on the objects in your image.

There might be also better ways to achieve your segmentation. Maybe you can test the Weka Segmentation: https://imagej.net/Trainable_Weka_Segmentation

Or the Morphological Segmentation of the MorphoLibJ offers maybe some more advanced options: https://imagej.net/MorphoLibJ

As for analysis you need to deal somehow with objects that are fused. You could exclude those from further analysis. You could try to achieve a better segmentation which includes object splitting. Also the skeleton analysis could allow you to differentiate between larger “branches”.

// forground - background color setting
run("Colors...", "foreground=white background=black selection=yellow");
run("Options...", "iterations=1 count=1 black do=Nothing");

// Find edges
run("Find Edges");

// threshold and convert to mask
setAutoThreshold("Huang dark");
setOption("BlackBackground", true);
run("Convert to Mask");

// binary operations to fill holes and close gaps
run("Dilate");
run("Fill Holes");
run("Close-");
run("Erode");

// clean up mask note the size and circularity filter
run("Analyze Particles...", "size=15000-Infinity circularity=0.00-0.20 show=Masks");

// invert LUT of resulting cleaned up mask
run("Invert LUT");

To run this macro just:
Plugins > New > Macro

Copy & Paste code
Open Image
Press Run

1 Like

Thank you very much! The macro works fine with the bigger images after some adjustment of the values. I thought about TWS as well, because I use it for another project to quantify TEM pictures and the plugin is incredibly powerful. But in my case i think its more useful to use the described workflow. At least for the beginning…

I worked further with it and will try to use the Skeleton function today in the evening!
Thank you so far! I hope to be able to show you my results later on or i might have to ask some more questions…

1 Like

Hello!
I tried out a lot came to this to run on a folder filled with different images to analyse.
Now im trying to go on and add the Skeletonize function to the little macro. But at this point i have some issues… I only want the Skeletonize function to be done on the selected ROIs (not on the whole image) and i would be really happy to project the longest shortest path on the original image (just as i did with the ROIs by saving them into a tiff, as you can see in the macro below), to make my measurements validate-able and to be able to explain my technique.
After all this, i want to save the measured longest shortest paths just in the same order as the measurements from ROI Manager were saved before(see the macro below).

//@ File (label = "Input directory", style = "directory") input
//@ File (label = "Output directory", style = "directory") output
//@ String (label = "File suffix", value = ".nd2") suffix
//@ String (label = "ROIs on original", value = "_clear") rois_on_orig
text1 = "Please delete wrongly chosen ROIs, then press OK to proceed measuring"
//setBatchMode(true) //prevents the single process windows to pop up
processFolder(input);

//personalize messages here for the user
//text1 = "Please delete wrongly chosen ROIs, then press OK to proceed measuring" //text shown when the user should delete wrongly chosen ROIs

// function to scan folders/subfolders/files to find files with correct suffix
function processFolder(input) {
	list = getFileList(input);
	list = Array.sort(list);
	for (i = 0; i < list.length; i++) {
		if(File.isDirectory(input + File.separator + list[i]))
			processFolder(input + File.separator + list[i]);
		if(endsWith(list[i], suffix))
			processFile(input, output, list[i]);
	}
}

//processes eyery single file found in the Folder by my script
function processFile(input, output, file) {
	
	run("Bio-Formats Importer", "open=[" + input + "/" + file +"] autoscale color_mode=Default rois_import=[ROI manager] view=Hyperstack stack_order=XYCZT");//opens image via bioformat-plugin
	// forground - background color setting
	rename(File.nameWithoutExtension); //rename original to get its name defined for the saving of the overlay later on
	run("Colors...", "foreground=white background=black selection=yellow");
	run("Options...", "iterations=1 count=1 black do=Nothing");
	run("Duplicate..."," "); //make a duplicate to process savely
	copy = getImageID(); // define variable "copy" with name of duplicate
	
	// Find edges
	run("Find Edges");
	
	// threshold and convert to mask
	setAutoThreshold("Huang dark");
	setOption("BlackBackground", true);
	run("Convert to Mask");
	
	// binary operations to fill holes and close gaps
	run("Dilate");
	run("Fill Holes");
	run("Close-");
	run("Erode");
	
	//set measurements to get information about shape/area/...
	run("Set Measurements...", "area mean min perimeter shape display redirect=None decimal=3");
	// clean up mask note the size and circularity filter
	//original: run("Analyze Particles...", "size=200-25000 circularity=0.00-0.20 show=Masks");
	run("Analyze Particles...", "size=100-20000 circularity=0.00-0.20 show=Masks exclude add");
	//measure analyzed particles
	// invert LUT of resulting cleaned up mask
	run("Invert LUT");
	
	//save the overlay on the original image as tiff
	selectWindow(File.nameWithoutExtension);
	run("Show Overlay");
	saveAs("tiff", output + "/" + File.nameWithoutExtension + rois_on_orig);

	//let the user deselect wrong ROIs
	roiManager("show all with labels");
	waitForUser(text1);
	roiManager("deselect");
	//roiManager("save selected", file-path)
	
	//measure ROIs 
	roiManager("Deselect");
	roiManager("Measure");
	roiManager("deselect");
	
	//save Results 
	//saveAs(file + "ROIs", output);
	selectWindow("Results");
	saveAs("text", output + "/" + File.nameWithoutExtension + "results1"); //save measured results from roi to output directory

Thanks for your help and patience,

Peter

Hi,

you make great progress, I will try to answer your questions and point you in the right direction:

Based on your description, I would just loop through the ROIs and duplicate out the image and then perform the analysis within this cropped part.

Projecting back any information sound a bit complicated for a simple macro. I would save each individual ROI with the extracted information. Just use a suitable naming pattern that connects each image ROI with the original image and a ROI. One can then validate the analysis on these smaller fragments.

The last question I don’t understand completely. Might depend on how the different analysis are selecting in what order things are measured… You would need to find a label in both analysis which would allow you to match the different measurements. Alternative is to just circumvent this issue by calculating means per image or do both measurements per object at the same time and save individual measurements and then collect them later…

Hello,

to be honest, its more kind of reading a lot and trying to understand what others did… Actually I’m feeling like an absolute beginner using ImageJ. But thank you very much!

That sounds good, as long as the duplicates are scaled the same as the ROI was in the original image. The nextquestion would be then, how to write the extracted longest shortest path into a tables same row as the corresponding measurements from the ROI. I found a related post and the cropping works great after adapting that to the macro. The size/scale seems to stay the same as well.
Is there a way to fill in the results of this into a table in the same row as the first measurements are filled in?

I think youre right. As well i think it should be alright to validate the analysis as i can out of the saved tiff with rois…

The problem is, that i would like to combine the measurements from the ROIManager (see macro) with the measurements from the skeleton tool (they belong to the same cell and i want to calculate a mean width out of shortest longest path and area, as it seems to be the most valuable and bias-free parameter to me). I thought about just running the “Skeleton & Analyze skeleton” on a binary made only out of the ROIs the macro.
(Only on the ROIs: This ist important, as it allows the user to deselect several ROIs from the first binary output (dead cells, artifacts,…)
Do you know, whether ImageJ processes the skeleton process in the same order as it does the Analyze particles? That would enable me to combine the both results tables after the lastly described processing just by copy’n’pasting them into the same excel… Skeleton on the whole image seems to work fine… and produces a similar results table as ROI-measuring does.

I tried out the Skeleton on a cropped roi and got this:


It seems to me, that the skeleton is shorter than the cell was. is that normal or am I just to tired and set something wrong?

Sorry for the very long answer… Its just that interesting and i feel that stupid :wink:

You can find a copy of my macro below, if you’re interested. Thanks a lot for your help!

Peter


//@ File (label = "Input directory", style = "directory") input
//@ File (label = "Output directory", style = "directory") output
//@ String (label = "File suffix", value = ".nd2") suffix
//@ String (label = "output suffix", value = "_clear") outputsuffix
//@ String (label = "ROIs on original", value = "_clear") rois_on_orig
text1 = "Please delete wrongly chosen ROIs, then press OK to proceed measuring"
//setBatchMode(true) //prevents the single process windows to pop up
processFolder(input);

//personalize messages here for the user
//text1 = "Please delete wrongly chosen ROIs, then press OK to proceed measuring" //text shown when the user should delete wrongly chosen ROIs

// function to scan folders/subfolders/files to find files with correct suffix
function processFolder(input) {
	list = getFileList(input);
	list = Array.sort(list);
	for (i = 0; i < list.length; i++) {
		if(File.isDirectory(input + File.separator + list[i]))
			processFolder(input + File.separator + list[i]);
		if(endsWith(list[i], suffix))
			processFile(input, output, list[i]);
	}
}

//processes eyery single file found in the Folder by my script
function processFile(input, output, file) {
	
	run("Bio-Formats Importer", "open=[" + input + "/" + file +"] autoscale color_mode=Default rois_import=[ROI manager] view=Hyperstack stack_order=XYCZT");//opens image via bioformat-plugin
	// forground - background color setting
	rename(File.nameWithoutExtension); //rename original to get its name defined for the saving of the overlay later on
	run("Colors...", "foreground=white background=black selection=yellow");
	run("Options...", "iterations=1 count=1 black do=Nothing");
	run("Duplicate..."," "); //make a duplicate to process savely
	copy = getImageID(); // define variable "copy" with name of duplicate

	//define imagespecific variables
	filename = File.nameWithoutExtension;
	mask = "Mask of "+ filename+ "-1";
	
	// Find edges
	run("Find Edges");
	
	// threshold and convert to mask
	setAutoThreshold("Huang dark");
	setOption("BlackBackground", true);
	run("Convert to Mask");
	
	// binary operations to fill holes and close gaps
	run("Dilate");
	run("Fill Holes");
	run("Close-");
	run("Erode");
	
	//set measurements to get information about shape/area/...
	run("Set Measurements...", "area mean min perimeter shape display redirect=None decimal=3");
	// clean up mask note the size and circularity filter
	//original: run("Analyze Particles...", "size=200-25000 circularity=0.00-0.20 show=Masks");
	run("Analyze Particles...", "size=100-20000 circularity=0.00-0.20 show=Masks exclude add");
	//measure analyzed particles
	// invert LUT of resulting cleaned up mask
	run("Invert LUT");
	
	//save the overlay on the original image as tiff
	selectWindow(File.nameWithoutExtension);
	run("Show Overlay");
	saveAs("tiff", output + "/" + File.nameWithoutExtension + rois_on_orig);

	//the following function could (with further changes)save every ROI as a single Image  
	//selectWindow(File.nameWithoutExtension + rois_on_orig + ".tif");
	//roiManager("show all without labels");
	//selectWindow(File.nameWithoutExtension + rois_on_orig + ".tif");

    //for (u=0; u<roiManager("count"); ++u) {
    //    run("Duplicate...", "title=crop");
    //    roiManager("Select", u);
    //    run("To Bounding Box");
    //    run("Enlarge...", "enlarge=30 pixel");
    //    run("Crop");
    //    saveAs("Tiff", output+"/croppedROIs"+File.separator+"ROI"+(u+1)+".tif");
    //    close();
    //    selectWindow(File.nameWithoutExtension + rois_on_orig + ".tif");
    //    roiManager("show all without labels");
	//	selectWindow(File.nameWithoutExtension + rois_on_orig + ".tif");
    //}

	//let the user deselect wrong ROIs
	roiManager("show all with labels");
	waitForUser(text1);
	roiManager("deselect");
	//roiManager("save selected", file-path)
	
	//measure ROIs 
	roiManager("Deselect");
	roiManager("Measure");
	roiManager("deselect");
	
	//save Results 
	//saveAs(file + "ROIs", output);
	selectWindow("Results");
	saveAs("text", output + "/" + File.nameWithoutExtension + "results1"); //save measured results from roi to output directory
	run("Close");
	selectWindow(mask);
	roiManager("show all without labels");
	
	
	//measure every single ROI with Skeletonize
	for (i = 0; i < list.length; i++){
        dirCropOutput = output+File.separator+filename;
        File.makeDirectory(dirCropOutput);     
        run("Duplicate...", "title=particles"); 
        selectWindow(mask);

        for (u=0; u<roiManager("count"); ++u) {
            run("Duplicate...", "title=crop");
            roiManager("Select", u);
            run("Crop");
           	//saveAs("Tiff", dirCropOutput+File.separator+"Skeleton_"+(u+1)+".tif"); //save ROI as image to bea able to check on that later on
            
            
            //insert skeletonize here?
            
            
           
           
           close();
             //Next round!
             selectWindow(mask);
        }
        //close();
        //selectWindow("particles");
        //close();
        //print("finished");    
}
	
}

Hi,

its great! We have all been at that point. Just a few steps now for your first workflow.

Rather than doing the global measurement on all ROIs, I would put the measurement before you do the crop. Then you know exactly that this is what you measured. No guessing involved.

You can extract from the one table measurements and add them to another table:

getResult("Column", row);
setResult("Column", row, value);

Just assign the result to a variable and then use them accordingly:

varMean = getResult("Mean", rowIndex);
setResult("Column",  rowIndex, varMean);

There are examples provided in the documentation: https://imagej.nih.gov/ij/developer/macro/functions.html#setResult

Hi!

I resume working on the macro but i had to do some other experiments in between… this is why it takes me some time to answer.

That sounds great! I will have to try that on the script because I still don’t really understand whether its going to be saved into the same results-table as the area measurements before. I will read the documentation about this!

Do you know something about this? Can i get the skeleton function to measure the “whole length”? I didnt really find a solution so far :wink: but i’m still searching :slight_smile:

Hi,

the thinning algorithm of the skeletonization is also taking a bit off the tips of the skeleton.
Is that what you mean by not the same length or whole length?

Cheers,
Christopher