File: tckdfc.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 (421 lines) | stat: -rw-r--r-- 14,621 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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
/* 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 "exception.h"
#include "header.h"
#include "image.h"
#include "progressbar.h"
#include "thread.h"
#include "thread_queue.h"
#include "transform.h"

#include "dwi/tractography/file.h"
#include "dwi/tractography/properties.h"

#include "dwi/tractography/mapping/loader.h"
#include "dwi/tractography/mapping/mapper.h"
#include "dwi/tractography/mapping/mapping.h"
#include "dwi/tractography/mapping/voxel.h"
#include "dwi/tractography/mapping/writer.h"



#define MAX_VOXEL_STEP_RATIO 0.333




using namespace MR;
using namespace App;

using namespace MR::DWI;
using namespace MR::DWI::Tractography;
using namespace MR::DWI::Tractography::Mapping;



const char* windows[] = { "rectangle", "triangle", "cosine", "hann", "hamming", "lanczos", nullptr };


void usage () {

AUTHOR = "Robert E. Smith (robert.smith@florey.edu.au)";

SYNOPSIS = "Perform the Track-Weighted Dynamic Functional Connectivity (TW-dFC) method";

DESCRIPTION
  + "This command generates a Track-Weighted Image (TWI), where the "
    "contribution from each streamline to the image is the Pearson "
    "correlation between the fMRI time series at the streamline endpoints."

  + "The output image can be generated in one of two ways "
    "(note that one of these two command-line options MUST be provided): "

  + "- \"Static\" functional connectivity (-static option): "
    "Each streamline contributes to a static 3D output image based on the "
    "correlation between the signals at the streamline endpoints using the "
    "entirety of the input time series."

  + "- \"Dynamic\" functional connectivity (-dynamic option): "
    "The output image is a 4D image, with the same number of volumes as "
    "the input fMRI time series. For each volume, the contribution from "
    "each streamline is calculated based on a finite-width sliding time "
    "window, centred at the timepoint corresponding to that volume."

  + "Note that the -backtrack option in this command is similar, but not precisely "
    "equivalent, to back-tracking as can be used with Anatomically-Constrained "
    "Tractography (ACT) in the tckgen command. However, here the feature does not "
    "change the streamlines trajectories in any way; it simply enables detection of "
    "the fact that the input fMRI image may not contain a valid timeseries underneath "
    "the streamline endpoint, and where this occurs, searches from the streamline "
    "endpoint inwards along the streamline trajectory in search of a valid "
    "timeseries to sample from the input image.";

ARGUMENTS
  + Argument ("tracks", "the input track file.").type_file_in()
  + Argument ("fmri",   "the pre-processed fMRI time series").type_image_in()
  + Argument ("output", "the output TW-dFC image").type_image_out();

OPTIONS
  + OptionGroup ("Options for toggling between static and dynamic TW-dFC methods; "
                 "note that one of these options MUST be provided")

  + Option ("static", "generate a \"static\" (3D) output image.")

  + Option ("dynamic", "generate a \"dynamic\" (4D) output image; "
                       "must additionally provide the shape and width (in volumes) of the sliding window.")
    + Argument ("shape").type_choice (windows)
    + Argument ("width").type_integer (3)


  + OptionGroup ("Options for setting the properties of the output image")

  + Option ("template",
      "an image file to be used as a template for the output (the output image "
      "will have the same transform and field of view).")
    + Argument ("image").type_image_in()

  + Option ("vox",
      "provide either an isotropic voxel size (in mm), or comma-separated list "
      "of 3 voxel dimensions.")
    + Argument ("size").type_sequence_float()

  + Option ("stat_vox",
      "define the statistic for choosing the final voxel intensities for a given contrast "
      "type given the individual values from the tracks passing through each voxel\n"
      "Options are: " + join(voxel_statistics, ", ") + " (default: mean)")
    + Argument ("type").type_choice (voxel_statistics)


  + OptionGroup ("Other options for affecting the streamline sampling & mapping behaviour")

  + Option ("backtrack",
            "if no valid timeseries is found at the streamline endpoint, back-track along "
            "the streamline trajectory until a valid timeseries is found")

  + Option ("upsample",
      "upsample the tracks by some ratio using Hermite interpolation before mapping "
      "(if omitted, an appropriate ratio will be determined automatically)")
    + Argument ("factor").type_integer (1);


  REFERENCES
  + "Calamante, F.; Smith, R.E.; Liang, X.; Zalesky, A.; Connelly, A " // Internal
    "Track-weighted dynamic functional connectivity (TW-dFC): a new method to study time-resolved functional connectivity. "
    "Brain Struct Funct, 2017, doi: 10.1007/s00429-017-1431-1";

}





// This class is similar to Mapping::MapWriter, but doesn't write to a HDD file on close
// Instead, the one timepoint volume generated during this iteration is written
//   into the one large buffer that contains the entire TW-dFC time series
class Receiver
{ MEMALIGN(Receiver)

  public:
    Receiver (const Header& header, const vox_stat_t stat_vox) :
        buffer (Image<float>::scratch (header, "TW-dFC scratch buffer")),
        vox_stat (stat_vox)
    {
      if (vox_stat == V_MIN) {
        for (auto l = Loop(buffer) (buffer); l; ++l)
          buffer.value() = std::numeric_limits<float>::infinity();
      } else if (vox_stat == V_MAX) {
        for (auto l = Loop(buffer) (buffer); l; ++l)
          buffer.value() = -std::numeric_limits<float>::infinity();
      }
    }


    bool operator() (const Mapping::SetVoxel&);
    void scale_by_count (Image<uint32_t>&);
    void write (Image<float>&);


  private:
    Image<float> buffer;
    const vox_stat_t vox_stat;

};



bool Receiver::operator() (const Mapping::SetVoxel& in)
{
  const float factor = in.factor;
  for (const auto& i : in) {
    assign_pos_of (i, 0, 3).to (buffer);
    switch (vox_stat) {
      case V_SUM:  buffer.value() += factor; break;
      case V_MIN:  buffer.value() = std::min (float(buffer.value()), factor); break;
      case V_MAX:  buffer.value() = std::max (float(buffer.value()), factor); break;
      case V_MEAN: buffer.value() += factor; break;
      // Unlike Mapping::MapWriter, don't need to deal with counts here
    }
  }
  return true;
}

void Receiver::scale_by_count (Image<uint32_t>& counts)
{
  assert (dimensions_match (buffer, counts, 0, 3));
  for (auto l = Loop(buffer) (buffer, counts); l; ++l) {
    if (counts.value())
      buffer.value() /= float(counts.value());
    else
      buffer.value() = 0.0f;
  }
}

void Receiver::write (Image<float>& out)
{
  for (auto l = Loop(buffer) (buffer, out); l; ++l)
    out.value() = buffer.value();
}







// Separate class for generating TDI i.e. receive SetVoxel & write directly to counts
class Count_receiver
{ MEMALIGN(Count_receiver)
  public:
    Count_receiver (Image<uint32_t>& out) :
        v (out) { }
    bool operator() (const Mapping::SetVoxel& in) {
      for (const auto& i : in) {
        assign_pos_of (i, 0, 3).to (v);
        v.value() = v.value() + 1;
      }
      return true;
    }
  private:
    Image<uint32_t> v;
};





void run ()
{
  bool is_static = get_options ("static").size();
  vector<float> window;

  auto opt = get_options ("dynamic");
  if (opt.size()) {
    if (is_static)
      throw Exception ("Do not specify both -static and -dynamic options");

    // Generate the window filter
    const int window_shape = opt[0][0];
    const ssize_t window_width = opt[0][1];
    if (!(window_width % 2))
      throw Exception ("Width of sliding time window must be an odd integer");

    window.resize (window_width);
    const ssize_t halfwidth = (window_width+1) / 2;
    const ssize_t centre = (window_width-1) / 2; // Element at centre of the window

    switch (window_shape) {

      case 0: // rectangular
        window.assign (window_width, 1.0);
        break;

      case 1: // triangle
        for (ssize_t i = 0; i != window_width; ++i)
          window[i] = 1.0 - (abs (i - centre) / default_type(halfwidth));
        break;

      case 2: // cosine
        for (ssize_t i = 0; i != window_width; ++i)
          window[i] = std::sin (i * Math::pi / default_type(window_width - 1));
        break;

      case 3: // hann
        for (ssize_t i = 0; i != window_width; ++i)
          window[i] = 0.5 * (1.0 - std::cos (2.0 * Math::pi * i / default_type(window_width - 1)));
        break;

      case 4: // hamming
        for (ssize_t i = 0; i != window_width; ++i)
          window[i] = 0.53836 - (0.46164 * std::cos (2.0 * Math::pi * i / default_type(window_width - 1)));
        break;

      case 5: // lanczos
        for (ssize_t i = 0; i != window_width; ++i) {
          const default_type v = 2.0 * Math::pi * abs (i - centre) / default_type(window_width - 1);
          window[i] = v ? std::max (0.0, (std::sin (v) / v)) : 1.0;
        }
        break;

      default:
        throw Exception ("Unsupported sliding window shape");
    }

  } else if (!is_static) {
    throw Exception ("Either the -static or -dynamic option must be provided");
  }

  const std::string tck_path = argument[0];
  Tractography::Properties properties;
  {
    // Just get the properties for now; will re-instantiate the reader multiple times later
    // TODO Constructor for properties using the file path?
    Tractography::Reader<float> tck_file (tck_path, properties);
  }
  const size_t num_tracks = properties["count"].empty() ? 0 : to<size_t> (properties["count"]);

  Image<float> fmri_image (Image<float>::open (argument[1]).with_direct_io(3));

  vector<default_type> voxel_size;
  opt = get_options("vox");
  if (opt.size())
    voxel_size = parse_floats (opt[0][0]);

  if (voxel_size.size() == 1)
    voxel_size.assign (3, voxel_size.front());
  else if (!voxel_size.empty() && voxel_size.size() != 3)
    throw Exception ("voxel size must either be a single isotropic value, or a list of 3 comma-separated voxel dimensions");

  if (!voxel_size.empty())
    INFO ("creating image with voxel dimensions [ " + str(voxel_size[0]) + " " + str(voxel_size[1]) + " " + str(voxel_size[2]) + " ]");

  Header header;
  opt = get_options ("template");
  if (opt.size()) {
    header = Header::open (opt[0][0]);
    if (!voxel_size.empty())
      Mapping::oversample_header (header, voxel_size);
  } else {
    if (voxel_size.empty())
      throw Exception ("please specify either a template image using the -template option, or the desired voxel size using the -vox option");
    Mapping::generate_header (header, argument[0], voxel_size);
  }

  header.datatype() = DataType::Float32;
  header.datatype().set_byte_order_native();
  if (is_static) {
    header.ndim() = 3;
  } else {
    header.ndim() = 4;
    header.size(3) = fmri_image.size(3);
  }
  add_line (header.keyval()["comments"], "TW-dFC image");

  size_t upsample_ratio;
  opt = get_options ("upsample");
  if (opt.size()) {
    upsample_ratio = opt[0][0];
    INFO ("track interpolation factor manually set to " + str(upsample_ratio));
  } else  {
    try {
      upsample_ratio = determine_upsample_ratio (header, properties, MAX_VOXEL_STEP_RATIO);
      INFO ("track interpolation factor automatically set to " + str(upsample_ratio));
    } catch (Exception& e) {
      e.push_back ("Try using -upsample option to explicitly set the streamline upsampling ratio;");
      e.push_back ("generally recommend a value of around (3 x step_size / voxel_size)");
      throw e;
    }
  }

  opt = get_options ("stat_vox");
  const vox_stat_t stat_vox = opt.size() ? vox_stat_t(int(opt[0][0])) : V_MEAN;

  Header H_3D (header);
  H_3D.ndim() = 3;

  if (is_static) {

    Tractography::Reader<float> tck_file (tck_path, properties);
    Mapping::TrackLoader loader (tck_file, num_tracks, "Generating (static) TW-dFC image");
    Mapping::TrackMapperTWI mapper (H_3D, SCALAR_MAP, ENDS_CORR);
    mapper.set_upsample_ratio (upsample_ratio);
    mapper.add_twdfc_static_image (fmri_image);
    Mapping::MapWriter<float> writer (header, argument[2], stat_vox);
    Thread::run_queue (loader, Thread::batch (Tractography::Streamline<>()), Thread::multi (mapper), Thread::batch (Mapping::SetVoxel()), writer);
    writer.finalise();

  } else {

    Image<uint32_t> counts;
    if (stat_vox == V_MEAN) {
      counts = Image<uint32_t>::scratch (H_3D, "Track count scratch buffer");
      Tractography::Reader<float> tck_file (tck_path, properties);
      Mapping::TrackLoader loader (tck_file, num_tracks, "Calculating initial TDI");
      Mapping::TrackMapperBase mapper (H_3D);
      mapper.set_upsample_ratio (upsample_ratio);
      Count_receiver receiver (counts);
      Thread::run_queue (loader, Thread::batch (Tractography::Streamline<>()), Thread::multi (mapper), Thread::batch (Mapping::SetVoxel()), receiver);
    }

    Image<float> out_image (Image<float>::create (argument[2], header));
    ProgressBar progress ("Generating TW-dFC image", header.size(3));
    for (ssize_t timepoint = 0; timepoint != header.size(3); ++timepoint) {

      {
        LogLevelLatch latch (0);
        Tractography::Reader<float> tck_file (tck_path, properties);
        Mapping::TrackLoader loader (tck_file);
        Mapping::TrackMapperTWI mapper (H_3D, SCALAR_MAP, ENDS_CORR);
        mapper.set_upsample_ratio (upsample_ratio);
        mapper.add_twdfc_dynamic_image (fmri_image, window, timepoint);
        Receiver receiver (H_3D, stat_vox);
        Thread::run_queue (loader, Thread::batch (Tractography::Streamline<>()), Thread::multi (mapper), Thread::batch (Mapping::SetVoxel()), receiver);

        if (stat_vox == V_MEAN)
          receiver.scale_by_count (counts);

        out_image.index(3) = timepoint;
        receiver.write (out_image);
      }

      ++progress;
    }

  }
}