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]);
}
|