///////////////////////////////////////////////
// SMA - SIMPLE MUSCLE ARCHITECTURE ANALYSIS //
///////////////////////////////////////////////
/*
* Reference:
* Seynnes OR, Cronin NJ. Simple Muscle Architecture Analysis (SMA): An ImageJ macro tool to automate measurements in B-mode ultrasound scans. PLoS One. 2020;15: e0229034. doi:10.1371/journal.pone.0229034
* Hosted at:
* https://github.com/oseynnes/SMA
*
* 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 MorphoLibJ v1.6.2 (https://imagej.net/plugins/morpholibj - Legland, D., Arganda-Carreras, I., & Andrey, P. (2016). MorphoLibJ: integrated library and plugins for mathematical morphology with ImageJ. Bioinformatics, 32(22), 3532–3534.
* 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 .
*/
////////////////
// Change log //
////////////////
/*
* Version 2.4
* - changed implementation of OJ plugin
* - add warning message if fascicles are orientated in the wrong direction.
* - changed aponeurosis fit
* - improved user scaling
*
*/
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Script parameters //
///////////////////////
/*
#@ String(value = " SMA - SIMPLE MUSCLE ARCHITECTURE ANALYSIS
doi:10.1371/journal.pone.0229034
https://github.com/oseynnes/SMA", visibility="MESSAGE") title
#@ String(value = "NB: The analysis only works with the 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 = "Fascicle model", choices= {"Straight", "Curved_spline", "Curved_circle"}, style="radioButtonHorizontal", description="Assume straight or curved fascicles. Circle method according to Muramatsu et al JAP 2002") geometry
#@ String (label = "Type of analysis", choices= {"Current file", "Open file", "Open folder"}, style="radioButtonHorizontal", description="Analyse single image or several images") analysis
#@ String(label = "Image cropping", choices= {"Automatic (requires identical scan depth)", "Manual"}, style="radioButtonHorizontal", persist=true) cropping
#@ String (choices={"None", "Automatic (requires metadata)", "Manual"}, style="radioButtonHorizontal", persist=true, description="Pixel scaling") scaling
#@ String(value = "
--------------- Aponeuroses --------------- ", visibility="MESSAGE") text4
#@ Integer (label="Tubeness sigma", value=8, style="slider", min=2, max=14, stepSize=2, persist=true, description="Standard deviation of the Gaussian filter. Proportional to aponeurosis thickness") Tsigma
#@ Boolean(label= "Enhance aponeuroses filter", value=false, persist=true, description="run 'Enhance Local Contrast' plugin (CLAHE). Not recommended unless aponeuroses lack contrast") clahe_ap
#@ Integer(label="Length (% of FoV width)", value=80, min=50, max=95, stepSize=5, persist=true, description="Expected length of detected aponeuroses relative to FoV width. default: 80%") apLength
#@ String(label = "Extrapolate from (% of aponeurosis length)", choices = {"100%", "50%"}, persist=true, description="") extrapolate_from
#@ Boolean(label= "Run an aponeurosis detection test (ONLY SINGLE IMAGES)", value=false, persist=true, description="Pause script to try other aponeurosis detection parameters") apo_test
#@ String(value = "
---------------- Fascicles ---------------- ", visibility="MESSAGE") text5
#@ Integer (label="ROI height (% of thickness or 1/2 thickness)", value = 50, style="slider", min=40, max=90, stepSize=5, persist=true) ROIheight
#@ Integer (label="ROI width (% of ROI width)", value = 60, style="slider", min=40, max=90, stepSize=10, persist=true) ROIwidth
#@ Boolean(label= "Enhance fascicle filter", value=false, persist=true, description="run 'Enhance Local Contrast' plugin (CLAHE). Not recommended unless fascicles lack contrast") clahe_fasc
#@ String(label = "Laplacian of Gaussian (sigma)", choices = {"0", "1", "2", "3", "4", "5", "6", "7"}, value=1, persist=true, description="Proportional to fascicle thickness: after running the test, choosing the value yielding the greatest angle is recommended") Osigma
#@ String(value = "
-------------------- Other -------------------- ", visibility="MESSAGE") text7
#@ Boolean(label="Print analysis parameters", value=true, persist=true, description="Add analysis parameters to the results table") param
#@ Boolean(label="Display outline of detected fascicle fragments", value=true, persist=true, description="Currently only works with single images") disp_fasc
#@ Boolean(label="Developer mode", value=false, persist=true, description="Disable batch mode and show intermediate steps of analysis") dev
*/
// GLOBAL VARIABLES ------------------------------------------------------------
var inputFile = "";
var output = "";
var input = "";
var extension = "";
var pano_crop = false;
var is_stack = false;
var manual_stack_crop = "Whole stack";
var fixed_ROI = newArray();
var image_titles = newArray();
var slice_n = 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();
var Error = newArray();
var Upper = "";
var Lower = "";
var x_upp = "";
var x_low = "";
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
macro "SMA - Simple Muscle Architecture" {
// description
plugins = newArray("OrientationJ Measure", "MorphoLibJ...", "Non-local Means Denoising", "Canny Edge Detector");
check_dependencies(plugins);
secondary_gui();
clear_results_and_roimanager();
if (analysis == "Current file" || analysis == "Open file") {
if (analysis == "Open file") {
input = File.getDirectory(inputFile);
file = File.getName(inputFile);
open_file(input+File.separator+file);
}
handle_stacks();
run("Select None");
run("Remove Overlay");
scaleFactor = scale(scaling, "");
if (dev == false) {
setBatchMode(true);
}
singleImageAnalysis();
} else { // process folder
count = 0;
countFiles(input);
n = 0;
n2 = 1; // count of processed scans
scaleFactor = scale(scaling, input);
if (cropping == "Manual") {
manual_roi = manual_cropping(input);
}
setBatchMode(true);
processFolder(input);
selectWindow("Processed");
run("Close");
}
function processFolder(input) {
list = getFileList(input);
list = Array.sort(list);
run("Text Window...", "name=Processed");
for (i = 0; i < list.length; i++) {
print("[Processed]", list[i] +" (scan #"+n2++ +" out of " +count+ ")" +"\n");
if (File.isDirectory(input + File.separator + list[i]))
processFolder(input + File.separator + list[i]);
if (endsWith(list[i], extension))
processFile(input, output, list[i]);
}
}
function processFile(input, output, file) {
open_file(input+File.separator+file);
singleImageAnalysis();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// start of singleImageAnalysis function //
///////////////////////////////////////////
function singleImageAnalysis() {
run("Collect Garbage");
start = getTime(); // Use this to time how long the whole code takes for one image
title = getTitle();
IDraw = getImageID();
if (is_stack== true) {
slice = getSliceNumber();
} else {
slice = NaN;
}
if (flip == true)
run("Flip Horizontally", "slice");
if (pano == true) {
// ONLY tested with Philips HD11 and Hologic Mach30
if (pano_crop == true) {
setTool("rotrect");
if (is("Batch Mode")) {
setBatchMode(false);
}
waitForUser("Select region to crop and press 'OK'");
run("Duplicate...", " ");
IDcropped = getImageID();
selectImage(IDraw); close();
IDraw = IDcropped;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Image Cropping - Filter and detect aponeuroses //
////////////////////////////////////////////////////
counter = 0;
while (counter == 0) {
selectImage(IDraw);
// run("Remove Overlay");
W = getWidth;
H = getHeight;
run("Set Scale...", "distance=0 known=0 pixel=1 unit=pixel");
// cropping
crop_boundaries = perform_cropping(cropping, H, W);
Le = crop_boundaries[0]; Up = crop_boundaries[1];
// aponeuroses detection
ap_filter = filter_aponeuroses(apLength, clahe_ap, Tsigma);
WFoV = ap_filter[0]; HFoV = ap_filter[1]; IDfilteredAp = ap_filter[2];
detect_aponeuroses(IDfilteredAp, HFoV, Up, Le, analysis, title);
ap_indices = select_indices(extrapolate_from, x_upp, x_low, Upper, Lower);
upper_idx1 = ap_indices[0]; upper_idx2 = ap_indices[1]; lower_idx1 = ap_indices[2]; lower_idx2 = ap_indices[3];
//Upper aponeurosis line (y_1 = b_1 * x + a_1)
params = least_squares_fit(Array.slice(x_upp, upper_idx1, upper_idx2), Array.slice(Upper, upper_idx1, upper_idx2));
a_1 = params[1]; b_1 = params[0];
betaUp = atan(b_1)* (180/PI); //angle upper aponeurosis
//Lower aponeurosis line (y_2 = b_2 * x + a_2)
params = least_squares_fit(Array.slice(x_low, lower_idx1, lower_idx2), Array.slice(Lower, lower_idx1, lower_idx2));
a_2 = params[1]; b_2 = params[0];
betaDeep = atan(b_2)* (180/PI); //angle deep aponeurosis
// Plot the curves on top of the existing image
selectImage(IDraw);
applyOverlay(Upper, x_upp);
applyOverlay(Lower, x_low);
counter = 1;
// User check of aponeurosis overlay
if (analysis == "Current file" || analysis == "Open file") { // Only implemented for single image analysis
if (apo_test == true && is_stack != true) { // prevent test on stacks
setBatchMode(false);
Dialog.createNonBlocking("Pause");
Dialog.addChoice("Aponeurosis detection", newArray("Accept and continue", "Try new parameters"), "Accept and continue");
Dialog.addSlider("Tubeness sigma", 2, 14, Tsigma);
Dialog.addCheckbox("Enhance aponeuroses filter", clahe_ap);
Dialog.addSlider("Length (% of FoV width)", 50, 80, apLength);
Dialog.addChoice("Extrapolate from (% of aponeurosis length)", newArray("50%", "100%"), extrapolate_from);
Dialog.show();
status = Dialog.getChoice();
if (status == "try new parameters") {
Tsigma = Dialog.getNumber();
clahe_ap = Dialog.getCheckbox();
apLength = Dialog.getNumber();
extrapolate_from = Dialog.getString();
counter = 0;
run("Remove Overlay");
setBatchMode(true);
} else {
setBatchMode(true); // continue analysis
}
}
}
}
// rename aponeuroses coordinates arrays
UAx = Array.copy(x_upp); //upper aponeurosis x values
UAy = Array.copy(Upper); //upper aponeurosis y values
LAx = Array.copy(x_low); //lower aponeurosis x values
LAy = Array.copy(Lower); //lower aponeurosis y values
// calculate mid-distance line between aponeuroses and get equation (2nd order polynom)
MAx = Array.copy(UAx);
MAy = newArray(UAy.length);
for (m=0; m 0) {
run("From ROI Manager");
}
roiManager("reset");
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Calculate fascicle length, pennation angle and thickness //
//////////////////////////////////////////////////////////////
if (geometry == "Curved_spline" || geometry == "Curved_circle") {
alphaDeep = -roi_store[3]; // with angle due to rotation
alphaUp = -roi_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;
// centre, radius, curvature and fascicle length
xs = newArray(Oux,IxFasc,Olx); ys = newArray(Ouy,IyFasc,Oly);
centre = getCircleCenter(xs, ys);
x_centre = centre[0]; y_centre = centre[1];
r = sqrt((x_centre-Oux)*(x_centre-Oux) + (y_centre-Ouy)*(y_centre-Ouy)); //radius
if (y_centre > 0) { //fascicle curved towards upper aponeurosis
Crv = 1/r; //curvature
} else {
Crv = -1/r;
}
if (geometry == "Curved_circle") {
theta1 = atan2(Ouy - y_centre, Oux - x_centre);
theta2 = atan2(Oly - y_centre, Olx - x_centre);
c_x = newArray();
c_y = newArray();
for (t = theta1; t <= theta2; t += 0.001) {
x = x_centre + r * cos(t);
y = y_centre + r * sin(t);
c_x = Array.concat(c_x, x);
c_y = Array.concat(c_y, y);
}
xs = c_x; ys = c_y; // redefine xs and ys as coordinates of arch
}
makeSelection("polyline", xs, ys);
run("Fit Spline");
run("Interpolate");
List.setMeasurements;
Lf = List.getValue("Length");
roiManager("add");
RoiManager.setPosition(slice);
} else { //if fascicles are analysed as straight lines
alphaDeep = -roi_store[2];
//Pennation angle
theta = alphaDeep + betaDeep;
slice_idx = lengthOf(slice_n);
if (theta < 0 && lengthOf(slice_n) == 0) {
Dialog.create("Image orientation");
Dialog.addMessage("The image(s) should be flipped horizontally. Continue anyway?");
Dialog.show();
}
//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;
//Offset coordinates on lower aponeurosis
Olx = (a_2 - a_o) / (b_o - b_2);
Oly = a_2 + b_2 * Olx;
//unscaled fascicle length
Lf = sqrt((Olx-Oux)*(Olx-Oux) + (Oly-Ouy)*(Oly-Ouy)); //fascicle length
Crv = NaN; //no curvature
r = NaN;
makeLine(Olx, Oly, Oux, Ouy);
roiManager("Add");
RoiManager.setPosition(slice);
}
//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 && left 0) {
Array.show("Images not processed", Error);
}
if (dev == false) {
setBatchMode(false);
}
} //end of macro
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// FUNCTIONS //
////////////////////
function check_dependencies(deps) {
// function description
missing = newArray();
List.setCommands;
for (i=0; i 0) {
exit("SMA requires the following plugins:"
+"\n "
+"\n- OrientationJ,"
+"\n- Canny Edge Detector"
+"\n- MorphoLiJ"
+"\n- Non-local Means Denoising,"
+"\n "
+"\nPlease check that the update sites 'BIG-EPFL', IJPB-plugins, 'Biomedgroup' and 'BioVoxxel' are selected."
+"\n\"https://imagej.net/Following_an_update_site.html\"");
}
}
function clear_results_and_roimanager() {
// function description
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");
}
}
function countFiles(input) {
list = getFileList(input);
for (i=0; i 1) {
run("Stack to RGB", "slices");
run("8-bit");
}
}
function scale(scaling_mode, folder_path) {
// function description
if (folder_path.length > 0) {
list = getFileList(folder_path);
open_file(folder_path+File.separator+list[0]);
}
if (scaling_mode == "Automatic (requires metadata)") {
scaleFactor = metadata_scale();
} else if (scaling_mode == "Manual") {
scaleFactor = userscale();
}
if (folder_path.length > 0) {
close ();
}
return scaleFactor;
}
function manual_cropping(folder_path) {
// function description
if (folder_path.length > 0) { // process folder
list = getFileList(folder_path);
open_file(folder_path+File.separator+list[0]);
if (flip == true) {
run("Flip Horizontally");
}
} else if (is_stack == true) {
if (fixed_ROI.length == 0) {
run("Duplicate...", " ");
} else { // manual_stack_crop == "Whole stack"
return fixed_ROI;
}
}
setTool("rectangle");
waitForUser("Select area. Click OK when done");
Roi.getBounds(x, y, width, height);
roi_coords = newArray(x, y, width, height);
if (manual_stack_crop == "Whole stack") {
fixed_ROI = Array.concat(fixed_ROI,roi_coords);
}
// setBatchMode(true);
if (folder_path.length > 0 || is_stack == true) {
close ();
}
return roi_coords;
}
function perform_cropping(cropping, H, W) {
/**
* This function performs cropping on an image based on the input parameters.
*
* cropping - The type of cropping to be performed; "Manual" or "Automatic".
* W - The width of the cropping window; used as an offset in the calculation of the left edge of the cropping rectangle.
* H - The height of the cropping window; used as an offset in the calculation of the top edge of the cropping rectangle.
*
* If the 'cropping' parameter is "Manual", the function performs manual cropping.
* If the 'cropping' parameter is "Automatic", the function performs automatic cropping.
*
* returns an array containing two elements: the left (Le) and top (Up) coordinates of the cropping rectangle.
*/
if (cropping == "Manual") {
if (dev == false) {
setBatchMode(false);
}
manual_roi = manual_cropping("");
if (dev == false) {
setBatchMode(true);
}
x = manual_roi[0]; y = manual_roi[1]; width = manual_roi[2]; height = manual_roi[3];
Le = x+W*0.005;
Up = y+H*0.01; // shifts cropped window down by offset
makeRectangle(Le, Up, width*0.99, height*0.98);
} else { // Automatic detection of FoV
run("Duplicate...", " ");
IDcopy1 = getImageID();
run("8-bit");
run("Set Scale...", "distance=0 known=0 pixel=1 unit=pixel");
run("Convolve...", "text1=[-1 -1 -1 -1 -1\n-1 -1 -1 -1 -1\n-1 -1 24 -1 -1\n-1 -1 -1 -1 -1\n-1 -1 -1 -1 -1\n] normalize");
run("Median...", "radius=2");
run("Auto Local Threshold", "method=Median radius=15 parameter_1=0 parameter_2=0 white");
run("Options...", "iterations=3 count=1 black do=Close");
run("Analyze Particles...", "size=10000-Infinity add");
roiManager("Select", 0);
getSelectionBounds(x, y, width, height);
roiManager("delete");
selectImage(IDcopy1); close();
run("Select None");
selectImage(IDraw);
Le = x+width*0.005;
Up = y+height*0.01; // shifts cropped window down by offset proportional to height
makeRectangle(Le, Up, width*0.98, height*0.97);
}
return newArray(Le, Up);
}
function enhance_filter(clahe_ap, Tsigma) {
// Apply various filter to enhance aponeuroses and return image ID of filtered image
run("8-bit");
run("Set Scale...", "distance=0 known=0 pixel=1 unit=pixel");
run("Subtract Background...", "rolling=50 sliding");
run("Non-local Means Denoising", "sigma=15 smoothing_factor=1 auto");
run("Bandpass Filter...", "filter_large=40 filter_small=3 suppress=None tolerance=5 saturate");
if (clahe_ap == true) {
run("Enhance Local Contrast (CLAHE)", "blocksize=36 histogram=256 maximum=4 mask=*None* fast_(less_accurate)"); // necessary for faint aponeuroses
}
run("Tubeness", "sigma=Tsigma use"); // start with 8
IDvessel1 = getImageID();
selectImage(IDvessel1);
run("8-bit");
return IDvessel1
}
function filter_aponeuroses(apLength, clahe_ap, Tsigma) {
run("Duplicate...", " ");
IDFoV = getImageID();
WFoV = getWidth; //Dimensions of the field of view
HFoV = getHeight;
setColor(0);
drawLine(0, 0, WFoV, 0);
minLength = apLength/100*WFoV;
// Preprocessing - enhance structures
IDvessel1 = enhance_filter(clahe_ap, Tsigma);
selectImage(IDFoV); close();
// Directional filter
fragment_length = WFoV / 10;
// fragment_length = minLength;
run("Directional Filtering", "type=Max operation=Opening line=fragment_length direction=2");
selectImage(IDvessel1); close();
IDvessel1 = getImageID();
if (isOpen("Log")){
selectWindow("Log"); run("Close");
}
// Preprocessing - Threshold from FFT
run("FFT");
IDFFT1 = getImageID();
selectImage(IDvessel1); close();
selectImage(IDFFT1);
Wfft = getWidth;
Hfft = getHeight;
setThreshold(find_lower_thresh(99.5), 255); // changed to suit MorpholibJ filter
setOption("BlackBackground", true);
run("Convert to Mask");
setColor(0);
makeRectangle(Wfft/2+11, 0, Wfft/2, Hfft/2);
fill();
makeRectangle(0, Hfft/2+1, Wfft/2-10, Hfft/2);
fill();
run("Inverse FFT");
IDinvFFT1 = getImageID();
selectImage(IDFFT1); close();
selectImage(IDinvFFT1);
run("Canny Edge Detector", "gaussian=2 low=2.5 high=7.5"); // changed to suit MorpholibJ filter
run("Analyze Particles...", "size=0-minLength show=Masks");
IDmask = getImageID(); // Mask of shorter lines
imageCalculator("Subtract create", IDinvFFT1, IDmask);
IDfilteredAp = getImageID();
selectImage(IDinvFFT1); close();
selectImage(IDmask); close();
return newArray(WFoV, HFoV, IDfilteredAp);
}
function detect_aponeuroses(IDfilteredAp, HFoV, Up, Le, analysis, title) {
// Start detecting position of aponeuroses
selectImage(IDfilteredAp);
U = round(0.3*HFoV);
V = U + 1;
Upper = lineFinder(0, U, 1); // Call to lineFinder function. 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;
selectImage(IDfilteredAp); close();
// warn if aponeurosis was not detected
err = "";
if (Upper.length < 10){
ok = 0;
if (analysis == "Current file" || analysis == "Open file") {
exit("Could not detect upper aponeurosis. Please try again with different value of tubeness sigma or try manual cropping");
} else {
err = "Could not detect upper aponeurosis. Please try again with different value of tubeness sigma or try manual cropping";
Error = Array.concat(Error, title +":"+err);
continue;
}
} else if (Lower.length < 10) {
ok = 0;
if (analysis == "Current file" || analysis == "Open file") {
exit("Could not detect lower aponeurosis. Please try again with different value of tubeness sigma or try manual cropping");
} else {
err = "Could not detect lower aponeurosis. Please try again with different value of tubeness sigma or try manual cropping";
Error = Array.concat(Error, title +":"+err);
continue;
}
}
if (ok == 1) {
L = round(0.05*WFoV);
R = round(0.95*WFoV);
W2 = R - L;
// Adjust aponeuroses to same length
if (Lower.length < Upper.length) {
maxL = Upper.length;
Lower2 = newArray(Upper.length);
for (t=0; t0)
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= nBelowThreshold) {
lowThresh = pxlValues[f]; // lower threshold corresponding to % of highest GV pixels in FFT
f = 99999999;//break
}
}
return lowThresh;
}
function lineFinder(in1, in2, in3) {
//in1: vert start point of line, in2: vert end point of line, in3: 1 for upper line, 0 for lower
// Calculate constants based on the width of field of view (WFoV)
L = round(0.05*WFoV); // Left boundary
R = round(0.95*WFoV); // Right boundary
M = 0.5*WFoV; // Middle point
// Search for lines from the middle of the image
makeLine(M, in1, M, in2);
profile = getProfile();
// Store the indices of potential lines in the 'hold' array
hold = newArray(1);
for (j=0; j 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);
// Initialize variables for line tracking
line = newArray(1); ind1 = 0; ind2 = 1;
// Determine the starting index based on the value of in3 (1 for upper line, 0 for lower)
if (in3 == 1)
a = hold.length - 1;
else
a = 0;
found = 0; // Flag to indicate if a suitable line is found
// Iterate until a suitable line is found or the search is exhausted
while (found != 1 && a >= 0 && a < hold.length) {
line[0] = hold[a]; // Start with right half of line
// Check just above/below each 'line' for a parallel line
if(in3 == 1)
makeLine(M+1, line[0]-21, M+1, line[0]-1);
else
makeLine(M+1, line[0]+1, M+1, line[0]+20);
pr = getProfile();
sum = 0;
// Calculate the sum of pixel intensities in the profile
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;
}
function getCircleCenter(xs, ys) {
// Calculate midpoints of the two lines
midPoint1x = (xs[0] + xs[1]) / 2;
midPoint1y = (ys[0] + ys[1]) / 2;
midPoint2x = (xs[1] + xs[2]) / 2;
midPoint2y = (ys[1] + ys[2]) / 2;
// Calculate the slopes of the two lines
slope1 = (ys[1] - ys[0]) / (xs[1] - xs[0]);
slope2 = (ys[2] - ys[1]) / (xs[2] - xs[1]);
// Calculate the slopes of the perpendicular bisectors
perpBisectorSlope1 = -1 / slope1;
perpBisectorSlope2 = -1 / slope2;
// Calculate the y-intercepts of the perpendicular bisectors
yIntercept1 = midPoint1y - perpBisectorSlope1 * midPoint1x;
yIntercept2 = midPoint2y - perpBisectorSlope2 * midPoint2x;
// The center of the circle is the intersection of the two perpendicular bisectors
x_centre = (yIntercept2 - yIntercept1) / (perpBisectorSlope1 - perpBisectorSlope2);
y_centre = perpBisectorSlope1 * x_centre + yIntercept1;
return newArray(x_centre, y_centre);
}
function middle_coordinates(xPoints, yPoints) {
//
nPoints = xPoints.length - 1;
lineLength = 0;
for (i=1; i= halfLineLength) {
midX = xPoints[i-1] + (xPoints[i] - xPoints[i-1]) * (halfLineLength - (runningLength - segmentLength)) / segmentLength;
midY = yPoints[i-1] + (yPoints[i] - yPoints[i-1]) * (halfLineLength - (runningLength - segmentLength)) / segmentLength;
midSegment = i - 1;
break;
}
}
return newArray(midX, midY, midSegment);
}
function metadata_scale() {
// function description
metadata = getMetadata("Info");
if (metadata.length > 200) { // DICOM metadata are typically larger
if (getInfo("Physical Delta X #1") > 0) {
scaling_tag = "Physical Delta X #1";
} else if (getInfo("Physical Delta X") > 0) {
scaling_tag = "Physical Delta X";
}
dcmScale = parseFloat(getInfo(scaling_tag)); // original scale in cm/pxl
scaling_factor = 1/dcmScale/10; // scaling factor in pxl/mm
} else {
Dialog.create("No metadata");
Dialog.addRadioButtonGroup("", newArray("No scaling", "Set scale manually", "Exit"), 1, 3, "No scaling")
Dialog.show();
action = Dialog.getRadioButton();
if (action == "No scaling") {
scaling_factor = 1;
} else if (action == "Set scale manually") {
scaling_factor = userscale(); // scaling factor in pxl/mm
} else {
exit("Analysis process ended");
}
}
return scaling_factor; // scaling factor in pxl/mm
}
function userscale() {
// function description
run("Select None");
load_settings_file();
Dialog.createNonBlocking("Manual scaling");
Dialog.setLocation(0, 0);
Dialog.addSlider("Scan depth (cm)", 2, 10, List.getValue("scale_distance"));
Dialog.addChoice("Machine:", newArray("Supersonic Mach30", "Philips HD11", "Telemed LogicScan", "Other"), "Philips HD11");
Dialog.show();
scale_distance = Dialog.getNumber(); List.set("scale_distance", scale_distance);
machine = Dialog.getChoice();
update_settings_file();
List.clear();
if (machine == "Other") {
setTool("line");
Dialog.createNonBlocking("Manual scaling");
Dialog.setLocation(0, 0);
Dialog.addMessage(" Draw a line over the scaling distance
(usually, over the scaling graduation over or on the side of the field of view) ");
Dialog.show();
getLine(x1, y1, x2, y2, lineWidth);
if (x1 == -1) userscale();
lineLength = sqrt(pow(x1-x2, 2) + pow(y1-y2, 2));
scale_factor = lineLength/(parseInt(scale_distance)*10); // scaling factor in pxl/mm
} else {
scale_factor = scale_from_depth(scale_distance);
}
return scale_factor
}
function scale_from_depth(depth) {
// Make calibration line and retrieve
setBatchMode(true);
if (machine == "Philips HD11") {
makeLine(796, 65, 796, 585);
} else if (machine == "Supersonic Mach30") {
makeLine(552, 90, 552, 505);
} else if (machine == "Telemed LogicScan") {
makeLine(75, 37, 75, 681);
}
arr = getProfile(); // get associated profile
run("Select None");
result = Array.findMaxima(arr, 30); // Find the maxima - will correspond to the tick marks.
result = Array.sort(result); // and sort it from min to max
if (result.length != depth + 1) {
exit("No scale detected. Load original image or select 'Other' as ultrasound machine.");
}
L = result[depth] - result[0];
setBatchMode(false);
return L/parseInt(depth*10); // scaling factor in pxl/mm
}
function handle_stacks() {
// function description
list = getList("image.titles");
if (list.length==0) {
List.clear();
exit("No image windows are open");
}
if (nSlices == 1) // not a stack
return;
is_stack = true;
parent = File.directory;
sep = File.separator;
if (endsWith(parent, sep+sep) == true) {
parent = substring(parent, 0, parent.length-1);
}
Dialog.createNonBlocking("Image stack");
Dialog.addCheckbox("Flip image horizontally", flip);
if (cropping == "Manual") {
choices1 = newArray("Each frame", "Whole stack");
Dialog.addRadioButtonGroup("Set manual cropping for:", choices1, 2, 1, "Whole stack");
}
choices2 = newArray("Current frame only", "Multiple frames");
Dialog.addRadioButtonGroup("Process:", choices2, 2, 1, "Current frame only");
Dialog.addSlider("start", 1, nSlices, 1);
Dialog.addSlider("end", 1, nSlices, nSlices);
Dialog.addMessage("Alternatively, enter comma-separated frame numbers");
Dialog.addString("Discrete frames", "", 4);
Dialog.addCheckbox("Save processed video (AVI)", false);
Dialog.addCheckbox("Save results (CSV)", false);
Dialog.show();
flip = Dialog.getCheckbox();
if (cropping == "Manual") {
manual_stack_crop = Dialog.getRadioButton();
}
target = Dialog.getRadioButton();
start = Dialog.getNumber();
end = Dialog.getNumber();
slices_string = Dialog.getString();
save_to_avi = Dialog.getCheckbox();
save_to_csv = Dialog.getCheckbox();
if (target == "Current frame only") {
start = getSliceNumber(); end = start;
}
if (slices_string != "") {
slices = split(slices_string, ",");
for (i = 0; i < slices.length; i++) {
slices[i] = parseInt(slices[i]);
}
start = 0; end = slices.length - 1;
}
scaleFactor = scale(scaling, "");
if (dev == false) {
setBatchMode(true);
}
run("Remove Overlay");
for (slice_number=start; slice_number 2) {
split_line = split(logstring_lines[i], "=");
List.set(split_line[0], split_line[1]);
}
}
}
function update_settings_file() {
// update settings
f = File.open(getDir("plugins")+"SMA_prefs.txt");
print(f, List.getList);
File.close(f);
}
function secondary_gui() {
// set additional dialog if required
if (pano == true || analysis == "Open file" || analysis == "Open folder") {
load_settings_file();
Dialog.createNonBlocking("Additional settings");
if (pano == true) {
Dialog.addMessage("Subselection of panoramic scan");
Dialog.setInsets(0, 0, 20);
Dialog.addCheckbox("Pre-crop image", List.getValue("pano_crop"));
}
if (analysis == "Open file") {
Dialog.addFile("Select a file", List.getValue("inputFile"));
} else if (analysis == "Open folder") {
Dialog.addDirectory("Input directory", List.getValue("input"));
Dialog.addDirectory("Output directory", List.getValue("output"));
Dialog.addChoice("File extension", newArray(".bmp", ".BMP", ".jpg", ".tif", ".png", ".dcm", ""), List.get("extension"));
}
Dialog.show();
if (pano == true) {
pano_crop = Dialog.getCheckbox(); List.set("pano_crop", pano_crop);
}
if (analysis == "Open file") {
inputFile = Dialog.getString(); List.set("inputFile", inputFile);
} else if (analysis == "Open folder") {
input = Dialog.getString(); List.set("input", input);
output = Dialog.getString(); List.set("output", output);
extension = Dialog.getChoice(); List.set("extension", extension);
}
update_settings_file();
List.clear();
}
}
function stack_to_avi(start, end, path) {
// function description
run("Flatten", "stack");
run("Make Substack...", "slices="+start+"-"+end);
subID = getImageID();
avi_path = path + "processed.avi";
run("AVI... ", "compression=JPEG frame=25 save="+path);
selectImage(subID); close();
}