/////////////////////////////////////////////// // 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 - C000Dd6C000C111D66C111C222D67C222D97C222De8C222C333D38C333D96C333De9C333D79C333C444DbaC444Dc8C444D3aC444C555D89C555Dc9C555D78C555C666DfaC666Dc7C666D2aC666D27C666C777Dd9C777D1aD26C777Dd7C777D88C777D28C777C888D17C888D39C888D68C888D36C888D98C888De7C888D16C888C999Da6Da7Da8Da9DaaDb9C999D49C999D56D57D58D59D5aC999CaaaD7aCaaaD99D9aCaaaD8aCbbbD69CbbbD6aCbbbD77CbbbDeaCbbbCcccDf9CcccDc6CcccCdddD35CdddD87CdddD25CdddDd8CdddD4aCeeeDd5CeeeD65CeeeD46D48De6CeeeD2bD95CeeeCfffDb8CfffDa5CfffD55CfffDcaCfffD37CfffD18D45D76CfffD00D01D02D03D04D05D06D07D08D09D0aD0bD0cD0dD0eD0fD10D11D12D13D14D15D19D1bD1cD1dD1eD1fD20D21D22D23D24D29D2cD2dD2eD2fD30D31D32D33D34D3bD3cD3dD3eD3fD40D41D42D43D44D47D4bD4cD4dD4eD4fD50D51D52D53D54D5bD5cD5dD5eD5fD60D61D62D63D64D6bD6cD6dD6eD6fD70D71D72D73D74D75D7bD7cD7dD7eD7fD80D81D82D83D84D85D86D8bD8cD8dD8eD8fD90D91D92D93D94D9bD9cD9dD9eD9fDa0Da1Da2Da3Da4DabDacDadDaeDafDb0Db1Db2Db3Db4Db5Db6Db7DbbDbcDbdDbeDbfDc0Dc1Dc2Dc3Dc4Dc5DcbDccDcdDceDcfDd0Dd1Dd2Dd3Dd4DdaDdbDdcDddDdeDdfDe0De1De2De3De4De5DebDecDedDeeDefDf0Df1Df2Df3Df4Df5Df6Df7Df8DfbDfcDfdDfeDff" { 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; }