/////////////////////////////////////////////// // SMA - SIMPLE MUSCLE ARCHITECTURE ANALYSIS // /////////////////////////////////////////////// // Requires OrientationJ plugin v16/01/2018 (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 . // 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=false, description="Required if image displays proximal side to the right") flip #@ String (label = "Type of analysis", choices= {"Image", "Folder"}, persist=false, style="radioButtonHorizontal", description="Analyse single image or several images") analysis #@ 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", "Manual"}, style="radioButtonHorizontal", persist=false) 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 = "Number of ROIs", value = 3, min=2, max=8, persist=false, description="number of ROIs used to detect dominant orientation") ROIn #@ Integer(label="ROI width (% of FoV width)", value = 60, min=50, max=80, stepSize=10, persist=false) ROIwidth #@ Integer(label="ROI height (% of thickness)", value = 90, min=40, max=90, stepSize=10, persist=false) ROIheight #@ Boolean (label="Automatic thresholding", value=true, persist=true, description="Based on % of pixels above threshold in the power spectrum") autThresh #@ Integer (label = "Manual threshold value", value = 175, persist=false) manThresh #@ String (label = "Laplacian of Gaussian (sigma)", choices = {"0", "1", "2","3", "4", "5", "6", "7", "Test"}, value=4, persist=false, description="Proportional to fascicle thickness: after running the test, choosing the value yielding the greatest angle is recommended") Osigma #@ String (label = "Main orientation method", choices = {"max", "median", "mean (not recommended)"}, persist=true, description="Choose value retained from analyses of ROIs ('max' is recommanded in most cases)") Pa #@ String(value = "---------------- Pixel scaling ----------------", visibility="MESSAGE") text5 #@ Boolean (label="Scaled measurements (requires identical scan depth)", 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 Thickness = newArray(); var Analysis_duration = newArray(); var Image_cropping = newArray(); var Tubeness_sigma = newArray(); var ROI_n = newArray(); var ROI_w = newArray(); var ROI_h = newArray(); var Thresholding = newArray(); var OrientationJ_sigma = newArray(); var Orientation = newArray(); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // This macro processes an opened image or all the images in a folder and any subfolders. macro "SMA - Simple Muscle Architecture Analysis" { 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 (count == 0) exit("This folder does not contain any "+extension+" file."); 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") } alpha = 0; beta = 0; theta = 0; Lf = 0; Th = 0; 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(ROIn); for (i=0; i= nBelowThreshold) { lowThresh = pxlValues[f]; // lower threshold corresponding to % of highest GV pixels in FFT f = 99999999;//break } } } else { lowThresh = manThresh; } 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 == "0") run("OrientationJ Measure", "sigma=0.0"); else if (Osigma == "1") run("OrientationJ Measure", "sigma=1.0"); else if (Osigma == "2") run("OrientationJ Measure", "sigma=2.0"); else if (Osigma == "3") run("OrientationJ Measure", "sigma=3.0"); else if (Osigma == "4") run("OrientationJ Measure", "sigma=4.0"); else if (Osigma == "5") run("OrientationJ Measure", "sigma=5.0"); else if (Osigma == "6") run("OrientationJ Measure", "sigma=6.0"); else if (Osigma == "7") run("OrientationJ Measure", "sigma=7.0"); else { run("OrientationJ Measure", "sigma=0.0"); run("OrientationJ Measure", "sigma=1.0"); run("OrientationJ Measure", "sigma=2.0"); run("OrientationJ Measure", "sigma=3.0"); run("OrientationJ Measure", "sigma=4.0"); run("OrientationJ Measure", "sigma=5.0"); run("OrientationJ Measure", "sigma=6.0"); run("OrientationJ Measure", "sigma=7.0"); exit(); } 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 (); } } Array.getStatistics(store, min, max, mean, stdDev); store1 = Array.sort(store); if (store1.length % 2 == 1) //if n ROI is odd integer median = store1[(store1.length-1)/2]; else median = (store1[(store1.length+1)/2] + store1[store1.length/2-1]) / 2; if (Pa == "median") alpha = median; else if (Pa == "max") alpha = min; else alpha = mean; // Array.print(store1); // print("alphaAngle: " + alpha); // print("medianAngle: " + median); // print("maxAngle: " + min); // print("meanAngle: " + mean); // print("minAngle: " + max); //--------------------------------------------------------- // Calculate fascicle length, pennation angle and thickness //Pennation angle theta = (-alpha) - beta; //Composite fascicle line (y_3 = b_3 * x + a_3) b_3 = tan(-alpha * (PI / 180)); if (b_2 <= 0) { a_3 = minLAy - b_3 * maxLAx; //Assumption: the upmost right point on the lower aponeurosis has coordinates (maxLAx, minLAy) } else { a_3 = maxLAy - b_3 * maxLAx; } //Fascicle-upper aponeurosis intersection coordinates Ix = (a_1 - a_3) / (b_3 - b_1); //yy = b_3 * xx + a_3 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 = maxLAx + ((minLAx - Hx1)/2); if (b_2 <= 0) { Oly = minLAy + ((maxLAy - Hy1)/2); } else { Oly = maxLAy + ((minLAy - Hy1)/2); } //Offset composite fascicle line (y_3 = b_3 * x + a_3) b_o = tan(-alpha * (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)); makeLine(Olx, Oly, Oux, Ouy); roiManager("Add"); n = roiManager("count"); //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) { roiManager("select", n-1); roiManager("translate", -(Omx-mFoVx), 0); roiManager("Update"); getLine(Nx1, Ny1, Nx2, Ny2, NlineWidth); // right most start point is Nx2,Ny2 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 && left 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