From bd15f84c17c010d1814e110e9eca4038bbbe8b60 Mon Sep 17 00:00:00 2001
From: Mark Jenkinson <mark@fmrib.ox.ac.uk>
Date: Wed, 17 Nov 2004 10:48:31 +0000
Subject: [PATCH] Apparently working version of code for Geoff, although
 specific numerical testing has not been done

---
 Makefile      |   8 +-
 cylsamp.cc    |  89 ++++++------
 groupttest.cc | 389 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 443 insertions(+), 43 deletions(-)
 create mode 100644 groupttest.cc

diff --git a/Makefile b/Makefile
index 3a63bd4..983a0a0 100644
--- a/Makefile
+++ b/Makefile
@@ -6,12 +6,13 @@ USRINCFLAGS = -I${INC_NEWMAT} -I${INC_ZLIB}
 USRLDFLAGS = -L${LIB_NEWMAT} -L${LIB_ZLIB}
 
 LIBS = -lss_32R -lfslio -lniftiio -lznz -lm -lz
-LIBCC = -lnewimage -lmiscmaths -lfslio -lniftiio -lznz -lnewmat -lutils -lm -lz
+LIBCC = -lnewimage -lmiscmaths -lprob -lfslio -lniftiio -lznz -lnewmat -lutils -lm -lz
 
 SN_OBJS = surface_norm.o
 CS_OBJS = cylsamp.o
+GT_OBJS = groupttest.o
 
-XFILES = siena_diff surface_norm cylsamp
+XFILES = siena_diff surface_norm cylsamp groupttest
 
 SCRIPTS = siena siena_flirt siena_cal sienax siena_flow2tal
 
@@ -30,6 +31,9 @@ surface_norm: ${SN_OBJS}
 cylsamp: ${CS_OBJS}
 	${CXX} ${CXXFLAGS} ${MJOPTFLAGS} ${LDFLAGS} -o $@ ${CS_OBJS} ${LIBCC}
 
+groupttest: ${GT_OBJS}
+	${CXX} ${CXXFLAGS} ${MJOPTFLAGS} ${LDFLAGS} -o $@ ${GT_OBJS} ${LIBCC}
+
 docscripts:
 	./makedoc
 
diff --git a/cylsamp.cc b/cylsamp.cc
index f4acc23..d352ab8 100644
--- a/cylsamp.cc
+++ b/cylsamp.cc
@@ -45,11 +45,8 @@ Option<float>  radius(string("-r"),5.0,
 		      string("radius of cylinder in mm (default is 5.0)"),
 		      false, requires_argument);
 Option<float>  height(string("-h"),10.0,
-		      string("height of cylinder in mm (default is 10.0)"),
+		      string("half-height of cylinder in mm (default is 10.0)"),
 		      false, requires_argument);
-Option<string> maskname(string("-m"), string(""),
-		      string("input mask image filename"),
-		      true, requires_argument);
 Option<string> normname(string("-n"), string(""),
 		      string("input surface normal image filename"),
 		      true, requires_argument);
@@ -68,61 +65,72 @@ int nonoptarg;
 
 // Local functions
 
+void zeronans(volume<float>& vol) 
+{
+  for (int z=vol.minz(); z<=vol.maxz(); z++) {
+    for (int y=vol.miny(); y<=vol.maxy(); y++) {
+      for (int x=vol.minx(); x<=vol.maxx(); x++) {
+	if (isnan(vol.value(x,y,z))) {
+	  vol(x,y,z)=0;
+	}
+      }
+    }
+  }
+}
+
+
+void zeronans(volume4D<float>& vol)
+{
+  for (int t=vol.mint(); t<=vol.maxt(); t++) {
+    zeronans(vol[t]);
+  }
+}
+
+
 int do_work(int argc, char* argv[]) 
 {
-  volume<float> vflow, vmask, vedgemask, vsamp;
+  volume<float> vflow, vedgemask, vsamp;
   volume4D<float> snorm;
   read_volume(vflow,flowname.value());
-  read_volume(vmask,maskname.value());
   read_volume(vedgemask,edgemaskname.value());
   read_volume4D(snorm,normname.value());
+  zeronans(vflow);
+  zeronans(vedgemask);
+  zeronans(snorm);
 
-  // force input to be a binary mask
-  if (verbose.value()) print_info(vmask,"vmask");
-  vsamp = vmask;
+  // set up output image
+  if (verbose.value()) print_info(vflow,"vflow");
+  vsamp = vflow;
 
   float r=radius.value();
   float r2 = r*r;
   float h=height.value();
   int len=(int) (sqrt(r*r + h*h)) + 1;
+  int lenx = ceil(len/vsamp.xdim());
+  int leny = ceil(len/vsamp.ydim());
+  int lenz = ceil(len/vsamp.zdim());
 
-  bool atedge;
   if (verbose.value()) { cerr << "Performing Cylindrical Sampling" << endl; }
-  for (int z=vmask.minz(); z<=vmask.maxz(); z++) {
-    for (int y=vmask.miny(); y<=vmask.maxy(); y++) {
-      for (int x=vmask.minx(); x<=vmask.maxx(); x++) {
-	atedge = false;
-	// check to see if it is an edge point
-	if ( (vmask(x,y,z)>0.5) ) {
-	  if (vmask(x,y,z-1)<0.5) atedge=true;
-	  else { 
-	    if (vmask(x,y-1,z)<0.5) atedge=true;
-	    else { 
-	      if (vmask(x-1,y,z)<0.5) atedge=true;
-	      else {
-		if (vmask(x+1,y,z)<0.5) atedge=true;
-		else {
-		  if (vmask(x,y+1,z)<0.5) atedge=true;
-		  else {
-		    if (vmask(x,y,z+1)<0.5) atedge=true;
-		  }
-		}
-	      }
-	    }
-	  }
-	}
-	if (atedge) {
+  for (int z=vsamp.minz(); z<=vsamp.maxz(); z++) {
+    for (int y=vsamp.miny(); y<=vsamp.maxy(); y++) {
+      for (int x=vsamp.minx(); x<=vsamp.maxx(); x++) {
+	// check to see if it is an edge point (where snorm != 0)
+	float vx, vy, vz;
+	vx = snorm(x,y,z,0);
+	vy = snorm(x,y,z,1);
+	vz = snorm(x,y,z,2);
+	if ( (fabs(vx)>1e-5) || (fabs(vy)>1e-5) || (fabs(vz)>1e-5) ) {
 	  // OK, now do the cylindrical sampling
 	  float tot=0.0;
 	  int num=0;
-	  for (int z1=Max(z-len,vmask.minz()); z1<=Min(z+len,vmask.maxz()); z1++) {
-	    for (int y1=Max(y-len,vmask.miny()); y1<=Min(y+len,vmask.maxy()); y1++) {
-	      for (int x1=Max(x-len,vmask.minx()); x1<=Min(x+len,vmask.maxx()); x1++) {
+	  for (int z1=Max(z-lenz,vsamp.minz()); z1<=Min(z+lenz,vsamp.maxz()); z1++) {
+	    for (int y1=Max(y-leny,vsamp.miny()); y1<=Min(y+leny,vsamp.maxy()); y1++) {
+	      for (int x1=Max(x-lenx,vsamp.minx()); x1<=Min(x+lenx,vsamp.maxx()); x1++) {
 		if (vedgemask(x1,y1,z1)>0.5) {
-		  float yy = snorm(x1,y1,z1,0) * (x1 - x) 
-		    + snorm(x1,y1,z1,1) * (y1 - y)  
-		    + snorm(x1,y1,z1,2) * (z1 - z);
+		  // yy is the projection of vector onto major axis of cylinder
+		  float yy = vx * (x1 - x) + vy * (y1 - y) + vz * (z1 - z);
 		  if (fabs(yy)<=h) {
+		    // xx2 is the square radius of the planar component of the vector
 		    float xx2 = ( Sqr(x1-x) + Sqr(y1-y) + Sqr(z1-z) ) - Sqr(yy);
 		    if (xx2 <= r2) {
 		      // inside cylinder
@@ -163,7 +171,6 @@ int main(int argc,char *argv[])
     //  the help message is printed)
     options.add(flowname);
     options.add(edgemaskname);
-    options.add(maskname);
     options.add(normname);
     options.add(outname);
     options.add(radius);
diff --git a/groupttest.cc b/groupttest.cc
new file mode 100644
index 0000000..ddb69e1
--- /dev/null
+++ b/groupttest.cc
@@ -0,0 +1,389 @@
+/*  groupttest.cc
+
+    Mark Jenkinson, FMRIB Image Analysis Group
+    Ana Juric, Mental Health Research Institute,
+       Centre for Neuroscience, University of Melbourne
+
+    Copyright (C) 2004 University of Oxford  */
+
+/*  CCOPYRIGHT  */
+
+// Calculates the surface normals for a mask, using a smoothed
+//  gradient calculation (all non-surface points get zero ouput)
+
+#define _GNU_SOURCE 1
+#define POSIX_SOURCE 1
+
+#include <vector>
+#include <algorithm>
+#include "newimage/newimageall.h"
+#include "miscmaths/miscmaths.h"
+#include "utils/options.h"
+#include "miscmaths/t2z.h"
+
+using namespace MISCMATHS;
+using namespace NEWIMAGE;
+using namespace Utilities;
+
+// The two strings below specify the title and example usage that is
+//  printed out as the help or usage message
+
+string title="groupttest (Version 1.1)\nCopyright(c) 2004, University of Oxford (Mark Jenkinson)";
+string examples="groupttest --na=<number in group A> --nb=<number in group B> -m <maskvol> -o <groupres> [options] <list of images for group A> <list of images for group B>\ne.g.   groupttest --na=15 --nb=15 -m maskvol -o groupres groupA/*.hdr* groupB/*.hdr*";
+
+// Each (global) object below specificies as option and can be accessed
+//  anywhere in this file (since they are global).  The order of the
+//  arguments needed is: name(s) of option, default value, help message,
+//       whether it is compulsory, whether it requires arguments
+// Note that they must also be included in the main() function or they
+//  will not be active.
+
+Option<bool> verbose(string("-v,--verbose"), false, 
+		     string("switch on diagnostic messages"), 
+		     false, no_argument);
+Option<bool> help(string("-h,--help"), false,
+		  string("display this message"),
+		  false, no_argument);
+Option<bool> conservativetest(string("--conservative"), false,
+			      string("use conservative FDR correction factor"),
+			      false, no_argument);
+Option<int>  numa(string("--na"),0,
+		  string("number of members of group A (normals)"),
+		  true, requires_argument);
+Option<int>  numb(string("--nb"),0,
+		  string("number of members of group B (patients)"),
+		  true, requires_argument);
+Option<string> ordername(string("--order"), string(""),
+		      string("~\toutput image of order values"),
+		      false, requires_argument);
+Option<string> maskname(string("-m"), string(""),
+		      string("input mask filename"),
+		      true, requires_argument);
+Option<string> outname(string("-o"), string(""),
+		       string("output base filename"),
+		       true, requires_argument);
+int nonoptarg;
+
+////////////////////////////////////////////////////////////////////////////
+
+// Support functions
+
+int save_as_image(const string& filename, const volume<float>& mask, 
+		  const Matrix& valmat)
+{
+    // put values back into volume format
+    if (verbose.value()) { cerr << "Saving results to " << filename << endl; }
+    volume4D<float> outvals;
+    outvals.addvolume(mask);
+    outvals.setmatrix(valmat.t(),mask);
+    return save_volume4D(outvals,filename);
+}
+
+Matrix get_coord_matrix(const volume<float>& mask)
+{
+  // construct a matrix of index values 1 -> Ntot
+  volume4D<float> outvals;
+  outvals.addvolume(mask);
+  Matrix index = outvals.matrix(mask);
+  int Ntot = index.Ncols();
+  for (int j=1; j<=Ntot; j++) {
+    index(1,j) = j;
+  }
+  outvals.setmatrix(index,mask);
+  // go through volume and set up new matrix *with coordinates*
+  Matrix coords(Ntot,3);
+  for (int z=mask.minz(); z<=mask.maxz(); z++) {
+    for (int y=mask.miny(); y<=mask.maxy(); y++) {
+      for (int x=mask.minx(); x<=mask.maxx(); x++) {
+	if (mask(x,y,z)>0.5) {
+	  int idx = MISCMATHS::round(outvals(x,y,z,0));
+	  coords(idx,1) = x;
+	  coords(idx,2) = y;
+	  coords(idx,3) = z;
+	}
+      }
+    }
+  }
+  return coords;
+}
+
+
+volume<float> calc_edge_mask(const volume<float>& vmask)
+{
+  volume<float> vtmp = vmask;
+  bool atedge;
+  if (verbose.value()) { cerr << "Extracting Edge Voxels" << endl; }
+  for (int z=vmask.minz(); z<=vmask.maxz(); z++) {
+    for (int y=vmask.miny(); y<=vmask.maxy(); y++) {
+      for (int x=vmask.minx(); x<=vmask.maxx(); x++) {
+	atedge = false;
+	if ( (vmask(x,y,z)>0.5) ) {
+	  if (vmask(x,y,z-1)<0.5) atedge=true;
+	  else { 
+	    if (vmask(x,y-1,z)<0.5) atedge=true;
+	    else { 
+	      if (vmask(x-1,y,z)<0.5) atedge=true;
+	      else {
+		if (vmask(x+1,y,z)<0.5) atedge=true;
+		else {
+		  if (vmask(x,y+1,z)<0.5) atedge=true;
+		  else {
+		    if (vmask(x,y,z+1)<0.5) atedge=true;
+		  }
+		}
+	      }
+	    }
+	  }
+	}
+	if (atedge) {
+	  vtmp(x,y,z)=1;
+	} else {
+	  vtmp(x,y,z)=0;
+	}
+      }
+    }
+  }
+  return vtmp;
+}
+
+ 
+//Function written by Ana Juric 
+// Gentleman and Jenkins approximation for the t-distribution p-values (Biometrika, 55(3), p 571, 1968)
+//  NB: gives coefficents (c1,..,c5) for:
+//    p(|t|<X) = 1 - (c5*X^5 + c4*X^4 + c3*X^3 + c2*X^2 + c1*X + 1)^(-8)
+
+double tTesting(double degreesOfFreedom, int coefficientNum)
+{
+        double coefficientMatrix[5][7]={
+                {0.09979441, -0.5818210, 1.390993, -1.222452, 2.151185, -5.537409, 11.42343},
+                {0.04431742,-0.2206018, -0.03317253, 5.679969, -12.96519, -5.166733, 13.49862},
+                {0.009694901, -0.1408854, 1.889930, -12.75532, 25.77532, -4.233736, 14.39630},
+                {-0.00009187228, 0.03789901, -1.280346, 9.249528, -19.08115, -2.777816, 16.46132},
+                {0.0005796020, -0.02763334, 0.4517029, -2.657697, 5.127212, -0.5657187, 21.83269} };
+ 
+        double coefficient;
+ 
+	double v=degreesOfFreedom;
+	double c6, c5, c4, c3, c2, c1, c0;
+	c6 = coefficientMatrix[coefficientNum][6];
+	c5 = coefficientMatrix[coefficientNum][5];
+	c4 = coefficientMatrix[coefficientNum][4];
+	c3 = coefficientMatrix[coefficientNum][3];
+	c2 = coefficientMatrix[coefficientNum][2];
+	c1 = coefficientMatrix[coefficientNum][1];
+	c0 = coefficientMatrix[coefficientNum][0];
+
+	// old version
+	/*
+        coefficient=
+                (((coefficientMatrix[coefficientNum][4]*(pow(degreesOfFreedom,(-4))))+
+                  (coefficientMatrix[coefficientNum][3]*(pow(degreesOfFreedom,(-3))))+
+                  (coefficientMatrix[coefficientNum][2]*(pow(degreesOfFreedom,(-2))))+
+                  (coefficientMatrix[coefficientNum][1]*(pow(degreesOfFreedom,(-1))))+
+                  (coefficientMatrix[coefficientNum][0]))
+                /((coefficientMatrix[coefficientNum][6]*(pow(degreesOfFreedom,(-2))))+
+                  (coefficientMatrix[coefficientNum][5]*(pow(degreesOfFreedom,(-1))))+1));
+	*/ 
+
+	// new version - note that both denom & numerator are multiplied by v^4 in order to
+	//  have positive powers of v only (not v^(-4), etc.)
+	coefficient = (c4 + v*(c3 + v*(c2 + v*(c1 + v*c0)))) 
+	  / (v*v*(c6 + v*(c5 + v)));
+        return coefficient;
+}
+
+
+double pvalue(double tX, double dof) {
+  // return the ONE SIDED t-test p-values: p(t>X)
+  //    based on the two-sided formula:
+  //    p(|t|>X) = (c5*X^5 + c4*X^4 + c3*X^3 + c2*X^2 + c1*X + 1)^(-8)
+  double p1, p, x;
+  // Code fragment by Ana Juric
+  // "initialises the coeffMatrix with the relevent values"
+  double c[5];
+  for(int anaj=0; anaj<5; anaj++) { c[anaj]=tTesting(dof,anaj); } 
+
+  x = fabs(tX);
+  p =  pow((1 + x*(c[0] + x*(c[1] + x*(c[2] + x*(c[3] + x*c[4]))))),-8.0);
+  if (tX>0) {
+    p1 = p/2.0;
+  } else {
+    p1 = 1 - p/2.0;
+  }
+  return p1;
+} 
+
+
+vector<int> get_sortindex(const Matrix& vals)
+{
+  // return the mapping of old indices to new indices in the
+  //   new *ascending* sort of vals
+  int length=vals.Nrows();
+  vector<pair<double, int> > sortlist(length);
+  for (int n=0; n<length; n++) {
+    sortlist[n] = pair<double, int>((double) vals(n+1,1),n+1);
+  }
+  sort(sortlist.begin(),sortlist.end());  // O(N.log(N))
+  vector<int> idx(length);
+  for (int n=0; n<length; n++) {
+    idx[n] = sortlist[n].second;
+  }
+  return idx;
+}
+
+
+////////////////////////////////////////////////////////////////////////////
+
+// Main function - this does all the work
+
+int do_work(int argc, char* argv[], int nonoptarg) 
+{
+  string basename = fslbasename(outname.value());
+
+  volume<float> vmask, vtmp;
+  read_volume(vmask,maskname.value());
+  if (verbose.value()) print_info(vmask,"vmask");
+  vmask = calc_edge_mask(vmask);
+
+  // get ready to read in flow images
+  int Ntot = MISCMATHS::round(vmask.sum());
+  int N1=numa.value();
+  int N2=numb.value();
+  if (verbose.value()) { cerr << "Ntot = " << Ntot << " ; Na,Nb = " << N1 << " , " << N2 << endl; }
+  Matrix newcol(Ntot,1), bigmatrix(Ntot,N1+N2), tvalmat(Ntot,1);
+  Matrix pmat(Ntot,1), logqmat(Ntot,1);
+  Matrix meana(Ntot,1), meanb(Ntot,1);
+
+  // read in images and accumulate values into bigmatrix
+  for (int n=1; n<=(N1+N2); n++) {
+    volume4D<float> vstat;
+    string filename = argv[nonoptarg + n - 1];
+    if (verbose.value()) { cerr << "Reading file " << filename << endl; }
+    read_volume4D(vstat,filename);
+    if (verbose.value()) { print_info(vstat,"vstat"); }
+    newcol = vstat.matrix(vmask);
+    for (int j=1; j<=Ntot; j++) {
+      bigmatrix(j,n) = newcol(1,j);
+    }
+  }
+
+  // Calculate t-values and relevant means and standard deviations
+  for (int j=1; j<=Ntot; j++) {
+    double sumx1=0, sumx2=0, sumx1sq=0, sumx2sq=0, meanx1=0, meanx2=0, sdx1=0, sdx2=0;
+    for (int m=1; m<=N1; m++) {
+      double x1 = bigmatrix(j,m);
+      sumx1 += x1;
+      sumx1sq += x1*x1;
+    }
+    for (int m=N1+1; m<=(N1+N2); m++) {
+      double x2 = bigmatrix(j,m);
+      sumx2 += x2;
+      sumx2sq += x2*x2;
+    }
+    meanx1 = sumx1 / N1;
+    meanx2 = sumx2 / N2;
+    meana(j,1) = meanx1;
+    meanb(j,1) = meanx2;
+    sdx1 = sqrt(sumx1sq/N1 - meanx1*meanx1);
+    sdx2 = sqrt(sumx2sq/N2 - meanx2*meanx2);
+    double sediff = sqrt(sdx1*sdx1 / N1 + sdx2*sdx2 / N2);
+    double tval = (meanx1 - meanx2)/sediff;
+    // Welch's degree of freedom (for unequal variances)
+    double dof = Sqr(sdx1*sdx1/N1 + sdx2*sdx2/N2) / 
+      ( Sqr(sdx1*sdx1/N1)/(N1-1) + Sqr(sdx2*sdx2/N2)/(N2-1) );
+    // store t value
+    tvalmat(j,1) = tval;
+    // convert t values to p values
+    pmat(j,1) = pvalue(tval,dof);
+  }
+
+  // save the image results
+  save_as_image(basename+"_meanA",vmask,meana);
+  save_as_image(basename+"_meanB",vmask,meanb);
+  save_as_image(basename+"_tvals",vmask,tvalmat);
+  save_as_image(basename+"_pvals",vmask,pmat);
+  // save the matrix result (coords + group means)
+  {
+    Matrix coords = get_coord_matrix(vmask);
+    Matrix save_result = ( coords | meana ) | meanb ;
+    write_ascii_matrix(save_result,basename+"_matrix");
+  }
+
+  // calculate FDR threshold required to make each voxel significant
+  // FDR formula is: p = n*q / (N * C)  
+  //   where n=order index, N=total number of p values, 
+  //         C=1 for the simple case, and 
+  //         C=1/1 + 1/2 + 1/3 + ... + 1/N for the most general correlation
+  // We use the inverse formula: q_{min} = N*C*p / n
+
+  if (verbose.value()) { cerr << "Calculating FDR values" << endl; } 
+  float C=1.0;
+  if (conservativetest.value()) {
+    for (int n=2; n<=Ntot; n++) { C+=1.0/((double) n); }
+  }
+
+  vector<int> norder = get_sortindex(pmat);
+  
+  for (int j=1; j<=Ntot; j++) {
+    double qval = pmat(j,1) * Ntot * C / norder[j-1];
+    // qval isn't a probability - it can be greater than 1, but don't show these
+    if (qval>1.0) qval=1.0;
+    logqmat(j,1) = -log10(qval);
+  }
+  
+  // save the FDR log(q_min) results
+  save_as_image(basename+"_qvals",vmask,logqmat);
+
+  // save the order values, if requested
+  if (ordername.set()) {
+    Matrix ordermat(Ntot,1);
+    for (int j=1; j<=Ntot; j++) { ordermat(j,1) = norder[j-1]; }
+    save_as_image(ordername.value(),vmask,ordermat);
+  }
+
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////
+
+int main(int argc,char *argv[])
+{
+
+  Tracer tr("main");
+  OptionParser options(title, examples);
+
+  try {
+    // must include all wanted options here (the order determines how
+    //  the help message is printed)
+    options.add(maskname);
+    options.add(outname);
+    options.add(numa);
+    options.add(numb);
+    options.add(ordername);
+    options.add(conservativetest);
+    options.add(verbose);
+    options.add(help);
+    
+    nonoptarg = options.parse_command_line(argc, argv);
+
+    // line below stops the program if the help was requested or 
+    //  a compulsory option was not set
+    if ( (help.value()) || (!options.check_compulsory_arguments(true)) )
+      {
+	options.usage();
+	exit(EXIT_FAILURE);
+      }
+    
+  }  catch(X_OptionError& e) {
+    options.usage();
+    cerr << endl << e.what() << endl;
+    exit(EXIT_FAILURE);
+  } catch(std::exception &e) {
+    cerr << e.what() << endl;
+  } 
+
+  // Call the local functions
+
+  return do_work(argc,argv,nonoptarg);
+}
+
-- 
GitLab