///////////////////////////////////////////////
// SMA - SIMPLE MUSCLE ARCHITECTURE ANALYSIS //
///////////////////////////////////////////////
// Requires OrientationJ plugin v2.0.3 (http://bigwww.epfl.ch/demo/orientation - Z. P sp ki, M. Storath, D. Sage, M. Unser, "Transforms and Operators for Directional Bioimage Analysis: A Survey," Advances in Anatomy, Embryology and Cell Biology, vol. 219, Focus on Bio-Image Informatics, Springer International Publishing, May 21, 2016.)
// Requires Canny Edge Detector plugin (https://imagej.nih.gov/ij/plugins/canny/index.html - Tom Gibara)
// Requires Non Local Means Denoise plugin v1.4.6 (https://imagej.net/Non_Local_Means_Denoise - Pascal Behnel, Thorsten Wagner)
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
/*
* Version 2.0
* Change log:
* - added possibility to take fascicle curvature into account
* this is implemented by detecting fascicle orientation in superficial and deep regions and
* reconstructing a composite fascicle by simple spline fitting of two fascicle segment or
* by circle fitting.
* - detection of fascicle orientation based on single (straight fascicle method) or paired
* (curved fascicles methods) ROIs. This change is possible because the image is now rotated
* by an angle equal to lower aponeurosis angle and ROIs can extend the full width of the FoV.
* - various changes to filtering method before measurement of fascicle orientation.
*/
// OPTION PARAMETERS ------------------------------------------------------------
/*
#@ String(value = "----- SMA - SIMPLE MUSCLE ARCHITECTURE ANALYSIS -----", visibility="MESSAGE") title
#@ String(value = "NB: Analysis only works with lower aponeurosis orientated horizontally or downwards to the left", visibility="MESSAGE") text0
#@ Boolean (label="Flip image horizontally", value=false, persist=true, description="Required if image displays proximal side to the right") flip
#@ Boolean (label="Panoramic scan", value=false, persist=true, description="panoramic or regular scan") pano
#@ String (label = "Type of analysis", choices= {"Image", "Folder"}, style="radioButtonHorizontal", description="Analyse single image or several images") analysis
#@ String (label = "Fascicle geometry", choices= {"Straight", "Curved_spline", "Curved_circle"}, style="radioButtonHorizontal", description="Assume straight or curved fascicles. Circle method according to Muramatsu et al JAP 2002") geometry
#@ Boolean (label="Print analysis parameters", value=true, persist=true, description="Add analysis parameters to the results table") param
#@ String(value = "------------------ Folder analysis ------------------", visibility="MESSAGE") text1
#@ File (label = "Input directory", style = "directory") input
#@ File (label = "Output directory", style = "directory") output
#@ String (label = "File extension", choices = {".bmp", ".BMP", ".jpg", ".tif", ".png"}) extension
#@ String(value = "------------------ Image cropping ------------------", visibility="MESSAGE") text2
#@ String (label = " ", choices= {"Automatic (requires identical scan depth)", "Manual"}, style="radioButtonHorizontal", persist=true) cropping
#@ String(value = "--------------- Aponeurosis detection ---------------", visibility="MESSAGE") text3
#@ Integer (label = "Tubeness sigma (default: 10)", value = 10, description="Standard deviation of the Gaussian filter. Proportional to aponeurosis thickness") Tsigma
#@ String(value = "---------------- Fascicle orientation ----------------", visibility="MESSAGE") text4
#@ Integer(label="ROI height (% of thickness or 1/2 thickness)", value = 50, min=50, max=90, stepSize=5, persist=false) ROIheight
#@ String (label = "Laplacian of Gaussian (sigma)", choices = {"0", "1", "2","3", "4", "5", "6", "7", "Test"}, value=1, persist=false, description="Proportional to fascicle thickness: after running the test, choosing the value yielding the greatest angle is recommended") Osigma
#@ String(value = "---------------- Pixel scaling ----------------", visibility="MESSAGE") text5
#@ Boolean (label="Scaled measurements", value=false, persist=false, description="Requires scaling bar and identical scan depth when analysing multiple images") scaling
#@ Integer(label="Scan depth (cm)", min=3, max=7, style="scroll bar", value = 5) depth
*/
// GLOBAL VARIABLES ------------------------------------------------------------
var Scan = newArray();
var ALPHA = newArray();
var Lower_aponeurosis_orientation = newArray();
var Pennation_angle = newArray();
var Fascicle_length = newArray();
var Curvature = newArray();
var Thickness = newArray();
var Analysis_duration = newArray();
var Image_cropping = newArray();
var Tubeness_sigma = newArray();
var ROI_h = newArray();
var Thresholding = newArray();
var OrientationJ_sigma = newArray();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This macro processes an opened image or all the images in a folder and any subfolders.
// macro "SMA - Simple Muscle Architecture" {
macro "SMA - Simple Muscle Architecture Action Tool - Txyssc" {
if (isOpen("Results")) { //Close results window (obtained from previous analysis, should be kept out of loop for stacks)
selectWindow("Results");
run("Close");
}
if (isOpen("ROI Manager")) {
selectWindow("ROI Manager");
run("Close");
}
if (analysis == "Image") {
title = getTitle();
run("Select None");
run("Remove Overlay");
if (cropping == "Manual") {
setTool("rectangle");
waitForUser("Select area. Click OK when done");
Roi.getBounds(x, y, width, height);
if (scaling == true) {
run("Select None");
setTool("line");
waitForUser("Select scaling line. Click OK when done");
getLine(x1, y1, x2, y2, lineWidth);
lineLength = sqrt(pow(x1-x2, 2) + pow(y1-y2, 2));
scaleFactor = lineLength/(depth*10);
}
setBatchMode(true);
singleImageAnalysis();
} else {
if (scaling == true) {
run("Select None");
setTool("line");
waitForUser("Select scaling line. Click OK when done");
getLine(x1, y1, x2, y2, lineWidth);
lineLength = sqrt(pow(x1-x2, 2) + pow(y1-y2, 2));
scaleFactor = lineLength/(depth*10);
}
setBatchMode(true);
singleImageAnalysis();
}
} else {
count = 0;
countFiles(input);
n = 0;
n2 = 1;
if (cropping == "Manual") {
list = getFileList(input);
open(input+File.separator+list[0]);
setTool("rectangle");
waitForUser("Select area. Click OK when done");
Roi.getBounds(x, y, width, height);
if (scaling == true) {
run("Select None");
setTool("line");
waitForUser("Select scaling line. Click OK when done");
getLine(x1, y1, x2, y2, lineWidth);
lineLength = sqrt(pow(x1-x2, 2) + pow(y1-y2, 2));
scaleFactor = lineLength/(depth*10);
}
close ();
setBatchMode(true);
processFolder(input);
} else {
if (scaling == true) {
run("Select None");
setTool("line");
waitForUser("Select scaling line. Click OK when done");
getLine(x1, y1, x2, y2, lineWidth);
lineLength = sqrt(pow(x1-x2, 2) + pow(y1-y2, 2));
scaleFactor = lineLength/(depth*10);
}
setBatchMode(true);
processFolder(input);
}
selectWindow("Processed");
run("Close");
}
function countFiles(input) {
list = getFileList(input);
for (i=0; i= nBelowThreshold) {
lowThresh = pxlValues[f]; // lower threshold corresponding to % of highest GV pixels in FFT
f = 99999999;//break
}
}
setThreshold(lowThresh, 255);
setOption("BlackBackground", true);
run("Convert to Mask");
setColor(0);
makeRectangle(Wfft/2+2, 0, Wfft/2, Hfft/2);
fill();
makeRectangle(0, Hfft/2+1, Wfft/2-1, Hfft/2);
fill();
run("Inverse FFT");
IDinvFFT1 = getImageID();
selectImage(IDFFT1);
close ();
selectImage(IDinvFFT1);
run("Canny Edge Detector", "gaussian=2 low=1 high=7.5");
//run("Dilate");
//run("Erode");
run("Analyze Particles...", "size=0-minLength show=Masks");
IDmask = getImageID(); // Mask of shorter lines
imageCalculator("Subtract create", IDinvFFT1,IDmask);
// Start detecting position of aponeuroses
U = round(0.3*HFoV);
V = U + 1;
Upper = lineFinder(0, U, 1); // The 1 is just a dummy variable so the function can distinguish Upper/Lower
up_st = newArray(2);
up_st[0] = Upper[Upper.length-2];
up_st[1] = Upper[Upper.length-1];
Upper = Array.slice(Upper, 0, Upper.length-2); // Last 2 elements are line indices
Lower = lineFinder(V, HFoV, 0);
lo_st = newArray(2);
lo_st[0] = Lower[Lower.length-2];
lo_st[1] = Lower[Lower.length-1];
Lower = Array.slice(Lower, 0, Lower.length-2);
ok = 1;
if (Upper.length < 10){
ok = 0;
exit("Could not detect upper aponeurosis. Please try again with different value of tubeness sigma or try manual cropping")
} else if (Lower.length < 10) {
ok = 0;
exit("Could not detect lower aponeurosis. Please try again with different value of tubeness sigma or try manual cropping")
}
if (ok == 1) {
L = round(0.05*WFoV);
R = round(0.95*WFoV);
W2 = R - L;
if (Lower.length < Upper.length) {
maxL = Upper.length;
Lower2 = newArray(Upper.length);
for (t=0; t 0) {
roiManager("Show All");
cont = 1;
}
if (cont > 0) {
store = newArray(n);
for (i=2; i175 || area<50) { //
doWand(x,y);
run("Clear");
}
}
run("Select None");
run("Options...", "iterations=1 count=1 black do=Open");
run("Create Selection");
selectImage(bin);
close ();
selectImage(IDROI);
run("Restore Selection");
// run("Clear Outside");
run("Make Inverse");
setColor(0);
fill();
run("Select None");
getStatistics(area, mean, min, max, std, histogram);
run("FFT");
IDFFT2 = getImageID();
selectImage(IDFFT2);
percentage = 99.82; // percentage of FFT pixels to threshold out
// percentage = 99.0;
nBins = 256;
resetMinAndMax();
getHistogram(pxlValues, histCounts, nBins);
// find culmulative sum
nPixels = 0;
for (f = 0; f= nBelowThreshold) {
lowThresh = pxlValues[f]; // lower threshold corresponding to % of highest GV pixels in FFT
f = 99999999;//break
}
}
setThreshold(lowThresh, 255);
run("Convert to Mask");
run("Inverse FFT");
IDinvFFT2 = getImageID();
selectImage(IDFFT2);
close ();
selectImage(IDinvFFT2);
run("8-bit");
run("Tubeness", "sigma=1 use");
IDvessel2 = getImageID();
selectImage(IDvessel2);
run("Select All");
if (Osigma == "test") {
for (o = 0; o < 8; o++) {
run("OrientationJ Measure", "sigma="+o);
}
exit();
} else {
run("OrientationJ Measure", "sigma="+Osigma);
}
selectImage(IDvessel2);
close ();
table = getInfo("log");
lines = split(table, "\n");
headings = split(lines[0], "\t");
values = split(lines[1], "\t");
selectWindow("Log");
run("Close");
store[i] = values[6];
store[i] = toString(store[i]);
store[i] = replace(store[i], ",", ".");
store[i] = parseFloat(store[i]);
selectImage(IDinvFFT2);
close ();
selectImage(IDROI);
close ();
}
}
selectImage(IDrawR);
close ();
roiManager("reset");
////////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate fascicle length, pennation angle and thickness
if (geometry == "Curved_spline" || geometry == "Curved_circle") {
alphaDeep = -store[3]; // with angle due to rotation
alphaUp = -store[2];
//Pennation angle
thetaDeep = alphaDeep + betaDeep; //orientation of lower part of fasc. relative to deep apon.
thetaUp = alphaUp + betaDeep; //orientation of upper part of fasc. relative to deep apon.
//Composite fascicle lines (y_3 = b_3 * x + a_3)
b_3d = tan(thetaDeep * (PI / 180)); // slope for the deeper region
b_3u = tan(thetaUp * (PI / 180)); // slope for the upper region
a_3d = LAy[LAy.length-1] - b_3d * LAx[LAx.length-1];
a_3u = MAy[MAy.length-1] - b_3u * MAx[MAx.length-1];
//Fascicle parts intersection coordinates
IxFasc = MAx[MAx.length/2]; // intersection placed on point in the middle of mid line
IyFasc = MAy[MAy.length/2];
//Upper fascicle part - upper aponeurosis intersection coordinates
tempxUp = (a_1 - a_3u) / (b_3u - b_1);
tempyUp = a_1 + b_1 * tempxUp;
//Offset upper fascicle part (y_3 = b_3 * x + a_3)
b_Uo = tan(thetaUp * (PI / 180));
a_Uo = IyFasc - b_Uo * IxFasc;
//Offset coordinates on upper aponeurosis for upper part of fascicle
Oux = (a_1 - a_Uo) / (b_Uo - b_1);
Ouy = a_1 + b_1 * Oux;
//Deeper fascicle part - lower aponeurosis intersection coordinates
tempxDeep = (a_2 - a_3d) / (b_3d - b_2);
tempyDeep = a_2 + b_2 * tempxDeep;
//Offset lower fascicle part (y_3 = b_3 * x + a_3)
b_Do = tan(thetaDeep * (PI / 180));
a_Do = IyFasc - b_Do * IxFasc;
//Offset coordinates on lower aponeurosis for lower part of fascicle
Olx = (a_2 - a_Do) / (b_Do - b_2);
Oly = a_2 + b_2 * Olx;
// From https://forum.image.sc/t/how-do-you-measure-the-angle-of-curvature-of-a-claw/2056/16
// get center of circle passing through fascicles intersection points and mid-depth fascicle point
x_centre = (b_Uo*b_Do*(-Ouy+Oly)+b_Uo*(IxFasc+Olx)-b_Do*(IxFasc+Oux))/(2*(b_Uo-b_Do));
y_centre = -1/b_Uo*(x_centre-(IxFasc+Oux)/2)+(IyFasc+Ouy)/2;
// angle, slope and intercept of segments between circle centre and fascicle intersections, and angle (gamma) between them
b_centreUp = (y_centre - Ouy) / (x_centre - Oux);
a_centreUp = -(y_centre - Ouy) / (x_centre - Oux) * x_centre + y_centre;
ang_Up = atan(b_centreUp)* (180/PI); //angle superficial segment
b_centreDeep = (y_centre - Oly) / (x_centre - Olx);
a_centreDeep = -(y_centre - Oly) / (x_centre - Olx) * x_centre + y_centre;
ang_Deep = atan(b_centreDeep)* (180/PI); //angle deep segment
tan_gamma = abs((b_centreUp-b_centreDeep)/(1+b_centreDeep*b_centreUp));
gamma = atan(tan_gamma)* (180/PI);
//radius, axes, curvature and fascicle length
r = sqrt((x_centre-Oux)*(x_centre-Oux) + (y_centre-Ouy)*(y_centre-Ouy)); //radius (to upper insertion)
r2 = sqrt((x_centre-Olx)*(x_centre-Olx) + (y_centre-Oly)*(y_centre-Oly)); //radius (to lower insertion)
if (y_centre > 0) { //fascicle curved towards upper aponeurosis
Crv = 1/r; //curvature
} else {
Crv = -1/r;
}
if (geometry == "Curved_circle") {
Lf = 2*PI*r*(gamma/360);
c_x = newArray(round(gamma+1));
c_y = newArray(round(gamma+1));
if (y_centre > 0) {
for (i = 0; i < round(gamma)+1; i++) {
c_x[i] = x_centre + r * cos((abs(ang_Deep)+i)*PI/180);
c_y[i] = y_centre - r * sin((abs(ang_Deep)+i)*PI/180); // "-" because the Y origin points downwards
}
} else {
for (i = 0; i < round(gamma)+1; i++) {
c_x[i] = x_centre - r * cos((abs(ang_Up)+i)*PI/180);
c_y[i] = y_centre + r * sin((abs(ang_Up)+i)*PI/180); // "-" because the Y origin points downwards
}
}
makeSelection(7, c_x, c_y); // make circle arc
roiManager("add");
} else { // Spline
makeSelection("polyline", newArray(Oux,IxFasc,Olx), newArray(Ouy,IyFasc,Oly));
run("Fit Spline");
run("Interpolate");
List.setMeasurements;
Lf = List.getValue("Length");
roiManager("add");
}
} else { //if fascicles are analysed as straight lines
alphaDeep = -store[2];
//Pennation angle
theta = alphaDeep + betaDeep;
//Composite fascicle line (y_3 = b_3 * x + a_3)
b_3 = tan(theta * (PI / 180));
a_3 = LAy[LAy.length-1] - b_3 * LAx[LAx.length-1]; //Assumption: the upmost right point on the lower aponeurosis
//Fascicle-upper aponeurosis intersection coordinates
Ix = (a_1 - a_3) / (b_3 - b_1);
Iy = a_1 + b_1 * Ix;
//Offset coordinates on lower aponeurosis
Hx1 = (Ix + b_2 * Iy - b_2 * a_2) / ((b_2 * b_2) + 1);
Hy1 = b_2 * ((Ix + b_2 * Iy - b_2 * a_2) / ((b_2 * b_2) + 1)) + a_2;
Olx = LAx[LAx.length-1] + ((LAx[0]-Hx1)/2);
Oly = LAy[LAy.length-1] + ((LAy[0]-Hy1)/2);
//Offset composite fascicle line (y_3 = b_3 * x + a_3)
b_o = tan(theta * (PI / 180));
a_o = Oly - b_o * Olx;
//Offset coordinates on upper aponeurosis
Oux = (a_1 - a_o) / (b_o - b_1);
Ouy = a_1 + b_1 * Oux;
//unscaled fascicle length
Lf = sqrt((Olx-Oux)*(Olx-Oux) + (Oly-Ouy)*(Oly-Ouy)); //fascicle length
Crv = 0; //no curvature
makeLine(Olx, Oly, Oux, Ouy);
roiManager("Add");
}
//adjust fascicle and ROIs display
Omx = (Olx+Oux)/2; //x coordinate of mid fascicle
mFoVx = UAx[(UAx.length)/2]; //x coordinate of mid FoV
if (Oux<0 || Olx>W) {
n = roiManager("count");
roiManager("select", n-1);
roiManager("translate", -(Omx-mFoVx), 0);
roiManager("Update");
if (geometry == "Straight") {
getLine(Nx1, Ny1, Nx2, Ny2, NlineWidth); // find start and end coordinates of straight line
} else {
Roi.getCoordinates(Spl_xpoints, Spl_ypoints) // find start and end coordinates of spline
Nx2 = Spl_xpoints[0];
Ny2 = Spl_ypoints[0];
Nx1 = Spl_xpoints[Spl_xpoints.length-1];
Ny1 = Spl_ypoints[Spl_ypoints.length-1];
}
left = abs(Nx2);
right = Nx1-W;
if (Nx2<0 && Nx10 && Nx1>W) {
Wnew=W+right;
run("Canvas Size...", "width=Wnew height=H position=Center-Left zero");
} else if (Nx2<0 && Nx1>W && left>right) {
Wnew=W+left*2;
run("Canvas Size...", "width=Wnew height=H position=Center zero");
for (i=0; iW && left0)
value = substring(value, 0, index3);
value = 0 + value; // convert to number
return value;
}
// This function returns the value of the specified
// tag (e.g., "0010,0010") as a string. Returns ""
// if the tag is not found.
function getTag(tag) {
info = getImageInfo();
index1 = indexOf(info, tag);
if (index1==-1) return "";
index1 = indexOf(info, ":", index1);
if (index1==-1) return "";
index2 = indexOf(info, "\n", index1);
value = substring(info, index1+1, index2);
return value;
}
//////////
function arrTidy(Arr) {
for (m=0; m 50) // 50 is a threshold- pixel intensities above 50 are white
hold = append(hold, j + in1, 0); // Store the indices of each possible line in hold
hold = Array.slice(hold, 1, hold.length);
line = newArray(1);
ind1 = 0;
ind2 = 1;
if (in3 == 1)
a = hold.length - 1;
else
a = 0;
found = 0;
while (found != 1 && a >= 0 && a < hold.length) {
line[0] = hold[a]; // Start with right half of line
if(in3 == 1)
makeLine(M+1, line[0]-21, M+1, line[0]-1); // Check just above/below each 'line' for a parallel line
else
makeLine(M+1, line[0]+1, M+1, line[0]+20);
pr = getProfile();
sum = 0;
for(c=0; c 50) {
b = (b - 1) + t;
line = append(line, b, 0);
t = prof.length;
ind2 = s; } }
}
b = line[0];
// Find left half of line
for(s=M; s>L; s--) { // WAS M-1, might change back
makeLine(s, b-1, s, b+1); // Assume aponeurosis location varies by no more than +/-1 per pixel
prof = getProfile();
for(t=0; t 50) {
b = (b - 1) + t;
line = append(line, b, 1);
t = prof.length;
ind1 = s; } }
}
}
if (line.length > 2) { // If a suitable line is found, stop the loop
a = hold.length;
found = 1; }
else if (in3 == 1 && a-1 >= 0) // Or increment and keep looking for a suitable line
a -= 1;
else if (in3 == 0 && a+1 < hold.length)
a += 1;
else // Otherwise stop the loop and move to the next section
a = hold.length;
}
// If no suitable line is found, find the most proximal (deep aponeurosis) or distal line
if(found == 0) {
for(a=0; a 50) {
b = (b - 1) + t;
line = append(line, b, 0);
t = prof.length;
ind2 = s; } }
}
b = line[0];
// Find left half of line
for(s=M-1; s>L; s--) {
makeLine(s, b-1, s, b+1); // Assume aponeurosis location varies by no more than +/-1 per pixel
prof = getProfile();
for(t=0; t 50) {
b = (b - 1) + t;
line = append(line, b, 1);
t = prof.length;
ind1 = s; } }
}
if(line.length > 0.60*WFoV)
a = hold.length;
}
}
line = append(line, ind1, 0); // Need the indices of the start/end of the accepted line
line = append(line, ind2, 0);
return line;
}
//////////
function compDiff() {
oz = newArray(p.length-1);
for(t=0; t=round(value) || a1[i]==round(value+1) || a1[i]==round(value-1)) && (a2[i]==round(condition) || a2[i]==round(condition+1) || a2[i]==round(condition-1))) return i;
return -1;
}