File: mrconvert.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 (576 lines) | stat: -rw-r--r-- 22,741 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
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
/* 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 "axes.h"
#include "command.h"
#include "header.h"
#include "image.h"
#include "transform.h"
#include "types.h"
#include "algo/threaded_copy.h"
#include "adapter/extract.h"
#include "adapter/permute_axes.h"
#include "file/json_utils.h"
#include "file/ofstream.h"
#include "dwi/gradient.h"
#include "metadata/phase_encoding.h"


using namespace MR;
using namespace App;



bool add_to_command_history = true;

void usage ()
{
  AUTHOR = "J-Donald Tournier (jdtournier@gmail.com) and Robert E. Smith (robert.smith@florey.edu.au)";

  SYNOPSIS = "Perform conversion between different file types and optionally "
             "extract a subset of the input image";

  DESCRIPTION
  + "If used correctly, this program can be a very useful workhorse. "
    "In addition to converting images between different formats, it can "
    "be used to extract specific studies from a data set, extract a "
    "specific region of interest, or flip the images. Some of the possible "
    "operations are described in more detail below."

  + "Note that for both the -coord and -axes options, indexing starts from 0 "
    "rather than 1. E.g. "
    "-coord 3 <#> selects volumes (the fourth dimension) from the series; "
    "-axes 0,1,2 includes only the three spatial axes in the output image."

  + "Additionally, for the second input to the -coord option and the -axes "
    "option, you can use any valid number sequence in the selection, as well "
    "as the 'end' keyword (see the main documentation for details); this can be "
    "particularly useful to select multiple coordinates."

  + "The -vox option is used to change the size of the voxels in the output "
    "image as reported in the image header; note however that this does not "
    "re-sample the image based on a new voxel size (that is done using the "
    "mrgrid command)."

  + "By default, the intensity scaling parameters in the input image header "
    "are passed through to the output image header when writing to an integer "
    "image, and reset to 0,1 (i.e. no scaling) for floating-point and binary "
    "images. Note that the -scaling option will therefore have no effect for "
    "floating-point or binary output images."

  + "The -axes option specifies which axes from the input image will be used "
    "to form the output image. This allows the permutation, omission, or "
    "addition of axes into the output image. The axes should be supplied as a "
    "comma-separated list of axis indices. If an axis from the input image is "
    "to be omitted from the output image, it must either already have a size of "
    "1, or a single coordinate along that axis must be selected by the user by "
    "using the -coord option. Examples are provided further below."

  + DWI::bvalue_scaling_description;


  EXAMPLES
  + Example ("Extract the first volume from a 4D image, and make the output a 3D image",
             "mrconvert in.mif -coord 3 0 -axes 0,1,2 out.mif",
             "The -coord 3 0 option extracts, from axis number 3 (which is the "
             "fourth axis since counting begins from 0; this is the axis that "
             "steps across image volumes), only coordinate number 0 (i.e. the "
             "first volume). The -axes 0,1,2 ensures that only the first three "
             "axes (i.e. the spatial axes) are retained; if this option were not "
             "used in this example, then image out.mif would be a 4D image, "
             "but it would only consist of a single volume, and mrinfo would "
             "report its size along the fourth axis as 1.")

  + Example ("Extract slice number 24 along the AP direction",
             "mrconvert volume.mif slice.mif -coord 1 24",
             "MRtrix3 uses a RAS (Right-Anterior-Superior) axis "
             "convention, and internally reorients images upon loading "
             "in order to conform to this as far as possible. So for "
             "non-exotic data, axis 1 should correspond (approximately) to the "
             "anterior-posterior direction.")

  + Example ("Extract only every other volume from a 4D image",
             "mrconvert all.mif every_other.mif -coord 3 1:2:end",
             "This example demonstrates two features: Use of the "
             "colon syntax to conveniently specify a number sequence "
             "(in the format \'start:step:stop\'); and use of the \'end\' "
             "keyword to generate this sequence up to the size of the "
             "input image along that axis (i.e. the number of volumes).")

  + Example ("Alter the image header to report a new isotropic voxel size",
             "mrconvert in.mif isotropic.mif -vox 1.25",
             "By providing a single value to the -vox option only, the "
             "specified value is used to set the voxel size in mm for all "
             "three spatial axes in the output image.")

  + Example ("Alter the image header to report a new anisotropic voxel size",
             "mrconvert in.mif anisotropic.mif -vox 1,,3.5",
             "This example will change the reported voxel size along the first "
             "and third axes (ideally left-right and inferior-superior) to "
             "1.0mm and 3.5mm respectively, and leave the voxel size along the "
             "second axis (ideally anterior-posterior) unchanged.")

  + Example ("Turn a single-volume 4D image into a 3D image",
             "mrconvert 4D.mif 3D.mif -axes 0,1,2",
             "Sometimes in the process of extracting or calculating a single "
             "3D volume from a 4D image series, the size of the image reported "
             "by mrinfo will be \"X x Y x Z x 1\", indicating that the resulting "
             "image is in fact also 4D, it just happens to contain only one "
             "volume. This example demonstrates how to convert this into a "
             "genuine 3D image (i.e. mrinfo will report the size as \"X x Y x Z\".")

  + Example ("Insert an axis of size 1 into the image",
             "mrconvert XYZD.mif XYZ1D.mif -axes 0,1,2,-1,3",
             "This example uses the value -1 as a flag to indicate to mrconvert "
             "where a new axis of unity size is to be inserted. In this particular "
             "example, the input image has four axes: the spatial axes X, Y and Z, "
             "and some form of data D is stored across the fourth axis (i.e. "
             "volumes). Due to insertion of a new axis, the output image is 5D: "
             "the three spatial axes (XYZ), a single volume (the size of the "
             "output image along the fourth axis will be 1), and data D "
             "will be stored as volume groups along the fifth axis of the image.")

  + Example ("Manually reset the data scaling parameters stored within the image header to defaults",
             "mrconvert with_scaling.mif without_scaling.mif -scaling 0.0,1.0",
             "This command-line option alters the parameters stored within the image "
             "header that provide a linear mapping from raw intensity values stored "
             "in the image data to some other scale. Where the raw data stored in a "
             "particular voxel is I, the value within that voxel is interpreted as: "
             "value = offset + (scale x I).  To adjust this scaling, the relevant "
             "parameters must be provided as a comma-separated 2-vector of "
             "floating-point values, in the format \"offset,scale\" (no quotation "
             "marks). This particular example sets the offset to zero and the scale "
             "to one, which equates to no rescaling of the raw intensity data.");


  ARGUMENTS
  + Argument ("input", "the input image.").type_image_in ()
  + Argument ("output", "the output image.").type_image_out ();

  OPTIONS

  + OptionGroup ("Options for manipulating fundamental image properties")

  + Option ("coord",
            "retain data from the input image only at the coordinates "
            "specified in the selection along the specified axis. The selection "
            "argument expects a number sequence, which can also include the "
            "'end' keyword.").allow_multiple()
    + Argument ("axis").type_integer (0)
    + Argument ("selection").type_sequence_int()

  + Option ("vox",
            "change the voxel dimensions reported in the output image header")
    + Argument ("sizes").type_sequence_float()

  + Option ("axes",
            "specify the axes from the input image that will be used to form the output image")
    + Argument ("axes").type_sequence_int()

  + Option ("scaling",
            "specify the data scaling parameters used to rescale the intensity values")
    + Argument ("values").type_sequence_float()


  + OptionGroup ("Options for handling JSON (JavaScript Object Notation) files")

  + Option ("json_import", "import data from a JSON file into header key-value pairs")
    + Argument ("file").type_file_in()

  + Option ("json_export", "export data from an image header key-value pairs into a JSON file")
    + Argument ("file").type_file_out()


  + OptionGroup ("Options to modify generic header entries")

  + Option ("clear_property",
            "remove the specified key from the image header altogether.").allow_multiple()
  + Argument ("key").type_text()

  + Option ("set_property",
            "set the value of the specified key in the image header.").allow_multiple()
  + Argument ("key").type_text()
  + Argument ("value").type_text()

  + Option ("append_property",
            "append the given value to the specified key in the image header (this adds the value specified as a new line in the header value).").allow_multiple()
  + Argument ("key").type_text()
  + Argument ("value").type_text()

  + Option ("copy_properties",
            "clear all generic properties and replace with the properties from the image / file specified.")
  + Argument ("source").type_various()


  + Stride::Options

  + DataType::options()

  + DWI::GradImportOptions ()
  + DWI::bvalue_scaling_option

  + DWI::GradExportOptions()

  + Metadata::PhaseEncoding::ImportOptions
  + Metadata::PhaseEncoding::ExportOptions;
}







void permute_DW_scheme (Header& H, const vector<int>& axes)
{
  auto in = DWI::parse_DW_scheme (H);
  if (!in.rows())
    return;

  Transform T (H);
  Eigen::Matrix3d permute = Eigen::Matrix3d::Zero();
  for (size_t axis = 0; axis != 3; ++axis)
    permute(axes[axis], axis) = 1.0;
  const Eigen::Matrix3d R = T.scanner2voxel.rotation() * permute * T.voxel2scanner.rotation();

  Eigen::MatrixXd out (in.rows(), in.cols());
  out.block(0, 3, out.rows(), out.cols()-3) = in.block(0, 3, in.rows(), in.cols()-3); // Copy b-values (and anything else stored in dw_scheme)
  for (int row = 0; row != in.rows(); ++row)
    out.block<1,3>(row, 0) = in.block<1,3>(row, 0) * R;

  DWI::set_DW_scheme (H, out);
}



void permute_PE_scheme (Header& H, const vector<int>& axes)
{
  auto in = Metadata::PhaseEncoding::parse_scheme (H.keyval(), H);
  if (!in.rows())
    return;

  Eigen::Matrix3d permute = Eigen::Matrix3d::Zero();
  for (size_t axis = 0; axis != 3; ++axis)
    permute(axes[axis], axis) = 1.0;

  Eigen::MatrixXd out (in.rows(), in.cols());
  out.block(0, 3, out.rows(), out.cols()-3) = in.block(0, 3, in.rows(), in.cols()-3); // Copy total readout times (and anything else stored in pe_scheme)
  for (int row = 0; row != in.rows(); ++row)
    out.block<1,3>(row, 0) = in.block<1,3>(row, 0) * permute;

  Metadata::PhaseEncoding::set_scheme (H.keyval(), out);
}



void permute_slice_direction (Header& H, const vector<int>& axes)
{
  using Metadata::BIDS::axis_vector_type;
  auto slice_encoding_it = H.keyval().find("SliceEncodingDirection");
  auto slice_timing_it = H.keyval().find("SliceTiming");
  if (slice_encoding_it == H.keyval().end() && slice_timing_it == H.keyval().end())
    return;
  if (slice_encoding_it == H.keyval().end()) {
    const axis_vector_type orig_dir({0, 0, 1});
    const axis_vector_type new_dir(orig_dir[axes[0]], orig_dir[axes[1]], orig_dir[axes[2]]);
    slice_encoding_it->second = Metadata::BIDS::vector2axisid(new_dir);
    WARN("Header field \"SliceEncodingDirection\" inferred to be \"k\" in input image"
         " and then transformed according to axis permutation"
         " in order to preserve validity of existing header field \"SliceTiming\"");
    return;
  }
  const axis_vector_type orig_dir = Metadata::BIDS::axisid2vector(slice_encoding_it->second);
  const axis_vector_type new_dir(orig_dir[axes[0]], orig_dir[axes[1]], orig_dir[axes[2]]);
  slice_encoding_it->second = Metadata::BIDS::vector2axisid(new_dir);
}




template <class ImageType>
inline vector<int> set_header (Header& header, const ImageType& input)
{
  header.ndim() = input.ndim();
  for (size_t n = 0; n < header.ndim(); ++n) {
    header.size(n) = input.size(n);
    header.spacing(n) = input.spacing(n);
    header.stride(n) = input.stride(n);
  }
  header.transform() = input.transform();

  auto opt = get_options ("axes");
  vector<int32_t> axes;
  if (opt.size()) {
    axes = parse_ints<int32_t> (opt[0][0]);
    header.ndim() = axes.size();
    for (size_t i = 0; i < axes.size(); ++i) {
      if (axes[i] >= static_cast<int> (input.ndim()))
        throw Exception ("axis supplied to option -axes is out of bounds");
      header.size(i) = axes[i] < 0 ? 1 : input.size (axes[i]);
      header.spacing(i) = axes[i] < 0 ? NaN : input.spacing (axes[i]);
    }
    permute_DW_scheme (header, axes);
    permute_PE_scheme (header, axes);
    permute_slice_direction (header, axes);
  } else {
    header.ndim() = input.ndim();
    axes.assign (input.ndim(), 0);
    for (size_t i = 0; i < axes.size(); ++i) {
      axes[i] = i;
      header.size (i) = input.size (i);
    }
  }

  opt = get_options ("vox");
  if (opt.size()) {
    vector<default_type> vox = parse_floats (opt[0][0]);
    if (vox.size() > header.ndim())
      throw Exception ("too many axes supplied to -vox option");
    if (vox.size() == 1)
      vox.resize (3, vox[0]);
    for (size_t n = 0; n < vox.size(); ++n) {
      if (std::isfinite (vox[n]))
        header.spacing(n) = vox[n];
    }
  }

  Stride::set_from_command_line (header);

  return axes;
}






template <typename T, class InputType>
void copy_permute (const InputType& in, Header& header_out, const std::string& output_filename)
{
  const auto axes = set_header (header_out, in);
  auto out = Image<T>::create (output_filename, header_out, add_to_command_history);
  DWI::export_grad_commandline (out);
  Metadata::PhaseEncoding::export_commandline (out);
  auto perm = Adapter::make <Adapter::PermuteAxes> (in, axes);
  threaded_copy_with_progress (perm, out, 0, std::numeric_limits<size_t>::max(), 2);
}






template <typename T>
void extract (Header& header_in, Header& header_out, const vector<vector<uint32_t>>& pos, const std::string& output_filename)
{
  auto in = header_in.get_image<T>();
  if (pos.empty()) {
    copy_permute<T, decltype(in)> (in, header_out, output_filename);
  } else {
    auto extract = Adapter::make<Adapter::Extract> (in, pos);
    copy_permute<T, decltype(extract)> (extract, header_out, output_filename);
  }
}










void run ()
{
  Header header_in = Header::open (argument[0]);
  Eigen::MatrixXd dw_scheme;
  try {
    dw_scheme = DWI::get_DW_scheme (header_in, DWI::get_cmdline_bvalue_scaling_behaviour());
  }
  catch (Exception& e) {
    if (get_options ("grad").size() || get_options ("fslgrad").size() || get_options ("bvalue_scaling").size())
      throw;
    e.display (2);
  }

  auto opt = get_options("json_import");
  if (!opt.empty())
    File::JSON::load(header_in, opt[0][0]);
  if (!get_options("import_pe_table").empty() || !get_options("import_pe_topup").empty() || !get_options("import_pe_eddy").empty())
    Metadata::PhaseEncoding::set_scheme(header_in.keyval(), Metadata::PhaseEncoding::get_scheme(header_in));

  Header header_out (header_in);
  header_out.datatype() = DataType::from_command_line (header_out.datatype());
  if (header_in.datatype().is_complex() && !header_out.datatype().is_complex())
    WARN ("requested datatype is real but input datatype is complex - imaginary component will be ignored");

  opt = get_options ("copy_properties");
  if (opt.size()) {
    header_out.keyval().clear();
    if (str(opt[0][0]) != "NULL") {
      try {
        const Header source = Header::open (opt[0][0]);
        header_out.keyval() = source.keyval();
      } catch (...) {
        try {
          File::JSON::load (header_out, opt[0][0]);
        } catch (...) {
          throw Exception ("Unable to obtain header key-value entries from spec \"" + str(opt[0][0]) + "\"");
        }
      }
    }
  }

  opt = get_options ("clear_property");
  for (size_t n = 0; n < opt.size(); ++n) {
    if (str(opt[n][0]) == "command_history")
      add_to_command_history = false;
    auto entry = header_out.keyval().find (opt[n][0]);
    if (entry == header_out.keyval().end()) {
      if (std::string(opt[n][0]) != "command_history") {
        WARN ("No header key/value entry \"" + opt[n][0] + "\" found; ignored");
      }
    } else {
      header_out.keyval().erase (entry);
    }
  }

  opt = get_options ("set_property");
  for (size_t n = 0; n < opt.size(); ++n) {
    if (str(opt[n][0]) == "command_history")
      add_to_command_history = false;
    header_out.keyval()[opt[n][0].as_text()] = opt[n][1].as_text();
  }

  opt = get_options ("append_property");
  for (size_t n = 0; n < opt.size(); ++n) {
    if (str(opt[n][0]) == "command_history")
      add_to_command_history = false;
    add_line (header_out.keyval()[opt[n][0].as_text()], opt[n][1].as_text());
  }




  opt = get_options ("coord");
  vector<vector<uint32_t>> pos;
  if (opt.size()) {
    pos.assign (header_in.ndim(), vector<uint32_t>());
    for (size_t n = 0; n < opt.size(); n++) {
      size_t axis = opt[n][0];
      if (axis >= header_in.ndim())
        throw Exception ("axis " + str(axis) + " provided with -coord option is out of range of input image");
      if (pos[axis].size())
        throw Exception ("\"coord\" option specified twice for axis " + str (axis));
      pos[axis] = parse_ints<uint32_t> (opt[n][1], header_in.size(axis)-1);

      auto minval = std::min_element(std::begin(pos[axis]), std::end(pos[axis]));
      if (*minval < 0)
        throw Exception ("coordinate position " + str(*minval) + " for axis " + str(axis) + " provided with -coord option is negative");
      auto maxval = std::max_element(std::begin(pos[axis]), std::end(pos[axis]));
      if (*maxval >= header_in.size(axis))
        throw Exception ("coordinate position " + str(*maxval) + " for axis " + str(axis) + " provided with -coord option is out of range of input image");

      header_out.size (axis) = pos[axis].size();
      if (axis == 3) {
        const auto grad = DWI::parse_DW_scheme (header_out);
        if (grad.rows()) {
          if ((ssize_t)grad.rows() != header_in.size(3)) {
            WARN ("Diffusion encoding of input file does not match number of image volumes; omitting gradient information from output image");
            DWI::clear_DW_scheme (header_out);
          } else {
            Eigen::MatrixXd extract_grad (pos[3].size(), grad.cols());
            for (size_t dir = 0; dir != pos[3].size(); ++dir)
              extract_grad.row (dir) = grad.row (pos[3][dir]);
            DWI::set_DW_scheme (header_out, extract_grad);
          }
        }
        Eigen::MatrixXd pe_scheme;
        try {
          pe_scheme = Metadata::PhaseEncoding::get_scheme (header_in);
          if (pe_scheme.rows()) {
            Eigen::MatrixXd extract_scheme (pos[3].size(), pe_scheme.cols());
            for (size_t vol = 0; vol != pos[3].size(); ++vol)
              extract_scheme.row (vol) = pe_scheme.row (pos[3][vol]);
            Metadata::PhaseEncoding::set_scheme (header_out.keyval(), extract_scheme);
          }
        } catch (...) {
          WARN ("Phase encoding scheme of input file does not match number of image volumes;"
                " omitting information from output image");
          Metadata::PhaseEncoding::clear_scheme(header_out.keyval());
        }
      }
    }

    for (size_t n = 0; n < header_in.ndim(); ++n) {
      if (pos[n].empty()) {
        pos[n].resize (header_in.size (n));
        for (uint32_t i = 0; i < uint32_t(pos[n].size()); i++)
          pos[n][i] = i;
      }
    }
  }


  opt = get_options ("scaling");
  if (opt.size()) {
    if (header_out.datatype().is_integer()) {
      vector<default_type> scaling = opt[0][0];
      if (scaling.size() != 2)
        throw Exception ("-scaling option expects comma-separated 2-vector of floating-point values");
      header_out.intensity_offset() = scaling[0];
      header_out.intensity_scale()  = scaling[1];
    }
    else
      WARN ("-scaling option has no effect for floating-point or binary images");
  }


  if (header_out.intensity_offset() == 0.0 && header_out.intensity_scale() == 1.0 && !header_out.datatype().is_floating_point()) {
    switch (header_out.datatype()() & DataType::Type) {
      case DataType::Bit:
      case DataType::UInt8:
      case DataType::UInt16:
      case DataType::UInt32:
        if (header_out.datatype().is_signed())
          extract<int32_t> (header_in, header_out, pos, argument[1]);
        else
          extract<uint32_t> (header_in, header_out, pos, argument[1]);
        break;
      case DataType::UInt64:
        if (header_out.datatype().is_signed())
          extract<int64_t> (header_in, header_out, pos, argument[1]);
        else
          extract<uint64_t> (header_in, header_out, pos, argument[1]);
        break;
      case DataType::Undefined: throw Exception ("invalid output image data type"); break;

    }
  }
  else {
    if (header_out.datatype().is_complex())
      extract<cdouble> (header_in, header_out, pos, argument[1]);
    else
      extract<double> (header_in, header_out, pos, argument[1]);
  }


  opt = get_options ("json_export");
  if (opt.size())
    File::JSON::save (header_out, opt[0][0], argument[1]);
}