File: dirstat.cpp

package info (click to toggle)
mrtrix3 3.0.8-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 15,300 kB
  • sloc: cpp: 130,470; python: 9,603; sh: 597; makefile: 62; xml: 47
file content (321 lines) | stat: -rw-r--r-- 11,202 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/* Copyright (c) 2008-2025 the MRtrix3 contributors.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Covered Software is provided under this License on an "as is"
 * basis, without warranty of any kind, either expressed, implied, or
 * statutory, including, without limitation, warranties that the
 * Covered Software is free of defects, merchantable, fit for a
 * particular purpose or non-infringing.
 * See the Mozilla Public License v. 2.0 for more details.
 *
 * For more details, see http://www.mrtrix.org/.
 */

#include "command.h"
#include "progressbar.h"
#include "header.h"
#include "dwi/gradient.h"
#include "dwi/shells.h"
#include "math/sphere.h"

#include "dwi/directions/file.h"



using namespace MR;
using namespace App;

void usage ()
{
  AUTHOR = "J-Donald Tournier (jdtournier@gmail.com)";

  SYNOPSIS = "Report statistics on a direction set";

  DESCRIPTION
    + "This command will accept as inputs:"
    + "- directions file in spherical coordinates (ASCII text, [ az el ] space-separated values, one per line);"
    + "- directions file in Cartesian coordinates (ASCII text, [ x y z ] space-separated values, one per line);"
    + "- DW gradient files (MRtrix format: ASCII text, [ x y z b ] space-separated values, one per line);"
    + "- image files, using the DW gradient scheme found in the header (or provided using the appropriate command line options below)."

    + "By default, this produces all relevant metrics for the direction set "
    "provided. If the direction set contains multiple shells, metrics are "
    "provided for each shell separately."

    + "Metrics are produced assuming a unipolar or bipolar electrostatic "
    "repulsion model, producing the potential energy (total, mean, min & max), "
    "and the nearest-neighbour angles (mean, min & max). The condition "
    "number is also produced for the spherical harmonic fits up to the highest "
    "harmonic order supported by the number of volumes. Finally, the norm of the "
    "mean direction vector is provided as a measure of the overall symmetry of "
    "the direction set (important with respect to eddy-current resilience)."

    + "Specific metrics can also be queried independently via the \"-output\" "
    "option, using these shorthands: \n"
    "U/B for unipolar/bipolar model, \n"
    "E/N for energy and nearest-neighbour respectively, \n"
    "t/-/+ for total/min/max respectively (mean implied otherwise); \n"
    "SHn for condition number of SH fit at order n (with n an even integer); \n"
    "ASYM for asymmetry index (norm of mean direction vector); \n"
    "N for the number of directions.";

  EXAMPLES
    + Example ("Default usage",
               "dirstat directions.txt",
               "This provides a pretty-printed list of all metrics available.")

    + Example ("Write a single metric of interest to standard output",
               "dirstat grad.b -shell 3000 -output SH8",
               "requests the condition number of SH fit of b=3000 shell "
               "directions at SH order 8")

    + Example ("Write multiple metrics of interest to standard output",
               "dirstat dwi.mif -output BN,BN-,BN+",
               "requests the mean, min and max nearest-neighour "
               "angles assuming a bipolar model.");

  ARGUMENTS
    + Argument ("dirs", "the text file or image containing the directions.").type_file_in();

  OPTIONS
    + Option ("output", "output selected metrics as a space-delimited list, "
        "suitable for use in scripts. This will produce one line of values per "
        "selected shell. Valid metrics are as specified in the description "
        "above.")
    +   Argument ("list")
    + DWI::ShellsOption
    + DWI::GradImportOptions();
}



int precision = 6;

void report (const std::string& title, Eigen::MatrixXd& directions);



void run ()
{
  Eigen::MatrixXd directions;

  try {
    directions = DWI::Directions::load_cartesian (argument[0]);
  }
  catch (Exception& E) {
    try {
      directions = load_matrix<double> (argument[0]);
    }
    catch (Exception& E) {
      auto header = Header::open (argument[0]);
      directions = DWI::get_DW_scheme (header);
    }
  }

  if (directions.cols() >= 4) {
    int n_start = 0;
    auto shells = DWI::Shells (directions).select_shells (false, false, false);
    if (get_options ("shells").empty() && shells.has_bzero() && shells.count() > 1) {
      n_start = 1;
      if (get_options("output").empty())
        print (std::string (argument[0]) + " (b=0) [ " + str(shells.smallest().count(), precision) + " volumes ]\n\n");
    }


    Eigen::MatrixXd dirs;

    for (size_t n = n_start; n < shells.count(); ++n) {
      dirs.resize (shells[n].count(), 3);
      for (size_t idx = 0; idx < shells[n].count(); ++idx)
        dirs.row (idx) = directions.row (shells[n].get_volumes()[idx]).head (3);

      report (std::string (argument[0]) + " (b=" + str(shells[n].get_mean()) + ")", dirs);
    }

  }
  else
    report (argument[0], directions);
}





vector<default_type> summarise_NN (const vector<double>& NN)
{
  double NN_min = std::numeric_limits<double>::max();
  double NN_mean = 0.0;
  double NN_max = 0.0;
  for (auto a : NN) {
    a = (180.0/Math::pi) * std::acos (a);
    NN_mean += a;
    NN_min = std::min (NN_min, a);
    NN_max = std::max (NN_max, a);
  }
  NN_mean /= NN.size();

  return { NN_mean, NN_min, NN_max };
}







vector<default_type> summarise_E (const vector<double>& E)
{
  double E_min = std::numeric_limits<double>::max();
  double E_total = 0.0;
  double E_max = 0.0;
  for (auto e : E) {
    E_total += e;
    E_min = std::min (E_min, e);
    E_max = std::max (E_max, e);
  }

  return { 0.5*E_total, E_total/E.size(), E_min, E_max };
}



class Metrics
{ MEMALIGN (Metrics)
  public:
    vector<default_type> BN, UN, BE, UE, SH;
    default_type ASYM;
    size_t ndirs;
};






Metrics compute (Eigen::MatrixXd& directions)
{
  if (directions.cols() < 3)
    throw Exception ("unexpected matrix size for scheme \"" + str(argument[0]) + "\"");
  Math::Sphere::normalise_cartesian (directions);

  vector<double> NN_bipolar (directions.rows(), -1.0);
  vector<double> NN_unipolar (directions.rows(), -1.0);

  vector<double> E_bipolar (directions.rows(), 0.0);
  vector<double> E_unipolar (directions.rows(), 0.0);

  for (ssize_t i = 0; i < directions.rows()-1; ++i) {
    for (ssize_t j = i+1; j < directions.rows(); ++j) {
      double cos_angle = directions.row(i).head(3).normalized().dot (directions.row(j).head(3).normalized());
      NN_unipolar[i] = std::max (NN_unipolar[i], cos_angle);
      NN_unipolar[j] = std::max (NN_unipolar[j], cos_angle);
      cos_angle = abs (cos_angle);
      NN_bipolar[i] = std::max (NN_bipolar[i], cos_angle);
      NN_bipolar[j] = std::max (NN_bipolar[j], cos_angle);

      double E = 1.0 / (directions.row(i).head(3) - directions.row(j).head(3)).norm();

      E_unipolar[i] += E;
      E_unipolar[j] += E;

      E += 1.0 / (directions.row(i).head(3) + directions.row(j).head(3)).norm();

      E_bipolar[i] += E;
      E_bipolar[j] += E;

    }
  }

  Metrics metrics;
  metrics.ndirs = directions.rows();
  metrics.UN = summarise_NN (NN_unipolar);
  metrics.BN = summarise_NN (NN_bipolar);
  metrics.UE = summarise_E (E_unipolar);
  metrics.BE = summarise_E (E_bipolar);

  for (size_t lmax = 2; lmax <= Math::SH::LforN (directions.rows()); lmax += 2)
    metrics.SH.push_back (DWI::condition_number_for_lmax (directions, lmax));

  metrics.ASYM = directions.leftCols(3).colwise().mean().norm();

  return metrics;
}




void output_selected (const Metrics& metrics, const std::string& selection)
{
  auto select = split (selection, ", \t\n", true);

  for (const auto& x : select) {
    const auto xl = lowercase(x);
    if (xl == "uet")       std::cout << metrics.UE[0] << " ";
    else if (xl == "ue")   std::cout << metrics.UE[1] << " ";
    else if (xl == "ue-")  std::cout << metrics.UE[2] << " ";
    else if (xl == "ue+")  std::cout << metrics.UE[3] << " ";
    else if (xl == "bet")  std::cout << metrics.BE[0] << " ";
    else if (xl == "be")   std::cout << metrics.BE[1] << " ";
    else if (xl == "be-")  std::cout << metrics.BE[2] << " ";
    else if (xl == "be+")  std::cout << metrics.BE[3] << " ";
    else if (xl == "un")   std::cout << metrics.UN[0] << " ";
    else if (xl == "un-")  std::cout << metrics.UN[1] << " ";
    else if (xl == "un+")  std::cout << metrics.UN[2] << " ";
    else if (xl == "bn")   std::cout << metrics.BN[0] << " ";
    else if (xl == "bn-")  std::cout << metrics.BN[1] << " ";
    else if (xl == "bn+")  std::cout << metrics.BN[2] << " ";
    else if (xl == "asym") std::cout << metrics.ASYM << " ";
    else if (xl == "n")    std::cout << metrics.ndirs << " ";
    else if (xl.substr(0,2) == "sh") {
      size_t order = to<size_t>(x.substr(2));
      if (order & 1U || order < 2)
        throw Exception ("spherical harmonic order must be an even positive integer");
      order = (order/2)-1;
      if (order >= metrics.SH.size())
        throw Exception ("spherical harmonic order requested is too large given number of directions");
      std::cout << metrics.SH[order] << " ";
    }
    else
      throw Exception ("unknown output specifier \"" + x + "\"");
  }

  std::cout << "\n";
}



void report (const std::string& title, Eigen::MatrixXd& directions)
{
  auto metrics = compute (directions);

  auto opt = get_options ("output");
  if (opt.size()) {
    output_selected (metrics, opt[0][0]);
    return;
  }

  std::string output = title + " [ " + str(metrics.ndirs, precision) + " directions ]\n";

  output += "\n  Bipolar electrostatic repulsion model:\n";
  output += "    nearest-neighbour angles: mean = " + str(metrics.BN[0], precision) + ", range [ " + str(metrics.BN[1], precision) + " - " + str(metrics.BN[2], precision) + " ]\n";
  output += "    energy: total = " + str(metrics.BE[0], precision) + ", mean = " + str(metrics.BE[1], precision) + ", range [ " + str(metrics.BE[2], precision) + " - " + str(metrics.BE[3], precision) + " ]\n";

  output += "\n  Unipolar electrostatic repulsion model:\n";
  output += "    nearest-neighbour angles: mean = " + str(metrics.UN[0], precision) + ", range [ " + str(metrics.UN[1], precision) + " - " + str(metrics.UN[2], precision) + " ]\n";
  output += "    energy: total = " + str(metrics.UE[0], precision) + ", mean = " + str(metrics.UE[1], precision) + ", range [ " + str(metrics.UE[2], precision) + " - " + str(metrics.UE[3], precision) + " ]\n";


  output += "\n  Spherical Harmonic fit:\n    condition numbers for lmax = 2 -> " + str(metrics.SH.size()*2) + ": " + str(metrics.SH, precision) + "\n";

  output += "\n  Asymmetry of sampling:\n    norm of mean direction vector = " + str(metrics.ASYM, precision) + "\n";
  if (metrics.ASYM >= 0.1)
    output += std::string("    WARNING: sampling is ") + ( metrics.ASYM >= 0.4 ? "strongly" : "moderately" )
            + " asymmetric - this may affect resiliance to eddy-current distortions\n";

  output += "\n";
  print (output);
}