//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)