/*
InteredgeDistance Macro for ImageJ/Fiji
By Santosh Patnaik, MD, PhD (drpatnaikREMOVECAPS@yahoo.com)
Under GPL2+ & LGPL3 licenses
https://sourceforge.net/projects/InteredgeDistance/
This tool measures the distance between two user-drawn lines on an image and can be used to quantify properties such as separation, thickness, and width. Run the macro to view its Help page.
Thanks to Miyuka Muramatsu, Masaki Higashino, and Image.Sc forum members for feedback.
*/
softwareReleaseDate = '30 Mar 2025';
softwareVersion = '3.0';
requires('1.53d');
//////////////////////////////////////
////////// 1. Settings ///////////
//////////////////////////////////////
//// Default setting values, and some "interpreted" settings. Macro's interface lets user set most settings.
// Colors to use on image for text, its background (Bg), and lines drawn by user (Ref/Test) or showing distances between lines (Dist). Use ImageJ-compatible values.
settingTextColor = 'White';
settingTextBgColor = 'Black';
settingDistColor = 'Yellow';
settingShortestDistColor = 'Red';
settingLongestDistColor = 'Green';
settingRefLineColor = 'Pink';
settingTestLineColor = 'Cyan';
// Filename prefix to save image: Alphanumerical string to use when saving a TIF-format copy of image with overlaid measurements in the same location as the measured image and with filename same as the image's but prefixed with settingFileSavePrefix and suffixed with Time. Leave empty to not save file (''). Default value = empty (file not saved).
settingFileSavePrefix = '';
// Minimum locations to measure at: Minimum individual distance measurements (i.e., distance measurements are made at minimum this many points). Should be 1 or larger. A value 3 is suggested. Default value = 3.
settingUsablePointsMin = 3;
// Maximum locations to measure at: Maximum individual distance measurements; i.e., distance measurements are made at maximum this many points. Should be 1 or larger (ignored if 0). Default value = 20.
settingUsablePointsMax = 20;
// Use splines: To spline-fit user-drawn lines before distance measurement. Default value = 1 (lines fit to splines). Set to 0 to disable spline fitting. Spline-fitting removes jarriness to smoothen user-drawn lines.
settingUseSplines = 1;
// Reference line: The macro uses one of the two user-drawn line as the reference line for measuring distance, to points along the line from the other user-drawn line (test line). Set to 'First' or 'Second' to respectively specify reference line to be the line that is drawn first or second. Default value = 'Longer' (i.e., longer of the two lines).
settingRefLineId = 'Longer';
// Test line 'offset': Ignore ends of the test line, if necessary, so that the portion of the line that is used for measurements does not 'extend' beyond the reference line. Default value = 1 (enabled). Set to 0 to disable.
settingIgnoreOffset = 1;
// Measurement directionality: Distance measurements can be constrained to be orthogonal/perpendicular to the user-drawn line, or to be parallel to each other and a user-drawn reference line, or to be radial to a user-denoted reference point (i.e., distances are corradial), if set as 'Orthogonal', 'Parallel', or 'Radial', respectively. Default value = 'None' (i.e., no such constraint).
settingDirectionality = 'None';
// The macro considers local tangentiality/directionality of a line at a particular point of the line, for example, to assess orthogonality of distance from the point to the other line. The value for this setting is used to identify the point that defines the tangent to be this many points (roughly, pixels) away in distance from the point at which tangentiality is considered. Default value = 1.
settingTangentingPointDist = 1;
// Measure equidistantly: To measure distance along equidistant points of one line to the other line, specify the uniform interval/distance between the equidistant points in pixels. Distances will be measured at locations this many points (approximate pixels along the line) apart. Should be 2 or larger. Set to 1 to let the macro decide the uniform interval based on settingUsablePointsMax and lengths of the user-drawn lines. Default value = 0 (i.e., disabled; distance measurement won't be at uniform interval along a line).
settingMeasureEquidistance = 0;
// ImageJ's interpolation interval (pixel unit). An interval of 1 roughly implies that every pixel is a point; with 2, a point is two pixels. Consider a value of 2 or 3 for large images to speed up measurement. Default value = 1. Permitted value: integer in range 1-10.
settingInterpolateInterval = 1;
// Flexibility during angle assessments, such as for directionality constraint (degree unit). Default = 1 (i.e., a value of 89.1 or 90.5 will indicate orthogonality). Permitted value: in range 0-30.
settingToleranceAngleDeviation = 1;
// Save individual distances: To save in the output file each of the multiple distances measured at multiple points, set this variable to 1. Default value = 0 (not saved).
settingSaveIndividualDistances = 0;
// Show stats on image: Enable to annotate image with a statistical summary of the distance measurement. The horizontal text will be placed in the top left region. Set to 0 to disable. Default value = 1.
settingShowStatsInImage = 1;
// Text size to use on image: Size in points of text displaying measurement values on image. Set to 0 to autoset based on area of measured region. Default value = 0.
settingTextSize = 0;
// Width of drawn lines: Size in pixels of lines drawn on image. Set to 0 to autoset based on area of measured region. Default value = 0.
settingLineWidth = 0;
//// Sanitize settings values. More sanitizations in code below.
if(settingRefLineId != 'Longer' && settingRefLineId != 'First' && settingRefLineId != 'Second'){
settingRefLineId == 'Longer';
}
if(settingDirectionality != 'None' && settingDirectionality != 'Orthogonal' && settingDirectionality != 'Parallel' && settingDirectionality != 'Radial'){
settingDirectionality = 'None';
}
if(isNaN(settingTangentingPointDist) || settingTangentingPointDist < 1){
settingTangentingPointDist = 1;
}
if(isNaN(settingUsablePointsMin) || settingUsablePointsMin < 1){
settingUsablePointsMin = 3;
}
///////////////////////////////////////
////////// 2. Help text ///////////
///////////////////////////////////////
//// Displayed in the Help window
helpHtml = ''
+ '
Help with Settings and Usage
'
+ 'InterEdge macro, version ' + softwareVersion + ', ' + softwareReleaseDate + ''
+ 'SETTINGS
'
+ 'The default settings should suffice for most cases. But intersection, length, offset, orientation, resolution, tortuosity, etc. of the drawn lines can give rise to edge cases that will require trying different settings (which may still give an unsatisfactory result). Default setting is used in case a value is improperly specified or fails sanity check.default
'
+ '
Number of locations to measure at (default "' + settingUsablePointsMax + '")
Distance measurements will be made at this many locations/points. Note that measurements may not be obtainable at all locations depending on other settings (such as Directionality). In such scenario, measurements will be made for the closest number unless it is < ' + settingUsablePointsMin + ', in which case measurements won\'t be made. Macro run will be slower with larger values. Set to 0 for no limit (macro run may be very slow).'
+ '
Angle tolerance (deg) (default "' + settingToleranceAngleDeviation + '")
Flexibility during angle assessments, such as for directionality constraint (degree unit). E.g., when set at 1, an angle of 89.1 or 90.5 will be deemed orthogonal (90 deg.). Permitted value: in range 0-30.'
+ '
Interpolation interval (px) (default "' + settingInterpolateInterval + '")
ImageJ\'s interpolation interval (pixel unit). An interval of 1 roughly implies that every pixel is a point; with 2, a point is two pixels. Consider a value of 2 or 3 for large images to speed up measurement. Permitted value: integer in range 1-10.'
+ '
Equidistance of locations (default "' + settingMeasureEquidistance + '")
To measure distance along equidistant points of one line (test line) to the other line (reference line), specify the uniform interval between the equidistant points in pixels as an integer > 1. Distances to the reference line will be measured at locations this many points (approximate pixels) apart along the test line. Set to 0 to disable. (i.e., distance measurement won\'t be at uniform interval along a line). To disable any constraint from Number of locations to measure at, set that setting\'s value to 0. Set to 1 to let the macro choose the interval value based on the number of locations and the length of the user-drawn lines. Measurement may not be possible at all the equidistant locations if there is a constraint with the Directionality setting.'
+ '
Filename prefix to save image (default "' + settingFileSavePrefix + '")
Alphanumerical string to use when saving a TIF-format copy of image with overlaid measurements. Rest of the filename, and file location will be same as for original image. If empty, an image copy won\'t be saved.'
+ '
User-drawn line to use as reference line (default "' + settingRefLineId + '")
Reference line is one of the two user-drawn lines, specified as first drawn, or second drawn, or the longer one, to which distances are measured from points along the other line (test line).'
+ '
Color for distance lines (default "' + settingDistColor + '")
Color to use for drawing the distance lines. Longest and shortest distances will always be shown in green and red respectively. These and other color settings not shown here can be changed by editing setting variables noted at the beginning of the macro code.'
+ '
Directionality (default "' + settingDirectionality + '")
Distances measured at various locations can be constrained to be approximately locally orthogonal (perpendicular), or parallel to each other and a user-drawn reference line, or radial to a user-denoted reference point (corradial). For orthogonality requirement, Local tangent-defining point must be > 0. Value of the Angle tolerance setting is utilized during directionality assessment. Because of limits imposed by image resolution, a directionality constraint may not be suitable in some cases.'
+ '
Local tangent-defining point (default "' + settingTangentingPointDist + '")
The macro considers local tangentiality/directionality of a line at a particular point of the line, for example, to assess orthogonality of distance from the point to the other line. The value for this setting is used to identify the point that defines the tangent to be this many points (roughly, pixels) away in distance from the point at which tangentiality is considered. Permitted value: integer > 0.'
+ '
Use splines (default "' + myBoolText(settingUseSplines) + '")
Enable to spline-fit the two user-drawn lines before distance measurement. Spline-fitting removes jarriness to smoothen user-drawn lines.'
+ '
Ignore offset (default "' + myBoolText(settingIgnoreOffset) + '")
Ignore ends of the test line, if necessary, so that the portion of the line that is used for measurements does not extend beyond the reference line. The test line is iteratively trimmed until the local tangentiality of the line at the terminal point is orthogonal to the line connecting the point and the reference line\'s terminal point towards that end.'
+ '
Show stats on image (default "' + myBoolText(settingShowStatsInImage) + '")
Enable to annotate image with a statistical summary of the distance measurement. The horizontal text will be placed in the top left region.'
+ '
Save individual distances (default "' + myBoolText(settingSaveIndividualDistances) + '")
Enable to also save length and directionality (angle) of each of the distances measured at multiple locations. By default, only statistical summary of the length values is saved (such as the mean, the value of the distance between the two lines). Saving of individual length and angle values permits analyses of other characteristics of the separation between the two lines.'
+ '
Result file (txt/csv)
Select an existing text/CSV file to save results in.'
+ '
Editing the macro file is required to change settings that are not shown here, such as text color.
'
+ 'USAGE
'
+ 'This macro tool calculates the distance between two user-drawn lines on an image and can be used to quantify properties such as separation, thickness, and width of objects in images. While theoretically a line has an infinite number of points, in an electronic image, there are only a limited number of points (pixels) in a line (i.e., lines are quantized in digital images). The macro determines the shortest distances from a certain number of points (locations) of one line to the other line, and the mean of these distances might be considered as the distance between the two lines..
'
+ 'To measure distances in one or many images, the macro is started without any open image. The macro will prompt to fix settings for the measurement session and to select a pre-existing file to write measurements to as text data. You will be then prompted to open an image to examine, and then to draw the two lines using ImageJ tools for straight, poly (segmented), or freehand lines. The macro then makes the measurements and prompts the user to save or ignore them. Measured distances and, optionally, their stats, are also shown on the image, with the largest and smallest distances in different colors.
'
+ 'It is important to follow the instructions that the macro displays; e.g., drawing a line first and then pressing an OK button. You can re-draw a line before proceeding to the next step by simply using the line tool and drawing again. The macro will automatically close images, and manually closing an open image window may cause an issue. But you may find it helpful to move an open image window to the left of your screen, or make it smaller, as the macro will show dialogs/windows in the center of screen.
'
+ 'While default settings should suffice, orientation, resolution, tortuosity, etc. of the drawn lines can give rise to edge cases that will require trying different settings. Distance measurement may be inaccurate if the lines intersect or if line directionality has large swings near the line-ends.
'
+ 'Speed with which the macro calculates distance will be slower for longer user-drawn lines and images with higher resolution. The macro may also run slow if the Number of locations to measure at setting is set to 0 or a high value, like > 300, or if there is a directionality requirement. A smaller Number of locations to measure at value will speed up measurement. Other options are to work with a lower-resolution image or use a value of 2 for interpolation interval.
'
+ 'One of the two user-drawn lines is used as the reference line by the macro (longer one by default but could be specified as either the first or second drawn line). You may need to think of justifications for designating a user-drawn line as the reference line. Points/locations along the non-reference line (test line) are selected, and the shortest distance of each of the selected points to the reference line is determined, while considering other settings, such as for directionality and equidistance.
'
+ 'These distance measurements can have a directionality constraint, such that they locally orthogonal (perpendicular), or are parallel to each other and to a user-drawn reference line, or are radial to a user-denoted reference point (corradial). For orthogonality assessment, the value of Local tangent-defining point is used to identify another point, this many points away (roughly, pixels) on the line from which distance is being measured so that orthogonality can be assessed (the two points form the tangential vector that is the base for orthogonality). The value should not be set too high and should be appropriate keeping in mind the image resolution, and lengths and shapes of the drawn lines.
'
+ 'Note that the accuracy of measurement is affected by image resolution and the distance between the two user-drawn lines. Fewer pixels means fewer points to work with. To account for this, angle comparison in directionality assessment can have flexibility as per the Angle tolerance setting. E.g., 89.1 deg. can be considered as 90 deg. (orthogonal) if angle tolerance is set at 1. But, even with such flexibility, it may not be possible to satisfy directionality constraint, for instance, in a region when the two lines get very close to each other.
'
+ 'The macro measures distance in pixels. The measurement can be converted by the user to other units such as µm based on the image resolution. Output (pixel unit) in comma-separated value (CSV) format, with all numbers rounded to one decimal, is written to the user-specified result file with columns/fields: File (image filename), Folder (parent folder of image file), Time (YYYY-MM-DD-HH.MM.SS format), Sequence (sequence number of measurement in the same macro run, useful when image measured multiple times), LengthLine1 and LengthLine2 (lengths of the user-drawn lines, before any spline-fitting as per Use splines), DistancesMeasured (number of points/locations at which distances were measured), and mean, SD, min., and max. values of the distances. The next two CSV fields contain X and Y pixel coordinates of points corresponding to the minimum and maximum distances, respectively, in format: X1/Y1:X2/Y2 (semi-colon-separated, in case of multiple equilength distances). These fields are followed by values of some of the macro settings (as used), such as the reference parallel line angularity degree (absolute value) or X/Y point coordinates in case directionality has a parallel or radial requirement. If Save individual distances is enabled, lengths and corresponding directionality angles (deg. unit) of each of the distances are also recorded in the CSV output in its last field as semi-colon-separated values. If settingFileSavePrefix is non-empty, a copy of the analyzed image with overlaid measurements is saved in the same folder as the image with filename same as the image but prefixed with settingFileSavePrefix and suffixed with Time.';
/////////////////////////////////////////
////////// 3. Macro start ///////////
/////////////////////////////////////////
//// Prompt user for settings and to select pre-existing csv/txt file to write output to (output is image file details, current time, and measurement)
close('*');
resultFile = 0;
while(resultFile == 0){
Dialog.create('InteredgeDistance macro' + ', v' + softwareVersion);
Dialog.addNumber('Number of locations to measure at', settingUsablePointsMax);
Dialog.addNumber('Angle tolerance (deg.)', settingToleranceAngleDeviation);
Dialog.addNumber('Interpolation interval (px)', settingInterpolateInterval);
Dialog.addNumber('Equidistance of locations', settingMeasureEquidistance);
Dialog.setInsets(0, 0, 0);
Dialog.addString('Filename prefix to save image', settingFileSavePrefix, 6);
Dialog.setInsets(0, 0, 0);
Dialog.addChoice('User-drawn line to use as reference line', newArray('Longer', 'First', 'Second'), settingRefLineId);
Dialog.setInsets(0, 0, 0);
Dialog.addChoice('Color of distance lines', newArray('Yellow', 'Black', 'Blue', 'Orange', 'White'), settingDistColor);
Dialog.setInsets(0, 0, 0);
Dialog.addChoice('Directionality', newArray('None', 'Orthogonal', 'Parallel', 'Radial'), settingDirectionality);
Dialog.setInsets(0, 0, 0);
Dialog.addNumber('Local tangent-defining point', settingTangentingPointDist);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Use splines', settingUseSplines);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Ignore offset', settingIgnoreOffset);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Show stats on image', settingShowStatsInImage);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Save individual distances', settingSaveIndividualDistances);
Dialog.setInsets(0, 0, 0);
Dialog.addFile('* Result file (txt/csv)', '');
Dialog.addMessage("\nAfter selecting Result file, hit OK to select image file");
Dialog.addHelp(helpHtml);
Dialog.show();
// Finalize values
settingUsablePointsMax = floor(Dialog.getNumber());
settingToleranceAngleDeviation = abs(Dialog.getNumber());
settingInterpolateInterval = floor(Dialog.getNumber());
settingMeasureEquidistance = floor(Dialog.getNumber());
settingFileSavePrefix = String.trim(Dialog.getString());
settingRefLineId = Dialog.getChoice();
settingDistColor = Dialog.getChoice();
settingDirectionality = Dialog.getChoice();
settingTangentingPointDist = floor(Dialog.getNumber());
settingUseSplines = Dialog.getCheckbox();
settingIgnoreOffset = Dialog.getCheckbox();
settingShowStatsInImage = Dialog.getCheckbox();
settingSaveIndividualDistances = Dialog.getCheckbox();
if(isNaN(settingUsablePointsMax) || settingUsablePointsMax < 0){
settingUsablePointsMax = 20;
}
if(isNaN(settingToleranceAngleDeviation) || settingToleranceAngleDeviation < 0 || settingToleranceAngleDeviation > 30){
settingToleranceAngleDeviation = 1;
}
if(isNaN(settingInterpolateInterval) || settingInterpolateInterval < 1 || settingInterpolateInterval > 10){
settingInterpolateInterval = 1;
}
if(isNaN(settingMeasureEquidistance) || settingMeasureEquidistance < 1){
settingMeasureEquidistance = 0;
}
if(isNaN(settingTangentingPointDist) || settingTangentingPointDist < 1){
settingTangentingPointDist = 1;
}
if(settingUseSplines){
settingUseSplines = 1;
}
if(settingIgnoreOffset){
settingIgnoreOffset = 1;
}
if(settingShowStatsInImage){
settingShowStatsInImage = 1;
}
if(settingSaveIndividualDistances){
settingSaveIndividualDistances = 1;
}
resultFile = Dialog.getString();
if(!File.exists(resultFile)){
resultFile = 0;
}
} // END while(resultFile == 0)
//// Initialize
isFirstImage = 1;
isNewImage = 1;
measuringImage = 1;
measurementNum = 0;
resultAvailable = 0;
changeSetting = '';
nextStep = '';
textSize = '';
outTxtHeader = 'File,Folder,Time,Sequence,LengthLine1,LengthLine2,DistancesMeasured,Mean,SD,Min,Max,CoordsMinDistance,CoordsMaxDistance,settingToleranceAngleDeviation,settingInterpolateInterval,settingMeasureEquidistance,settingUseSplines,settingIgnoreOffset,settingDirectionality,settingTangentingPointDist,settingDirectionalityParallelRefDegAbsolute,settingDirectionalityRadialRefCoord';
if(settingSaveIndividualDistances == 1){
outTxtHeader = outTxtHeader + ',Values[length],Values[angleDegree]';
}
outTxtHeader = outTxtHeader + "\n";
// Check if header (column names) needs to be written to output
if(startsWith(File.openAsRawString(resultFile, 50), 'File,Folder,Time')){
outTxtHeader = '';
}
// To do: account for result file used in past with different settingSaveIndividualDistances values
////////////////////////////////////////////////////
////////// 4. Performing measurement ///////////
////////////////////////////////////////////////////
while(measuringImage == 1){
//// Show dialog box prompting user for next action after measuring, and do the needed for the action
if(resultAvailable != 0){
Dialog.createNonBlocking('Next');
Dialog.addMessage('Measurement made (px): ' + resultAvailable + "\n\nCheck in the image window");
dialogItems = newArray('Save measurements and Quit', 'Save measurements and Measure another image', 'Save measurements and again Measure opened image', "Don't save measurements and Measure another image", "Don't save measurements and again Measure opened image", "Don't save measurements and Quit");
Dialog.addRadioButtonGroup('Select NEXT action', dialogItems, dialogItems.length, 1, 'Save measurements and Measure another image');
Dialog.addCheckbox('Change settings for new measurement', 0);
Dialog.show();
changeSetting = Dialog.getCheckbox();
nextStep = Dialog.getRadioButton();
}
if(nextStep == 'Save measurements and Quit'){
File.append(outTxtHeader + outTxt, resultFile);
if(settingFileSavePrefix != ''){
mySaveAnnotatedImage();
}
myQuit(0);
}
if(nextStep == 'Save measurements and Measure another image'){
File.append(outTxtHeader + outTxt, resultFile);
if(settingFileSavePrefix != ''){
mySaveAnnotatedImage();
}
close('*');
isFirstImage = 0;
isNewImage = 1;
measuringImage = 1;
resultAvailable = 0;
outTxtHeader = '';
outTxt = '';
textSize = 0;
}
if(nextStep == 'Save measurements and again Measure opened image'){
File.append(outTxtHeader + outTxt, resultFile);
isFirstImage = 0;
isNewImage = 0;
openedImages = getList('image.titles');
if(openedImages.length == 0){
isNewImage = 1;
}else if(settingShowStatsInImage != 0){
Overlay.removeSelection(endOverlayCount - 1);
measurementStats = d2s(measurementNum - 1, 0) + ': ' + resultAvailable;
makeText(String.trim(measurementStats), 0, (fontHeight * (measurementNum - 2)));
Overlay.addSelection('', 0, settingTextBgColor);
makeText(measurementNum - 1, refLineFirstX, refLineFirstY);
Overlay.addSelection('', 0, settingTextBgColor);
Overlay.show;
run('Select None');
endOverlayCount = endOverlayCount + 1;
}
measuringImage = 1;
resultAvailable = 0;
outTxtHeader = '';
outTxt = '';
}
if(nextStep == "Don't save measurements and Measure another image"){
if(measurementNum > 1 && settingFileSavePrefix != ''){
mySavePrevAnnotatedImage();
}
close('*');
isNewImage = 1;
measuringImage = 1;
resultAvailable = 0;
if(isFirstImage == 0){
outTxtHeader = '';
}
outTxt = '';
textSize = 0;
}
if(nextStep == "Don't save measurements and again Measure opened image"){
measurementNum--;
openedImages = getList('image.titles');
if(openedImages.length == 0){
isNewImage = 1;
}else{
for(i = endOverlayCount; i > startOverlayCount; i--){
Overlay.removeSelection(i - 1);
}
isNewImage = 0;
}
measuringImage = 1;
resultAvailable = 0;
if(isFirstImage == 0){
outTxtHeader = '';
}
outTxt = '';
if(measurementNum == 1){
textSize = 0;
}
}
if(nextStep == "Don't save measurements and Quit"){
if(measurementNum > 1 && settingFileSavePrefix != ''){
mySavePrevAnnotatedImage();
}
myQuit(0);
}
// To do: Retain drawn lines and re-measure
// Changing measurement settings during the run if user indicated so in the dialog box
// To do: This code block also used for initial settings dialog, so rewrite as function that remembers value states
if(changeSetting == 1){
Dialog.create('Change measurement settings');
Dialog.addNumber('Number of locations to measure at', settingUsablePointsMax);
Dialog.addNumber('Angle tolerance (deg.)', settingToleranceAngleDeviation);
Dialog.addNumber('Interpolation interval (px)', settingInterpolateInterval);
Dialog.addNumber('Equidistance of locations', settingMeasureEquidistance);
Dialog.setInsets(0, 0, 0);
Dialog.addString('Filename prefix to save image', settingFileSavePrefix, 6);
Dialog.setInsets(0, 0, 0);
Dialog.addChoice('User-drawn line to use as reference line', newArray('Longer', 'First', 'Second'), settingRefLineId);
Dialog.setInsets(0, 0, 0);
Dialog.addChoice('Color of distance lines', newArray('Yellow', 'Black', 'Blue', 'Orange', 'White'), settingDistColor);
Dialog.setInsets(0, 0, 0);
Dialog.addChoice('Directionality', newArray('None', 'Orthogonal', 'Parallel', 'Radial'), settingDirectionality);
Dialog.setInsets(0, 0, 0);
Dialog.addNumber('Local tangent-defining point', settingTangentingPointDist);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Use splines', settingUseSplines);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Ignore offset', settingIgnoreOffset);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Show stats on image', settingShowStatsInImage);
Dialog.setInsets(0, 0, 0);
Dialog.addCheckbox('Save individual distances', settingSaveIndividualDistances);
Dialog.setInsets(0, 0, 0);
Dialog.addMessage("\nHit OK to continue");
Dialog.addHelp(helpHtml);
Dialog.show();
// Finalize values
settingUsablePointsMax = floor(Dialog.getNumber());
settingToleranceAngleDeviation = abs(Dialog.getNumber());
settingInterpolateInterval = floor(Dialog.getNumber());
settingMeasureEquidistance = floor(Dialog.getNumber());
settingFileSavePrefix = String.trim(Dialog.getString());
settingRefLineId = Dialog.getChoice();
settingDistColor = Dialog.getChoice();
settingDirectionality = Dialog.getChoice();
settingTangentingPointDist = floor(Dialog.getNumber());
settingUseSplines = Dialog.getCheckbox();
settingIgnoreOffset = Dialog.getCheckbox();
settingShowStatsInImage = Dialog.getCheckbox();
settingSaveIndividualDistances = Dialog.getCheckbox();
if(isNaN(settingUsablePointsMax) || settingUsablePointsMax < 0){
settingUsablePointsMax = 20;
}
if(isNaN(settingToleranceAngleDeviation) || settingToleranceAngleDeviation < 0 || settingToleranceAngleDeviation > 30){
settingToleranceAngleDeviation = 1;
}
if(isNaN(settingInterpolateInterval) || settingInterpolateInterval < 1 || settingInterpolateInterval > 10){
settingInterpolateInterval = 1;
}
if(isNaN(settingMeasureEquidistance) || settingMeasureEquidistance < 1){
settingMeasureEquidistance = 0;
}
if(isNaN(settingTangentingPointDist) || settingTangentingPointDist < 1){
settingTangentingPointDist = 1;
}
if(settingUseSplines){
settingUseSplines = 1;
}
if(settingIgnoreOffset){
settingIgnoreOffset = 1;
}
if(settingShowStatsInImage){
settingShowStatsInImage = 1;
}
if(settingSaveIndividualDistances){
settingSaveIndividualDistances = 1;
}
} // END if(changeSetting == 1)
if(isNewImage == 1){
imgPath = File.openDialog('Select image file');
open(imgPath);
// To do: check if not an image
imgFile = File.getName(imgPath);
imgFileNoExtension = File.getNameWithoutExtension(imgPath);
imgDir = File.getParent(imgPath);
measurementNum = 1;
measurementStats = '';
}
//// Prompt user to draw the two lines for the two edges
liningAttempted = 0;
do{
openedImages = getList('image.titles');
if(openedImages.length == 0){
waitForUser("No image!\n\nThe macro has to quit. Please re-run it.");
setKeyDown('none');
exit;
}
// Initialize
lengthDrawnLine1 = 0;
lengthDrawnLine2 = 0;
testLinePoints = newArray(0);
setTool('polyline');
// The two lines should be long enough, and overlap enough; otherwise, user has to redraw
if(liningAttempted == 0){
waitForUser("[ FIRST ]: on image, draw a line of any type to mark one edge\n\n[ THEN ]: hit OK\n\n\n( to quit, also hold shift key )");
}else{
// Line drawing issue. Remove drawn lines and prompt re-drawing.
Overlay.removeRois('drawnLine1-' + measurementNum);
Overlay.removeRois('drawnLine2-' + measurementNum);
run('Select None');
waitForUser("Sorry, there was an issue. Please re-draw lines.\n\nIf issue persists, restart macro to change settings.\n\n[ FIRST ]: on image, draw a line to mark one edge\n\n[ THEN ]: hit OK\n\n\n( to quit, also hold shift key )");
}
myQuit(1); // Quit in case user wanted to as per above
// Track overlay elements in case some need to be removed later
startOverlayCount = Overlay.size;
// Line 1
if(selectionType() == 5 || selectionType() == 6 || selectionType() == 7){
lengthDrawnLine1 = getValue('Length');
if(lengthDrawnLine1 > 0){
liningAttempted = 1;
if(settingUseSplines == 1 && (selectionType() == 6 || selectionType() == 7)){
run('Fit Spline');
}
run('Interpolate', 'interval=' + settingInterpolateInterval);
Roi.getCoordinates(x1, y1);
Roi.setName('drawnLine1-' + measurementNum);
Overlay.addSelection(settingRefLineColor, 1);
drawnLine1Key = Overlay.size - 1;
run('Select None');
// Line 2, if line 1 seems OK
setTool('polyline');
waitForUser("[ NOW ]: draw the second line for the other edge\n\n[ THEN ]: hit OK\n\n\n( to quit, also hold shift key )");
myQuit(1);
if((selectionType() == 5 || selectionType() == 6 || selectionType() == 7)){
lengthDrawnLine2 = getValue('Length');
if(lengthDrawnLine2 > 0){
if(settingUseSplines == 1 && (selectionType() == 6 || selectionType() == 7)){
run('Fit Spline');
}
run('Interpolate', 'interval=' + settingInterpolateInterval);
Roi.getCoordinates(x2, y2);
Roi.setName('drawnLine2-' + measurementNum);
Overlay.addSelection(settingTestLineColor, 1);
drawnLine2Key = Overlay.size - 1;
run('Select None');
} // END if(lengthDrawnLine2 > 0)
} // END if(selectionType() == 5/6/7)
} // END if(lengthDrawnLine1 > 0)
} // END if(selectionType() == 5/6/7)
// Distances are measured to reference line (refLine) from points along the other line (testLine)
if(lengthDrawnLine1 > 0 && lengthDrawnLine2 > 0){
refLineX = x1; refLineY = y1;
testLineX = x2; testLineY = y2;
// Identify the reference/test lines
if((settingRefLineId == 'Longer' && lengthDrawnLine1 < lengthDrawnLine2) || settingRefLineId == 'Second'){
refLineX = x2; refLineY = y2;
testLineX = x1; testLineY = y1;
Overlay.activateSelection(drawnLine1Key);
Roi.setStrokeColor(settingTestLineColor);
run('Select None');
Overlay.activateSelection(drawnLine2Key);
Roi.setStrokeColor(settingRefLineColor);
run('Select None');
}
// Make line directions to be along increasing X (Y if X constant); i.e., X value of first point is the least. Ordering needed because of angle calculation and to make min. dist. estimation efficient (below)
if(refLineX[0] > refLineX[refLineX.length - 1] || (refLineX[0] == refLineX[refLineX.length - 1] && refLineY[0] > refLineY[refLineY.length - 1])){
refLineX = Array.reverse(refLineX);
refLineY = Array.reverse(refLineY);
}
refLineFirstX = round(refLineX[0]);
refLineFirstY = round(refLineY[0]);
refLineLastX = round(refLineX[refLineX.length - 1]);
refLineLastY = round(refLineY[refLineY.length - 1]);
if(testLineX[0] > testLineX[testLineX.length - 1] || (testLineX[0] == testLineX[testLineX.length - 1] && testLineY[0] > testLineY[testLineY.length - 1])){
testLineX = Array.reverse(testLineX);
testLineY = Array.reverse(testLineY);
}
// Ignore ends of the testLine so that its measured portion does not 'extend' beyond the refLine. Test line points are successively removed until the tangent vector at the next point is orthogonal to the line connecting the point and the refLine's terminal point towards that end.
if(settingIgnoreOffset == 1){
// To do: Better logic?
trimmingTestLine = 1;
while(testLineX.length > settingTangentingPointDist && trimmingTestLine == 1){
deg = myDegreeThreePoints(refLineFirstX, testLineX[0], testLineX[settingTangentingPointDist], refLineFirstY, testLineY[0], testLineY[settingTangentingPointDist]);
if(!isNaN(deg) && deg < 90){
testLineX = Array.deleteIndex(testLineX, 0);
testLineY = Array.deleteIndex(testLineY, 0);
}else{
trimmingTestLine = 0;
}
}
trimmingTestLine = 1;
while(testLineX.length > settingTangentingPointDist && trimmingTestLine == 1){
endKey = testLineX.length - 1;
deg = myDegreeThreePoints(refLineLastX, testLineX[endKey], testLineX[endKey - settingTangentingPointDist], refLineLastY, testLineY[endKey], testLineY[endKey - settingTangentingPointDist]);
if(!isNaN(deg) && deg < 90){
testLineX = Array.deleteIndex(testLineX, endKey);
testLineY = Array.deleteIndex(testLineY, endKey);
}else{
trimmingTestLine = 0;
}
}
} // END if(settingIgnoreOffset == 1)
// Use only some points of testLine for measurements
if(settingMeasureEquidistance == 0){
testLinePoints = Array.getSequence(testLineX.length);
if(settingUsablePointsMax != 0 && settingUsablePointsMax <= testLineX.length){
testLinePoints = myRandomize(Array.getSequence(testLineX.length));
testLinePoints = Array.sort(Array.slice(testLinePoints, 0, settingUsablePointsMax));
}
}else{
// Sanitize settingMeasureEquidistance
if(settingMeasureEquidistance == 1){
if(settingUsablePointsMax != 0){
settingMeasureEquidistanceVal = round(testLineX.length / settingUsablePointsMax);
}else{
settingMeasureEquidistanceVal = testLineX.length / 20;
}
if(settingMeasureEquidistanceVal < 2){
settingMeasureEquidistanceVal = 5;
}
}else{
settingMeasureEquidistanceVal = settingMeasureEquidistance;
}
pointIds = Array.getSequence(testLineX.length);
for(i = 0; i < pointIds.length; i++){
id = pointIds[i];
for(j = i; j < pointIds.length - 1; j++){
if(j - i > settingMeasureEquidistanceVal){
i = j - 1;
j = 1e99;
testLinePoints = myAppend(testLinePoints, id);
}
}
if(j - i <= settingMeasureEquidistanceVal){
i = 1e99;
testLinePoints = myAppend(testLinePoints, id);
}
if(settingUsablePointsMax != 0 && testLinePoints.length == settingUsablePointsMax){
i = 1e99;
}
}
} // END else for settingMeasureEquidistance != 0
if(testLinePoints.length < settingUsablePointsMin){
lengthDrawnLine1 = 0;
}
} // END if(lengthDrawnLine1 > 0 && lengthDrawnLine2 > 0)
}while(lengthDrawnLine1 == 0 || lengthDrawnLine2 == 0)
// Prompt user to draw the reference straight line for parallelism and note its direction (~angle)
settingDirectionalityParallelRefDeg = '';
if(settingDirectionality == 'Parallel'){
do{
setTool('line');
waitForUser("[ NOW ]: using the straight line tool, draw the reference line parallel to which distances will be measured\n\n[ THEN ]: hit OK\n\n\n( to quit, also hold shift key )");
myQuit(1);
if(selectionType() == 5){
getSelectionCoordinates(xp, yp);
settingDirectionalityParallelRefDeg = 57.296 * atan((yp[1] - yp[0])/(xp[1] - xp[0]));
}
}while(settingDirectionalityParallelRefDeg == '')
}
// Prompt user to denote the reference point for radialism
settingDirectionalityRadialRefX = '';
settingDirectionalityRadialRefY = '';
if(settingDirectionality == 'Radial'){
do{
setTool('point');
waitForUser("[ NOW ]: using the point tool, indicate the reference point radial to which distances will be measured\n\n[ THEN ]: hit OK\n\n\n( to quit, also hold shift key )");
myQuit(1);
if(selectionType() == 10){
getSelectionCoordinates(xr, yr);
settingDirectionalityRadialRefX = xr[0];
settingDirectionalityRadialRefY = yr[0];
}
}while(settingDirectionalityRadialRefX == '')
}
if(settingDirectionality == 'Orthogonal' && (settingTangentingPointDist < 1 || testLineX.length < (settingTangentingPointDist + 1))){
settingDirectionality = 'None';
} else if(settingDirectionality == 'Parallel' && settingDirectionalityParallelRefDeg == ''){
settingDirectionality = 'None';
} else if(settingDirectionality == 'Radial' && settingDirectionalityRadialRefX == ''){
settingDirectionality = 'None';
}
// Measure distance-squared between testLine points and each point along refLine; record min. value to output; assess directionality as needed; note coordinates for min. and max. distances
shortestDistLengths = newArray(0);
shortestDistCoords = newArray(0);
shortestDistAngles = newArray(0);
// Distance to point for tangentiality. Tangentiality of the line at point p is its directionality towards a point that is settingTangentingPointDist points further from p.
tangentPointDist = settingTangentingPointDist;
if(tangentPointDist > 0.5 * testLineX.length){
tangentPointDist = 1;
}
// Use cross product to identify the 'side' of testLine's starting segment (tangential at first point) that the refLine's starting segment is in
refSide = -1;
if((testLineX[0] - testLineX[tangentPointDist])*(refLineY[0] - testLineY[tangentPointDist]) > (testLineY[0] - testLineY[tangentPointDist])*(refLineX[0] - testLineX[tangentPointDist])){
refSide = 1;
}
altRefSideTested = 0;
testingAltRefSide = 0;
for(i = 0; i < testLinePoints.length; i++){
j = testLinePoints[i];
dists = newArray(0);
// Points to use for tangentiality, which will determine side-ness, orthogonality, etc. of points
tangentPoint1 = j;
tangentPoint2 = tangentPoint1 + tangentPointDist;
if(tangentPoint2 > (testLineX.length - 1)){
tangentPoint1 = j - tangentPointDist;
tangentPoint2 = tangentPoint1 + tangentPointDist;
}
// Only refLine points that are on a specific 'side' of testLine ('refSide') are used for distance measurement. If no such point exists, refLine points on the other side are considered, and if one is found, refSide is re-set as this side
if(testingAltRefSide == 1){
altRefSideTested = 1;
testingAltRefSide = 0;
}
for(k = 0; k < refLineX.length; k++){
// Cross product is used to assess side-ness of refLine points with respect to the tangentiality at the testLine point
side = -1;
if((testLineX[tangentPoint1] - testLineX[tangentPoint2])*(refLineY[k] - testLineY[tangentPoint2]) > (testLineY[tangentPoint1] - testLineY[tangentPoint2])*(refLineX[k] - testLineX[tangentPoint2])){
side = 1;
}
if(side != refSide){
dists = myAppend(dists, 1e99);
continue;
}
// No directionality constraint
if(settingDirectionality == 'None'){
dists = myAppend(dists, pow(testLineX[j] - refLineX[k], 2) + pow(testLineY[j] - refLineY[k], 2));
}else{
// Orthogonality constraint
if(settingDirectionality == 'Orthogonal'){
deg = myDegreeThreePoints(refLineX[k], testLineX[tangentPoint1], testLineX[tangentPoint2], refLineY[k], testLineY[tangentPoint1], testLineY[tangentPoint2]);
if(isNaN(deg) || abs(deg - 90) < settingToleranceAngleDeviation){
dists = myAppend(dists, pow(testLineX[j] - refLineX[k], 2) + pow(testLineY[j] - refLineY[k], 2));
}else{
dists = myAppend(dists, 1e99);
}
// Parallelism constraint
}else if(settingDirectionality == 'Parallel'){
deg = 57.296 * atan((testLineY[j] - refLineY[k]) / (testLineX[j] - refLineX[k]));
if(abs(deg - settingDirectionalityParallelRefDeg) < settingToleranceAngleDeviation){
dists = myAppend(dists, pow(testLineX[j] - refLineX[k], 2) + pow(testLineY[j] - refLineY[k], 2));
}else{
dists = myAppend(dists, 1e99);
}
// Radialism constraint
}else if(settingDirectionality == 'Radial'){
deg = myDegreeThreePoints(refLineX[k], testLineX[j], settingDirectionalityRadialRefX, refLineY[k], testLineY[j], settingDirectionalityRadialRefY);
if(isNaN(deg) || abs(deg - 180) < settingToleranceAngleDeviation || abs(deg - 0) < settingToleranceAngleDeviation){
dists = myAppend(dists, pow(testLineX[j] - refLineX[k], 2) + pow(testLineY[j] - refLineY[k], 2));
}else{
dists = myAppend(dists, 1e99);
}
}
}
} // END for(k = 0; k < refLineX.length; k++)
// Set width of drawn lines
lineWidth = floor(settingLineWidth);
if(lineWidth < 1){
lineWidth = floor(0.001 * sqrt(pow(refLineLastX - refLineFirstX, 2) + pow(refLineLastY - refLineFirstY, 2)) + 1);
Overlay.setStrokeWidth(lineWidth);
}
// Identify and plot shortest distance
if(dists.length > 0){
kRanks = Array.rankPositions(dists);
kMin = kRanks[0];
if(dists[kMin] < 1e99){
shortestDistLengths = myAppend(shortestDistLengths, sqrt(dists[kMin]));
shortestDistCoords = myAppend(shortestDistCoords, d2s(testLineX[j], 1) + '/' + d2s(testLineY[j], 1) + ':' + d2s(refLineX[kMin], 1) + '/' + d2s(refLineY[kMin], 1));
shortestDistAngles = myAppend(shortestDistAngles, Math.toDegrees(atan2(testLineY[j] - refLineY[kMin], testLineX[j] - refLineX[kMin])));
setColor(settingDistColor);
setLineWidth(lineWidth);
Overlay.drawLine(testLineX[j], testLineY[j], refLineX[kMin], refLineY[kMin]);
Overlay.show; // If shown later, color may change
}else{
if(altRefSideTested == 0){
i--;
refSide = -1 * refSide;
testingAltRefSide = 1;
}else{
refSide = -1 * refSide;
}
}
}
}
// Exit in case no measurement was collected for any reason
if(shortestDistLengths.length == 0){
waitForUser("Sorry, there was an issue. The macro could not perform measurements and will exit.\n\nPlease retry with differently drawn lines or with different macro settings.");
myQuit(0);
}
// Stats for the shortestDistLengths (shortest distances)
Array.getStatistics(shortestDistLengths, min, max, mean, sd);
resultAvailable = 'Avg. ' + d2s(mean, 1) + ', SD ' + d2s(sd, 1) + ', range ' + d2s(min, 1) + '-' + d2s(max, 1) + ' (n = ' + shortestDistLengths.length + ')';
// Min. and max. shortestDistLengths, many of whose values may be same
iRanks = Array.rankPositions(shortestDistLengths);
iRanksN = iRanks.length;
shortestDistLengthsMin = shortestDistLengths[iRanks[0]];
shortestDistLengthsMax = shortestDistLengths[iRanks[iRanksN - 1]];
shortestDistMinCoords = '';
shortestDistMaxCoords = '';
if(shortestDistLengthsMin != shortestDistLengthsMax){
setLineWidth(lineWidth);
setColor(settingShortestDistColor);
i = 0;
while(i < iRanksN && shortestDistLengths[iRanks[i]] == shortestDistLengthsMin){
coords = shortestDistCoords[iRanks[i]];
shortestDistMinCoords = shortestDistMinCoords + coords + ';';
coords = split(coords, "/:");
Overlay.drawLine(coords[0], coords[1], coords[2], coords[3]);
Overlay.show;
i++;
}
setColor(settingLongestDistColor);
i = iRanksN - 1;
while(i > 0 && shortestDistLengths[iRanks[i]] == shortestDistLengthsMax){
coords = shortestDistCoords[iRanks[i]];
shortestDistMaxCoords = shortestDistMaxCoords + coords + ';';
coords = split(coords, "/:");
Overlay.drawLine(coords[0], coords[1], coords[2], coords[3]);
Overlay.show;
i--;
}
}
// Show stats of the shortest distances on image
if(settingShowStatsInImage != 0){
setColor(settingTextColor);
if(textSize == 0){
textSize = floor(settingTextSize);
}
if(textSize < 1){
textSize = floor(0.01 * sqrt(pow(refLineLastX - refLineFirstX, 2) + pow(refLineLastY - refLineFirstY, 2)) + 8);
}
setFont('Monospaced', textSize, 'antialiased');
fontHeight = getValue('font.height');
makeText(' ' + resultAvailable, 0, (fontHeight * (measurementNum - 1)));
Overlay.addSelection('', 0, settingTextBgColor);
Overlay.show;
}
// For saving measurements, with current time, and including individual distances as per macro's setting
getDateAndTime(year, month, dayOfWeek, dayOfMonth, hour, minute, second, msec);
measureTime = d2s(year, 0) + '-' + IJ.pad(month + 1, 2) + '-' + IJ.pad(dayOfMonth, 2) + '-' + IJ.pad(hour, 2) + '.' + IJ.pad(minute, 2) + '.' + IJ.pad(second, 2);
// Noting some setting values to save as well
parallelRef = '';
if(settingDirectionalityParallelRefDeg != ''){
parallelRef = abs(d2s(settingDirectionalityParallelRefDeg, 1));
}
radialRef = '';
if(settingDirectionalityRadialRefX != ''){
radialRef = d2s(settingDirectionalityRadialRefX, 1) + '/' + d2s(settingDirectionalityRadialRefY, 1);
}
outTxt = imgFile + ',' + imgDir + ',' + measureTime + ',' + measurementNum + ',' + d2s(lengthDrawnLine1, 1) + ',' + d2s(lengthDrawnLine2, 1) + ',' + shortestDistLengths.length + ',' + d2s(mean, 1) + ',' + d2s(sd, 1) + ',' + d2s(min, 1) + ',' + d2s(max, 1) + ',' + replace(shortestDistMinCoords, ';$', '') + ',' + replace(shortestDistMaxCoords, ';$', '') + ',' + settingToleranceAngleDeviation + ',' + settingInterpolateInterval + ',' + settingMeasureEquidistance + ',' + settingUseSplines + ',' + settingIgnoreOffset + ',' + settingDirectionality + ',' + tangentPointDist + ',' + parallelRef + ',' + radialRef;
if(settingSaveIndividualDistances == 1){
outTxt = outTxt + ',';
for(i = 0; i < shortestDistLengths.length; i++){
if(i > 0){
outTxt = outTxt + ';';
}
outTxt = outTxt + d2s(shortestDistLengths[i], 1);
}
outTxt = outTxt + ',';
for(i = 0; i < shortestDistAngles.length; i++){
if(i > 0){
outTxt = outTxt + ';';
}
outTxt = outTxt + d2s(shortestDistAngles[i], 1);
}
}
measurementNum++;
// End cycle, noting count of overlay elements
endOverlayCount = Overlay.size;
} // END while(measuringImage == 1)
////////////////////////////////////////
//////// 5. Custom functions ////////
////////////////////////////////////////
// Add to array
function myAppend(arr, value){
arr2 = newArray(arr.length+1);
for(i = 0; i < arr.length; i++){
arr2[i] = arr[i];
}
arr2[arr.length] = value;
return arr2;
}
// Get true/false value as text
function myBoolText(num){
if(num > 0){
return 'true';
}
return 'false';
}
// Get (acute) angle in degrees between 3 points specified by X/Y coordinates at point #2 (p2)
function myDegreeThreePoints(p1x, p2x, p3x, p1y, p2y, p3y){
p1p2 = pow(p1x - p2x, 2) + pow(p1y - p2y, 2);
p2p3 = pow(p2x - p3x, 2) + pow(p2y - p3y, 2);
p1p3 = pow(p1x - p3x, 2) + pow(p1y - p3y, 2);
// Returns NaN if p2 = p1 or p3, else between approx. 0 and 180; 57.29 = 180/PI
return 57.296 * acos((p1p2 + p2p3 - p1p3)/(2 * sqrt(p1p2) * sqrt(p2p3)));
}
// Quit macro nicely – invoked in multiple instances, some of which may be user-initiated with a Shift key press
function myQuit(withShiftKey) {
if(withShiftKey == 0){
close('*');
setKeyDown('none');
exit;
}else if(isKeyDown('Shift') == true){
close('*');
setKeyDown('none');
exit;
}
}
// Randomize array
function myRandomize(array){
n = array.length;
while(n > 1){
k = n * random();
n--;
temp = array[n];
array[n] = array[k];
array[k] = temp;
}
return array;
}
// Save annotated image file, before quitting or moving to another image, when the same image is re-measured and user wants to keep the last measurement; also re-show the last stats text in image to indicate last measurement sequence number, and mark refLine with the number
function mySaveAnnotatedImage(){
openedImages = getList('image.titles');
if(openedImages.length == 0){
waitForUser("No image!\n\nDon't close image while the macro is running.\n\nMeasurements were saved but\n\nimage annotated with measurements was not saved.");
}else{
if(settingShowStatsInImage != 0 && measurementNum > 2){
Overlay.removeSelection(endOverlayCount - 1);
measurementStats = d2s(measurementNum - 1, 0) + ': ' + resultAvailable;
makeText(String.trim(measurementStats), 0, (fontHeight * (measurementNum - 2)));
Overlay.addSelection('', 0, settingTextBgColor);
makeText(measurementNum - 1, refLineFirstX, refLineFirstY);
Overlay.addSelection('', 0, settingTextBgColor);
}
Overlay.flatten;
save(imgDir + File.separator + settingFileSavePrefix + imgFileNoExtension + '_' + measureTime + '.tif');
close();
}
}
// Save annotated image file, before quitting or moving to another image, when the same image is re-measured but user does not want to keep the last measurement
function mySavePrevAnnotatedImage(){
openedImages = getList('image.titles');
if(openedImages.length == 0){
waitForUser("No image!\n\nDon't close image while the macro is running.\n\nMeasurements were saved but\n\nimage annotated with measurements was not saved.");
}else{
for(i = endOverlayCount; i > startOverlayCount; i--){
Overlay.removeSelection(i - 1);
}
Overlay.flatten;
save(imgDir + File.separator + settingFileSavePrefix + imgFileNoExtension + '_' + measureTime + '.tif');
close();
}
}