From a2d106748b8052c8db2dff99d81992ddc30ff371 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Wed, 28 Jan 2026 23:51:30 -0500 Subject: [PATCH 1/3] add trx support to tckconvert --- CMakeLists.txt | 2 + cpp/cmd/CMakeLists.txt | 3 + cpp/cmd/tckconvert.cpp | 396 +++++++++++++++++-- docs/reference/commands/tckconvert.rst | 13 +- testing/binaries/tests/tckconvert/tckconvert | 38 ++ 5 files changed, 416 insertions(+), 36 deletions(-) create mode 100644 testing/binaries/tests/tckconvert/tckconvert diff --git a/CMakeLists.txt b/CMakeLists.txt index 1214c97dd9..a77ed81973 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,8 @@ include(Dependencies) include(CompilerCache) use_compiler_cache() +find_package(trx-cpp CONFIG REQUIRED) + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git AND NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git/hooks/pre-commit) message(WARNING "pre-commit git hook for clang-format not found!\n" diff --git a/cpp/cmd/CMakeLists.txt b/cpp/cmd/CMakeLists.txt index 4de37c0462..e98cc1677c 100644 --- a/cpp/cmd/CMakeLists.txt +++ b/cpp/cmd/CMakeLists.txt @@ -36,6 +36,9 @@ function(add_cmd CMD_SRC IS_GUI) $,mrtrix::gui,mrtrix::core> mrtrix::executable-version ) + if(CMD_NAME STREQUAL "tckconvert") + target_link_libraries(${CMD_NAME} PRIVATE trx-cpp::trx) + endif() set_target_properties(${CMD_NAME} PROPERTIES LINK_DEPENDS_NO_SHARED true RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin diff --git a/cpp/cmd/tckconvert.cpp b/cpp/cmd/tckconvert.cpp index 11a41de462..be3bd61aa0 100644 --- a/cpp/cmd/tckconvert.cpp +++ b/cpp/cmd/tckconvert.cpp @@ -16,7 +16,7 @@ #include #include -#include +#include #include "command.h" #include "dwi/tractography/file.h" @@ -24,7 +24,9 @@ #include "file/matrix.h" #include "file/name_parser.h" #include "file/ofstream.h" +#include "file/path.h" #include "raw.h" +#include using namespace MR; using namespace App; @@ -49,9 +51,19 @@ void usage() { DESCRIPTION + "The program currently supports" " MRtrix .tck files (input/output)," + " TRX .trx files (input/output)," " ascii text files (input/output)," " VTK polydata files (input/output)," - " and RenderMan RIB (export only)."; + " and RenderMan RIB (export only)." + + + "TRX files can include data per streamline (dps), data per vertex (dpv)," + " data per group (dpg), and group membership. By default, tckconvert only" + " modifies streamline geometry: these fields are preserved only for TRX->TRX" + " conversion and are discarded when converting to other formats that do not" + " support them. The -dps and -dpv options can be used to add new TRX data" + " entries during conversion. When applying a coordinate transform, only" + " the streamline positions are modified; any TRX metadata values are left" + " unchanged."; EXAMPLES + Example("Writing multiple ASCII files, one per streamline", @@ -62,8 +74,8 @@ void usage() { " output-0000.txt, output-0001.txt, output-0002.txt, ..."); ARGUMENTS - + Argument ("input", "the input track file.").type_tracks_in().type_file_in().type_text() - + Argument ("output", "the output track file.").type_tracks_out().type_file_out(); + + Argument ("input", "the input track file.").type_file_in().type_directory_in() + + Argument ("output", "the output track file.").type_file_out().type_directory_out(); OPTIONS + Option ("scanner2voxel", @@ -110,7 +122,23 @@ void usage() { + OptionGroup ("Options specific to VTK writer") + Option ("ascii", "write an ASCII VTK file" - " (binary by default)"); + " (binary by default)") + + + OptionGroup ("Options specific to TRX writer") + + + Option ("trxdirectory", "write an uncompressed TRX directory instead of a .trx file") + + + Option ("dps", "add data-per-streamline values from a text file") + .allow_multiple() + + Argument ("name").type_text() + + Argument ("datatype").type_text() + + Argument ("path").type_file_in() + + + Option ("dpv", "add data-per-vertex values from a TSF file") + .allow_multiple() + + Argument ("name").type_text() + + Argument ("datatype").type_text() + + Argument ("path").type_file_in(); } // clang-format on @@ -329,6 +357,206 @@ class ASCIIWriter : public WriterInterface { std::vector count; }; +struct DpsSpec { + std::string name; + std::string dtype; + std::string path; +}; + +struct DpvSpec { + std::string name; + std::string dtype; + std::string path; +}; + +static std::vector get_dps_specs() { + std::vector specs; + for (const auto &opt : get_options("dps")) { + if (opt.args.size() != 3) { + throw Exception("Option -dps requires 3 arguments: name datatype path"); + } + specs.push_back({std::string(opt[0]), std::string(opt[1]), std::string(opt[2])}); + } + return specs; +} + +static std::vector get_dpv_specs() { + std::vector specs; + for (const auto &opt : get_options("dpv")) { + if (opt.args.size() != 3) { + throw Exception("Option -dpv requires 3 arguments: name datatype path"); + } + specs.push_back({std::string(opt[0]), std::string(opt[1]), std::string(opt[2])}); + } + return specs; +} + +static bool trx_has_aux_data(const trxmmap::TrxFile *trx) { + if (!trx) + return false; + return !(trx->groups.empty() && trx->data_per_streamline.empty() && trx->data_per_vertex.empty() && + trx->data_per_group.empty()); +} + +static bool is_trx_directory(std::string_view path) { + try { + return trxmmap::is_trx_directory(std::string(path)); + } catch (const std::exception &) { + return false; + } +} + +static void apply_transform_to_trx(trxmmap::TrxFile *trx, const transform_type &T) { + if (!trx || !trx->streamlines) + return; + if (T.isApprox(transform_type::Identity())) + return; + const auto T_float = T.cast(); + auto &data = trx->streamlines->_data; + for (Eigen::Index i = 0; i < data.rows(); ++i) { + Eigen::Vector3f pos(data(i, 0), data(i, 1), data(i, 2)); + pos = T_float * pos; + data(i, 0) = pos[0]; + data(i, 1) = pos[1]; + data(i, 2) = pos[2]; + } +} + +class TRXReader : public ReaderInterface { +public: + explicit TRXReader(std::string_view file) : trx(nullptr), current(0), num_streamlines(0), has_aux_data(false) { + try { + const std::string filename(file); + if (is_trx_directory(filename)) { + trx = trxmmap::load_from_directory(filename); + } else { + trx = trxmmap::load_from_zip(filename); + } + if (trx && trx->streamlines && trx->streamlines->_offsets.size() > 0) + num_streamlines = trx->streamlines->_offsets.size() - 1; + has_aux_data = trx_has_aux_data(trx); + } catch (const std::exception &e) { + throw Exception(e.what()); + } + } + + bool operator()(Streamline &tck) { + tck.clear(); + if (!trx || current >= num_streamlines) + return false; + const Eigen::Index start = trx->streamlines->_offsets(current, 0); + const Eigen::Index end = trx->streamlines->_offsets(current + 1, 0); + tck.reserve(end - start); + for (Eigen::Index i = start; i < end; ++i) { + Eigen::Vector3f p; + p[0] = trx->streamlines->_data(i, 0); + p[1] = trx->streamlines->_data(i, 1); + p[2] = trx->streamlines->_data(i, 2); + tck.push_back(p); + } + ++current; + return true; + } + + ~TRXReader() { + if (!trx) + return; + try { + trx->close(); + delete trx; + trx = nullptr; + } catch (const std::exception &e) { + Exception(e.what()).display(); + App::exit_error_code = 1; + } + } + + bool has_metadata() const { return has_aux_data; } + +private: + trxmmap::TrxFile *trx; + Eigen::Index current; + Eigen::Index num_streamlines; + bool has_aux_data; +}; + +class TRXWriter : public WriterInterface { +public: + TRXWriter(std::string_view file, + size_t nb_streamlines, + size_t nb_vertices, + const std::vector &dps_specs, + const std::vector &dpv_specs, + std::string_view final_output, + bool rename_on_save) + : output(file), + final_output(final_output), + rename_on_save(rename_on_save), + current_streamline(0), + current_vertex(0) { + try { + trx = new trxmmap::TrxFile(static_cast(nb_vertices), static_cast(nb_streamlines), nullptr); + if (trx->streamlines && trx->streamlines->_offsets.size() > 0) + trx->streamlines->_offsets(0, 0) = 0; + for (const auto &spec : dps_specs) { + trxmmap::add_dps_from_text(*trx, spec.name, spec.dtype, spec.path); + } + for (const auto &spec : dpv_specs) { + trxmmap::add_dpv_from_tsf(*trx, spec.name, spec.dtype, spec.path); + } + } catch (const std::exception &e) { + throw Exception(e.what()); + } + } + + bool operator()(const Streamline &tck) { + if (!trx || !trx->streamlines) + return false; + const Eigen::Index tck_size = static_cast(tck.size()); + for (Eigen::Index i = 0; i < tck_size; ++i) { + const auto &pos = tck[static_cast(i)]; + trx->streamlines->_data(current_vertex + i, 0) = pos[0]; + trx->streamlines->_data(current_vertex + i, 1) = pos[1]; + trx->streamlines->_data(current_vertex + i, 2) = pos[2]; + } + trx->streamlines->_lengths(current_streamline, 0) = static_cast(tck_size); + trx->streamlines->_offsets(current_streamline + 1, 0) = static_cast(current_vertex + tck_size); + current_vertex += tck_size; + ++current_streamline; + return true; + } + + ~TRXWriter() { + if (!trx) + return; + try { + trxmmap::save(*trx, output, ZIP_CM_STORE); + if (rename_on_save) { + std::error_code ec; + std::filesystem::remove_all(final_output, ec); + std::filesystem::rename(output, final_output, ec); + if (ec) { + throw std::runtime_error("Failed to rename TRX directory to " + final_output + ": " + ec.message()); + } + } + trx->close(); + delete trx; + trx = nullptr; + } catch (const std::exception &e) { + Exception(e.what()).display(); + App::exit_error_code = 1; + } + } + +private: + std::string output; + std::string final_output; + bool rename_on_save; + trxmmap::TrxFile *trx = nullptr; + Eigen::Index current_streamline; + Eigen::Index current_vertex; +}; + class PLYWriter : public WriterInterface { public: PLYWriter(std::string_view file, @@ -680,15 +908,119 @@ class RibWriter : public WriterInterface { }; void run() { + const bool input_is_trx_dir = is_trx_directory(argument[0]); + const bool input_is_trx = Path::has_suffix(argument[0], ".trx") || input_is_trx_dir; + const bool write_trx_directory = get_options("trxdirectory").size(); + const bool output_is_trx = Path::has_suffix(argument[1], ".trx") || write_trx_directory; + std::string trx_output = std::string(argument[1]); + std::string trx_save_path = trx_output; + bool rename_trx_directory = false; + if (write_trx_directory) { + if (!Path::has_suffix(trx_output, ".trx")) { + trx_output += ".trx"; + } + if (Path::has_suffix(trx_output, ".trx")) { + trx_save_path = trx_output.substr(0, trx_output.size() - 4); + rename_trx_directory = true; + } + } + const auto dps_specs = get_dps_specs(); + const auto dpv_specs = get_dpv_specs(); + if (!dps_specs.empty() && !output_is_trx) { + throw Exception("Option -dps requires TRX output."); + } + if (!dpv_specs.empty() && !output_is_trx) { + throw Exception("Option -dpv requires TRX output."); + } + + // Tranform matrix + transform_type T; + T.setIdentity(); + size_t nopts = 0; + auto opt = get_options("scanner2voxel"); + if (!opt.empty()) { + auto header = Header::open(opt[0][0]); + T = MR::Transform(header).scanner2voxel; + nopts++; + } + opt = get_options("scanner2image"); + if (!opt.empty()) { + auto header = Header::open(opt[0][0]); + T = MR::Transform(header).scanner2image; + nopts++; + } + opt = get_options("voxel2scanner"); + if (!opt.empty()) { + auto header = Header::open(opt[0][0]); + T = MR::Transform(header).voxel2scanner; + nopts++; + } + opt = get_options("image2scanner"); + if (!opt.empty()) { + auto header = Header::open(opt[0][0]); + T = MR::Transform(header).image2scanner; + nopts++; + } + if (nopts > 1) { + throw Exception("Transform options are mutually exclusive."); + } + + if (input_is_trx && output_is_trx) { + trxmmap::TrxFile *trx = nullptr; + try { + if (input_is_trx_dir) { + trx = trxmmap::load_from_directory(argument[0]); + } else { + trx = trxmmap::load_from_zip(argument[0]); + } + } catch (const std::exception &e) { + throw Exception(e.what()); + } + if (!trx) + throw Exception("Failed to load TRX input."); + apply_transform_to_trx(trx, T); + for (const auto &spec : dps_specs) { + trxmmap::add_dps_from_text(*trx, spec.name, spec.dtype, spec.path); + } + for (const auto &spec : dpv_specs) { + trxmmap::add_dpv_from_tsf(*trx, spec.name, spec.dtype, spec.path); + } + try { + trxmmap::save(*trx, trx_save_path, ZIP_CM_STORE); + if (rename_trx_directory) { + std::error_code ec; + std::filesystem::remove_all(trx_output, ec); + std::filesystem::rename(trx_save_path, trx_output, ec); + if (ec) { + throw Exception("Failed to rename TRX directory to " + trx_output + ": " + ec.message()); + } + } + trx->close(); + delete trx; + } catch (const std::exception &e) { + if (trx) { + trx->close(); + delete trx; + } + throw Exception(e.what()); + } + return; + } + // Reader Properties properties; std::unique_ptr> reader; + TRXReader *trx_reader = nullptr; if (Path::has_suffix(argument[0], ".tck")) { reader.reset(new Reader(argument[0], properties)); } else if (Path::has_suffix(argument[0], ".txt")) { reader.reset(new ASCIIReader(argument[0])); } else if (Path::has_suffix(argument[0], ".vtk")) { reader.reset(new VTKReader(argument[0])); + } else if (input_is_trx) { + auto *trx_reader_ptr = new TRXReader(argument[0]); + reader.reset(trx_reader_ptr); + trx_reader = trx_reader_ptr; } else { throw Exception("Unsupported input file type."); } @@ -709,40 +1041,34 @@ void run() { writer.reset(new RibWriter(argument[1])); } else if (Path::has_suffix(argument[1], ".txt")) { writer.reset(new ASCIIWriter(argument[1])); + } else if (output_is_trx) { + size_t nb_streamlines = 0; + size_t nb_vertices = 0; + Streamline count_tck; + std::unique_ptr> count_reader; + if (Path::has_suffix(argument[0], ".tck")) { + count_reader.reset(new Reader(argument[0], properties)); + } else if (Path::has_suffix(argument[0], ".txt")) { + count_reader.reset(new ASCIIReader(argument[0])); + } else if (Path::has_suffix(argument[0], ".vtk")) { + count_reader.reset(new VTKReader(argument[0])); + } else if (input_is_trx) { + count_reader.reset(new TRXReader(argument[0])); + } else { + throw Exception("Unsupported input file type."); + } + while ((*count_reader)(count_tck)) { + nb_streamlines++; + nb_vertices += count_tck.size(); + } + writer.reset(new TRXWriter( + trx_save_path, nb_streamlines, nb_vertices, dps_specs, dpv_specs, trx_output, rename_trx_directory)); } else { throw Exception("Unsupported output file type."); } - // Tranform matrix - transform_type T; - T.setIdentity(); - size_t nopts = 0; - auto opt = get_options("scanner2voxel"); - if (!opt.empty()) { - auto header = Header::open(opt[0][0]); - T = Transform(header).scanner2voxel; - nopts++; - } - opt = get_options("scanner2image"); - if (!opt.empty()) { - auto header = Header::open(opt[0][0]); - T = Transform(header).scanner2image; - nopts++; - } - opt = get_options("voxel2scanner"); - if (!opt.empty()) { - auto header = Header::open(opt[0][0]); - T = Transform(header).voxel2scanner; - nopts++; - } - opt = get_options("image2scanner"); - if (!opt.empty()) { - auto header = Header::open(opt[0][0]); - T = Transform(header).image2scanner; - nopts++; - } - if (nopts > 1) { - throw Exception("Transform options are mutually exclusive."); + if (input_is_trx && !output_is_trx && trx_reader && trx_reader->has_metadata()) { + WARN("TRX metadata (dps/dpv/dpg/groups) will be discarded when converting to non-TRX formats."); } // Copy diff --git a/docs/reference/commands/tckconvert.rst b/docs/reference/commands/tckconvert.rst index 99657b0e14..c96e972b9f 100644 --- a/docs/reference/commands/tckconvert.rst +++ b/docs/reference/commands/tckconvert.rst @@ -21,7 +21,9 @@ Usage Description ----------- -The program currently supports MRtrix .tck files (input/output), ascii text files (input/output), VTK polydata files (input/output), and RenderMan RIB (export only). +The program currently supports MRtrix .tck files (input/output), TRX .trx files (input/output), ascii text files (input/output), VTK polydata files (input/output), and RenderMan RIB (export only). + +TRX files can include data per streamline (dps), data per vertex (dpv), data per group (dpg), and group membership. tckconvert only operates on streamline geometry: these fields are preserved only for TRX->TRX conversion and are discarded when converting to other formats that do not support them. When applying a coordinate transform, only the streamline positions are modified; any TRX metadata values are left unchanged. Example usages -------------- @@ -65,6 +67,15 @@ Options specific to VTK writer - **-ascii** write an ASCII VTK file (binary by default) +Options specific to TRX writer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **-trxdirectory** write an uncompressed TRX directory instead of a .trx file + +- **-dps name datatype path** add data-per-streamline values from a text file (datatype: float16/float32/float64) + +- **-dpv name datatype path** add data-per-vertex values from a TSF file (datatype: float16/float32/float64) + Standard options ^^^^^^^^^^^^^^^^ diff --git a/testing/binaries/tests/tckconvert/tckconvert b/testing/binaries/tests/tckconvert/tckconvert new file mode 100644 index 0000000000..1e1666a270 --- /dev/null +++ b/testing/binaries/tests/tckconvert/tckconvert @@ -0,0 +1,38 @@ +tckconvert -ascii tracks.tck tmp.vtk -force && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tmp.vtk > tmppoints1.txt && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tckconvert/out0.vtk > tmppoints2.txt && testing_diff_matrix tmppoints1.txt tmppoints2.txt -abs 1e-4 +tckconvert tracks.tck tmp.vtk -force +tckconvert -ascii tracks.tck tmp.vtk -force && awk '/LINES/,0' tmp.vtk > tmplines1.txt && awk '/LINES/,0' tckconvert/out0.vtk > tmplines2.txt && diff tmplines1.txt tmplines2.txt +tckconvert -ascii tracks.tck -scanner2voxel dwi.mif tmp.vtk -force && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tmp.vtk > tmppoints1.txt && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tckconvert/out1.vtk > tmppoints2.txt && testing_diff_matrix tmppoints1.txt tmppoints2.txt -abs 1e-4 +tckconvert -ascii tracks.tck -scanner2voxel dwi.mif tmp.vtk -force && awk '/LINES/,0' tmp.vtk > tmplines1.txt && awk '/LINES/,0' tckconvert/out1.vtk > tmplines2.txt && diff tmplines1.txt tmplines2.txt +tckedit tracks.tck -number 10 tmp.tck -nthread 0 && tckconvert tmp.tck tmp-[].txt && cat tmp-*.txt > tmp-all.txt && testing_diff_matrix tmp-all.txt tckconvert/out2-all.txt -abs 1e-4 +tckconvert tckconvert/out2-[2:9].txt tmp.tck -force && testing_diff_tck tmp.tck tckconvert/out3.tck +echo 1 2 3 > tmp.txt && tckconvert -force -quiet tmp.txt tmp.tck && tckconvert -quiet -force tmp.tck tmp.rib && [ $(wc -l < tmp.rib ) == 4 ] +tckconvert -force -quiet tckconvert/empty.vtk tmp.tck + +# convert tck -> binary vtk: +tckconvert tracks.tck tmp.vtk -binary -force -quiet && diff tmp.vtk tckconvert/tracks-binary.vtk + +# convert binary vtk -> ascii vtk: +tckconvert tckconvert/tracks-binary.vtk tmp.vtk -ascii -force && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tmp.vtk > tmppoints1.txt && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tckconvert/out0.vtk > tmppoints2.txt && testing_diff_matrix tmppoints1.txt tmppoints2.txt -abs 1e-4 + +# convert binary vtk -> tck: +tckconvert tckconvert/tracks-binary.vtk tmp.tck -force && testing_diff_tck tmp.tck tracks.tck + +# convert TRX -> tck (compressed + directory): +tckconvert tckconvert/gs.trx tmp.tck -force && testing_diff_tck tmp.tck tckconvert/gs.tck +tckconvert tckconvert/gs_fldr.trx tmp.tck -force && testing_diff_tck tmp.tck tckconvert/gs.tck + +# convert tck -> TRX -> tck (compressed output): +tckconvert tckconvert/gs.tck tmp.trx -force && tckconvert tmp.trx tmp.tck -force && testing_diff_tck tmp.tck tckconvert/gs.tck + +# Test adding a data-per-streamline value to a TRX: + +# There are 13 streamlines in gs.tck, so we need 13 values in dps.txt: +for n in {0..12} +do + echo "${n}.0" >> dps.txt +done + +# Test adding a data-per-streamline value to a TRX: +tckconvert gs.tck tmp_dps_float32.trx -trxdirectory -dps dps float32 dps.txt -force && [[ -f tmp_dps_float32.trx/dps/dps.float32 ]] +tckconvert gs.tck tmp_dps_float16.trx -trxdirectory -dps dps float16 dps.txt -force && [[ -f tmp_dps_float16.trx/dps/dps.float16 ]] +tckconvert gs.tck tmp_dps_float64.trx -trxdirectory -dps dps float64 dps.txt -force && [[ -f tmp_dps_float64.trx/dps/dps.float64 ]] \ No newline at end of file From ecc77c6da239c5f298afc00c1613c6ae1fe24532 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Fri, 30 Jan 2026 14:35:25 -0500 Subject: [PATCH 2/3] smart pointers, new style tests, cmake --- cmake/Dependencies.cmake | 14 +++++++ cpp/cmd/tckconvert.cpp | 32 +++++++--------- testing/binaries/tests/tckconvert/tckconvert | 38 ------------------- testing/binaries/tests/tckconvert/trx_read | 3 ++ .../tests/tckconvert/trx_uncompressed_read | 3 ++ .../tests/tckconvert/trx_write_dps_float16 | 10 +++++ .../tests/tckconvert/trx_write_dps_float32 | 11 ++++++ .../tests/tckconvert/trx_write_dps_float64 | 11 ++++++ 8 files changed, 66 insertions(+), 56 deletions(-) delete mode 100644 testing/binaries/tests/tckconvert/tckconvert create mode 100644 testing/binaries/tests/tckconvert/trx_read create mode 100644 testing/binaries/tests/tckconvert/trx_uncompressed_read create mode 100644 testing/binaries/tests/tckconvert/trx_write_dps_float16 create mode 100644 testing/binaries/tests/tckconvert/trx_write_dps_float32 create mode 100644 testing/binaries/tests/tckconvert/trx_write_dps_float64 diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 1c0910a9d2..0883592754 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -80,6 +80,20 @@ add_library(nifti::nifti ALIAS nifti) target_include_directories(nifti INTERFACE "${NIFTI_INCLUDE_DIRS}") +# TRX Format +if(MRTRIX_USE_SYSTEM_TRXCPP) + find_package(trx-cpp CONFIG REQUIRED) +else() + set(trxcpp_url "https://github.com/tee-ar-ex/trx-cpp") + FetchContent_Declare( + trx-cpp + DOWNLOAD_EXTRACT_TIMESTAMP ON + GIT_REPOSITORY ${trxcpp_url} + GIT_TAG ea9ebcbf4b08e047e6493eb8077f789cdfac36b7 + ) + FetchContent_MakeAvailable(trx-cpp) +endif() + # Google Test if(MRTRIX_BUILD_TESTS) set(googletest_version 1.17.0) diff --git a/cpp/cmd/tckconvert.cpp b/cpp/cmd/tckconvert.cpp index be3bd61aa0..491fb2f44d 100644 --- a/cpp/cmd/tckconvert.cpp +++ b/cpp/cmd/tckconvert.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "command.h" #include "dwi/tractography/file.h" @@ -428,13 +429,13 @@ class TRXReader : public ReaderInterface { try { const std::string filename(file); if (is_trx_directory(filename)) { - trx = trxmmap::load_from_directory(filename); + trx.reset(trxmmap::load_from_directory(filename)); } else { - trx = trxmmap::load_from_zip(filename); + trx.reset(trxmmap::load_from_zip(filename)); } if (trx && trx->streamlines && trx->streamlines->_offsets.size() > 0) num_streamlines = trx->streamlines->_offsets.size() - 1; - has_aux_data = trx_has_aux_data(trx); + has_aux_data = trx_has_aux_data(trx.get()); } catch (const std::exception &e) { throw Exception(e.what()); } @@ -463,8 +464,7 @@ class TRXReader : public ReaderInterface { return; try { trx->close(); - delete trx; - trx = nullptr; + trx.reset(); } catch (const std::exception &e) { Exception(e.what()).display(); App::exit_error_code = 1; @@ -474,7 +474,7 @@ class TRXReader : public ReaderInterface { bool has_metadata() const { return has_aux_data; } private: - trxmmap::TrxFile *trx; + std::unique_ptr> trx; Eigen::Index current; Eigen::Index num_streamlines; bool has_aux_data; @@ -495,7 +495,7 @@ class TRXWriter : public WriterInterface { current_streamline(0), current_vertex(0) { try { - trx = new trxmmap::TrxFile(static_cast(nb_vertices), static_cast(nb_streamlines), nullptr); + trx.reset(new trxmmap::TrxFile(static_cast(nb_vertices), static_cast(nb_streamlines), nullptr)); if (trx->streamlines && trx->streamlines->_offsets.size() > 0) trx->streamlines->_offsets(0, 0) = 0; for (const auto &spec : dps_specs) { @@ -540,8 +540,7 @@ class TRXWriter : public WriterInterface { } } trx->close(); - delete trx; - trx = nullptr; + trx.reset(); } catch (const std::exception &e) { Exception(e.what()).display(); App::exit_error_code = 1; @@ -552,7 +551,7 @@ class TRXWriter : public WriterInterface { std::string output; std::string final_output; bool rename_on_save; - trxmmap::TrxFile *trx = nullptr; + std::unique_ptr> trx; Eigen::Index current_streamline; Eigen::Index current_vertex; }; @@ -966,19 +965,19 @@ void run() { } if (input_is_trx && output_is_trx) { - trxmmap::TrxFile *trx = nullptr; + std::unique_ptr> trx; try { if (input_is_trx_dir) { - trx = trxmmap::load_from_directory(argument[0]); + trx.reset(trxmmap::load_from_directory(argument[0])); } else { - trx = trxmmap::load_from_zip(argument[0]); + trx.reset(trxmmap::load_from_zip(argument[0])); } } catch (const std::exception &e) { throw Exception(e.what()); } if (!trx) throw Exception("Failed to load TRX input."); - apply_transform_to_trx(trx, T); + apply_transform_to_trx(trx.get(), T); for (const auto &spec : dps_specs) { trxmmap::add_dps_from_text(*trx, spec.name, spec.dtype, spec.path); } @@ -996,12 +995,9 @@ void run() { } } trx->close(); - delete trx; } catch (const std::exception &e) { - if (trx) { + if (trx) trx->close(); - delete trx; - } throw Exception(e.what()); } return; diff --git a/testing/binaries/tests/tckconvert/tckconvert b/testing/binaries/tests/tckconvert/tckconvert deleted file mode 100644 index 1e1666a270..0000000000 --- a/testing/binaries/tests/tckconvert/tckconvert +++ /dev/null @@ -1,38 +0,0 @@ -tckconvert -ascii tracks.tck tmp.vtk -force && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tmp.vtk > tmppoints1.txt && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tckconvert/out0.vtk > tmppoints2.txt && testing_diff_matrix tmppoints1.txt tmppoints2.txt -abs 1e-4 -tckconvert tracks.tck tmp.vtk -force -tckconvert -ascii tracks.tck tmp.vtk -force && awk '/LINES/,0' tmp.vtk > tmplines1.txt && awk '/LINES/,0' tckconvert/out0.vtk > tmplines2.txt && diff tmplines1.txt tmplines2.txt -tckconvert -ascii tracks.tck -scanner2voxel dwi.mif tmp.vtk -force && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tmp.vtk > tmppoints1.txt && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tckconvert/out1.vtk > tmppoints2.txt && testing_diff_matrix tmppoints1.txt tmppoints2.txt -abs 1e-4 -tckconvert -ascii tracks.tck -scanner2voxel dwi.mif tmp.vtk -force && awk '/LINES/,0' tmp.vtk > tmplines1.txt && awk '/LINES/,0' tckconvert/out1.vtk > tmplines2.txt && diff tmplines1.txt tmplines2.txt -tckedit tracks.tck -number 10 tmp.tck -nthread 0 && tckconvert tmp.tck tmp-[].txt && cat tmp-*.txt > tmp-all.txt && testing_diff_matrix tmp-all.txt tckconvert/out2-all.txt -abs 1e-4 -tckconvert tckconvert/out2-[2:9].txt tmp.tck -force && testing_diff_tck tmp.tck tckconvert/out3.tck -echo 1 2 3 > tmp.txt && tckconvert -force -quiet tmp.txt tmp.tck && tckconvert -quiet -force tmp.tck tmp.rib && [ $(wc -l < tmp.rib ) == 4 ] -tckconvert -force -quiet tckconvert/empty.vtk tmp.tck - -# convert tck -> binary vtk: -tckconvert tracks.tck tmp.vtk -binary -force -quiet && diff tmp.vtk tckconvert/tracks-binary.vtk - -# convert binary vtk -> ascii vtk: -tckconvert tckconvert/tracks-binary.vtk tmp.vtk -ascii -force && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tmp.vtk > tmppoints1.txt && awk '/POINTS/{s=1;next}/LINES/{s=0}s' tckconvert/out0.vtk > tmppoints2.txt && testing_diff_matrix tmppoints1.txt tmppoints2.txt -abs 1e-4 - -# convert binary vtk -> tck: -tckconvert tckconvert/tracks-binary.vtk tmp.tck -force && testing_diff_tck tmp.tck tracks.tck - -# convert TRX -> tck (compressed + directory): -tckconvert tckconvert/gs.trx tmp.tck -force && testing_diff_tck tmp.tck tckconvert/gs.tck -tckconvert tckconvert/gs_fldr.trx tmp.tck -force && testing_diff_tck tmp.tck tckconvert/gs.tck - -# convert tck -> TRX -> tck (compressed output): -tckconvert tckconvert/gs.tck tmp.trx -force && tckconvert tmp.trx tmp.tck -force && testing_diff_tck tmp.tck tckconvert/gs.tck - -# Test adding a data-per-streamline value to a TRX: - -# There are 13 streamlines in gs.tck, so we need 13 values in dps.txt: -for n in {0..12} -do - echo "${n}.0" >> dps.txt -done - -# Test adding a data-per-streamline value to a TRX: -tckconvert gs.tck tmp_dps_float32.trx -trxdirectory -dps dps float32 dps.txt -force && [[ -f tmp_dps_float32.trx/dps/dps.float32 ]] -tckconvert gs.tck tmp_dps_float16.trx -trxdirectory -dps dps float16 dps.txt -force && [[ -f tmp_dps_float16.trx/dps/dps.float16 ]] -tckconvert gs.tck tmp_dps_float64.trx -trxdirectory -dps dps float64 dps.txt -force && [[ -f tmp_dps_float64.trx/dps/dps.float64 ]] \ No newline at end of file diff --git a/testing/binaries/tests/tckconvert/trx_read b/testing/binaries/tests/tckconvert/trx_read new file mode 100644 index 0000000000..4ef54ae290 --- /dev/null +++ b/testing/binaries/tests/tckconvert/trx_read @@ -0,0 +1,3 @@ +#!/bin/bash +# convert TRX -> tck: +tckconvert gs.trx tmp.tck -force && testing_diff_tck tmp.tck gs.tck \ No newline at end of file diff --git a/testing/binaries/tests/tckconvert/trx_uncompressed_read b/testing/binaries/tests/tckconvert/trx_uncompressed_read new file mode 100644 index 0000000000..2ae068ae7a --- /dev/null +++ b/testing/binaries/tests/tckconvert/trx_uncompressed_read @@ -0,0 +1,3 @@ +#!/bin/bash +# convert TRX (uncompressed directory) -> tck: +tckconvert gs_fldr.trx tmp.tck -force && testing_diff_tck tmp.tck gs.tck \ No newline at end of file diff --git a/testing/binaries/tests/tckconvert/trx_write_dps_float16 b/testing/binaries/tests/tckconvert/trx_write_dps_float16 new file mode 100644 index 0000000000..51e72274ac --- /dev/null +++ b/testing/binaries/tests/tckconvert/trx_write_dps_float16 @@ -0,0 +1,10 @@ +#!/bin/bash +# Test adding a data-per-streamline value to a TRX with float16 datatype +# Also tests writing an uncompressed TRX directory. +# There are 13 streamlines in gs.tck, so we need 13 values in dps.txt: +for n in {0..12} +do + echo "${n}.0" >> dps.txt +done + +tckconvert gs.tck tmp_dps_float16.trx -trxdirectory -dps dps float16 dps.txt -force && [[ -f tmp_dps_float16.trx/dps/dps.float16 ]] \ No newline at end of file diff --git a/testing/binaries/tests/tckconvert/trx_write_dps_float32 b/testing/binaries/tests/tckconvert/trx_write_dps_float32 new file mode 100644 index 0000000000..ed5dc170b5 --- /dev/null +++ b/testing/binaries/tests/tckconvert/trx_write_dps_float32 @@ -0,0 +1,11 @@ +#!/bin/bash +# Test adding a data-per-streamline value to a TRX with float32 datatype +# Also tests writing an uncompressed TRX directory. +# There are 13 streamlines in gs.tck, so we need 13 values in dps.txt: +for n in {0..12} +do + echo "${n}.0" >> dps.txt +done + +# Test adding a data-per-streamline value to a TRX: +tckconvert gs.tck tmp_dps_float32.trx -trxdirectory -dps dps float32 dps.txt -force && [[ -f tmp_dps_float32.trx/dps/dps.float32 ]] \ No newline at end of file diff --git a/testing/binaries/tests/tckconvert/trx_write_dps_float64 b/testing/binaries/tests/tckconvert/trx_write_dps_float64 new file mode 100644 index 0000000000..7323b49eae --- /dev/null +++ b/testing/binaries/tests/tckconvert/trx_write_dps_float64 @@ -0,0 +1,11 @@ +#!/bin/bash +# Test adding a data-per-streamline value to a TRX with float64 datatype +# Also tests writing an uncompressed TRX directory. +# There are 13 streamlines in gs.tck, so we need 13 values in dps.txt: +for n in {0..12} +do + echo "${n}.0" >> dps.txt +done + +# Test adding a data-per-streamline value to a TRX: +tckconvert gs.tck tmp_dps_float64.trx -trxdirectory -dps dps float64 dps.txt -force && [[ -f tmp_dps_float64.trx/dps/dps.float64 ]] \ No newline at end of file From ae7d3be51500e45cc73d8ee485e234deb1e1fcdd Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Fri, 13 Mar 2026 14:02:56 -0400 Subject: [PATCH 3/3] Update for newest trx-cpp --- cmake/Dependencies.cmake | 2 +- cpp/cmd/tckconvert.cpp | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 0883592754..e6122403e9 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -89,7 +89,7 @@ else() trx-cpp DOWNLOAD_EXTRACT_TIMESTAMP ON GIT_REPOSITORY ${trxcpp_url} - GIT_TAG ea9ebcbf4b08e047e6493eb8077f789cdfac36b7 + GIT_TAG b628275c51f37f9c9070aff29d906e29453be3ce ) FetchContent_MakeAvailable(trx-cpp) endif() diff --git a/cpp/cmd/tckconvert.cpp b/cpp/cmd/tckconvert.cpp index 491fb2f44d..e9601145bb 100644 --- a/cpp/cmd/tckconvert.cpp +++ b/cpp/cmd/tckconvert.cpp @@ -392,7 +392,7 @@ static std::vector get_dpv_specs() { return specs; } -static bool trx_has_aux_data(const trxmmap::TrxFile *trx) { +static bool trx_has_aux_data(const trx::TrxFile *trx) { if (!trx) return false; return !(trx->groups.empty() && trx->data_per_streamline.empty() && trx->data_per_vertex.empty() && @@ -401,13 +401,13 @@ static bool trx_has_aux_data(const trxmmap::TrxFile *trx) { static bool is_trx_directory(std::string_view path) { try { - return trxmmap::is_trx_directory(std::string(path)); + return trx::is_trx_directory(std::string(path)); } catch (const std::exception &) { return false; } } -static void apply_transform_to_trx(trxmmap::TrxFile *trx, const transform_type &T) { +static void apply_transform_to_trx(trx::TrxFile *trx, const transform_type &T) { if (!trx || !trx->streamlines) return; if (T.isApprox(transform_type::Identity())) @@ -429,9 +429,9 @@ class TRXReader : public ReaderInterface { try { const std::string filename(file); if (is_trx_directory(filename)) { - trx.reset(trxmmap::load_from_directory(filename)); + trx = trx::load_from_directory(filename); } else { - trx.reset(trxmmap::load_from_zip(filename)); + trx = trx::load_from_zip(filename); } if (trx && trx->streamlines && trx->streamlines->_offsets.size() > 0) num_streamlines = trx->streamlines->_offsets.size() - 1; @@ -474,7 +474,7 @@ class TRXReader : public ReaderInterface { bool has_metadata() const { return has_aux_data; } private: - std::unique_ptr> trx; + std::unique_ptr> trx; Eigen::Index current; Eigen::Index num_streamlines; bool has_aux_data; @@ -495,14 +495,14 @@ class TRXWriter : public WriterInterface { current_streamline(0), current_vertex(0) { try { - trx.reset(new trxmmap::TrxFile(static_cast(nb_vertices), static_cast(nb_streamlines), nullptr)); + trx.reset(new trx::TrxFile(static_cast(nb_vertices), static_cast(nb_streamlines), nullptr)); if (trx->streamlines && trx->streamlines->_offsets.size() > 0) trx->streamlines->_offsets(0, 0) = 0; for (const auto &spec : dps_specs) { - trxmmap::add_dps_from_text(*trx, spec.name, spec.dtype, spec.path); + trx->add_dps_from_text(spec.name, spec.dtype, spec.path); } for (const auto &spec : dpv_specs) { - trxmmap::add_dpv_from_tsf(*trx, spec.name, spec.dtype, spec.path); + trx->add_dpv_from_tsf(spec.name, spec.dtype, spec.path); } } catch (const std::exception &e) { throw Exception(e.what()); @@ -530,7 +530,7 @@ class TRXWriter : public WriterInterface { if (!trx) return; try { - trxmmap::save(*trx, output, ZIP_CM_STORE); + trx->save(output, ZIP_CM_STORE); if (rename_on_save) { std::error_code ec; std::filesystem::remove_all(final_output, ec); @@ -551,7 +551,7 @@ class TRXWriter : public WriterInterface { std::string output; std::string final_output; bool rename_on_save; - std::unique_ptr> trx; + std::unique_ptr> trx; Eigen::Index current_streamline; Eigen::Index current_vertex; }; @@ -965,12 +965,12 @@ void run() { } if (input_is_trx && output_is_trx) { - std::unique_ptr> trx; + std::unique_ptr> trx; try { if (input_is_trx_dir) { - trx.reset(trxmmap::load_from_directory(argument[0])); + trx = trx::load_from_directory(argument[0]); } else { - trx.reset(trxmmap::load_from_zip(argument[0])); + trx = trx::load_from_zip(argument[0]); } } catch (const std::exception &e) { throw Exception(e.what()); @@ -979,13 +979,13 @@ void run() { throw Exception("Failed to load TRX input."); apply_transform_to_trx(trx.get(), T); for (const auto &spec : dps_specs) { - trxmmap::add_dps_from_text(*trx, spec.name, spec.dtype, spec.path); + trx->add_dps_from_text(spec.name, spec.dtype, spec.path); } for (const auto &spec : dpv_specs) { - trxmmap::add_dpv_from_tsf(*trx, spec.name, spec.dtype, spec.path); + trx->add_dpv_from_tsf(spec.name, spec.dtype, spec.path); } try { - trxmmap::save(*trx, trx_save_path, ZIP_CM_STORE); + trx->save(trx_save_path, ZIP_CM_STORE); if (rename_trx_directory) { std::error_code ec; std::filesystem::remove_all(trx_output, ec);