Skip to content
Open
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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ set(SOURCE
src/nyx/slideprops.cpp
src/nyx/strpat.cpp
src/nyx/workflow_2d_segmented.cpp
src/nyx/workflow_2d_fmaps.cpp
src/nyx/workflow_2d_whole.cpp
src/nyx/workflow_3d_fmaps.cpp
src/nyx/workflow_3d_segmented.cpp
src/nyx/workflow_3d_whole.cpp
src/nyx/workflow_pythonapi.cpp
Expand Down
75 changes: 62 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,54 @@ f = nyx.featurize_directory (intensity_dir=dir, label_dir=dir)
```


### Feature maps (sliding kernel) mode

Feature maps mode computes features at every position of a sliding kernel across each ROI, producing spatial feature maps instead of a single feature vector per ROI. This is useful for generating spatially resolved feature representations for downstream analysis or machine learning.

#### 2D feature maps

```python
from nyxus import Nyxus, save_fmaps_to_tiff

nyx = Nyxus(["*ALL_INTENSITY*"], fmaps=True, fmaps_radius=2) # 5x5 kernel
results = nyx.featurize_directory("/path/to/intensities", "/path/to/labels")

# results is a list of dicts, one per parent ROI:
# [
# {
# "parent_roi_label": 1,
# "intensity_image": "img1.tif",
# "mask_image": "seg1.tif",
# "origin_x": 10, "origin_y": 20,
# "features": {
# "MEAN": numpy.array(shape=(map_h, map_w)),
# "STDDEV": numpy.array(shape=(map_h, map_w)),
# ...
# }
# },
# ...
# ]

# Save feature maps as TIFF stacks (requires tifffile)
save_fmaps_to_tiff(results, "output/tiff/")
```

#### 3D feature maps

```python
from nyxus import Nyxus3D, save_fmaps_to_nifti

nyx = Nyxus3D(["*3D_ALL_INTENSITY*"], fmaps=True, fmaps_radius=1) # 3x3x3 kernel
results = nyx.featurize_directory("/path/to/volumes", "/path/to/masks")

# Each feature map is a 3D numpy array shaped (map_d, map_h, map_w)

# Save as NIfTI volumes (requires nibabel)
save_fmaps_to_nifti(results, "output/nifti/", voxel_size=(0.5, 0.5, 1.0))
```

Note: Feature maps mode returns numpy arrays rather than DataFrames, since the output is inherently image-shaped. Arrow/Parquet output is not supported in this mode.

## Further steps

For more information on all of the available options and features, check out [the documentation](#).
Expand Down Expand Up @@ -271,19 +319,20 @@ print(nyx.get_params())
will print the dictionary

```bash
{'coarse_gray_depth': 256,
'features': ['*ALL*'],
'gabor_f0': 0.1,
'gabor_freqs': [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0],
'gabor_gamma': 0.1,
'gabor_kersize': 16,
'gabor_sig2lam': 0.8,
'gabor_theta': 45.0,
'gabor_thold': 0.025,
'ibsi': 0,
'n_loader_threads': 1,
'n_feature_calc_threads': 4,
'neighbor_distance': 5,
{'binning_origin': 'zero',
'coarse_gray_depth': 256,
'features': ['*ALL*'],
'gabor_f0': 0.1,
'gabor_freqs': [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0],
'gabor_gamma': 0.1,
'gabor_kersize': 16,
'gabor_sig2lam': 0.8,
'gabor_theta': 45.0,
'gabor_thold': 0.025,
'ibsi': 0,
'n_loader_threads': 1,
'n_feature_calc_threads': 4,
'neighbor_distance': 5,
'pixels_per_micron': 1.0}
```

Expand Down
4 changes: 2 additions & 2 deletions ci-utils/envs/conda_cpp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 11 additions & 2 deletions docs/source/References/Classes.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
Nyxus Classes
================
.. autosummary::
:toctree: stubs
:toctree: stubs

nyxus.Nyxus
nyxus.Nested
nyxus.Nyxus3D
nyxus.Nested

Feature Map I/O
================
.. autosummary::
:toctree: stubs

nyxus.save_fmaps_to_tiff
nyxus.save_fmaps_to_nifti
44 changes: 30 additions & 14 deletions docs/source/cmdline_and_examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ should adhere to columns "WIPP I/O role" and "WIPP type".
- integer
- input
- integer
* - --binningOrigin
- (Optional) Origin of the intensity binning range for texture features. 'zero' bins span [0, max] (default). 'min' bins span [min, max], adapting to the actual data range (PyRadiomics-compatible). Default: '--binningOrigin=zero'
- string constant
- input
- enum
* - --gaborfreqs
- (Optional) Feature GABOR: custom denominators of :math:`\pi` as frequencies of Gabor filter's harmonic factor. Default: '--gaborfreqs=1,2,4,8,16,32,64'
- list of integer constants
Expand Down Expand Up @@ -135,9 +140,19 @@ should adhere to columns "WIPP I/O role" and "WIPP type".
- path
* - --arrowOutputType
- (Optional) Type of Arrow file to write the feature results to. Current options are 'arrow' for Arrow IPC or 'parquet' for Parquet
- string
- string
- output
- enum
* - --fmaps
- (Optional) Enable feature maps mode. When enabled, a sliding kernel is moved across each ROI and features are computed at every position, producing spatial feature maps instead of a single feature vector per ROI. Acceptable values: true, false. Default: '--fmaps=false'. Not compatible with Arrow/Parquet output.
- string constant
- input
- enum
* - --fmapsRadius
- (Optional) Radius of the sliding kernel in feature maps mode. The kernel size is (2 * radius + 1). For example, '--fmapsRadius=2' produces a 5x5 kernel (2D) or 5x5x5 kernel (3D). Default: '--fmapsRadius=2'
- integer
- input
- integer

Examples
========
Expand Down Expand Up @@ -378,19 +393,20 @@ will print the dictionary

.. code-block:: bash

{'coarse_gray_depth': 256,
'features': ['*ALL*'],
'gabor_f0': 0.1,
'gabor_freqs': [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0],
'gabor_gamma': 0.1,
'gabor_kersize': 16,
'gabor_sig2lam': 0.8,
'gabor_theta': 45.0,
'gabor_thold': 0.025,
'ibsi': 0,
'n_loader_threads': 1,
'n_feature_calc_threads': 4,
'neighbor_distance': 5,
{'binning_origin': 'zero',
'coarse_gray_depth': 256,
'features': ['*ALL*'],
'gabor_f0': 0.1,
'gabor_freqs': [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0],
'gabor_gamma': 0.1,
'gabor_kersize': 16,
'gabor_sig2lam': 0.8,
'gabor_theta': 45.0,
'gabor_thold': 0.025,
'ibsi': 0,
'n_loader_threads': 1,
'n_feature_calc_threads': 4,
'neighbor_distance': 5,
'pixels_per_micron': 1.0}

There is also the option to pass arguments to this function to only receive a subset of parameter values. The arguments should be
Expand Down
5 changes: 5 additions & 0 deletions src/nyx/cli_option_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,6 +62,10 @@
#define clo_ANISO_Y "--anisoy"
#define clo_ANISO_Z "--anisoz"

// Feature maps
#define clo_FMAPS "--fmaps" // Enable feature maps mode. "true" or "false"
#define clo_FMAPS_RADIUS "--fmapsRadius" // Kernel radius for feature maps. Integer >= 1. Example: "2" (produces 5x5 kernel)

// Result options
#define clo_NOVAL "--noval" // -> raw_noval
#define clo_TINYVAL "--tinyval" // -> raw_tiny
Expand Down
43 changes: 40 additions & 3 deletions src/nyx/environment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ void Environment::show_cmdline_help()
<< "\t\t\tDefault: 5 \n"
<< "\t\t" << OPT << clo_COARSEGRAYDEPTH << "=<custom number of grayscale levels> \n"
<< "\t\t\tDefault: 64 \n"
<< "\t\t" << OPT << clo_BINNINGORIGIN << "=<zero|min> 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 << "=<one or more comma separated rotation angles from set {0, 45, 90, and 135}> \n"
<< "\t\t\tDefault: 0,45,90,135 \n"
<< "\t\t" << OPT << clo_VERBOSITY << "=<levels of verbosity 0 (silence), 1 (minimum output), 2 (1 + timing), 3 (2 + roi metrics + more timing), 4 (3 + diagnostic information)> \n"
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -474,6 +477,9 @@ bool Environment::parse_cmdline(int argc, char** argv)
|| find_string_argument(i, clo_RESULTFNAME, nyxus_result_fname)
|| find_string_argument(i, clo_CLI_DIM, raw_dim)

|| find_string_argument(i, clo_FMAPS, raw_fmaps)
|| find_string_argument(i, clo_FMAPS_RADIUS, raw_fmaps_radius)

#ifdef CHECKTIMING
|| find_string_argument(i, clo_EXCLUSIVETIMING, rawExclusiveTiming)
#endif
Expand Down Expand Up @@ -663,9 +669,21 @@ 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)
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 a positive integer constant\n";
return false;
}
}

// parse BINNINGORIGIN -- negate coarse_grayscale_depth for "min" origin (PyRadiomics-style)
if (!raw_binning_origin.empty())
{
if (raw_binning_origin == "min")
coarse_grayscale_depth = -std::abs(coarse_grayscale_depth);
else if (raw_binning_origin != "zero")
{
std::cerr << "Error: " << clo_COARSEGRAYDEPTH << "=" << raw_coarse_grayscale_depth << ": expecting an integer constant\n";
std::cerr << "Error: " << clo_BINNINGORIGIN << "=" << raw_binning_origin << ": expecting 'zero' or 'min'\n";
return false;
}
}
Expand Down Expand Up @@ -900,6 +918,25 @@ bool Environment::parse_cmdline(int argc, char** argv)
ibsi_compliance = false;
}

//==== Parse feature maps options
if (!raw_fmaps.empty())
{
std::string tmp = raw_fmaps;
std::transform(tmp.begin(), tmp.end(), tmp.begin(), ::tolower);
fmaps_mode = (tmp == "true" || tmp == "1" || tmp == "on");
}

if (!raw_fmaps_radius.empty())
{
int r = 0;
if (sscanf(raw_fmaps_radius.c_str(), "%d", &r) != 1 || r < 1)
{
std::cerr << "Error: " << clo_FMAPS_RADIUS << "=" << raw_fmaps_radius << ": expecting an integer >= 1\n";
return false;
}
fmaps_kernel_radius = r;
}

// Success
return true;
}
Expand All @@ -915,7 +952,7 @@ 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;
}
Expand Down
18 changes: 17 additions & 1 deletion src/nyx/environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class Environment: public BasicEnvironment
int get_floating_point_precision();

int get_coarse_gray_depth();
void set_coarse_gray_depth(unsigned int new_depth);
void set_coarse_gray_depth(int new_depth);

// implementation of SKIPROI
bool roi_is_blacklisted (const std::string& fname, int roi_label);
Expand Down Expand Up @@ -148,6 +148,21 @@ class Environment: public BasicEnvironment
ResultOptions resultOptions;
std::tuple<bool, std::optional<std::string>> parse_result_options_4cli ();

// feature maps options
bool fmaps_mode = false;
int fmaps_kernel_radius = 2;
std::string raw_fmaps;
std::string raw_fmaps_radius;

/// @brief Returns the kernel side length: 2*radius+1
int fmaps_kernel_size() const { return 2 * fmaps_kernel_radius + 1; }

/// @brief Returns true if fmaps mode conflicts with the current save option.
bool fmaps_prevents_arrow() const
{
return fmaps_mode && (saveOption == Nyxus::SaveOption::saveArrowIPC || saveOption == Nyxus::SaveOption::saveParquet);
}

// feature settings
Fsettings fsett_PixelIntensity,
fsett_BasicMorphology,
Expand Down Expand Up @@ -242,6 +257,7 @@ class Environment: public BasicEnvironment

int coarse_grayscale_depth; //= 64;
std::string raw_coarse_grayscale_depth; //= "";
std::string raw_binning_origin; //= "" (default "zero"; alternative "min")

// data members implementing RAMLIMIT
std::string rawRamLimit; //= "";
Expand Down
9 changes: 6 additions & 3 deletions src/nyx/features/3d_glcm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ void D3_GLCM_feature::calculate (LR& r, const Fsettings& s)
SimpleCube<PixIntens> 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
auto greyBinningInfo = STNGS_GLCM_GREYDEPTH(s);
if (greyBinningInfo == 0)
greyBinningInfo = s[(int)NyxSetting::GREYDEPTH].ival;
if (STNGS_IBSI(s))
greyBinningInfo = 0;

bin_intensities_3d (D, r.aux_image_cube, r.aux_min, r.aux_max, greyBinningInfo);
Expand All @@ -64,7 +67,7 @@ void D3_GLCM_feature::calculate (LR& r, const Fsettings& s)
D,
r.aux_min,
r.aux_max,
STNGS_GLCM_GREYDEPTH(s),
greyBinningInfo,
STNGS_IBSI(s),
STNGS_NAN(s));
}
Expand Down
2 changes: 1 addition & 1 deletion src/nyx/features/3d_ngtdm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ void D3_NGTDM_feature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&
D.allocate_from_cloud(r.raw_pixels_NT, r.aabb, false);

// Gather zones
unsigned int nGrays = STNGS_NGTDM_GREYDEPTH(s); // former theEnvironment.get_coarse_gray_depth()
int nGrays = STNGS_NGTDM_GREYDEPTH(s); // former theEnvironment.get_coarse_gray_depth()
for (int row = 0; row < D.get_height(); row++)
for (int col = 0; col < D.get_width(); col++)
{
Expand Down
9 changes: 5 additions & 4 deletions src/nyx/features/glcm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ 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
int nGreys = STNGS_GLCM_GREYDEPTH(s);
if (nGreys == 0)
nGreys = s[(int)NyxSetting::GREYDEPTH].ival;
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);
Expand Down
2 changes: 1 addition & 1 deletion src/nyx/features/gldm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ void GLDMFeature::osized_calculate (LR& r, const Fsettings& s, ImageLoader&)
// Prepare ROI's intensity range for normalize_I()
PixIntens piRange = r.aux_max - r.aux_min;

unsigned int nGrays = STNGS_NGREYS(s); // former theEnvironment.get_coarse_gray_depth()
int nGrays = STNGS_NGREYS(s); // former theEnvironment.get_coarse_gray_depth()

size_t height = D.get_height(),
width = D.get_width();
Expand Down
Loading
Loading