diff --git a/ci-utils/envs/conda_cpp.txt b/ci-utils/envs/conda_cpp.txt index 8a963274..2cb5b864 100644 --- a/ci-utils/envs/conda_cpp.txt +++ b/ci-utils/envs/conda_cpp.txt @@ -9,5 +9,5 @@ xsimd >=13,<14 cmake dcmtk >=3.6.9 fmjpeg2koj >=1.0.3 -libarrow -libparquet +libarrow <=23.0.0 +libparquet <=23.0.0 diff --git a/ci-utils/install_prereq_linux.sh b/ci-utils/install_prereq_linux.sh index 74a41817..82e26f18 100755 --- a/ci-utils/install_prereq_linux.sh +++ b/ci-utils/install_prereq_linux.sh @@ -9,7 +9,7 @@ # BUILD_Z5_DEP=1 -BULD_DCMTK_DEP=1 +BUILD_DCMTK_DEP=1 BUILD_ARROW_DEP=0 BUILD_BOOST_DEP=1 @@ -24,7 +24,7 @@ done if [[ "${min_build,,}" == "yes" ]]; then BUILD_Z5_DEP=0 - BULD_DCMTK_DEP=0 + BUILD_DCMTK_DEP=0 BUILD_ARROW_DEP=0 BUILD_BOOST_DEP=0 fi @@ -152,7 +152,7 @@ cmake -DCMAKE_INSTALL_PREFIX=../../"$LOCAL_INSTALL_DIR"/ -DCMAKE_PREFIX_PATH=. make install -j4 cd ../../ -if [[ $BULD_DCMTK_DEP -eq 1 ]]; then +if [[ $BUILD_DCMTK_DEP -eq 1 ]]; then curl -L https://github.com/glennrp/libpng/archive/refs/tags/v1.6.53.zip -o v1.6.53.zip unzip v1.6.53.zip cd libpng-1.6.53 @@ -202,7 +202,7 @@ fi make install -j4 cd ../../ -if [[ $BULD_DCMTK_DEP -eq 1 ]]; then +if [[ $BUILD_DCMTK_DEP -eq 1 ]]; then curl -L https://github.com/DCMTK/dcmtk/archive/refs/tags/DCMTK-3.6.9.zip -o DCMTK-3.6.9.zip unzip DCMTK-3.6.9.zip cd dcmtk-DCMTK-3.6.9/CMake diff --git a/src/nyx/cli_option_constants.h b/src/nyx/cli_option_constants.h index a782d715..7618f9e0 100644 --- a/src/nyx/cli_option_constants.h +++ b/src/nyx/cli_option_constants.h @@ -16,6 +16,7 @@ #define clo_XYRESOLUTION "--pixelsPerCentimeter" // pixels per centimeter #define clo_PXLDIST "--pixelDistance" // used in neighbor features #define clo_COARSEGRAYDEPTH "--coarseGrayDepth" // Environment :: raw_coarse_grayscale_depth +#define clo_BINNINGORIGIN "--binningOrigin" // Environment :: "zero" (default) or "min" (PyRadiomics-style) #define clo_RAMLIMIT "--ramLimit" // Optional. Limit for treating ROIs as non-trivial and for setting the batch size of trivial ROIs. Default - amount of available system RAM #define clo_TEMPDIR "--tempDir" // Optional. Used in processing non-trivial features. Default - system temp directory #define clo_IBSICOMPLIANCE "--ibsi" // skip binning for grey level and grey tone features diff --git a/src/nyx/env_features.cpp b/src/nyx/env_features.cpp index c2d0f14a..8c5959eb 100644 --- a/src/nyx/env_features.cpp +++ b/src/nyx/env_features.cpp @@ -699,6 +699,7 @@ void Environment::compile_feature_settings() s[(int)NyxSetting::USEGPU].bval = using_gpu(); s[(int)NyxSetting::VERBOSLVL].ival = get_verbosity_level(); s[(int)NyxSetting::IBSI].bval = ibsi_compliance; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(get_binning_origin()); // propagate binning origin to per-feature settings } } diff --git a/src/nyx/environment.cpp b/src/nyx/environment.cpp index 1e0712d2..41846ad0 100644 --- a/src/nyx/environment.cpp +++ b/src/nyx/environment.cpp @@ -191,6 +191,8 @@ void Environment::show_cmdline_help() << "\t\t\tDefault: 5 \n" << "\t\t" << OPT << clo_COARSEGRAYDEPTH << "= \n" << "\t\t\tDefault: 64 \n" + << "\t\t" << OPT << clo_BINNINGORIGIN << "= Origin of intensity binning range \n" + << "\t\t\t'zero' bins from [0, max] (default), 'min' bins from [min, max] (PyRadiomics-compatible) \n" << "\t\t" << OPT << clo_GLCMANGLES << "= \n" << "\t\t\tDefault: 0,45,90,135 \n" << "\t\t" << OPT << clo_VERBOSITY << "= \n" @@ -441,6 +443,7 @@ bool Environment::parse_cmdline(int argc, char** argv) find_string_argument(i, clo_GLCMOFFSET, glcmOptions.rawOffs) || find_string_argument(i, clo_PXLDIST, pixel_distance) || find_string_argument(i, clo_COARSEGRAYDEPTH, raw_coarse_grayscale_depth) || + find_string_argument(i, clo_BINNINGORIGIN, raw_binning_origin) || find_string_argument(i, clo_VERBOSITY, rawVerbosity) || find_string_argument(i, clo_IBSICOMPLIANCE, raw_ibsi_compliance) || find_string_argument(i, clo_RAMLIMIT, rawRamLimit) || @@ -663,9 +666,27 @@ bool Environment::parse_cmdline(int argc, char** argv) if (!raw_coarse_grayscale_depth.empty()) { // string -> integer - if (sscanf(raw_coarse_grayscale_depth.c_str(), "%d", &coarse_grayscale_depth) != 1) + // Reject zero and negative values — grey depth must be at least 1 + // for valid binning. Previously only parse failure was checked, + // which allowed depth=0 to slip through and be misinterpreted + // as IBSI mode. + if (sscanf(raw_coarse_grayscale_depth.c_str(), "%d", &coarse_grayscale_depth) != 1 || coarse_grayscale_depth < 1) { - std::cerr << "Error: " << clo_COARSEGRAYDEPTH << "=" << raw_coarse_grayscale_depth << ": expecting an integer constant\n"; + std::cerr << "Error: " << clo_COARSEGRAYDEPTH << "=" << raw_coarse_grayscale_depth << ": expecting a positive integer constant\n"; + return false; + } + } + + // parse BINNINGORIGIN + if (!raw_binning_origin.empty()) + { + if (raw_binning_origin == "min") + binning_origin_ = BinningOrigin::min_based; + else if (raw_binning_origin == "zero") + binning_origin_ = BinningOrigin::zero; + else + { + std::cerr << "Error: " << clo_BINNINGORIGIN << "=" << raw_binning_origin << ": expecting 'zero' or 'min'\n"; return false; } } @@ -915,11 +936,21 @@ int Environment::get_coarse_gray_depth() return coarse_grayscale_depth; } -void Environment::set_coarse_gray_depth(unsigned int new_depth) +void Environment::set_coarse_gray_depth(int new_depth) { coarse_grayscale_depth = new_depth; } +BinningOrigin Environment::get_binning_origin() const +{ + return binning_origin_; +} + +void Environment::set_binning_origin(BinningOrigin bo) +{ + binning_origin_ = bo; +} + bool Environment::set_ram_limit(size_t megabytes) { // Megabytes to bytes diff --git a/src/nyx/environment.h b/src/nyx/environment.h index c089a898..2cc7801e 100644 --- a/src/nyx/environment.h +++ b/src/nyx/environment.h @@ -112,7 +112,12 @@ class Environment: public BasicEnvironment int get_floating_point_precision(); int get_coarse_gray_depth(); - void set_coarse_gray_depth(unsigned int new_depth); + // Signed int to match sscanf %d target type and enable validation + // of negative values in parse_cmdline. + void set_coarse_gray_depth(int new_depth); + + BinningOrigin get_binning_origin() const; + void set_binning_origin(BinningOrigin bo); // implementation of SKIPROI bool roi_is_blacklisted (const std::string& fname, int roi_label); @@ -242,6 +247,8 @@ class Environment: public BasicEnvironment int coarse_grayscale_depth; //= 64; std::string raw_coarse_grayscale_depth; //= ""; + std::string raw_binning_origin; //= "" (default "zero"; alternative "min") + BinningOrigin binning_origin_ = BinningOrigin::zero; // data members implementing RAMLIMIT std::string rawRamLimit; //= ""; diff --git a/src/nyx/feature_settings.h b/src/nyx/feature_settings.h index f8184567..bf427c98 100644 --- a/src/nyx/feature_settings.h +++ b/src/nyx/feature_settings.h @@ -2,6 +2,13 @@ #include +// Binning origin strategy for texture features +enum class BinningOrigin : int +{ + zero = 0, // bins span [0, max] (default Nyxus/MATLAB behavior) + min_based = 1 // bins span [min, max] (PyRadiomics-compatible behavior) +}; + // feature settings union FeatureSetting { @@ -40,6 +47,8 @@ enum class NyxSetting : int GLRLM_GREYDEPTH, // GLSZM GLSZM_GREYDEPTH, + // Binning origin + BINNING_ORIGIN, // __COUNT__ }; @@ -65,5 +74,6 @@ enum class NyxSetting : int #define STNGS_GLSZM_GREYDEPTH(obj) (obj[(int)NyxSetting::GLSZM_GREYDEPTH].ival) #define STNGS_NGTDM_GREYDEPTH(obj) (obj[(int)NyxSetting::NGTDM_GREYDEPTH].ival) #define STNGS_NGTDM_RADIUS(obj) (obj[(int)NyxSetting::NGTDM_RADIUS].ival) +#define STNGS_BINNING_ORIGIN(obj) (static_cast(obj[(int)NyxSetting::BINNING_ORIGIN].ival)) diff --git a/src/nyx/features/3d_glcm.cpp b/src/nyx/features/3d_glcm.cpp index 505c4dce..2ca5961b 100644 --- a/src/nyx/features/3d_glcm.cpp +++ b/src/nyx/features/3d_glcm.cpp @@ -48,11 +48,18 @@ void D3_GLCM_feature::calculate (LR& r, const Fsettings& s) SimpleCube D; D.allocate (w,h,d); - auto greyBinningInfo = STNGS_GLCM_GREYDEPTH(s); // former Nyxus::theEnvironment.get_coarse_gray_depth() - if (STNGS_IBSI(s)) // former Nyxus::theEnvironment.ibsi_compliance + // Use GLCM-specific grey depth if set via metaparams, otherwise fall back to global. + // GLCM_GREYDEPTH defaults to 0 when not explicitly configured (e.g. no metaparams + // set), so fall back to the global GREYDEPTH to avoid treating it as IBSI mode. + auto greyBinningInfo = STNGS_GLCM_GREYDEPTH(s); + if (greyBinningInfo == 0) + greyBinningInfo = s[(int)NyxSetting::GREYDEPTH].ival; + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyBinningInfo = 0; - bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyBinningInfo); + bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyBinningInfo, binOrigin); // calculate features for all the 13 directions for (const ShiftToNeighbor & sh : shifts) @@ -64,8 +71,9 @@ void D3_GLCM_feature::calculate (LR& r, const Fsettings& s) D, r.aux_min, r.aux_max, - STNGS_GLCM_GREYDEPTH(s), - STNGS_IBSI(s), + greyBinningInfo, + ibsi, + binOrigin, STNGS_NAN(s)); } } @@ -176,9 +184,9 @@ void D3_GLCM_feature::save_value(std::vector>& fvals) //xxxx deprecated in PyR xxxx fvals[(int)Feature3D::GLCM_VARIANCE_AVE][0] = calc_ave(fvals_variance); } -void D3_GLCM_feature::extract_texture_features_at_angle(int dx, int dy, int dz, const SimpleCube& binned_greys, PixIntens min_val, PixIntens max_val, int n_greys, bool ibsi, double soft_nan) +void D3_GLCM_feature::extract_texture_features_at_angle(int dx, int dy, int dz, const SimpleCube& binned_greys, PixIntens min_val, PixIntens max_val, int n_greys, bool ibsi, BinningOrigin binOrigin, double soft_nan) { - calculateCoocMatAtAngle (P_matrix, dx, dy, dz, binned_greys, min_val, max_val, n_greys, ibsi); + calculateCoocMatAtAngle (P_matrix, dx, dy, dz, binned_greys, min_val, max_val, n_greys, ibsi, binOrigin); // Blank cooc-matrix? -- no point to use it, assign each feature value '0' and return. if (sum_p == 0) @@ -270,7 +278,8 @@ void D3_GLCM_feature::calculateCoocMatAtAngle( PixIntens grays_min_val, PixIntens grays_max_val, int n_greys, - bool ibsi) + bool ibsi, + BinningOrigin binOrigin) { int w = D.width(), h = D.height(), @@ -281,8 +290,11 @@ void D3_GLCM_feature::calculateCoocMatAtAngle( if (ibsi) greyInfo = 0; - // allocate the cooc and intensities matrices - if (radiomics_grey_binning(greyInfo)) + // Allocate the cooc and intensities matrices. + // '&& !ibsi' guard: IBSI mode uses n_levels=0 and its own branch below, + // regardless of binOrigin. Without the guard, IBSI + min_based would + // incorrectly enter the radiomics path. + if (binOrigin == BinningOrigin::min_based && !ibsi) { // unique intensities std::unordered_set U(D.begin(), D.end()); @@ -294,7 +306,7 @@ void D3_GLCM_feature::calculateCoocMatAtAngle( GLCM.allocate((int)I.size(), (int)I.size()); } else - if (matlab_grey_binning(greyInfo)) + if (binOrigin == BinningOrigin::zero && !ibsi) // zero-based (MATLAB) binning; !ibsi excludes IBSI which uses its own branch { auto n_matlab_levels = greyInfo; I.resize(n_matlab_levels); @@ -334,7 +346,7 @@ void D3_GLCM_feature::calculateCoocMatAtAngle( lvl_a = D.zyx(zslice + dz, row + dy, col + dx); // Skip 0-intensity pixels (usually out of mask pixels) - if (ibsi_grey_binning(greyInfo)) + if (ibsi) if (lvl_a == 0 || lvl_b == 0) continue; @@ -342,8 +354,9 @@ void D3_GLCM_feature::calculateCoocMatAtAngle( int a = lvl_a, b = lvl_b; - // raw intensities need to be modified for different grey binning paradigms (Matlab, PyRadiomics, IBSI) - if (radiomics_grey_binning(greyInfo)) + // Raw intensities need to be modified for different grey binning paradigms. + // '&& !ibsi' ensures IBSI mode falls through to the else (matlab/IBSI) branch. + if ((binOrigin == BinningOrigin::min_based && !ibsi)) { // skip zeroes if (a == 0 || b == 0) @@ -364,8 +377,9 @@ void D3_GLCM_feature::calculateCoocMatAtAngle( (GLCM.xy(a, b))++; - // Radiomics GLCM is symmetric, Matlab one is not - if (D3_GLCM_feature::symmetric_glcm || radiomics_grey_binning(greyInfo) || ibsi_grey_binning(greyInfo)) + // Radiomics and IBSI GLCMs are symmetric; Matlab is not. + // Equivalent to: symmetric_glcm || radiomics || ibsi + if (D3_GLCM_feature::symmetric_glcm || (binOrigin == BinningOrigin::min_based && !ibsi) || ibsi) (GLCM.xy(b, a))++; } } diff --git a/src/nyx/features/3d_glcm.h b/src/nyx/features/3d_glcm.h index 82c7290e..228bf84b 100644 --- a/src/nyx/features/3d_glcm.h +++ b/src/nyx/features/3d_glcm.h @@ -144,7 +144,7 @@ class D3_GLCM_feature : public FeatureMethod, public TextureFeature PixIntens max_val, bool normalize); - void extract_texture_features_at_angle (int dx, int dy, int dz, const SimpleCube & grays, PixIntens min_val, PixIntens max_val, int n_greys, bool ibsi, double soft_nan); + void extract_texture_features_at_angle (int dx, int dy, int dz, const SimpleCube & grays, PixIntens min_val, PixIntens max_val, int n_greys, bool ibsi, BinningOrigin binOrigin, double soft_nan); void calculateCoocMatAtAngle( // out @@ -157,7 +157,8 @@ class D3_GLCM_feature : public FeatureMethod, public TextureFeature PixIntens min_val, PixIntens max_val, int n_greys, - bool ibsi); + bool ibsi, + BinningOrigin binOrigin); void calculatePxpmy(); void calculate_by_row_mean(); diff --git a/src/nyx/features/3d_gldm.cpp b/src/nyx/features/3d_gldm.cpp index 663b2d4f..a762dcbb 100644 --- a/src/nyx/features/3d_gldm.cpp +++ b/src/nyx/features/3d_gldm.cpp @@ -91,13 +91,15 @@ void D3_GLDM_feature::calculate (LR& r, const Fsettings& s) D.allocate(w, h, d); auto greyInfo = STNGS_GLDM_GREYDEPTH(s); // former Nyxus::theEnvironment.get_coarse_gray_depth() - if (STNGS_IBSI(s)) // former Nyxus::theEnvironment.ibsi_compliance + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; - bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo); + bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo, binOrigin); // allocate intensities matrix - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { auto n_ibsi_levels = *std::max_element(D.begin(), D.end()); @@ -113,8 +115,13 @@ void D3_GLDM_feature::calculate (LR& r, const Fsettings& s) std::sort(I.begin(), I.end()); } - // zero (backround) intensity at given grey binning method - PixIntens zeroI = matlab_grey_binning(greyInfo) ? 1 : 0; + // Determine the background intensity value. In zero-based (MATLAB) binning, + // intensity 0 gets mapped to bin 1, so background becomes 1. In all other + // modes (min-based radiomics, IBSI) background stays at 0. The three-part + // condition: greyInfo>0 confirms binning is active, BinningOrigin::zero + // selects MATLAB mode, and !ibsi excludes IBSI which handles background + // via its own 0-intensity skip logic. + PixIntens zeroI = (greyInfo > 0 && binOrigin == BinningOrigin::zero && !ibsi) ? 1 : 0; // Gather zones for (int zslice = 0; zslice < d; zslice++) diff --git a/src/nyx/features/3d_gldzm.cpp b/src/nyx/features/3d_gldzm.cpp index 6fda0553..239f8eaf 100644 --- a/src/nyx/features/3d_gldzm.cpp +++ b/src/nyx/features/3d_gldzm.cpp @@ -69,15 +69,17 @@ void D3_GLDZM_feature::prepare_GLDZM_matrix_kit (SimpleMatrix& GLD auto greyInfo_localFeature = D3_GLDZM_feature::n_levels; if (greyInfo_localFeature != 0 && greyInfo != greyInfo_localFeature) greyInfo = greyInfo_localFeature; - if (STNGS_IBSI(s)) // former Nyxus::theEnvironment.ibsi_compliance + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; auto& imR = r.aux_image_cube; - bin_intensities_3d (D, imR, r.aux_min, r.aux_max, greyInfo); + bin_intensities_3d (D, imR, r.aux_min, r.aux_max, greyInfo, binOrigin); // allocate intensities matrix std::vector I; - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { auto n_ibsi_levels = *std::max_element(D.begin(), D.end()); I.resize(n_ibsi_levels); @@ -106,7 +108,7 @@ void D3_GLDZM_feature::prepare_GLDZM_matrix_kit (SimpleMatrix& GLD continue; // Skip 0-intensity pixels (usually out of mask pixels) - if (ibsi_grey_binning(greyInfo)) + if (ibsi) if (inten == 0) continue; diff --git a/src/nyx/features/3d_glrlm.cpp b/src/nyx/features/3d_glrlm.cpp index b52e2eb7..f3e31e0c 100644 --- a/src/nyx/features/3d_glrlm.cpp +++ b/src/nyx/features/3d_glrlm.cpp @@ -145,15 +145,17 @@ void D3_GLRLM_feature::calculate (LR& r, const Fsettings& s) G.allocate (w,h,d); auto greyInfo = STNGS_GLRLM_GREYDEPTH(s); - if (STNGS_IBSI(s)) + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; - bin_intensities_3d (G, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo); + bin_intensities_3d (G, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo, binOrigin); // sorted intensities std::vector I; - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { auto n_ibsi_levels = *std::max_element (G.begin(), G.end()); I.resize (n_ibsi_levels); diff --git a/src/nyx/features/3d_glszm.cpp b/src/nyx/features/3d_glszm.cpp index 3b740e2b..f4d40d23 100644 --- a/src/nyx/features/3d_glszm.cpp +++ b/src/nyx/features/3d_glszm.cpp @@ -488,15 +488,17 @@ void D3_GLSZM_feature::calculate (LR& r, const Fsettings& s) D.allocate(w, h, d); auto greyInfo = STNGS_GLSZM_GREYDEPTH(s); // former Nyxus::theEnvironment.get_coarse_gray_depth() - if (STNGS_IBSI(s)) // former Nyxus::theEnvironment.ibsi_compliance + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; - bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo); + bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo, binOrigin); // gather unique intensities std::unordered_set U; - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { // ibsi approach to intensities auto n_ibsi_levels = *std::max_element(D.begin(), D.end()); @@ -513,8 +515,13 @@ void D3_GLSZM_feature::calculate (LR& r, const Fsettings& s) std::sort(I.begin(), I.end()); } - // zero (backround) intensity at given grey binning approach - PixIntens zeroI = matlab_grey_binning (greyInfo) ? 1 : 0; + // Determine the background intensity value. In zero-based (MATLAB) binning, + // intensity 0 gets mapped to bin 1, so background becomes 1. In all other + // modes (min-based radiomics, IBSI) background stays at 0. The three-part + // condition: greyInfo>0 confirms binning is active, BinningOrigin::zero + // selects MATLAB mode, and !ibsi excludes IBSI which handles background + // via its own 0-intensity skip logic. + PixIntens zeroI = (greyInfo > 0 && binOrigin == BinningOrigin::zero && !ibsi) ? 1 : 0; // gather intensity zones std::vector > Zones; diff --git a/src/nyx/features/3d_intensity.cpp b/src/nyx/features/3d_intensity.cpp index 91292a1c..e4ffc794 100644 --- a/src/nyx/features/3d_intensity.cpp +++ b/src/nyx/features/3d_intensity.cpp @@ -17,31 +17,6 @@ D3_VoxelIntensityFeatures::D3_VoxelIntensityFeatures() : FeatureMethod("PixelInt provide_features ({D3_VoxelIntensityFeatures::featureset}); } -bool matlab_grey_binning (int greybinning_info) { return greybinning_info > 0; } -bool radiomics_grey_binning (int greybinning_info) { return greybinning_info < 0; } -// returns 1-based bin indices -PixIntens to_grayscale_radiomix(PixIntens x, PixIntens min__, PixIntens max__, int binCount) -{ - if (x) - { - double binW = double(max__ - min__) / double(binCount); - PixIntens y = (PixIntens)(double(x - min__) / binW + 1); - if (y > binCount) - y = binCount; // the last bin is +1 unit wider - return y; - } - else - return 0; -} - -void bin_intensities_3d (std::vector &S, const std::vector &I, PixIntens min_I_inten, PixIntens max_I_inten, int greybin_info) -{ - // radiomics binning - auto n = I.size(); - for (size_t i = 0; i < n; i++) - S[i].inten = to_grayscale_radiomix (I[i].inten, min_I_inten, max_I_inten, std::abs(greybin_info)); -} - void D3_VoxelIntensityFeatures::calculate (LR &r, const Fsettings& s, const Dataset &ds) { // bin intensities diff --git a/src/nyx/features/3d_ngtdm.cpp b/src/nyx/features/3d_ngtdm.cpp index cf6dce21..a89ce35f 100644 --- a/src/nyx/features/3d_ngtdm.cpp +++ b/src/nyx/features/3d_ngtdm.cpp @@ -193,10 +193,12 @@ void D3_NGTDM_feature::calculate (LR& r, const Fsettings& s) D.allocate(w, h, d); auto greyInfo = STNGS_NGTDM_GREYDEPTH(s); - if (STNGS_IBSI(s)) + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; - bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo); + bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyInfo, binOrigin); // unique intensities (set) std::unordered_set U (D.begin(), D.end()); @@ -226,8 +228,13 @@ void D3_NGTDM_feature::calculate (LR& r, const Fsettings& s) std::for_each (D.begin(), D.end(), [](PixIntens& x) {x += 1;}); } - // zero (backround) intensity at given grey binning method - PixIntens zeroI = matlab_grey_binning(greyInfo) ? 1 : 0; + // Determine the background intensity value. In zero-based (MATLAB) binning, + // intensity 0 gets mapped to bin 1, so background becomes 1. In all other + // modes (min-based radiomics, IBSI) background stays at 0. The three-part + // condition: greyInfo>0 confirms binning is active, BinningOrigin::zero + // selects MATLAB mode, and !ibsi excludes IBSI which handles background + // via its own 0-intensity skip logic. + PixIntens zeroI = (greyInfo > 0 && binOrigin == BinningOrigin::zero && !ibsi) ? 1 : 0; // is binned data informative? if (I.size() < 2) diff --git a/src/nyx/features/glcm.cpp b/src/nyx/features/glcm.cpp index 11748ff4..2efbfaaa 100644 --- a/src/nyx/features/glcm.cpp +++ b/src/nyx/features/glcm.cpp @@ -18,14 +18,18 @@ void GLCMFeature::calculate (LR& r, const Fsettings& s) // Clear the feature values buffers clear_result_buffers(); - // Skip feature calculation in case of bad data - // (We need to smart-select the greyInfo rather than just theEnvironment.get_coarse_gray_depth()) - int nGreys = STNGS_GLCM_GREYDEPTH(s), - offset = STNGS_GLCM_OFFSET(s); + // Use GLCM-specific grey depth if set via metaparams, otherwise fall back to global. + // GLCM_GREYDEPTH defaults to 0 when not explicitly configured (e.g. no metaparams + // set), so fall back to the global GREYDEPTH to avoid treating it as IBSI mode. + int nGreys = STNGS_GLCM_GREYDEPTH(s); + if (nGreys == 0) + nGreys = s[(int)NyxSetting::GREYDEPTH].ival; + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + int offset = STNGS_GLCM_OFFSET(s); double softNAN = s[(int)NyxSetting::SOFTNAN].rval; - auto binnedMin = bin_pixel(r.aux_min, r.aux_min, r.aux_max, nGreys); - auto binnedMax = bin_pixel(r.aux_max, r.aux_min, r.aux_max, nGreys); + auto binnedMin = bin_pixel(r.aux_min, r.aux_min, r.aux_max, nGreys, binOrigin); + auto binnedMax = bin_pixel(r.aux_max, r.aux_min, r.aux_max, nGreys, binOrigin); if (binnedMin == binnedMax) { auto w = softNAN; // safe NAN @@ -353,6 +357,7 @@ void GLCMFeature::calculateCoocMatAtAngle( { int nGreys = s[(int)NyxSetting::GREYDEPTH].ival; bool ibsi = s[(int)NyxSetting::IBSI].bval; + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) //--- grey bining --- int rows = grays.height, @@ -382,10 +387,13 @@ void GLCMFeature::calculateCoocMatAtAngle( greyInfo = greyInfo_localFeature; if (ibsi) greyInfo = 0; - bin_intensities (D, imR, grays_min_val, grays_max_val, greyInfo); + bin_intensities (D, imR, grays_min_val, grays_max_val, greyInfo, binOrigin); - // allocate the cooc and intensities matrices - if (radiomics_grey_binning(greyInfo)) + // Allocate the cooc and intensities matrices. + // The '&& !ibsi' guard is needed because IBSI mode sets n_levels=0 (handled by + // the else/IBSI branch below) regardless of binOrigin. Without this guard, an + // IBSI run with binOrigin==min_based would incorrectly enter the radiomics path. + if ((binOrigin == BinningOrigin::min_based && !ibsi)) { // unique intensities std::unordered_set U(D.begin(), D.end()); @@ -397,7 +405,7 @@ void GLCMFeature::calculateCoocMatAtAngle( GLCM.allocate((int)I.size(), (int)I.size()); } else - if (matlab_grey_binning(greyInfo)) + if ((binOrigin == BinningOrigin::zero && !ibsi)) // zero-based (MATLAB) binning; !ibsi excludes IBSI which uses its own branch { auto n_matlab_levels = greyInfo; I.resize (n_matlab_levels); @@ -438,7 +446,7 @@ void GLCMFeature::calculateCoocMatAtAngle( lvl_a = D.yx(row + dy, col + dx); // Skip 0-intensity pixels (usually out of mask pixels) - if (ibsi_grey_binning(greyInfo)) + if (ibsi) if (lvl_a == 0 || lvl_b == 0) continue; @@ -446,8 +454,9 @@ void GLCMFeature::calculateCoocMatAtAngle( int a = lvl_a, b = lvl_b; - // raw intensities need to be modified for different grey binning paradigms (Matlab, PyRadiomics, IBSI) - if (radiomics_grey_binning(greyInfo)) + // Raw intensities need to be modified for different grey binning paradigms. + // '&& !ibsi' ensures IBSI mode falls through to the else (matlab/IBSI) branch. + if ((binOrigin == BinningOrigin::min_based && !ibsi)) { // skip zeroes if (a == 0 || b == 0) @@ -468,8 +477,9 @@ void GLCMFeature::calculateCoocMatAtAngle( (GLCM.xy(a,b)) ++; - // Radiomics GLCM is symmetric, Matlab one is not - if (GLCMFeature::symmetric_glcm || radiomics_grey_binning(greyInfo) || ibsi_grey_binning(greyInfo)) + // Radiomics and IBSI GLCMs are symmetric; Matlab is not. + // The condition is equivalent to the old: symmetric_glcm || radiomics || ibsi + if (GLCMFeature::symmetric_glcm || (binOrigin == BinningOrigin::min_based && !ibsi) || ibsi) (GLCM.xy(b, a))++; } } diff --git a/src/nyx/features/gldm.cpp b/src/nyx/features/gldm.cpp index 2378ef82..54f65dbb 100644 --- a/src/nyx/features/gldm.cpp +++ b/src/nyx/features/gldm.cpp @@ -48,13 +48,15 @@ void GLDMFeature::calculate (LR& r, const Fsettings& s) // bin intensities auto greyInfo = STNGS_NGREYS(s); // former theEnvironment.get_coarse_gray_depth() - if (STNGS_IBSI(s)) // Nyxus::theEnvironment.ibsi_compliance + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; auto& imR = r.aux_image_matrix.ReadablePixels(); - bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo); + bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo, binOrigin); // allocate intensities matrix - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { auto n_ibsi_levels = *std::max_element(D.begin(), D.end()); diff --git a/src/nyx/features/gldzm.cpp b/src/nyx/features/gldzm.cpp index 49410823..f99ac48a 100644 --- a/src/nyx/features/gldzm.cpp +++ b/src/nyx/features/gldzm.cpp @@ -50,7 +50,7 @@ void GLDZMFeature::calc_gldzm_matrix (SimpleMatrix & GLDZM, const } } -void GLDZMFeature::prepare_GLDZM_matrix_kit (SimpleMatrix& GLDZM, int& Ng, int& Nd, std::vector& greysLUT, LR& r, int n_greys, bool ibsi) +void GLDZMFeature::prepare_GLDZM_matrix_kit (SimpleMatrix& GLDZM, int& Ng, int& Nd, std::vector& greysLUT, LR& r, int n_greys, bool ibsi, BinningOrigin binOrigin) { //==== Compose the distance matrix @@ -69,11 +69,11 @@ void GLDZMFeature::prepare_GLDZM_matrix_kit (SimpleMatrix& GLDZM, greyInfo = 0; auto& imR = r.aux_image_matrix.ReadablePixels(); - bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo); + bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo, binOrigin); // allocate intensities matrix std::vector I; - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { auto n_ibsi_levels = *std::max_element(D.begin(), D.end()); I.resize(n_ibsi_levels); @@ -101,7 +101,7 @@ void GLDZMFeature::prepare_GLDZM_matrix_kit (SimpleMatrix& GLDZM, continue; // Skip 0-intensity pixels (usually out of mask pixels) - if (ibsi_grey_binning(greyInfo)) + if (ibsi) if (inten == 0) continue; @@ -293,7 +293,7 @@ void GLDZMFeature::calculate (LR& r, const Fsettings& s) SimpleMatrix GLDZM; int Ng, // number of grey levels Nd; // maximum number of non-zero dependencies - prepare_GLDZM_matrix_kit (GLDZM, Ng, Nd, greyLevelsLUT, r, STNGS_NGREYS(s), STNGS_IBSI(s)); + prepare_GLDZM_matrix_kit (GLDZM, Ng, Nd, greyLevelsLUT, r, STNGS_NGREYS(s), STNGS_IBSI(s), STNGS_BINNING_ORIGIN(s)); //==== Calculate vectors of totals by intensity (Mx) and by distance (Md) std::vector Mx, Md; diff --git a/src/nyx/features/gldzm.h b/src/nyx/features/gldzm.h index 67483fbd..43100b15 100644 --- a/src/nyx/features/gldzm.h +++ b/src/nyx/features/gldzm.h @@ -50,7 +50,7 @@ class GLDZMFeature : public FeatureMethod, public TextureFeature static bool required(const FeatureSet& fs) { return fs.anyEnabled (GLDZMFeature::featureset); } // Calculates the GLDZ-matrix, its dimensions, and a vector of sorted grey levels - void prepare_GLDZM_matrix_kit (SimpleMatrix& GLDZM, int& Ng, int& Nd, std::vector& greyLevelsLUT, LR& r, int n_greys, bool ibsi); + void prepare_GLDZM_matrix_kit (SimpleMatrix& GLDZM, int& Ng, int& Nd, std::vector& greyLevelsLUT, LR& r, int n_greys, bool ibsi, BinningOrigin binOrigin); static int n_levels; // default value: 0 diff --git a/src/nyx/features/glrlm.cpp b/src/nyx/features/glrlm.cpp index 4d59f803..695c2ff0 100644 --- a/src/nyx/features/glrlm.cpp +++ b/src/nyx/features/glrlm.cpp @@ -82,14 +82,16 @@ void GLRLMFeature::calculate (LR& r, const Fsettings& s) auto greyInfo_localFeature = GLRLMFeature::n_levels; if (greyInfo_localFeature != 0 && greyInfo != greyInfo_localFeature) greyInfo = greyInfo_localFeature; - if (STNGS_IBSI(s)) // former Nyxus::theEnvironment.ibsi_compliance + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; auto& imR = r.aux_image_matrix.ReadablePixels(); - bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo); + bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo, binOrigin); // allocate intensities matrix - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { auto n_ibsi_levels = *std::max_element(D.begin(), D.end()); I.resize(n_ibsi_levels); diff --git a/src/nyx/features/glszm.cpp b/src/nyx/features/glszm.cpp index d37e5a87..502de4de 100644 --- a/src/nyx/features/glszm.cpp +++ b/src/nyx/features/glszm.cpp @@ -238,13 +238,15 @@ void GLSZMFeature::calculate (LR& r, const Fsettings& s) // Squeeze the intensity range auto greyInfo = STNGS_NGREYS(s); // former theEnvironment.get_coarse_gray_depth() - if (STNGS_IBSI(s)) // former Nyxus::theEnvironment.ibsi_compliance + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; auto& imR = r.aux_image_matrix.ReadablePixels(); - bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo); + bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo, binOrigin); // allocate intensities matrix - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { auto n_ibsi_levels = *std::max_element(D.begin(), D.end()); I.resize(n_ibsi_levels); diff --git a/src/nyx/features/ngtdm.cpp b/src/nyx/features/ngtdm.cpp index 63de8971..0396f8fe 100644 --- a/src/nyx/features/ngtdm.cpp +++ b/src/nyx/features/ngtdm.cpp @@ -40,20 +40,22 @@ void NGTDMFeature::calculate (LR& r, const Fsettings& s) pixData& D = M.WriteablePixels(); auto& imR = r.aux_image_matrix.ReadablePixels(); - // bin intensities + // bin intensities auto greyInfo = STNGS_NGREYS(s); // former theEnvironment.get_coarse_gray_depth() auto greyInfo_localFeature = NGTDMFeature::n_levels; if (greyInfo_localFeature != 0 && greyInfo != greyInfo_localFeature) greyInfo = greyInfo_localFeature; - if (STNGS_IBSI(s)) // fomer Nyxus::theEnvironment.ibsi_compliance + auto binOrigin = STNGS_BINNING_ORIGIN(s); // zero-based (Nyxus/MATLAB) or min-based (PyRadiomics) + bool ibsi = STNGS_IBSI(s); + if (ibsi) greyInfo = 0; - bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo); + bin_intensities (D, imR, r.aux_min, r.aux_max, greyInfo, binOrigin); // unique intensities std::unordered_set U (D.begin(), D.end()); U.erase(0); // discard intensity '0' - if (ibsi_grey_binning(greyInfo)) + if (ibsi) { // intensities 0-max auto max_I = *std::max_element(U.begin(), U.end()); diff --git a/src/nyx/features/texture_feature.h b/src/nyx/features/texture_feature.h index e2a435e1..a580985d 100644 --- a/src/nyx/features/texture_feature.h +++ b/src/nyx/features/texture_feature.h @@ -2,6 +2,7 @@ #include "image_matrix.h" #include "image_cube.h" +#include "../feature_settings.h" struct AngleShift { @@ -20,88 +21,76 @@ class TextureFeature return target_I; } - void bin_intensities (pixData& S, const pixData& I, PixIntens min_I_inten, PixIntens max_I_inten, int greybin_info) + // Bin intensities using the explicit BinningOrigin enum. + // Replaces the former tri-state integer encoding (greybin_info > 0 = matlab, + // < 0 = radiomics, == 0 = IBSI) with clearer dispatch: n_levels==0 means IBSI + // (no binning), then BinningOrigin selects min-based (PyRadiomics) vs zero-based + // (Nyxus/MATLAB) binning. Dispatch order: IBSI first because n_levels==0 is + // unambiguous, then min_based, then zero as the default. + void bin_intensities (pixData& S, const pixData& I, PixIntens min_I_inten, PixIntens max_I_inten, int n_levels, BinningOrigin bo) { - if (radiomics_grey_binning(greybin_info)) + if (n_levels == 0) { - // radiomics binning + // no binning (IBSI) auto n = I.size(); for (size_t i = 0; i < n; i++) - S[i] = to_grayscale_radiomix (I[i], min_I_inten, max_I_inten, std::abs(greybin_info)); + S[i] = I[i]; return; } - if (matlab_grey_binning(greybin_info)) - { - // matlab binning - auto n = I.size(); - int n_matlab_levels = greybin_info; - - prep_bin_array_matlab (max_I_inten, n_matlab_levels); - for (size_t i = 0; i < n; i++) - S[i] = bin_array_matlab (I[i]); - } - else + if (bo == BinningOrigin::min_based) { - // no binning (IBSI) + // radiomics binning auto n = I.size(); for (size_t i = 0; i < n; i++) - S[i] = I[i]; + S[i] = to_grayscale_radiomix (I[i], min_I_inten, max_I_inten, n_levels); + return; } + // matlab binning (BinningOrigin::zero) + auto n = I.size(); + prep_bin_array_matlab (max_I_inten, n_levels); + for (size_t i = 0; i < n; i++) + S[i] = bin_array_matlab (I[i]); } - void bin_intensities_3d (SimpleCube & S, const SimpleCube & I, PixIntens min_I_inten, PixIntens max_I_inten, int greybin_info) + void bin_intensities_3d (SimpleCube & S, const SimpleCube & I, PixIntens min_I_inten, PixIntens max_I_inten, int n_levels, BinningOrigin bo) { - if (radiomics_grey_binning(greybin_info)) + if (n_levels == 0) + { + // no binning (IBSI) + S.assign (I.begin(), I.end()); + return; + } + if (bo == BinningOrigin::min_based) { // radiomics binning auto n = I.size(); for (size_t i = 0; i < n; i++) - S[i] = to_grayscale_radiomix(I[i], min_I_inten, max_I_inten, std::abs(greybin_info)); + S[i] = to_grayscale_radiomix(I[i], min_I_inten, max_I_inten, n_levels); + return; } - else - if (matlab_grey_binning(greybin_info)) - { - // matlab binning - auto n = I.size(); - int n_matlab_levels = greybin_info; - prep_bin_array_matlab(max_I_inten, n_matlab_levels); - for (size_t i = 0; i < n; i++) - S[i] = bin_array_matlab(I[i]); - } - else - { - // no binning (IBSI) - S.assign (I.begin(), I.end()); - } + // matlab binning (BinningOrigin::zero) + auto n = I.size(); + prep_bin_array_matlab(max_I_inten, n_levels); + for (size_t i = 0; i < n; i++) + S[i] = bin_array_matlab(I[i]); } - static PixIntens bin_pixel (PixIntens x, PixIntens min_I_inten, PixIntens max_I_inten, int greybin_info) + static PixIntens bin_pixel (PixIntens x, PixIntens min_I_inten, PixIntens max_I_inten, int n_levels, BinningOrigin bo) { - if (radiomics_grey_binning(greybin_info)) - { - // radiomics binning - auto y = to_grayscale_radiomix (x, min_I_inten, max_I_inten, std::abs(greybin_info)); - return y; - } - else - if (matlab_grey_binning(greybin_info)) - { - // matlab binning - int n_matlab_levels = greybin_info; - auto y = bin_pixel_matlab(x, max_I_inten, n_matlab_levels); //to_grayscale_matlab (x, n_matlab_levels); - return y; - } - else + if (n_levels == 0) { // no binning (IBSI) return x; } + if (bo == BinningOrigin::min_based) + { + // radiomics binning + return to_grayscale_radiomix (x, min_I_inten, max_I_inten, n_levels); + } + // matlab binning (BinningOrigin::zero) + return bin_pixel_matlab(x, max_I_inten, n_levels); } - static inline bool matlab_grey_binning (int greybinning_info) { return greybinning_info > 0; } - static inline bool radiomics_grey_binning (int greybinning_info) { return greybinning_info < 0; } - static inline bool ibsi_grey_binning (int greybinning_info) { return greybinning_info == 0; } - // returns 1-based bin indices static inline PixIntens to_grayscale_radiomix (PixIntens x, PixIntens min__, PixIntens max__, int binCount) { diff --git a/src/nyx/python/new_bindings_py.cpp b/src/nyx/python/new_bindings_py.cpp index 8a377ba3..d0ad01c8 100644 --- a/src/nyx/python/new_bindings_py.cpp +++ b/src/nyx/python/new_bindings_py.cpp @@ -36,7 +36,7 @@ namespace Nyxus { }; -using ParameterTypes = std::variant, std::vector>; +using ParameterTypes = std::variant, std::vector>; // Defined in nested.cpp bool mine_segment_relations ( @@ -67,7 +67,7 @@ void initialize_environment( const std::vector &features, int neighbor_distance, float pixels_per_micron, - uint32_t coarse_gray_depth, + int coarse_gray_depth, uint32_t n_reduce_threads, int using_gpu, bool ibsi, @@ -79,7 +79,8 @@ void initialize_environment( int verb_lvl, float aniso_x, float aniso_y, - float aniso_z) + float aniso_z, + const std::string& binning_origin = "zero") { Environment & theEnvironment = Nyxus::findenv (instid); @@ -92,6 +93,7 @@ void initialize_environment( theEnvironment.set_coarse_gray_depth(coarse_gray_depth); theEnvironment.n_reduce_threads = n_reduce_threads; theEnvironment.ibsi_compliance = ibsi; + theEnvironment.set_binning_origin(binning_origin == "min" ? BinningOrigin::min_based : BinningOrigin::zero); // Throws exception if invalid feature is passed theEnvironment.expand_featuregroups(); @@ -147,14 +149,15 @@ void set_environment_params_imp ( const std::vector &features = {}, int neighbor_distance = -1, float pixels_per_micron = -1, - uint32_t coarse_gray_depth = 0, + int coarse_gray_depth = 0, uint32_t n_reduce_threads = 0, int using_gpu = -2, float dynamic_range = -1, float min_intensity = -1, float max_intensity = -1, int ram_limit_mb = -1, - int verb_level = 0) + int verb_level = 0, + const std::string& binning_origin = "") { Environment & theEnvironment = Nyxus::findenv (instid); @@ -174,6 +177,10 @@ void set_environment_params_imp ( theEnvironment.set_coarse_gray_depth(coarse_gray_depth); } + if (!binning_origin.empty()) { + theEnvironment.set_binning_origin(binning_origin == "min" ? BinningOrigin::min_based : BinningOrigin::zero); + } + if (n_reduce_threads != 0) { theEnvironment.n_reduce_threads = n_reduce_threads; } @@ -947,6 +954,7 @@ std::map get_params_imp (uint64_t instid, const std params["neighbor_distance"] = theEnvironment.n_pixel_distance; params["pixels_per_micron"] = theEnvironment.xyRes; params["coarse_gray_depth"] = theEnvironment.get_coarse_gray_depth(); + params["binning_origin"] = std::string(theEnvironment.get_binning_origin() == BinningOrigin::min_based ? "min" : "zero"); params["n_feature_calc_threads"] = theEnvironment.n_reduce_threads; params["ibsi"] = theEnvironment.ibsi_compliance; diff --git a/src/nyx/python/nyxus/nyxus.py b/src/nyx/python/nyxus/nyxus.py index a2725e07..bc17803c 100644 --- a/src/nyx/python/nyxus/nyxus.py +++ b/src/nyx/python/nyxus/nyxus.py @@ -120,6 +120,11 @@ class Nyxus: X-dimension scale factor anisotropy_y: float (optional, default 1.0) Y-dimension scale factor + binning_origin: str (optional, default "zero") + Origin of the intensity binning range for texture features. + "zero" - bins span [0, max] (default Nyxus/MATLAB behavior). + "min" - bins span [min, max], adapting to the actual data range + (PyRadiomics-compatible behavior). """ def __init__( @@ -131,10 +136,11 @@ def __init__( 'neighbor_distance', 'pixels_per_micron', 'coarse_gray_depth', 'n_feature_calc_threads', 'use_gpu_device', 'ibsi', 'gabor_kersize', 'gabor_gamma', 'gabor_sig2lam', 'gabor_f0', - 'gabor_thold', 'gabor_thetas', 'gabor_freqs', 'channel_signature', + 'gabor_thold', 'gabor_thetas', 'gabor_freqs', 'channel_signature', 'parent_channel', 'child_channel', 'aggregate', 'dynamic_range', 'min_intensity', 'max_intensity', 'ram_limit', 'verbose', - 'anisotropy_x', 'anisotropy_y' + 'anisotropy_x', 'anisotropy_y', + 'binning_origin' } # Check for unexpected keyword arguments @@ -164,15 +170,19 @@ def __init__( verb_lvl = kwargs.get('verbose', 0) aniso_x = kwargs.get('anisotropy_x', 1.0) aniso_y = kwargs.get('anisotropy_y', 1.0) - + binning_origin = kwargs.get('binning_origin', 'zero') + if neighbor_distance <= 0: raise ValueError("Neighbor distance must be greater than zero.") if pixels_per_micron <= 0: raise ValueError("Pixels per micron must be greater than zero.") - if coarse_gray_depth <= 0: - raise ValueError("Custom number of grayscale levels (parameter coarse_gray_depth, default=64) must be non-negative.") + if coarse_gray_depth < 1: + raise ValueError("coarse_gray_depth must be >= 1.") + + if binning_origin not in ('zero', 'min'): + raise ValueError("binning_origin must be 'zero' or 'min'.") if n_feature_calc_threads < 1: raise ValueError("There must be at least one feature calculation thread.") @@ -209,7 +219,8 @@ def __init__( verb_lvl, aniso_x, aniso_y, - aniso_z) + aniso_z, + binning_origin) self.set_gabor_feature_params( kersize = gabor_kersize, @@ -697,12 +708,13 @@ def set_environment_params (self, **params): 'min_intensity', 'max_intensity', 'ram_limit', + 'binning_origin', ] - + for key in params: if key not in valid_params: raise ValueError(f'Invalid environment parameter {key}. Value parameters are {params}') - + features = params.get('features', []) neighbor_distance = params.get ('neighbor_distance', -1) pixels_per_micron = params.get ('pixels_per_micron', -1) @@ -714,10 +726,11 @@ def set_environment_params (self, **params): min_intensity = params.get('min_intensity', -1) max_intensity = params.get('max_intensity', -1) ram_limit = params.get('ram_limit', -1) - + binning_origin = params.get('binning_origin', "") + set_environment_params_imp (id(self), - features, - neighbor_distance, + features, + neighbor_distance, pixels_per_micron, coarse_gray_depth, n_reduce_threads, @@ -726,13 +739,14 @@ def set_environment_params (self, **params): min_intensity, max_intensity, ram_limit, - verb_lvl) - + verb_lvl, + binning_origin) + def set_params(self, **params): """Sets parameters of the Nyxus class Keyword args: - + * features: List[str], * neighbor_distance * pixels_per_micron @@ -778,16 +792,21 @@ def set_params(self, **params): elif (key == "ibsi"): set_if_ibsi_imp (id(self), value) - + + elif key == "binning_origin": + if value not in ('zero', 'min'): + raise ValueError("binning_origin must be 'zero' or 'min'.") + environment_params["binning_origin"] = value + else: if (key not in available_environment_params): raise ValueError ("Invalid parameter: ", key) else: environment_params[key] = value - + if (len(gabor_params) > 0): self.set_gabor_feature_params(**gabor_params) - + if (len(environment_params) > 0): self.set_environment_params(**environment_params) @@ -944,6 +963,11 @@ class Nyxus3D: Y-dimension scale factor anisotropy_z: float (optional, default 1.0) Z-dimension scale factor + binning_origin: str (optional, default "zero") + Origin of the intensity binning range for texture features. + "zero" - bins span [0, max] (default Nyxus/MATLAB behavior). + "min" - bins span [min, max], adapting to the actual data range + (PyRadiomics-compatible behavior). """ def __init__( @@ -953,13 +977,14 @@ def __init__( ): valid_keys = { 'neighbor_distance', 'pixels_per_micron', 'coarse_gray_depth', - 'n_feature_calc_threads', 'use_gpu_device', 'ibsi', 'channel_signature', - 'parent_channel', 'child_channel', 'aggregate', + 'n_feature_calc_threads', 'use_gpu_device', 'ibsi', 'channel_signature', + 'parent_channel', 'child_channel', 'aggregate', 'dynamic_range', 'min_intensity', 'max_intensity', 'ram_limit', 'verbose', - 'anisotropy_x', + 'anisotropy_x', 'anisotropy_y', - 'anisotropy_z' + 'anisotropy_z', + 'binning_origin' } # Check for unexpected keyword arguments @@ -982,15 +1007,19 @@ def __init__( aniso_x = kwargs.get('anisotropy_x', 1.0) aniso_y = kwargs.get('anisotropy_y', 1.0) aniso_z = kwargs.get('anisotropy_z', 1.0) - + binning_origin = kwargs.get('binning_origin', 'zero') + if neighbor_distance <= 0: raise ValueError("Neighbor distance must be greater than zero.") if pixels_per_micron <= 0: raise ValueError("Pixels per micron must be greater than zero.") - if coarse_gray_depth <= 0: - raise ValueError("Custom number of grayscale levels (parameter coarse_gray_depth, default=64) must be non-negative.") + if coarse_gray_depth < 1: + raise ValueError("coarse_gray_depth must be >= 1.") + + if binning_origin not in ('zero', 'min'): + raise ValueError("binning_origin must be 'zero' or 'min'.") if n_feature_calc_threads < 1: raise ValueError("There must be at least one feature calculation thread.") @@ -1035,8 +1064,9 @@ def __init__( verb_lvl, aniso_x, aniso_y, - aniso_z) - + aniso_z, + binning_origin) + # list of valid outputs that are used throughout featurize functions self._valid_output_types = ['pandas', 'arrowipc', 'parquet'] @@ -1257,13 +1287,14 @@ def set_environment_params (self, **params): 'verbose', 'dynamic_range', 'min_intensity', - 'max_intensity' + 'max_intensity', + 'binning_origin', ] - + for key in params: if key not in valid_params: raise ValueError(f'Invalid environment parameter {key}. Value parameters are {params}') - + features = params.get('features', []) neighbor_distance = params.get ('neighbor_distance', -1) pixels_per_micron = params.get ('pixels_per_micron', -1) @@ -1275,10 +1306,11 @@ def set_environment_params (self, **params): min_intensity = params.get('min_intensity', -1) max_intensity = params.get('max_intensity', -1) ram_limit = -1 # no limit - + binning_origin = params.get('binning_origin', "") + set_environment_params_imp (id(self), - features, - neighbor_distance, + features, + neighbor_distance, pixels_per_micron, coarse_gray_depth, n_reduce_threads, @@ -1287,13 +1319,14 @@ def set_environment_params (self, **params): min_intensity, max_intensity, ram_limit, - verb_lvl) - + verb_lvl, + binning_origin) + def set_params(self, **params): """Sets parameters of the Nyxus class Keyword args: - + * features: List[str], * neighbor_distance * pixels_per_micron @@ -1304,9 +1337,9 @@ def set_params(self, **params): * dynamic_range (float): Desired dynamic range of voxels of a floating point TIFF image. * min_intensity (float): Minimum intensity of voxels of a floating point TIFF image. * max_intensity (float): Maximum intensity of voxels of a floating point TIFF image. - + """ - + available_environment_params = [ "features", "neighbor_distance", @@ -1319,24 +1352,28 @@ def set_params(self, **params): "min_intensity", "max_intensity" ] - + environment_params = {} - + gabor_params = {} - - + + for key, value in params.items(): - + if (key == "ibsi"): set_if_ibsi_imp (id(self), value) - + + elif key == "binning_origin": + if value not in ('zero', 'min'): + raise ValueError("binning_origin must be 'zero' or 'min'.") + environment_params["binning_origin"] = value + else: if (key not in available_environment_params): raise ValueError(f"Invalid parameter {key}.") else: environment_params[key] = value - - + if (len(environment_params) > 0): self.set_environment_params(**environment_params) @@ -1562,8 +1599,9 @@ def __init__( verb_lvl, aniso_x, aniso_y, - aniso_z) - + aniso_z, + "zero") + # list of valid outputs that are used throughout featurize functions self._valid_output_types = ['pandas', 'arrowipc', 'parquet'] @@ -1991,13 +2029,14 @@ def set_environment_params (self, **params): 'verbose', 'dynamic_range', 'min_intensity', - 'max_intensity' + 'max_intensity', + 'binning_origin', ] - + for key in params: if key not in valid_params: raise ValueError(f'Invalid environment parameter {key}. Value parameters are {params}') - + features = params.get('features', []) neighbor_distance = params.get ('neighbor_distance', -1) pixels_per_micron = params.get ('pixels_per_micron', -1) @@ -2009,10 +2048,11 @@ def set_environment_params (self, **params): min_intensity = params.get('min_intensity', -1) max_intensity = params.get('max_intensity', -1) ram_limit = -1 # no limit - + binning_origin = params.get('binning_origin', "") + set_environment_params_imp (id(self), features, - neighbor_distance, + neighbor_distance, pixels_per_micron, coarse_gray_depth, n_reduce_threads, @@ -2021,13 +2061,14 @@ def set_environment_params (self, **params): min_intensity, max_intensity, ram_limit, - verb_lvl) - + verb_lvl, + binning_origin) + def set_params(self, **params): """Sets parameters of the Nyxus class Keyword args: - + * features: List[str], * neighbor_distance * pixels_per_micron diff --git a/tests/python/test_nyxus.py b/tests/python/test_nyxus.py index d5e578a1..b85192a1 100644 --- a/tests/python/test_nyxus.py +++ b/tests/python/test_nyxus.py @@ -829,11 +829,11 @@ def test_3d_glcm_compatibility (self): Testing Nyxus 3D GLCM features compatibility with Radiomics library ''' - nyx = nyxus.Nyxus3D (["*3D_GLCM*"]) + nyx = nyxus.Nyxus3D (["*3D_GLCM*"], binning_origin="min") assert nyx is not None # configure Nyxus 3D GLCM to mock Radiomics - nyx.set_metaparam ("3glcm/greydepth=-20") # corresponds to Radiomics setting "binCount:20" + nyx.set_metaparam ("3glcm/greydepth=20") # corresponds to Radiomics setting "binCount:20" nyx.set_metaparam ("3glcm/offset=1") nyx.set_metaparam ("3glcm/numang=13") nyx.set_metaparam ("3glcm/sparseintensities=true") @@ -901,11 +901,11 @@ def test_3d_gldm_compatibility (self): Testing Nyxus 3D GLDM features compatibility with Radiomics library ''' - nyx = nyxus.Nyxus3D (["*3D_GLDM*"]) + nyx = nyxus.Nyxus3D (["*3D_GLDM*"], binning_origin="min") assert nyx is not None # configure Nyxus 3D GLDM to mock Radiomics - nyx.set_metaparam ("3gldm/greydepth=-20") # corresponds to Radiomics setting "binCount:20" + nyx.set_metaparam ("3gldm/greydepth=20") # corresponds to Radiomics setting "binCount:20" # calculate features testsRoot = str (pathlib.Path(__file__).parent.parent.resolve()) # parent.parent to reach the data owned by c++ tests @@ -988,11 +988,11 @@ def test_3d_glrlm_compatibility (self): Testing Nyxus 3D GLRLM features compatibility with Radiomics library ''' - nyx = nyxus.Nyxus3D (["*3D_GLRLM*"]) + nyx = nyxus.Nyxus3D (["*3D_GLRLM*"], binning_origin="min") assert nyx is not None # configure Nyxus 3D GLRLM to mock Radiomics - nyx.set_metaparam ("3glrlm/greydepth=-20") # corresponds to Radiomics setting "binCount:20" + nyx.set_metaparam ("3glrlm/greydepth=20") # corresponds to Radiomics setting "binCount:20" # calculate features testsRoot = str (pathlib.Path(__file__).parent.parent.resolve()) # parent.parent to reach the data owned by c++ tests @@ -1044,11 +1044,11 @@ def test_3d_glszm_compatibility (self): Testing Nyxus 3D GLSZM features compatibility with Radiomics library ''' - nyx = nyxus.Nyxus3D (["*3D_GLSZM*"]) + nyx = nyxus.Nyxus3D (["*3D_GLSZM*"], binning_origin="min") assert nyx is not None # configure Nyxus 3D GLSZM to mock Radiomics - nyx.set_metaparam ("3glszm/greydepth=-20") # corresponds to Radiomics setting "binCount:20" + nyx.set_metaparam ("3glszm/greydepth=20") # corresponds to Radiomics setting "binCount:20" # calculate features testsRoot = str (pathlib.Path(__file__).parent.parent.resolve()) # parent.parent to reach the data owned by c++ tests diff --git a/tests/test_3d_glcm.h b/tests/test_3d_glcm.h index 80677669..0505b910 100644 --- a/tests/test_3d_glcm.h +++ b/tests/test_3d_glcm.h @@ -110,7 +110,8 @@ void test_3glcm_feature (const Nyxus::Feature3D& expecting_fcode, const std::str // (4) GLCM-specific feature settings mocking default pyRadiomics settings - s[(int)NyxSetting::GLCM_GREYDEPTH].ival = -100; // intentionally negative to activate radiomics binCount-based grey-binning + s[(int)NyxSetting::GLCM_GREYDEPTH].ival = 100; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); s[(int)NyxSetting::GLCM_OFFSET].ival = 1; s[(int)NyxSetting::GLCM_SPARSEINTENS].bval = true; diff --git a/tests/test_all.cc b/tests/test_all.cc index f40d39f9..424f87d4 100644 --- a/tests/test_all.cc +++ b/tests/test_all.cc @@ -31,12 +31,31 @@ #include "test_compat_3d_ngtdm.h" #include "test_compat_3d_glrlm.h" #include "test_compat_3d_glszm.h" +#include "test_binning_origin.h" #ifdef USE_ARROW #include "test_arrow.h" #include "test_arrow_file_name.h" #endif +//***** Binning origin ***** + +TEST(TEST_NYXUS, TEST_BIN_PIXEL_ZERO_ORIGIN) { + ASSERT_NO_THROW(Nyxus::test_bin_pixel_zero_origin()); +} + +TEST(TEST_NYXUS, TEST_BIN_PIXEL_MIN_ORIGIN) { + ASSERT_NO_THROW(Nyxus::test_bin_pixel_min_origin()); +} + +TEST(TEST_NYXUS, TEST_GLCM_BINNING_ORIGIN_DIVERGENCE) { + ASSERT_NO_THROW(Nyxus::test_glcm_binning_origin_divergence()); +} + +TEST(TEST_NYXUS, TEST_NGTDM_BINNING_ORIGIN_DIVERGENCE) { + ASSERT_NO_THROW(Nyxus::test_ngtdm_binning_origin_divergence()); +} + //***** 2D contour and multicontour ***** TEST(TEST_NYXUS, TEST_CONTOUR_MULTI_1) { diff --git a/tests/test_binning_origin.h b/tests/test_binning_origin.h new file mode 100644 index 00000000..be26086a --- /dev/null +++ b/tests/test_binning_origin.h @@ -0,0 +1,149 @@ +#pragma once + +#include +#include "../src/nyx/features/texture_feature.h" +#include "../src/nyx/features/glcm.h" +#include "../src/nyx/features/ngtdm.h" +#include "../src/nyx/roi_cache.h" +#include "test_data.h" +#include "test_main_nyxus.h" + +namespace Nyxus +{ + // "zero" origin, MATLAB-style: + // slope = n_levels / max, intercept = 1 + // bin = floor(slope * x + intercept), clipped to [1, n_levels] + static void test_bin_pixel_zero_origin() + { + int n_bins = 10; + PixIntens min_I = 50, max_I = 200, x = 100; + + // slope = 10 / 200 = 0.05, intercept = 1 - 0.05*0 = 1 + // bin = floor(0.05 * 100 + 1) = floor(6) = 6 + auto result = TextureFeature::bin_pixel(x, min_I, max_I, n_bins, BinningOrigin::zero); + ASSERT_EQ(result, 6); + } + + // "min" origin, PyRadiomics-style: + // binWidth = (max - min) / binCount + // bin = floor((x - min) / binWidth) + 1 + static void test_bin_pixel_min_origin() + { + int n_bins = 10; + PixIntens min_I = 50, max_I = 200, x = 100; + + // binWidth = (200 - 50) / 10 = 15 + // bin = floor((100 - 50) / 15) + 1 = floor(3.33) + 1 = 4 + auto result = TextureFeature::bin_pixel(x, min_I, max_I, n_bins, BinningOrigin::min_based); + ASSERT_EQ(result, 4); + } + + // End-to-end: GLCM_ASM computed with "zero" vs "min" binning origin + // must differ, and each must match its known ground truth. + static void test_glcm_binning_origin_divergence() + { + Fsettings s; + s.resize((int)NyxSetting::__COUNT__); + s[(int)NyxSetting::SOFTNAN].rval = 0.0; + s[(int)NyxSetting::TINY].rval = 0.0; + s[(int)NyxSetting::SINGLEROI].bval = false; + s[(int)NyxSetting::PIXELSIZEUM].rval = 100; + s[(int)NyxSetting::PIXELDISTANCE].ival = 5; + s[(int)NyxSetting::USEGPU].bval = false; + s[(int)NyxSetting::VERBOSLVL].ival = 0; + s[(int)NyxSetting::IBSI].bval = false; + s[(int)NyxSetting::GLCM_OFFSET].ival = 1; + GLCMFeature::symmetric_glcm = false; + GLCMFeature::angles = { 0, 45, 90, 135 }; + + int feature = int(Feature2D::GLCM_ASM); + + // --- "zero" origin (MATLAB) --- + s[(int)NyxSetting::GREYDEPTH].ival = 100; + s[(int)NyxSetting::GLCM_GREYDEPTH].ival = 100; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::zero); + + LR roi_zero; + GLCMFeature f_zero; + load_masked_test_roi_data(roi_zero, ibsi_phantom_z1_intensity, ibsi_phantom_z1_mask, + sizeof(ibsi_phantom_z1_mask) / sizeof(NyxusPixel)); + ASSERT_NO_THROW(f_zero.calculate(roi_zero, s)); + roi_zero.initialize_fvals(); + f_zero.save_value(roi_zero.fvals); + double val_zero = roi_zero.fvals[feature][0]; + + // --- "min" origin (PyRadiomics) --- + s[(int)NyxSetting::GREYDEPTH].ival = 100; + s[(int)NyxSetting::GLCM_GREYDEPTH].ival = 100; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); + + LR roi_min; + GLCMFeature f_min; + load_masked_test_roi_data(roi_min, ibsi_phantom_z1_intensity, ibsi_phantom_z1_mask, + sizeof(ibsi_phantom_z1_mask) / sizeof(NyxusPixel)); + ASSERT_NO_THROW(f_min.calculate(roi_min, s)); + roi_min.initialize_fvals(); + f_min.save_value(roi_min.fvals); + double val_min = roi_min.fvals[feature][0]; + + // The two binning origins must produce different GLCM values + ASSERT_NE(val_zero, val_min) + << "zero_origin=" << val_zero << " min_origin=" << val_min; + + // Ground truth for GLCM_ASM angle-0 on ibsi_phantom_z1 at 100 bins + ASSERT_TRUE(agrees_gt(val_zero, 0.148438, 100.)) + << "zero_origin GLCM_ASM=" << val_zero; + ASSERT_TRUE(agrees_gt(val_min, 0.140625, 100.)) + << "min_origin GLCM_ASM=" << val_min; + } + + // End-to-end: NGTDM_COARSENESS computed with "zero" vs "min" binning origin + // must differ — exercises the non-GLCM texture feature path (bin_intensities). + static void test_ngtdm_binning_origin_divergence() + { + Fsettings s; + s.resize((int)NyxSetting::__COUNT__); + s[(int)NyxSetting::SOFTNAN].rval = 0.0; + s[(int)NyxSetting::TINY].rval = 0.0; + s[(int)NyxSetting::SINGLEROI].bval = false; + s[(int)NyxSetting::PIXELSIZEUM].rval = 100; + s[(int)NyxSetting::PIXELDISTANCE].ival = 5; + s[(int)NyxSetting::USEGPU].bval = false; + s[(int)NyxSetting::VERBOSLVL].ival = 0; + s[(int)NyxSetting::IBSI].bval = false; + + int feature = int(Feature2D::NGTDM_COARSENESS); + + // --- "zero" origin (MATLAB) --- + s[(int)NyxSetting::GREYDEPTH].ival = 100; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::zero); + NGTDMFeature::n_levels = 0; // let it use GREYDEPTH + + LR roi_zero; + NGTDMFeature f_zero; + load_masked_test_roi_data(roi_zero, ibsi_phantom_z1_intensity, ibsi_phantom_z1_mask, + sizeof(ibsi_phantom_z1_mask) / sizeof(NyxusPixel)); + ASSERT_NO_THROW(f_zero.calculate(roi_zero, s)); + roi_zero.initialize_fvals(); + f_zero.save_value(roi_zero.fvals); + double val_zero = roi_zero.fvals[feature][0]; + + // --- "min" origin (PyRadiomics) --- + s[(int)NyxSetting::GREYDEPTH].ival = 100; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); + NGTDMFeature::n_levels = 0; + + LR roi_min; + NGTDMFeature f_min; + load_masked_test_roi_data(roi_min, ibsi_phantom_z1_intensity, ibsi_phantom_z1_mask, + sizeof(ibsi_phantom_z1_mask) / sizeof(NyxusPixel)); + ASSERT_NO_THROW(f_min.calculate(roi_min, s)); + roi_min.initialize_fvals(); + f_min.save_value(roi_min.fvals); + double val_min = roi_min.fvals[feature][0]; + + // The two binning origins must produce different NGTDM values + ASSERT_NE(val_zero, val_min) + << "zero_origin=" << val_zero << " min_origin=" << val_min; + } +} diff --git a/tests/test_compat_3d_fo_radiomics.h b/tests/test_compat_3d_fo_radiomics.h index ea165d90..a5a0ed4b 100644 --- a/tests/test_compat_3d_fo_radiomics.h +++ b/tests/test_compat_3d_fo_radiomics.h @@ -111,7 +111,8 @@ void test_compat_radiomics_3fo_feature (const Nyxus::Feature3D &expected_fcode, s[(int)NyxSetting::SOFTNAN].rval = 0.0; s[(int)NyxSetting::TINY].rval = 0.0; s[(int)NyxSetting::SINGLEROI].bval = false; - s[(int)NyxSetting::GREYDEPTH].ival = -20; // intentionally negative to activate radiomics binCount-based grey-binning + s[(int)NyxSetting::GREYDEPTH].ival = 20; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); s[(int)NyxSetting::PIXELSIZEUM].rval = 100; s[(int)NyxSetting::PIXELDISTANCE].ival = 5; s[(int)NyxSetting::USEGPU].bval = false; diff --git a/tests/test_compat_3d_glcm.h b/tests/test_compat_3d_glcm.h index a4674aa2..97a81e57 100644 --- a/tests/test_compat_3d_glcm.h +++ b/tests/test_compat_3d_glcm.h @@ -173,7 +173,8 @@ void test_compat_3glcm_feature (const Nyxus::Feature3D& expecting_fcode, const s // (4) GLCM-specific feature settings mocking default pyRadiomics settings - s[(int)NyxSetting::GLCM_GREYDEPTH].ival = -20; // intentionally negative to activate radiomics binCount-based grey-binning + s[(int)NyxSetting::GLCM_GREYDEPTH].ival = 20; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); s[(int)NyxSetting::GLCM_OFFSET].ival = 1; s[(int)NyxSetting::GLCM_SPARSEINTENS].bval = true; diff --git a/tests/test_compat_3d_gldm.h b/tests/test_compat_3d_gldm.h index 87bfb435..c707329e 100644 --- a/tests/test_compat_3d_gldm.h +++ b/tests/test_compat_3d_gldm.h @@ -100,7 +100,8 @@ void test_compat_3gldm_feature (const Nyxus::Feature3D & expecting_fcode, const // (4) GLCM-specific feature settings mocking default pyRadiomics settings - s[(int)NyxSetting::GLDM_GREYDEPTH].ival = -20; // intentionally negative to activate radiomics binCount-based grey-binning + s[(int)NyxSetting::GLDM_GREYDEPTH].ival = 20; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); // (5) feature extraction diff --git a/tests/test_compat_3d_glrlm.h b/tests/test_compat_3d_glrlm.h index b1321f36..bbeffeb3 100644 --- a/tests/test_compat_3d_glrlm.h +++ b/tests/test_compat_3d_glrlm.h @@ -185,7 +185,8 @@ void test_compat_3glrlm_feature(const Nyxus::Feature3D& expecting_fcode, const s // (4) NGTDM-specific feature settings mocking default pyRadiomics settings - s[(int)NyxSetting::GLRLM_GREYDEPTH].ival = -20; // intentionally negative to activate radiomics binCount-based grey-binning + s[(int)NyxSetting::GLRLM_GREYDEPTH].ival = 20; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); // (5) feature extraction diff --git a/tests/test_compat_3d_glszm.h b/tests/test_compat_3d_glszm.h index 1c67b402..87b6c7de 100644 --- a/tests/test_compat_3d_glszm.h +++ b/tests/test_compat_3d_glszm.h @@ -101,7 +101,8 @@ void test_compat_3glszm_feature (const Nyxus::Feature3D& expecting_fcode, const // (4) GLCM-specific feature settings mocking default pyRadiomics settings - s[(int)NyxSetting::GLSZM_GREYDEPTH].ival = -20; // intentionally negative to activate radiomics binCount-based grey-binning + s[(int)NyxSetting::GLSZM_GREYDEPTH].ival = 20; + s[(int)NyxSetting::BINNING_ORIGIN].ival = static_cast(BinningOrigin::min_based); // (5) feature extraction diff --git a/tests/test_ibsi_gldzm.h b/tests/test_ibsi_gldzm.h index 98d59b40..5de6d3c0 100644 --- a/tests/test_ibsi_gldzm.h +++ b/tests/test_ibsi_gldzm.h @@ -57,7 +57,7 @@ void test_ibsi_gldzm_matrix() SimpleMatrix GLDZM; int Ng, // number of grey levels Nd; // maximum number of non-zero dependencies - ASSERT_NO_THROW(f.prepare_GLDZM_matrix_kit (GLDZM, Ng, Nd, greysLUT, roidata, STNGS_NGREYS(s), STNGS_IBSI(s))); + ASSERT_NO_THROW(f.prepare_GLDZM_matrix_kit (GLDZM, Ng, Nd, greysLUT, roidata, STNGS_NGREYS(s), STNGS_IBSI(s), STNGS_BINNING_ORIGIN(s))); // Count discrepancies int n_mismatches = 0;