//Primary development version kept on MD's Vaio under /home/mdoube/ImageJ/macros
//edits made to other copies will not be kept!
//MomentMacroJ_v0.6.3.txt,a version of the MomentMacro modified for use with ImageJ.
//original code downloaded from http://www.hopkinsmedicine.org/FAE/MomentMacroJ_v1_3.txt
//feb 2008 and modified for updated ImageJ macro functions, 16-bit images, stacks and QCT
//designed to handle 16-bit, calibrated greyscale image stacks with DICOM header
//formula for calculation of moments fixed (added value for pixel area, w^3*h/12)
//formula for calculation of greatest distance from principal axes fixed (so section modulus fixed)
//Requires ImageJ version 1.41e or later.
//See http://rsb.info.nih.gov/ij/upgrade/index.html for the latest version of ImageJ.
//This version: 2008-11-05
// Copyright Michael Doube 2008
// 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 .
//to do:
//Allow ROI-based methods to find more than one particle per slice
//Smarten up detection of cortical bone, especially for epiphyses
//migrate to Java!
var range, dumbarea, perimeter, type, xbox, ybox, xadjust, yadjust, xCoordinates, yCoordinates, perimSelect;
var Sn,Sx,Sy,Sxx,Syy,Sxy,rot2, Theta, Jz, Cx, Cy, maxRadMax, maxRadMin; Imax; Imin;
var wSn,wSx,wSy,wSxx,wSyy,wSxy,wrot2, wTheta, wJz, wCx, wCy, wmaxRadMax, wmaxRadMin; wImax; wImin;
var row;
var pi = 3.14159265359;
var deg2rad = pi / 180;
var rad2deg = 180 / pi;
var pmin, pmax, intercept, slope;
var bones = newArray("other bone", "scapula", "humerus", "radius", "ulna", "metacarpal", "pelvis", "femur", "tibia", "fibula", "metatarsal");
var scalex = 1;
var scaley = 1;
var scalez = 1;
var microns = fromCharCode(181,109);
var units = newArray("mm", microns, "nm", "px");
var airhu = -1000; //Hounsfield units for air,
var bonehu = 0; //bone / air interface (50/50 bone and air)
var maxhu = 4000; //and upper limit for bone
var caloffset = calibrate(0);
var calslope = (calibrate(100)-caloffset) / 100;
var tmin, tmax, tav, feret, fraccort;
var ycentroid, rcmax, rcmin;
var degs = fromCharCode(176);
//macro to find the distance between two points and the midslice of a stack.
macro "MidPoint [g]" {
name = getTag("0010,0010");
title = getTitle();
getVoxelSize(width, height, depth, unit);
depth = getNumericTag("0018,0050");
print("Pixel Scale:",width, height, depth, unit);
//Initialise the results window
if (isOpen("Results")) row = nResults;
else row = 0;
//ask us which bone we are looking at
bonedef = "scapula";
for (n=0; n=0) {
airhu = 0;
}
else if (min > 31000) {
airhu = 31768;
}
else if (min < -800){
airhu = -1000;
} else {airhu = 0;}
} else {0.156
var isCalibrated = true;
Dialog.addMessage("Image is calibrated\nEnter HU below:");
airhu = -1000;
}
bonehu = airhu + 1000;
maxhu = airhu + 5000;
Dialog.addNumber("Air:", airhu);
Dialog.addNumber("Bone Min:", bonehu);
Dialog.addNumber("Bone Max:", maxhu);
Dialog.show();
doThickness = Dialog.getCheckbox();
doAxes = Dialog.getCheckbox();
doCentroids = Dialog.getCheckbox();
doOutline = Dialog.getCheckbox();
doCopy = Dialog.getCheckbox();
doStack = Dialog.getCheckbox();
bone = Dialog.getChoice();
for (n=0; n bonehu) && (calibrate(getPixel(x,y)) <= maxhu)) {
if (doUnweighted){
Sn++; //number of pixels
Sx += x; //distance from parallel axis to pixel
Sy += y; //normal distance from parallel axis summed over pixels
Sxx += x*x; //squared normal distances from parallel axis (Iz)
Syy += y*y;
Sxy += y*x;
}
if(doWeighted){
//if you have a function for converting pixel values to real densities, use it here
//this normalisation assumes that the value for air is 0% bone and the upper limit set is 100% bone
norm = (calibrate(getPixel(x,y))-airhu) / range;
wSn += norm;
wSx += x*norm;
wSy += y*norm;
wSxx += x*x*norm;
wSyy += y*y*norm;
wSxy += x*y*norm;
}
}
}
}
if(doUnweighted){
Cx = Sx / Sn; //x-coordinate of binary centroid
Cy = Sy / Sn; //y-coordinate of binary centroid
Myy = Sxx - (Sx*Sx / Sn) + Sn/12;
Mxx = Syy - (Sy*Sy / Sn) + Sn/12;
Mxy = Sxy - (Sx*Sy / Sn);
if (Mxy==0) Theta=0;
else Theta = atan((Mxx-Myy+sqrt(sqr(Mxx-Myy)+(4*sqr(Mxy))))/(2*Mxy)) * rad2deg;
rot2 = Theta * deg2rad;
Imax = (Mxx+Myy)/2 + sqrt(sqr(((Mxx-Myy)/2))+Mxy*Mxy);
Imin = (Mxx+Myy)/2 - sqrt(sqr(((Mxx-Myy)/2))+Mxy*Mxy);
}
if (doWeighted){
wCx = wSx / wSn ; //x-coordinate of weighted centroid
wCy = wSy / wSn ; //y-coordinate of weighted centroid
wMyy = wSxx - (wSx*wSx / wSn) + wSn/12;
wMxx = wSyy - (wSy*wSy / wSn) + wSn/12;
wMxy = wSxy - (wSx*wSy / wSn);
if (wMxy==0) wTheta = 0;
else wTheta = atan((wMxx - wMyy+sqrt(sqr(wMxx - wMyy)+4*sqr(wMxy)))/(2*wMxy)) * rad2deg;
wrot2 = wTheta * deg2rad;
wImax = (wMxx+wMyy)/2 + sqrt(sqr(((wMxx-wMyy)/2))+wMxy*wMxy);
wImin = (wMxx+wMyy)/2 - sqrt(sqr(((wMxx-wMyy)/2))+wMxy*wMxy);
}
maxRadMax = 0; maxRadMin = 0;
wmaxRadMax = 0; wmaxRadMin = 0;
for (y=roiY; y<=roiY+roiHeight; y++) {
for (x=roiX; x<=roiX+roiWidth; x++) {
if ((calibrate(getPixel(x,y)) > bonehu) && (calibrate(getPixel(x,y)) <= maxhu)) {
if (doUnweighted){
//maximum distance from minimum principal axis (longer)
maxRadMin = maxOf(maxRadMin, abs((x-Cx)*cos(rot2) + (y-Cy)*sin(rot2)));
//maximum distance from maximum principal axis (shorter)
maxRadMax = maxOf(maxRadMax, abs((y-Cy)*cos(rot2) - (x-Cx)*sin(rot2)));
}
if(doWeighted){
wmaxRadMin = maxOf(wmaxRadMin, abs((x-wCx)*cos(wrot2) + (y-wCy)*sin(wrot2)));
wmaxRadMax = maxOf(wmaxRadMax, abs((y-wCy)*cos(wrot2) - (x-wCx)*sin(wrot2)));
}
}
}
}
if(doUnweighted){
Rmax = sqrt(Imax / Sn); //length of major axis, formerly R1
Rmin = sqrt(Imin / Sn); //length of minor axis, formerly R2
Zmax = Imax / maxRadMin; //save section moduli
Zmin = Imin / maxRadMax;
rot2 = 0;
}
if(doWeighted){
wRmax = sqrt(wImax / wSn);
wRmin = sqrt(wImin / wSn);
wZmax = wImax / wmaxRadMin; //save section moduli
wZmin = wImin / wmaxRadMax;
wrot2 = 0;
}
selectWindow(title);
GetPerimeter();
//Draw principal axes and centroids
if(doAxes) {
if (doCopy){
selectWindow(title+'_Annotated');
setColor("white");
} else {
selectWindow(title);
setColor("white");
}
DrawAxis();
}
if (doCentroids) {
if (doCopy){
selectWindow(title+'_Annotated');
setColor("white");
} else {
selectWindow(title);
setColor("white");
}
DrawCentroids();
}
if (doCopy) {
selectWindow(title+'_Annotated');
setSlice(nSlices);
if (doOutline) {
for (n = 0; n Theta > 90 )
setResult("Rmax ("+unit+")", row, Rmax * scalex); //length of major axis
setResult("Rmin ("+unit+")", row, Rmin * scalex); //length of minor axis
setResult("Zmax ("+unit+"^3)", row, Zmax * scalex*sqr(scalex)); //In structural engineering, the section modulus of a beam (Z) is the ratio of a cross section's second moment of area (I) to its greatest distance from the neutral axis (Xmaxrad and Ymaxrad).
setResult("Zmin ("+unit+"^3)", row, Zmin * scalex*sqr(scalex)); //The section modulus is directly related to the strength of a corresponding beam. It is expressed in units of volume.
}
if (doWeighted){
setResult("wCSA ("+unit+"^2)", row, wSn * sqr(scalex)); //weighted cross-sectional area, weighting each pixel by its brightness (~volume fraction)
setResult("wCx ("+unit+")", row, wCx * scalex); //weighted centroid x coordinate in original image
setResult("wCy ("+unit+")", row, wCy * scalex); //weighted centroid y coordinate in original image
setResult("wImax ("+unit+"^4)", row, wImax * sqr(sqr(scalex))); //weighted second moment of area around the major axis
setResult("wImin ("+unit+"^4)", row, wImin * sqr(sqr(scalex))); //weighted second moment of area around the minor axis
setResult("wJz ("+unit+"^4)", row, (wImax + wImax) * sqr(sqr(scalex))); //weighted polar moment of inertia
setResult("wTheta "+degs, row, wTheta); //angle of weighted major axis to horizontal (+ve wTheta = -ve gradient, -90 > wTheta > 90 )
setResult("wRmax ("+unit+")", row, wRmax * scalex); //length of major axis
setResult("wRmin ("+unit+")", row, wRmin * scalex); //length of minor axis
setResult("wZmax ("+unit+"^3)", row, wZmax * scalex * sqr(scalex)); //weighted section modulus
setResult("wZmin ("+unit+"^3)", row, wZmin * scalex * sqr(scalex)); //weighted section modulus
}
row++;
selectWindow(title);
resetThreshold();
DrawSelection();
}
if (doCopy) {
selectWindow(title+'_Annotated');
setSlice(nSlices);
run("Delete Slice");
setVoxelSize(scalex, scaley, scalez, unit);
}
setBatchMode("exit and display");
updateResults();
hours = floor((getTime()-starttime) / 3600000 );
minutes = floor((getTime() - starttime - hours * 3600000) / 60000);
seconds = floor((getTime() - starttime - hours * 3600000 - minutes * 60000) / 1000);
tenths = round((getTime() - starttime - hours * 3600000 - minutes * 60000 - seconds * 1000)/100);
showStatus("Macro complete. Duration: "+hours+":"+minutes+":"+seconds+"."+tenths+"s");
}
// End of macros
// Begin functions
function sqr(n) {
return n*n;
}
function DrawAxis() {
//optional function to draw major/minor axis of ellipse
setLineWidth(1);
if(doUnweighted){
//unweighted axes
drawLine(Cx - xadjust - cos((Theta+90) * deg2rad) * 2 * Rmin, Cy - yadjust - sin((Theta+90) * deg2rad) * 2 * Rmin,
Cx - xadjust + cos((Theta+90) * deg2rad) * 2 * Rmin, Cy - yadjust + sin((Theta+90) * deg2rad) * 2 * Rmin);
drawLine(Cx - xadjust - cos((0-Theta) * deg2rad) * 2 * Rmax, Cy - yadjust + sin((0-Theta) * deg2rad) * 2 * Rmax,
Cx - xadjust + cos((0-Theta) * deg2rad) * 2 * Rmax, Cy -yadjust - sin((0-Theta) * deg2rad) * 2 * Rmax);
}
if(doWeighted){
//weighted axes
drawLine(wCx - xadjust - cos((wTheta+90) * deg2rad) * 2 * wRmin, wCy - yadjust - sin((wTheta+90) * deg2rad) * 2 * wRmin,
wCx - xadjust + cos((wTheta+90) * deg2rad) * 2 * wRmin, wCy - yadjust + sin((wTheta+90) * deg2rad) * 2 * wRmin);
drawLine(wCx - xadjust- cos((0-wTheta) * deg2rad) * 2 * wRmax, wCy - yadjust + sin((0-wTheta) * deg2rad) * 2 * wRmax,
wCx - xadjust + cos((0-wTheta) * deg2rad) * 2 * wRmax, wCy - yadjust - sin((0-wTheta) * deg2rad) * 2 * wRmax);
}
}
function DrawCentroids() {
setLineWidth(1);
if(doUnweighted){
//mark unweighted centroid with a circle
drawOval(Cx-4-xadjust, Cy-4-yadjust,9,9);
}
if(doWeighted){
//mark weighted centroid with a cross
drawLine(wCx+4-xadjust, wCy-yadjust, wCx-4-xadjust, wCy-yadjust);
drawLine(wCx-xadjust, wCy+4-yadjust, wCx-xadjust, wCy-4-yadjust);
}
}
function GetPerimeter() {
// Figure out the outer perimeter of the section and the area it encloses
// setAutoThreshold();
if (isCalibrated) setThreshold(bonehu, maxhu);
else setThreshold((bonehu-caloffset)/calslope, (maxhu-caloffset)/calslope);
if (doWeighted) {
doWand(xadjust, wCy);
} else if (doUnweighted) {
doWand(xadjust, Cy);
}
resetThreshold();
perimSelect = selectionType;
if (selectionType == 4) {
getRawStatistics(dumbarea); //area enclosed within the periosteum including marrow space as solid
getSelectionCoordinates(xCoordinates, yCoordinates);
ncoord = lengthOf(xCoordinates);
perimeter = 0;
for (p = 0; p < ncoord-2; p=p+2){
//wand selections are a set of points on vertices of the selection
//so the distance between n and n+1 is always an integer pixel value
//along pixel edges. Better to pythagorise between every second vertice
perimeter = perimeter + sqrt(sqr((xCoordinates[p]-xCoordinates[p+2]))+sqr((yCoordinates[p]-yCoordinates[p+2])));
}
perimeter = perimeter + sqrt(sqr((xCoordinates[ncoord-2]-xCoordinates[0]))+sqr((yCoordinates[ncoord-2]-yCoordinates[0])));
// if (doOutline) run("Draw");
} else {
perimeter = 0/0;
dumbarea = 0/0;
}
}
function GetSelection() {
//Function to get the ROI containing the specimen
type = selectionType();
if (type != -1) {
getSelectionCoordinates(xbox, ybox);
}
}
function DrawSelection() {
//Function to draw the ROI containing the specimen
if (type != -1 ){
makeSelection(type, xbox, ybox);
} else {
run("Select None");
}
}
function CorticalThickness() {
//Apply segmentation scheme
//get periosteal and endosteal coordinates
//for each periosteal point, work out nearest endosteal point
//keep shortest distance, do some stats with it
selectWindow(title);
run("Duplicate...", "title="+getTitle()+"-thickness");
// setAutoThreshold();
if (isCalibrated) setThreshold(bonehu, maxhu);
else setThreshold((bonehu-caloffset)/calslope, (maxhu-caloffset)/calslope);
run("Make Binary");
run("Erode");
run("Smooth");
npix = 0;
xsum = 0;
ysum = 0;
for (x = 0; x < getWidth(); x++){
for (y = 0; y < getHeight; y++){
if (getPixel(x,y) > 0){
npix++;
xsum = xsum + x;
ysum = ysum + y;
}
}
}
xcentroid = xsum / npix;
ycentroid = ysum / npix;
run("Select None");
doWand(xcentroid, ycentroid);
getSelectionCoordinates(endostx, endosty);
getRawStatistics(marrowarea);
run("Select None");
doWand(0, ycentroid);
getSelectionCoordinates(periostx, periosty);
getRawStatistics(totalarea);
fraccort = (totalarea - marrowarea) / totalarea;
close();
tmin = 999999999999;
tmax = 0;
tsum = 0;
tn = 0;
feret = 0;
for (p = 0; p tmax) tmax = dmin;
tsum = tsum + dmin;
tn++;
//work out Feret's diameter
for (q = p+1; q < lengthOf(periostx)-1; q++){
f = sqrt(sqr((periostx[q]-periostx[p])) + sqr((periosty[q]-periosty[p])));
if (f > feret) feret = f;
}
}
tav = tsum / tn;
}
function RotatingCalipers() {
selectWindow(title);
run("Duplicate...", "title="+getTitle()+"-calipers");
// setAutoThreshold();
if (isCalibrated) setThreshold(bonehu, maxhu);
else setThreshold((bonehu-caloffset)/calslope, (maxhu-caloffset)/calslope);
run("Make Binary");
run("Select None");
doWand(0, ycentroid);
if (selectionType()>-1) {
run("Convex Hull");
getSelectionCoordinates(perimx, perimy);
close();
ymin = 999999;
ymax = 0;
npoints = lengthOf(perimx);
for (n = 0; n < npoints; n++){
if (perimy[n] < ymin){
ymin = perimy[n];
podal = n; //n is always 0 in ImageJ, start point of selection is minimum x value within set with minimum y
}
if (perimy[n] > ymax){
ymax = perimy[n];
antipodal = n;
}
}
angles = newArray(npoints);
for (n = 0; n < npoints - 1; n++){
angles[n] = atan2(perimy[n]-perimy[n+1] , perimx[n]-perimx[n+1]);
}
angles[npoints-1] = atan2(perimy[npoints-1]-perimy[0] , perimx[npoints-1]-perimx[0]);
for (n=0; n 0) {
starttheta = endtheta;
} else {
if (angles[antipodal - 1] > 0) {
testangle = angles[antipodal - 1] - pi;
} else {
testangle = angles[antipodal - 1] + pi;
}
if (angles[npoints -1] <= testangle){
starttheta = angles[npoints - 1];
} else {
starttheta = testangle;
}
}
if (angles[antipodal] > 0) {
testangle = angles[antipodal] - pi;
} else {
testangle = angles[antipodal] + pi;
}
if (antipodal <= npoints - 1) {
if (angles[podal] > testangle && angles[podal] * testangle > 0) {
endtheta = angles[podal];
podincr = 1;
} else if (angles[podal] < testangle) {
endtheta = testangle;
antipodincr = 1;
} else if (angles[podal] > testangle && angles[podal] * testangle < 0) {
endtheta = testangle;
antipodincr = 1;
} else {
endtheta = angles[podal];
podincr = 1;
antipodincr = 1;
}
} else {
endtheta = 0;
}
thetah = atan2(perimy[antipodal] - perimy[podal] , perimx[antipodal] - perimx[podal]);
dp = sqrt(sqr((perimx[antipodal]-perimx[podal])) + sqr((perimy[antipodal]-perimy[podal])));
if (dp > rcmax) rcmax = dp;
if (starttheta >= endtheta){
ds = dp*abs(cos(-thetah + starttheta - pi/2));
de = dp*abs(cos(-thetah + endtheta - pi/2));
if (ds < rcmin) rcmin = ds;
else if (de < rcmin) rcmin = de;
}
else if (starttheta < endtheta && starttheta * endtheta < 0){
ds = dp*abs(cos(-thetah + starttheta - pi/2));
de = dp*abs(cos(-thetah + - 3*pi/2));
if (ds < rcmin) rcmin = ds;
else if (de < rcmin) rcmin = de;
ds = dp*abs(cos(-thetah + pi/2));
de = dp*abs(cos(-thetah + endtheta - pi/2));
if (ds < rcmin) rcmin = ds;
else if (de < rcmin) rcmin = de;
}
podal = podal + podincr;
antipodal = antipodal + antipodincr;
}} else close();
}
//DICOM meta-info handling code
// This function returns the numeric value of the
// specified tag (e.g., "0018,0050"). Returns NaN
// (not-a-number) if the tag is not found or it
// does not have a numeric value.
function getNumericTag(tag) {
value = getTag(tag);
if (value=="") return NaN;
index3 = indexOf(value, "\\");
if (index3 > 0) value = substring(value, 0, index3);
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;
}
//Attributions from http://www.hopkinsmedicine.org/FAE/MomentMacroJ_v1_3.txt:
// Written by: Matthew Warfel - Cornell University - 4/4/97
// Modified by: Stanley Serafin- Johns Hopkins University - 6/30/00
// Modified and adapted for ImageJ by: Valerie Burke DeLeon - 2/21/05
// Updates:
// 5/24/2005 - v1.2 added "DrawAxis" function modified from MomentMacro (VBD)
// 3/17/2006 - v1.2 renamed "neutral axes" to more correct term "principal axes" (VBD)
// 7/21/2006 - v1.3 replaced "/*" with "//" character to define initial comment lines, following reports of comments read as code (VBD)