/** Align_3D / Bone_3D_Moments
*tool to calculate centroid and principal axes
*of a thresholded stack; assumes a 16-bit CT scan
*of a bone in air - default thresholds are 0 and 4000 HU
*bone density assumed to be 1.8 g/cm^3
*Eigen decomposition performed with the Jama package
*http://math.nist.gov/javanumerics/jama/
*Outputs stack data aligned to principal axes
*
*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 .
*
*@author Michael Doube
*@version 0.2
*/
import java.awt.Rectangle;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.process.ImageProcessor;
import ij.plugin.filter.PlugInFilter;
import ij.measure.Calibration;
import ij.measure.ResultsTable;
import ij.gui.*;
import Jama.*;
public class Bone_3D_Moments implements PlugInFilter {
ImagePlus imp;
protected ImageStack stack;
public int setup(String arg, ImagePlus imp) {
stack = imp.getStack();
this.imp = imp;
return DOES_16 + STACK_REQUIRED + SUPPORTS_MASKING;
}
public void run(ImageProcessor ip) {
ResultsTable rt3D = ResultsTable.getResultsTable();
short minT = 0; //minimum bone value in HU
short maxT = 4000; //maximum bone value in HU
Calibration cal = imp.getCalibration();
double vW = cal.pixelWidth;
double vH = cal.pixelHeight;
double vD = cal.pixelDepth;
String units = cal.getUnits();
double[] coeff = cal.getCoefficients();
float[] CTable = cal.getCTable();
String valueUnit = cal.getValueUnit();
if (cal.calibrated()) {
//convert HU limits to pixel values
IJ.log("Image is calibrated, using "+minT+" and "+maxT+" "+valueUnit+" as bone cutoffs");
minT = (short)Math.round(cal.getRawValue(minT));
maxT = (short)Math.round(cal.getRawValue(maxT));
IJ.log("Vox Width: "+vW+"; Vox Height: "+vH+" Vox Depth: "+vD+" "+units);
IJ.log("Calibration coefficients:"+coeff[0]+","+coeff[1]);
IJ.log("minT = "+minT+", maxT = "+maxT);
}
else {
IJ.log("Image is not calibrated, using user-determined threshold");
IJ.run("Threshold...");
new WaitForUserDialog("This image is not density calibrated.\nSet the threshold, then click OK.").show();
minT = (short)ip.getMinThreshold();
maxT = (short)ip.getMaxThreshold();
}
String title = imp.getTitle();
int startSlice = 1;
int endSlice = stack.getSize();
GenericDialog gd = new GenericDialog("Limit Slices");
gd.addNumericField("Start Slice:",startSlice,0);
gd.addNumericField("End Slice:",endSlice,0);
gd.showDialog();
if (gd.wasCanceled()) {
IJ.error("PlugIn canceled!");
return;
}
startSlice = (int)gd.getNextNumber();
endSlice = (int)gd.getNextNumber();
int al = stack.getSize() + 1;
//2D centroids
double[] xc; double[] yc;
xc = new double[al]; yc = new double[al];
//3D centroids
double xcent = 0; double ycent = 0; double zcent = 0;
//pixel counters
double cstack = 0;
Rectangle r = ip.getRoi();
int offset, i;
int w = stack.getWidth();
int h = stack.getHeight();
int sliceSize = w*h;
boolean[] emptySlices;
emptySlices = new boolean[al];
short[] slicePixels = new short[sliceSize];
double[] cslice;
cslice = new double[al];
int sl = w*h*al;
int maxIndex = sl-1;
short[] sourceWorkArray = new short[sl];
for (int n = 0; n= minT && slicePixels[i] <= maxT){
cslice[s]++;
sumx += (double)x;
sumy += (double)y;
}
}
}
if (cslice[s] > 0){
xc[s] = sumx/cslice[s];
yc[s] = sumy/cslice[s];
xcent += xc[s]*cslice[s];
ycent += yc[s]*cslice[s];
zcent += cslice[s]*s;
cstack += cslice[s];
emptySlices[s] = false;
} else {
emptySlices[s] = true;
}
}
double centX3D; double centY3D; double centZ3D;
int centXV; int centYV; int centZV;
if (cstack > 0){
centX3D = xcent*vW/cstack; centY3D = ycent*vH/cstack; centZ3D = zcent*vD/cstack; //centroid in real units
centXV = (int)Math.round(xcent/cstack); centYV = (int)Math.round(ycent/cstack); centZV = (int)Math.round(zcent/cstack); //centroid in voxels
} else {
centX3D = -1; centY3D = -1; centZ3D = -1;
IJ.error("Empty Stack","No voxels are available for calculation.\nCheck your ROI and threshold.");
return;
}
//END OF CENTROID CALCULATION
//START OF 3D MOMENT CALCULATIONS
//Our CT scans are not quantitative as they contain sharpening artefact
//so density (g.cm^-3) is left out of this calculation and estimated at the end
double Icxx = 0; double Icyy = 0; double Iczz = 0;
double Icxy = 0; double Icxz = 0; double Icyz = 0;
for (int z=startSlice;z<=endSlice;z++) {
IJ.showStatus("Calculating inertia tensor...");
IJ.showProgress(z, endSlice);
if (!emptySlices[z]){
slicePixels = (short[])stack.getPixels(z);
for (int y=r.y; y<(r.y+r.height); y++) {
offset = y*w;
for (int x=r.x; x<(r.x+r.width); x++) {
i = offset + x;
if (slicePixels[i] >= minT && slicePixels[i] <= maxT){
//moments around the 3 orthogonal axes which are parallel to the
//reference frame's axes and whose intersection is the 3D centroid
Icxx += (y*vH-centY3D)*(y*vH-centY3D)+(z*vD-centZ3D)*(z*vD-centZ3D);
Icyy += (x*vW-centX3D)*(x*vW-centX3D)+(z*vD-centZ3D)*(z*vD-centZ3D);
Iczz += (y*vH-centY3D)*(y*vH-centY3D)+(x*vW-centX3D)*(x*vW-centX3D);
Icxy += (x*vW-centX3D)*(y*vH-centY3D);
Icxz += (x*vW-centX3D)*(z*vD-centZ3D);
Icyz += (y*vH-centY3D)*(z*vD-centZ3D);
}
}
}
}
}
//we have to multiply each element of the sum by the mass of the element
//which is vW*vH*vD * rho. Since all voxels are the same, we can
//multiply the sum by voxel volume
double voxelVolume = vW*vH*vD;
Icxx *= voxelVolume;
Icyy *= voxelVolume;
Iczz *= voxelVolume;
Icxy *= voxelVolume;
Icxz *= voxelVolume;
Icyz *= voxelVolume;
//add a term for the moment of each voxel around its COM, parallel to each axis
double shapeVolume = cstack*voxelVolume;
Icxx += shapeVolume*(vH*vH + vD*vD) / 12;
Icyy += shapeVolume*(vW*vW + vD*vD) / 12;
Iczz += shapeVolume*(vH*vH + vW*vW) / 12;
//then we multiply by density later to get the mass term
//create the intertia tensor matrix
double[][] inertiaTensor = new double[3][3];
inertiaTensor[0][0] = Icxx;
inertiaTensor[1][1] = Icyy;
inertiaTensor[2][2] = Iczz;
inertiaTensor[0][1] = -Icxy;
inertiaTensor[0][2] = -Icxz;
inertiaTensor[1][0] = -Icxy;
inertiaTensor[1][2] = -Icyz;
inertiaTensor[2][0] = -Icxz;
inertiaTensor[2][1] = -Icyz;
Matrix inertiaTensorMatrix = new Matrix(inertiaTensor);
//do the Eigenvalue decomposition
EigenvalueDecomposition E = new EigenvalueDecomposition(inertiaTensorMatrix);
Matrix eVal = E.getD();
Matrix eVec = E.getV();
double[][] eigenValues = eVal.getArrayCopy();
double[][] eigenVectors = eVec.getArrayCopy();
//check orientation of eigenvectors and correct them
if (eigenVectors[2][0] < 0){
// eVec = eVec.times(-1); //this changes coordinate frame from right-hand- to left-hand-rule
double[][] eVecA = eVec.getArray();
//rotate 180 deg
for (int row = 0; row < 3; row++){
for (int col = 0; col < 2; col++){
eVecA[row][col] *= -1;
}
}
eigenVectors = eVec.getArrayCopy();
}
Matrix eVecInv = eVec.inverse();
double[][] eigenVectorsInverse = eVecInv.getArrayCopy();
//sometimes the eigen vector is pointing 180 deg in the wrong direction - why? HOw to fix?
IJ.log("\nEigenvector matrix");
for (int j = 0; j<3; j++){
IJ.log("||"+eigenVectors[j][0]+" | "+eigenVectors[j][1]+" | "+eigenVectors[j][2]+"||");
}
IJ.log("\nInverse Eigenvector matrix");
for (int j = 0; j<3; j++){
IJ.log("||"+eigenVectorsInverse[j][0]+" | "+eigenVectorsInverse[j][1]+" | "+eigenVectorsInverse[j][2]+"||");
}
//cortical bone apparent density (material density * volume fraction) from Mow & Huiskes (2005) p.140
//using 1.8 g.cm^-3: 1mm^3 = 0.0018 g = 0.0000018 kg = 1.8*10^-6 kg; 1mm^2 = 10^-6 m^2
//conversion coefficient from mm^5 to kg.m^2 = 1.8*10^-12
double cc = 1.8*Math.pow(10, -12);
rt3D.incrementCounter();
rt3D.addLabel("Label", imp.getTitle());
rt3D.addValue("Xc ("+units+")", centX3D);
rt3D.addValue("Yc ("+units+")", centY3D);
rt3D.addValue("Zc ("+units+")", centZ3D);
rt3D.addValue("Vol (mm^3)", shapeVolume);
rt3D.addValue("Icxx (kg.m^2)", Icxx*cc);
rt3D.addValue("Icyy (kg.m^2)", Icyy*cc);
rt3D.addValue("Iczz (kg.m^2)", Iczz*cc);
rt3D.addValue("Icxy (kg.m^2)", Icxy*cc);
rt3D.addValue("Icxz (kg.m^2)", Icxz*cc);
rt3D.addValue("Icyz (kg.m^2)", Icyz*cc);
rt3D.addValue("I1 (kg.m^2)", eigenValues[2][2]*cc);
rt3D.addValue("I2 (kg.m^2)", eigenValues[1][1]*cc);
rt3D.addValue("I3 (kg.m^2)", eigenValues[0][0]*cc);
rt3D.show("Results");
//for each voxel in the target stack, find the corresponding source voxel
for (int z=1; z