Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename sh2power to sh2metric, new "entropy" operation #2985

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions cmd/sh2metric.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/* Copyright (c) 2008-2024 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 <string>
#include <vector>

#include "algo/threaded_loop.h"
#include "command.h"
#include "dwi/directions/set.h"
#include "image.h"
#include "image_helpers.h"
#include "math/SH.h"
#include "math/entropy.h"
#include "mrtrix.h"

using namespace MR;
using namespace App;

constexpr size_t default_direction_set = 1281;

std::vector<std::string> metrics = {"entropy", "power"};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'metrics' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]

std::vector<std::string> metrics = {"entropy", "power"};
                         ^


// clang-format off
void usage() {

AUTHOR = "J-Donald Tournier ([email protected]) and Robert E. Smith <[email protected]>";

SYNOPSIS = "Compute voxel-wise metrics from one or more spherical harmonics images";

DESCRIPTION
+ "Depending on the particular metric being computed,"
" the command may only accept a single input SH image;"
" wheras other metrics may accept multiple SH images as input (eg. ODFs)"
" and compute a single scalar output image."

+ "The various metrics available are detailed individually below."

+ "\"entropy\":"
" this metric computes the entropy (in nits, ie. logarithm base e)"
" of one or more spherical harmonics functions."
" This can be thought of as being inversely proportional to the overall \"complexity\""
" of the (set of) spherical harmonics function(s)."

+ "\"power\":"
" this metric computes the sum of squared SH coefficients,"
" which equals the mean-squared amplitude of the spherical function it represents."

+ Math::SH::encoding_description;

ARGUMENTS
+ Argument ("SH", "the input spherical harmonics coefficients image").type_image_in().allow_multiple()
+ Argument ("metric", "the metrc to compute; one of: " + join(metrics, ",")).type_choice(metrics)
+ Argument ("power", "the output metric image").type_image_out();

OPTIONS
+ OptionGroup ("Options specific to the \"entropy\" metric")
+ Option ("normalise", "normalise the voxel-wise entropy measure to the range [0.0, 1.0]")

+ OptionGroup ("Options specific to the \"power\" metric")
+ Option ("spectrum", "output the power spectrum,"
" i.e., the power contained within each harmonic degree (l=0, 2, 4, ...)"
" as a 4D image.");

}
// clang-format on

void run_entropy() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'run_entropy' has cognitive complexity of 30 (threshold 25) [readability-function-cognitive-complexity]

void run_entropy() {
     ^
Additional context

cmd/sh2metric.cpp:83: +1, including nesting penalty of 0, nesting level increased to 1

  for (size_t i = 0; i != argument.size() - 2; ++i) {
  ^

cmd/sh2metric.cpp:87: +2, including nesting penalty of 1, nesting level increased to 2

    if (i == 0) {
    ^

cmd/sh2metric.cpp:89: +1, nesting level increased to 2

    } else {
      ^

cmd/sh2metric.cpp:90: +3, including nesting penalty of 2, nesting level increased to 3

      if (!voxel_grids_match_in_scanner_space(header, H_out))
      ^

cmd/sh2metric.cpp:105: nesting level increased to 1

    Processor(const std::vector<Image<float>> &SH_images,
    ^

cmd/sh2metric.cpp:112: +2, including nesting penalty of 1, nesting level increased to 2

      for (const auto &i : SH_images)
      ^

cmd/sh2metric.cpp:116: nesting level increased to 1

    Processor(const Processor &that)
    ^

cmd/sh2metric.cpp:118: +2, including nesting penalty of 1, nesting level increased to 2

      for (const auto &i : that.images)
      ^

cmd/sh2metric.cpp:122: nesting level increased to 1

    bool operator()(Iterator &pos) {
    ^

cmd/sh2metric.cpp:124: +2, including nesting penalty of 1, nesting level increased to 2

      for (auto &image : images) {
      ^

cmd/sh2metric.cpp:128: +3, including nesting penalty of 2, nesting level increased to 3

        for (auto l = Loop(3)(image); l; ++l)
        ^

cmd/sh2metric.cpp:136: +2, including nesting penalty of 1, nesting level increased to 2

      } catch (Exception &) {
        ^

cmd/sh2metric.cpp:154: nesting level increased to 1

      Shared(const std::vector<Image<float>> &SH_images, const DWI::Directions::Set &dirs, const bool normalise)
      ^

cmd/sh2metric.cpp:157: +2, including nesting penalty of 1, nesting level increased to 2

        for (ssize_t row = 0; row != dirs.size(); ++row)
        ^

cmd/sh2metric.cpp:160: +2, including nesting penalty of 1, nesting level increased to 2

        for (const auto &i : SH_images) {
        ^

cmd/sh2metric.cpp:162: +3, including nesting penalty of 2, nesting level increased to 3

          if (transforms.find(lmax) == transforms.end())
          ^

cmd/sh2metric.cpp:166: +2, including nesting penalty of 1, nesting level increased to 2

        if (normalise)
        ^

cmd/sh2metric.cpp:169: nesting level increased to 1

      vector_type operator()(vector_type &SH_coefs) const {
      ^

cmd/sh2metric.cpp:175: nesting level increased to 1

      size_t get_num_dirs() const { return num_dirs; }
      ^

cmd/sh2metric.cpp:176: nesting level increased to 1

      default_type normalise(const default_type in) const { return normalisation(in); }
      ^

cmd/sh2metric.cpp:184: nesting level increased to 1

        Normalisation()
        ^

cmd/sh2metric.cpp:187: nesting level increased to 1

        void initialise(const size_t num_images, const DWI::Directions::Set &dirs, const transform_type transform) {
        ^

cmd/sh2metric.cpp:200: nesting level increased to 1

        default_type operator()(const default_type in) const {
        ^

cmd/sh2metric.cpp:201: +2, including nesting penalty of 1, nesting level increased to 2

          if (!std::isfinite(lower) || !std::isfinite(upper))
          ^

cmd/sh2metric.cpp:201: +1

          if (!std::isfinite(lower) || !std::isfinite(upper))
                                    ^

cmd/sh2metric.cpp:182: nesting level increased to 1

      class Normalisation {
            ^

cmd/sh2metric.cpp:182: nesting level increased to 1

      class Normalisation {
            ^

cmd/sh2metric.cpp:182: nesting level increased to 1

      class Normalisation {
            ^

cmd/sh2metric.cpp:151: nesting level increased to 1

    class Shared {
          ^

cmd/sh2metric.cpp:151: nesting level increased to 1

    class Shared {
          ^

cmd/sh2metric.cpp:151: nesting level increased to 1

    class Shared {
          ^

cmd/sh2metric.cpp:151: nesting level increased to 1

    class Shared {
          ^

cmd/sh2metric.cpp:151: nesting level increased to 1

    class Shared {
          ^

cmd/sh2metric.cpp:102: nesting level increased to 1

  class Processor {
        ^

cmd/sh2metric.cpp:102: nesting level increased to 1

  class Processor {
        ^

std::vector<Image<float>> SH_images;
size_t max_lmax = 0;
Header H_out;
for (size_t i = 0; i != argument.size() - 2; ++i) {
Header header(Header::open(argument[i]));
Math::SH::check(header);
max_lmax = std::max(max_lmax, Math::SH::LforN(header.size(3)));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'ssize_t' (aka 'long') to signed type 'int' is implementation-defined [bugprone-narrowing-conversions]

    max_lmax = std::max(max_lmax, Math::SH::LforN(header.size(3)));
                                                  ^

if (i == 0) {
H_out = header;
} else {
if (!voxel_grids_match_in_scanner_space(header, H_out))
throw Exception("All input SH images must have matching voxel grids");
H_out.merge_keyval(header);
}
SH_images.emplace_back(header.get_image<float>());
}
H_out.ndim() = 3;

DWI::Directions::Set dirs(default_direction_set);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'dirs' of type 'DWI::Directions::Set' can be declared 'const' [misc-const-correctness]

Suggested change
DWI::Directions::Set dirs(default_direction_set);
DWI::Directions::Set const dirs(default_direction_set);

Image<float> image_out(Image<float>::create(argument[argument.size() - 1], H_out));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'image_out' of type 'Image' can be declared 'const' [misc-const-correctness]

Suggested change
Image<float> image_out(Image<float>::create(argument[argument.size() - 1], H_out));
Image<float> const image_out(Image<float>::create(argument[argument.size() - 1], H_out));

const bool normalise = !get_options("normalise").empty();

class Processor {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'processor' of type 'class Processor' can be declared 'const' [misc-const-correctness]

  class Processor {
  ^

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: class 'Processor' defines a copy constructor but does not define a destructor, a copy assignment operator, a move constructor or a move assignment operator [cppcoreguidelines-special-member-functions]

  class Processor {
        ^

public:
using vector_type = Eigen::Matrix<default_type, Eigen::Dynamic, 1>;
Processor(const std::vector<Image<float>> &SH_images,
const DWI::Directions::Set &dirs,
const Image<float> &output_image,
const bool normalise)
: out(output_image),
concat_amps(SH_images.size() * dirs.size()),
shared(new Shared(SH_images, dirs, normalise)) {
for (const auto &i : SH_images)
images.emplace_back(Image<float>(i));
}

Processor(const Processor &that)
: out(that.out), SH_coefs(that.SH_coefs.size()), concat_amps(that.concat_amps.size()), shared(that.shared) {
for (const auto &i : that.images)
images.emplace_back(Image<float>(i));
}

bool operator()(Iterator &pos) {
ssize_t offset = 0;
for (auto &image : images) {
assign_pos_of(pos).to(image);
image.index(3) = 0;
SH_coefs.resize(image.size(3));
for (auto l = Loop(3)(image); l; ++l)
SH_coefs[image.index(3)] = image.value();
concat_amps.segment(offset, shared->get_num_dirs()) = (*shared)(SH_coefs);
offset += shared->get_num_dirs();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'size_t' (aka 'unsigned long') to signed type 'ssize_t' (aka 'long') is implementation-defined [bugprone-narrowing-conversions]

        offset += shared->get_num_dirs();
                  ^

}
assign_pos_of(pos).to(out);
try {
out.value() = shared->normalise(Math::Entropy::nits(concat_amps));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'default_type' (aka 'double') to 'value_type' (aka 'float') [bugprone-narrowing-conversions]

        out.value() = shared->normalise(Math::Entropy::nits(concat_amps));
                      ^

} catch (Exception &) {
out.value() = std::numeric_limits<float>::quiet_NaN();
}
return true;
}

protected:
std::vector<Image<float>> images;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member variable 'images' has protected visibility [cppcoreguidelines-non-private-member-variables-in-classes]

    std::vector<Image<float>> images;
                              ^

Image<float> out;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member variable 'out' has protected visibility [cppcoreguidelines-non-private-member-variables-in-classes]

    Image<float> out;
                 ^

vector_type SH_coefs;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member variable 'SH_coefs' has protected visibility [cppcoreguidelines-non-private-member-variables-in-classes]

    vector_type SH_coefs;
                ^

vector_type concat_amps;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member variable 'concat_amps' has protected visibility [cppcoreguidelines-non-private-member-variables-in-classes]

    vector_type concat_amps;
                ^


// Construct the requisite SH2A transforms given the set of input images
// Note that multiple ODFs may have the same lmax,
// and can therefore make use of the same transform
class Shared {
public:
using transform_type = Eigen::Matrix<default_type, Eigen::Dynamic, Eigen::Dynamic>;
Shared(const std::vector<Image<float>> &SH_images, const DWI::Directions::Set &dirs, const bool normalise)
: num_dirs(dirs.size()) {
Eigen::Matrix<default_type, Eigen::Dynamic, 3> dirs_as_matrix(dirs.size(), 3);
for (ssize_t row = 0; row != dirs.size(); ++row)
dirs_as_matrix.row(row) = dirs[row];
size_t max_lmax = 0;
for (const auto &i : SH_images) {
const size_t lmax = Math::SH::LforN(i.size(3));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'ssize_t' (aka 'long') to signed type 'int' is implementation-defined [bugprone-narrowing-conversions]

          const size_t lmax = Math::SH::LforN(i.size(3));
                                              ^

if (transforms.find(lmax) == transforms.end())
transforms.insert(std::make_pair(lmax, Math::SH::init_transform_cart(dirs_as_matrix, lmax)));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'size_t' (aka 'unsigned long') to signed type 'int' is implementation-defined [bugprone-narrowing-conversions]

            transforms.insert(std::make_pair(lmax, Math::SH::init_transform_cart(dirs_as_matrix, lmax)));
                                                                                                 ^

max_lmax = std::max(max_lmax, lmax);
}
if (normalise)
normalisation.initialise(SH_images.size(), dirs, transforms[max_lmax]);
}
vector_type operator()(vector_type &SH_coefs) const {
const size_t lmax = Math::SH::LforN(SH_coefs.size());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'Index' (aka 'long') to signed type 'int' is implementation-defined [bugprone-narrowing-conversions]

        const size_t lmax = Math::SH::LforN(SH_coefs.size());
                                            ^

auto transform_it = transforms.find(lmax);
assert(transform_it != transforms.end());
return (transform_it->second * SH_coefs).eval();
}
size_t get_num_dirs() const { return num_dirs; }
default_type normalise(const default_type in) const { return normalisation(in); }

protected:
const size_t num_dirs;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member 'num_dirs' of type 'const size_t' (aka 'const unsigned long') is const qualified [cppcoreguidelines-avoid-const-or-ref-data-members]

      const size_t num_dirs;
                   ^

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member variable 'num_dirs' has protected visibility [cppcoreguidelines-non-private-member-variables-in-classes]

      const size_t num_dirs;
                   ^

std::map<size_t, transform_type> transforms;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: member variable 'transforms' has protected visibility [cppcoreguidelines-non-private-member-variables-in-classes]

      std::map<size_t, transform_type> transforms;
                                       ^


class Normalisation {
public:
Normalisation()
: lower(std::numeric_limits<default_type>::quiet_NaN()),
upper(std::numeric_limits<default_type>::quiet_NaN()) {}
void initialise(const size_t num_images, const DWI::Directions::Set &dirs, const transform_type transform) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: parameter 'dirs' is unused [misc-unused-parameters]

cmd/sh2metric.cpp:167:

-           normalisation.initialise(SH_images.size(), dirs, transforms[max_lmax]);
+           normalisation.initialise(SH_images.size(), transforms[max_lmax]);
Suggested change
void initialise(const size_t num_images, const DWI::Directions::Set &dirs, const transform_type transform) {
void initialise(const size_t num_images, const transform_type transform) {

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the const qualified parameter 'transform' is copied for each invocation; consider making it a reference [performance-unnecessary-value-param]

Suggested change
void initialise(const size_t num_images, const DWI::Directions::Set &dirs, const transform_type transform) {
void initialise(const size_t num_images, const DWI::Directions::Set &dirs, const transform_type& transform) {

Eigen::Matrix<default_type, Eigen::Dynamic, 1> delta_coefs;
Math::SH::delta(
delta_coefs, Eigen::Matrix<default_type, 3, 1>({0.0, 0.0, 1.0}), Math::SH::LforN(transform.cols()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'size_t' (aka 'unsigned long') to signed type 'int' is implementation-defined [bugprone-narrowing-conversions]

              delta_coefs, Eigen::Matrix<default_type, 3, 1>({0.0, 0.0, 1.0}), Math::SH::LforN(transform.cols()));
                                                                               ^

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'Index' (aka 'long') to signed type 'int' is implementation-defined [bugprone-narrowing-conversions]

              delta_coefs, Eigen::Matrix<default_type, 3, 1>({0.0, 0.0, 1.0}), Math::SH::LforN(transform.cols()));
                                                                                               ^

// Get amplitude samples of this delta function,
// and pad out zeroes to simulate all other input SH functions being zero
Eigen::Matrix<default_type, Eigen::Dynamic, 1> amps = transform * delta_coefs;
amps.conservativeResizeLike(
Eigen::Matrix<default_type, Eigen::Dynamic, 1>::Zero(num_images * transform.rows()));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'size_t' (aka 'unsigned long') to signed type 'Index' (aka 'long') is implementation-defined [bugprone-narrowing-conversions]

              Eigen::Matrix<default_type, Eigen::Dynamic, 1>::Zero(num_images * transform.rows()));
                                                                   ^

lower = Math::Entropy::nits(amps);
upper = Math::Entropy::nits(
Eigen::Matrix<default_type, Eigen::Dynamic, 1>::Constant(num_images * transform.rows(), 1.0));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'size_t' (aka 'unsigned long') to signed type 'Index' (aka 'long') is implementation-defined [bugprone-narrowing-conversions]

              Eigen::Matrix<default_type, Eigen::Dynamic, 1>::Constant(num_images * transform.rows(), 1.0));
                                                                       ^

}
default_type operator()(const default_type in) const {
if (!std::isfinite(lower) || !std::isfinite(upper))
return in;
return std::max(0.0, std::min(1.0, ((in - lower) / (upper - lower))));
}

protected:
default_type lower;
default_type upper;
} normalisation;
};
std::shared_ptr<Shared> shared;

} processor(SH_images, dirs, image_out, normalise);

ThreadedLoop("computing entropy", H_out).run(processor);
}

void run_power() {
auto SH_data = Image<float>::open(argument[0]);
Math::SH::check(SH_data);

Header power_header(SH_data);

const bool spectrum = !get_options("spectrum").empty();

const int lmax = Math::SH::LforN(SH_data.size(3));
INFO("calculating spherical harmonic power up to degree " + str(lmax));

if (spectrum)
power_header.size(3) = 1 + lmax / 2;
else
power_header.ndim() = 3;
power_header.datatype() = DataType::Float32;

auto power_data = Image<float>::create(argument[1], power_header);

auto f1 = [&](decltype(power_data) &P, decltype(SH_data) &SH) {
P.index(3) = 0;
for (int l = 0; l <= lmax; l += 2) {
float power = 0.0;
for (int m = -l; m <= l; ++m) {
SH.index(3) = Math::SH::index(l, m);
float val = SH.value();
power += Math::pow2(val);
}
P.value() = power / (Math::pi * 4);
++P.index(3);
}
};

auto f2 = [&](decltype(power_data) &P, decltype(SH_data) &SH) {
float power = 0.0;
for (int l = 0; l <= lmax; l += 2) {
for (int m = -l; m <= l; ++m) {
SH.index(3) = Math::SH::index(l, m);
float val = SH.value();
power += Math::pow2(val);
}
}
P.value() = power / (Math::pi * 4);
};

auto loop = ThreadedLoop("calculating SH power", SH_data, 0, 3);
if (spectrum)
loop.run(f1, power_data, SH_data);
else
loop.run(f2, power_data, SH_data);
}

void run() {
switch (int(argument[argument.size() - 2])) {
case 0: // "entropy"
run_entropy();
return;
case 1: // "power"
run_power();
return;
}
}
100 changes: 0 additions & 100 deletions cmd/sh2power.cpp

This file was deleted.

Loading
Loading