Skip to content
Snippets Groups Projects
groupttest.cc 12.3 KiB
Newer Older
/*  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;
} 


Mark Jenkinson's avatar
Mark Jenkinson committed

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++) {
Mark Jenkinson's avatar
Mark Jenkinson committed
    idx[sortlist[n].second-1] = n+1;
  }
  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);
}