From 84f782e8d23cb95569ac24098515949809d4d855 Mon Sep 17 00:00:00 2001 From: Taylor Hanayik <hanayik@gmail.com> Date: Fri, 16 Jun 2023 11:11:33 +0100 Subject: [PATCH 1/6] add --json option and associated logic --- .gitignore | 1 + fslstats.cc | 1263 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 867 insertions(+), 397 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/fslstats.cc b/fslstats.cc index 03514a9..83a9873 100644 --- a/fslstats.cc +++ b/fslstats.cc @@ -15,16 +15,18 @@ #include "newimage/newimageall.h" #include "newimage/costfns.h" - using namespace std; using namespace NEWMAT; using namespace NEWIMAGE; -int print_usage(const string& progname) { - cout << "Usage: fslstats [preoptions] <input> [options]" << endl << endl; +int print_usage(const string &progname) +{ + cout << "Usage: fslstats [preoptions] <input> [options]" << endl + << endl; cout << "preoption -t will give a separate output line for each 3D volume of a 4D timeseries" << endl; cout << "preoption -K < indexMask > will generate seperate n submasks from indexMask, for indexvalues 1..n where n is the maximum index value in indexMask, and generate statistics for each submask" << endl; - cout << "Note - options are applied in order, e.g. -M -l 10 -M will report the non-zero mean, apply a threshold and then report the new nonzero mean" << endl << endl; + cout << "Note - options are applied in order, e.g. -M -l 10 -M will report the non-zero mean, apply a threshold and then report the new nonzero mean" << endl + << endl; cout << "-l <lthresh> : set lower threshold" << endl; cout << "-u <uthresh> : set upper threshold" << endl; cout << "-r : output <robust min intensity> <robust max intensity>" << endl; @@ -44,12 +46,16 @@ int print_usage(const string& progname) { cout << "-C : output centre-of-gravity (cog) in voxel coordinates" << endl; cout << "-p <n> : output nth percentile (n between 0 and 100)" << endl; cout << "-P <n> : output nth percentile (for nonzero voxels)" << endl; - cout << "-a : use absolute values of all image intensities"<< endl; + cout << "-a : use absolute values of all image intensities" << endl; cout << "-n : treat NaN or Inf as zero for subsequent stats" << endl; cout << "-k <mask> : use the specified image (filename) for masking - overrides lower and upper thresholds" << endl; cout << "-d <image> : take the difference between the base image and the image specified here" << endl; cout << "-h <nbins> : output a histogram (for the thresholded/masked voxels only) with nbins" << endl; - cout << "-H <nbins> <min> <max> : output a histogram (for the thresholded/masked voxels only) with nbins and histogram limits of min and max" << endl << endl; + cout << "-H <nbins> <min> <max> : output a histogram (for the thresholded/masked voxels only) with nbins and histogram limits of min and max" << endl; + // add --json output format option + // keys will correspond to the options above + cout << "--json : output in JSON format" << endl + << endl; cout << "Note - thresholds are not inclusive ie lthresh<allowed<uthresh" << endl; return 1; } @@ -57,461 +63,924 @@ int print_usage(const string& progname) { // Some specialised nonzero functions just for speedup // (it avoids generating masks when not absolutely necessary) -long nonzerocount(const volume4D<float>& vol) +long nonzerocount(const volume4D<float> &vol) { - long totn=0; - for(volume4D<float>::fast_const_iterator it=vol.fbegin(), end=vol.fend(); it<end; it++ ) - if ( (*it) != 0 ) + long totn = 0; + for (volume4D<float>::fast_const_iterator it = vol.fbegin(), end = vol.fend(); it < end; it++) + if ((*it) != 0) totn++; return totn; } -double nonzeromean(const volume4D<float>& vol) +double nonzeromean(const volume4D<float> &vol) { - double total=0.0; - long int totn=0; - for(volume4D<float>::fast_const_iterator it=vol.fbegin(), end=vol.fend(); it<end; it++ ) - if ( (*it) != 0 ) { - total+=(double)(*it); + double total = 0.0; + long int totn = 0; + for (volume4D<float>::fast_const_iterator it = vol.fbegin(), end = vol.fend(); it < end; it++) + if ((*it) != 0) + { + total += (double)(*it); totn++; } - if (totn>0) - total/=totn; + if (totn > 0) + total /= totn; return total; } -double nonzerostddev(const volume4D<float>& vol) +double nonzerostddev(const volume4D<float> &vol) { - double totv=0.0, totvv=0.0; - long int totn=0; - for(volume4D<float>::fast_const_iterator it=vol.fbegin(), end=vol.fend(); it<end; it++ ) - if ( (*it) != 0 ) { - totvv+=(double)((*it)*(*it)); - totv+=(double)(*it); + double totv = 0.0, totvv = 0.0; + long int totn = 0; + for (volume4D<float>::fast_const_iterator it = vol.fbegin(), end = vol.fend(); it < end; it++) + if ((*it) != 0) + { + totvv += (double)((*it) * (*it)); + totv += (double)(*it); totn++; } - double var=0.0; - if (totn>1) { - double meanval = totv/totn; - var = (totvv - totn*meanval*meanval)/(totn-1); + double var = 0.0; + if (totn > 1) + { + double meanval = totv / totn; + var = (totvv - totn * meanval * meanval) / (totn - 1); } return std::sqrt(var); } -template<class M> +template <class M> int generateNonZeroMask(const M &mask, volume4D<float> &masknz, const volume4D<float> &input) { - masknz = binarise(input,0.0f,0.0f,inclusive,true)*mask; + masknz = binarise(input, 0.0f, 0.0f, inclusive, true) * mask; return 0; } -int generate_masks(volume4D<float>& mask, volume4D<float>& masknz, const volume4D<float>& input, const float& lthr, const float& uthr) +int generate_masks(volume4D<float> &mask, volume4D<float> &masknz, const volume4D<float> &input, const float <hr, const float &uthr) +{ + mask = binarise(input, lthr, uthr, exclusive); + return generateNonZeroMask(mask, masknz, input); +} + +// print function that can handle outputting in JSON format or not +void print_value(const string &key, const string &value, string &json, const bool jsonOutput) { - mask = binarise(input,lthr,uthr,exclusive); - return generateNonZeroMask(mask,masknz,input); + // append the key and value to the json string + if (jsonOutput) + { + json.append("\"" + key + "\": \"" + value + "\", "); + } + else + { + cout << value << " "; + } } -int fmrib_main_float(int argc, char* argv[],const bool timeseriesMode, const string& indexMaskName) + +int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const string &indexMaskName) { + // determine if the user wants to output in JSON format + bool jsonOutput = false; + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "--json") == 0) + { + jsonOutput = true; + break; + } + } + // if JSON output is requested, then we need to store the output as a string + // and then output it at the end via cout + // don't forget to close the json at the end with a "}" + string json = ""; + cout.setf(ios::dec); cout.setf(ios::fixed, ios::floatfield); cout.setf(ios::left, ios::adjustfield); cout.precision(6); volume<float> vol, inputMaster; volume<int> indexMask; - if ( timeseriesMode || indexMaskName != "" ) - read_volume4D(inputMaster,argv[1]); + if (timeseriesMode || indexMaskName != "") + read_volume4D(inputMaster, argv[1]); else - read_volume4D(vol,argv[1]); - volume<float> & indexMaster = (timeseriesMode ) ? vol : inputMaster; - if ( indexMaskName != "" ) - read_volume4D(indexMask,indexMaskName); + read_volume4D(vol, argv[1]); + volume<float> &indexMaster = (timeseriesMode) ? vol : inputMaster; + if (indexMaskName != "") + read_volume4D(indexMask, indexMaskName); int nTimepoints((timeseriesMode) ? inputMaster.tsize() : 1), nIndices((indexMaskName != "") ? indexMask.max() : 1); - for ( int timepoint=0; timepoint < nTimepoints ; timepoint++ ) { - for ( int index=1; index <= nIndices; index++ ) { - if ( timeseriesMode ) - vol=inputMaster[timepoint]; - volume<float> mask, masknz; - float lthr(-numeric_limits<float>::max()); - float uthr(numeric_limits<float>::max()); - if ( indexMask.nvoxels() ) { - if ( indexMask.dimensionality() > 3 ) { - cerr << "Index mask must be 3D" << endl; - return 3; - } - copyconvert(indexMask,mask); - mask.binarise(index-1,index+1,exclusive); - vol=indexMaster*mask; - if (mask.max() < 1) { - cout << "missing label: " << index << " in mask" << endl; - continue; - } - generateNonZeroMask(mask,masknz,vol); - } - int narg(2); - - while (narg<argc) { - string sarg(argv[narg]); - if (sarg=="-n") { - for (int t=0; t<vol.tsize(); t++) - for (int z=0; z<vol.zsize(); z++) - for (int y=0; y<vol.ysize(); y++) - for (int x=0; x<vol.xsize(); x++) - if (!isfinite((double)vol(x,y,z,t))) - vol(x,y,z,t)=0; - } else if (sarg=="-m") { - if (mask.nvoxels()>0) cout << vol.mean(mask) << " "; - else cout << vol.mean() << " "; - } else if (sarg=="-M") { - if (masknz.nvoxels()>0) cout << vol.mean(masknz) << " "; - else { - double nzmean=0; - nzmean = nonzeromean(vol); - cout << nzmean << " "; - } - } else if (sarg=="-X") { - ColumnVector coord(4); - vector<int64_t> iCoords; - vol.min(mask,iCoords); - coord << (Real)iCoords[7] << (Real)iCoords[8] << (Real)iCoords[9] << 1.0; - coord = vol.niftivox2newimagevox_mat().i() * coord; - cout << MISCMATHS::round(coord(1)) << " " << - MISCMATHS::round(coord(2)) << " " << MISCMATHS::round(coord(3)) << " "; - } else if (sarg=="-x") { - ColumnVector coord(4); - vector<int64_t> iCoords; - vol.min(mask,iCoords); - coord << (Real)iCoords[0] << (Real)iCoords[1] << (Real)iCoords[2] << 1.0; - coord = vol.niftivox2newimagevox_mat().i() * coord; - cout << MISCMATHS::round(coord(1)) << " " << - MISCMATHS::round(coord(2)) << " " << MISCMATHS::round(coord(3)) << " "; - } else if (sarg=="-w") { - if (masknz.nvoxels()<1) { //Need to generate non-zeromask - generate_masks(mask,masknz,vol,lthr,uthr); - vol*=mask; - } - int xmin=masknz.xsize()-1,xmax(0),ymin=masknz.ysize()-1,ymax(0),zmin=masknz.zsize()-1,zmax(0),tmin=masknz.tsize()-1,tmax(0); - for(int t=0;t<masknz.tsize();t++) - for(int z=0;z<masknz.zsize();z++) - for(int y=0;y<masknz.ysize();y++) - for(int x=0;x<masknz.xsize();x++) - if (masknz(x,y,z,t)>0.5) { - if (x<xmin) xmin=x; - if (x>xmax) xmax=x; - if (y<ymin) ymin=y; - if (y>ymax) ymax=y; - if (z<zmin) zmin=z; - if (z>zmax) zmax=z; - if (t<tmin) tmin=t; - if (t>tmax) tmax=t; - } - // change voxel coords from newimage to nifti convention for output - ColumnVector v(4); - v << xmin << ymin << zmin << 1.0; - v = masknz.niftivox2newimagevox_mat().i() * v; - xmin = MISCMATHS::round(v(1)); - ymin = MISCMATHS::round(v(2)); - zmin = MISCMATHS::round(v(3)); - v << xmax << ymax << zmax << 1.0; - v = masknz.niftivox2newimagevox_mat().i() * v; - xmax = MISCMATHS::round(v(1)); - ymax = MISCMATHS::round(v(2)); - zmax = MISCMATHS::round(v(3)); - if (xmin>xmax) { int tmp=xmax; xmax=xmin; xmin=tmp; } - if (ymin>ymax) { int tmp=ymax; ymax=ymin; ymin=tmp; } - if (zmin>zmax) { int tmp=zmax; zmax=zmin; zmin=tmp; } - // now output nifti coords - cout << xmin << " " << 1+xmax-xmin << " " << ymin << " " << 1+ymax-ymin << " " << zmin << " " << 1+zmax-zmin << " " << tmin << " " << 1+tmax-tmin << " "; - } else if (sarg=="-e") { - if (mask.nvoxels()<1) { - generate_masks(mask,masknz,vol,lthr,uthr); - vol*=mask; - } - ColumnVector hist; - int nbins=1000; - double entropy=0; - hist = vol.histogram(nbins,mask); - double ntot = hist.Sum(); - for (int j=1; j<=nbins; j++) { - if (hist(j)>0) { - entropy -= (hist(j)/ntot) * log(hist(j)/ntot); - } - } - entropy /= log((double) nbins); - cout << entropy << " "; - } else if (sarg=="-E") { - ColumnVector hist; - int nbins=1000; - double entropy=0; - if (mask.nvoxels()<1) { - generate_masks(mask,masknz,vol,lthr,uthr); - vol*=mask; - } - hist = vol.histogram(nbins,masknz); - double ntot = hist.Sum(); - for (int j=1; j<=nbins; j++) { - if (hist(j)>0) { - entropy -= (hist(j)/ntot) * log(hist(j)/ntot); - } - } - entropy /= log((double) nbins); - cout << entropy << " "; - } else if (sarg=="-k") { - narg++; - volume<float> mask2; - if (narg>=argc) { - cerr << "Must specify an argument to -k" << endl; - exit(2); - } - read_volume4D(mask,argv[narg]); - if ( !samesize(mask,vol,3) || mask.tsize() > vol.tsize() ) { - cerr << "Mask and image must be the same size" << endl; - exit(3); - } - if (timeseriesMode && mask.tsize() != 1 ) { mask2=mask[timepoint]; } - if ( mask.tsize() != vol.tsize() && mask.tsize() != 1) - while (mask.tsize() < vol.tsize() ) { // copy the last max volume until the correct size is reached - mask.addvolume(mask[mask.tsize()-1]); - } - mask.binarise(0.5); - if (mask.tsize()!=1) { - generateNonZeroMask(mask,masknz,vol); - vol*=mask; - } - else { - generateNonZeroMask(mask[0],masknz,vol); - vol*=mask[0]; - } - } else if (sarg=="-d") { - narg++; - if (narg>=argc) { - cerr << "Must specify an argument to -d" << endl; - exit(2); - } - volume4D<float> image2,image3; - read_volume4D(image2,argv[narg]); - if (!samesize(image2[0],vol[0])) { - cerr << "Image used with -d must be the same size as the base image" << endl; - exit(3); - } - if (timeseriesMode) { image3=image2[timepoint]; } - if ( image2.tsize() > vol.tsize() ) { - cerr << "Image must be the same size as the base image" << endl; - exit(3); - } - if ( image2.tsize() != vol.tsize() && image2.tsize() != 1) { - // copy the last max volume until the correct size is reached - while (image2.tsize() < vol.tsize() ) { - image2.addvolume(image2[image2.maxt()]); - } - } - // now substract the new image from the base image - vol -= image2; - } else if (sarg=="-l") { - narg++; - if (narg<argc) { - lthr = atof(argv[narg]); - } else { - cerr << "Must specify an argument to -l" << endl; - exit(2); - } - generate_masks(mask,masknz,vol,lthr,uthr); - vol*=mask; - } else if (sarg=="-u") { - narg++; - if (narg<argc) { - uthr = atof(argv[narg]); - } else { - cerr << "Must specify an argument to -u" << endl; - exit(2); - } - generate_masks(mask,masknz,vol,lthr,uthr); - vol*=mask; - } else if (sarg=="-a") { - vol = abs(vol); - } else if (sarg=="-v") { - if (mask.nvoxels()>0) { - long int nvox = mask.sum(); - if (mask.tsize() == 1) nvox = nvox * vol.tsize(); - cout << (long int) nvox << " " - << nvox * vol.xdim() * vol.ydim() * vol.zdim() << " "; - } else { - cout << (long int) vol.nvoxels() * vol.tsize() << " " - << vol.nvoxels() * vol.tsize() * vol.xdim() * vol.ydim() * vol.zdim() << " "; - } - } else if (sarg=="-V") { - if (masknz.nvoxels()>0) { - cout << (long int) masknz.sum() << " " - << masknz.sum() * vol.xdim() * vol.ydim() * vol.zdim() << " "; - } else { - long int nzvox; - nzvox = nonzerocount(vol); - cout << nzvox << " " - << nzvox * vol.xdim() * vol.ydim() * vol.zdim() << " "; - } - } else if (sarg=="-D") { - // hidden debug option! - cout << vol.sum() << " "; - } else if (sarg=="-s") { - if (mask.nvoxels()>0) cout << vol.stddev(mask) << " "; - else cout << vol.stddev() << " "; - } else if (sarg=="-S") { - if (masknz.nvoxels()>0) { - cout << vol.stddev(masknz) << " "; - } else { - cout << nonzerostddev(vol) << " "; - } - } else if (sarg=="-r") { - vector<float> limits(vol.robustlimits(mask)); - cout << limits[0] << " " << limits[1] << " "; - } else if (sarg=="-R") { - cout << vol.min(mask) << " " << vol.max(mask) << " "; - } else if (sarg=="-c") { - ColumnVector cog(4); - // convert from fsl mm to voxel to nifti sform coord - cog.SubMatrix(1,3,1,1) = vol.cog(); - cog(4) = 1.0; - cog = vol.newimagevox2mm_mat() * cog; - cout << cog(1) << " " << cog(2) << " " << cog(3) << " " ; - } else if (sarg=="-C") { - ColumnVector cog(4); - // convert from fsl mm to fsl voxel coord to nifti voxel coord - cog.SubMatrix(1,3,1,1) = vol.cog(); - cog(4) = 1.0; - cog = vol.niftivox2newimagevox_mat().i() * cog; - cout << cog(1) << " " << cog(2) << " " << cog(3) << " " ; - } else if (sarg=="-p") { - float n; - narg++; - if (narg<argc) { - n = atof(argv[narg]); - } else { - cerr << "Must specify an argument to -p" << endl; - exit(2); - } - if ( (n<0) || (n>100) ) { - cerr << "Percentile must be between 0 and 100" << endl; - exit(1); - } - if (mask.nvoxels()>0) cout << vol.percentile((float) n/100.0, mask) << " "; - else cout << vol.percentile((float) n/100.0) << " "; - } else if (sarg=="-P") { - float n; - narg++; - if (narg<argc) { - n = atof(argv[narg]); - } else { - cerr << "Must specify an argument to -P" << endl; - exit(2); - } - if ( (n<0) || (n>100) ) { - cerr << "Percentile must be between 0 and 100" << endl; - exit(1); - } - if (mask.nvoxels()<1) { - generate_masks(mask,masknz,vol,lthr,uthr); - vol*=mask; - } - cout << vol.percentile((float) n/100.0,masknz) << " "; - } else if (sarg=="-h") { - float n; - narg++; - if (narg<argc) { - n = atof(argv[narg]); - } else { - cerr << "Must specify the number of bins" << endl; - exit(2); - } - int nbins = (int) n; - if (nbins<1) { - cerr << "Must specify at least 1 bin" << endl; - exit(1); - } - if (mask.nvoxels()>0) { - cout << vol.histogram(nbins,vol.min(),vol.max(),mask) << " "; + for (int timepoint = 0; timepoint < nTimepoints; timepoint++) + { + if (jsonOutput) + { + if (timepoint == 0){ + json.append("{\"volumes\": ["); } else { - cout << vol.histogram(nbins,vol.min(),vol.max()) << " "; + json.append(", "); } - } else if (sarg=="-H") { - float n; - narg++; - if (narg<argc) { - n = atof(argv[narg]); - } else { - cerr << "Must specify the number of bins" << endl; - exit(2); + } + for (int index = 1; index <= nIndices; index++) + { + // if json mode is enabled + if (jsonOutput) + { + if (index == 1){ + json.append("{\"indices\": [{"); + } else { + json.append(", {"); + } } - int nbins = (int) n; - if (nbins<1) { - cerr << "Must specify at least 1 bin" << endl; - exit(1); + if (timeseriesMode) + vol = inputMaster[timepoint]; + volume<float> mask, masknz; + float lthr(-numeric_limits<float>::max()); + float uthr(numeric_limits<float>::max()); + if (indexMask.nvoxels()) + { + if (indexMask.dimensionality() > 3) + { + cerr << "Index mask must be 3D" << endl; + return 3; + } + copyconvert(indexMask, mask); + mask.binarise(index - 1, index + 1, exclusive); + vol = indexMaster * mask; + if (mask.max() < 1) + { + cout << "missing label: " << index << " in mask" << endl; + continue; + } + generateNonZeroMask(mask, masknz, vol); } - float min=0; - narg++; - if (narg<argc) { - min = atof(argv[narg]); - } else { - cerr << "Must specify the histogram minimum intensity" << endl; - exit(2); + int narg(2); + bool last = false; + while (narg < argc) + { + + last = !(narg < argc - 1); + string sarg(argv[narg]); + if (sarg == "-n") + { + for (int t = 0; t < vol.tsize(); t++) + for (int z = 0; z < vol.zsize(); z++) + for (int y = 0; y < vol.ysize(); y++) + for (int x = 0; x < vol.xsize(); x++) + if (!isfinite((double)vol(x, y, z, t))) + vol(x, y, z, t) = 0; + } + else if (sarg == "-m") + { + if (mask.nvoxels() > 0){ + string val = to_string(vol.mean(mask)); + print_value(sarg, val, json, jsonOutput); + } else { + string val = to_string(vol.mean()); + print_value(sarg, val, json, jsonOutput); + } + } + else if (sarg == "-M") + { + if (masknz.nvoxels() > 0) + print_value(sarg, to_string(vol.mean(masknz)), json, jsonOutput); + else + { + double nzmean = 0; + nzmean = nonzeromean(vol); + string val = to_string(nzmean); + print_value(sarg, val, json, jsonOutput); + } + } + else if (sarg == "-X") + { + ColumnVector coord(4); + vector<int64_t> iCoords; + vol.min(mask, iCoords); + coord << (Real)iCoords[7] << (Real)iCoords[8] << (Real)iCoords[9] << 1.0; + coord = vol.niftivox2newimagevox_mat().i() * coord; + //cout << MISCMATHS::round(coord(1)) << " " << MISCMATHS::round(coord(2)) << " " << MISCMATHS::round(coord(3)) << " "; + auto coord1 = MISCMATHS::round(coord(1)); + auto coord2 = MISCMATHS::round(coord(2)); + auto coord3 = MISCMATHS::round(coord(3)); + string coordString = ""; + coordString.append(to_string(coord1)); + coordString.append(" "); + coordString.append(to_string(coord2)); + coordString.append(" "); + coordString.append(to_string(coord3)); + print_value( + sarg, + coordString, + json, + jsonOutput); + + } + else if (sarg == "-x") + { + ColumnVector coord(4); + vector<int64_t> iCoords; + vol.min(mask, iCoords); + coord << (Real)iCoords[0] << (Real)iCoords[1] << (Real)iCoords[2] << 1.0; + coord = vol.niftivox2newimagevox_mat().i() * coord; + //cout << MISCMATHS::round(coord(1)) << " " << MISCMATHS::round(coord(2)) << " " << MISCMATHS::round(coord(3)) << " "; + auto coord1 = MISCMATHS::round(coord(1)); + auto coord2 = MISCMATHS::round(coord(2)); + auto coord3 = MISCMATHS::round(coord(3)); + string coordString = ""; + coordString.append(to_string(coord1)); + coordString.append(" "); + coordString.append(to_string(coord2)); + coordString.append(" "); + coordString.append(to_string(coord3)); + print_value( + sarg, + coordString, + json, + jsonOutput); + } + else if (sarg == "-w") + { + if (masknz.nvoxels() < 1) + { // Need to generate non-zeromask + generate_masks(mask, masknz, vol, lthr, uthr); + vol *= mask; + } + int xmin = masknz.xsize() - 1, xmax(0), ymin = masknz.ysize() - 1, ymax(0), zmin = masknz.zsize() - 1, zmax(0), tmin = masknz.tsize() - 1, tmax(0); + + for (int t = 0; t < masknz.tsize(); t++) + for (int z = 0; z < masknz.zsize(); z++) + for (int y = 0; y < masknz.ysize(); y++) + for (int x = 0; x < masknz.xsize(); x++) + if (masknz(x, y, z, t) > 0.5) + { + if (x < xmin) + xmin = x; + if (x > xmax) + xmax = x; + if (y < ymin) + ymin = y; + if (y > ymax) + ymax = y; + if (z < zmin) + zmin = z; + if (z > zmax) + zmax = z; + if (t < tmin) + tmin = t; + if (t > tmax) + tmax = t; + } + // change voxel coords from newimage to nifti convention for output + ColumnVector v(4); + v << xmin << ymin << zmin << 1.0; + v = masknz.niftivox2newimagevox_mat().i() * v; + xmin = MISCMATHS::round(v(1)); + ymin = MISCMATHS::round(v(2)); + zmin = MISCMATHS::round(v(3)); + v << xmax << ymax << zmax << 1.0; + v = masknz.niftivox2newimagevox_mat().i() * v; + xmax = MISCMATHS::round(v(1)); + ymax = MISCMATHS::round(v(2)); + zmax = MISCMATHS::round(v(3)); + if (xmin > xmax) + { + int tmp = xmax; + xmax = xmin; + xmin = tmp; + } + if (ymin > ymax) + { + int tmp = ymax; + ymax = ymin; + ymin = tmp; + } + if (zmin > zmax) + { + int tmp = zmax; + zmax = zmin; + zmin = tmp; + } + // now output nifti coords + string coords = ""; + coords.append(to_string(xmin)); + coords.append(" "); + coords.append(to_string(1 + xmax - xmin)); + coords.append(" "); + coords.append(to_string(ymin)); + coords.append(" "); + coords.append(to_string(1 + ymax - ymin)); + coords.append(" "); + coords.append(to_string(zmin)); + coords.append(" "); + coords.append(to_string(1 + zmax - zmin)); + coords.append(" "); + coords.append(to_string(tmin)); + coords.append(" "); + coords.append(to_string(1 + tmax - tmin)); + print_value( + sarg, + coords, + json, + jsonOutput); + } + else if (sarg == "-e") + { + if (mask.nvoxels() < 1) + { + generate_masks(mask, masknz, vol, lthr, uthr); + vol *= mask; + } + ColumnVector hist; + int nbins = 1000; + double entropy = 0; + hist = vol.histogram(nbins, mask); + double ntot = hist.Sum(); + for (int j = 1; j <= nbins; j++) + { + if (hist(j) > 0) + { + entropy -= (hist(j) / ntot) * log(hist(j) / ntot); + } + } + entropy /= log((double)nbins); + print_value( + sarg, + to_string(entropy), + json, + jsonOutput + ); + } + else if (sarg == "-E") + { + ColumnVector hist; + int nbins = 1000; + double entropy = 0; + if (mask.nvoxels() < 1) + { + generate_masks(mask, masknz, vol, lthr, uthr); + vol *= mask; + } + hist = vol.histogram(nbins, masknz); + double ntot = hist.Sum(); + for (int j = 1; j <= nbins; j++) + { + if (hist(j) > 0) + { + entropy -= (hist(j) / ntot) * log(hist(j) / ntot); + } + } + entropy /= log((double)nbins); + print_value( + sarg, + to_string(entropy), + json, + jsonOutput + ); + } + else if (sarg == "-k") + { + narg++; + volume<float> mask2; + if (narg >= argc) + { + cerr << "Must specify an argument to -k" << endl; + exit(2); + } + read_volume4D(mask, argv[narg]); + if (!samesize(mask, vol, 3) || mask.tsize() > vol.tsize()) + { + cerr << "Mask and image must be the same size" << endl; + exit(3); + } + if (timeseriesMode && mask.tsize() != 1) + { + mask2 = mask[timepoint]; + } + if (mask.tsize() != vol.tsize() && mask.tsize() != 1) + while (mask.tsize() < vol.tsize()) + { // copy the last max volume until the correct size is reached + mask.addvolume(mask[mask.tsize() - 1]); + } + mask.binarise(0.5); + if (mask.tsize() != 1) + { + generateNonZeroMask(mask, masknz, vol); + vol *= mask; + } + else + { + generateNonZeroMask(mask[0], masknz, vol); + vol *= mask[0]; + } + } + else if (sarg == "-d") + { + narg++; + if (narg >= argc) + { + cerr << "Must specify an argument to -d" << endl; + exit(2); + } + volume4D<float> image2, image3; + read_volume4D(image2, argv[narg]); + if (!samesize(image2[0], vol[0])) + { + cerr << "Image used with -d must be the same size as the base image" << endl; + exit(3); + } + if (timeseriesMode) + { + image3 = image2[timepoint]; + } + if (image2.tsize() > vol.tsize()) + { + cerr << "Image must be the same size as the base image" << endl; + exit(3); + } + if (image2.tsize() != vol.tsize() && image2.tsize() != 1) + { + // copy the last max volume until the correct size is reached + while (image2.tsize() < vol.tsize()) + { + image2.addvolume(image2[image2.maxt()]); + } + } + // now substract the new image from the base image + vol -= image2; + } + else if (sarg == "-l") + { + narg++; + if (narg < argc) + { + lthr = atof(argv[narg]); + } + else + { + cerr << "Must specify an argument to -l" << endl; + exit(2); + } + generate_masks(mask, masknz, vol, lthr, uthr); + vol *= mask; + } + else if (sarg == "-u") + { + narg++; + if (narg < argc) + { + uthr = atof(argv[narg]); + } + else + { + cerr << "Must specify an argument to -u" << endl; + exit(2); + } + generate_masks(mask, masknz, vol, lthr, uthr); + vol *= mask; + } + else if (sarg == "-a") + { + vol = abs(vol); + } + else if (sarg == "-v") + { + if (mask.nvoxels() > 0) + { + long int nvox = mask.sum(); + if (mask.tsize() == 1) + nvox = nvox * vol.tsize(); + + string nvox_str = ""; + nvox_str.append(to_string(nvox)); + nvox_str.append(" "); + nvox_str.append(to_string(nvox * vol.xdim() * vol.ydim() * vol.zdim())); + print_value( + sarg, + nvox_str, + json, + jsonOutput + ); + } + else + { + string nvox_str = ""; + nvox_str.append(to_string((long int)vol.nvoxels() * vol.tsize())); + nvox_str.append(" "); + nvox_str.append(to_string(vol.nvoxels() * vol.tsize() * vol.xdim() * vol.ydim() * vol.zdim())); + print_value( + sarg, + nvox_str, + json, + jsonOutput); + } + } + else if (sarg == "-V") + { + if (masknz.nvoxels() > 0) + { + string nvox_str = ""; + nvox_str.append(to_string((long int)masknz.sum())); + nvox_str.append(" "); + nvox_str.append(to_string(masknz.sum() * vol.xdim() * vol.ydim() * vol.zdim())); + print_value( + sarg, + nvox_str, + json, + jsonOutput); + } + else + { + long int nzvox; + nzvox = nonzerocount(vol); + string nzvox_str = ""; + nzvox_str.append(to_string(nzvox)); + nzvox_str.append(" "); + nzvox_str.append(to_string(nzvox * vol.xdim() * vol.ydim() * vol.zdim())); + print_value( + sarg, + nzvox_str, + json, + jsonOutput); + } + } + else if (sarg == "-D") + { + // hidden debug option! + print_value( + sarg, + to_string(vol.sum()), + json, + jsonOutput + ); + } + else if (sarg == "-s") + { + if (mask.nvoxels() > 0) + print_value( + sarg, + to_string(vol.stddev(mask)), + json, + jsonOutput + ); + else + print_value( + sarg, + to_string(vol.stddev()), + json, + jsonOutput + ); + } + else if (sarg == "-S") + { + if (masknz.nvoxels() > 0) + { + print_value( + sarg, + to_string(vol.stddev(masknz)), + json, + jsonOutput + ); + } + else + { + print_value( + sarg, + to_string(nonzerostddev(vol)), + json, + jsonOutput + ); + } + } + else if (sarg == "-r") + { + vector<float> limits(vol.robustlimits(mask)); + print_value( + sarg, + to_string(limits[0]).append(" ").append(to_string(limits[1])), + json, + jsonOutput + ); + } + else if (sarg == "-R") + { + print_value( + sarg, + to_string(vol.min(mask)).append(" ").append(to_string(vol.max(mask))), + json, + jsonOutput + ); + } + else if (sarg == "-c") + { + ColumnVector cog(4); + cog.SubMatrix(1, 3, 1, 1) = vol.cog(); + cog(4) = 1.0; + cog = vol.newimagevox2mm_mat() * cog; + print_value( + sarg, + to_string(cog(1)).append(" ").append(to_string(cog(2))).append(" ").append(to_string(cog(3))), + json, + jsonOutput + ); + } + else if (sarg == "-C") + { + ColumnVector cog(4); + // convert from fsl mm to fsl voxel coord to nifti voxel coord + cog.SubMatrix(1, 3, 1, 1) = vol.cog(); + cog(4) = 1.0; + cog = vol.niftivox2newimagevox_mat().i() * cog; + print_value( + sarg, + to_string(cog(1)).append(" ").append(to_string(cog(2))).append(" ").append(to_string(cog(3))), + json, + jsonOutput + ); + } + else if (sarg == "-p") + { + float n; + narg++; + if (narg < argc) + { + n = atof(argv[narg]); + } + else + { + cerr << "Must specify an argument to -p" << endl; + exit(2); + } + if ((n < 0) || (n > 100)) + { + cerr << "Percentile must be between 0 and 100" << endl; + exit(1); + } + if (mask.nvoxels() > 0) + print_value( + sarg, + to_string(vol.percentile((float)n / 100.0, mask)), + json, + jsonOutput + ); + else + print_value( + sarg, + to_string(vol.percentile((float)n / 100.0)), + json, + jsonOutput + ); + } + else if (sarg == "-P") + { + float n; + narg++; + if (narg < argc) + { + n = atof(argv[narg]); + } + else + { + cerr << "Must specify an argument to -P" << endl; + exit(2); + } + if ((n < 0) || (n > 100)) + { + cerr << "Percentile must be between 0 and 100" << endl; + exit(1); + } + if (mask.nvoxels() < 1) + { + generate_masks(mask, masknz, vol, lthr, uthr); + vol *= mask; + } + print_value( + sarg, + to_string(vol.percentile((float)n / 100.0, masknz)), + json, + jsonOutput + ); + } + else if (sarg == "-h") + { + float n; + narg++; + if (narg < argc) + { + n = atof(argv[narg]); + } + else + { + cerr << "Must specify the number of bins" << endl; + exit(2); + } + int nbins = (int)n; + if (nbins < 1) + { + cerr << "Must specify at least 1 bin" << endl; + exit(1); + } + if (mask.nvoxels() > 0) + { + auto hist = vol.histogram(nbins, vol.min(), vol.max(), mask); + // loop over histogram values and and build a string to print + string hist_str = ""; + for (int i = 0; i < hist.Nrows(); i++) + { + hist_str += to_string(hist(i+1)); + if (i < hist.Nrows() - 1) + { + hist_str += " "; + } + } + print_value( + sarg, + hist_str, + json, + jsonOutput + ); + } + else + { + auto hist = vol.histogram(nbins, vol.min(), vol.max()); + string hist_str = ""; + for (int i = 0; i < hist.Nrows(); i++) + { + hist_str += to_string(hist(i+1)); + if (i < hist.Nrows() - 1) + { + hist_str += " "; + } + } + print_value( + sarg, + hist_str, + json, + jsonOutput + ); + } + } + else if (sarg == "-H") + { + float n; + narg++; + if (narg < argc) + { + n = atof(argv[narg]); + } + else + { + cerr << "Must specify the number of bins" << endl; + exit(2); + } + int nbins = (int)n; + if (nbins < 1) + { + cerr << "Must specify at least 1 bin" << endl; + exit(1); + } + float min = 0; + narg++; + if (narg < argc) + { + min = atof(argv[narg]); + } + else + { + cerr << "Must specify the histogram minimum intensity" << endl; + exit(2); + } + float max = 0; + narg++; + if (narg < argc) + { + max = atof(argv[narg]); + } + else + { + cerr << "Must specify the histogram maximum intensity" << endl; + exit(2); + } + if (mask.nvoxels() > 0) + { + auto hist = vol.histogram(nbins, min, max, mask); + string hist_str = ""; + for (int i = 0; i < hist.Nrows(); i++) + { + hist_str += to_string(hist(i+1)); + if (i < hist.Nrows() - 1) + { + hist_str += " "; + } + } + print_value( + sarg, + hist_str, + json, + jsonOutput + ); + } + else + { + auto hist = vol.histogram(nbins, min, max); + string hist_str = ""; + for (int i = 0; i < hist.Nrows(); i++) + { + hist_str += to_string(hist(i+1)); + if (i < hist.Nrows() - 1) + { + hist_str += " "; + } + } + print_value( + sarg, + hist_str, + json, + jsonOutput + ); + } + } + else + { + // only print cerr if not the --json option + if (sarg != "--json") + { + cerr << "Unrecognised option: " << sarg << endl; + exit(3); + } else if (sarg == "--json" && last) { + // remove the trailing comma and space + json.pop_back(); + json.pop_back(); + } + } + + narg++; } - float max=0; - narg++; - if (narg<argc) { - max = atof(argv[narg]); - } else { - cerr << "Must specify the histogram maximum intensity" << endl; - exit(2); + // if json mode is enabled + if (jsonOutput) + { + // close the json object + json.append("}"); } - if (mask.nvoxels()>0) { - cout << vol.histogram(nbins,min,max,mask) << " "; - } else { - cout << vol.histogram(nbins,min,max) << " "; + if (index < nIndices && !jsonOutput) + { + cout << endl; } - } else { - cerr << "Unrecognised option: " << sarg << endl; - exit(3); + // cout << endl; + } // end nIndices + // if json mode is enabled + if (jsonOutput) + { + // close the json object + json.append("]}"); } - - narg++; - } - cout << endl; - } - + if (timepoint < nTimepoints - 1 && !jsonOutput) + { + cout << endl; + } + // cout << endl; + } // end timepoints + if (jsonOutput) + { + // close the json object + json.append("]}"); + cout << json; + } else { + cout << endl; } return 0; } - - -int main(int argc,char *argv[]) +int main(int argc, char *argv[]) { Tracer tr("main"); string progname(argv[0]); bool timeseriesMode(false); string indexMask(""); - while ( argc > 2 && ( string(argv[1])=="-t" || string(argv[1]) =="-K" ) ) { - if ( string(argv[1])=="-t" ) - timeseriesMode=true; - if ( string(argv[1])=="-K" ) { - indexMask=string(argv[2]); + while (argc > 2 && (string(argv[1]) == "-t" || string(argv[1]) == "-K")) + { + if (string(argv[1]) == "-t") + timeseriesMode = true; + if (string(argv[1]) == "-K") + { + indexMask = string(argv[2]); argv++; argc--; } argv++; argc--; } - if (argc < 3 ) + if (argc < 3) return print_usage(progname); - try { - return fmrib_main_float(argc,argv,timeseriesMode,indexMask); - } catch(std::exception &e) { + try + { + return fmrib_main_float(argc, argv, timeseriesMode, indexMask); + } + catch (std::exception &e) + { cerr << e.what() << endl; - } catch (...) { + } + catch (...) + { // do nothing - just exit without garbage message } return -1; - } -- GitLab From 08e56c4632edf175221ec9c263dc1b77efb99e92 Mon Sep 17 00:00:00 2001 From: Taylor Hanayik <hanayik@gmail.com> Date: Fri, 16 Jun 2023 14:08:31 +0100 Subject: [PATCH 2/6] add previous avwutils tests and new fslstats test --- .gitignore | 3 +- tests/avscale/feedsRun | 116 +++++++++++++++ tests/fix_orient/feedsRun | 166 ++++++++++++++++++++++ tests/fslchpixdim/feedsRun | 58 ++++++++ tests/fslcomplex/feedsRun | 132 +++++++++++++++++ tests/fslcreatehd/feedsRun | 265 +++++++++++++++++++++++++++++++++++ tests/fslcreatehd/mni2mm.xml | 44 ++++++ tests/fslstats/feedsRun | 180 ++++++++++++++++++++++++ 8 files changed, 963 insertions(+), 1 deletion(-) create mode 100755 tests/avscale/feedsRun create mode 100755 tests/fix_orient/feedsRun create mode 100755 tests/fslchpixdim/feedsRun create mode 100755 tests/fslcomplex/feedsRun create mode 100755 tests/fslcreatehd/feedsRun create mode 100644 tests/fslcreatehd/mni2mm.xml create mode 100755 tests/fslstats/feedsRun diff --git a/.gitignore b/.gitignore index 600d2d3..49b91c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode \ No newline at end of file +.vscode +pyfeeds-out \ No newline at end of file diff --git a/tests/avscale/feedsRun b/tests/avscale/feedsRun new file mode 100755 index 0000000..32062a0 --- /dev/null +++ b/tests/avscale/feedsRun @@ -0,0 +1,116 @@ +#!/usr/bin/env fslpython + +# test avscale with different input affines + + +import sys +import os + +import fsl.utils.run as run + +import numpy as np + + +PI = np.pi +PION2 = np.pi / 2 + + +# Each test has the form (input-affine, expected-output) +# where expected-output comprises: +# - scales +# - translations +# - rotations +# - skews +# - L/R orientation (preserved or swapped) +tests = [ + + ([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (0, 0, 0), (0, 0, 0), 'preserved']), + ([[2, 0, 0, 0], + [0, 2, 0, 0], + [0, 0, 2, 0], + [0, 0, 0, 1]], [(2, 2, 2), (0, 0, 0), (0, 0, 0), (0, 0, 0), 'preserved']), + ([[-2, 0, 0, 0], + [ 0, 2, 0, 0], + [ 0, 0, 2, 0], + [ 0, 0, 0, 1]], [(2, 2, 2), (0, 0, 0), (0, 0, PI), (0, 0, 0), 'swapped']), + ([[0, 0, 1, 0], + [0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (0, -PION2, 0), (0, 0, 0), 'swapped']), + ([[ 0, 0, -1, 0], + [ 0, -1, 0, 0], + [-1, 0, 0, 0], + [ 0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (-PI, PION2, 0), (0, 0, 0), 'preserved']), + ([[0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (0, 0, PION2), (0, 0, 0), 'swapped']), + ([[1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (PION2, 0, 0), (0, 0, 0), 'swapped']), + ([[-2, 0, 0, 90], + [ 0, 2, 0, -126], + [ 0, 0, 2, -72], + [ 0, 0, 0, 1]], [(2, 2, 2), (90, -126, -72), (0, 0, PI), (0, 0, 0), 'swapped']), + +] + +def read_avscale_output(output): + lines = output.split('\n') + # scales + # translations + # rotations + # skews + # l/r orientation (preserved or swapped) + scales = [l for l in lines if l.startswith('Scales ')] [0] + translations = [l for l in lines if l.startswith('Translations ')] [0] + rotations = [l for l in lines if l.startswith('Rotation Angles')][0] + skews = [l for l in lines if l.startswith('Skews ')] [0] + orient = [l for l in lines if l.startswith('Left-Right ')] [0] + + scales = [float(s) for s in scales .split()[-3:]] + translations = [float(s) for s in translations.split()[-3:]] + rotations = [float(s) for s in rotations .split()[-3:]] + skews = [float(s) for s in skews .split()[-3:]] + orient = orient.split()[-1] + + return { + 'scales' : np.array(scales), + 'translations' : np.array(translations), + 'rotations' : np.array(rotations), + 'skews' : np.array(skews), + 'orient' : orient.lower(), + } + + +def test_avscale(): + + for affine, expected in tests: + affine = np.array(affine) + np.savetxt('affine.mat', affine, '%0.6f') + print(affine) + output = run.runfsl('avscale --allparams affine.mat') + output = read_avscale_output(output) + + scales, translations, rotations, skews, orient = expected + + try: + assert np.all(np.isclose(scales, output['scales'])) + assert np.all(np.isclose(translations, output['translations'])) + assert np.all(np.isclose(rotations, output['rotations'])) + assert np.all(np.isclose(skews, output['skews'])) + assert orient == output['orient'] + except AssertionError: + print(affine) + print(output) + raise + + +if __name__ == '__main__': + if len(sys.argv) > 1: + os.chdir(sys.argv[1]) + test_avscale() diff --git a/tests/fix_orient/feedsRun b/tests/fix_orient/feedsRun new file mode 100755 index 0000000..0e0e575 --- /dev/null +++ b/tests/fix_orient/feedsRun @@ -0,0 +1,166 @@ +#!/usr/bin/env fslpython +"""Test the standard recipe for fixing a broken orientation, from "The labels +in FSLView are wrong or missing and no conversion to NIfTI can fix this", on +this page: + +https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/Orientation%20Explained + + If this really cannot be fixed at the conversion stage, then it is + possible to fix by the follow (only to be attempted after reading the + following section on background information on NIfTI orientation): + fslorient -deleteorient imagename ; fslswapdim imagename a b c imagename ; + fslorient -setqformcode 1 imagename ; where a b c need to be chosen, + usually by trial and error, so that the image after this is in the same + orientation (as viewed in FSLView) as the MNI152 images. However, unless + you have some way of telling left from right in the image (e.g. by a + vitamin capsule or clearly known structural asymmetry) this method can + easily result in an incorrect left-right labelling and so should not be + attempted. + +The recipe involves: + + 1. resetting the image affines + + 2. transposing the data array until it is in a + RAS orientation + + 3. Enabling the qform affine which, after step + 1, should be a diagonal matrix with pixdims + as scaling parameters. + +Specifically, the recipe is as follows: + + # 1. clear the sform/qform affines + fslorient -deleteorient image + + # 2. rotate the data into RAS orientation (exact + # command depends on original data orientation) + fslswapdim image x z y image + + # 3. reset the qform code. The qform should be + # a diagonal matrix, and the image data array + # should be in RAS orientation, so after this + # step the image should be labelled correctly. + fslorient -setqformcode 1 image + + +This recipe makes some key assumptions: + + - fslorient -deleteorient (step 1) deleting/ + clearing the affines - the qform must be + reset to be a scaling matrix. + + - fslswapdim (step 2) not touching the affines + when the s/qform codes are 0 + +These are required so that, in step 3, the qform is a diagonal matrix with +correct scaling paramaters. + +Some bugs were introduced in FSL 6.0.0 (fixed in FSL 6.0.6) which broke the +above assumptions: + + - fslorient -deleteorient was not clearing the original + affines + - fslswapdim was applying the rotations/flips to both the + data, and to the s/qforms, even if the s/qform codes were + set to 0. +""" + +import numpy as np +import nibabel as nib +import os +import subprocess as sp +import shlex +import sys +import traceback + +def sprun(cmd): + sp.run(shlex.split(cmd), check=True) + + +# Test the original recipe for fixing the +# orientation of an image, as described above +def test_fix_orient(): + + data = np.random.random((10, 10, 10)) + orig_qform = np.array([[2, 0, 0, 10], + [0, 2, 0, -20], + [0, 0, 2, 50], + [0, 0, 0, 1]]) + image = nib.Nifti1Image(data, np.eye(4)) + image.set_sform(np.eye(4), 0) + image.set_qform(orig_qform, 1) + + image.to_filename('orig.nii.gz') + sprun('imcp orig cleared') + sprun('fslorient -deleteorient cleared') + sprun('fslswapdim cleared x z y swapped') + sprun('imcp swapped fixed') + sprun('fslorient -setqformcode 1 fixed') + + fixed = nib.load('fixed.nii.gz') + + new_data = fixed.get_fdata() + new_qform, qcode = fixed.get_qform(coded=True) + _, scode = fixed.get_sform(coded=True) + + # new qform should be a scaling matrix (not + # necessarily equivalent to the original + assert qcode == 1 + assert scode == 0 + assert np.all(np.isclose(new_qform, np.diag([2, 2, 2, 1]))) + assert np.all(np.isclose(new_data, data.transpose((0, 2, 1)))) + + +# An alternate strategy which involves +# explicitly clobbering the qform +def test_fix_orient_alternate(): + # original qform + data = np.random.random((10, 10, 10)) + orig_qform = np.array([[2, 0, 0, 10], + [0, 2, 0, -20], + [0, 0, 2, 50], + [0, 0, 0, 1]]) + image = nib.Nifti1Image(data, np.eye(4)) + image.set_sform(np.eye(4), 0) + image.set_qform(orig_qform, 1) + + image.to_filename('orig.nii.gz') + sprun('imcp orig cleared') + sprun('fslorient -deleteorient cleared') + sprun('fslswapdim cleared x z y swapped') + sprun('imcp swapped fixed1') + sprun('fslorient -setqform 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 fixed1') + sprun('imcp fixed1 fixed') + sprun('fslorient -setqformcode 1 fixed') + + fixed = nib.load('fixed.nii.gz') + + new_data = fixed.get_fdata() + new_qform, qcode = fixed.get_qform(coded=True) + _, scode = fixed.get_sform(coded=True) + + # new qform should be a scaling matrix (not + # necessarily equivalent to the original + assert qcode == 1 + assert scode == 0 + assert np.all(np.isclose(new_qform, np.diag([2, 2, 2, 1]))) + assert np.all(np.isclose(new_data, data.transpose((0, 2, 1)))) + + +if __name__ == '__main__': + tests = [test_fix_orient, + test_fix_orient_alternate] + + result = 0 + for test in tests: + try: + os.chdir(sys.argv[1]) + test() + print(f'\nTest {test.__name__} PASSED') + except Exception as e: + print(f'\nTest {test.__name__} FAILED') + traceback.print_exc() + result = 1 + + sys.exit(result) diff --git a/tests/fslchpixdim/feedsRun b/tests/fslchpixdim/feedsRun new file mode 100755 index 0000000..56bcee4 --- /dev/null +++ b/tests/fslchpixdim/feedsRun @@ -0,0 +1,58 @@ +#!/usr/bin/env fslpython + +import os +import sys +import subprocess as sp + +import numpy as np +import nibabel as nib + + +def create_image(shape, pixdim): + pixdim = list(pixdim) + data = np.random.random(shape).astype(np.float32) + hdr = nib.Nifti1Header() + + hdr.set_data_dtype(np.float32) + hdr.set_data_shape(shape) + hdr.set_zooms(pixdim[:len(shape)]) + + return nib.Nifti1Image(data, np.eye(4), hdr) + + +def check_image(origimg, changedimg, exppixdim): + + gotshape = changedimg.shape + gotpixdim = list(changedimg.header.get_zooms()) + gotdata = changedimg.get_fdata() + + assert origimg.shape == gotshape + assert gotpixdim == exppixdim + assert np.all(origimg.get_fdata() == gotdata) + + +def run_tests(): + # (image shape, command, exppixdims) + tests = [ + ((5, 5, 5), '2 2 2', (2, 2, 2)), + ((5, 5, 5), '2 2 2 2', (2, 2, 2)), + ((5, 5, 5, 5), '2 2 2', (2, 2, 2, 1)), + ((5, 5, 5, 5), '2 2 2 2', (2, 2, 2, 2)), + ] + + for shape, cmd, exppixdim in tests: + + imgfile = 'image.nii.gz' + img = create_image(shape, [1] * len(shape)) + img.to_filename(imgfile) + + try: + sp.run(['fslchpixdim', imgfile] + list(cmd.split())) + check_image(img, nib.load(imgfile), list(exppixdim)) + + finally: + os.remove(imgfile) + + +if __name__ == '__main__': + sys.exit(run_tests()) diff --git a/tests/fslcomplex/feedsRun b/tests/fslcomplex/feedsRun new file mode 100755 index 0000000..a84cc1d --- /dev/null +++ b/tests/fslcomplex/feedsRun @@ -0,0 +1,132 @@ +#!/usr/bin/env fslpython +"""Test that fslcomplex produces correct outputs. Specifically tests that the +output files have orientation info that matches that of the input files. +""" + +import numpy as np +import nibabel as nib +import subprocess as sp +import os.path as op +import shlex +import sys +import traceback + +from fsl.utils.tempdir import tempdir +from fsl.data.image import addExt + + +def sprun(cmd): + print(f'RUN {cmd}') + sp.run(shlex.split(cmd), check=True) + + +def imtest(path): + try: + addExt(path) + return True + except Exception: + return False + + +# radio=[True|False] -> whether or not +# to add a flip on the affine X axis +def create_test_input_data(radio, seed=1): + + np.random.seed(seed) + + real = np.random.randint(0, 100, (10, 10, 10)).astype(np.float32) + imag = np.random.randint(0, 100, (10, 10, 10)).astype(np.float32) + affine = np.diag([3, 3, 3, 1]) + + if radio: + affine[0, 0] *= -1 + affine[:3, 3] = [37, 20, 30] + real = np.flip(real, 0) + imag = np.flip(imag, 0) + else: + affine[:3, 3] = [10, 20, 30] + + comp = real + imag * 1j + abso = np.abs(comp) + phase = np.arctan2(comp.imag, comp.real) + comp4d = np.concatenate((comp.reshape(10, 10, 10, 1), + comp.reshape(10, 10, 10, 1)), 3) + + real = nib.Nifti1Image(real, affine) + imag = nib.Nifti1Image(imag, affine) + comp = nib.Nifti1Image(comp, affine) + comp4d = nib.Nifti1Image(comp4d, affine) + abso = nib.Nifti1Image(abso, affine) + phase = nib.Nifti1Image(phase, affine) + + real .set_qform(*real .get_sform(coded=True)) + imag .set_qform(*comp .get_sform(coded=True)) + comp .set_qform(*comp .get_sform(coded=True)) + comp4d.set_qform(*comp4d.get_sform(coded=True)) + abso .set_qform(*abso .get_sform(coded=True)) + phase .set_qform(*phase .get_sform(coded=True)) + + real .to_filename('real_in.nii.gz') + imag .to_filename('imag_in.nii.gz') + comp .to_filename('complex_in.nii.gz') + comp4d.to_filename('complex4d_in.nii.gz') + abso .to_filename('abs_in.nii.gz') + phase .to_filename('phase_in.nii.gz') + + +def compare_images(file1, file2): + img1 = nib.load(addExt(file1)) + img2 = nib.load(addExt(file2)) + data1 = np.asanyarray(img1.dataobj) + data2 = np.asanyarray(img2.dataobj) + assert img1.header['sform_code'] == img2.header['sform_code'] + assert img1.header['qform_code'] == img2.header['qform_code'] + assert np.all(np.isclose(img1.get_sform(), img2.get_sform())) + assert np.all(np.isclose(img1.get_qform(), img2.get_qform())) + assert np.all(np.isclose(img1.header['pixdim'], img2.header['pixdim'])) + assert np.all(np.isclose(data1, data2)) + + +def test_fslcomplex_call(args, infiles, outfiles, radio): + with tempdir(): + create_test_input_data(radio) + sprun(f'fslcomplex {args} {infiles} {outfiles}') + for outfile in outfiles.split(' '): + + if not imtest(outfile): + continue + + prefix = outfile.split('_')[0] + infile = f'{prefix}_in' + + print(f'Comparing {outfile} against {infile}') + + compare_images(infile, outfile) + + +if __name__ == '__main__': + tests = [ + ('-realabs', 'complex_in', 'abs_out'), + ('-realphase', 'complex_in', 'phase_out'), + ('-realpolar', 'complex_in', 'abs_out phase_out'), + ('-realcartesian', 'complex_in', 'real_out imag_out'), + ('-complex', 'real_in imag_in', 'complex_out'), + ('-complexpolar', 'abs_in phase_in', 'complex_out'), + ('-copyonly', 'complex_in', 'complex_out'), + ('-complexsplit', 'complex4d_in', 'complex_out 0 0'), + ('-complexsplit', 'complex4d_in', 'complex_out 1 1'), + ('-complexmerge', 'complex_in complex_in', 'complex4d_out'), + ] + + result = 0 + for radio in [True, False]: + for args, infiles, outfiles in tests: + try: + test_fslcomplex_call(args, infiles, outfiles, radio) + print(f'\nTest fslcomplex {args} {infiles} {outfiles} [radio: {radio}] PASSED') + except Exception as e: + print(f'\nTest fslcomplex {args} {infiles} {outfiles} [radio: {radio}] FAILED') + traceback.print_exc() + result = 1 + + sys.exit(result) diff --git a/tests/fslcreatehd/feedsRun b/tests/fslcreatehd/feedsRun new file mode 100755 index 0000000..339553d --- /dev/null +++ b/tests/fslcreatehd/feedsRun @@ -0,0 +1,265 @@ +#!/usr/bin/env fslpython + +import os +import os.path as op +import sys +import subprocess as sp + +import numpy as np +import nibabel as nib + + +THISDIR = op.dirname(op.abspath(__file__)) + + +# 2=char, 4=short, 8=int, 16=float, 64=double +DTYPE_MAPPING = { + 2 : np.uint8, + 4 : np.int16, + 8 : np.int32, + 16 : np.float32, + 64 : np.float64 +} + +MNI152_2MM_AFFINE = np.array([[-2, 0, 0, 90], + [ 0, 2, 0, -126], + [ 0, 0, 2, -72], + [ 0, 0, 0, 1]]) + + +def validate_result(imgfile, expshape, exppixdim, exporigin, expdtype): + + img = nib.load(imgfile) + hdr = img.header + expshape = list(expshape) + exppixdim = list(exppixdim) + exporigin = list(exporigin) + + if (len(expshape) == 3) or \ + expshape[3] in (0, 1): + expndims = 3 + else: + expndims = 4 + + expaffine = np.diag(exppixdim[:3] + [1]) + expaffine[0, 0] *= -1 + expaffine[:3, 3] = exporigin + expaffine[0, 3] *= exppixdim[0] + expaffine[1, 3] *= -exppixdim[1] + expaffine[2, 3] *= -exppixdim[2] + + assert len(img.shape) == expndims + assert list(img.shape) == expshape[ :expndims] + assert list(hdr.get_zooms()) == exppixdim[:expndims] + assert img.get_data_dtype() == expdtype + assert np.all(np.isclose(img.affine, expaffine)) + + +def create_image(shape, pixdim, origin, dtype): + pixdim = list(pixdim) + affine = np.diag(pixdim[:3] + [1]) + affine[:3, 3] = origin + data = np.random.randint(1, 100, shape).astype(dtype) + hdr = nib.Nifti1Header() + + hdr.set_data_dtype(dtype) + hdr.set_data_shape(shape) + hdr.set_zooms(pixdim[:len(shape)]) + + return nib.Nifti1Image(data, affine, hdr) + + +def test_new_file(): + tests = [ + + # 4th dim of size 0 or 1 should + # result in a 3D image (this + # is coded in validate_result) + ' 5 5 5 0 2 2 2 0 0 0 0 2', + ' 5 5 5 0 2 2 2 1 0 0 0 2', + ' 5 5 5 1 2 2 2 1 0 0 0 2', + ' 5 5 5 1 2 2 2 1 0 0 0 4', + ' 5 5 5 1 2 2 2 1 0 0 0 8', + ' 5 5 5 1 2 2 2 1 0 0 0 16', + ' 5 5 5 1 2 2 2 1 1 2 3 64', + ' 5 5 5 1 0.5 1.5 1.25 1 0 0 0 2', + ' 5 5 5 1 0.5 1.5 1.25 1.5 0 0 0 2', + ' 5 5 5 1 0.5 1.5 1.25 0.5 0 0 0 2', + '30 30 30 5 5 10 3 5 10 20 10 2', + ] + + for test in tests: + args = list(test.split()) + imgfile = 'image.nii.gz' + + try: + os.remove(imgfile) + except: + pass + + print(['fslcreatehd'] + args + [imgfile]) + + sp.run(['fslcreatehd'] + args + [imgfile]) + + args = [float(a) for a in args] + expshape = args[:4] + exppixdim = args[4:8] + exporigin = args[8:11] + expdtype = DTYPE_MAPPING[args[11]] + + try: + validate_result( + imgfile, expshape, exppixdim, exporigin, expdtype) + finally: + os.remove(imgfile) + + +def test_existing_file(): + + # each test is a tuple containing: + # - dtype_of_existing_image + # - shape_of_existing_image + # - exp_shape (set to None if image should not be overwritten) + # - fslcreatehd_args + tests = [ + # 4th dim of 0 or 1 should result in a 3D image + (2, (5, 5, 5), None, '5 5 5 0 2 2 2 1 0 0 0 2'), + (2, (5, 5, 5), None, '5 5 5 0 2 2 2 1 1 2 3 2'), + (2, (5, 5, 5), None, '5 5 5 1 2 2 2 2 0 0 0 2'), + + # dtype arg should be ignored for existing images + (2, (5, 5, 5), None, '5 5 5 0 2 2 2 1 0 0 0 4'), + (4, (5, 5, 5), None, '5 5 5 0 2 2 2 1 0 0 0 2'), + + # data should be overwritten if any + # of the first 3 dims are different, + # or if a 4th dim is specified + (2, (5, 5, 5), (5, 3, 5), '5 3 5 0 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5), (5, 3, 5), '5 3 5 0 2 2 2 2 0 0 0 4'), + (2, (5, 5, 5), (5, 3, 5), '5 3 5 1 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5), (5, 3, 5), '5 3 5 1 2 2 2 2 1 2 3 2'), + (2, (5, 5, 5), (5, 3, 5, 5), '5 3 5 5 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5), (5, 3, 5, 5), '5 3 5 5 2 2 2 2 1 2 3 2'), + + # 4D - same rules apply + (2, (5, 5, 5, 5), None, '5 5 5 5 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), None, '5 5 5 5 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), None, '5 5 5 5 2 2 2 2 0 0 0 4'), + (4, (5, 5, 5, 5), None, '5 5 5 5 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), None, '5 5 5 5 2 2 2 2 1 2 3 2'), + + # data overwritten if nelements + # change + (2, (5, 5, 5, 5), (5, 5, 5), '5 5 5 0 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), (5, 5, 5), '5 5 5 1 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), (5, 3, 5), '5 3 5 0 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), (5, 3, 5), '5 3 5 1 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), (5, 3, 5), '5 3 5 1 2 2 2 2 1 2 3 2'), + (2, (5, 5, 5, 5), (5, 5, 5, 2), '5 5 5 2 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), (5, 3, 5, 5), '5 3 5 5 2 2 2 2 0 0 0 2'), + (2, (5, 5, 5, 5), (5, 3, 5, 5), '5 3 5 5 2 2 2 2 1 2 3 2'), + ] + + for (dtype, shape, expshape, args) in tests: + imgfile = 'image.nii.gz' + dtype = DTYPE_MAPPING[dtype] + args = list(args.split()) + img = create_image(shape, (1, 1, 1, 1), (0, 0, 0), dtype) + + img.to_filename(imgfile) + + sp.run(['fslcreatehd'] + args + [imgfile]) + + checkdata = expshape is None + + if expshape is None: + expshape = shape + + args = [float(a) for a in args] + exppixdim = args[4:8] + exporigin = args[8:11] + + try: + validate_result(imgfile, expshape, exppixdim, exporigin, dtype) + + if checkdata: + data = np.asanyarray(img.dataobj) + datacopy = np.asanyarray(nib.load(imgfile).dataobj) + assert np.all(data == datacopy) + + finally: + os.remove(imgfile) + + +def test_new_file_xml(): + xmlfile = op.join(THISDIR, 'mni2mm.xml') + sp.run(['fslcreatehd', xmlfile, 'mni.nii.gz']) + img = nib.load('mni.nii.gz') + + assert img.shape == (91, 109, 91) + assert img.header.get_zooms()[:3] == (2.0, 2.0, 2.0) + assert img.header['intent_code'] == 10 + assert img.header['intent_p1'] == 20 + assert img.header['intent_p2'] == 30 + assert img.header['intent_p3'] == 40 + + +def test_existing_file_xml_same_shape(): + xmlfile = op.join(THISDIR, 'mni2mm.xml') + img = create_image((91, 109, 91), (3, 3, 3), (5, 5, 5), np.float32) + img.to_filename('image.nii.gz') + + sp.run(['fslcreatehd', xmlfile, 'image.nii.gz']) + + result = nib.load('image.nii.gz') + + assert result.shape == (91, 109, 91) + assert result.header.get_zooms()[:3] == (2.0, 2.0, 2.0) + + # bug in FSL<=6.0.4 caused intent codes to not be set + assert result.header['intent_code'] == 10 + assert result.header['intent_p1'] == 20 + assert result.header['intent_p2'] == 30 + assert result.header['intent_p3'] == 40 + assert np.all(result.affine == MNI152_2MM_AFFINE) + + # data should be preserved + assert np.all(result.get_fdata() == img.get_fdata()) + + +def test_existing_file_xml_different_shape(): + + xmlfile = op.join(THISDIR, 'mni2mm.xml') + img = create_image((20, 20, 20), (3, 3, 3), (5, 5, 5), np.float32) + + img.to_filename('image.nii.gz') + + sp.run(['fslcreatehd', xmlfile, 'image.nii.gz']) + + result = nib.load('image.nii.gz') + + assert result.shape == (91, 109, 91) + assert result.header.get_zooms()[:3] == (2.0, 2.0, 2.0) + assert result.header['intent_code'] == 10 + assert result.header['intent_p1'] == 20 + assert result.header['intent_p2'] == 30 + assert result.header['intent_p3'] == 40 + assert np.all(result.affine == MNI152_2MM_AFFINE) + + # data should be cleared + assert np.all(result.get_fdata() == 0) + + +def main(): + outdir = sys.argv[1] + os.chdir(outdir) + + test_new_file() + test_existing_file() + test_new_file_xml() + test_existing_file_xml_same_shape() + test_existing_file_xml_different_shape() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/fslcreatehd/mni2mm.xml b/tests/fslcreatehd/mni2mm.xml new file mode 100644 index 0000000..06b8043 --- /dev/null +++ b/tests/fslcreatehd/mni2mm.xml @@ -0,0 +1,44 @@ +<nifti_image + image_offset = '352' + ndim = '3' + nx = '91' + ny = '109' + nz = '91' + nt = '1' + dx = '2' + dy = '2' + dz = '2' + dt = '1' + datatype = '4' + nvox = '902629' + nbyper = '2' + scl_slope = '1' + scl_inter = '0' + intent_code = '10' + intent_p1 = '20' + intent_p2 = '30' + intent_p3 = '40' + intent_name = '' + toffset = '0' + xyz_units = '2' + time_units = '8' + freq_dim = '0' + phase_dim = '0' + slice_dim = '0' + descrip = 'FSL5.0' + aux_file = '' + qform_code = '4' + qfac = '-1' + quatern_b = '0' + quatern_c = '1' + quatern_d = '0' + qoffset_x = '90' + qoffset_y = '-126' + qoffset_z = '-72' + sform_code = '4' + sto_xyz_matrix = '-2 0 0 90 0 2 0 -126 0 0 2 -72 0 0 0 1' + slice_code = '0' + slice_start = '0' + scl_end = '0' + scl_duration = '0' +/> diff --git a/tests/fslstats/feedsRun b/tests/fslstats/feedsRun new file mode 100755 index 0000000..c6d1305 --- /dev/null +++ b/tests/fslstats/feedsRun @@ -0,0 +1,180 @@ +#!/usr/bin/env fslpython + +# test fslstats with a bunch of different options +# +# Usage: fslstats [preoptions] <input> [options] +# +# preoption -t will give a separate output line for each 3D volume of a 4D timeseries +# preoption -K < indexMask > will generate seperate n submasks from indexMask, for indexvalues 1..n where n is the maximum index value in indexMask, and generate statistics for each submask +# Note - options are applied in order, e.g. -M -l 10 -M will report the non-zero mean, apply a threshold and then report the new nonzero mean + +# -l <lthresh> : set lower threshold +# -u <uthresh> : set upper threshold +# -r : output <robust min intensity> <robust max intensity> +# -R : output <min intensity> <max intensity> +# -e : output mean entropy ; mean(-i*ln(i)) +# -E : output mean entropy (of nonzero voxels) +# -v : output <voxels> <volume> +# -V : output <voxels> <volume> (for nonzero voxels) +# -m : output mean +# -M : output mean (for nonzero voxels) +# -s : output standard deviation +# -S : output standard deviation (for nonzero voxels) +# -w : output smallest ROI <xmin> <xsize> <ymin> <ysize> <zmin> <zsize> <tmin> <tsize> containing nonzero voxels +# -x : output co-ordinates of maximum voxel +# -X : output co-ordinates of minimum voxel +# -c : output centre-of-gravity (cog) in mm coordinates +# -C : output centre-of-gravity (cog) in voxel coordinates +# -p <n> : output nth percentile (n between 0 and 100) +# -P <n> : output nth percentile (for nonzero voxels) +# -a : use absolute values of all image intensities +# -n : treat NaN or Inf as zero for subsequent stats +# -k <mask> : use the specified image (filename) for masking - overrides lower and upper thresholds +# -d <image> : take the difference between the base image and the image specified here +# -h <nbins> : output a histogram (for the thresholded/masked voxels only) with nbins +# -H <nbins> <min> <max> : output a histogram (for the thresholded/masked voxels only) with nbins and histogram limits of min and max + +# Note - thresholds are not inclusive ie lthresh<allowed<uthresh + +import sys +import os +import fsl.utils.run as run +import numpy as np +import nibabel as nib + +# set the random seed so that the tests are reproducible +np.random.seed(0) + +options = [ + {"option": "-r", "expected": "0.018533 0.978824"}, + {"option": "-R", "expected": "0.000546 0.999809"}, + {"option": "-e", "expected": "0.906103"}, + {"option": "-E", "expected": "0.906103"}, + {"option": "-v", "expected": "1000 1000.000000"}, + {"option": "-V", "expected": "1000 1000.000000"}, + {"option": "-m", "expected": "0.495922"}, + {"option": "-M", "expected": "0.495922"}, + {"option": "-s", "expected": "0.290744"}, + {"option": "-S", "expected": "0.290744"}, + {"option": "-w", "expected": "0 10 0 10 0 10 0 1"}, + {"option": "-x", "expected": "9 7 4"}, + {"option": "-X", "expected": "8 2 1"}, + {"option": "-c", "expected": "4.498706 4.545873 4.613188"}, + {"option": "-C", "expected": "4.498706 4.545873 4.613188"}, + {"option": "-p 50", "expected": "0.482584"}, + {"option": "-P 50", "expected": "0.482584"}, +] + +# create a list of tests and expected results +tests = [] +for option in options: + tests.append( + { + "preoptions": "", + "mask": "", + "options": option["option"], + "expected": option["expected"], + } + ) + +tests_with_preoptions = [ + { + "preoptions": "-K", + "mask": "mask.nii.gz", + "options": "-m", + "expected": "0.948930 \n0.947671 \n1.003258 \n1.010696", + }, + { + "preoptions": "-t -K", + "mask": "mask.nii.gz", + "options": "-m", + "expected": "0.459736 \n0.476035 \n0.504080 \n0.549485 \n0.489194 \n0.471636 \n0.499178 \n0.461211", + }, + { + "preoptions": "-t", + "mask": "", + "options": "-m", + "expected": "0.496675 \n0.487950", + }, +] + +# taken from fslchpixdim test +def create_image(shape, pixdim): + pixdim = list(pixdim) + data = np.random.random(shape).astype(np.float32) + hdr = nib.Nifti1Header() + hdr.set_data_dtype(np.float32) + hdr.set_data_shape(shape) + hdr.set_zooms(pixdim[:len(shape)]) + + return nib.Nifti1Image(data, np.eye(4), hdr) + +def create_mask(input_img, n_rois=4): + ''' + Create a mask image from the input image. + The mask image will have n_rois different values with random sizes randomly placed in the image. + ''' + data = input_img.get_fdata() + mask = np.zeros_like(data) + for i in range(n_rois + 1): + roi_size = np.random.randint(1, data.size) + roi_value = i + roi = np.random.choice(data.size, roi_size, replace=False) + mask.flat[roi] = roi_value + return nib.Nifti1Image(mask, input_img.affine, input_img.header) + + +def test_fslstats(): + imgfile = 'test.nii.gz' + shape = (10,10,10) + img = create_image(shape, [1] * len(shape)) + img.to_filename(imgfile) + mask = create_mask(img) + mask.to_filename('mask.nii.gz') + + # run the tests witoout preoptions + for test in tests: + cmd = f"fslstats {test['preoptions']} {test['mask']} {imgfile} {test['options']}" + # remove any double spaces from empty test options + cmd = " ".join(cmd.split()) + print(cmd) + output = run.runfsl(cmd) + # trim any trailing whitespace from the output (fslstats adds a newline at the end) + output = output.rstrip() + try: + assert output == test["expected"] + except AssertionError: + print(cmd) + print(output) + print(test["expected"]) + raise + + # run the tests with preoptions + imgfile = 'test_4d.nii.gz' + shape = (10,10,10, 2) + img = create_image(shape, [1] * len(shape)) + img.to_filename(imgfile) + for test in tests_with_preoptions: + cmd = f"fslstats {test['preoptions']} {test['mask']} {imgfile} {test['options']}" + # remove any double spaces from empty test options + cmd = " ".join(cmd.split()) + print(cmd, flush=True) + output = run.runfsl(cmd) + # trim any trailing whitespace from the output (fslstats adds a newline at the end) + output = output.rstrip() + try: + assert output == test["expected"] + except AssertionError: + print('Failed test') + print(cmd) + print('Output') + print(output) + print('Expected') + print(test["expected"]) + raise + + +if __name__ == "__main__": + if len(sys.argv) > 1: + os.chdir(sys.argv[1]) + test_fslstats() -- GitLab From df55aff586ac0d50054bfff32f55c2a7d5090bce Mon Sep 17 00:00:00 2001 From: Taylor Hanayik <hanayik@gmail.com> Date: Tue, 20 Jun 2023 16:00:32 +0100 Subject: [PATCH 3/6] add json lib; update unit tests; add lfs --- .gitattributes | 1 + fslstats.cc | 570 ++++++++++++----------------- tests/fslstats/feedsRun | 50 +-- tests/fslstats/imageForDiff.nii.gz | 3 + tests/fslstats/mask.nii.gz | 3 + tests/fslstats/test.nii.gz | 3 + tests/fslstats/test_4d.nii.gz | 3 + 7 files changed, 268 insertions(+), 365 deletions(-) create mode 100644 .gitattributes create mode 100644 tests/fslstats/imageForDiff.nii.gz create mode 100644 tests/fslstats/mask.nii.gz create mode 100644 tests/fslstats/test.nii.gz create mode 100644 tests/fslstats/test_4d.nii.gz diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..07f0463 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.nii.gz filter=lfs diff=lfs merge=lfs -text diff --git a/fslstats.cc b/fslstats.cc index 83a9873..8e90fd2 100644 --- a/fslstats.cc +++ b/fslstats.cc @@ -8,21 +8,27 @@ #include <limits> #include <string> +#include <typeinfo> #include "utils/fsl_isfinite.h" #include "armawrap/newmat.h" #include "miscmaths/miscmaths.h" #include "newimage/newimageall.h" #include "newimage/costfns.h" +#include "nlohmann/json.hpp" using namespace std; using namespace NEWMAT; using namespace NEWIMAGE; +using json = nlohmann::json; int print_usage(const string &progname) { cout << "Usage: fslstats [preoptions] <input> [options]" << endl << endl; + // add --json output format option + // keys will correspond to the options above + cout << "preoption -json: output in JSON format to standard out" << endl; cout << "preoption -t will give a separate output line for each 3D volume of a 4D timeseries" << endl; cout << "preoption -K < indexMask > will generate seperate n submasks from indexMask, for indexvalues 1..n where n is the maximum index value in indexMask, and generate statistics for each submask" << endl; cout << "Note - options are applied in order, e.g. -M -l 10 -M will report the non-zero mean, apply a threshold and then report the new nonzero mean" << endl @@ -51,11 +57,7 @@ int print_usage(const string &progname) cout << "-k <mask> : use the specified image (filename) for masking - overrides lower and upper thresholds" << endl; cout << "-d <image> : take the difference between the base image and the image specified here" << endl; cout << "-h <nbins> : output a histogram (for the thresholded/masked voxels only) with nbins" << endl; - cout << "-H <nbins> <min> <max> : output a histogram (for the thresholded/masked voxels only) with nbins and histogram limits of min and max" << endl; - // add --json output format option - // keys will correspond to the options above - cout << "--json : output in JSON format" << endl - << endl; + cout << "-H <nbins> <min> <max> : output a histogram (for the thresholded/masked voxels only) with nbins and histogram limits of min and max" << endl; cout << "Note - thresholds are not inclusive ie lthresh<allowed<uthresh" << endl; return 1; } @@ -120,38 +122,35 @@ int generate_masks(volume4D<float> &mask, volume4D<float> &masknz, const volume4 return generateNonZeroMask(mask, masknz, input); } -// print function that can handle outputting in JSON format or not -void print_value(const string &key, const string &value, string &json, const bool jsonOutput) +// print function that can handle outputting in JSON format or not. Use a template to allow for different types +template <typename T> +void update_json(string key, T &value, json &j) { - // append the key and value to the json string - if (jsonOutput) - { - json.append("\"" + key + "\": \"" + value + "\", "); - } - else - { - cout << value << " "; - } + j[key] = value; } +template <typename T> +void update_json(string key, initializer_list<T> &value, json &j) +{ + j[key] = value; +} -int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const string &indexMaskName) +template <typename T> +void update_json(string key, vector<T> &value, json &j) { - // determine if the user wants to output in JSON format - bool jsonOutput = false; - for (int i = 1; i < argc; i++) - { - if (strcmp(argv[i], "--json") == 0) - { - jsonOutput = true; - break; - } - } - // if JSON output is requested, then we need to store the output as a string - // and then output it at the end via cout - // don't forget to close the json at the end with a "}" - string json = ""; - + j[key] = value; +} + +void update_json(string key, ColumnVector &value, json &j) +{ + j[key] = value; +} + +int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const string &indexMaskName, const bool jsonMode = false) +{ + + json js; + cout.setf(ios::dec); cout.setf(ios::fixed, ios::floatfield); cout.setf(ios::left, ios::adjustfield); @@ -167,27 +166,16 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st read_volume4D(indexMask, indexMaskName); int nTimepoints((timeseriesMode) ? inputMaster.tsize() : 1), nIndices((indexMaskName != "") ? indexMask.max() : 1); + // initialise the volumes array in the json object. This will get appended to later + js["volumes"] = json::array(); + for (int timepoint = 0; timepoint < nTimepoints; timepoint++) { - if (jsonOutput) - { - if (timepoint == 0){ - json.append("{\"volumes\": ["); - } else { - json.append(", "); - } - } + json tp = json::object(); + tp["indices"] = json::array(); for (int index = 1; index <= nIndices; index++) { - // if json mode is enabled - if (jsonOutput) - { - if (index == 1){ - json.append("{\"indices\": [{"); - } else { - json.append(", {"); - } - } + json idx = json::object(); if (timeseriesMode) vol = inputMaster[timepoint]; volume<float> mask, masknz; @@ -229,23 +217,41 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st else if (sarg == "-m") { if (mask.nvoxels() > 0){ - string val = to_string(vol.mean(mask)); - print_value(sarg, val, json, jsonOutput); + auto val = vol.mean(mask); + if (jsonMode){ + update_json(sarg, val, idx); + } else { + cout << val << " "; + } } else { - string val = to_string(vol.mean()); - print_value(sarg, val, json, jsonOutput); + // print_value(sarg, val, json, jsonMode); + auto val = vol.mean(); + if (jsonMode){ + update_json(sarg, val, idx); + } else { + cout << val << " "; + } } } else if (sarg == "-M") { - if (masknz.nvoxels() > 0) - print_value(sarg, to_string(vol.mean(masknz)), json, jsonOutput); + if (masknz.nvoxels() > 0){ + auto val = vol.mean(masknz); + if (jsonMode){ + update_json(sarg, val, idx); + } else { + cout << val << " "; + } + } else { double nzmean = 0; nzmean = nonzeromean(vol); - string val = to_string(nzmean); - print_value(sarg, val, json, jsonOutput); + if (jsonMode){ + update_json(sarg, nzmean, idx); + } else { + cout << nzmean << " "; + } } } else if (sarg == "-X") @@ -259,17 +265,12 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st auto coord1 = MISCMATHS::round(coord(1)); auto coord2 = MISCMATHS::round(coord(2)); auto coord3 = MISCMATHS::round(coord(3)); - string coordString = ""; - coordString.append(to_string(coord1)); - coordString.append(" "); - coordString.append(to_string(coord2)); - coordString.append(" "); - coordString.append(to_string(coord3)); - print_value( - sarg, - coordString, - json, - jsonOutput); + auto coordList = {coord1, coord2, coord3}; + if (jsonMode){ + update_json(sarg, coordList, idx); + } else { + cout << coord1 << " " << coord2 << " " << coord3 << " "; + } } else if (sarg == "-x") @@ -283,17 +284,12 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st auto coord1 = MISCMATHS::round(coord(1)); auto coord2 = MISCMATHS::round(coord(2)); auto coord3 = MISCMATHS::round(coord(3)); - string coordString = ""; - coordString.append(to_string(coord1)); - coordString.append(" "); - coordString.append(to_string(coord2)); - coordString.append(" "); - coordString.append(to_string(coord3)); - print_value( - sarg, - coordString, - json, - jsonOutput); + auto coordList = {coord1, coord2, coord3}; + if (jsonMode){ + update_json(sarg, coordList, idx); + } else { + cout << coord1 << " " << coord2 << " " << coord3 << " "; + } } else if (sarg == "-w") { @@ -357,28 +353,12 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st zmax = zmin; zmin = tmp; } - // now output nifti coords - string coords = ""; - coords.append(to_string(xmin)); - coords.append(" "); - coords.append(to_string(1 + xmax - xmin)); - coords.append(" "); - coords.append(to_string(ymin)); - coords.append(" "); - coords.append(to_string(1 + ymax - ymin)); - coords.append(" "); - coords.append(to_string(zmin)); - coords.append(" "); - coords.append(to_string(1 + zmax - zmin)); - coords.append(" "); - coords.append(to_string(tmin)); - coords.append(" "); - coords.append(to_string(1 + tmax - tmin)); - print_value( - sarg, - coords, - json, - jsonOutput); + vector<int> coords = {xmin, 1 + xmax - xmin, ymin, 1 + ymax - ymin, zmin, 1 + zmax - zmin, tmin, 1 + tmax - tmin}; + if (jsonMode){ + update_json(sarg, coords, idx); + } else { + cout << coords[0] << " " << coords[1] << " " << coords[2] << " " << coords[3] << " " << coords[4] << " " << coords[5] << " " << coords[6] << " " << coords[7] << " "; + } } else if (sarg == "-e") { @@ -400,12 +380,11 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st } } entropy /= log((double)nbins); - print_value( - sarg, - to_string(entropy), - json, - jsonOutput - ); + if (jsonMode){ + update_json(sarg, entropy, idx); + } else { + cout << entropy << " "; + } } else if (sarg == "-E") { @@ -427,12 +406,11 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st } } entropy /= log((double)nbins); - print_value( - sarg, - to_string(entropy), - json, - jsonOutput - ); + if (jsonMode){ + update_json(sarg, entropy, idx); + } else { + cout << entropy << " "; + } } else if (sarg == "-k") { @@ -547,125 +525,125 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st if (mask.tsize() == 1) nvox = nvox * vol.tsize(); - string nvox_str = ""; - nvox_str.append(to_string(nvox)); - nvox_str.append(" "); - nvox_str.append(to_string(nvox * vol.xdim() * vol.ydim() * vol.zdim())); - print_value( - sarg, - nvox_str, - json, - jsonOutput - ); + auto volume_v = nvox * vol.xdim() * vol.ydim() * vol.zdim(); + // promote nvox to float to maintain types in list + auto nvox_volume = {static_cast<float>(nvox), volume_v}; + if (jsonMode){ + update_json(sarg, nvox_volume, idx); + } else { + cout << nvox << " " << volume_v << " "; + } } else { - string nvox_str = ""; - nvox_str.append(to_string((long int)vol.nvoxels() * vol.tsize())); - nvox_str.append(" "); - nvox_str.append(to_string(vol.nvoxels() * vol.tsize() * vol.xdim() * vol.ydim() * vol.zdim())); - print_value( - sarg, - nvox_str, - json, - jsonOutput); + long int nvox = vol.nvoxels() * vol.tsize(); + auto volume_v = nvox * vol.xdim() * vol.ydim() * vol.zdim(); + // promote nvox to float to maintain types in list + auto nvox_volume = {static_cast<float>(nvox), volume_v}; + if (jsonMode){ + update_json(sarg, nvox_volume, idx); + } else { + cout << nvox << " " << volume_v << " "; + } } } else if (sarg == "-V") { if (masknz.nvoxels() > 0) { - string nvox_str = ""; - nvox_str.append(to_string((long int)masknz.sum())); - nvox_str.append(" "); - nvox_str.append(to_string(masknz.sum() * vol.xdim() * vol.ydim() * vol.zdim())); - print_value( - sarg, - nvox_str, - json, - jsonOutput); + long int nvox = masknz.sum(); + auto volume_V = nvox * vol.xdim() * vol.ydim() * vol.zdim(); + // promote nvox to float to maintain types in list + auto nvox_volume = {static_cast<float>(nvox), volume_V}; + if (jsonMode){ + update_json(sarg, nvox_volume, idx); + } else { + cout << nvox << " " << volume_V << " "; + } } else { long int nzvox; nzvox = nonzerocount(vol); - string nzvox_str = ""; - nzvox_str.append(to_string(nzvox)); - nzvox_str.append(" "); - nzvox_str.append(to_string(nzvox * vol.xdim() * vol.ydim() * vol.zdim())); - print_value( - sarg, - nzvox_str, - json, - jsonOutput); + auto volume_V = nzvox * vol.xdim() * vol.ydim() * vol.zdim(); + // promate nzvox to float to maintain types in list + auto nvox_volume = {static_cast<float>(nzvox), volume_V}; + if (jsonMode){ + update_json(sarg, nvox_volume, idx); + } else { + cout << nzvox << " " << volume_V << " "; + } } } else if (sarg == "-D") { // hidden debug option! - print_value( - sarg, - to_string(vol.sum()), - json, - jsonOutput - ); + auto sum = vol.sum(); + if (jsonMode){ + update_json(sarg, sum, idx); + } else { + cout << sum << " "; + } } else if (sarg == "-s") { - if (mask.nvoxels() > 0) - print_value( - sarg, - to_string(vol.stddev(mask)), - json, - jsonOutput - ); - else - print_value( - sarg, - to_string(vol.stddev()), - json, - jsonOutput - ); + if (mask.nvoxels() > 0){ + auto stddev = vol.stddev(mask); + if (jsonMode){ + update_json(sarg, stddev, idx); + } else { + cout << stddev << " "; + } + } + else { + auto stddev = vol.stddev(); + if (jsonMode){ + update_json(sarg, stddev, idx); + } else { + cout << stddev << " "; + } + } } else if (sarg == "-S") { if (masknz.nvoxels() > 0) { - print_value( - sarg, - to_string(vol.stddev(masknz)), - json, - jsonOutput - ); + auto stddev = vol.stddev(masknz); + if (jsonMode){ + update_json(sarg, stddev, idx); + } else { + cout << stddev << " "; + } } else { - print_value( - sarg, - to_string(nonzerostddev(vol)), - json, - jsonOutput - ); + auto stddev = nonzerostddev(vol); + if (jsonMode){ + update_json(sarg, stddev, idx); + } else { + cout << stddev << " "; + } } } else if (sarg == "-r") { vector<float> limits(vol.robustlimits(mask)); - print_value( - sarg, - to_string(limits[0]).append(" ").append(to_string(limits[1])), - json, - jsonOutput - ); + if (jsonMode){ + update_json(sarg, limits, idx); + } else { + cout << limits[0] << " " << limits[1] << " "; + } } else if (sarg == "-R") { - print_value( - sarg, - to_string(vol.min(mask)).append(" ").append(to_string(vol.max(mask))), - json, - jsonOutput - ); + auto min_R = vol.min(mask); + auto max_R = vol.max(mask); + auto minmax_R = {min_R, max_R}; + if (jsonMode){ + update_json(sarg, minmax_R, idx); + } else { + cout << min_R << " " << max_R << " "; + } } else if (sarg == "-c") { @@ -673,12 +651,15 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st cog.SubMatrix(1, 3, 1, 1) = vol.cog(); cog(4) = 1.0; cog = vol.newimagevox2mm_mat() * cog; - print_value( - sarg, - to_string(cog(1)).append(" ").append(to_string(cog(2))).append(" ").append(to_string(cog(3))), - json, - jsonOutput - ); + // remove the last element + cog = cog.Rows(1, 3); + // reshape to a row vector + cog = cog.t(); + if (jsonMode){ + update_json(sarg, cog, idx); + } else { + cout << cog(1) << " " << cog(2) << " " << cog(3) << " "; + } } else if (sarg == "-C") { @@ -687,12 +668,15 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st cog.SubMatrix(1, 3, 1, 1) = vol.cog(); cog(4) = 1.0; cog = vol.niftivox2newimagevox_mat().i() * cog; - print_value( - sarg, - to_string(cog(1)).append(" ").append(to_string(cog(2))).append(" ").append(to_string(cog(3))), - json, - jsonOutput - ); + // remove the last element + cog = cog.Rows(1, 3); + // reshape to a row vector + cog = cog.t(); + if (jsonMode){ + update_json(sarg, cog, idx); + } else { + cout << cog(1) << " " << cog(2) << " " << cog(3) << " "; + } } else if (sarg == "-p") { @@ -712,20 +696,22 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st cerr << "Percentile must be between 0 and 100" << endl; exit(1); } - if (mask.nvoxels() > 0) - print_value( - sarg, - to_string(vol.percentile((float)n / 100.0, mask)), - json, - jsonOutput - ); - else - print_value( - sarg, - to_string(vol.percentile((float)n / 100.0)), - json, - jsonOutput - ); + if (mask.nvoxels() > 0){ + auto percentile_p = vol.percentile((float)n / 100.0, mask); + if (jsonMode){ + update_json(sarg, percentile_p, idx); + } else { + cout << percentile_p << " "; + } + } + else { + auto percentile_p = vol.percentile((float)n / 100.0); + if (jsonMode){ + update_json(sarg, percentile_p, idx); + } else { + cout << percentile_p << " "; + } + } } else if (sarg == "-P") { @@ -750,12 +736,12 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st generate_masks(mask, masknz, vol, lthr, uthr); vol *= mask; } - print_value( - sarg, - to_string(vol.percentile((float)n / 100.0, masknz)), - json, - jsonOutput - ); + auto percentile_P = vol.percentile((float)n / 100.0, masknz); + if (jsonMode){ + update_json(sarg, percentile_P, idx); + } else { + cout << percentile_P << " "; + } } else if (sarg == "-h") { @@ -779,41 +765,20 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st if (mask.nvoxels() > 0) { auto hist = vol.histogram(nbins, vol.min(), vol.max(), mask); - // loop over histogram values and and build a string to print - string hist_str = ""; - for (int i = 0; i < hist.Nrows(); i++) - { - hist_str += to_string(hist(i+1)); - if (i < hist.Nrows() - 1) - { - hist_str += " "; - } + if (jsonMode){ + update_json(sarg, hist, idx); + } else { + cout << hist << " "; } - print_value( - sarg, - hist_str, - json, - jsonOutput - ); } else { auto hist = vol.histogram(nbins, vol.min(), vol.max()); - string hist_str = ""; - for (int i = 0; i < hist.Nrows(); i++) - { - hist_str += to_string(hist(i+1)); - if (i < hist.Nrows() - 1) - { - hist_str += " "; - } + if (jsonMode){ + update_json(sarg, hist, idx); + } else { + cout << hist << " "; } - print_value( - sarg, - hist_str, - json, - jsonOutput - ); } } else if (sarg == "-H") @@ -859,91 +824,41 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st } if (mask.nvoxels() > 0) { - auto hist = vol.histogram(nbins, min, max, mask); - string hist_str = ""; - for (int i = 0; i < hist.Nrows(); i++) - { - hist_str += to_string(hist(i+1)); - if (i < hist.Nrows() - 1) - { - hist_str += " "; - } + auto hist = vol.histogram(nbins,min,max,mask); + if (jsonMode){ + update_json(sarg, hist, idx); + } else { + cout << hist << " "; } - print_value( - sarg, - hist_str, - json, - jsonOutput - ); } else { auto hist = vol.histogram(nbins, min, max); - string hist_str = ""; - for (int i = 0; i < hist.Nrows(); i++) - { - hist_str += to_string(hist(i+1)); - if (i < hist.Nrows() - 1) - { - hist_str += " "; - } + if (jsonMode){ + update_json(sarg, hist, idx); + } else { + cout << hist << " "; } - print_value( - sarg, - hist_str, - json, - jsonOutput - ); } } else { - // only print cerr if not the --json option - if (sarg != "--json") - { - cerr << "Unrecognised option: " << sarg << endl; - exit(3); - } else if (sarg == "--json" && last) { - // remove the trailing comma and space - json.pop_back(); - json.pop_back(); - } + cerr << "Unrecognised option: " << sarg << endl; + exit(3); } - narg++; } - // if json mode is enabled - if (jsonOutput) - { - // close the json object - json.append("}"); - } - if (index < nIndices && !jsonOutput) - { + tp["indices"] += idx; + if (!jsonMode) cout << endl; - } - // cout << endl; } // end nIndices - // if json mode is enabled - if (jsonOutput) - { - // close the json object - json.append("]}"); - } - if (timepoint < nTimepoints - 1 && !jsonOutput) - { - cout << endl; - } - // cout << endl; + // append the timepoint to the the volumes array + js["volumes"] += tp; } // end timepoints - if (jsonOutput) - { - // close the json object - json.append("]}"); - cout << json; - } else { - cout << endl; - } + if (jsonMode){ + // print the json with 4 spaces of indentation + cout << js.dump(4) << endl; + } return 0; } @@ -953,11 +868,14 @@ int main(int argc, char *argv[]) Tracer tr("main"); string progname(argv[0]); bool timeseriesMode(false); + bool jsonMode(false); string indexMask(""); - while (argc > 2 && (string(argv[1]) == "-t" || string(argv[1]) == "-K")) + while (argc > 2 && (string(argv[1]) == "-t" || string(argv[1]) == "-K" || string(argv[1]) == "-json")) { if (string(argv[1]) == "-t") timeseriesMode = true; + if (string(argv[1]) == "-json") + jsonMode = true; if (string(argv[1]) == "-K") { indexMask = string(argv[2]); @@ -971,7 +889,7 @@ int main(int argc, char *argv[]) return print_usage(progname); try { - return fmrib_main_float(argc, argv, timeseriesMode, indexMask); + return fmrib_main_float(argc, argv, timeseriesMode, indexMask, jsonMode); } catch (std::exception &e) { diff --git a/tests/fslstats/feedsRun b/tests/fslstats/feedsRun index c6d1305..a007f1d 100755 --- a/tests/fslstats/feedsRun +++ b/tests/fslstats/feedsRun @@ -53,6 +53,8 @@ options = [ {"option": "-v", "expected": "1000 1000.000000"}, {"option": "-V", "expected": "1000 1000.000000"}, {"option": "-m", "expected": "0.495922"}, + {"option": "-m -a", "expected": "0.495922"}, + {"option": "-m -a -n", "expected": "0.495922"}, {"option": "-M", "expected": "0.495922"}, {"option": "-s", "expected": "0.290744"}, {"option": "-S", "expected": "0.290744"}, @@ -63,6 +65,12 @@ options = [ {"option": "-C", "expected": "4.498706 4.545873 4.613188"}, {"option": "-p 50", "expected": "0.482584"}, {"option": "-P 50", "expected": "0.482584"}, + {"option": "-m -h 10 -c", "expected": "0.495922 101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000 \n 4.498706 4.545873 4.613188"}, + {"option": "-m -h 10", "expected": "0.495922 101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, + {"option": "-h 10", "expected": "101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, + {"option": "-H 10 0 1", "expected": "98.000000 \n102.000000 \n107.000000 \n108.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, + {"option": "-l 0.25 -u 0.75 -m", "expected": "0.495150"}, + {"option": "-d imageForDiff.nii.gz -m", "expected": "0.000000"}, ] # create a list of tests and expected results @@ -82,55 +90,24 @@ tests_with_preoptions = [ "preoptions": "-K", "mask": "mask.nii.gz", "options": "-m", - "expected": "0.948930 \n0.947671 \n1.003258 \n1.010696", + "expected": "1.078044 \n1.028827 \n0.986428 \n1.020869", }, { "preoptions": "-t -K", "mask": "mask.nii.gz", "options": "-m", - "expected": "0.459736 \n0.476035 \n0.504080 \n0.549485 \n0.489194 \n0.471636 \n0.499178 \n0.461211", + "expected": "0.526682 \n0.515652 \n0.492337 \n0.511661 \n0.551362 \n0.513176 \n0.494091 \n0.509208", }, { "preoptions": "-t", "mask": "", "options": "-m", - "expected": "0.496675 \n0.487950", + "expected": "0.503236 \n0.504035", }, ] -# taken from fslchpixdim test -def create_image(shape, pixdim): - pixdim = list(pixdim) - data = np.random.random(shape).astype(np.float32) - hdr = nib.Nifti1Header() - hdr.set_data_dtype(np.float32) - hdr.set_data_shape(shape) - hdr.set_zooms(pixdim[:len(shape)]) - - return nib.Nifti1Image(data, np.eye(4), hdr) - -def create_mask(input_img, n_rois=4): - ''' - Create a mask image from the input image. - The mask image will have n_rois different values with random sizes randomly placed in the image. - ''' - data = input_img.get_fdata() - mask = np.zeros_like(data) - for i in range(n_rois + 1): - roi_size = np.random.randint(1, data.size) - roi_value = i - roi = np.random.choice(data.size, roi_size, replace=False) - mask.flat[roi] = roi_value - return nib.Nifti1Image(mask, input_img.affine, input_img.header) - - def test_fslstats(): imgfile = 'test.nii.gz' - shape = (10,10,10) - img = create_image(shape, [1] * len(shape)) - img.to_filename(imgfile) - mask = create_mask(img) - mask.to_filename('mask.nii.gz') # run the tests witoout preoptions for test in tests: @@ -151,9 +128,6 @@ def test_fslstats(): # run the tests with preoptions imgfile = 'test_4d.nii.gz' - shape = (10,10,10, 2) - img = create_image(shape, [1] * len(shape)) - img.to_filename(imgfile) for test in tests_with_preoptions: cmd = f"fslstats {test['preoptions']} {test['mask']} {imgfile} {test['options']}" # remove any double spaces from empty test options @@ -175,6 +149,4 @@ def test_fslstats(): if __name__ == "__main__": - if len(sys.argv) > 1: - os.chdir(sys.argv[1]) test_fslstats() diff --git a/tests/fslstats/imageForDiff.nii.gz b/tests/fslstats/imageForDiff.nii.gz new file mode 100644 index 0000000..f4d00fe --- /dev/null +++ b/tests/fslstats/imageForDiff.nii.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14105cd775bf8e192299695a90848e285690c2dafb086bdd8830c5665fb0394b +size 3716 diff --git a/tests/fslstats/mask.nii.gz b/tests/fslstats/mask.nii.gz new file mode 100644 index 0000000..3a7c155 --- /dev/null +++ b/tests/fslstats/mask.nii.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9a0b1719ca95cf36b44ddcde54121fa28411d060ab0d288303c80935eb429b1 +size 786 diff --git a/tests/fslstats/test.nii.gz b/tests/fslstats/test.nii.gz new file mode 100644 index 0000000..f4d00fe --- /dev/null +++ b/tests/fslstats/test.nii.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14105cd775bf8e192299695a90848e285690c2dafb086bdd8830c5665fb0394b +size 3716 diff --git a/tests/fslstats/test_4d.nii.gz b/tests/fslstats/test_4d.nii.gz new file mode 100644 index 0000000..247023d --- /dev/null +++ b/tests/fslstats/test_4d.nii.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a48fc73f54d9aba889968a99d58339d495950301970f166166bede44dd38e623 +size 7304 -- GitLab From 475ab8aa0e0cf382ed6414d16e4a983ba596837b Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauldmccarthy@gmail.com> Date: Mon, 26 Jun 2023 11:22:31 +0100 Subject: [PATCH 4/6] TEST: avscale is part of flirt, not avwutils --- tests/avscale/feedsRun | 116 ----------------------------------------- 1 file changed, 116 deletions(-) delete mode 100755 tests/avscale/feedsRun diff --git a/tests/avscale/feedsRun b/tests/avscale/feedsRun deleted file mode 100755 index 32062a0..0000000 --- a/tests/avscale/feedsRun +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env fslpython - -# test avscale with different input affines - - -import sys -import os - -import fsl.utils.run as run - -import numpy as np - - -PI = np.pi -PION2 = np.pi / 2 - - -# Each test has the form (input-affine, expected-output) -# where expected-output comprises: -# - scales -# - translations -# - rotations -# - skews -# - L/R orientation (preserved or swapped) -tests = [ - - ([[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (0, 0, 0), (0, 0, 0), 'preserved']), - ([[2, 0, 0, 0], - [0, 2, 0, 0], - [0, 0, 2, 0], - [0, 0, 0, 1]], [(2, 2, 2), (0, 0, 0), (0, 0, 0), (0, 0, 0), 'preserved']), - ([[-2, 0, 0, 0], - [ 0, 2, 0, 0], - [ 0, 0, 2, 0], - [ 0, 0, 0, 1]], [(2, 2, 2), (0, 0, 0), (0, 0, PI), (0, 0, 0), 'swapped']), - ([[0, 0, 1, 0], - [0, 1, 0, 0], - [1, 0, 0, 0], - [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (0, -PION2, 0), (0, 0, 0), 'swapped']), - ([[ 0, 0, -1, 0], - [ 0, -1, 0, 0], - [-1, 0, 0, 0], - [ 0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (-PI, PION2, 0), (0, 0, 0), 'preserved']), - ([[0, 1, 0, 0], - [1, 0, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (0, 0, PION2), (0, 0, 0), 'swapped']), - ([[1, 0, 0, 0], - [0, 0, 1, 0], - [0, 1, 0, 0], - [0, 0, 0, 1]], [(1, 1, 1), (0, 0, 0), (PION2, 0, 0), (0, 0, 0), 'swapped']), - ([[-2, 0, 0, 90], - [ 0, 2, 0, -126], - [ 0, 0, 2, -72], - [ 0, 0, 0, 1]], [(2, 2, 2), (90, -126, -72), (0, 0, PI), (0, 0, 0), 'swapped']), - -] - -def read_avscale_output(output): - lines = output.split('\n') - # scales - # translations - # rotations - # skews - # l/r orientation (preserved or swapped) - scales = [l for l in lines if l.startswith('Scales ')] [0] - translations = [l for l in lines if l.startswith('Translations ')] [0] - rotations = [l for l in lines if l.startswith('Rotation Angles')][0] - skews = [l for l in lines if l.startswith('Skews ')] [0] - orient = [l for l in lines if l.startswith('Left-Right ')] [0] - - scales = [float(s) for s in scales .split()[-3:]] - translations = [float(s) for s in translations.split()[-3:]] - rotations = [float(s) for s in rotations .split()[-3:]] - skews = [float(s) for s in skews .split()[-3:]] - orient = orient.split()[-1] - - return { - 'scales' : np.array(scales), - 'translations' : np.array(translations), - 'rotations' : np.array(rotations), - 'skews' : np.array(skews), - 'orient' : orient.lower(), - } - - -def test_avscale(): - - for affine, expected in tests: - affine = np.array(affine) - np.savetxt('affine.mat', affine, '%0.6f') - print(affine) - output = run.runfsl('avscale --allparams affine.mat') - output = read_avscale_output(output) - - scales, translations, rotations, skews, orient = expected - - try: - assert np.all(np.isclose(scales, output['scales'])) - assert np.all(np.isclose(translations, output['translations'])) - assert np.all(np.isclose(rotations, output['rotations'])) - assert np.all(np.isclose(skews, output['skews'])) - assert orient == output['orient'] - except AssertionError: - print(affine) - print(output) - raise - - -if __name__ == '__main__': - if len(sys.argv) > 1: - os.chdir(sys.argv[1]) - test_avscale() -- GitLab From 6ed1dff606f4b2d92b59851531dd75782cd03cba Mon Sep 17 00:00:00 2001 From: Taylor Hanayik <hanayik@gmail.com> Date: Mon, 26 Jun 2023 15:23:28 +0100 Subject: [PATCH 5/6] add json tests; code cleanup; --- fslstats.cc | 2 - tests/fslstats/feedsRun | 113 ++++++++++++++---- tests/fslstats/test_cogMM_c.json | 15 +++ tests/fslstats/test_cogVox_C.json | 15 +++ .../test_diff_d_imageForDiff.nii.gz_m.json | 11 ++ tests/fslstats/test_entropyNonzero_E.json | 11 ++ tests/fslstats/test_entropy_e.json | 11 ++ tests/fslstats/test_histLimits_H_10_0_1.json | 22 ++++ tests/fslstats/test_hist_h_10.json | 22 ++++ tests/fslstats/test_maxCoord_x.json | 15 +++ tests/fslstats/test_meanHistCog_m_h_10_c.json | 28 +++++ tests/fslstats/test_meanHist_m_h_10.json | 23 ++++ tests/fslstats/test_meanNonzero_M.json | 11 ++ tests/fslstats/test_mean_m.json | 11 ++ tests/fslstats/test_mean_m_a.json | 11 ++ tests/fslstats/test_mean_m_a_n.json | 11 ++ tests/fslstats/test_minCoord_X.json | 15 +++ tests/fslstats/test_notRobust_R.json | 14 +++ .../fslstats/test_percentileNonzero_P_50.json | 11 ++ tests/fslstats/test_percentile_p_50.json | 11 ++ ...st_preopIndexMaskMeanTimeSeries_t_K_m.json | 36 ++++++ .../fslstats/test_preopIndexMaskMean_K_m.json | 20 ++++ .../test_preopMeanTimeSeries_t_m.json | 18 +++ tests/fslstats/test_robust_r.json | 14 +++ tests/fslstats/test_roi_w.json | 20 ++++ tests/fslstats/test_stdevNonzero_S.json | 11 ++ tests/fslstats/test_stdev_s.json | 11 ++ .../test_threshold_l_0.25_u_0.75_m.json | 11 ++ tests/fslstats/test_volumeNonzero_V.json | 14 +++ tests/fslstats/test_volume_v.json | 14 +++ 30 files changed, 525 insertions(+), 27 deletions(-) create mode 100644 tests/fslstats/test_cogMM_c.json create mode 100644 tests/fslstats/test_cogVox_C.json create mode 100644 tests/fslstats/test_diff_d_imageForDiff.nii.gz_m.json create mode 100644 tests/fslstats/test_entropyNonzero_E.json create mode 100644 tests/fslstats/test_entropy_e.json create mode 100644 tests/fslstats/test_histLimits_H_10_0_1.json create mode 100644 tests/fslstats/test_hist_h_10.json create mode 100644 tests/fslstats/test_maxCoord_x.json create mode 100644 tests/fslstats/test_meanHistCog_m_h_10_c.json create mode 100644 tests/fslstats/test_meanHist_m_h_10.json create mode 100644 tests/fslstats/test_meanNonzero_M.json create mode 100644 tests/fslstats/test_mean_m.json create mode 100644 tests/fslstats/test_mean_m_a.json create mode 100644 tests/fslstats/test_mean_m_a_n.json create mode 100644 tests/fslstats/test_minCoord_X.json create mode 100644 tests/fslstats/test_notRobust_R.json create mode 100644 tests/fslstats/test_percentileNonzero_P_50.json create mode 100644 tests/fslstats/test_percentile_p_50.json create mode 100644 tests/fslstats/test_preopIndexMaskMeanTimeSeries_t_K_m.json create mode 100644 tests/fslstats/test_preopIndexMaskMean_K_m.json create mode 100644 tests/fslstats/test_preopMeanTimeSeries_t_m.json create mode 100644 tests/fslstats/test_robust_r.json create mode 100644 tests/fslstats/test_roi_w.json create mode 100644 tests/fslstats/test_stdevNonzero_S.json create mode 100644 tests/fslstats/test_stdev_s.json create mode 100644 tests/fslstats/test_threshold_l_0.25_u_0.75_m.json create mode 100644 tests/fslstats/test_volumeNonzero_V.json create mode 100644 tests/fslstats/test_volume_v.json diff --git a/fslstats.cc b/fslstats.cc index 8e90fd2..b29d9f9 100644 --- a/fslstats.cc +++ b/fslstats.cc @@ -199,11 +199,9 @@ int fmrib_main_float(int argc, char *argv[], const bool timeseriesMode, const st generateNonZeroMask(mask, masknz, vol); } int narg(2); - bool last = false; while (narg < argc) { - last = !(narg < argc - 1); string sarg(argv[narg]); if (sarg == "-n") { diff --git a/tests/fslstats/feedsRun b/tests/fslstats/feedsRun index a007f1d..8e722db 100755 --- a/tests/fslstats/feedsRun +++ b/tests/fslstats/feedsRun @@ -46,31 +46,31 @@ import nibabel as nib np.random.seed(0) options = [ - {"option": "-r", "expected": "0.018533 0.978824"}, - {"option": "-R", "expected": "0.000546 0.999809"}, - {"option": "-e", "expected": "0.906103"}, - {"option": "-E", "expected": "0.906103"}, - {"option": "-v", "expected": "1000 1000.000000"}, - {"option": "-V", "expected": "1000 1000.000000"}, - {"option": "-m", "expected": "0.495922"}, - {"option": "-m -a", "expected": "0.495922"}, - {"option": "-m -a -n", "expected": "0.495922"}, - {"option": "-M", "expected": "0.495922"}, - {"option": "-s", "expected": "0.290744"}, - {"option": "-S", "expected": "0.290744"}, - {"option": "-w", "expected": "0 10 0 10 0 10 0 1"}, - {"option": "-x", "expected": "9 7 4"}, - {"option": "-X", "expected": "8 2 1"}, - {"option": "-c", "expected": "4.498706 4.545873 4.613188"}, - {"option": "-C", "expected": "4.498706 4.545873 4.613188"}, - {"option": "-p 50", "expected": "0.482584"}, - {"option": "-P 50", "expected": "0.482584"}, - {"option": "-m -h 10 -c", "expected": "0.495922 101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000 \n 4.498706 4.545873 4.613188"}, - {"option": "-m -h 10", "expected": "0.495922 101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, - {"option": "-h 10", "expected": "101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, - {"option": "-H 10 0 1", "expected": "98.000000 \n102.000000 \n107.000000 \n108.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, - {"option": "-l 0.25 -u 0.75 -m", "expected": "0.495150"}, - {"option": "-d imageForDiff.nii.gz -m", "expected": "0.000000"}, + {"name": "robust", "option": "-r", "expected": "0.018533 0.978824"}, + {"name": "notRobust", "option": "-R", "expected": "0.000546 0.999809"}, + {"name": "entropy", "option": "-e", "expected": "0.906103"}, + {"name": "entropyNonzero", "option": "-E", "expected": "0.906103"}, + {"name": "volume", "option": "-v", "expected": "1000 1000.000000"}, + {"name": "volumeNonzero", "option": "-V", "expected": "1000 1000.000000"}, + {"name": "mean", "option": "-m", "expected": "0.495922"}, + {"name": "mean", "option": "-m -a", "expected": "0.495922"}, + {"name": "mean", "option": "-m -a -n", "expected": "0.495922"}, + {"name": "meanNonzero", "option": "-M", "expected": "0.495922"}, + {"name": "stdev", "option": "-s", "expected": "0.290744"}, + {"name": "stdevNonzero", "option": "-S", "expected": "0.290744"}, + {"name": "roi","option": "-w", "expected": "0 10 0 10 0 10 0 1"}, + {"name": "maxCoord", "option": "-x", "expected": "9 7 4"}, + {"name": "minCoord", "option": "-X", "expected": "8 2 1"}, + {"name": "cogMM", "option": "-c", "expected": "4.498706 4.545873 4.613188"}, + {"name": "cogVox","option": "-C", "expected": "4.498706 4.545873 4.613188"}, + {"name": "percentile", "option": "-p 50", "expected": "0.482584"}, + {"name": "percentileNonzero","option": "-P 50", "expected": "0.482584"}, + {"name": "meanHistCog", "option": "-m -h 10 -c", "expected": "0.495922 101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000 \n 4.498706 4.545873 4.613188"}, + {"name": "meanHist", "option": "-m -h 10", "expected": "0.495922 101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, + {"name": "hist", "option": "-h 10", "expected": "101.000000 \n99.000000 \n108.000000 \n107.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, + {"name": "histLimits", "option": "-H 10 0 1", "expected": "98.000000 \n102.000000 \n107.000000 \n108.000000 \n102.000000 \n93.000000 \n99.000000 \n88.000000 \n95.000000 \n108.000000"}, + {"name": "threshold", "option": "-l 0.25 -u 0.75 -m", "expected": "0.495150"}, + {"name": "diff", "option": "-d imageForDiff.nii.gz -m", "expected": "0.000000"}, ] # create a list of tests and expected results @@ -82,6 +82,7 @@ for option in options: "mask": "", "options": option["option"], "expected": option["expected"], + "name": option["name"], } ) @@ -91,21 +92,49 @@ tests_with_preoptions = [ "mask": "mask.nii.gz", "options": "-m", "expected": "1.078044 \n1.028827 \n0.986428 \n1.020869", + "name": "preopIndexMaskMean", }, { "preoptions": "-t -K", "mask": "mask.nii.gz", "options": "-m", "expected": "0.526682 \n0.515652 \n0.492337 \n0.511661 \n0.551362 \n0.513176 \n0.494091 \n0.509208", + "name": "preopIndexMaskMeanTimeSeries", }, { "preoptions": "-t", "mask": "", "options": "-m", "expected": "0.503236 \n0.504035", + "name": "preopMeanTimeSeries", }, ] +json_tests = [] +for option in options: + file_string = f"test_{option['name']}_{option['option'].replace(' ', '_')}.json" + file_string = file_string.replace("-", "") + json_tests.append( + { + "preoptions": "", + "mask": "", + "options": option["option"], + "expected": file_string, + } + ) + +for option in tests_with_preoptions: + file_string = f"test_{option['name']}_{option['preoptions'].replace(' ', '_')}_{option['options'].replace(' ', '_')}.json" + file_string = file_string.replace("-", "") + json_tests.append( + { + "preoptions": option["preoptions"], + "mask": option["mask"], + "options": option["options"], + "expected": file_string, + } + ) + def test_fslstats(): imgfile = 'test.nii.gz' @@ -121,8 +150,11 @@ def test_fslstats(): try: assert output == test["expected"] except AssertionError: + print('Failed test') print(cmd) + print('Output') print(output) + print('Expected') print(test["expected"]) raise @@ -147,6 +179,37 @@ def test_fslstats(): print(test["expected"]) raise + # run the json tests + for test in json_tests: + cmd = f"fslstats -json {test['preoptions']} {test['mask']} {imgfile} {test['options']}" + # remove any double spaces from empty test options + cmd = " ".join(cmd.split()) + print(cmd, flush=True) + output = run.runfsl(cmd) + + # try to load the expected json file + # if it exists, compare the output to the expected + try: + print(test["expected"]) + with open(test["expected"], "r") as json_file: + # read the file as a string + expected = json_file.read() + try: + assert output == expected + except AssertionError: + print('Failed test') + print(cmd) + print('Output') + print(output) + print('Expected') + print(expected) + raise + except FileNotFoundError: + # if the expected json file doesn't exist, create it from the output + # usefull for creating new tests or the first time a test is run + with open(test["expected"], "w") as json_file: + json_file.write(output) + if __name__ == "__main__": test_fslstats() diff --git a/tests/fslstats/test_cogMM_c.json b/tests/fslstats/test_cogMM_c.json new file mode 100644 index 0000000..758ec3c --- /dev/null +++ b/tests/fslstats/test_cogMM_c.json @@ -0,0 +1,15 @@ +{ + "volumes": [ + { + "indices": [ + { + "-c": [ + 4.505939956803816, + 4.486097425091172, + 4.488288600081377 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_cogVox_C.json b/tests/fslstats/test_cogVox_C.json new file mode 100644 index 0000000..1b6b7b0 --- /dev/null +++ b/tests/fslstats/test_cogVox_C.json @@ -0,0 +1,15 @@ +{ + "volumes": [ + { + "indices": [ + { + "-C": [ + 4.505939956803816, + 4.486097425091172, + 4.488288600081377 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_diff_d_imageForDiff.nii.gz_m.json b/tests/fslstats/test_diff_d_imageForDiff.nii.gz_m.json new file mode 100644 index 0000000..76d250a --- /dev/null +++ b/tests/fslstats/test_diff_d_imageForDiff.nii.gz_m.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 0.0077142266542650755 + } + ] + } + ] +} diff --git a/tests/fslstats/test_entropyNonzero_E.json b/tests/fslstats/test_entropyNonzero_E.json new file mode 100644 index 0000000..9573a6e --- /dev/null +++ b/tests/fslstats/test_entropyNonzero_E.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-E": 0.9468094676591793 + } + ] + } + ] +} diff --git a/tests/fslstats/test_entropy_e.json b/tests/fslstats/test_entropy_e.json new file mode 100644 index 0000000..407d186 --- /dev/null +++ b/tests/fslstats/test_entropy_e.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-e": 0.9468094676591793 + } + ] + } + ] +} diff --git a/tests/fslstats/test_histLimits_H_10_0_1.json b/tests/fslstats/test_histLimits_H_10_0_1.json new file mode 100644 index 0000000..4ce81ec --- /dev/null +++ b/tests/fslstats/test_histLimits_H_10_0_1.json @@ -0,0 +1,22 @@ +{ + "volumes": [ + { + "indices": [ + { + "-H": [ + 193.0, + 220.0, + 194.0, + 201.0, + 190.0, + 184.0, + 191.0, + 199.0, + 200.0, + 228.0 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_hist_h_10.json b/tests/fslstats/test_hist_h_10.json new file mode 100644 index 0000000..2e210d2 --- /dev/null +++ b/tests/fslstats/test_hist_h_10.json @@ -0,0 +1,22 @@ +{ + "volumes": [ + { + "indices": [ + { + "-h": [ + 198.0, + 216.0, + 194.0, + 200.0, + 190.0, + 184.0, + 191.0, + 199.0, + 199.0, + 229.0 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_maxCoord_x.json b/tests/fslstats/test_maxCoord_x.json new file mode 100644 index 0000000..1a20172 --- /dev/null +++ b/tests/fslstats/test_maxCoord_x.json @@ -0,0 +1,15 @@ +{ + "volumes": [ + { + "indices": [ + { + "-x": [ + 4, + 8, + 7 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_meanHistCog_m_h_10_c.json b/tests/fslstats/test_meanHistCog_m_h_10_c.json new file mode 100644 index 0000000..58380fd --- /dev/null +++ b/tests/fslstats/test_meanHistCog_m_h_10_c.json @@ -0,0 +1,28 @@ +{ + "volumes": [ + { + "indices": [ + { + "-c": [ + 4.505939956803816, + 4.486097425091172, + 4.488288600081377 + ], + "-h": [ + 198.0, + 216.0, + 194.0, + 200.0, + 190.0, + 184.0, + 191.0, + 199.0, + 199.0, + 229.0 + ], + "-m": 0.5036357601464115 + } + ] + } + ] +} diff --git a/tests/fslstats/test_meanHist_m_h_10.json b/tests/fslstats/test_meanHist_m_h_10.json new file mode 100644 index 0000000..c8316e5 --- /dev/null +++ b/tests/fslstats/test_meanHist_m_h_10.json @@ -0,0 +1,23 @@ +{ + "volumes": [ + { + "indices": [ + { + "-h": [ + 198.0, + 216.0, + 194.0, + 200.0, + 190.0, + 184.0, + 191.0, + 199.0, + 199.0, + 229.0 + ], + "-m": 0.5036357601464115 + } + ] + } + ] +} diff --git a/tests/fslstats/test_meanNonzero_M.json b/tests/fslstats/test_meanNonzero_M.json new file mode 100644 index 0000000..e7892ba --- /dev/null +++ b/tests/fslstats/test_meanNonzero_M.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-M": 0.5036357601464115 + } + ] + } + ] +} diff --git a/tests/fslstats/test_mean_m.json b/tests/fslstats/test_mean_m.json new file mode 100644 index 0000000..3763ba8 --- /dev/null +++ b/tests/fslstats/test_mean_m.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 0.5036357601464115 + } + ] + } + ] +} diff --git a/tests/fslstats/test_mean_m_a.json b/tests/fslstats/test_mean_m_a.json new file mode 100644 index 0000000..3763ba8 --- /dev/null +++ b/tests/fslstats/test_mean_m_a.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 0.5036357601464115 + } + ] + } + ] +} diff --git a/tests/fslstats/test_mean_m_a_n.json b/tests/fslstats/test_mean_m_a_n.json new file mode 100644 index 0000000..3763ba8 --- /dev/null +++ b/tests/fslstats/test_mean_m_a_n.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 0.5036357601464115 + } + ] + } + ] +} diff --git a/tests/fslstats/test_minCoord_X.json b/tests/fslstats/test_minCoord_X.json new file mode 100644 index 0000000..aa72b2e --- /dev/null +++ b/tests/fslstats/test_minCoord_X.json @@ -0,0 +1,15 @@ +{ + "volumes": [ + { + "indices": [ + { + "-X": [ + 8, + 1, + 6 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_notRobust_R.json b/tests/fslstats/test_notRobust_R.json new file mode 100644 index 0000000..b5b7acd --- /dev/null +++ b/tests/fslstats/test_notRobust_R.json @@ -0,0 +1,14 @@ +{ + "volumes": [ + { + "indices": [ + { + "-R": [ + 0.00036734374589286745, + 0.9998085498809814 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_percentileNonzero_P_50.json b/tests/fslstats/test_percentileNonzero_P_50.json new file mode 100644 index 0000000..47aeceb --- /dev/null +++ b/tests/fslstats/test_percentileNonzero_P_50.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-P": 0.5013243556022644 + } + ] + } + ] +} diff --git a/tests/fslstats/test_percentile_p_50.json b/tests/fslstats/test_percentile_p_50.json new file mode 100644 index 0000000..a105e7c --- /dev/null +++ b/tests/fslstats/test_percentile_p_50.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-p": 0.5013243556022644 + } + ] + } + ] +} diff --git a/tests/fslstats/test_preopIndexMaskMeanTimeSeries_t_K_m.json b/tests/fslstats/test_preopIndexMaskMeanTimeSeries_t_K_m.json new file mode 100644 index 0000000..9f0e754 --- /dev/null +++ b/tests/fslstats/test_preopIndexMaskMeanTimeSeries_t_K_m.json @@ -0,0 +1,36 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 0.5266819608311916 + }, + { + "-m": 0.5156517274568186 + }, + { + "-m": 0.49233699466459285 + }, + { + "-m": 0.5116614613561304 + } + ] + }, + { + "indices": [ + { + "-m": 0.5513619920395724 + }, + { + "-m": 0.5131755568141885 + }, + { + "-m": 0.49409089087108476 + }, + { + "-m": 0.5092079986783641 + } + ] + } + ] +} diff --git a/tests/fslstats/test_preopIndexMaskMean_K_m.json b/tests/fslstats/test_preopIndexMaskMean_K_m.json new file mode 100644 index 0000000..fd62a2f --- /dev/null +++ b/tests/fslstats/test_preopIndexMaskMean_K_m.json @@ -0,0 +1,20 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 1.078043952870764 + }, + { + "-m": 1.0288272842710071 + }, + { + "-m": 0.9864278855356776 + }, + { + "-m": 1.0208694600344945 + } + ] + } + ] +} diff --git a/tests/fslstats/test_preopMeanTimeSeries_t_m.json b/tests/fslstats/test_preopMeanTimeSeries_t_m.json new file mode 100644 index 0000000..b2b481f --- /dev/null +++ b/tests/fslstats/test_preopMeanTimeSeries_t_m.json @@ -0,0 +1,18 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 0.503236498022452 + } + ] + }, + { + "indices": [ + { + "-m": 0.504035022270371 + } + ] + } + ] +} diff --git a/tests/fslstats/test_robust_r.json b/tests/fslstats/test_robust_r.json new file mode 100644 index 0000000..9810ac1 --- /dev/null +++ b/tests/fslstats/test_robust_r.json @@ -0,0 +1,14 @@ +{ + "volumes": [ + { + "indices": [ + { + "-r": [ + 0.018357284367084503, + 0.982818067073822 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_roi_w.json b/tests/fslstats/test_roi_w.json new file mode 100644 index 0000000..d023769 --- /dev/null +++ b/tests/fslstats/test_roi_w.json @@ -0,0 +1,20 @@ +{ + "volumes": [ + { + "indices": [ + { + "-w": [ + 0, + 10, + 0, + 10, + 0, + 10, + 0, + 2 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_stdevNonzero_S.json b/tests/fslstats/test_stdevNonzero_S.json new file mode 100644 index 0000000..cdb0547 --- /dev/null +++ b/tests/fslstats/test_stdevNonzero_S.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-S": 0.2949828225878523 + } + ] + } + ] +} diff --git a/tests/fslstats/test_stdev_s.json b/tests/fslstats/test_stdev_s.json new file mode 100644 index 0000000..b473581 --- /dev/null +++ b/tests/fslstats/test_stdev_s.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-s": 0.29498282199940035 + } + ] + } + ] +} diff --git a/tests/fslstats/test_threshold_l_0.25_u_0.75_m.json b/tests/fslstats/test_threshold_l_0.25_u_0.75_m.json new file mode 100644 index 0000000..6712e13 --- /dev/null +++ b/tests/fslstats/test_threshold_l_0.25_u_0.75_m.json @@ -0,0 +1,11 @@ +{ + "volumes": [ + { + "indices": [ + { + "-m": 0.49646345829352356 + } + ] + } + ] +} diff --git a/tests/fslstats/test_volumeNonzero_V.json b/tests/fslstats/test_volumeNonzero_V.json new file mode 100644 index 0000000..9a1ed33 --- /dev/null +++ b/tests/fslstats/test_volumeNonzero_V.json @@ -0,0 +1,14 @@ +{ + "volumes": [ + { + "indices": [ + { + "-V": [ + 2000.0, + 2000.0 + ] + } + ] + } + ] +} diff --git a/tests/fslstats/test_volume_v.json b/tests/fslstats/test_volume_v.json new file mode 100644 index 0000000..5e4fb29 --- /dev/null +++ b/tests/fslstats/test_volume_v.json @@ -0,0 +1,14 @@ +{ + "volumes": [ + { + "indices": [ + { + "-v": [ + 2000.0, + 2000.0 + ] + } + ] + } + ] +} -- GitLab From 2eb99bb1f2a6cce96a7b038b0053de573f8bfbac Mon Sep 17 00:00:00 2001 From: Taylor Hanayik <hanayik@gmail.com> Date: Mon, 26 Jun 2023 17:19:04 +0100 Subject: [PATCH 6/6] update expected values for FSL 6.0.6.5 --- Makefile | 2 +- tests/fslstats/feedsRun | 2 +- tests/fslstats/test_preopIndexMaskMean_K_m.json | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 29eaa0c..b1692d3 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PROJNAME = avwutils LIBS = -lfsl-newimage -lfsl-miscmaths -lfsl-cprob -lfsl-NewNifti -lfsl-utils -lfsl-znz XFILES = fslorient fslstats fslmerge \ - fslpspec fslroi fslnvols fsl2ascii fslascii2img \ + fslpspec fslroi fslnvols fsl2ascii fslascii2img \ fslsplit fslcc fslinterleave \ fslhd fslcpgeom fslcreatehd fslmaths \ fslcomplex fslfft fslmeants fslslice fslswapdim_exe \ diff --git a/tests/fslstats/feedsRun b/tests/fslstats/feedsRun index 8e722db..6e6c725 100755 --- a/tests/fslstats/feedsRun +++ b/tests/fslstats/feedsRun @@ -91,7 +91,7 @@ tests_with_preoptions = [ "preoptions": "-K", "mask": "mask.nii.gz", "options": "-m", - "expected": "1.078044 \n1.028827 \n0.986428 \n1.020869", + "expected": "0.539022 \n0.514414 \n0.493214 \n0.510435", "name": "preopIndexMaskMean", }, { diff --git a/tests/fslstats/test_preopIndexMaskMean_K_m.json b/tests/fslstats/test_preopIndexMaskMean_K_m.json index fd62a2f..13bd72e 100644 --- a/tests/fslstats/test_preopIndexMaskMean_K_m.json +++ b/tests/fslstats/test_preopIndexMaskMean_K_m.json @@ -3,16 +3,16 @@ { "indices": [ { - "-m": 1.078043952870764 + "-m": 0.539021976435382 }, { - "-m": 1.0288272842710071 + "-m": 0.5144136421355036 }, { - "-m": 0.9864278855356776 + "-m": 0.4932139427678388 }, { - "-m": 1.0208694600344945 + "-m": 0.5104347300172473 } ] } -- GitLab