Skip to content

Add stats/roivolumes module and GM volumes support in atlas_roimetrics - #375

Open
baptisp wants to merge 24 commits into
nf-neuro:mainfrom
baptisp:feat/stats-roivolumes
Open

Add stats/roivolumes module and GM volumes support in atlas_roimetrics#375
baptisp wants to merge 24 commits into
nf-neuro:mainfrom
baptisp:feat/stats-roivolumes

Conversation

@baptisp

@baptisp baptisp commented Jul 24, 2026

Copy link
Copy Markdown

Describe your changes

New module stats/roivolumes that computes voxel counts and volumes (mm³) for ROI regions. Supports two modes: WM (list of binary/TDI masks, one per bundle) and GM (single label atlas with JSON Look-Up Table (LUT)). Uses nibabel and numpy directly — no scilpy CLI dependency.

List test packages used by your module

tractometry.zip (WM binary masks test)
plot.zip (GM label atlas test from Brainnetome atlas)

Checklist before requesting a review

  • Create the tool:
    • Edit ./modules/nf-neuro/<category>/<tool>/main.nf
    • Edit ./modules/nf-neuro/<category>/<tool>/meta.yml
    • Edit ./modules/nf-neuro/<category>/<tool>/environment.yml
  • Generate the tests:
    • Edit ./modules/nf-neuro/<category>/<tool>/tests/main.nf.test
    • Run the tests to generate the main.nf.test.snap snapshots
  • Ensure the syntax is correct :
    • Run prettier and editorconfig-checker to fix common syntax issues
    • Run nf-core modules lint and fix all errors [1]
    • Ensure your variables have good, clear names

[1] Note: main_nf_ext_key and main_nf_meta_key failures remain. To my understanding, these are nf-core v4.0.3-only checks not enforced by the nf-neuro CI (which uses ≤3.5.2). The same patterns (ext.first_suffix, ext.use_label, ext.key_substrs_to_remove, meta.session, meta.run) exist in the already-merged stats/metricsinroi module. Please advise if a repo-wide fix is expected

Thank you for your time for reviewing this contribution!

Baptiste Pierrard and others added 19 commits July 22, 2026 12:28
…metrics

- New module stats/roivolumes: computes voxel count and volume (mm³) per
  WM bundle mask or per GM label region using nibabel/numpy (no external
  tools). Supports both binary-mask mode (WM) and label-atlas mode (GM).

- atlas_roimetrics subworkflow: adds optional GM Desikan parcellation ROI
  metrics (STATS_GM_ROIMETRICS) and optional volume computation for both
  WM bundles (STATS_WM_VOLUMES) and GM regions (STATS_GM_VOLUMES), gated
  by options.run_gm_roimetrics and options.run_roi_volumes respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace non-existent IFGWM_uni.nii.gz with IFGWM_labels_map.nii.gz
- Use checkIfExists:false for stub test (params.modules_testdata_base_path not set in nf-neuro repo)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…traction

When run_roi_metrics = false, STATS_METRICSINROI and STATS_GM_ROIMETRICS
are skipped. Atlas registration and volume computation (STATS_WM_VOLUMES,
STATS_GM_VOLUMES) still run as normal. This allows volumes-only runs
without waiting for diffusion metrics extraction.

Default is true for backward compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…i_metrics to meta.yml

Document the three options added with GM parcellation and volume features
so UTILS_OPTIONS recognises them and does not emit unknown-option warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tab bug

<<-END_VERSIONS strips leading tabs, not spaces. This module uses space
indentation so the END_VERSIONS terminator was leaked into versions.yml,
causing a YAML parse error at pipeline teardown. Write versions.yml
inside the Python block instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lumns

Previously, meta.id and meta.session were concatenated via ext.prefix,
and the BIDS session prefix from warped mask filenames bled into the
bundle/region name.

Changes:
- WM output: subject_id, session, bundle, volume_voxels, volume_mm3
  (bundle name stripped of session prefix from ANTSAPPLYTRANSFORMS output)
- GM output: subject_id, session, region, volume_voxels, volume_mm3
  (subject_id and session split from the fused prefix string)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sion

The 'sample' column contained the fused ext.prefix (e.g. sub-XXX_ses-YYYYMMDD).
Replace it with two standard BIDS columns:
  subject_id (meta.id) | session (meta.session) | roi | metrics...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add meta.run as a dedicated 'run' column in both metricsinroi and
roivolumes outputs. Column is empty when no run is present in meta,
which is the common case. This disambiguates datasets where multiple
T1/DWI runs exist within the same session.

Column order: subject_id | session | run | roi/bundle/region | values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ession stripping

- metricsinroi: transpose scil_volume_stats_in_labels JSON from metric-centric
  to region-centric so roi column contains GM regions and metric names become
  column headers (was inverted before)
- metricsinroi: rename header column subject_id -> sid
- metricsinroi: add outer-key normalization jq (strip leading _, desc-xxx__rest -> rest_xxx)
- roivolumes: rename header column subject_id -> sid
- roivolumes: fix session prefix stripping in WM bundle names (run substrs loop
  first to remove subject prefix, then strip session prefix from shorter key)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The scilpy container ships jq 1.5 which does not support elif.
Replace the outer-key normalization jq filter with nested if-else-end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The container's jq (1.5) does not support accessing named capture groups
via field syntax (.d, .rest) inside if-then branches. Replace with
split("__") which is universally supported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ables

- Add exit 1 to the missing-LUT guard so the process fails immediately
  with a clear message instead of continuing to the scil command
- Rename rois/metrics reassignment variables to roi_names/metric_names
  to avoid shadowing the process input path variables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d conda deps

Remove the hardcoded session-prefix stripping from WM bundle names (lines
that stripped session_id + "_" from the start of bundle names after
key_substrs_to_remove). This logic is pipeline-specific (depends on how
sf-tractomics names warped files when meta.id excludes the session) and
does not belong in a reusable module. Pipelines that need this behaviour
should add "${meta.session}_" to task.ext.key_substrs_to_remove.

Also populate environment.yml with nibabel>=4.0.0 and numpy>=1.24.0, the
actual runtime dependencies used by the module (previously empty, breaking
conda execution mode).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… test config

- Add docker.io/ registry prefix to container declaration
- Remove name field from environment.yml (fails nf-core schema validation)
- Add ext.prefix to test nextflow.config to prevent null_ in substrs
- Add generated nf-test snapshots

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…est config

- Add docker.io/ registry prefix to container declaration
- Remove name field from environment.yml (nf-core schema disallows it)
- Add ext.prefix to test nextflow.config to prevent null_ in substrs_to_remove

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
….yml

nf-core schema requires package=version (not >=).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@baptisp
baptisp requested a review from a team July 24, 2026 21:19

@AlexVCaron AlexVCaron left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Very nice work, eager to see it merged into the library ! I left a few comments, don't hesitate to reach out for any question !

tag "$meta.id"
label 'process_single'

container "docker.io/scilus/scilpy:2.2.2_cpu"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since you dabble without Scilpy, let's create a custom lightweight container for this module using Seqera Containers. Here's the built image to use : community.wave.seqera.io/library/nibabel_numpy:4148b92a8cbc6976

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You are right, thank you for the reference. I swapped the dockers and checked the output is identical.

Comment on lines +30 to +32
In WM mode (use_label = false): list of binary or weighted NIfTI mask files,
one per bundle. In GM mode (use_label = true): single NIfTI label atlas where
each integer value corresponds to a region defined in the Look-Up Table (LUT).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also add a description of the expected output name (aka how it's extracted from the filename) when this input is a list of rois (what you call WM mode).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, the WM/GM modes nomenclature should be generalized, it's not specific to the single context. It's atlas/labelmap mode versus labels mode. I really like that the module can handle both !

@baptisp baptisp Aug 22, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Also add a description of the expected output name (aka how it's extracted from the filename) when this input is a list of rois (what you call WM mode).

Added. The ROIs input description now spells out the rule for labels mode.
In practice: take the basename, strip the .nii / .nii.gz
extension, then remove each substring listed in key_substrs_to_remove from both the start and the end of what remains.
There's a worked example in the description with
key_substrs_to_remove = ["sub-01_", "_warped"], the file sub-01_AF_L_warped.nii.gz gives the ROI name AF_L.

Also, the WM/GM modes nomenclature should be generalized, it's not specific to the single context. It's atlas/labelmap mode versus labels mode. I really like that the module can handle both !

No problem, i updated the meta.yml description, both inputs, the output, all three args, the inline comments in
main.nf, and the two test case names. GM mode is now atlas/labelmap mode (use_label = true) and WM mode is labels
mode (use_label = false).
Atlas/labelmap mode counts regions inside one image whose integer voxel values encode them (a Desikan parcellation, a FreeSurfer segmentation, any LUT-driven atlas), while labels mode counts one file per ROI across a list of separate masks (tractography bundles, lesion masks, tissue masks). Basically the rename exists because the split is about how many files are handed in, not whether the tissue happens to be grey or white matter.

Also the csv outputs of both modes are unified into a single column under the name 'rois' ( instead of 'regions' or 'bundle' which were likely too specific).

ch_stats_mean = channel.empty()
ch_stats_std = channel.empty()

if (options.run_roi_metrics != false) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the reason for the negated check against false ?

@baptisp baptisp Aug 22, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

There is no reason. The key is always boolean. However, this check no longer exists since the key disappeared after my modifications for the generalization of the nomenclature.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be preferable the subworkflow takes an already registered atlas in inputs. This way, a user can configure its own registration flow the way he wants.

@baptisp baptisp Aug 22, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think you're right about the fact that the subworkflow currently welds two unrelated jobs together (getting the atlas into subject space, and computing statistics on it). It opens the door to duplicate registration works. However, I am not sure this PR should implement that. @levje is the author and maintainer of atlas_roimetrics, and this is a redesign of his interface rather than a fix to what I'm adding. I think this should be the object of a different PR.
For now, I added the registered atlas as an optional input, namely 'ch_registered_atlas', rather than modifying the whole flow.

Baptiste Pierrard added 5 commits August 22, 2026 15:48
The output CSV named its ROI column `bundle` in labels mode and `region` in
atlas/labelmap mode, so a consumer had to know which mode produced a file
before it could parse it. Both are now `roi`, making the emitted column set
identical in every case: sid, session, run, roi, volume_voxels, volume_mm3.

BREAKING CHANGE: the ROI column of *_volumes.csv is renamed from bundle/region to roi.
Adds an optional fourth input, ch_registered_atlas, carrying ROIs already in
each subject's space as a list of masks, a labelmap with its LUT, or both. It
is selected with a new use_registered_atlas option, mutually exclusive with
use_atlas_iit. On that path no atlas is downloaded and no registration runs,
so callers can bring their own registration flow.

Downstream statistics now read two provenance-neutral channels rather than the
transform outputs directly, which leaves behaviour under use_atlas_iit
unchanged — the four existing snapshots are byte-identical.

BREAKING CHANGE: ATLAS_ROIMETRICS takes four inputs instead of three. Callers
not supplying a registered atlas must pass channel.empty() at position 2.
…ut axes

The option set still named things after tissue rather than after ROI
representation, and it merged two independent decisions into one. Replace
run_gm_roimetrics, run_roi_metrics and run_roi_volumes with two lists:

  roi_sources: 'masks' and/or 'labelmap'
  roi_outputs: 'metrics' and/or 'volumes'

Four booleans become two options with full 2x2 selectivity. Each source is
warped only when requested, so asking for the labelmap alone no longer warps
the bundle masks. The labelmap gate no longer needs to be OR'd past for the
pre-registered atlas: both provenances read the same two axes.

Rename the process aliases accordingly. The transforms and the registration
only ever run under the IIT atlas, so they are scoped to it; the statistics
serve both provenances, so they are named after the representation they read.

  REGISTER_ATLAS_REF      -> REGISTER_IIT_REF
  TRANSFORM_ATLAS_BUNDLES -> TRANSFORM_IIT_BUNDLES
  TRANSFORM_GM_ATLAS      -> TRANSFORM_IIT_GM_ATLAS
  STATS_METRICSINROI      -> STATS_MASK_METRICS
  STATS_GM_ROIMETRICS     -> STATS_LABELMAP_METRICS
  STATS_WM_VOLUMES        -> STATS_MASK_VOLUMES
  STATS_GM_VOLUMES        -> STATS_LABELMAP_VOLUMES

BREAKING CHANGE: the emitted channels are renamed to mask_stats_json,
mask_stats_tab_mean, mask_stats_tab_std, mask_volumes and their labelmap_*
counterparts. Callers must update both their option map and their output
references, and any config selector naming the renamed processes.
Three leftovers used tissue words to describe behaviour that is not
tissue-specific.

atlas_iit's run_gm_roimetrics option computes no ROI metrics -- it decides
whether the GM Desikan atlas is fetched at all. Renamed to fetch_gm_desikan,
which keeps GM only where it is part of the artefact's name.

A comment in stats/metricsinroi still referred to "WM mode", and the
first_suffix example in stats/roivolumes used "atlas-iit-gm", tying the
module's documentation to one pipeline's tissue naming.

Occurrences that name what the IIT atlas actually distributes are left alone:
the GM Desikan filenames, the gm_atlas and gm_lut emits, and the atlas_iit_gm_*
options.

BREAKING CHANGE: the atlas_iit option run_gm_roimetrics is renamed to
fetch_gm_desikan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants