diff --git a/.devcontainer/devops/Dockerfile b/.devcontainer/devops/Dockerfile index 1ac4a3ec..31a62f14 100755 --- a/.devcontainer/devops/Dockerfile +++ b/.devcontainer/devops/Dockerfile @@ -67,3 +67,5 @@ RUN pipx ensurepath && \ # Export history for saving between container sessions RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" \ && echo "$SNIPPET" >> "/home/$USERNAME/.bashrc" + +ENTRYPOINT ["/bin/sh", "-lc", "pipx ensurepath && exec \"$@\""] diff --git a/.github/workflows/lint_component.yml b/.github/workflows/lint_component.yml index c6077000..18a8a2c6 100644 --- a/.github/workflows/lint_component.yml +++ b/.github/workflows/lint_component.yml @@ -45,33 +45,24 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: Wandalen/wretry.action@v3.7.2 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - attempt_delay: 5000 - action: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: | - python-version: "3.10" + python-version: "3.10" - uses: abatilo/actions-poetry@7b6d33e44b4f08d7021a1dee3c044e9c253d6439 # v3.0.0 with: poetry-version: "1.8.*" - name: Setup Java - uses: Wandalen/wretry.action@v3.7.2 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: - attempt_delay: 5000 - action: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: | - distribution: "temurin" - java-version: "17" + distribution: "temurin" + java-version: "17" - name: Setup nextflow - uses: Wandalen/wretry.action@v3.7.2 + uses: nf-core/setup-nextflow@561fcfc7146dcb12e3871909b635ab092a781f34 # v2.0.0 with: - attempt_delay: 5000 - action: nf-core/setup-nextflow@561fcfc7146dcb12e3871909b635ab092a781f34 # v2.0.0 - with: | - version: ${{ inputs.nextflow_version }} + version: ${{ inputs.nextflow_version }} - name: Install nf-core tools run: | diff --git a/.github/workflows/run_checks_suite.yml b/.github/workflows/run_checks_suite.yml index b8c0e4d3..434bccdf 100644 --- a/.github/workflows/run_checks_suite.yml +++ b/.github/workflows/run_checks_suite.yml @@ -6,7 +6,7 @@ on: description: "Nextflow version to use" required: false type: string - default: "24.04.4" + default: "25.04.6" nf_core_version: description: "nf-core version to use" required: false @@ -25,13 +25,10 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: Wandalen/wretry.action@v3.7.2 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - attempt_delay: 5000 - action: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: | - python-version: "3.11" - cache: "pip" + python-version: "3.11" + cache: "pip" - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 # FIXME Flip this off once we get to less than a couple hundred. Adding @@ -46,13 +43,10 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node - uses: Wandalen/wretry.action@v3.7.2 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - attempt_delay: 5000 - action: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: | - node-version: "20" - cache: "npm" + node-version: "20" + cache: "npm" - name: Install Prettier run: npm ci @@ -66,13 +60,10 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Node - uses: Wandalen/wretry.action@v3.7.2 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - attempt_delay: 5000 - action: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: | - node-version: "20" - cache: "npm" + node-version: "20" + cache: "npm" - name: Install editorconfig-checker run: npm ci @@ -193,13 +184,7 @@ jobs: - runner: scilus-docker-large path: modules/nf-neuro/betcrop/synthbet - runner: scilus-docker-large - path: modules/nf-neuro/registration/synthregistration - - runner: scilus-docker-large - path: subworkflows/nf-neuro/preproc_t1 - - runner: scilus-docker-large - path: subworkflows/nf-neuro/registration - - runner: scilus-docker-large - path: subworkflows/nf-neuro/anatomical_segmentation + path: modules/nf-neuro/registration/synthmorph exclude: - path: subworkflows/nf-neuro/load_test_data uses: ./.github/workflows/test_component.yml diff --git a/.github/workflows/test_component.yml b/.github/workflows/test_component.yml index 02f591ab..bd558f69 100644 --- a/.github/workflows/test_component.yml +++ b/.github/workflows/test_component.yml @@ -77,30 +77,22 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: Wandalen/wretry.action@v3.7.2 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - attempt_delay: 5000 - action: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: | - python-version: "3.11" - cache: "pip" + python-version: "3.11" + cache: "pip" - name: Setup Java - uses: Wandalen/wretry.action@v3.7.2 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: - attempt_delay: 5000 - action: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 - with: | - distribution: "temurin" - java-version: "17" + distribution: "temurin" + java-version: "17" - name: Setup nextflow - uses: Wandalen/wretry.action@v3.7.2 + uses: nf-core/setup-nextflow@561fcfc7146dcb12e3871909b635ab092a781f34 # v2.0.0 with: - attempt_delay: 5000 - action: nf-core/setup-nextflow@561fcfc7146dcb12e3871909b635ab092a781f34 # v2.0.0 - with: | - version: ${{ inputs.nextflow_version }} + version: ${{ inputs.nextflow_version }} + - uses: nf-core/setup-nf-test@fbd9d701dd1f41a38b151a737a0f12e97f3c4c56 # v1.3.5 with: version: ${{ inputs.nf_test_version }} @@ -197,27 +189,6 @@ jobs: retention-days: 1 compression-level: 9 - - name: Collect test working directories - if: failure() - run: | - mkdir -p tests_workdir - for t in ${{ env.NXF_WORKDIR }}/.nf-test/tests/**/work/**/.command.log - do - tag=$(sed -n '3p' $(dirname $t)/.command.run | cut -d' ' -f3 | tr -d "'") - cp -R $(dirname $t) tests_workdir/$tag - done - - - name: Upload test working directories - if: failure() - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 - with: - name: tests-workdir-${{ steps.test-run-identifier.outputs.uid }} - path: tests_workdir/ - overwrite: true - retention-days: 1 - compression-level: 9 - include-hidden-files: true - - name: Clean up if: always() run: | diff --git a/modules/nf-neuro/bundle/recognize/main.nf b/modules/nf-neuro/bundle/recognize/main.nf index 4e7b9b88..41506e06 100644 --- a/modules/nf-neuro/bundle/recognize/main.nf +++ b/modules/nf-neuro/bundle/recognize/main.nf @@ -23,13 +23,19 @@ process BUNDLE_RECOGNIZE { def rbx_processes = task.cpus ? "--processes " + task.cpus : "--processes 1" def outlier_alpha = task.ext.outlier_alpha ? "--alpha " + task.ext.outlier_alpha : "" """ + if [[ "$transform" == *.txt ]]; then + ConvertTransformFile 3 $transform transform.mat --convertToAffineType \ + && transform="transform.mat" \ + || echo "TXT transform file conversion failed, using original file." + fi + mkdir recobundles/ scil_tractogram_segment_with_bundleseg ${tractograms} ${config} ${directory}/ ${transform} --inverse --out_dir recobundles/ \ -v DEBUG $minimal_vote_ratio $seed $rbx_processes for bundle_file in recobundles/*.trk; do - bname=\$(basename \${bundle_file} .trk) - out_cleaned=${prefix}__\${bname}_cleaned.trk + bname=\$(basename \${bundle_file} .trk | sed 's/${prefix}_\\+//') + out_cleaned=${prefix}_\${bname}_cleaned.trk scil_bundle_reject_outliers \${bundle_file} "\${out_cleaned}" ${outlier_alpha} done @@ -46,7 +52,7 @@ process BUNDLE_RECOGNIZE { scil_bundle_reject_outliers -h # dummy output for single bundle - touch ${prefix}__AF_L_cleaned.trk + touch ${prefix}_AF_L_cleaned.trk cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/nf-neuro/bundle/recognize/tests/main.nf.test.snap b/modules/nf-neuro/bundle/recognize/tests/main.nf.test.snap index 1bb32334..ebae8040 100644 --- a/modules/nf-neuro/bundle/recognize/tests/main.nf.test.snap +++ b/modules/nf-neuro/bundle/recognize/tests/main.nf.test.snap @@ -8,11 +8,11 @@ "id": "test", "single_end": false }, - "test__bundle_1_cleaned.trk", - "test__bundle_2_cleaned.trk", - "test__bundle_3_cleaned.trk", - "test__bundle_4_cleaned.trk", - "test__bundle_5_cleaned.trk" + "test_bundle_1_cleaned.trk", + "test_bundle_2_cleaned.trk", + "test_bundle_3_cleaned.trk", + "test_bundle_4_cleaned.trk", + "test_bundle_5_cleaned.trk" ] ], "versions": [ @@ -21,10 +21,10 @@ } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.7" + "nf-test": "0.9.0", + "nextflow": "25.04.8" }, - "timestamp": "2025-09-17T17:40:02.174844814" + "timestamp": "2025-10-16T16:21:06.178446125" }, "bundle - recognize - stub-run": { "content": [ @@ -35,7 +35,7 @@ "id": "test", "single_end": false }, - "test__AF_L_cleaned.trk" + "test_AF_L_cleaned.trk" ] ], "versions": [ @@ -45,9 +45,9 @@ ], "meta": { "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nextflow": "25.04.8" }, - "timestamp": "2025-09-17T17:40:14.633526447" + "timestamp": "2025-10-16T16:21:40.42494296" }, "bundle - recognize - base": { "content": [ @@ -58,11 +58,11 @@ "id": "test", "single_end": false }, - "test__bundle_1_cleaned.trk", - "test__bundle_2_cleaned.trk", - "test__bundle_3_cleaned.trk", - "test__bundle_4_cleaned.trk", - "test__bundle_5_cleaned.trk" + "test_bundle_1_cleaned.trk", + "test_bundle_2_cleaned.trk", + "test_bundle_3_cleaned.trk", + "test_bundle_4_cleaned.trk", + "test_bundle_5_cleaned.trk" ] ], "versions": [ @@ -71,9 +71,9 @@ } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.7" + "nf-test": "0.9.0", + "nextflow": "25.04.8" }, - "timestamp": "2025-09-17T17:39:29.968449034" + "timestamp": "2025-10-16T16:20:14.719020953" } -} +} \ No newline at end of file diff --git a/modules/nf-neuro/registration/anattodwi/main.nf b/modules/nf-neuro/registration/anattodwi/main.nf index 5c8521e3..1520de47 100644 --- a/modules/nf-neuro/registration/anattodwi/main.nf +++ b/modules/nf-neuro/registration/anattodwi/main.nf @@ -5,23 +5,27 @@ process REGISTRATION_ANATTODWI { container "scilus/scilus:2.2.0" input: - tuple val(meta), path(t1), path(b0), path(metric) + tuple val(meta), path(fixed_reference), path(moving_anat), path(metric) output: - tuple val(meta), path("*0GenericAffine.mat") , emit: affine - tuple val(meta), path("*1Warp.nii.gz") , emit: warp - tuple val(meta), path("*1InverseWarp.nii.gz") , emit: inverse_warp - tuple val(meta), path("*t1_warped.nii.gz") , emit: t1_warped - tuple val(meta), path("*_registration_anattodwi_mqc.gif") , emit: mqc, optional: true - path "versions.yml" , emit: versions + tuple val(meta), path("*_warped.nii.gz") , emit: anat_warped + tuple val(meta), path("*_forward1_affine.mat") , emit: forward_affine + tuple val(meta), path("*_forward0_warp.nii.gz") , emit: forward_warp + tuple val(meta), path("*_backward1_warp.nii.gz") , emit: backward_warp + tuple val(meta), path("*_backward0_affine.mat") , emit: backward_affine + tuple val(meta), path("*_forward*.{nii.gz,mat}", arity: '1..2') , emit: forward_image_transform + tuple val(meta), path("*_backward*.{nii.gz,mat}", arity: '1..2') , emit: backward_image_transform + tuple val(meta), path("*_backward*.{nii.gz,mat}", arity: '1..2') , emit: forward_tractogram_transform + tuple val(meta), path("*_forward*.{nii.gz,mat}", arity: '1..2') , emit: backward_tractogram_transform + tuple val(meta), path("*_registration_anattodwi_mqc.gif") , emit: mqc, optional: true + path "versions.yml" , emit: versions when: - task.ext.when == null || task.ext.when + task.ext.when == null || task.ext.when script: def prefix = task.ext.prefix ?: "${meta.id}" - - def run_qc = task.ext.run_qc ? task.ext.run_qc : false + def run_qc = task.ext.run_qc as Boolean || false """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus @@ -30,34 +34,39 @@ process REGISTRATION_ANATTODWI { export ANTS_RANDOM_SEED=1234 antsRegistration --dimensionality 3 --float 0\ - --output [output,outputWarped.nii.gz,outputInverseWarped.nii.gz]\ + --output [forward,warped.nii.gz]\ --interpolation Linear --use-histogram-matching 0\ --winsorize-image-intensities [0.005,0.995]\ - --initial-moving-transform [$b0,$t1,1]\ + --initial-moving-transform [$fixed_reference,$moving_anat,1]\ --transform Rigid['0.2']\ - --metric MI[$b0,$t1,1,32,Regular,0.25]\ + --metric MI[$fixed_reference,$moving_anat,1,32,Regular,0.25]\ --convergence [500x250x125x50,1e-6,10] --shrink-factors 8x4x2x1\ --smoothing-sigmas 3x2x1x0\ --transform Affine['0.2']\ - --metric MI[$b0,$t1,1,32,Regular,0.25]\ + --metric MI[$fixed_reference,$moving_anat,1,32,Regular,0.25]\ --convergence [500x250x125x50,1e-6,10] --shrink-factors 8x4x2x1\ --smoothing-sigmas 3x2x1x0\ --transform SyN[0.1,3,0]\ - --metric MI[$b0,$t1,1,32]\ - --metric CC[$metric,$t1,1,4]\ + --metric MI[$fixed_reference,$moving_anat,1,32]\ + --metric CC[$metric,$moving_anat,1,4]\ --convergence [50x25x10,1e-6,10] --shrink-factors 4x2x1\ --smoothing-sigmas 3x2x1 - mv outputWarped.nii.gz ${prefix}__t1_warped.nii.gz - mv output0GenericAffine.mat ${prefix}__output0GenericAffine.mat - mv output1InverseWarp.nii.gz ${prefix}__output1InverseWarp.nii.gz - mv output1Warp.nii.gz ${prefix}__output1Warp.nii.gz + moving_id=\$(basename $moving_anat .nii.gz) + moving_id=\${moving_id#${meta.id}_*} + + mv warped.nii.gz ${prefix}_\${moving_id}_warped.nii.gz + mv forward0GenericAffine.mat ${prefix}_forward1_affine.mat + mv forward1Warp.nii.gz ${prefix}_forward0_warp.nii.gz + mv forward1InverseWarp.nii.gz ${prefix}_backward1_warp.nii.gz + + antsApplyTransforms -d 3 -t [${prefix}_forward1_affine.mat,1] \ + -o Linear[${prefix}_backward0_affine.mat] ### ** QC ** ### - if $run_qc; - then + if $run_qc; then # Extract dimensions. - dim=\$(mrinfo ${prefix}__t1_warped.nii.gz -size) + dim=\$(mrinfo ${prefix}_\${moving_id}_warped.nii.gz -size) read sagittal_dim coronal_dim axial_dim <<< "\${dim}" # Get middle slices. @@ -68,9 +77,12 @@ process REGISTRATION_ANATTODWI { # Set viz params. viz_params="--display_slice_number --display_lr --size 256 256" + # Get fixed ID, moving ID already computed + fixed_id=\$(basename $fixed_reference .nii.gz) + fixed_id=\${fixed_id#${meta.id}_*} + # Iterate over images. - for image in t1_warped b0; - do + for image in \${moving_id}_warped \${fixed_id}; do mrconvert *\${image}.nii.gz *\${image}_viz.nii.gz -stride -1,2,3 scil_viz_volume_screenshot *\${image}_viz.nii.gz \${image}_coronal.png \ --slices \$coronal_mid --axis coronal \$viz_params @@ -79,11 +91,10 @@ process REGISTRATION_ANATTODWI { scil_viz_volume_screenshot *\${image}_viz.nii.gz \${image}_axial.png \ --slices \$axial_mid --axis axial \$viz_params - if [ \$image != b0 ]; - then - title="Warped T1" + if [ \$image != \${fixed_id} ]; then + title="Warped \${moving_id^^}" else - title="Reference B0" + title="Reference \${fixed_id^^}" fi convert +append \${image}_coronal*.png \${image}_axial*.png \ @@ -97,50 +108,52 @@ process REGISTRATION_ANATTODWI { # Create GIF. convert -delay 10 -loop 0 -morph 10 \ - t1_warped_mosaic.png b0_mosaic.png t1_warped_mosaic.png \ + \${moving_id}_warped_mosaic.png \${fixed_id}_mosaic.png \${moving_id}_warped_mosaic.png \ ${prefix}_registration_anattodwi_mqc.gif # Clean up. - rm t1_warped_mosaic.png b0_mosaic.png + rm \${moving_id}_warped_mosaic.png \${fixed_id}_mosaic.png fi cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*: v?([0-9.a-zA-Z-]+).*/\\1/') + imagemagick: \$(convert -version | grep "Version:" | sed -E 's/.*ImageMagick ([0-9.-]+).*/\\1/') mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') - imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ stub: def prefix = task.ext.prefix ?: "${meta.id}" + def run_qc = task.ext.run_qc as Boolean || false """ - set +e - function handle_code () { - local code=\$? - ignore=( 1 ) - [[ " \${ignore[@]} " =~ " \$code " ]] || exit \$code - } - trap 'handle_code' ERR - antsRegistration -h + antsApplyTransforms -h + mrconvert -h scil_viz_volume_screenshot -h - convert -h + convert -help . + + moving_id=\$(basename $moving_anat .nii.gz) + moving_id=\${moving_id#${meta.id}_*} + + touch ${prefix}_\${moving_id}_warped.nii.gz + touch ${prefix}_forward1_affine.mat + touch ${prefix}_forward0_warp.nii.gz + touch ${prefix}_backward1_warp.nii.gz + touch ${prefix}_backward0_affine.mat - touch ${prefix}__t1_warped.nii.gz - touch ${prefix}__output0GenericAffine.mat - touch ${prefix}__output1InverseWarp.nii.gz - touch ${prefix}__output1Warp.nii.gz - touch ${prefix}__registration_anattodwi_mqc.gif + if $run_qc; then + touch ${prefix}_registration_anattodwi_mqc.gif + fi cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*: v?([0-9.a-zA-Z-]+).*/\\1/') + imagemagick: \$(convert -version | grep "Version:" | sed -E 's/.*ImageMagick ([0-9.-]+).*/\\1/') mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') - imagemagick: \$(convert -version | sed -n 's/.*ImageMagick \\([0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\.[0-9]\\{1,\\}\\).*/\\1/p') + scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-neuro/registration/anattodwi/meta.yml b/modules/nf-neuro/registration/anattodwi/meta.yml index aa5e0681..0f269050 100644 --- a/modules/nf-neuro/registration/anattodwi/meta.yml +++ b/modules/nf-neuro/registration/anattodwi/meta.yml @@ -1,6 +1,11 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json name: registration_anattodwi -description: Anatomical image registration on a diffusion image. +description: | + Registration computing transformations moving images from anatomical space into diffusion space. The `moving_anat` image + represents the anatomical space, moving to diffusion space, onto its `fixed_reference` and its `metric` representations. + + Configuration, direction and order of the registration follows the [tractoflow](https://doi.org/10.1016/j.neuroimage.2020.116889) + publication. keywords: - nifti - registration @@ -8,82 +13,166 @@ keywords: - dwi tools: - ANTs: - description: Advanced Normalization Tools (ANTs) for image processing. - homepage: http://stnava.github.io/ANTs/ + description: "Advanced Normalization Tools." + homepage: "https://github.com/ANTsX/ANTs" + documentation: "http://stnava.github.io/ANTsDoc/" doi: "10.1038/s41598-021-87564-6" - identifier: "" + - ImageMagick: + description: "ImageMagick is a software suite to create, edit, compose, or convert bitmap images." + homepage: "https://imagemagick.org/" + documentation: "https://imagemagick.org/script/command-line-processing.php" + - MRtrix3: + description: "MRtrix3 is a software package for processing diffusion MRI data." + homepage: "https://www.mrtrix3.org/" + documentation: "https://mrtrix.readthedocs.io/en/latest/" + doi: "10.1016/j.neuroimage.2019.116137" + - Scilpy: + description: "Scilpy is a Python library for processing diffusion MRI data." + homepage: "https://github.com/scilus/scilpy" + documentation: "https://scilpy.readthedocs.io/en/latest/" +args: + - run_qc: + type: boolean + description: "Run quality control for the registration process" + default: false input: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - t1: + - fixed_reference: type: file - description: Nifti image file to register on dwi + description: Nifti image file - fixed DWI reference (usually b0) pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - b0: + - moving_anat: type: file - description: Nifti image file - b0 + description: Nifti image file - moving anat to register (usually T1w) pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - metric: type: file - description: Nifti image file metric used to register (fa) + description: Nifti image file - additional fixed metric (usually FA) pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format output: - affine: + anat_warped: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*0GenericAffine.mat": + - "*_warped.nii.gz": type: file - description: Affine transformation matrix. - pattern: "*0GenericAffine.mat" + description: Anatomical image warped to DWI space + pattern: "*_warped.nii.gz" + ontologies: + - edam: http://edamontology.org/format_4001 # NIFTI format + forward_affine: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward1_affine.mat": + type: file + description: Affine transformation matrix to fixed space. + pattern: "*_forward1_affine.mat" ontologies: [] - warp: + forward_warp: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*1Warp.nii.gz": + - "*_forward0_warp.nii.gz": type: file - description: Warp field. - pattern: "*1Warp.{nii,nii.gz}" + description: Deformation field to fixed space. + pattern: "*_forward0_warp.nii.gz" ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - inverse_warp: + backward_warp: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*1InverseWarp.nii.gz": + - "*_backward1_warp.nii.gz": type: file - description: Inverse warp field. - pattern: "*1InverseWarp.{nii,nii.gz}" + description: Deformation field to moving space. + pattern: "*_backward1_warp.nii.gz" ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - t1_warped: + backward_affine: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*t1_warped.nii.gz": + - "*_backward0_affine.mat": type: file - description: Anatomical T1 warped to dwi space. - pattern: "*t1_warped.{nii,nii.gz}" - ontologies: - - edam: http://edamontology.org/format_4001 # NIFTI format + description: Affine transformation matrix to moving space. + pattern: "*_backward0_affine.mat" + ontologies: [] + forward_image_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward*.{nii.gz,mat}": + type: list + description: | + Tuple, Transformation files to warp images in fixed space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ forward_warp, forward_affine ] ]. + pattern: "*_forward*.{nii.gz,mat}" + ontologies: [] + backward_image_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward*.{nii.gz,mat}": + type: list + description: | + Tuple, transformation files to warp images in moving space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ backward_affine, backward_warp ] ]. + pattern: "*_backward*.{nii.gz,mat}" + ontologies: [] + forward_tractogram_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward*.{nii.gz,mat}": + type: list + description: | + Tuple, transformation files to warp tractograms into fixed space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ backward_affine, backward_warp ] ]. + pattern: "*_backward*.{nii.gz,mat}" + ontologies: [] + backward_tractogram_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward*.{nii.gz,mat}": + type: list + description: | + Tuple, transformation files to warp tractograms into moving space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ forward_affine, forward_warp ] ]. + pattern: "*_forward*.{nii.gz,mat}" + ontologies: [] mqc: - - meta: type: map @@ -96,6 +185,7 @@ output: .gif file containing quality control image for the registration process. For use in MultiQC report. pattern: "*_registration_anattodwi_mqc.gif" + optional: true ontologies: [] versions: - versions.yml: @@ -104,5 +194,7 @@ output: pattern: versions.yml ontologies: - edam: http://edamontology.org/format_3750 # YAML + authors: - "@medde" + - "@AlexVCaron" diff --git a/modules/nf-neuro/registration/anattodwi/tests/main.nf.test b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test index 0b675aa2..bc9f06d0 100644 --- a/modules/nf-neuro/registration/anattodwi/tests/main.nf.test +++ b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test @@ -53,8 +53,8 @@ nextflow_process { file("\${test_data_directory}/fa.nii.gz") ] } - input[0] = ch_t1w - .join(ch_b0) + input[0] = ch_b0 + .join(ch_t1w) .join(ch_fa) """ } @@ -98,8 +98,8 @@ nextflow_process { file("\${test_data_directory}/fa.nii.gz") ] } - input[0] = ch_t1w - .join(ch_b0) + input[0] = ch_b0 + .join(ch_t1w) .join(ch_fa) """ } diff --git a/modules/nf-neuro/registration/anattodwi/tests/main.nf.test.snap b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test.snap index 437f536f..b9ca0127 100644 --- a/modules/nf-neuro/registration/anattodwi/tests/main.nf.test.snap +++ b/modules/nf-neuro/registration/anattodwi/tests/main.nf.test.snap @@ -8,7 +8,7 @@ "id": "test", "single_end": false }, - "test__output0GenericAffine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" + "test_T1w_warped.nii.gz:md5,39c30c6251cba224989bc74a31540121" ] ], "1": [ @@ -17,16 +17,19 @@ "id": "test", "single_end": false }, - "test__output1Warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5" + "test_forward1_affine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" ] ], + "10": [ + "versions.yml:md5,f7db1ae1e2dd017daa92c91fbd41a28f" + ], "2": [ [ { "id": "test", "single_end": false }, - "test__output1InverseWarp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" + "test_forward0_warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5" ] ], "3": [ @@ -35,7 +38,7 @@ "id": "test", "single_end": false }, - "test__t1_warped.nii.gz:md5,39c30c6251cba224989bc74a31540121" + "test_backward1_warp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" ] ], "4": [ @@ -44,78 +47,189 @@ "id": "test", "single_end": false }, - "test_registration_anattodwi_mqc.gif:md5,687252d31b0109f99a174b4e3960e4e6" + "test_backward0_affine.mat:md5,13ea200e4b163f8a47038ff61524cb1b" ] ], "5": [ - "versions.yml:md5,f49f4ab58360830a5cdb39d7530b45b1" + [ + { + "id": "test", + "single_end": false + }, + [ + "test_forward0_warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5", + "test_forward1_affine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" + ] + ] ], - "affine": [ + "6": [ [ { "id": "test", "single_end": false }, - "test__output0GenericAffine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" + [ + "test_backward0_affine.mat:md5,13ea200e4b163f8a47038ff61524cb1b", + "test_backward1_warp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" + ] ] ], - "inverse_warp": [ + "7": [ [ { "id": "test", "single_end": false }, - "test__output1InverseWarp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" + [ + "test_backward0_affine.mat:md5,13ea200e4b163f8a47038ff61524cb1b", + "test_backward1_warp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" + ] ] ], - "mqc": [ + "8": [ [ { "id": "test", "single_end": false }, - "test_registration_anattodwi_mqc.gif:md5,687252d31b0109f99a174b4e3960e4e6" + [ + "test_forward0_warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5", + "test_forward1_affine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" + ] ] ], - "t1_warped": [ + "9": [ [ { "id": "test", "single_end": false }, - "test__t1_warped.nii.gz:md5,39c30c6251cba224989bc74a31540121" + "test_registration_anattodwi_mqc.gif:md5,9cbc1ce8755821996dae18080cf0a23c" ] ], - "versions": [ - "versions.yml:md5,f49f4ab58360830a5cdb39d7530b45b1" + "anat_warped": [ + [ + { + "id": "test", + "single_end": false + }, + "test_T1w_warped.nii.gz:md5,39c30c6251cba224989bc74a31540121" + ] + ], + "backward_affine": [ + [ + { + "id": "test", + "single_end": false + }, + "test_backward0_affine.mat:md5,13ea200e4b163f8a47038ff61524cb1b" + ] + ], + "backward_image_transform": [ + [ + { + "id": "test", + "single_end": false + }, + [ + "test_backward0_affine.mat:md5,13ea200e4b163f8a47038ff61524cb1b", + "test_backward1_warp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" + ] + ] + ], + "backward_tractogram_transform": [ + [ + { + "id": "test", + "single_end": false + }, + [ + "test_forward0_warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5", + "test_forward1_affine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" + ] + ] + ], + "backward_warp": [ + [ + { + "id": "test", + "single_end": false + }, + "test_backward1_warp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" + ] ], - "warp": [ + "forward_affine": [ [ { "id": "test", "single_end": false }, - "test__output1Warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5" + "test_forward1_affine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" ] + ], + "forward_image_transform": [ + [ + { + "id": "test", + "single_end": false + }, + [ + "test_forward0_warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5", + "test_forward1_affine.mat:md5,18d03f6c1fd587b00b3bc9363eb8ed4f" + ] + ] + ], + "forward_tractogram_transform": [ + [ + { + "id": "test", + "single_end": false + }, + [ + "test_backward0_affine.mat:md5,13ea200e4b163f8a47038ff61524cb1b", + "test_backward1_warp.nii.gz:md5,6a04d7c100075b4ba7a276a55a4094bd" + ] + ] + ], + "forward_warp": [ + [ + { + "id": "test", + "single_end": false + }, + "test_forward0_warp.nii.gz:md5,aa42d4dd6f227d986cd99df45425d5c5" + ] + ], + "mqc": [ + [ + { + "id": "test", + "single_end": false + }, + "test_registration_anattodwi_mqc.gif:md5,9cbc1ce8755821996dae18080cf0a23c" + ] + ], + "versions": [ + "versions.yml:md5,f7db1ae1e2dd017daa92c91fbd41a28f" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-09T20:08:38.758050028" + "timestamp": "2025-10-28T12:57:20.84201822" }, "registration - anattodwi -stub-run": { "content": [ [ - "versions.yml:md5,f49f4ab58360830a5cdb39d7530b45b1" + "versions.yml:md5,f7db1ae1e2dd017daa92c91fbd41a28f" ] ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-09T20:08:45.279539751" + "timestamp": "2025-10-28T12:57:40.842368795" } } \ No newline at end of file diff --git a/modules/nf-neuro/registration/ants/main.nf b/modules/nf-neuro/registration/ants/main.nf index 633f1937..6ca63ac1 100644 --- a/modules/nf-neuro/registration/ants/main.nf +++ b/modules/nf-neuro/registration/ants/main.nf @@ -6,66 +6,74 @@ process REGISTRATION_ANTS { container "scilus/scilus:2.2.0" input: - tuple val(meta), path(fixedimage), path(movingimage), path(mask) /* optional, input = [] */ + tuple val(meta), path(fixed_image), path(moving_image), path(mask) //** optional, input = [] **// output: - tuple val(meta), path("*_warped.nii.gz") , emit: image - tuple val(meta), path("*__output0GenericAffine.mat") , emit: affine - tuple val(meta), path("*__output1InverseAffine.mat") , emit: inverse_affine - tuple val(meta), path("*__output1Warp.nii.gz") , emit: warp, optional:true - tuple val(meta), path("*__output0InverseWarp.nii.gz") , emit: inverse_warp, optional: true - tuple val(meta), path("*_registration_ants_mqc.gif") , emit: mqc, optional: true - path "versions.yml" , emit: versions + tuple val(meta), path("*_warped.nii.gz") , emit: image_warped + tuple val(meta), path("*_forward1_affine.mat") , emit: forward_affine, optional: true + tuple val(meta), path("*_forward0_warp.nii.gz") , emit: forward_warp, optional: true + tuple val(meta), path("*_backward1_warp.nii.gz") , emit: backward_warp, optional: true + tuple val(meta), path("*_backward0_affine.mat") , emit: backward_affine, optional: true + tuple val(meta), path("*_forward*.{nii.gz,mat}", arity: '1..2') , emit: forward_image_transform + tuple val(meta), path("*_backward*.{nii.gz,mat}", arity: '1..2') , emit: backward_image_transform + tuple val(meta), path("*_backward*.{nii.gz,mat}", arity: '1..2') , emit: forward_tractogram_transform + tuple val(meta), path("*_forward*.{nii.gz,mat}", arity: '1..2') , emit: backward_tractogram_transform + tuple val(meta), path("*_registration_ants_mqc.gif") , emit: mqc, optional: true + path "versions.yml" , emit: versions when: - task.ext.when == null || task.ext.when + task.ext.when == null || task.ext.when script: + def initialization_types = ["geometric center": 0, "intensities": 1, "origin": 2] def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" - def suffix_qc = task.ext.suffix_qc ? "${task.ext.suffix_qc}" : "" - def ants = task.ext.quick ? "antsRegistrationSyNQuick.sh " : "antsRegistrationSyN.sh " - def dimension = task.ext.dimension ? "-d " + task.ext.dimension : "-d 3" - def transform = task.ext.transform ? task.ext.transform : "s" - def seed = task.ext.random_seed ? " -e " + task.ext.random_seed : "-e 1234" - def run_qc = task.ext.run_qc ? task.ext.run_qc : false - - if ( task.ext.threads ) args += "-n " + task.ext.threads - if ( task.ext.initial_transform ) args += " -i " + task.ext.initial_transform - if ( task.ext.histogram_bins ) args += " -r " + task.ext.histogram_bins - if ( task.ext.spline_distance ) args += " -s " + task.ext.spline_distance - if ( task.ext.gradient_step ) args += " -g " + task.ext.gradient_step - if ( task.ext.mask ) args += " -x $mask" - if ( task.ext.type ) args += " -p " + task.ext.type - if ( task.ext.histogram_matching ) args += " -j " + task.ext.histogram_matching - if ( task.ext.repro_mode ) args += " -y " + task.ext.repro_mode - if ( task.ext.collapse_output ) args += " -z " + task.ext.collapse_output + def suffix_qc = task.ext.suffix_qc ?: "" + def ants = task.ext.quick ? "antsRegistrationSyNQuick.sh" : "antsRegistrationSyN.sh" + def dimension = "-d ${task.ext.dimension ?: 3}" + def transform = task.ext.transform ?: "s" + def seed = " -e ${task.ext.random_seed ?: 1234}" + def run_qc = task.ext.run_qc as Boolean || false + + args += " -n $task.cpus" + if ( mask ) args += " -x $mask" + if ( task.ext.initial_transform ) args += " -i [$fixed_image,$moving_image,${initialization_types[task.ext.initial_transform]}]" + if ( task.ext.histogram_bins ) args += " -r $task.ext.histogram_bins" + if ( task.ext.spline_distance ) args += " -s $task.ext.spline_distance" + if ( task.ext.gradient_step ) args += " -g $task.ext.gradient_step" + if ( task.ext.precision ) args += " -p $task.ext.precision" + if ( task.ext.histogram_matching ) args += " -j $task.ext.histogram_matching" + if ( task.ext.repro_mode ) args += " -y $task.ext.repro_mode" + if ( task.ext.collapse_output ) args += " -z $task.ext.collapse_output" """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - $ants $dimension -f $fixedimage -m $movingimage -o output -t $transform $args $seed + $ants $dimension -f $fixed_image -m $moving_image -o output -t $transform $args $seed - mv outputWarped.nii.gz ${prefix}__warped.nii.gz - mv output0GenericAffine.mat ${prefix}__output0GenericAffine.mat + moving_id=\$(basename $moving_image .nii.gz) + moving_id=\${moving_id#${meta.id}_*} - if [ $transform != "t" ] && [ $transform != "r" ] && [ $transform != "a" ]; - then - mv output1InverseWarp.nii.gz ${prefix}__output0InverseWarp.nii.gz - mv output1Warp.nii.gz ${prefix}__output1Warp.nii.gz + mv outputWarped.nii.gz ${prefix}_\${moving_id}_warped.nii.gz + + if [ $transform != "bo" ] && [ $transform != "so" ]; then + mv output0GenericAffine.mat ${prefix}_forward1_affine.mat + fi + + if [ $transform != "t" ] && [ $transform != "r" ] && [ $transform != "a" ]; then + mv output1InverseWarp.nii.gz ${prefix}_backward1_warp.nii.gz + mv output1Warp.nii.gz ${prefix}_forward0_warp.nii.gz fi - antsApplyTransforms -d 3 -i $fixedimage -r $movingimage \ - -o Linear[${prefix}__output1InverseAffine.mat] \ - -t [${prefix}__output0GenericAffine.mat,1] + antsApplyTransforms -d 3 -t [${prefix}_forward1_affine.mat,1] \ + -o Linear[${prefix}_backward0_affine.mat] ### ** QC ** ### - if $run_qc; - then - mv $fixedimage fixedimage.nii.gz - extract_dim=\$(mrinfo fixedimage.nii.gz -size) + if $run_qc; then + mv $fixed_image fixed_image.nii.gz + extract_dim=\$(mrinfo fixed_image.nii.gz -size) read sagittal_dim coronal_dim axial_dim <<< "\${extract_dim}" # Get the middle slice @@ -73,11 +81,14 @@ process REGISTRATION_ANTS { axial_dim=\$((\$axial_dim / 2)) sagittal_dim=\$((\$sagittal_dim / 2)) + # Get fixed ID, moving ID already computed + fixed_id=\$(basename $fixed_image .nii.gz) + fixed_id=\${fixed_id#${meta.id}_*} + # Set viz params. viz_params="--display_slice_number --display_lr --size 256 256" # Iterate over images. - for image in fixedimage warped; - do + for image in fixed_image warped; do mrconvert *\${image}.nii.gz *\${image}_viz.nii.gz -stride -1,2,3 scil_viz_volume_screenshot *\${image}_viz.nii.gz \${image}_coronal.png \ --slices \$coronal_dim --axis coronal \$viz_params @@ -85,66 +96,79 @@ process REGISTRATION_ANTS { --slices \$sagittal_dim --axis sagittal \$viz_params scil_viz_volume_screenshot *\${image}_viz.nii.gz \${image}_axial.png \ --slices \$axial_dim --axis axial \$viz_params - if [ \$image != fixedimage ]; - then - title="T1 Warped" + + if [ \$image != fixed_image ]; then + title="Warped \${moving_id^^}" else - title="fixedimage" + title="Reference \${fixed_id^^}" fi + convert +append \${image}_coronal*.png \${image}_axial*.png \ \${image}_sagittal*.png \${image}_mosaic.png convert -annotate +20+230 "\${title}" -fill white -pointsize 30 \ \${image}_mosaic.png \${image}_mosaic.png + # Clean up. rm \${image}_coronal*.png \${image}_sagittal*.png \${image}_axial*.png done + # Create GIF. convert -delay 10 -loop 0 -morph 10 \ - warped_mosaic.png fixedimage_mosaic.png warped_mosaic.png \ + warped_mosaic.png fixed_image_mosaic.png warped_mosaic.png \ ${prefix}_${suffix_qc}_registration_ants_mqc.gif + # Clean up. rm *_mosaic.png fi cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*: v?([0-9.a-zA-Z-]+).*/\\1/') - mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') imagemagick: \$(convert -version | grep "Version:" | sed -E 's/.*ImageMagick ([0-9.-]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') + scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ stub: - def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" + def suffix_qc = task.ext.suffix_qc ?: "" + def run_qc = task.ext.run_qc as Boolean || false """ set +e function handle_code () { - local code=\$? - ignore=( 1 ) - [[ " \${ignore[@]} " =~ " \$code " ]] || exit \$code + local code=\$? + ignore=( 1 ) + [[ " \${ignore[@]} " =~ " \$code " ]] || exit \$code + } + + # Local trap to ignore awaited non-zero exit codes + { + trap 'handle_code' ERR + antsRegistrationSyNQuick.sh -h } - trap 'handle_code' ERR - antsRegistrationSyNQuick.sh -h antsApplyTransforms -h - convert -h + convert -help . scil_viz_volume_screenshot -h - touch ${prefix}__t1_warped.nii.gz - touch ${prefix}__output0GenericAffine.mat - touch ${prefix}__output1InverseAffine.mat - touch ${prefix}__output0InverseWarp.nii.gz - touch ${prefix}__output1Warp.nii.gz + touch ${prefix}_t1_warped.nii.gz + touch ${prefix}_forward1_affine.mat + touch ${prefix}_forward0_warp.nii.gz + touch ${prefix}_backward1_warp.nii.gz + touch ${prefix}_backward0_affine.mat + + if $run_qc; then + touch ${prefix}_${suffix_qc}_registration_ants_mqc.gif + fi cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*: v?([0-9.a-zA-Z-]+).*/\\1/') - mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') imagemagick: \$(convert -version | grep "Version:" | sed -E 's/.*ImageMagick ([0-9.-]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') + scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-neuro/registration/ants/meta.yml b/modules/nf-neuro/registration/ants/meta.yml index 73657632..9ff787d9 100644 --- a/modules/nf-neuro/registration/ants/meta.yml +++ b/modules/nf-neuro/registration/ants/meta.yml @@ -1,45 +1,155 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json -name: registration_ants -description: Image registration with antsRegistrationSyN or antsRegistrationSyNQuick +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/scilus/nf-neuro/main/modules/meta-schema.json +name: "registration_ants" +description: | + Image registration with antsRegistrationSyN or antsRegistrationSyNQuick. + + Defaults: 3D images and 3-stage registration (rigid + affine + deformable) + + Main features: + (1) Supports multiple transformation types (see `transform` argument). + (2) Supports initial transformations (see `initial_transform` argument) via a closure, e.g. `{ [$fixedimage,$movingimage,0] }`. + (3) Automatic creation of backward (inverse) transformations matrices. + (4) Curated combined output transformations for REGISTRATION_ANTSAPPLYTRANSFORMS and REGISTRATION_TRANSFORMTRACTOGRAM + processes : `forward_image_transform`, `forward_tractogram_transform`, and their `backward` versions. + (5) Quality control (QC) image generation for MultiQC reports. keywords: - nifti - registration - - SyN - - SyNQuick - - trk + - antsRegistrationSyN + - antsRegistrationSyNQuick tools: - ANTs: - description: Advanced Normalization Tools (ANTs) for image processing. - homepage: http://stnava.github.io/ANTs/ + description: "Advanced Normalization Tools." + homepage: "https://github.com/ANTsX/ANTs" + documentation: "http://stnava.github.io/ANTsDoc/" doi: "10.1038/s41598-021-87564-6" - identifier: "" + - ImageMagick: + description: "ImageMagick is a software suite to create, edit, compose, or convert bitmap images." + homepage: "https://imagemagick.org/" + documentation: "https://imagemagick.org/script/command-line-processing.php" + - MRtrix3: + description: "MRtrix3 is a software package for processing diffusion MRI data." + homepage: "https://www.mrtrix3.org/" + documentation: "https://mrtrix.readthedocs.io/en/latest/" + doi: "10.1016/j.neuroimage.2019.116137" + - Scilpy: + description: "Scilpy is a Python library for processing diffusion MRI data." + homepage: "https://github.com/scilus/scilpy" + documentation: "https://scilpy.readthedocs.io/en/latest/" +args: + - quick: + type: boolean + description: "Use antsRegistrationSyNQuick instead of antsRegistrationSyN." + default: false + - repro_mode: + type: boolean + description: "Run in reproducibility mode (single threaded)." + default: false + - histogram_matching: + type: boolean + description: "Perform histogram matching between images before registration." + default: false + - transform: + type: string + description: | + Type of transformation to perform : + - t : translation (1 stage) + - r : rigid (1 stage) + - a : rigid + affine (2 stages) + - s : rigid + affine + deformable syn (3 stages) + - sr : rigid + deformable syn (2 stages) + - so : deformable syn only (1 stage) + - b : rigid + affine + deformable b-spline syn (3 stages) + - br : rigid + deformable b-spline syn (2 stages) + - bo : deformable b-spline syn only (1 stage) + default: "s" + choices: + - t + - r + - a + - s + - sr + - so + - b + - br + - bo + - initial_transform: + type: string + description: Algorithmic initialization by geometric center, intensities, or origin. + default: "" + choices: + - geometric center + - intensities + - origin + - dimension: + type: int + description: "Number of spatial dimensions of the images" + default: 3 + - gradient_step: + type: float + description: "Gradient step size for the optimization of SyN and B-spline SyN." + default: 0.1 + - histogram_bins: + type: int + description: "Number of histogram bins when using the Mutual Information metric with SyN and B-spline SyN." + default: 32 + - spline_distance: + type: float + description: "Distance between control points for B-spline SyN." + default: 26 + - run_qc: + type: boolean + description: "Run quality control (QC) to generate a MultiQC report." + default: false + - suffix_qc: + type: string + description: "Suffix for the QC image file." + default: "" + - precision: + type: string + description: "Precision of the output image." + default: "float" + choices: + - float + - double + - collapse_output: + type: boolean + description: "Collapse output transformations into a single file." + default: false + - random_seed: + type: int + description: "Random seed for reproducibility." + default: 1234 input: - # Only when we have meta - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - fixedimage: + - fixed_image: type: file description: Fixed image(s) or source image(s) or reference image(s) pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - movingimage: + - moving_image: type: file description: Moving image(s) or target image(s) pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - mask: type: file description: Mask(s) for the fixed image space pattern: "*.{nii,nii.gz}" + mandatory: false ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format output: - image: + image_warped: - - meta: type: map description: | @@ -51,52 +161,108 @@ output: pattern: "*_warped.nii.gz" ontologies: - edam: http://edamontology.org/format_3989 # GZIP format - affine: + forward_affine: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*__output0GenericAffine.mat": + - "*_forward1_affine.mat": type: file description: Affine transformation from moving to fixed - pattern: "*__output0GenericAffine.mat" + pattern: "*_forward1_affine.mat" + optional: true ontologies: [] - inverse_affine: + forward_warp: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*__output1InverseAffine.mat": + - "*_forward0_warp.nii.gz": type: file - description: Affine transformation from fixed to moving - pattern: "*__output1InverseAffine.mat" - ontologies: [] - warp: + description: Nifti volume containing warp field from moving to fixed + pattern: "*_forward0_warp.nii.gz" + optional: true + ontologies: + - edam: http://edamontology.org/format_3989 # GZIP format + backward_warp: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*__output1Warp.nii.gz": + - "*_backward1_warp.nii.gz": type: file - description: Nifti volume containing warp field from moving to fixed - pattern: "*__output1Warp.nii.gz" + description: Nifti volume containing warp field from fixed to moving + pattern: "*_backward1_warp.nii.gz" + optional: true ontologies: - edam: http://edamontology.org/format_3989 # GZIP format - inverse_warp: + backward_affine: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*__output0InverseWarp.nii.gz": + - "*_backward0_affine.mat": type: file - description: Nifti volume containing warp field from fixed to moving - pattern: "*__output0InverseWarp.nii.gz" - ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format + description: Affine transformation from fixed to moving + pattern: "*_backward0_affine.mat" + optional: true + ontologies: [] + forward_image_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward*.{nii.gz,mat}": + type: list + description: | + Tuple, Transformation files to warp images in fixed space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ forward_warp, forward_affine ] ]. + pattern: "*_forward*.{nii.gz,mat}" + ontologies: [] + backward_image_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward*.{nii.gz,mat}": + type: list + description: | + Tuple, transformation files to warp images in moving space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ backward_affine, backward_warp ] ]. + pattern: "*_backward*.{nii.gz,mat}" + ontologies: [] + forward_tractogram_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward*.{nii.gz,mat}": + type: list + description: | + Tuple, transformation files to warp tractograms into fixed space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ backward_affine, backward_warp ] ]. + pattern: "*_backward*.{nii.gz,mat}" + ontologies: [] + backward_tractogram_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward*.{nii.gz,mat}": + type: list + description: | + Tuple, transformation files to warp tractograms into moving space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ forward_affine, forward_warp ] ]. + pattern: "*_forward*.{nii.gz,mat}" + ontologies: [] mqc: - - meta: type: map @@ -108,6 +274,7 @@ output: description: .gif file containing quality control image for the registration process. Made for use in MultiQC report. pattern: "*_registration_ants_mqc.gif" + optional: true ontologies: [] versions: - versions.yml: @@ -118,3 +285,4 @@ output: - edam: http://edamontology.org/format_3750 # YAML authors: - "@ThoumyreStanislas" + - "@AlexVCaron" diff --git a/modules/nf-neuro/registration/ants/tests/main.nf.test b/modules/nf-neuro/registration/ants/tests/main.nf.test index 41c856ad..df3133e0 100644 --- a/modules/nf-neuro/registration/ants/tests/main.nf.test +++ b/modules/nf-neuro/registration/ants/tests/main.nf.test @@ -3,6 +3,7 @@ nextflow_process { name "Test Process REGISTRATION_ANTS" script "../main.nf" process "REGISTRATION_ANTS" + config "./nextflow.config" tag "modules" tag "modules_nfneuro" @@ -17,22 +18,21 @@ nextflow_process { script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf" process { """ - input[0] = Channel.from( [ "T1w.zip", "b0.zip" ] ) + input[0] = Channel.from( [ "T1w.zip", "others.zip" ] ) input[1] = "test.load-test-data" """ } } } - test("registration - ants") { - config "./nextflow.config" + test("registration - ants - SyN") { when { process { """ ch_split_test_data = LOAD_DATA.out.test_data_directory .branch{ T1w: it.simpleName == "T1w" - b0: it.simpleName == "b0" + moving: it.simpleName == "others" } ch_T1w = ch_split_test_data.T1w.map{ test_data_directory -> [ @@ -40,15 +40,21 @@ nextflow_process { file("\${test_data_directory}/T1w.nii.gz") ] } - ch_b0 = ch_split_test_data.b0.map{ + ch_moving = ch_split_test_data.moving.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz") + file("\${test_data_directory}/t1.nii.gz") ] } - input[0] = ch_b0 - .join(ch_T1w) - .map{ it + [[]] } + ch_T1w_mask = ch_split_test_data.T1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w_mask.nii.gz") + ] + } + input[0] = ch_T1w + .join(ch_moving) + .join(ch_T1w_mask) """ } } @@ -60,7 +66,7 @@ nextflow_process { } } - test("registration - ants - quick") { + test("registration - ants - SyN quick") { config "./nextflow_quick.config" when { process { @@ -68,7 +74,7 @@ nextflow_process { ch_split_test_data = LOAD_DATA.out.test_data_directory .branch{ T1w: it.simpleName == "T1w" - b0: it.simpleName == "b0" + moving: it.simpleName == "others" } ch_T1w = ch_split_test_data.T1w.map{ test_data_directory -> [ @@ -76,15 +82,21 @@ nextflow_process { file("\${test_data_directory}/T1w.nii.gz") ] } - ch_b0 = ch_split_test_data.b0.map{ + ch_moving = ch_split_test_data.moving.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1.nii.gz") + ] + } + ch_T1w_mask = ch_split_test_data.T1w.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz") + file("\${test_data_directory}/T1w_mask.nii.gz") ] } - input[0] = ch_b0 - .join(ch_T1w) - .map{ it + [[]] } + input[0] = ch_T1w + .join(ch_moving) + .join(ch_T1w_mask) """ } } @@ -96,31 +108,37 @@ nextflow_process { } } - test("registration - ants - options") { - config "./nextflow_options.config" + test("registration - ants - no warps") { + config "./nextflow_no_warp.config" when { process { """ ch_split_test_data = LOAD_DATA.out.test_data_directory .branch{ T1w: it.simpleName == "T1w" - b0: it.simpleName == "b0" + moving: it.simpleName == "others" } ch_T1w = ch_split_test_data.T1w.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/T1w.nii.gz"), - file("\${test_data_directory}/T1w_mask.nii.gz") + file("\${test_data_directory}/T1w.nii.gz") ] } - ch_b0 = ch_split_test_data.b0.map{ + ch_moving = ch_split_test_data.moving.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz") + file("\${test_data_directory}/t1.nii.gz") ] } - input[0] = ch_b0 - .join(ch_T1w) + ch_T1w_mask = ch_split_test_data.T1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w_mask.nii.gz") + ] + } + input[0] = ch_T1w + .join(ch_moving) + .join(ch_T1w_mask) """ } } @@ -141,23 +159,29 @@ nextflow_process { ch_split_test_data = LOAD_DATA.out.test_data_directory .branch{ T1w: it.simpleName == "T1w" - b0: it.simpleName == "b0" + moving: it.simpleName == "others" } ch_T1w = ch_split_test_data.T1w.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/T1w.nii.gz"), - file("\${test_data_directory}/T1w_mask.nii.gz") + file("\${test_data_directory}/T1w.nii.gz") + ] + } + ch_moving = ch_split_test_data.moving.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/t1.nii.gz") ] } - ch_b0 = ch_split_test_data.b0.map{ + ch_T1w_mask = ch_split_test_data.T1w.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz") + file("\${test_data_directory}/T1w_mask.nii.gz") ] } - input[0] = ch_b0 - .join(ch_T1w) + input[0] = ch_T1w + .join(ch_moving) + .join(ch_T1w_mask) """ } } diff --git a/modules/nf-neuro/registration/ants/tests/main.nf.test.snap b/modules/nf-neuro/registration/ants/tests/main.nf.test.snap index 88a1961d..75d3eb00 100644 --- a/modules/nf-neuro/registration/ants/tests/main.nf.test.snap +++ b/modules/nf-neuro/registration/ants/tests/main.nf.test.snap @@ -1,5 +1,17 @@ { - "registration - ants": { + "registration - ants - stub": { + "content": [ + [ + "versions.yml:md5,78610891ed0adf1a5fe5f0de034d7555" + ] + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-28T13:00:52.757761907" + }, + "registration - ants - SyN quick": { "content": [ { "0": [ @@ -7,7 +19,7 @@ { "id": "test" }, - "test__warped.nii.gz:md5,6491f46d2c2cae09338effb989ec3bf7" + "test_t1_warped.nii.gz:md5,5534f8cb21fcc44f577d520e6083bb83" ] ], "1": [ @@ -15,15 +27,18 @@ { "id": "test" }, - "test__output0GenericAffine.mat:md5,3b6d0eaddd216d15bf50a116c2f09065" + "test_forward1_affine.mat:md5,dde4de8a1ca4ff1647e55dc1fece402c" ] ], + "10": [ + "versions.yml:md5,78610891ed0adf1a5fe5f0de034d7555" + ], "2": [ [ { "id": "test" }, - "test__output1InverseAffine.mat:md5,14b90a8217065e42b21102de73a489c1" + "test_forward0_warp.nii.gz:md5,96b2d18c7537db76366e4557b44f24fe" ] ], "3": [ @@ -31,7 +46,7 @@ { "id": "test" }, - "test__output1Warp.nii.gz:md5,db8e148adf0e32d551cf5e75ca3e76ab" + "test_backward1_warp.nii.gz:md5,28f58437d878a6ea0c81c0e780e2333f" ] ], "4": [ @@ -39,70 +54,165 @@ { "id": "test" }, - "test__output0InverseWarp.nii.gz:md5,3069b784eaa199078d21ecbd5366e27b" + "test_backward0_affine.mat:md5,8cc894275b42e3b78efca499caf22e30" ] ], "5": [ - + [ + { + "id": "test" + }, + [ + "test_forward0_warp.nii.gz:md5,96b2d18c7537db76366e4557b44f24fe", + "test_forward1_affine.mat:md5,dde4de8a1ca4ff1647e55dc1fece402c" + ] + ] ], "6": [ - "versions.yml:md5,99bd266eefb45cc5ba309704aeecac74" + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,8cc894275b42e3b78efca499caf22e30", + "test_backward1_warp.nii.gz:md5,28f58437d878a6ea0c81c0e780e2333f" + ] + ] ], - "affine": [ + "7": [ [ { "id": "test" }, - "test__output0GenericAffine.mat:md5,3b6d0eaddd216d15bf50a116c2f09065" + [ + "test_backward0_affine.mat:md5,8cc894275b42e3b78efca499caf22e30", + "test_backward1_warp.nii.gz:md5,28f58437d878a6ea0c81c0e780e2333f" + ] ] ], - "image": [ + "8": [ [ { "id": "test" }, - "test__warped.nii.gz:md5,6491f46d2c2cae09338effb989ec3bf7" + [ + "test_forward0_warp.nii.gz:md5,96b2d18c7537db76366e4557b44f24fe", + "test_forward1_affine.mat:md5,dde4de8a1ca4ff1647e55dc1fece402c" + ] ] ], - "inverse_affine": [ + "9": [ [ { "id": "test" }, - "test__output1InverseAffine.mat:md5,14b90a8217065e42b21102de73a489c1" + "test_T1_to_T1_slab_registration_ants_mqc.gif:md5,e5730d99d11edfc9802776c782edbf17" ] ], - "inverse_warp": [ + "backward_affine": [ [ { "id": "test" }, - "test__output0InverseWarp.nii.gz:md5,3069b784eaa199078d21ecbd5366e27b" + "test_backward0_affine.mat:md5,8cc894275b42e3b78efca499caf22e30" ] ], - "mqc": [ - + "backward_image_transform": [ + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,8cc894275b42e3b78efca499caf22e30", + "test_backward1_warp.nii.gz:md5,28f58437d878a6ea0c81c0e780e2333f" + ] + ] ], - "versions": [ - "versions.yml:md5,99bd266eefb45cc5ba309704aeecac74" + "backward_tractogram_transform": [ + [ + { + "id": "test" + }, + [ + "test_forward0_warp.nii.gz:md5,96b2d18c7537db76366e4557b44f24fe", + "test_forward1_affine.mat:md5,dde4de8a1ca4ff1647e55dc1fece402c" + ] + ] + ], + "backward_warp": [ + [ + { + "id": "test" + }, + "test_backward1_warp.nii.gz:md5,28f58437d878a6ea0c81c0e780e2333f" + ] + ], + "forward_affine": [ + [ + { + "id": "test" + }, + "test_forward1_affine.mat:md5,dde4de8a1ca4ff1647e55dc1fece402c" + ] ], - "warp": [ + "forward_image_transform": [ [ { "id": "test" }, - "test__output1Warp.nii.gz:md5,db8e148adf0e32d551cf5e75ca3e76ab" + [ + "test_forward0_warp.nii.gz:md5,96b2d18c7537db76366e4557b44f24fe", + "test_forward1_affine.mat:md5,dde4de8a1ca4ff1647e55dc1fece402c" + ] ] + ], + "forward_tractogram_transform": [ + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,8cc894275b42e3b78efca499caf22e30", + "test_backward1_warp.nii.gz:md5,28f58437d878a6ea0c81c0e780e2333f" + ] + ] + ], + "forward_warp": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz:md5,96b2d18c7537db76366e4557b44f24fe" + ] + ], + "image_warped": [ + [ + { + "id": "test" + }, + "test_t1_warped.nii.gz:md5,5534f8cb21fcc44f577d520e6083bb83" + ] + ], + "mqc": [ + [ + { + "id": "test" + }, + "test_T1_to_T1_slab_registration_ants_mqc.gif:md5,e5730d99d11edfc9802776c782edbf17" + ] + ], + "versions": [ + "versions.yml:md5,78610891ed0adf1a5fe5f0de034d7555" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-09T20:09:06.126342072" + "timestamp": "2025-10-28T13:00:13.19463937" }, - "registration - ants - quick": { + "registration - ants - SyN": { "content": [ { "0": [ @@ -110,7 +220,7 @@ { "id": "test" }, - "test__warped.nii.gz:md5,45822afae6c0dc33551b690f6f10dbe4" + "test_t1_warped.nii.gz:md5,8d9c289924c4d2c8edab3feae669d1c3" ] ], "1": [ @@ -118,15 +228,18 @@ { "id": "test" }, - "test__output0GenericAffine.mat:md5,17e711c6a2f73415269b4272344890bc" + "test_forward1_affine.mat:md5,16a42a74c35c9fda7786250b333bef86" ] ], + "10": [ + "versions.yml:md5,78610891ed0adf1a5fe5f0de034d7555" + ], "2": [ [ { "id": "test" }, - "test__output1InverseAffine.mat:md5,e5df1f989e0340746d9742ff2d0a7a99" + "test_forward0_warp.nii.gz:md5,9a3b969b74ac82ab0940b8b94677c7b9" ] ], "3": [ @@ -134,7 +247,7 @@ { "id": "test" }, - "test__output1Warp.nii.gz:md5,813a4218d33d5acfebadc4ba0e49df8b" + "test_backward1_warp.nii.gz:md5,3f2d23cce4fbcf970099eecd629d4019" ] ], "4": [ @@ -142,7 +255,7 @@ { "id": "test" }, - "test__output0InverseWarp.nii.gz:md5,813a4218d33d5acfebadc4ba0e49df8b" + "test_backward0_affine.mat:md5,317e067757768c8e75de9d12f84563c9" ] ], "5": [ @@ -150,72 +263,147 @@ { "id": "test" }, - "test_T1_to_DWI_registration_ants_mqc.gif:md5,9cdd7e9bf7facc3f95a61de03e15d47d" + [ + "test_forward0_warp.nii.gz:md5,9a3b969b74ac82ab0940b8b94677c7b9", + "test_forward1_affine.mat:md5,16a42a74c35c9fda7786250b333bef86" + ] ] ], "6": [ - "versions.yml:md5,99bd266eefb45cc5ba309704aeecac74" + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,317e067757768c8e75de9d12f84563c9", + "test_backward1_warp.nii.gz:md5,3f2d23cce4fbcf970099eecd629d4019" + ] + ] + ], + "7": [ + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,317e067757768c8e75de9d12f84563c9", + "test_backward1_warp.nii.gz:md5,3f2d23cce4fbcf970099eecd629d4019" + ] + ] ], - "affine": [ + "8": [ [ { "id": "test" }, - "test__output0GenericAffine.mat:md5,17e711c6a2f73415269b4272344890bc" + [ + "test_forward0_warp.nii.gz:md5,9a3b969b74ac82ab0940b8b94677c7b9", + "test_forward1_affine.mat:md5,16a42a74c35c9fda7786250b333bef86" + ] ] ], - "image": [ + "9": [ + + ], + "backward_affine": [ [ { "id": "test" }, - "test__warped.nii.gz:md5,45822afae6c0dc33551b690f6f10dbe4" + "test_backward0_affine.mat:md5,317e067757768c8e75de9d12f84563c9" ] ], - "inverse_affine": [ + "backward_image_transform": [ [ { "id": "test" }, - "test__output1InverseAffine.mat:md5,e5df1f989e0340746d9742ff2d0a7a99" + [ + "test_backward0_affine.mat:md5,317e067757768c8e75de9d12f84563c9", + "test_backward1_warp.nii.gz:md5,3f2d23cce4fbcf970099eecd629d4019" + ] ] ], - "inverse_warp": [ + "backward_tractogram_transform": [ [ { "id": "test" }, - "test__output0InverseWarp.nii.gz:md5,813a4218d33d5acfebadc4ba0e49df8b" + [ + "test_forward0_warp.nii.gz:md5,9a3b969b74ac82ab0940b8b94677c7b9", + "test_forward1_affine.mat:md5,16a42a74c35c9fda7786250b333bef86" + ] ] ], - "mqc": [ + "backward_warp": [ [ { "id": "test" }, - "test_T1_to_DWI_registration_ants_mqc.gif:md5,9cdd7e9bf7facc3f95a61de03e15d47d" + "test_backward1_warp.nii.gz:md5,3f2d23cce4fbcf970099eecd629d4019" ] ], - "versions": [ - "versions.yml:md5,99bd266eefb45cc5ba309704aeecac74" + "forward_affine": [ + [ + { + "id": "test" + }, + "test_forward1_affine.mat:md5,16a42a74c35c9fda7786250b333bef86" + ] + ], + "forward_image_transform": [ + [ + { + "id": "test" + }, + [ + "test_forward0_warp.nii.gz:md5,9a3b969b74ac82ab0940b8b94677c7b9", + "test_forward1_affine.mat:md5,16a42a74c35c9fda7786250b333bef86" + ] + ] ], - "warp": [ + "forward_tractogram_transform": [ [ { "id": "test" }, - "test__output1Warp.nii.gz:md5,813a4218d33d5acfebadc4ba0e49df8b" + [ + "test_backward0_affine.mat:md5,317e067757768c8e75de9d12f84563c9", + "test_backward1_warp.nii.gz:md5,3f2d23cce4fbcf970099eecd629d4019" + ] ] + ], + "forward_warp": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz:md5,9a3b969b74ac82ab0940b8b94677c7b9" + ] + ], + "image_warped": [ + [ + { + "id": "test" + }, + "test_t1_warped.nii.gz:md5,8d9c289924c4d2c8edab3feae669d1c3" + ] + ], + "mqc": [ + + ], + "versions": [ + "versions.yml:md5,78610891ed0adf1a5fe5f0de034d7555" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-09T20:09:21.409703964" + "timestamp": "2025-10-28T12:58:47.595933738" }, - "registration - ants - options": { + "registration - ants - no warps": { "content": [ { "0": [ @@ -223,7 +411,7 @@ { "id": "test" }, - "test__warped.nii.gz:md5,c090343bdf2af2eac20a051b3cf9bb20" + "test_t1_warped.nii.gz:md5,c13330e3f71c57f2517dcd6f59f64ff1" ] ], "1": [ @@ -231,93 +419,151 @@ { "id": "test" }, - "test__output0GenericAffine.mat:md5,d3230976041b8532b14a00e2e5760395" + "test_forward1_affine.mat:md5,7387bc8f93dd29d2a0aadb9d5dc06d5b" ] ], + "10": [ + "versions.yml:md5,78610891ed0adf1a5fe5f0de034d7555" + ], "2": [ - [ - { - "id": "test" - }, - "test__output1InverseAffine.mat:md5,10a6c2eededdc85709bbff395f0aab3a" - ] + ], "3": [ ], "4": [ - + [ + { + "id": "test" + }, + "test_backward0_affine.mat:md5,733babe33e1656d0f85064caf24d6f82" + ] ], "5": [ [ { "id": "test" }, - "test__registration_ants_mqc.gif:md5,1bfa145273f0ece8a2293efc7d2a8c2e" + [ + "test_forward1_affine.mat:md5,7387bc8f93dd29d2a0aadb9d5dc06d5b" + ] ] ], "6": [ - "versions.yml:md5,99bd266eefb45cc5ba309704aeecac74" + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,733babe33e1656d0f85064caf24d6f82" + ] + ] + ], + "7": [ + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,733babe33e1656d0f85064caf24d6f82" + ] + ] ], - "affine": [ + "8": [ [ { "id": "test" }, - "test__output0GenericAffine.mat:md5,d3230976041b8532b14a00e2e5760395" + [ + "test_forward1_affine.mat:md5,7387bc8f93dd29d2a0aadb9d5dc06d5b" + ] ] ], - "image": [ + "9": [ + + ], + "backward_affine": [ [ { "id": "test" }, - "test__warped.nii.gz:md5,c090343bdf2af2eac20a051b3cf9bb20" + "test_backward0_affine.mat:md5,733babe33e1656d0f85064caf24d6f82" ] ], - "inverse_affine": [ + "backward_image_transform": [ [ { "id": "test" }, - "test__output1InverseAffine.mat:md5,10a6c2eededdc85709bbff395f0aab3a" + [ + "test_backward0_affine.mat:md5,733babe33e1656d0f85064caf24d6f82" + ] ] ], - "inverse_warp": [ + "backward_tractogram_transform": [ + [ + { + "id": "test" + }, + [ + "test_forward1_affine.mat:md5,7387bc8f93dd29d2a0aadb9d5dc06d5b" + ] + ] + ], + "backward_warp": [ ], - "mqc": [ + "forward_affine": [ [ { "id": "test" }, - "test__registration_ants_mqc.gif:md5,1bfa145273f0ece8a2293efc7d2a8c2e" + "test_forward1_affine.mat:md5,7387bc8f93dd29d2a0aadb9d5dc06d5b" ] ], - "versions": [ - "versions.yml:md5,99bd266eefb45cc5ba309704aeecac74" + "forward_image_transform": [ + [ + { + "id": "test" + }, + [ + "test_forward1_affine.mat:md5,7387bc8f93dd29d2a0aadb9d5dc06d5b" + ] + ] ], - "warp": [ + "forward_tractogram_transform": [ + [ + { + "id": "test" + }, + [ + "test_backward0_affine.mat:md5,733babe33e1656d0f85064caf24d6f82" + ] + ] + ], + "forward_warp": [ + + ], + "image_warped": [ + [ + { + "id": "test" + }, + "test_t1_warped.nii.gz:md5,c13330e3f71c57f2517dcd6f59f64ff1" + ] + ], + "mqc": [ + ], + "versions": [ + "versions.yml:md5,78610891ed0adf1a5fe5f0de034d7555" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" - }, - "timestamp": "2025-10-09T20:09:34.159219563" - }, - "registration - ants - stub": { - "content": [ - [ - "versions.yml:md5,99bd266eefb45cc5ba309704aeecac74" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-09T20:09:40.567822165" + "timestamp": "2025-10-28T13:00:29.996790917" } } \ No newline at end of file diff --git a/modules/nf-neuro/registration/ants/tests/nextflow.config b/modules/nf-neuro/registration/ants/tests/nextflow.config index 3750dac9..ee34944b 100644 --- a/modules/nf-neuro/registration/ants/tests/nextflow.config +++ b/modules/nf-neuro/registration/ants/tests/nextflow.config @@ -2,5 +2,6 @@ process { withName: "REGISTRATION_ANTS" { publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } ext.repro_mode = 1 + ext.initial_transform = "intensities" } } diff --git a/modules/nf-neuro/registration/ants/tests/nextflow_options.config b/modules/nf-neuro/registration/ants/tests/nextflow_no_warp.config similarity index 84% rename from modules/nf-neuro/registration/ants/tests/nextflow_options.config rename to modules/nf-neuro/registration/ants/tests/nextflow_no_warp.config index 88c9a90a..3e060ea6 100644 --- a/modules/nf-neuro/registration/ants/tests/nextflow_options.config +++ b/modules/nf-neuro/registration/ants/tests/nextflow_no_warp.config @@ -2,14 +2,11 @@ process { withName: "REGISTRATION_ANTS" { publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } ext.quick = true - ext.run_qc = true - ext.threads = 1 ext.transform = "r" ext.histogram_bins = 4 ext.spline_distance = 26 ext.gradient_step = 0.1 ext.histogram_matching = 0 - ext.repro_mode = 0 ext.collapse_output = 0 ext.random_seed = 1234 } diff --git a/modules/nf-neuro/registration/ants/tests/nextflow_quick.config b/modules/nf-neuro/registration/ants/tests/nextflow_quick.config index 9fd98e5b..18651856 100644 --- a/modules/nf-neuro/registration/ants/tests/nextflow_quick.config +++ b/modules/nf-neuro/registration/ants/tests/nextflow_quick.config @@ -2,8 +2,7 @@ process { withName: "REGISTRATION_ANTS" { publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } ext.quick = true - ext.repro_mode = 1 ext.run_qc = true - ext.suffix_qc = "T1_to_DWI" + ext.suffix_qc = "T1_to_T1_slab" } } diff --git a/modules/nf-neuro/registration/antsapplytransforms/main.nf b/modules/nf-neuro/registration/antsapplytransforms/main.nf index 7b0b9551..437edf41 100644 --- a/modules/nf-neuro/registration/antsapplytransforms/main.nf +++ b/modules/nf-neuro/registration/antsapplytransforms/main.nf @@ -5,10 +5,10 @@ process REGISTRATION_ANTSAPPLYTRANSFORMS { container "scilus/scilus:2.2.0" input: - tuple val(meta), path(image), path(reference), path(warp), path(affine) + tuple val(meta), path(images, arity: '1..*'), path(reference), path(transformations, arity: '1..*') output: - tuple val(meta), path("*__warped.nii.gz") , emit: warped_image + tuple val(meta), path("*.{nii,nii.gz}") , emit: warped_image tuple val(meta), path("*_registration_antsapplytransforms_mqc.gif") , emit: mqc, optional: true path "versions.yml" , emit: versions @@ -17,41 +17,39 @@ process REGISTRATION_ANTSAPPLYTRANSFORMS { script: def prefix = task.ext.prefix ?: "${meta.id}" - def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}__warped" : "__warped" - def suffix_qc = task.ext.suffix_qc ? "${task.ext.suffix_qc}" : "" + def suffix = "_${task.ext.suffix ?: "warped"}" + def suffix_qc = task.ext.suffix_qc ? "_${task.ext.suffix_qc}" : "" - def output_dtype = task.ext.output_dtype ? "-u " + task.ext.output_dtype : "" - def dimensionality = task.ext.dimensionality ? "-d " + task.ext.dimensionality : "-d 3" - def image_type = task.ext.image_type ? "-e " + task.ext.image_type : "-e 0" - def interpolation = task.ext.interpolation ? "-n " + task.ext.interpolation : "" - def default_val = task.ext.default_val ? "-f " + task.ext.default_val : "" - def run_qc = task.ext.run_qc ? task.ext.run_qc : false + def dimensionality = "-d ${task.ext.dimensionality ?: 3}" + def image_type = "-e ${task.ext.image_type ?: 0}" + def interpolation = "-n ${task.ext.interpolation ?: "Linear"}" + def output_dtype = "-u ${task.ext.output_dtype ?: "default"}" + def default_val = "-f ${task.ext.default_val ?: 0}" + def run_qc = task.ext.run_qc as Boolean || false """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - for image in $image; - do \ + for image in $images; do ext=\${image#*.} bname=\$(basename \${image} .\${ext}) - antsApplyTransforms $dimensionality\ - -i \$image\ - -r $reference\ - -o ${prefix}__\${bname}${suffix}.nii.gz\ - $interpolation\ - -t $warp $affine\ - $output_dtype\ - $image_type\ - $default_val + antsApplyTransforms $dimensionality \ + -i \$image \ + -r $reference \ + -o ${prefix}_\${bname}${suffix}.nii.gz \ + $interpolation \ + ${transformations.collect{ t -> "-t $t" }.join(" ")} \ + $output_dtype \ + $image_type \ + $default_val ### ** QC ** ### - if $run_qc; - then - mv $reference reference.nii.gz - extract_dim=\$(mrinfo ${prefix}__\${bname}${suffix}.nii.gz -size) + if $run_qc; then + ln -sf $reference reference.nii.gz + extract_dim=\$(mrinfo ${prefix}_\${bname}${suffix}.nii.gz -size) read sagittal_dim coronal_dim axial_dim <<< "\${extract_dim}" # Get the middle slice @@ -61,78 +59,80 @@ process REGISTRATION_ANTSAPPLYTRANSFORMS { # Set viz params. viz_params="--display_slice_number --display_lr --size 256 256" + # Iterate over images. - for image in reference \${bname}${suffix}; - do - mrconvert *\${image}.nii.gz *\${image}_viz.nii.gz -stride -1,2,3 + for image in reference \${bname}${suffix}; do + mrconvert *\${image}.nii.gz *\${image}_viz.nii.gz -stride -1,2,3 -force scil_viz_volume_screenshot *\${image}_viz.nii.gz \${image}_coronal.png \ --slices \$coronal_dim --axis coronal \$viz_params scil_viz_volume_screenshot *\${image}_viz.nii.gz \${image}_sagittal.png \ --slices \$sagittal_dim --axis sagittal \$viz_params scil_viz_volume_screenshot *\${image}_viz.nii.gz \${image}_axial.png \ --slices \$axial_dim --axis axial \$viz_params - if [ \$image != reference ]; - then + + if [ \$image != reference ]; then title="Transformed" else title="Reference" fi + convert +append \${image}_coronal*.png \${image}_axial*.png \ \${image}_sagittal*.png \${image}_mosaic.png convert -annotate +20+230 "\${title}" -fill white -pointsize 30 \ \${image}_mosaic.png \${image}_mosaic.png # Clean up. rm \${image}_coronal*.png \${image}_sagittal*.png \${image}_axial*.png + rm *\${image}_viz.nii.gz done + # Create GIF. convert -delay 10 -loop 0 -morph 10 \ \${bname}${suffix}_mosaic.png reference_mosaic.png \${bname}${suffix}_mosaic.png \ ${prefix}_\${bname}${suffix_qc}_registration_antsapplytransforms_mqc.gif + # Clean up. rm *_mosaic.png + rm reference.nii.gz fi done cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*: v?([0-9.a-zA-Z-]+).*/\\1/') - mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') imagemagick: \$(convert -version | grep "Version:" | sed -E 's/.*ImageMagick ([0-9.-]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') + scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ stub: def prefix = task.ext.prefix ?: "${meta.id}" - def suffix = task.ext.first_suffix ? "${task.ext.first_suffix}__warped" : "__warped" + def suffix = "_${task.ext.suffix ?: "warped"}" + def suffix_qc = task.ext.suffix_qc ? "_${task.ext.suffix_qc}" : "" + def run_qc = task.ext.run_qc as Boolean || false """ - set +e - function handle_code () { - local code=\$? - ignore=( 1 ) - [[ " \${ignore[@]} " =~ " \$code " ]] || exit \$code - } - trap 'handle_code' ERR - antsApplyTransforms -h - convert -h scil_viz_volume_screenshot -h + convert -help . - for image in $image; - do \ + for image in $images; do ext=\${image#*.} bname=\$(basename \${image} .\${ext}) - touch ${prefix}__\${bname}${suffix}.nii.gz + touch ${prefix}_\${bname}${suffix}.nii.gz + + if $run_qc; then + touch ${prefix}_\${bname}${suffix_qc}_registration_antsapplytransforms_mqc.gif + fi done cat <<-END_VERSIONS > versions.yml "${task.process}": - scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*: v?([0-9.a-zA-Z-]+).*/\\1/') - mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') imagemagick: \$(convert -version | grep "Version:" | sed -E 's/.*ImageMagick ([0-9.-]+).*/\\1/') + mrtrix: \$(mrinfo -version 2>&1 | grep "== mrinfo" | sed -E 's/== mrinfo ([0-9.]+).*/\\1/') + scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ } diff --git a/modules/nf-neuro/registration/antsapplytransforms/meta.yml b/modules/nf-neuro/registration/antsapplytransforms/meta.yml index 181fb889..e3182eef 100644 --- a/modules/nf-neuro/registration/antsapplytransforms/meta.yml +++ b/modules/nf-neuro/registration/antsapplytransforms/meta.yml @@ -15,14 +15,14 @@ args: type: string description: | Prefix to add to the output file name. - e.g. `warped_` will result in `warped_image.nii.gz`. + e.g. `warped` will result in `warped_image.nii.gz`. default: "${meta.id}" - suffix: type: string description: | - Suffix to add to the output file name. - e.g. `_warped` will result in `image_warped.nii.gz`. - default: "__warped" + Obligatory suffix to add to the output file name to prevent overwrite + of input files. e.g. : the suffix `warped` will result in the name `image_warped.nii.gz`. + default: "warped" - suffix_qc: type: string description: | @@ -31,24 +31,34 @@ args: - dimensionality: type: number description: | - Dimensionality of the input image. + Dimensionality of input images. e.g. `2` for 2D images, `3` for 3D images. + default: 3 - image_type: type: integer - description: Type of the input image. + description: | + Type of the input image : + - 0: scalar + - 1: vector + - 2: tensor + - 3: time series + - 4: multichannel + - 5: five-dimensional + default: 0 choices: - - 0 (scalar) - - 1 (vector) - - 2 (tensor) - - 3 (time series) - - 4 (multichannel) - - 5 (five-dimensional) + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 - interpolation: type: string description: | Interpolation method to use for the transformation. Content in `[...]` is optional. Refer the the antsApplyTransforms documentation for more details. + default: Linear choices: - Linear - NearestNeighbor @@ -63,6 +73,7 @@ args: - output_dtype: type: string description: Output data type for the warped image. + default: default choices: - char - uchar @@ -76,6 +87,7 @@ args: description: | Default value to use for the input image. It specifies the voxel value when the input point maps outside the output domain. + default: 0 - run_qc: type: boolean description: | @@ -88,29 +100,25 @@ input: description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - image: - type: file - description: Input image to register + - images: + type: list + description: Input images to transform onto reference. pattern: "*.{nii.nii.gz}" - ontologies: [] + mandatory: true + ontologies: + - edam: http://edamontology.org/format_4001 # NIFTI format] - reference: type: file - description: Reference image for registration + description: Reference image for transformation. pattern: "*.{nii.nii.gz}" - ontologies: [] - - warp: - type: file - description: Warp transformation file to warp image or trk. - pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - affine: - type: file - description: Affine or rigig transformation file to warp image or trk (*mat). - If a rigid or affine transformation needs to be inverted before being applied, - use antsApplyTransforms with the -o Linear[inversedTransform,1], as this - module does not handles it. - pattern: "*.mat" + - transformations: + type: list + description: Transformation files (in the correct order). + pattern: "*.{nii.gz,mat}" + mandatory: true ontologies: [] output: warped_image: @@ -119,10 +127,10 @@ output: description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*__warped.nii.gz": + - "*.{nii,nii.gz}": type: file - description: Warped image. - pattern: "*__warped.{nii.nii.gz}" + description: Warped image(s). + pattern: "*.{nii,nii.gz}" ontologies: [] mqc: - - meta: @@ -136,6 +144,7 @@ output: .gif file containing quality control image for the registration process. Made for use in MultiQC report. pattern: "*_registration_antsapplytransforms_mqc.gif" + optional: true ontologies: [] versions: - versions.yml: diff --git a/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test index 1aaddbef..569e2dfb 100644 --- a/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test +++ b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test @@ -3,6 +3,7 @@ nextflow_process { name "Test Process REGISTRATION_ANTSAPPLYTRANSFORMS" script "../main.nf" process "REGISTRATION_ANTSAPPLYTRANSFORMS" + config "./nextflow.config" tag "modules" tag "modules_nfneuro" @@ -25,9 +26,6 @@ nextflow_process { } test("registration - antsapplytransforms") { - - config "./nextflow.config" - when { process { """ @@ -36,7 +34,6 @@ nextflow_process { [ id:'test', single_end:false ], // meta map file("\${test_data_directory}/b0.nii.gz"), file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), - file("\${test_data_directory}/output1Warp.nii.gz"), file("\${test_data_directory}/output0GenericAffine.mat") ]} """ @@ -50,23 +47,76 @@ nextflow_process { } } - test("registration - antsapplytransforms - stub-run") { + test("registration - antsapplytransforms - multiple transforms") { + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory + .map{ test_data_directory -> [ + [ id:'test' ], // meta map + file("\${test_data_directory}/b0.nii.gz"), + file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), + [ + file("\${test_data_directory}/output1Warp.nii.gz"), + file("\${test_data_directory}/output0GenericAffine.mat") + ] + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("registration - antsapplytransforms - multiple images") { + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory + .map{ test_data_directory -> [ + [ id:'test' ], // meta map + [ + file("\${test_data_directory}/b0.nii.gz"), + file("\${test_data_directory}/b0.nii.gz").copyTo( + "\${test_data_directory}/b0_copy.nii.gz" + ) + ], + file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), + [ + file("\${test_data_directory}/output1Warp.nii.gz"), + file("\${test_data_directory}/output0GenericAffine.mat") + ] + ]} + """ + } + } + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + test("registration - antsapplytransforms - stub-run") { tag "stub" options "-stub-run" - - config "./nextflow.config" - when { process { """ input[0] = LOAD_DATA.out.test_data_directory .map{ test_data_directory -> [ - [ id:'test', single_end:false ], // meta map + [ id:'test' ], // meta map file("\${test_data_directory}/b0.nii.gz"), file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"), - file("\${test_data_directory}/output1Warp.nii.gz"), - file("\${test_data_directory}/output0GenericAffine.mat") + [ + file("\${test_data_directory}/output1Warp.nii.gz"), + file("\${test_data_directory}/output0GenericAffine.mat") + ] ]} """ } diff --git a/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test.snap b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test.snap index 5a153e08..07555956 100644 --- a/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test.snap +++ b/modules/nf-neuro/registration/antsapplytransforms/tests/main.nf.test.snap @@ -1,4 +1,53 @@ { + "registration - antsapplytransforms - multiple transforms": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test_b0_to_template.nii.gz:md5,00916b78f727d65a811774f932aa9ce7" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2" + ] + ], + "2": [ + "versions.yml:md5,bb62f30da9c67a5b3ee5d8316ca38cc5" + ], + "mqc": [ + [ + { + "id": "test" + }, + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2" + ] + ], + "versions": [ + "versions.yml:md5,bb62f30da9c67a5b3ee5d8316ca38cc5" + ], + "warped_image": [ + [ + { + "id": "test" + }, + "test_b0_to_template.nii.gz:md5,00916b78f727d65a811774f932aa9ce7" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-28T13:11:02.911101572" + }, "registration - antsapplytransforms": { "content": [ { @@ -8,7 +57,7 @@ "id": "test", "single_end": false }, - "test__b0b0__warped.nii.gz:md5,00916b78f727d65a811774f932aa9ce7" + "test_b0_to_template.nii.gz:md5,9e2bcde0e32d10aa9bfbfedfddb65dc3" ] ], "1": [ @@ -17,11 +66,11 @@ "id": "test", "single_end": false }, - "test_b0b0_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2" + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,a03fb2d2bc43f533144a8633947d3601" ] ], "2": [ - "versions.yml:md5,fad1b8896193c2260d2e7f7916ab7130" + "versions.yml:md5,bb62f30da9c67a5b3ee5d8316ca38cc5" ], "mqc": [ [ @@ -29,11 +78,11 @@ "id": "test", "single_end": false }, - "test_b0b0_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2" + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,a03fb2d2bc43f533144a8633947d3601" ] ], "versions": [ - "versions.yml:md5,fad1b8896193c2260d2e7f7916ab7130" + "versions.yml:md5,bb62f30da9c67a5b3ee5d8316ca38cc5" ], "warped_image": [ [ @@ -41,27 +90,88 @@ "id": "test", "single_end": false }, - "test__b0b0__warped.nii.gz:md5,00916b78f727d65a811774f932aa9ce7" + "test_b0_to_template.nii.gz:md5,9e2bcde0e32d10aa9bfbfedfddb65dc3" ] ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-09T20:09:53.057544388" + "timestamp": "2025-10-28T13:10:09.054640289" }, "registration - antsapplytransforms - stub-run": { "content": [ [ - "versions.yml:md5,fad1b8896193c2260d2e7f7916ab7130" + "versions.yml:md5,bb62f30da9c67a5b3ee5d8316ca38cc5" ] ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-28T13:12:56.162636033" + }, + "registration - antsapplytransforms - multiple images": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + [ + "test_b0_copy_to_template.nii.gz:md5,00916b78f727d65a811774f932aa9ce7", + "test_b0_to_template.nii.gz:md5,00916b78f727d65a811774f932aa9ce7" + ] + ] + ], + "1": [ + [ + { + "id": "test" + }, + [ + "test_b0_copy_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2", + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2" + ] + ] + ], + "2": [ + "versions.yml:md5,bb62f30da9c67a5b3ee5d8316ca38cc5" + ], + "mqc": [ + [ + { + "id": "test" + }, + [ + "test_b0_copy_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2", + "test_b0_to_template_registration_antsapplytransforms_mqc.gif:md5,f92266ae3c705c11fb3f552ca44f8fe2" + ] + ] + ], + "versions": [ + "versions.yml:md5,bb62f30da9c67a5b3ee5d8316ca38cc5" + ], + "warped_image": [ + [ + { + "id": "test" + }, + [ + "test_b0_copy_to_template.nii.gz:md5,00916b78f727d65a811774f932aa9ce7", + "test_b0_to_template.nii.gz:md5,00916b78f727d65a811774f932aa9ce7" + ] + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-09T20:09:58.870177157" + "timestamp": "2025-10-28T13:12:40.241476817" } } \ No newline at end of file diff --git a/modules/nf-neuro/registration/antsapplytransforms/tests/nextflow.config b/modules/nf-neuro/registration/antsapplytransforms/tests/nextflow.config index 8e50f45e..21a130ea 100644 --- a/modules/nf-neuro/registration/antsapplytransforms/tests/nextflow.config +++ b/modules/nf-neuro/registration/antsapplytransforms/tests/nextflow.config @@ -1,12 +1,12 @@ process { withName: "REGISTRATION_ANTSAPPLYTRANSFORMS" { - ext.interpolation = "linear" - ext.first_suffix = "b0" + ext.interpolation = "Linear" + ext.suffix = "to_template" ext.dimensionality = 3 ext.image_type = 0 ext.output_dtype = "float" ext.default_val = 0 ext.run_qc = true - ext.suffix_qc = "b0_to_template" + ext.suffix_qc = "to_template" } } diff --git a/modules/nf-neuro/registration/convert/main.nf b/modules/nf-neuro/registration/convert/main.nf index 3c9af61e..b57054e2 100644 --- a/modules/nf-neuro/registration/convert/main.nf +++ b/modules/nf-neuro/registration/convert/main.nf @@ -6,12 +6,11 @@ process REGISTRATION_CONVERT { containerOptions "--env FSLOUTPUTTYPE='NIFTI_GZ'" input: - tuple val(meta), path(deform), path(affine), path(source), path(target), path(fs_license) + tuple val(meta), path(transformation), val(input_type), val(output_type), path(reference), path(affine_source), path(fs_license) output: - tuple val(meta), path("*out_warp.{nii,nii.gz,mgz,m3z}") , emit: deform_transform - tuple val(meta), path("*out_affine.{txt,lta,mat,dat}") , emit: affine_transform, optional: true - path "versions.yml" , emit: versions + tuple val(meta), path("*out_{affine,warp}.{nii,nii.gz,mgz,m3z,txt,lta,mat,dat}") , emit: transformation + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when @@ -20,19 +19,48 @@ process REGISTRATION_CONVERT { def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" - //For arguments definition, lta_convert -h - def invert = task.ext.invert ? "--invert" : "" - def source_geometry_affine = "$source" ? "--src " + "$source" : "" - def target_geometry_affine = "$target" ? "--trg " + "$target" : "" - def in_format_affine = task.ext.in_format_affine ? "--in" + task.ext.in_format_affine + " " + "$affine" : "--inlta " + "$affine" - def out_format_affine = task.ext.out_format_affine ? "--out" + task.ext.out_format_affine : "--outitk" + def transform_types = [ + affine: [lta: "lta", fsl: "mat", mni: "xfm", reg: "dat", niftyreg: "txt", itk: "txt", vox: "txt"], + warp: [m3z: "m3z", fsl: "nii.gz", lps: "nii.gz", itk: "nii.gz", ras: "nii.gz", vox: "mgz"] + ] + def spaces = ["ras2ras", "vox2vox", "register_dat"] + + // Validation transformation type and coercion with conversion type + def in_extension = transformation.name.tokenize('.')[1..-1].join('.') + def transform_type = transform_types.affine.find{ it.value == in_extension } ? "affine" : "warp" + if ( transform_type == "warp" && !transform_types.warp.containsKey(output_type) ) { + error "Invalid combination of transformation type and conversion type: ${transform_type} to ${output_type}." + } - //For arguments definition, mri_warp_convert -h - def source_geometry_deform = "$source" ? "--insrcgeom " + "$source" : "" - def in_format_deform = task.ext.in_format_deform ? "--in" + task.ext.in_format_deform + " " + "$deform" : "--inras " + "$deform" - def out_format_deform = task.ext.out_format_deform ? "--out" + task.ext.out_format_deform : "--outitk" - def downsample = task.ext.downsample ? "--downsample" : "" + def out_extension = transform_types[transform_type][output_type] + def output_name = "${prefix}_out_${transform_type}.${out_extension}" + def command = transform_type == "affine" ? "lta_convert" : "mri_warp_convert" + + if ( transform_type == "affine" ) { + // Affine transformations are defined on the target space + args += " --in$input_type $transformation --out$output_type $output_name --trg $reference" + + // Validate source geometry is available for conversion to .lta + if ( output_type == "lta" ) { + if ( !affine_source ) error "Source geometry must be provided for conversion to .lta." + args += " --src $affine_source" + } + + if ( task.ext.invert ) args += " --invert" + if ( task.ext.conform ) args += " --trgconform" + if ( task.ext.output_space ) { + if ( !spaces.contains(task.ext.output_space) ) { + error "Invalid output space: ${task.ext.output_space}. Valid options are: ${spaces.join(', ')}" + } + args += " --outspace " + task.ext.output_space + } + } + else { + // Deformable transformations are defined on the source space + args += " --insrcgeom $reference --in$input_type $transformation --out$output_type $output_name" + if ( task.ext.downsample ) args += " --downsample" + } """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus export OMP_NUM_THREADS=1 @@ -40,62 +68,48 @@ process REGISTRATION_CONVERT { cp $fs_license \$FREESURFER_HOME/license.txt - if [[ -f "$affine" ]]; - then - declare -A affine_dictionnary=( ["--outlta"]="lta" \ - ["--outfsl"]="mat" \ - ["--outmni"]="xfm" \ - ["--outreg"]="dat" \ - ["--outniftyreg"]="txt" \ - ["--outitk"]="txt" \ - ["--outvox"]="txt" ) - - ext_affine=\${affine_dictionnary[${out_format_affine}]} - - lta_convert ${invert} ${source_geometry_affine} ${target_geometry_affine} ${in_format_affine} ${out_format_affine} ${prefix}__out_affine.\${ext_affine} - fi - - declare -A deform_dictionnary=( ["--outm3z"]="m3z" \ - ["--outfsl"]="nii.gz" \ - ["--outlps"]="nii.gz" \ - ["--outitk"]="nii.gz" \ - ["--outras"]="nii.gz" \ - ["--outvox"]="mgz" ) - - ext_deform=\${deform_dictionnary[${out_format_deform}]} - - mri_warp_convert ${source_geometry_deform} ${downsample} ${in_format_deform} ${out_format_deform} ${prefix}__out_warp.\${ext_deform} + $command $args rm \$FREESURFER_HOME/license.txt cat <<-END_VERSIONS > versions.yml "${task.process}": - Freesurfer: \$(mri_convert -version | grep "freesurfer" | sed -E 's/.* ([0-9.]+).*/\\1/') + freesurfer: \$(mri_convert -version | grep "freesurfer" | sed -E 's/.* ([0-9.]+).*/\\1/') END_VERSIONS """ stub: - def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" + def transform_types = [ + affine: [lta: "lta", fsl: "mat", mni: "xfm", reg: "dat", niftyreg: "txt", itk: "txt", vox: "txt"], + warp: [m3z: "m3z", fsl: "nii.gz", lps: "nii.gz", itk: "nii.gz", ras: "nii.gz", vox: "mgz"] + ] + // Validation transformation type and coercion with conversion type + def in_extension = transformation.name.tokenize('.')[1..-1].join('.') + def transform_type = transform_types.affine.find{ it.value == in_extension } ? "affine" : "warp" + if ( transform_type == "warp" && !transform_types.warp.containsKey(output_type) ) { + error "Invalid combination of transformation type and conversion type: ${transform_type} to ${output_type}." + } + def out_extension = transform_types[transform_type][output_type] + def output_name = "${prefix}_out_${transform_type}.${out_extension}" """ set +e function handle_code () { - local code=\$? - ignore=( 1 ) - [[ " \${ignore[@]} " =~ " \$code " ]] || exit \$code + local code=\$? + ignore=( 1 ) + [[ " \${ignore[@]} " =~ " \$code " ]] || exit \$code } trap 'handle_code' ERR lta_convert --help mri_warp_convert --help - touch ${prefix}__out_warp.nii.gz - touch ${prefix}__out_affine.txt + touch $output_name cat <<-END_VERSIONS > versions.yml "${task.process}": - Freesurfer: \$(mri_convert -version | grep "freesurfer" | sed -E 's/.* ([0-9.]+).*/\\1/') + freesurfer: \$(mri_convert -version | grep "freesurfer" | sed -E 's/.* ([0-9.]+).*/\\1/') END_VERSIONS """ } diff --git a/modules/nf-neuro/registration/convert/meta.yml b/modules/nf-neuro/registration/convert/meta.yml index 95cbb1ac..7dea3ccd 100644 --- a/modules/nf-neuro/registration/convert/meta.yml +++ b/modules/nf-neuro/registration/convert/meta.yml @@ -15,37 +15,94 @@ tools: homepage: https://surfer.nmr.mgh.harvard.edu/ doi: 10.1016/j.neuroimage.2012.01.021 identifier: "" +args: + - invert: + type: boolean + description: FOR AFFINE CONVERSION ONLY. Invert the transformation. + default: false + - conform: + type: boolean + description: FOR AFFINE CONVERSION ONLY. Conform target geometry before conversion (see freesurfer documentation). + default: false + - output_space: + type: string + description: FOR AFFINE CONVERSION ONLY. Reference space to convert to. Default is ras2ras. + default: "ras2ras" + choices: + - ras2ras + - vox2vox + - register_dat + - downsample: + type: boolean + description: FOR WARP CONVERSION ONLY. Downsample the input warp to 2mm isotropic resolution. + default: false input: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - deform: + - transformation: type: file - description: Deform transform to convert. Default usage expects Freesurfer .mgz - format from mri_synthmorph - pattern: "*.{nii,nii.gz,mgz,m3z}" - ontologies: - - edam: http://edamontology.org/format_4001 # NIFTI format - - affine: - type: file - description: Affine transform to convert. Default usage expects Freesurfer .lta - format from mri_synthmorph - pattern: "*.{lta,txt,xfm,dat}" + description: Input transformation to convert. + pattern: "*.{nii,nii.gz,mgz,m3z,lta,txt,xfm,dat}" + mandatory: true ontologies: [] - - source: + - input_type: + type: string + description: | + Type of input transformation : + - affine : lta, fsl, mni, reg, niftyreg, itk, vox + - deform : m3z, fsl, lps, itk, ras, vox + mandatory: true + choices: + - lta + - fsl + - mni + - reg + - niftyreg + - itk + - vox + - m3z + - fsl + - lps + - ras + - vox + - output_type: + type: string + description: | + Type of output transformation : + - affine : lta, fsl, mni, reg, niftyreg, itk + - deform : m3z, fsl, lps, itk, ras, vox + mandatory: true + choices: + - lta + - fsl + - mni + - reg + - niftyreg + - itk + - vox + - m3z + - fsl + - lps + - ras + - vox + - reference: type: file - description: Moving Nifti volume used for registration. Defines source image - geometry + description: | + Reference volume used for registration. For affine transformations, it's usually the target + image, and for warp the moving (source) image. Validate with the tools you used to generate + the transformation. pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - target: + - affine_source: type: file - description: Fixed Nifti volume used for registration. Defines target image - geometry. (optional) + description: ONLY USED FOR CONVERSION OF AFFINES IN LTA FORMAT. Moving (source) Nifti volume used for registration. pattern: "*.{nii,nii.gz}" + mandatory: false ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - fs_license: @@ -55,30 +112,21 @@ input: Optional. If you have already set your license as prescribed by Freesurfer (copied to a .license file in your $FREESURFER_HOME), this is not required. pattern: "*.txt" + mandatory: true ontologies: [] output: - deform_transform: + transformation: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*out_warp.{nii,nii.gz,mgz,m3z}": + - "*out_{affine,warp}.{nii,nii.gz,mgz,m3z,txt,lta,mat,dat}": type: file - description: Deform transform. Default usage outputs ANTs (ITK) format .nii.gz - pattern: "*out_warp.{nii,nii.gz,mgz,m3z}" + description: Affine or warp converted to the desired format. + pattern: "*out_{affine,warp}.{nii,nii.gz,mgz,m3z,txt,lta,mat,dat}" ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - affine_transform: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'test', single_end:false ]` - - "*out_affine.{txt,lta,mat,dat}": - type: map - description: Affine transform. Default usage outputs ANTs (ITK) format .txt - pattern: "*out_affine.{txt,lta,mat,dat}" versions: - versions.yml: type: file @@ -88,3 +136,4 @@ output: - edam: http://edamontology.org/format_3750 # YAML authors: - "@anroy1" + - "@AlexVCaron" diff --git a/modules/nf-neuro/registration/convert/tests/main.nf.test b/modules/nf-neuro/registration/convert/tests/main.nf.test index bfb5ad0b..086b8702 100644 --- a/modules/nf-neuro/registration/convert/tests/main.nf.test +++ b/modules/nf-neuro/registration/convert/tests/main.nf.test @@ -3,6 +3,7 @@ nextflow_process { name "Test Process REGISTRATION_CONVERT" script "../main.nf" process "REGISTRATION_CONVERT" + config "./nextflow.config" tag "modules" tag "modules_nfneuro" @@ -25,10 +26,7 @@ nextflow_process { } } - test("registration - convert - default") { - - config "./nextflow_default.config" - + test("registration - convert - deformation - ras to itk (lps)") { when { process { """ @@ -38,29 +36,28 @@ nextflow_process { reslice: it.simpleName == "freesurfer_reslice" transforms: it.simpleName == "freesurfer_transforms" } - ch_transforms = ch_split_test_data.transforms.map{ + ch_transform = ch_split_test_data.transforms.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/fs_deform.nii.gz"), - [] + file("\${test_data_directory}/fs_deform.nii.gz") ] } - ch_reslice = ch_split_test_data.reslice.map{ + ch_reference = ch_split_test_data.reslice.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/t1_reslice.nii.gz"), - [] + file("\${test_data_directory}/t1_reslice.nii.gz") ] } - ch_freesurfer = ch_split_test_data.freesurfer.map{ + ch_license = ch_split_test_data.freesurfer.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/license.txt") ] } - input[0] = ch_transforms - .join(ch_reslice) - .join(ch_freesurfer) + input[0] = ch_transform + .join(ch_reference) + .join(ch_license) + .map{ meta, transform, reference, license -> [ meta, transform, "ras", "lps", reference, [], license ] } """ } } @@ -73,10 +70,7 @@ nextflow_process { } } - test("registration - convert - affine") { - - config "./nextflow_default.config" - + test("registration - convert - affine - lta to fsl") { when { process { """ @@ -86,29 +80,28 @@ nextflow_process { reslice: it.simpleName == "freesurfer_reslice" transforms: it.simpleName == "freesurfer_transforms" } - ch_transforms = ch_split_test_data.transforms.map{ + ch_transform = ch_split_test_data.transforms.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/fs_deform.nii.gz"), file("\${test_data_directory}/fs_affine.lta") ] } - ch_reslice = ch_split_test_data.reslice.map{ + ch_reference = ch_split_test_data.reslice.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/t1_reslice.nii.gz"), file("\${test_data_directory}/fa_reslice.nii.gz") ] } - ch_freesurfer = ch_split_test_data.freesurfer.map{ + ch_license = ch_split_test_data.freesurfer.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/license.txt") ] } - input[0] = ch_transforms - .join(ch_reslice) - .join(ch_freesurfer) + input[0] = ch_transform + .join(ch_reference) + .join(ch_license) + .map{ meta, transform, reference, license -> [ meta, transform, "lta", "fsl", reference, [], license ] } """ } } @@ -121,10 +114,7 @@ nextflow_process { } } - test("registration - convert - fsants") { - - config "./nextflow_fsants.config" - + test("registration - convert - mixed - to itk") { when { process { """ @@ -136,75 +126,34 @@ nextflow_process { } ch_transforms = ch_split_test_data.transforms.map{ test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/fs_deform.nii.gz"), - file("\${test_data_directory}/fs_affine.lta") + [[ id:'test1' ], [ id:'test2' ]], + [ + file("\${test_data_directory}/fs_deform.nii.gz"), + file("\${test_data_directory}/fs_affine.lta") + ], + [ "ras", "lta" ], + [ "itk" ] ] } - ch_reslice = ch_split_test_data.reslice.map{ + ch_reference = ch_split_test_data.reslice.map{ test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/t1_reslice.nii.gz"), - file("\${test_data_directory}/fa_reslice.nii.gz") + [[ id:'test1' ], [ id:'test2' ]], + [ + file("\${test_data_directory}/t1_reslice.nii.gz"), + file("\${test_data_directory}/fa_reslice.nii.gz") + ] ] } - ch_freesurfer = ch_split_test_data.freesurfer.map{ - test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/license.txt") - ] + ch_license = ch_split_test_data.freesurfer.map{ + test_data_directory -> file("\${test_data_directory}/license.txt") } input[0] = ch_transforms - .join(ch_reslice) - .join(ch_freesurfer) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - } - - test("registration - convert - fslfs") { - - config "./nextflow_fslfs.config" - - when { - process { - """ - ch_split_test_data = LOAD_DATA.out.test_data_directory - .branch{ - freesurfer: it.simpleName == "freesurfer" - reslice: it.simpleName == "freesurfer_reslice" - transforms: it.simpleName == "freesurfer_transforms" + .join(ch_reference) + .transpose() + .combine(ch_license) + .map{ meta, transform, intype, outtype, reference, license -> + [ meta, transform, intype, outtype, reference, [], license ] } - ch_transforms = ch_split_test_data.transforms.map{ - test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/fsl_deform.nii.gz"), - file("\${test_data_directory}/fsl_affine.mat") - ] - } - ch_reslice = ch_split_test_data.reslice.map{ - test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/t1_reslice.nii.gz"), - file("\${test_data_directory}/fa_reslice.nii.gz") - ] - } - ch_freesurfer = ch_split_test_data.freesurfer.map{ - test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/license.txt") - ] - } - input[0] = ch_transforms - .join(ch_reslice) - .join(ch_freesurfer) """ } } @@ -212,19 +161,12 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot( - file(process.out.affine_transform.get(0).get(1)).name, - process.out.deform_transform, - process.out.versions - ).match() } + { assert snapshot(process.out).match() } ) } } - test("registration - convert - fslants") { - - config "./nextflow_fslants.config" - + test("registration - convert - affine - to lta (needs source)") { when { process { """ @@ -234,77 +176,35 @@ nextflow_process { reslice: it.simpleName == "freesurfer_reslice" transforms: it.simpleName == "freesurfer_transforms" } - ch_transforms = ch_split_test_data.transforms.map{ + ch_transform = ch_split_test_data.transforms.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/fsl_deform.nii.gz"), file("\${test_data_directory}/fsl_affine.mat") ] } - ch_reslice = ch_split_test_data.reslice.map{ + ch_reference = ch_split_test_data.reslice.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/t1_reslice.nii.gz"), file("\${test_data_directory}/fa_reslice.nii.gz") ] } - ch_freesurfer = ch_split_test_data.freesurfer.map{ + ch_source = ch_split_test_data.reslice.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/license.txt") + file("\${test_data_directory}/t1_reslice.nii.gz") ] } - input[0] = ch_transforms - .join(ch_reslice) - .join(ch_freesurfer) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - } - - test("registration - convert - antsfs") { - - config "./nextflow_antsfs.config" - - when { - process { - """ - ch_split_test_data = LOAD_DATA.out.test_data_directory - .branch{ - freesurfer: it.simpleName == "freesurfer" - reslice: it.simpleName == "freesurfer_reslice" - transforms: it.simpleName == "freesurfer_transforms" - } - ch_transforms = ch_split_test_data.transforms.map{ - test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/ants_deform.nii.gz"), - file("\${test_data_directory}/ants_affine.txt") - ] - } - ch_reslice = ch_split_test_data.reslice.map{ - test_data_directory -> [ - [ id:'test' ], - file("\${test_data_directory}/t1_reslice.nii.gz"), - file("\${test_data_directory}/fa_reslice.nii.gz") - ] - } - ch_freesurfer = ch_split_test_data.freesurfer.map{ + ch_license = ch_split_test_data.freesurfer.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/license.txt") ] } - input[0] = ch_transforms - .join(ch_reslice) - .join(ch_freesurfer) + input[0] = ch_transform + .map{ meta, transform -> [meta, transform, "fsl", "lta"] } + .join(ch_reference) + .join(ch_source) + .join(ch_license) """ } } @@ -313,8 +213,7 @@ nextflow_process { assertAll( { assert process.success }, { assert snapshot( - file(process.out.affine_transform.get(0).get(1)).name, - process.out.deform_transform, + file(process.out.transformation.get(0).get(1)).name, process.out.versions ).match() } ) @@ -334,29 +233,28 @@ nextflow_process { reslice: it.simpleName == "freesurfer_reslice" transforms: it.simpleName == "freesurfer_transforms" } - ch_transforms = ch_split_test_data.transforms.map{ + ch_transform = ch_split_test_data.transforms.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/fs_deform.nii.gz"), - [] ] } - ch_reslice = ch_split_test_data.reslice.map{ + ch_reference = ch_split_test_data.reslice.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/t1_reslice.nii.gz"), - [] ] } - ch_freesurfer = ch_split_test_data.freesurfer.map{ + ch_license = ch_split_test_data.freesurfer.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/license.txt") ] } - input[0] = ch_transforms - .join(ch_reslice) - .join(ch_freesurfer) + input[0] = ch_transform + .join(ch_reference) + .join(ch_license) + .map{ meta, transform, reference, license -> [ meta, transform, "ras", "itk", reference, [], license ] } """ } } diff --git a/modules/nf-neuro/registration/convert/tests/main.nf.test.snap b/modules/nf-neuro/registration/convert/tests/main.nf.test.snap index 86d456c7..a5d2e898 100644 --- a/modules/nf-neuro/registration/convert/tests/main.nf.test.snap +++ b/modules/nf-neuro/registration/convert/tests/main.nf.test.snap @@ -1,54 +1,30 @@ { - "registration - convert - fsants": { + "registration - convert - stub-run": { "content": [ - { - "0": [ - [ - { - "id": "test" - }, - "test__out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" - ] - ], - "1": [ - [ - { - "id": "test" - }, - "test__out_affine.txt:md5,5f989a979be61faa578ad619377a8a07" - ] - ], - "2": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" - ], - "affine_transform": [ - [ - { - "id": "test" - }, - "test__out_affine.txt:md5,5f989a979be61faa578ad619377a8a07" - ] - ], - "deform_transform": [ - [ - { - "id": "test" - }, - "test__out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" - ] - ], - "versions": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" - ] - } + [ + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" + ] ], "meta": { "nf-test": "0.9.0", - "nextflow": "25.04.3" + "nextflow": "25.04.7" }, - "timestamp": "2025-06-04T03:19:41.033223181" + "timestamp": "2025-10-02T20:44:37.839613055" }, - "registration - convert - default": { + "registration - convert - affine - to lta (needs source)": { + "content": [ + "test_out_affine.lta", + [ + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "25.04.8" + }, + "timestamp": "2025-10-16T15:03:48.505663297" + }, + "registration - convert - affine - lta to fsl": { "content": [ { "0": [ @@ -56,108 +32,65 @@ { "id": "test" }, - "test__out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + "test_out_affine.mat:md5,c88fcae39c94d8af95ca00592c2f2634" ] ], "1": [ - + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" ], - "2": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" - ], - "affine_transform": [ - - ], - "deform_transform": [ + "transformation": [ [ { "id": "test" }, - "test__out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + "test_out_affine.mat:md5,c88fcae39c94d8af95ca00592c2f2634" ] ], "versions": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.3" - }, - "timestamp": "2025-06-04T03:23:23.394821601" - }, - "registration - convert - antsfs": { - "content": [ - "test__out_affine.lta", - [ - [ - { - "id": "test" - }, - "test__out_warp.nii.gz:md5,599fbe3a6b85d61c0c67cea8be4972b7" - ] - ], - [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.3" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-06-04T03:22:34.322609153" + "timestamp": "2025-10-29T11:13:34.703328482" }, - "registration - convert - fslants": { + "registration - convert - mixed - to itk": { "content": [ { "0": [ [ { - "id": "test" + "id": "test1" }, - "test__out_warp.nii.gz:md5,94feb8f0e648256eaa5ae0a47e5702c6" + "test1_out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" ] ], "1": [ - [ - { - "id": "test" - }, - "test__out_affine.txt:md5,074e8ac5777a91ba0808cd58c5a0cc44" - ] - ], - "2": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" ], - "affine_transform": [ + "transformation": [ [ { - "id": "test" + "id": "test1" }, - "test__out_affine.txt:md5,074e8ac5777a91ba0808cd58c5a0cc44" - ] - ], - "deform_transform": [ - [ - { - "id": "test" - }, - "test__out_warp.nii.gz:md5,94feb8f0e648256eaa5ae0a47e5702c6" + "test1_out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" ] ], "versions": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.3" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-06-04T03:24:33.171811516" + "timestamp": "2025-10-29T11:13:43.762952295" }, - "registration - convert - affine": { + "registration - convert - deformation - ras to itk (lps)": { "content": [ { "0": [ @@ -165,66 +98,29 @@ { "id": "test" }, - "test__out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + "test_out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" ] ], "1": [ - [ - { - "id": "test" - }, - "test__out_affine.txt:md5,5f989a979be61faa578ad619377a8a07" - ] - ], - "2": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" - ], - "affine_transform": [ - [ - { - "id": "test" - }, - "test__out_affine.txt:md5,5f989a979be61faa578ad619377a8a07" - ] + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" ], - "deform_transform": [ + "transformation": [ [ { "id": "test" }, - "test__out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + "test_out_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" ] ], "versions": [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + "versions.yml:md5,8fa42279d7793bb057cd5680bcb241e7" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.3" - }, - "timestamp": "2025-06-04T03:21:57.24523274" - }, - "registration - convert - fslfs": { - "content": [ - "test__out_affine.lta", - [ - [ - { - "id": "test" - }, - "test__out_warp.nii.gz:md5,df432a27c586e57cad93af7509941cd4" - ] - ], - [ - "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.3" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-06-04T03:15:42.966436752" + "timestamp": "2025-10-29T11:13:25.855140969" } -} +} \ No newline at end of file diff --git a/modules/nf-neuro/registration/convert/tests/nextflow_default.config b/modules/nf-neuro/registration/convert/tests/nextflow.config similarity index 100% rename from modules/nf-neuro/registration/convert/tests/nextflow_default.config rename to modules/nf-neuro/registration/convert/tests/nextflow.config diff --git a/modules/nf-neuro/registration/convert/tests/nextflow_antsfs.config b/modules/nf-neuro/registration/convert/tests/nextflow_antsfs.config deleted file mode 100644 index 0b93daa8..00000000 --- a/modules/nf-neuro/registration/convert/tests/nextflow_antsfs.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - withName: "REGISTRATION_CONVERT" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.in_format_affine = "itk" - ext.out_format_affine = "lta" - ext.in_format_deform = "itk" - ext.out_format_deform = "ras" - } -} diff --git a/modules/nf-neuro/registration/convert/tests/nextflow_fsants.config b/modules/nf-neuro/registration/convert/tests/nextflow_fsants.config deleted file mode 100644 index 000a4d92..00000000 --- a/modules/nf-neuro/registration/convert/tests/nextflow_fsants.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - withName: "REGISTRATION_CONVERT" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.in_format_affine = "lta" - ext.out_format_affine = "itk" - ext.in_format_deform = "ras" - ext.out_format_deform = "itk" - } -} diff --git a/modules/nf-neuro/registration/convert/tests/nextflow_fslants.config b/modules/nf-neuro/registration/convert/tests/nextflow_fslants.config deleted file mode 100644 index 82e10d53..00000000 --- a/modules/nf-neuro/registration/convert/tests/nextflow_fslants.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - withName: "REGISTRATION_CONVERT" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.in_format_affine = "fsl" - ext.out_format_affine = "itk" - ext.in_format_deform = "fsl" - ext.out_format_deform = "itk" - } -} diff --git a/modules/nf-neuro/registration/convert/tests/nextflow_fslfs.config b/modules/nf-neuro/registration/convert/tests/nextflow_fslfs.config deleted file mode 100644 index 6e1b2e0c..00000000 --- a/modules/nf-neuro/registration/convert/tests/nextflow_fslfs.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - withName: "REGISTRATION_CONVERT" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.in_format_affine = "fsl" - ext.out_format_affine = "lta" - ext.in_format_deform = "fsl" - ext.out_format_deform = "ras" - } -} diff --git a/modules/nf-neuro/registration/easyreg/main.nf b/modules/nf-neuro/registration/easyreg/main.nf index e81c2095..1058b798 100644 --- a/modules/nf-neuro/registration/easyreg/main.nf +++ b/modules/nf-neuro/registration/easyreg/main.nf @@ -2,58 +2,43 @@ process REGISTRATION_EASYREG { tag "$meta.id" - label 'process_single' label 'process_high' container "freesurfer/freesurfer:7.4.1" input: - tuple val(meta), path(reference), path(floating), path(ref_segmentation), path(flo_segmentation) + tuple val(meta), path(fixed_image), path(moving_image), path(fixed_segmentation), path(moving_segmentation) output: - tuple val(meta), path("*_reference_registered.nii.gz") , emit: ref_reg - tuple val(meta), path("*_floating_registered.nii.gz") , emit: flo_reg - tuple val(meta), path("*_reference_segmentation.nii.gz") , emit: ref_seg, optional: true - tuple val(meta), path("*_floating_segmentation.nii.gz") , emit: flo_seg, optional: true - tuple val(meta), path("*_forward_field.nii.gz") , emit: fwd_field, optional: true - tuple val(meta), path("*_backward_field.nii.gz") , emit: bak_field, optional: true - path "versions.yml" , emit: versions + tuple val(meta), path("*_warped.nii.gz") , emit: image_warped + tuple val(meta), path("*_warped_reference.nii.gz") , emit: fixed_warped + tuple val(meta), path("*_forward0_warp.nii.gz") , emit: forward_warp, optional: true + tuple val(meta), path("*_backward0_warp.nii.gz") , emit: backward_warp, optional: true + tuple val(meta), path("*_warped_segmentation.nii.gz") , emit: segmentation_warped, optional: true + tuple val(meta), path("*_warped_reference_segmentation.nii.gz") , emit: fixed_segmentation_warped, optional: true + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when script: def prefix = task.ext.prefix ?: "${meta.id}" - def field = task.ext.field ? "--fwd_field ${prefix}_forward_field.nii.gz --bak_field ${prefix}_backward_field.nii.gz " : "" - def threads = task.ext.threads ? "--threads " + task.ext.threads : "" - def affine = task.ext.affine ? "--affine_only " : "" - + def affine_only = task.ext.affine_only ? "--affine_only " : "" + fixed_segmentation = "--ref_seg ${fixed_segmentation ?: "${prefix}_warped_segmentation.nii.gz" }" + moving_segmentation = "--flo_seg ${moving_segmentation ?: "${prefix}_warped_reference_segmentation.nii.gz" }" """ export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 - if [[ -f "$ref_segmentation" ]]; - then - reference_segmentation=$ref_segmentation - else - reference_segmentation="${prefix}_reference_segmentation.nii.gz" - fi - - if [[ -f "$flo_segmentation" ]]; - then - floating_segmentation=$flo_segmentation - else - floating_segmentation="${prefix}_floating_segmentation.nii.gz" - fi - - mri_easyreg \ - --ref $reference --flo $floating \ - --ref_seg \${reference_segmentation} \ - --flo_seg \${floating_segmentation} \ - --flo_reg ${prefix}_floating_registered.nii.gz \ - --ref_reg ${prefix}_reference_registered.nii.gz \ - $field $threads $affine + mri_easyreg --ref $fixed_image \ + --flo $moving_image \ + --flo_reg ${prefix}_warped.nii.gz \ + --ref_reg ${prefix}_warped_reference.nii.gz \ + --fwd_field ${prefix}_forward0_warp.nii.gz \ + --bak_field ${prefix}_backward0_warp.nii.gz \ + $fixed_segmentation $moving_segmentation \ + --threads $task.cpus $affine_only cat <<-END_VERSIONS > versions.yml "${task.process}": @@ -67,12 +52,12 @@ process REGISTRATION_EASYREG { """ mri_easyreg -h - touch ${prefix}_reference_registered.nii.gz - touch ${prefix}_floating_registered.nii.gz - touch ${prefix}_reference_segmentation.nii.gz - touch ${prefix}_floating_segmentation.nii.gz - touch ${prefix}_forward_field.nii.gz - touch ${prefix}_backward_field.nii.gz + touch ${prefix}_warped.nii.gz + touch ${prefix}_warped_reference.nii.gz + touch ${prefix}_warped_segmentation.nii.gz + touch ${prefix}_warped_reference_segmentation.nii.gz + touch ${prefix}_forward0_warp.nii.gz + touch ${prefix}_backward0_warp.nii.gz cat <<-END_VERSIONS > versions.yml "${task.process}": diff --git a/modules/nf-neuro/registration/easyreg/meta.yml b/modules/nf-neuro/registration/easyreg/meta.yml index e47e6f6d..45af4f3a 100644 --- a/modules/nf-neuro/registration/easyreg/meta.yml +++ b/modules/nf-neuro/registration/easyreg/meta.yml @@ -15,124 +15,130 @@ tools: homepage: https://surfer.nmr.mgh.harvard.edu/ doi: 10.1016/j.neuroimage.2012.01.021 identifier: "" +args: + - affine_only: + type: boolean + description: Only perform affine registration + default: false input: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1', single_end:false ]` - - reference: + - fixed_image: type: file - description: the reference image in .nii(.gz) or .mgz format (note that, since - the method is symmetric, the choice of reference vs floating is arbitrary). + description: Reference image in .nii(.gz) or .mgz format (note that, since the method is symmetric, the choice of reference vs floating is arbitrary). pattern: "*.{nii,nii.gz,mgz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - floating: + - moving_image: type: file - description: the floating image in .nii(.gz) or .mgz format. + description: Image to register in .nii(.gz) or .mgz format. pattern: "*.{nii,nii.gz,mgz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - ref_segmentation: + - fixed_segmentation: type: file - description: file with the SynthSeg v2 (non-robust) segmentation + parcellation - of the reference image. If it does not exist, EasyReg will create it. If it - already exists (e.g., from a previous EasyReg run), then EasyReg will read - it from disk (which is faster than segmenting). + description: | + File with the SynthSeg v2 (non-robust) segmentation + parcellation of the reference image. + If it does not exist, EasyReg will create it. pattern: "*.{nii,nii.gz}" + mandatory: false ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - flo_segmentation: + - moving_segmentation: type: file - description: file with the SynthSeg v2 (non-robust) segmentation + parcellation - of the floating image. If it does not exist, EasyReg will create it. If it - already exists (e.g., from a previous EasyReg run), then EasyReg will read - it from disk (which is faster than segmenting). + description: | + File with the SynthSeg v2 (non-robust) segmentation + parcellation of the floating image. + If it does not exist, EasyReg will create it. pattern: "*.{nii,nii.gz}" + mandatory: false ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format output: - ref_reg: + image_warped: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1', single_end:false ]` - - "*_reference_registered.nii.gz": + - "*_warped.nii.gz": type: file - description: this is the file where the deformed (registered) reference image - is written. - pattern: "*_reference_registered.nii.gz" + description: Image warped onto the reference. + pattern: "*_warped.nii.gz" ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format - flo_reg: + - edam: http://edamontology.org/format_4001 # NIFTI format + fixed_warped: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1', single_end:false ]` - - "*_floating_registered.nii.gz": + - "*_warped_reference.nii.gz": type: file - description: this is the file where the deformed (registered) floating image - is written. - pattern: "*_floating_registered.nii.gz" + description: Reference warped. + pattern: "*_warped_reference.nii.gz" ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format - ref_seg: + - edam: http://edamontology.org/format_4001 # NIFTI format + forward_warp: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1', single_end:false ]` - - "*_reference_segmentation.nii.gz": + - "*_forward0_warp.nii.gz": type: file - description: file with the SynthSeg v2 (non-robust) segmentation + parcellation - of the reference image. Will produce image only if not passed as input. - pattern: "*_reference_segmentation.nii.gz" + description: Forward deformation field, composed of all registration stages (affine+deformation). + pattern: "*_forward0_warp.nii.gz" + optional: true ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format - flo_seg: + - edam: http://edamontology.org/format_4001 # NIFTI format + backward_warp: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1', single_end:false ]` - - "*_floating_segmentation.nii.gz": + - "*_backward0_warp.nii.gz": type: file - description: file with the SynthSeg v2 (non-robust) segmentation + parcellation - of the floating image. Will produce image only if not passed as input. - pattern: "*_floating_segmentation.nii.gz" + description: Backward deformation field, composed of all registration stages (inv-deformation+inv-affine). + pattern: "*_backward0_warp.nii.gz" + optional: true ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format - fwd_field: + - edam: http://edamontology.org/format_4001 # NIFTI format + segmentation_warped: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1', single_end:false ]` - - "*_forward_field.nii.gz": + - "*_warped_segmentation.nii.gz": type: file - description: this is the file where the forward deformation field is written. - The deformation includes both the affine and nonlinear components. Must - be a nifti (.nii/.nii.gz) or .mgz file; it is encoded as the real world - (RAS) coordinates of the target location for each voxel. - pattern: "*_forward_field.nii.gz" + description: | + SynthSeg v2 (non-robust) segmentation + parcellation on the warped image. + Will produce image only if not passed as input. + pattern: "*_warped_segmentation.nii.gz" + optional: true ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format - bak_field: + - edam: http://edamontology.org/format_4001 # NIFTI format + fixed_segmentation_warped: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1', single_end:false ]` - - "*_backward_field.nii.gz": + - "*_warped_reference_segmentation.nii.gz": type: file - description: this is the file where the backward deformation field is written. - It must also be a nifty or mgz file. - pattern: "*_backward_field.nii.gz" + description: | + SynthSeg v2 (non-robust) segmentation + parcellation on the reference image. + Will produce image only if not passed as input. + pattern: "*_warped_reference_segmentation.nii.gz" + optional: true ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format + - edam: http://edamontology.org/format_4001 # NIFTI format versions: - versions.yml: type: file @@ -142,5 +148,6 @@ output: - edam: http://edamontology.org/format_3750 # YAML authors: - "@ThoumyreStanislas" + - "@AlexVCaron" maintainers: - "@ThoumyreStanislas" diff --git a/modules/nf-neuro/registration/easyreg/tests/main.nf.test b/modules/nf-neuro/registration/easyreg/tests/main.nf.test index 40f35f4f..17b9834c 100644 --- a/modules/nf-neuro/registration/easyreg/tests/main.nf.test +++ b/modules/nf-neuro/registration/easyreg/tests/main.nf.test @@ -44,13 +44,12 @@ nextflow_process { ch_b0 = ch_split_test_data.b0.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz"), - [], - [] + file("\${test_data_directory}/b0.nii.gz") ] } input[0] = ch_t1 .join(ch_b0) + .map{ meta, t1, b0 -> [meta, t1, b0, [], []] } """ } } @@ -58,12 +57,12 @@ nextflow_process { assertAll( { assert process.success }, { assert snapshot( - file(process.out.ref_seg.get(0).get(1)).name, - file(process.out.flo_seg.get(0).get(1)).name, - niftiMD5SUM(process.out.ref_reg.get(0).get(1), 6), - niftiMD5SUM(process.out.flo_reg.get(0).get(1), 6), - niftiMD5SUM(process.out.fwd_field.get(0).get(1), 6), - niftiMD5SUM(process.out.bak_field.get(0).get(1), 6), + file(process.out.fixed_segmentation_warped.get(0).get(1)).name, + file(process.out.segmentation_warped.get(0).get(1)).name, + niftiMD5SUM(process.out.fixed_warped.get(0).get(1), 6), + niftiMD5SUM(process.out.image_warped.get(0).get(1), 6), + niftiMD5SUM(process.out.forward_warp.get(0).get(1), 6), + niftiMD5SUM(process.out.backward_warp.get(0).get(1), 6), process.out.versions ).match() } ) @@ -90,13 +89,12 @@ nextflow_process { ch_b0 = ch_split_test_data.b0.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz"), - [], - [] + file("\${test_data_directory}/b0.nii.gz") ] } input[0] = ch_t1 .join(ch_b0) + .map{ meta, t1, b0 -> [meta, t1, b0, [], []] } """ } } diff --git a/modules/nf-neuro/registration/easyreg/tests/main.nf.test.snap b/modules/nf-neuro/registration/easyreg/tests/main.nf.test.snap index cb847b08..d9c53e2f 100644 --- a/modules/nf-neuro/registration/easyreg/tests/main.nf.test.snap +++ b/modules/nf-neuro/registration/easyreg/tests/main.nf.test.snap @@ -1,21 +1,21 @@ { "registration - easyreg": { "content": [ - "test_reference_segmentation.nii.gz", - "test_floating_segmentation.nii.gz", - "test_reference_registered.nii.gz:md5:header,c5e41f89848f91c53a9a7be44970d4b1,data,1501221fe23cd62bfdafb33367cadf4d", - "test_floating_registered.nii.gz:md5:header,ec5893cd9ea024e630c4444bd914c331,data,d75ae3fc935cd5cc70ea6797a4c70775", - "test_forward_field.nii.gz:md5:header,0db0a80786ff39864cc17506ed1a0146,data,e4fa6c626729cbf236c34680e5c76df4", - "test_backward_field.nii.gz:md5:header,74c92ee4cd3c4abfecf3da6cdb1d4650,data,8f26afa5c3de21469466213498542486", + "test_warped_reference_segmentation.nii.gz", + "test_warped_segmentation.nii.gz", + "test_warped_reference.nii.gz:md5:header,c5e41f89848f91c53a9a7be44970d4b1,data,1501221fe23cd62bfdafb33367cadf4d", + "test_warped.nii.gz:md5:header,ec5893cd9ea024e630c4444bd914c331,data,d75ae3fc935cd5cc70ea6797a4c70775", + "test_forward0_warp.nii.gz:md5:header,0db0a80786ff39864cc17506ed1a0146,data,e4fa6c626729cbf236c34680e5c76df4", + "test_backward0_warp.nii.gz:md5:header,74c92ee4cd3c4abfecf3da6cdb1d4650,data,8f26afa5c3de21469466213498542486", [ "versions.yml:md5,4661210880c42a986923e8257f64c760" ] ], "meta": { - "nf-test": "0.9.0-rc1", - "nextflow": "24.04.4" + "nf-test": "0.9.0", + "nextflow": "25.04.8" }, - "timestamp": "2024-09-12T16:01:24.00559" + "timestamp": "2025-10-16T15:11:21.026914183" }, "registration - easyreg - stub-run": { "content": [ @@ -25,8 +25,8 @@ ], "meta": { "nf-test": "0.9.0", - "nextflow": "25.04.2" + "nextflow": "25.04.6" }, - "timestamp": "2025-05-30T18:56:08.423992682" + "timestamp": "2025-07-19T01:06:31.625578918" } } \ No newline at end of file diff --git a/modules/nf-neuro/registration/easyreg/tests/nextflow.config b/modules/nf-neuro/registration/easyreg/tests/nextflow.config index 71494b4d..8873712e 100644 --- a/modules/nf-neuro/registration/easyreg/tests/nextflow.config +++ b/modules/nf-neuro/registration/easyreg/tests/nextflow.config @@ -1,9 +1,8 @@ process { - memory = '10G' withName: "REGISTRATION_EASYREG" { publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.field = true - ext.affine = true - ext.threads = 1 + ext.affine_only = true + memory = '10G' + cpus = 1 } } diff --git a/modules/nf-neuro/registration/synthmorph/environment.yml b/modules/nf-neuro/registration/synthmorph/environment.yml new file mode 100644 index 00000000..74450f6b --- /dev/null +++ b/modules/nf-neuro/registration/synthmorph/environment.yml @@ -0,0 +1,3 @@ +channels: [] +dependencies: [] +name: registration_synthmorph diff --git a/modules/nf-neuro/registration/synthmorph/main.nf b/modules/nf-neuro/registration/synthmorph/main.nf new file mode 100644 index 00000000..7082ba87 --- /dev/null +++ b/modules/nf-neuro/registration/synthmorph/main.nf @@ -0,0 +1,143 @@ +process REGISTRATION_SYNTHMORPH { + tag "$meta.id" + label 'process_high' + + container "freesurfer/synthmorph:4" + containerOptions { + (workflow.containerEngine == 'docker') ? '--entrypoint ""' : "" + } + + input: + tuple val(meta), path(fixed_image), path(moving_image) + + output: + tuple val(meta), path("*_warped.nii.gz") , emit: image_warped + tuple val(meta), path("*_forward{0,1,_standalone}_affine.lta") , emit: forward_affine, optional: true + tuple val(meta), path("*_forward0_deform.nii.gz") , emit: forward_warp, optional: true + tuple val(meta), path("*_backward1_deform.nii.gz") , emit: backward_warp, optional: true + tuple val(meta), path("*_backward{0,_standalone}_affine.lta") , emit: backward_affine, optional: true + tuple val(meta), path("*_forward[!_]*.{lta,nii.gz}", arity: '1..*') , emit: forward_image_transform + tuple val(meta), path("*_backward[!_]*.{lta,nii.gz}", arity: '1..*') , emit: backward_image_transform + tuple val(meta), path("*_backward[!_]*.{lta,nii.gz}", arity: '1..*') , emit: forward_tractogram_transform + tuple val(meta), path("*_forward[!_]*.{lta,nii.gz}", arity: '1..*') , emit: backward_tractogram_transform + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def prefix = task.ext.prefix ?: "${meta.id}" + + def models = task.ext.models ?: ["affine", "deform"] + def weights = (task.ext.weights ?: [null] * models.size()).collect{ it ? "-w $it" : "none" }.join(" ") + def use_gpu = task.ext.use_gpu ? "-g" : "" + def regularization = "-r ${task.ext.regularization ?: 0.5}" + def steps = "-n ${task.ext.steps ?: 7 }" + def extent = "-e ${task.ext.extent ?: 256}" + def update_header = task.ext.disable_resampling ? "-H" : "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + export CUDA_VISIBLE_DEVICES="-1" + + echo "Available memory : ${task.memory}" + + moving=$moving_image + mv $fixed_image fixed.nii.gz + + declare -A extension=( ["affine"]="lta" \ + ["rigid"]="lta" \ + ["deform"]="nii.gz" \ + ["joint"]="nii.gz" ) + + weights=( $weights ) + + i=0 + skip=0 + j=${models.size()} + initializer="" + init_assoc="" + for model in ${models.join(" ")}; do + echo "Processing model: \$model" + # Post-incrementation ensure no error on last index = 0 + ((j--)) + + weight="" + if [ "\${weights[i + skip]}" != "none" ]; then + weight="-w \${weights[i + skip]}" + + if [ "\$model" = "joint" ]; then + # Pre-incrementation ensure no error on first index = 0 + ((++skip)) + if [ "\${weights[i + skip]}" = "none" ]; then + echo "Joint deformations need 2 weights, only 1 given" + exit 1 + else + weight="\$weight -w \${weights[i + skip]}" + fi + fi + fi + + args="" + if [ \$model = "joint" ] || [ \$model = "deform" ]; then + args="$regularization $steps" + else + args="$update_header" + fi + + if [ \$initializer ]; then + args="\$args -i \$initializer" + fi + + mri_synthmorph register \$moving fixed.nii.gz -v -m \$model \$weight \$args \ + -t ${prefix}_forward\${j}_\$model.\${extension[\$model]} \ + -T ${prefix}_backward\${i}_\$model.\${extension[\$model]} \ + -o warped.nii.gz -j $task.cpus $extent $use_gpu + + if [ \$initializer ]; then + # Retag initializer file to standalone using sed + # - replace the number after forward/backward to standalone + mv \$initializer \$(echo "\$initializer" | sed -r 's/(_forward|_backward)[[:digit:]]+/\\1_standalone/') + mv \$init_assoc \$(echo "\$init_assoc" | sed -r 's/(_forward|_backward)[[:digit:]]+/\\1_standalone/') + fi + + if [ \${extension[\$model]} = "lta" ]; then + initializer=${prefix}_forward\${j}_\$model.\${extension[\$model]} + init_assoc=${prefix}_backward\${i}_\$model.\${extension[\$model]} + else + moving=warped.nii.gz + fi + + # Pre-incrementation ensure no error on first index = 0 + ((++i)) + + done + + mv warped.nii.gz ${prefix}_warped.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + synthmorph: 4 + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + mri_synthmorph -h + + touch ${prefix}_warped.nii.gz + touch ${prefix}_forward1_affine.lta + touch ${prefix}_forward0_warp.nii.gz + touch ${prefix}_backward1_warp.nii.gz + touch ${prefix}_backward0_affine.lta + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + synthmorph: 4 + END_VERSIONS + """ +} diff --git a/modules/nf-neuro/registration/synthmorph/meta.yml b/modules/nf-neuro/registration/synthmorph/meta.yml new file mode 100644 index 00000000..72c12183 --- /dev/null +++ b/modules/nf-neuro/registration/synthmorph/meta.yml @@ -0,0 +1,213 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +name: registration_synthmorph +description: Perform registration using SynthMorph from Freesurfer. Outputs transforms + in Freesurfer format .lta for affine and .nii.gz (synthmorph also supports .mgz) + for deform, both in RAS orientation. Conversion to other formats is done using lta_convert + and mri_warp_convert respectively, which support a wide range of conversion formats + and orientations, such as ANTs ans FSL. To convert the output of this module, use + the registration/convert module successively to it. Note that tests using synthmorph + are non-reproductible. +keywords: + - Registration + - Brain imaging + - MRI + - Synthetic + - AI + - CNN +tools: + - Freesurfer: + description: | + Open source neuroimaging toolkit for processing, analyzing, and visualizing human brain MR images. + homepage: https://surfer.nmr.mgh.harvard.edu/ + doi: 10.1016/j.neuroimage.2012.01.021 + identifier: "" + - SynthMorph: + description: "Synthmorph registration method" + homepage: "https://martinos.org/malte/synthmorph/" + doi: 10.1109/TMI.2021.3116879 + identifier: "" +args: + - models: + type: list + description: List of transformation models to apply to the moving image in series. + default: '["affine", "deform"]' + choices: + - rigid + - affine + - deform + - joint + - weights: + type: list + description: | + List of weights to apply to each model. Must supply one per model on none. + Use "none" to specify no weight for a given model. When using the "joint" + model, you must add 2 weights in the list for affine and warp. + default: "" + - use_gpu: + type: boolean + description: Use GPU to accelerate computations (must enable GPU support). + default: false + - regularization: + type: float + description: Regularizationto add to deformation steps. + default: 0.5 + - steps: + type: integer + description: Number of iteration for the optimisation of deformations. + default: 7 + - extent: + type: integer + description: Size of the deformation grid in voxels. + default: 256 + choices: + - 192 + - 256 + - disable_resampling: + type: boolean + description: Update image header instead of performing resampling of the image grid. + default: false +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - fixed_image: + type: file + description: Nifti volume fixed for registration + pattern: "*.{nii,nii.gz}" + mandatory: true + ontologies: + - edam: http://edamontology.org/format_4001 + - moving_image: + type: file + description: Nifti volume moving for registration + pattern: "*.{nii,nii.gz}" + mandatory: true + ontologies: + - edam: http://edamontology.org/format_4001 +output: + image_warped: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_warped.nii.gz": + type: file + description: Warped image + pattern: "*_warped.nii.gz" + ontologies: + - edam: http://edamontology.org/format_4001 # NIFTI format + forward_affine: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward{0,1,_standalone}_affine.lta": + type: file + description: Affine transformation matrix to fixed space. + pattern: "*_forward{0,1,_standalone}_affine.lta" + optional: true + ontologies: [] + forward_warp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward0_deform.nii.gz": + type: file + description: Deformation field to fixed space. + pattern: "*_forward0_deform.nii.gz" + optional: true + ontologies: + - edam: http://edamontology.org/format_4001 # NIFTI format + backward_warp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward1_deform.nii.gz": + type: file + description: Deformation field to moving space. + pattern: "*_backward1_deform.nii.gz" + optional: true + ontologies: + - edam: http://edamontology.org/format_4001 # NIFTI format + backward_affine: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward{0,_standalone}_affine.lta": + type: file + description: Affine transformation matrix to moving space. + pattern: "*_backward{0,_standalone}_affine.lta" + optional: true + ontologies: [] + forward_image_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward[!_]*.{lta,nii.gz}": + type: list + description: | + Tuple, transformation files to warp images to fixed space, in the correct order + for REGISTRATION_ANTSAPPLYTRANSFORMS : [ meta, [ forward_warp, forward_affine ] ]. + pattern: "*_forward[!_]*.{lta,nii.gz}" + ontologies: [] + backward_image_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward[!_]*.{lta,nii.gz}": + type: list + description: | + Tuple, transformation files to warp images to moving space, in the correct order for + REGISTRATION_ANTSAPPLYTRANSFORMS : [ meta, [ backward_affine, backward_warp ] ]. + pattern: "*_backward[!_]*.{lta,nii.gz}" + ontologies: [] + forward_tractogram_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_backward[!_]*.{lta,nii.gz}": + type: list + description: | + Tuple, transformation files to warp tractograms to fixed space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ backward_affine, backward_warp ] ]. + pattern: "*_backward[!_]*.{lta,nii.gz}" + ontologies: [] + backward_tractogram_transform: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*_forward[!_]*.{lta,nii.gz}": + type: list + description: | + Tuple, transformation files to warp tractograms to moving space, in the correct order + for REGISTRATION_TRANSFORMTRACTOGRAM : [ meta, [ forward_warp, forward_affine ] ]. + pattern: "*_forward[!_]*.{lta,nii.gz}" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: versions.yml + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@anroy1" + - "@AlexVCaron" diff --git a/modules/nf-neuro/registration/synthregistration/tests/main.nf.test b/modules/nf-neuro/registration/synthmorph/tests/main.nf.test similarity index 73% rename from modules/nf-neuro/registration/synthregistration/tests/main.nf.test rename to modules/nf-neuro/registration/synthmorph/tests/main.nf.test index a880ed8c..e2065f6a 100644 --- a/modules/nf-neuro/registration/synthregistration/tests/main.nf.test +++ b/modules/nf-neuro/registration/synthmorph/tests/main.nf.test @@ -1,13 +1,13 @@ nextflow_process { - name "Test Process REGISTRATION_SYNTHREGISTRATION" + name "Test Process REGISTRATION_SYNTHMORPH" script "../main.nf" - process "REGISTRATION_SYNTHREGISTRATION" + process "REGISTRATION_SYNTHMORPH" tag "modules" tag "modules_nfneuro" tag "registration" - tag "registration/synthregistration" + tag "registration/synthmorph" tag "subworkflows" tag "subworkflows/load_test_data" @@ -27,7 +27,7 @@ nextflow_process { } } - test("registration - synthregistration") { + test("registration - synthmorph") { config "./nextflow.config" when { process { @@ -45,16 +45,21 @@ nextflow_process { assertAll( { assert process.success }, { assert snapshot( - file(process.out.warped_image.get(0).get(1)).name, - file(process.out.warp.get(0).get(1)).name, - file(process.out.affine.get(0).get(1)).name, - process.out.versions + process.out + .findAll{ channel -> !channel.key.isInteger() && channel.value } + .collectEntries{ channel -> + [(channel.key): ["versions"].contains(channel.key) + ? channel.value + : channel.value.collect{ subject -> + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } + } ] + } ).match() } ) } } - test("registration - synthregistration - stub-run") { + test("registration - synthmorph - stub-run") { tag "stub" options "-stub-run" when { diff --git a/modules/nf-neuro/registration/synthmorph/tests/main.nf.test.snap b/modules/nf-neuro/registration/synthmorph/tests/main.nf.test.snap new file mode 100644 index 00000000..2f86a41a --- /dev/null +++ b/modules/nf-neuro/registration/synthmorph/tests/main.nf.test.snap @@ -0,0 +1,109 @@ +{ + "registration - synthmorph": { + "content": [ + { + "backward_affine": [ + [ + { + "id": "test", + "single_end": false + }, + "test_backward_standalone_affine.lta" + ] + ], + "backward_image_transform": [ + [ + { + "id": "test", + "single_end": false + }, + "test_backward1_deform.nii.gz" + ] + ], + "backward_tractogram_transform": [ + [ + { + "id": "test", + "single_end": false + }, + "test_forward0_deform.nii.gz" + ] + ], + "backward_warp": [ + [ + { + "id": "test", + "single_end": false + }, + "test_backward1_deform.nii.gz" + ] + ], + "forward_affine": [ + [ + { + "id": "test", + "single_end": false + }, + "test_forward_standalone_affine.lta" + ] + ], + "forward_image_transform": [ + [ + { + "id": "test", + "single_end": false + }, + "test_forward0_deform.nii.gz" + ] + ], + "forward_tractogram_transform": [ + [ + { + "id": "test", + "single_end": false + }, + "test_backward1_deform.nii.gz" + ] + ], + "forward_warp": [ + [ + { + "id": "test", + "single_end": false + }, + "test_forward0_deform.nii.gz" + ] + ], + "image_warped": [ + [ + { + "id": "test", + "single_end": false + }, + "test_warped.nii.gz" + ] + ], + "versions": [ + "versions.yml:md5,0375426411588cd1712e84ad66aab5ca" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "25.04.8" + }, + "timestamp": "2025-10-16T19:51:21.67628815" + }, + "registration - synthmorph - stub-run": { + "content": [ + [ + "versions.yml:md5,0375426411588cd1712e84ad66aab5ca" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "25.04.8" + }, + "timestamp": "2025-10-16T19:51:37.175530527" + } +} \ No newline at end of file diff --git a/modules/nf-neuro/registration/synthmorph/tests/nextflow.config b/modules/nf-neuro/registration/synthmorph/tests/nextflow.config new file mode 100644 index 00000000..b5a2b4fa --- /dev/null +++ b/modules/nf-neuro/registration/synthmorph/tests/nextflow.config @@ -0,0 +1,10 @@ +process { + withName: "REGISTRATION_SYNTHMORPH" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.models = ["affine", "deform"] + ext.regularization = 0.9 + ext.steps = 9 + ext.extent = 192 + memory = '18G' + } +} diff --git a/modules/nf-neuro/registration/synthmorph/tests/tags.yml b/modules/nf-neuro/registration/synthmorph/tests/tags.yml new file mode 100644 index 00000000..8e3b6188 --- /dev/null +++ b/modules/nf-neuro/registration/synthmorph/tests/tags.yml @@ -0,0 +1,2 @@ +registration/synthmorph: + - "modules/nf-neuro/registration/synthmorph/**" diff --git a/modules/nf-neuro/registration/synthregistration/environment.yml b/modules/nf-neuro/registration/synthregistration/environment.yml deleted file mode 100644 index 13b71ee4..00000000 --- a/modules/nf-neuro/registration/synthregistration/environment.yml +++ /dev/null @@ -1,3 +0,0 @@ -channels: [] -dependencies: [] -name: registration_synthregistration diff --git a/modules/nf-neuro/registration/synthregistration/main.nf b/modules/nf-neuro/registration/synthregistration/main.nf deleted file mode 100644 index 87eb32c7..00000000 --- a/modules/nf-neuro/registration/synthregistration/main.nf +++ /dev/null @@ -1,62 +0,0 @@ -process REGISTRATION_SYNTHREGISTRATION { - tag "$meta.id" - label 'process_high' - - container "freesurfer/synthmorph:4" - containerOptions { - (workflow.containerEngine == 'docker') ? '--entrypoint "" --env PYTHONPATH="/freesurfer/env/lib/python3.11/site-packages"' : "--env PYTHONPATH='/freesurfer/env/lib/python3.11/site-packages'" - } - - input: - tuple val(meta), path(moving), path(fixed) - - output: - tuple val(meta), path("*__output_warped.nii.gz") , emit: warped_image - tuple val(meta), path("*__deform_warp.nii.gz") , emit: warp - tuple val(meta), path("*__affine_warp.lta") , emit: affine - path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when - - script: - def prefix = task.ext.prefix ?: "${meta.id}" - - def affine = task.ext.affine ? "-m " + task.ext.affine : "-m affine" - def warp = task.ext.warp ? "-m " + task.ext.warp : "-m deform" - def header = task.ext.header ? "-H" : "" - def gpu = task.ext.gpu ? "-g" : "" - def lambda = task.ext.lambda ? "-r " + task.ext.lambda : "" - def steps = task.ext.steps ? "-n " + task.ext.steps : "" - def extent = task.ext.extent ? "-e " + task.ext.extent : "" - def weight = task.ext.weight ? "-w " + task.ext.weight : "" - - """ - export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1 - export OMP_NUM_THREADS=1 - export OPENBLAS_NUM_THREADS=1 - mri_synthmorph -j $task.cpus ${affine} -t ${prefix}__affine_warp.lta $moving $fixed - mri_synthmorph -j $task.cpus ${warp} ${gpu} ${lambda} ${steps} ${extent} ${weight} -i ${prefix}__affine_warp.lta -t ${prefix}__deform_warp.nii.gz -o ${prefix}__output_warped.nii.gz $moving $fixed - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - synthmoprh: 4 - END_VERSIONS - """ - - stub: - def prefix = task.ext.prefix ?: "${meta.id}" - - """ - mri_synthmorph -h - - touch ${prefix}__output_warped.nii.gz - touch ${prefix}__deform_warp.nii.gz - touch ${prefix}__affine_warp.lta - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - synthmoprh: 4 - END_VERSIONS - """ -} diff --git a/modules/nf-neuro/registration/synthregistration/meta.yml b/modules/nf-neuro/registration/synthregistration/meta.yml deleted file mode 100644 index 67eae3b2..00000000 --- a/modules/nf-neuro/registration/synthregistration/meta.yml +++ /dev/null @@ -1,83 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json -name: registration_synthregistration -description: Perform registration using SynthMorph from Freesurfer. Outputs transforms - in Freesurfer format .lta for affine and .nii.gz (synthmorph also supports .mgz) - for deform, both in RAS orientation. Conversion to other formats is done using lta_convert - and mri_warp_convert respectively, which support a wide range of conversion formats - and orientations, such as ANTs ans FSL. To convert the output of this module, use - the registration/convert module successively to it. Note that tests using synthmorph - are non-reproductible. -keywords: - - Registration - - Brain imaging - - MRI - - Synthetic - - AI - - CNN -tools: - - Freesurfer: - description: | - Open source neuroimaging toolkit for processing, analyzing, and visualizing human brain MR images. - homepage: https://surfer.nmr.mgh.harvard.edu/ - doi: 10.1016/j.neuroimage.2012.01.021 - identifier: "" -input: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'test', single_end:false ]` - - moving: - type: file - description: Nifti volume moving for registration - pattern: "*.{nii,nii.gz}" - ontologies: - - edam: http://edamontology.org/format_4001 # NIFTI format - - fixed: - type: file - description: Nifti volume fixed for registration - pattern: "*.{nii,nii.gz}" - ontologies: - - edam: http://edamontology.org/format_4001 # NIFTI format -output: - warped_image: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'test', single_end:false ]` - - "*__output_warped.nii.gz": - type: file - description: Warped image - pattern: "*__output_warped.nii.gz" - ontologies: - - edam: http://edamontology.org/format_3989 # GZIP format - warp: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'test', single_end:false ]` - - "*__deform_warp.nii.gz": - type: list - description: Nifti volume containing warp field from moving to fixed - pattern: "*__deform_warp.nii.gz" - affine: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. `[ id:'test', single_end:false ]` - - "*__affine_warp.lta": - type: list - description: Affine transformation from moving to fixed - pattern: "*__affine_warp.lta" - versions: - - versions.yml: - type: file - description: File containing software versions - pattern: versions.yml - ontologies: - - edam: http://edamontology.org/format_3750 # YAML -authors: - - "@anroy1" diff --git a/modules/nf-neuro/registration/synthregistration/tests/main.nf.test.snap b/modules/nf-neuro/registration/synthregistration/tests/main.nf.test.snap deleted file mode 100644 index 2bb83c4a..00000000 --- a/modules/nf-neuro/registration/synthregistration/tests/main.nf.test.snap +++ /dev/null @@ -1,29 +0,0 @@ -{ - "registration - synthregistration": { - "content": [ - "test__output_warped.nii.gz", - "test__deform_warp.nii.gz", - "test__affine_warp.lta", - [ - "versions.yml:md5,6f946408922f25f1a8182d35615b6d7b" - ] - ], - "meta": { - "nf-test": "0.9.2", - "nextflow": "24.04.3" - }, - "timestamp": "2024-12-18T13:10:28.733204" - }, - "registration - synthregistration - stub-run": { - "content": [ - [ - "versions.yml:md5,6f946408922f25f1a8182d35615b6d7b" - ] - ], - "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.2" - }, - "timestamp": "2025-05-30T18:56:18.76904087" - } -} \ No newline at end of file diff --git a/modules/nf-neuro/registration/synthregistration/tests/nextflow.config b/modules/nf-neuro/registration/synthregistration/tests/nextflow.config deleted file mode 100644 index 20888b8f..00000000 --- a/modules/nf-neuro/registration/synthregistration/tests/nextflow.config +++ /dev/null @@ -1,10 +0,0 @@ -process { - withName: "REGISTRATION_SYNTHREGISTRATION" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - memory = "32G" - ext.affine = "affine" - ext.warp = "deform" - ext.lambda = 0.9 - ext.steps = 9 - } -} diff --git a/modules/nf-neuro/registration/synthregistration/tests/tags.yml b/modules/nf-neuro/registration/synthregistration/tests/tags.yml deleted file mode 100644 index e06c6bf6..00000000 --- a/modules/nf-neuro/registration/synthregistration/tests/tags.yml +++ /dev/null @@ -1,2 +0,0 @@ -registration/synthregistration: - - "modules/nf-neuro/registration/synthregistration/**" diff --git a/modules/nf-neuro/registration/tractogram/main.nf b/modules/nf-neuro/registration/tractogram/main.nf index fe1a9086..ad80f9f8 100644 --- a/modules/nf-neuro/registration/tractogram/main.nf +++ b/modules/nf-neuro/registration/tractogram/main.nf @@ -2,14 +2,14 @@ process REGISTRATION_TRACTOGRAM { tag "$meta.id" label 'process_single' - container "scilus/scilpy:2.2.0_cpu" + container "scilus/scilus:2.2.0" input: - tuple val(meta), path(anat), path(transfo), path(tractogram), path(ref) /* optional, input = [] */, path(deformation) /* optional, input = [] */ + tuple val(meta), path(anat), path(affine), path(tractogram), path(reference), path(deformation) output: - tuple val(meta), path("*__*.{trk,tck}"), emit: warped_tractogram - path "versions.yml" , emit: versions + tuple val(meta), path("*.{trk,tck}") , emit: tractogram + path "versions.yml" , emit: versions when: task.ext.when == null || task.ext.when @@ -17,43 +17,56 @@ process REGISTRATION_TRACTOGRAM { script: def prefix = task.ext.prefix ?: "${meta.id}" def suffix = task.ext.suffix ? "_${task.ext.suffix}" : "" - def reference = "$ref" ? "--reference $ref" : "" + reference = "$reference" ? "--reference $reference" : "" def in_deformation = "$deformation" ? "--in_deformation $deformation" : "" def inverse = task.ext.inverse ? "--inverse" : "" def reverse_operation = task.ext.reverse_operation ? "--reverse_operation" : "" - def force = task.ext.force ? "-f" : "" - def cut_invalid = task.ext.cut_invalid ? "--cut_invalid" : "" + def invalid_management = task.ext.invalid_streamlines ?: "cut" + def cut_invalid = invalid_management == "cut" ? "--cut_invalid" : "" def remove_single_point = task.ext.remove_single_point ? "--remove_single_point" : "" def remove_overlapping_points = task.ext.remove_overlapping_points ? "--remove_overlapping_points" : "" def threshold = task.ext.threshold ? "--threshold " + task.ext.threshold : "" def no_empty = task.ext.no_empty ? "--no_empty" : "" """ - for tractogram in ${tractogram}; - do \ + affine=$affine + if [[ "$affine" == *.txt ]]; then + ConvertTransformFile 3 $affine affine.mat --convertToAffineType \ + && affine="affine.mat" \ + || echo "TXT affine transform file conversion failed, using original file." + fi + + for tractogram in ${tractogram}; do ext=\${tractogram#*.} bname=\$(basename \${tractogram} .\${ext} | sed 's/${prefix}_\\+//') + name=${prefix}_\${bname}${suffix}.\${ext} + + scil_tractogram_apply_transform \$tractogram $anat \$affine \$name \ + $in_deformation \ + $inverse \ + $reverse_operation \ + $reference \ + --keep_invalid -f - scil_tractogram_apply_transform \$tractogram $anat $transfo tmp.trk\ - $in_deformation\ - $inverse\ - $reverse_operation\ - $force\ - $reference - - scil_tractogram_remove_invalid tmp.trk ${prefix}__\${bname}${suffix}.\${ext}\ - $cut_invalid\ - $remove_single_point\ - $remove_overlapping_points\ - $threshold\ - $no_empty\ - -f + if [[ "$invalid_management" == "keep" ]]; then + echo "Skip invalid streamline detection: \$name" + continue + fi + + scil_tractogram_remove_invalid \$name \$name \ + $cut_invalid\ + $remove_single_point\ + $remove_overlapping_points\ + $threshold\ + $no_empty\ + -f done cat <<-END_VERSIONS > versions.yml "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9.a-zA-Z-]+).*/\\1/') scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ @@ -65,16 +78,16 @@ process REGISTRATION_TRACTOGRAM { scil_tractogram_apply_transform -h scil_tractogram_remove_invalid -h - for tractogram in ${tractogram}; - do \ + for tractogram in ${tractogram}; do ext=\${tractogram#*.} bname=\$(basename \${tractogram} .\${ext} | sed 's/${prefix}_\\+//') - - touch ${prefix}__\${bname}${suffix}.\${ext} + name=${prefix}_\${bname}${suffix}.\${ext} + touch \$name done cat <<-END_VERSIONS > versions.yml "${task.process}": + ants: \$(antsRegistration --version | grep "Version" | sed -E 's/.*v([0-9.a-zA-Z-]+).*/\\1/') scilpy: \$(uv pip -q -n list | grep scilpy | tr -s ' ' | cut -d' ' -f2) END_VERSIONS """ diff --git a/modules/nf-neuro/registration/tractogram/meta.yml b/modules/nf-neuro/registration/tractogram/meta.yml index 531df2b7..1c060f29 100644 --- a/modules/nf-neuro/registration/tractogram/meta.yml +++ b/modules/nf-neuro/registration/tractogram/meta.yml @@ -7,11 +7,57 @@ keywords: - Bundles - Tractogram tools: + - ANTs: + description: perform registration using transform as input + homepage: https://antsx.github.io/ANTsRCore/reference/antsApplyTransforms.html + identifier: "" - scilpy: description: The Sherbrooke Connectivity Imaging Lab (SCIL) Python dMRI processing toolbox. homepage: https://github.com/scilus/scilpy.git identifier: "" +args: + - inverse: + type: boolean + description: | + If true, the affine transform will be inverted before being applied to the tractogram. + default: false + - reverse_operation: + type: boolean + description: | + If true, applies affine and deformation in reverse order (deformation first). + default: false + - invalid_streamlines: + type: string + description: | + How to manage invalid streamlines after applying the transform. Either 'keep' invalid + streamlines in the tractogram, 'cut' invalid segments and keep the remainder or 'remove' + invalid streamlines entierly. NOTE : selecting 'keep' disables overlapping and single-point + streamlines detection. + default: "cut" + choices: + - keep + - cut + - remove + - remove_single_point: + type: boolean + description: | + If true, removes single-point streamlines after applying the transform. + default: false + - remove_overlapping_points: + type: boolean + description: | + If true, remove streamlines with overlapping points after applying the transform. + default: false + - no_empty: + type: boolean + description: Don't save empty tractograms. + default: false + - threshold: + type: float + description: | + Maximum distance between two points to be considered overlapping, in mm. + default: 0.001 input: - - meta: type: map @@ -22,41 +68,46 @@ input: type: file description: FA nifti format as anatomical image pattern: "*.{nii,nii.gz}" + mandatory: true ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format - - transfo: + - affine: type: file description: ANTs affine transform pattern: "*.mat" + mandatory: true ontologies: [] - tractogram: type: file description: Tractogram or list of tractograms to register pattern: "*.{trk,tck}" + mandatory: true ontologies: [] - - ref: + - reference: type: file description: Reference anatomy for tck/vtk/fib/dpy file support (.nii or .nii.gz) (optional) pattern: "*.{tck,vtk,fib,dpy}" + mandatory: false ontologies: [] - deformation: type: file description: Path to the file containing a deformation field (optional) pattern: "*.{nii,nii.gz}" + mandatory: false ontologies: - edam: http://edamontology.org/format_4001 # NIFTI format output: - warped_tractogram: + tractogram: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'test', single_end:false ]` - - "*__*.{trk,tck}": + - "*.{trk,tck}": type: file description: Warped tractogram(s). - pattern: "*__*.{trk,tck}" + pattern: "*.{trk,tck}" ontologies: [] versions: - versions.yml: @@ -67,3 +118,4 @@ output: - edam: http://edamontology.org/format_3750 # YAML authors: - "@scilus" + - "@AlexVCaron" diff --git a/modules/nf-neuro/registration/tractogram/tests/keep_invalid.config b/modules/nf-neuro/registration/tractogram/tests/keep_invalid.config new file mode 100644 index 00000000..b7dbc0ba --- /dev/null +++ b/modules/nf-neuro/registration/tractogram/tests/keep_invalid.config @@ -0,0 +1,5 @@ +process { + withName: "REGISTRATION_TRACTOGRAM" { + ext.invalid_streamlines = "keep" + } +} diff --git a/modules/nf-neuro/registration/tractogram/tests/main.nf.test b/modules/nf-neuro/registration/tractogram/tests/main.nf.test index 6acabd93..0adc8bf6 100644 --- a/modules/nf-neuro/registration/tractogram/tests/main.nf.test +++ b/modules/nf-neuro/registration/tractogram/tests/main.nf.test @@ -1,6 +1,5 @@ nextflow_process { - name "Test Process REGISTRATION_TRACTOGRAM" script "../main.nf" process "REGISTRATION_TRACTOGRAM" @@ -26,10 +25,7 @@ nextflow_process { } } - test("registration - tractogram_bundles") { - - when { process { """ @@ -53,13 +49,37 @@ nextflow_process { { assert snapshot(process.out).match() } ) } - } - test("registration - tractogram") { + test("registration - tractogram - custom suffix") { + config "./suffix.config" + when { + process { + """ + input[0] = LOAD_DATA.out.test_data_directory.map{ + test_data_directory -> [ + [ id:'test', single_end:false ], // meta map + file("\${test_data_directory}/bundle_all_1mm.nii.gz", checkIfExists: true), + file("\${test_data_directory}/affine.txt", checkIfExists: true), + file("\${test_data_directory}/fibercup_atlas/subj_1/bundle_2.trk", checkIfExists: true), + [], + [] + ] + } + """ + } + } - config "./nextflow_suffix.config" + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + test("registration - tractogram - keep invalid") { + config "./keep_invalid.config" when { process { """ @@ -83,7 +103,6 @@ nextflow_process { { assert snapshot(process.out).match() } ) } - } test("registration - tractogram - stub-run") { @@ -113,6 +132,5 @@ nextflow_process { { assert snapshot(process.out.versions).match() } ) } - } } diff --git a/modules/nf-neuro/registration/tractogram/tests/main.nf.test.snap b/modules/nf-neuro/registration/tractogram/tests/main.nf.test.snap index b3172550..849705c0 100644 --- a/modules/nf-neuro/registration/tractogram/tests/main.nf.test.snap +++ b/modules/nf-neuro/registration/tractogram/tests/main.nf.test.snap @@ -1,4 +1,39 @@ { + "registration - tractogram - keep invalid": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_bundle_2.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9" + ] + ], + "1": [ + "versions.yml:md5,452295f58ea9ab6aa25264de21b111c1" + ], + "tractogram": [ + [ + { + "id": "test", + "single_end": false + }, + "test_bundle_2.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9" + ] + ], + "versions": [ + "versions.yml:md5,452295f58ea9ab6aa25264de21b111c1" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "25.04.8" + }, + "timestamp": "2025-10-16T15:24:45.131144667" + }, "registration - tractogram_bundles": { "content": [ { @@ -9,60 +44,60 @@ "single_end": false }, [ - "test__bundle_0.trk:md5,6e14cc02b66d12d5dde0a0701918385a", - "test__bundle_1.trk:md5,ef5a759144ab7e8d2f5abf30a463a152", - "test__bundle_2.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9", - "test__bundle_3.trk:md5,8877161b9aebaa6b12a4d7a8f8410407", - "test__bundle_4.trk:md5,6c8658b946154896952ca89e88fcf29f", - "test__bundle_5.trk:md5,fa058f93d9bfe4bed2ff027e9acd5682", - "test__bundle_6.trk:md5,125e50561b4b0c5fd8ee23867c28bc51" + "test_bundle_0.trk:md5,6e14cc02b66d12d5dde0a0701918385a", + "test_bundle_1.trk:md5,ef5a759144ab7e8d2f5abf30a463a152", + "test_bundle_2.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9", + "test_bundle_3.trk:md5,8877161b9aebaa6b12a4d7a8f8410407", + "test_bundle_4.trk:md5,6c8658b946154896952ca89e88fcf29f", + "test_bundle_5.trk:md5,fa058f93d9bfe4bed2ff027e9acd5682", + "test_bundle_6.trk:md5,125e50561b4b0c5fd8ee23867c28bc51" ] ] ], "1": [ - "versions.yml:md5,504210d056544efae377411b749443e5" - ], - "versions": [ - "versions.yml:md5,504210d056544efae377411b749443e5" + "versions.yml:md5,452295f58ea9ab6aa25264de21b111c1" ], - "warped_tractogram": [ + "tractogram": [ [ { "id": "test", "single_end": false }, [ - "test__bundle_0.trk:md5,6e14cc02b66d12d5dde0a0701918385a", - "test__bundle_1.trk:md5,ef5a759144ab7e8d2f5abf30a463a152", - "test__bundle_2.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9", - "test__bundle_3.trk:md5,8877161b9aebaa6b12a4d7a8f8410407", - "test__bundle_4.trk:md5,6c8658b946154896952ca89e88fcf29f", - "test__bundle_5.trk:md5,fa058f93d9bfe4bed2ff027e9acd5682", - "test__bundle_6.trk:md5,125e50561b4b0c5fd8ee23867c28bc51" + "test_bundle_0.trk:md5,6e14cc02b66d12d5dde0a0701918385a", + "test_bundle_1.trk:md5,ef5a759144ab7e8d2f5abf30a463a152", + "test_bundle_2.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9", + "test_bundle_3.trk:md5,8877161b9aebaa6b12a4d7a8f8410407", + "test_bundle_4.trk:md5,6c8658b946154896952ca89e88fcf29f", + "test_bundle_5.trk:md5,fa058f93d9bfe4bed2ff027e9acd5682", + "test_bundle_6.trk:md5,125e50561b4b0c5fd8ee23867c28bc51" ] ] + ], + "versions": [ + "versions.yml:md5,452295f58ea9ab6aa25264de21b111c1" ] } ], "meta": { "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nextflow": "25.04.8" }, - "timestamp": "2025-09-22T22:17:00.981071105" + "timestamp": "2025-10-16T15:23:46.56008019" }, "registration - tractogram - stub-run": { "content": [ [ - "versions.yml:md5,504210d056544efae377411b749443e5" + "versions.yml:md5,452295f58ea9ab6aa25264de21b111c1" ] ], "meta": { "nf-test": "0.9.0", "nextflow": "25.04.7" }, - "timestamp": "2025-09-22T22:17:25.469054766" + "timestamp": "2025-10-02T20:59:23.756300025" }, - "registration - tractogram": { + "registration - tractogram - custom suffix": { "content": [ { "0": [ @@ -71,30 +106,30 @@ "id": "test", "single_end": false }, - "test__bundle_2_mni.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9" + "test_bundle_2_mni.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9" ] ], "1": [ - "versions.yml:md5,504210d056544efae377411b749443e5" + "versions.yml:md5,452295f58ea9ab6aa25264de21b111c1" ], - "versions": [ - "versions.yml:md5,504210d056544efae377411b749443e5" - ], - "warped_tractogram": [ + "tractogram": [ [ { "id": "test", "single_end": false }, - "test__bundle_2_mni.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9" + "test_bundle_2_mni.trk:md5,824bcbf796cbce3e1c93e39d1c98dcf9" ] + ], + "versions": [ + "versions.yml:md5,452295f58ea9ab6aa25264de21b111c1" ] } ], "meta": { "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nextflow": "25.04.8" }, - "timestamp": "2025-09-22T22:17:13.547090754" + "timestamp": "2025-10-16T15:24:16.677031753" } } \ No newline at end of file diff --git a/modules/nf-neuro/registration/tractogram/tests/nextflow.config b/modules/nf-neuro/registration/tractogram/tests/nextflow.config index a32e45e8..a5cab34f 100644 --- a/modules/nf-neuro/registration/tractogram/tests/nextflow.config +++ b/modules/nf-neuro/registration/tractogram/tests/nextflow.config @@ -4,7 +4,6 @@ process { ext.inverse = true ext.force = true ext.reverse_operation = true - ext.cut_invalid = true ext.remove_single_point = true ext.remove_overlapping_points = true ext.threshold = 0.001 diff --git a/modules/nf-neuro/registration/tractogram/tests/nextflow_suffix.config b/modules/nf-neuro/registration/tractogram/tests/nextflow_suffix.config deleted file mode 100644 index c9553be0..00000000 --- a/modules/nf-neuro/registration/tractogram/tests/nextflow_suffix.config +++ /dev/null @@ -1,14 +0,0 @@ -process { - withName: "REGISTRATION_TRACTOGRAM" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.inverse = true - ext.force = true - ext.reverse_operation = true - ext.cut_invalid = true - ext.remove_single_point = true - ext.remove_overlapping_points = true - ext.threshold = 0.001 - ext.no_empty = true - ext.suffix = 'mni' - } -} diff --git a/modules/nf-neuro/registration/tractogram/tests/suffix.config b/modules/nf-neuro/registration/tractogram/tests/suffix.config new file mode 100644 index 00000000..c0cca21e --- /dev/null +++ b/modules/nf-neuro/registration/tractogram/tests/suffix.config @@ -0,0 +1,5 @@ +process { + withName: "REGISTRATION_TRACTOGRAM" { + ext.suffix = 'mni' + } +} diff --git a/poetry.lock b/poetry.lock index 6a321926..3d52216b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1269,13 +1269,13 @@ ubiquerg = ">=0.6.3" [[package]] name = "peppy" -version = "0.40.7" +version = "0.40.8" description = "A python-based project metadata manager for portable encapsulated projects" optional = false python-versions = "*" files = [ - {file = "peppy-0.40.7-py3-none-any.whl", hash = "sha256:5d2351f6cee6172eb82b90149b931a7827ed0445965b23ba3c36ddb70b3c2b3e"}, - {file = "peppy-0.40.7.tar.gz", hash = "sha256:5e3ddce8b5ef70bdf81e2839d5429bee267549265c07e0618469fe70a716e8ea"}, + {file = "peppy-0.40.8-py3-none-any.whl", hash = "sha256:9d146f8db5a7867de754a63b7012ead1b884c0dddb2609e878ee5548701cd59e"}, + {file = "peppy-0.40.8.tar.gz", hash = "sha256:2dcfe8e348130e94570340e2dfc96ce672d8af78c8ae97e21f2b0afe22ce2a8a"}, ] [package.dependencies] @@ -1510,24 +1510,34 @@ wcwidth = "*" [[package]] name = "psutil" -version = "7.1.0" +version = "7.1.2" description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" files = [ - {file = "psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13"}, - {file = "psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5"}, - {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3"}, - {file = "psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3"}, - {file = "psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d"}, - {file = "psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca"}, - {file = "psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d"}, - {file = "psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07"}, - {file = "psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2"}, + {file = "psutil-7.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0cc5c6889b9871f231ed5455a9a02149e388fffcb30b607fb7a8896a6d95f22e"}, + {file = "psutil-7.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8e9e77a977208d84aa363a4a12e0f72189d58bbf4e46b49aae29a2c6e93ef206"}, + {file = "psutil-7.1.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d9623a5e4164d2220ecceb071f4b333b3c78866141e8887c072129185f41278"}, + {file = "psutil-7.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:364b1c10fe4ed59c89ec49e5f1a70da353b27986fa8233b4b999df4742a5ee2f"}, + {file = "psutil-7.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f101ef84de7e05d41310e3ccbdd65a6dd1d9eed85e8aaf0758405d022308e204"}, + {file = "psutil-7.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:20c00824048a95de67f00afedc7b08b282aa08638585b0206a9fb51f28f1a165"}, + {file = "psutil-7.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e09cfe92aa8e22b1ec5e2d394820cf86c5dff6367ac3242366485dfa874d43bc"}, + {file = "psutil-7.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa6342cf859c48b19df3e4aa170e4cfb64aadc50b11e06bb569c6c777b089c9e"}, + {file = "psutil-7.1.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:625977443498ee7d6c1e63e93bacca893fd759a66c5f635d05e05811d23fb5ee"}, + {file = "psutil-7.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a24bcd7b7f2918d934af0fb91859f621b873d6aa81267575e3655cd387572a7"}, + {file = "psutil-7.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:329f05610da6380982e6078b9d0881d9ab1e9a7eb7c02d833bfb7340aa634e31"}, + {file = "psutil-7.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7b04c29e3c0c888e83ed4762b70f31e65c42673ea956cefa8ced0e31e185f582"}, + {file = "psutil-7.1.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c9ba5c19f2d46203ee8c152c7b01df6eec87d883cfd8ee1af2ef2727f6b0f814"}, + {file = "psutil-7.1.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a486030d2fe81bec023f703d3d155f4823a10a47c36784c84f1cc7f8d39bedb"}, + {file = "psutil-7.1.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3efd8fc791492e7808a51cb2b94889db7578bfaea22df931424f874468e389e3"}, + {file = "psutil-7.1.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2aeb9b64f481b8eabfc633bd39e0016d4d8bbcd590d984af764d80bf0851b8a"}, + {file = "psutil-7.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:8e17852114c4e7996fe9da4745c2bdef001ebbf2f260dec406290e66628bdb91"}, + {file = "psutil-7.1.2-cp37-abi3-win_arm64.whl", hash = "sha256:3e988455e61c240cc879cb62a008c2699231bf3e3d061d7fce4234463fd2abb4"}, + {file = "psutil-7.1.2.tar.gz", hash = "sha256:aa225cdde1335ff9684708ee8c72650f6598d5ed2114b9a7c5802030b1785018"}, ] [package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel", "wheel", "wmi"] +dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel", "wmi"] test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32", "setuptools", "wheel", "wmi"] [[package]] @@ -2282,13 +2292,13 @@ files = [ [[package]] name = "ruamel-yaml" -version = "0.18.15" +version = "0.18.16" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3.8" files = [ - {file = "ruamel.yaml-0.18.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701"}, - {file = "ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700"}, + {file = "ruamel.yaml-0.18.16-py3-none-any.whl", hash = "sha256:048f26d64245bae57a4f9ef6feb5b552a386830ef7a826f235ffb804c59efbba"}, + {file = "ruamel.yaml-0.18.16.tar.gz", hash = "sha256:a6e587512f3c998b2225d68aa1f35111c29fad14aed561a26e73fab729ec5e5a"}, ] [package.dependencies] @@ -2541,13 +2551,13 @@ typer = ["typer (>=0.9.0)"] [[package]] name = "typer" -version = "0.19.2" +version = "0.20.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.8" files = [ - {file = "typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9"}, - {file = "typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca"}, + {file = "typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a"}, + {file = "typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37"}, ] [package.dependencies] diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test index 07390814..41654690 100644 --- a/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test @@ -3,6 +3,7 @@ nextflow_workflow { name "Test Subworkflow ANATOMICAL_SEGMENTATION" script "../main.nf" workflow "ANATOMICAL_SEGMENTATION" + config "./nextflow.config" tag "subworkflows" tag "subworkflows_nfneuro" @@ -31,7 +32,6 @@ nextflow_workflow { } test("anatomical_segmentation - fslfast") { - config "./nextflow.config" when { workflow { """ @@ -86,7 +86,6 @@ nextflow_workflow { } test("anatomical_segmentation - freesurferseg") { - config "./nextflow.config" when { workflow { """ @@ -139,7 +138,6 @@ nextflow_workflow { } test("anatomical_segmentation - both") { - config "./nextflow.config" when { workflow { """ @@ -200,11 +198,8 @@ nextflow_workflow { } test("anatomical_segmentation - synthseg") { - config "./nextflow.config" + config "./synthseg.config" when { - params { - run_synthseg = true - } workflow { """ ch_split_test_data = LOAD_DATA.out.test_data_directory diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test.snap b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test.snap index 6d011b38..b86d206d 100644 --- a/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test.snap +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/main.nf.test.snap @@ -264,4 +264,4 @@ }, "timestamp": "2025-09-30T16:36:44.805357" } -} +} \ No newline at end of file diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow.config b/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow.config index 8730f1c4..0293c16f 100644 --- a/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow.config +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow.config @@ -1,5 +1,3 @@ process { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - } diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow_synthseg.config b/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow_synthseg.config deleted file mode 100644 index 49ea1e16..00000000 --- a/subworkflows/nf-neuro/anatomical_segmentation/tests/nextflow_synthseg.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - withName: "SEGMENTATION_SYNTHSEG" { - memory = "16G" - ext.fast = true - } -} - -params.run_synthseg = true diff --git a/subworkflows/nf-neuro/anatomical_segmentation/tests/synthseg.config b/subworkflows/nf-neuro/anatomical_segmentation/tests/synthseg.config new file mode 100644 index 00000000..f9171df3 --- /dev/null +++ b/subworkflows/nf-neuro/anatomical_segmentation/tests/synthseg.config @@ -0,0 +1,7 @@ +process { + withName: "SEGMENTATION_SYNTHSEG" { + ext.fast = true + } +} + +params.run_synthseg = true diff --git a/subworkflows/nf-neuro/bundle_seg/main.nf b/subworkflows/nf-neuro/bundle_seg/main.nf index ff489e2d..a15af23b 100644 --- a/subworkflows/nf-neuro/bundle_seg/main.nf +++ b/subworkflows/nf-neuro/bundle_seg/main.nf @@ -1,18 +1,19 @@ -include { REGISTRATION_ANTS } from '../../../modules/nf-neuro/registration/ants/main' -include { BUNDLE_RECOGNIZE } from '../../../modules/nf-neuro/bundle/recognize/main' +include { BUNDLE_RECOGNIZE } from '../../../modules/nf-neuro/bundle/recognize/main' + +include { REGISTRATION } from '../registration/main' def fetch_bundleseg_atlas(atlasUrl, configUrl, dest) { - def atlas = new File("$dest/atlas.zip").withOutputStream { out -> + def atlas = new File("$dest/atlas.zip").withOutputStream{ out -> new URL(atlasUrl).withInputStream { from -> out << from; } } - def config = new File("$dest/config.zip").withOutputStream { out -> + def config = new File("$dest/config.zip").withOutputStream{ out -> new URL(configUrl).withInputStream { from -> out << from; } } def atlasFile = new java.util.zip.ZipFile("$dest/atlas.zip") - atlasFile.entries().each { it -> + atlasFile.entries().each{ it -> def path = java.nio.file.Paths.get("$dest/atlas/" + it.name) if(it.directory){ java.nio.file.Files.createDirectories(path) @@ -27,7 +28,7 @@ def fetch_bundleseg_atlas(atlasUrl, configUrl, dest) { } def configFile = new java.util.zip.ZipFile("$dest/config.zip") - configFile.entries().each { it -> + configFile.entries().each{ it -> def path = java.nio.file.Paths.get("$dest/config/" + it.name) if(it.directory){ java.nio.file.Files.createDirectories(path) @@ -43,52 +44,66 @@ def fetch_bundleseg_atlas(atlasUrl, configUrl, dest) { } workflow BUNDLE_SEG { - take: - ch_fa // channel: [ val(meta), [ fa ] ] - ch_tractogram // channel: [ val(meta), [ tractogram ] ] - + ch_fa // channel: [ val(meta), [ fa ] ] + ch_tractogram // channel: [ val(meta), [ tractogram ] ] + ch_freesurfer_license // channel: [ val(meta), path(fs_license) ] main: + if ( params.run_easyreg ) error "The BUNDLE_SEG workflow does not support the easyreg registration method." + if ( params.run_synthmorph ) { + ch_freesurfer_license.ifEmpty{ error "Synthmorph registration need a Freesurfer License to run." } + } ch_versions = Channel.empty() + ch_mqc = Channel.empty() // ** Setting up Atlas reference channels. ** // if ( params.atlas_directory ) { - atlas_anat = Channel.fromPath("$params.atlas_directory/atlas/mni_masked.nii.gz", checkIfExists: true, relative: true) - atlas_config = Channel.fromPath("$params.atlas_directory/config/config_fss_1.json", checkIfExists: true, relative: true) - atlas_average = Channel.fromPath("$params.atlas_directory/atlas/atlas/", checkIfExists: true, relative: true) + ch_atlas_anat = Channel.fromPath("$params.atlas_directory/atlas/mni_masked.nii.gz", checkIfExists: true, relative: true) + ch_atlas_config = Channel.fromPath("$params.atlas_directory/config/config_fss_1.json", checkIfExists: true, relative: true) + ch_atlas_average = Channel.fromPath("$params.atlas_directory/atlas/atlas/", checkIfExists: true, relative: true) } else { if ( !file("$workflow.workDir/atlas/mni_masked.nii.gz").exists() ) { - fetch_bundleseg_atlas( "https://zenodo.org/records/10103446/files/atlas.zip?download=1", - "https://zenodo.org/records/10103446/files/config.zip?download=1", - "${workflow.workDir}/") + fetch_bundleseg_atlas( + "https://zenodo.org/records/10103446/files/atlas.zip?download=1", + "https://zenodo.org/records/10103446/files/config.zip?download=1", + "${workflow.workDir}/" + ) } - atlas_anat = Channel.fromPath("$workflow.workDir/atlas/mni_masked.nii.gz") - atlas_config = Channel.fromPath("$workflow.workDir/config/config_fss_1.json") - atlas_average = Channel.fromPath("$workflow.workDir/atlas/atlas/") + ch_atlas_anat = Channel.fromPath("$workflow.workDir/atlas/mni_masked.nii.gz") + ch_atlas_config = Channel.fromPath("$workflow.workDir/config/config_fss_1.json") + ch_atlas_average = Channel.fromPath("$workflow.workDir/atlas/atlas/") } // ** Register the atlas to subject's space. Set up atlas file as moving image ** // // ** and subject anat as fixed image. ** // - ch_register = ch_fa.combine(atlas_anat) - .map{ it + [[]] } - - REGISTRATION_ANTS ( ch_register ) - ch_versions = ch_versions.mix(REGISTRATION_ANTS.out.versions.first()) + ch_atlas_anat = ch_fa + .combine(ch_atlas_anat) + .map{ meta, _fa, anat -> [meta, anat] } + + REGISTRATION( + ch_atlas_anat, + ch_fa, + Channel.empty(), + Channel.empty(), + Channel.empty(), + Channel.empty(), + ch_freesurfer_license + ) + ch_versions = ch_versions.mix(REGISTRATION.out.versions.first()) + ch_mqc = ch_mqc.mix(REGISTRATION.out.mqc) // ** Perform bundle recognition and segmentation ** // ch_recognize_bundle = ch_tractogram - .join(REGISTRATION_ANTS.out.affine) - .combine(atlas_config) - .combine(atlas_average) + .join(REGISTRATION.out.forward_affine) + .combine(ch_atlas_config) + .combine(ch_atlas_average) BUNDLE_RECOGNIZE ( ch_recognize_bundle ) ch_versions = ch_versions.mix(BUNDLE_RECOGNIZE.out.versions.first()) - - emit: bundles = BUNDLE_RECOGNIZE.out.bundles // channel: [ val(meta), [ bundles ] ] - + mqc = ch_mqc // channel: [ *mqc.* ] versions = ch_versions // channel: [ versions.yml ] } diff --git a/subworkflows/nf-neuro/bundle_seg/meta.yml b/subworkflows/nf-neuro/bundle_seg/meta.yml index dd033ce0..28912faf 100644 --- a/subworkflows/nf-neuro/bundle_seg/meta.yml +++ b/subworkflows/nf-neuro/bundle_seg/meta.yml @@ -8,18 +8,44 @@ description: | Diffusion MRI. Cham: Springer Nature Switzerland (2023) --------- Steps -------------------- - Antomical Registration (ANTs) + Anatomical Registration (ANTs) Use the FA map from the subject to register the atlas anatomical file and compute the transformations. Bundle Recognition (scilpy) Perform bundle recognition and extraction using BundleSeg. + + --------- Experimental features ----- + !!! DISCLAIMER !!! + The following features are experimental and may not work as expected. While we run tests to ensure + computational stability of the subworkflow, we cannot guarantee the correctness of the results. + Anatomical Registration (SynthMorph) : params.run_synthmorph = true + Synthmorph is a machine learning-based registration method developed by the Freesurfer team. It is made + available in this subworkflow by the REGISTRATION subworkflow, please refer to its documentation for + more details. keywords: - BundleSeg - WM bundles - Tractogram - Segmentation components: - - registration/ants - bundle/recognize + - registration +args: + - run_synthmorph: + type: boolean + description: "Run SynthMorph registration instead of ANTs." + default: false + - atlas_directory: + type: directory + default: "" + description: | + Use an alternative atlas to the default BundleSeg one available on Zenodo (https://zenodo.org/records/10103446). + The folder MUST follow this specific structure: + atlas_directory + ├── atlas + │ └── pop_average + ├── centroids + ├── *.json (config file) + └── *.{nii,nii.gz} (atlas anatomical file) input: - ch_fa: type: file @@ -28,25 +54,21 @@ input: space and the subject's space. Structure: [ val(meta), path(fa) ] pattern: "*.{nii,nii.gz}" + mandatory: true - ch_tractogram: type: file description: | The input channel containing the whole-brain tractogram to be segmented. Structure: [ val(meta), path(tractogram) ] pattern: "*.trk" - - atlas_directory: - type: directory + mandatory: true + - ch_freesurfer_license: + type: file description: | - The input channel containing the atlas directory. The folder MUST follow this specific structure: - atlas_directory - ├── atlas - │ └── pop_average - ├── centroids - ├── *.json (config file) - └── *.{nii,nii.gz} (atlas anatomical file) - If no directory is provided, the subworkflow will automatically fetch the atlas archive available on - Zenodo (https://zenodo.org/records/10103446). - Structure: [ path(directory) ] + ONLY USED WITH SYNTHMORPH REGISTRATION. The input channel containing the Freesurfer license file. + Structure: [ val(meta), path(license) ] + pattern: "*.txt" + mandatory: false output: - bundles: type: file @@ -54,6 +76,13 @@ output: Channel containing all the segmented bundle files. Structure: [ val(meta), path(bundles) ] pattern: "*.trk" + - mqc: + type: file + description: | + Channel containing QC data for MultiQC reports. + Structure: [ *mqc.* ] + pattern: "*mqc.*" + optional: true - versions: type: file description: | @@ -62,5 +91,6 @@ output: pattern: "versions.yml" authors: - "@gagnonanthony" + - "@AlexVCaron" maintainers: - "@gagnonanthony" diff --git a/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test b/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test index 15d681cc..c2e63aae 100644 --- a/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test +++ b/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test @@ -8,9 +8,9 @@ nextflow_workflow { tag "subworkflows" tag "subworkflows_nfneuro" tag "subworkflows/bundle_seg" + tag "subworkflows/registration" tag "bundle/recognize" - tag "registration/ants" tag "load_test_data" @@ -22,30 +22,83 @@ nextflow_workflow { script "../../load_test_data/main.nf" process { """ - input[0] = Channel.from( [ "tracking.zip" ] ) + input[0] = Channel.from( [ "tracking.zip", "freesurfer.zip" ] ) input[1] = "test.load-test-data" """ } } } - test("rbx - base - no_atlas_folder") { + test("rbx - download atlas - ants registration") { + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + tracking: it.simpleName == "tracking" + freesurfer: it.simpleName == "freesurfer" + } + input[0] = ch_split_test_data.tracking.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/fa.nii.gz") + ] + } + input[1] = ch_split_test_data.tracking.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/pft.trk") + ] + } + input[2] = Channel.empty() + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot( + workflow.out + .findAll{ channel -> !channel.key.isInteger() && channel.value } + .collectEntries{ channel -> + [(channel.key): ["versions"].contains(channel.key) + ? channel.value + : channel.value.collect{ subject -> + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } + } ] + } + ).match() } + ) + } + } + + test("rbx - download atlas - synthmorph registration") { + config "./synthmorph.config" when { workflow { """ - input[0] = LOAD_DATA.out.test_data_directory.map{ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + tracking: it.simpleName == "tracking" + freesurfer: it.simpleName == "freesurfer" + } + input[0] = ch_split_test_data.tracking.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/fa.nii.gz") ] } - input[1] = LOAD_DATA.out.test_data_directory.map{ + input[1] = ch_split_test_data.tracking.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/pft.trk") ] } + input[2] = ch_split_test_data.freesurfer.map{ + test_data_directory -> file("\${test_data_directory}/license.txt") + } """ } } @@ -60,7 +113,7 @@ nextflow_workflow { [(channel.key): ["versions"].contains(channel.key) ? channel.value : channel.value.collect{ subject -> - [ subject[0] ] + subject[1..-1].collect{ entry -> entry ? file(entry).name : "" } + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } } ] } ).match() } diff --git a/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test.snap b/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test.snap index c74639eb..a155eb84 100644 --- a/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test.snap +++ b/subworkflows/nf-neuro/bundle_seg/tests/main.nf.test.snap @@ -1,55 +1,48 @@ { - "rbx - base - no_atlas_folder": { + "rbx - download atlas - synthmorph registration": { "content": [ { - "0": [ + "bundles": [ [ { - "id": "test", - "single_end": false + "id": "test" }, - [ - "test__AF_L_cleaned.trk:md5,b9a6567efffa206202f37c11afea2391", - "test__CC_Fr_2_cleaned.trk:md5,ad18f4415d6d5baae1ec81359ccffecb", - "test__CC_Pr_Po_cleaned.trk:md5,dca763cddc2decfa64baf452cc035b5a", - "test__CG_R_An_cleaned.trk:md5,79c5f2469b1fcae2599247ced8d68e07", - "test__CG_R_cleaned.trk:md5,79c5f2469b1fcae2599247ced8d68e07", - "test__FPT_L_Brainstem_cleaned.trk:md5,d6c9af62d9ad65d95257093148bf4d9a", - "test__FPT_L_cleaned.trk:md5,d6c9af62d9ad65d95257093148bf4d9a", - "test__ILF_L_cleaned.trk:md5,00f9f6e33e59bfe8bbe3a5afd18b0f2e", - "test__MCP_cleaned.trk:md5,ed86370180b35e125ae410aa3d520625", - "test__OR_ML_L_cleaned.trk:md5,721b658ed8a4de104c4e7c207d4fe170", - "test__POPT_R_Brainstem_cleaned.trk:md5,6c07d476650f4bca894c4c2454a1783f", - "test__POPT_R_cleaned.trk:md5,6c07d476650f4bca894c4c2454a1783f", - "test__PYT_L_cleaned.trk:md5,ac9973ef987c34ecee0328004033ea46", - "test__PYT_R_Brainstem_cleaned.trk:md5,28b4be34422e356030b6b081efca31d5", - "test__PYT_R_cleaned.trk:md5,50f8127bbaab6f4b9fea790a7c11679d", - "test__SLF_L_cleaned.trk:md5,3660a62ee7e0e413f7417e80c1842e60" - ] + "test_AF_L_cleaned.trk" ] ], - "1": [ - "versions.yml:md5,84a499436fbf8f1367ec4d8b21cf3abd", - "versions.yml:md5,94e5ea77581b6f43f79c3e7e4fc03030" - ], + "versions": [ + "versions.yml:md5,43c929afa23d3d0382a449b9ba99d7fb", + "versions.yml:md5,afaf6c2953accc507d8777303eea8718" + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-28T15:47:10.799145752" + }, + "rbx - download atlas - ants registration": { + "content": [ + { "bundles": [ [ { "id": "test" }, - "test__AF_L_cleaned.trk" + "test_AF_L_cleaned.trk" ] ], "versions": [ - "versions.yml:md5,00dbfc46ac61e9445b3ec72920926402", + "versions.yml:md5,9a3df973fb36ebec9763f10ff59ad875", "versions.yml:md5,afaf6c2953accc507d8777303eea8718" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.04.8" }, - "timestamp": "2025-10-17T17:38:41.717732229" + "timestamp": "2025-10-24T15:13:08.130425506" } } \ No newline at end of file diff --git a/subworkflows/nf-neuro/bundle_seg/tests/nextflow.config b/subworkflows/nf-neuro/bundle_seg/tests/nextflow.config index 2089b09d..45e1b42c 100644 --- a/subworkflows/nf-neuro/bundle_seg/tests/nextflow.config +++ b/subworkflows/nf-neuro/bundle_seg/tests/nextflow.config @@ -1,11 +1,11 @@ process { withName: "REGISTRATION_ANTS" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } ext.quick = true ext.threads = 1 ext.random_seed = 44 ext.repro_mode = 1 - ext.transform = "r" - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.transform = "a" } withName: "BUNDLE_RECOGNIZE" { publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } diff --git a/subworkflows/nf-neuro/bundle_seg/tests/synthmorph.config b/subworkflows/nf-neuro/bundle_seg/tests/synthmorph.config new file mode 100644 index 00000000..12eec6f8 --- /dev/null +++ b/subworkflows/nf-neuro/bundle_seg/tests/synthmorph.config @@ -0,0 +1,11 @@ +process { + withName: "REGISTRATION_SYNTHMORPH" { + ext.models = ["affine"] + ext.regularization = 0.9 + ext.steps = 9 + ext.extent = 192 + memory = '2G' + } +} + +params.run_synthmorph = true diff --git a/subworkflows/nf-neuro/output_template_space/main.nf b/subworkflows/nf-neuro/output_template_space/main.nf index 9b3f74b0..0a5a9997 100644 --- a/subworkflows/nf-neuro/output_template_space/main.nf +++ b/subworkflows/nf-neuro/output_template_space/main.nf @@ -3,12 +3,13 @@ include { IMAGE_APPLYMASK as MASK_T1W } from '../../../module include { IMAGE_APPLYMASK as MASK_T2W } from '../../../modules/nf-neuro/image/applymask/main.nf' include { BETCROP_FSLBETCROP as BET_T1W } from '../../../modules/nf-neuro/betcrop/fslbetcrop/main.nf' include { BETCROP_FSLBETCROP as BET_T2W } from '../../../modules/nf-neuro/betcrop/fslbetcrop/main.nf' -include { REGISTRATION_ANTS } from '../../../modules/nf-neuro/registration/ants/main.nf' include { REGISTRATION_ANTSAPPLYTRANSFORMS as WARPIMAGES } from '../../../modules/nf-neuro/registration/antsapplytransforms/main.nf' include { REGISTRATION_ANTSAPPLYTRANSFORMS as WARPMASK } from '../../../modules/nf-neuro/registration/antsapplytransforms/main.nf' include { REGISTRATION_ANTSAPPLYTRANSFORMS as WARPLABELS } from '../../../modules/nf-neuro/registration/antsapplytransforms/main.nf' include { REGISTRATION_TRACTOGRAM } from '../../../modules/nf-neuro/registration/tractogram/main.nf' +include { REGISTRATION } from '../registration/main.nf' + workflow OUTPUT_TEMPLATE_SPACE { take: @@ -17,22 +18,24 @@ workflow OUTPUT_TEMPLATE_SPACE { ch_mask_files // channel: [ val(meta), [ mask_files ] ] ch_labels_files // channel: [ val(meta), [ labels_files ] ] ch_trk_files // channel: [ val(meta), [ trk_files ] ] - + ch_freesurfer_license // channel: [ val(freesurfer_license) ] main: ch_versions = Channel.empty() + ch_mqc = Channel.empty() // ** First, let's assess if the desired template exists in ** // // ** the templateflow home directory (user-specified as params. ** // // ** or default to $outdir/../templateflow) ** // if ( !file("${params.templateflow_home}/tpl-${params.template}").exists() ) { log.info("Template ${params.template} not found in " + - "${params.templateflow_home}. Will be downloaded." + - "If you do not have access to the internet while running" + - "this pipeline, please download the template manually" + + "${params.templateflow_home}. Will be downloaded. " + + "If you do not have access to the internet while running " + + "this pipeline, please download the template manually " + "and provide the location using --templateflow_home.") - log.info("${params.template} will be downloaded at resolution " + - "${params.templateflow_res} from cohort ${params.templateflow_cohort}.") + log.info("Downloading ${params.template} :") + log.info(" - Resolution : ${params.templateflow_res}mm ") + log.info(" - Cohort : ${params.templateflow_cohort ?: "none"}") UTILS_TEMPLATEFLOW ( [ @@ -41,6 +44,7 @@ workflow OUTPUT_TEMPLATE_SPACE { params.templateflow_cohort != null ? params.templateflow_cohort : [] ] ) + // TODO: look into adding metadata and citations to MultiQC report ch_versions = ch_versions.mix(UTILS_TEMPLATEFLOW.out.versions) // ** Setting outputs ** // @@ -84,7 +88,7 @@ workflow OUTPUT_TEMPLATE_SPACE { } ch_brain_mask = ch_brain_mask - | branch { + .branch { TRUE: it[0] != [] FALSE: false } @@ -92,55 +96,67 @@ workflow OUTPUT_TEMPLATE_SPACE { // ** If the template has a brain mask, we will use it ** // if ( ! ch_brain_mask.FALSE ) { ch_bet_tpl_t1w = ch_t1w_tpl - | combine(ch_brain_mask) - | map{ t1w, mask -> [ [id: "template"], t1w, mask ] } + .combine(ch_brain_mask) + .map{ t1w, mask -> [ [id: "template"], t1w, mask ] } MASK_T1W ( ch_bet_tpl_t1w ) ch_versions = ch_versions.mix(MASK_T1W.out.versions) // ** Strip the template from the meta field so we can combine it ** // ch_t1w_tpl = MASK_T1W.out.image - | map{ _meta, image -> image } + .map{ _meta, image -> image } ch_bet_tpl_t2w = ch_t2w_tpl - | combine(ch_brain_mask) - | map{ t2w, mask -> [ [id: "template"], t2w, mask ] } + .combine(ch_brain_mask) + .map{ t2w, mask -> [ [id: "template"], t2w, mask ] } MASK_T2W ( ch_bet_tpl_t2w ) ch_versions = ch_versions.mix(MASK_T2W.out.versions) // ** Strip the template from the meta field so we can combine it ** // ch_t2w_tpl = MASK_T2W.out.image - | map{ _meta, image -> image } + .map{ _meta, image -> image } } else { // ** The template may not have a brain mask, so we will ** // // ** run BET by default (bit painful, but necessary) ** // ch_bet_tpl_t1w = ch_t1w_tpl - | map{ t1w -> [ [id: "template"], t1w, [], [] ] } + .map{ t1w -> [ [id: "template"], t1w, [], [] ] } BET_T1W ( ch_bet_tpl_t1w ) ch_versions = ch_versions.mix(BET_T1W.out.versions) // ** Strip the template from the meta field so we can combine it ** // ch_t1w_tpl = BET_T1W.out.image - | map{ _meta, image -> image } + .map{ _meta, image -> image } ch_bet_tpl_t2w = ch_t2w_tpl - | map{ t2w -> [ [id: "template"], t2w, [], [] ] } + .map{ t2w -> [ [id: "template"], t2w, [], [] ] } BET_T2W ( ch_bet_tpl_t2w ) ch_versions = ch_versions.mix(BET_T2W.out.versions) // ** Strip the template from the meta field so we can combine it ** // ch_t2w_tpl = BET_T2W.out.image - | map{ _meta, image -> image } + .map{ _meta, image -> image } } - // ** Register the subject to the template space ** // - ch_registration = ch_anat - | combine(params.use_template_t2w ? ch_t2w_tpl : ch_t1w_tpl) - | map{ meta, anat, tpl -> tuple(meta, tpl, anat, []) } + ch_template = ch_anat + .map{ meta, _anat -> meta } + .combine(params.use_template_t2w ? ch_t2w_tpl : ch_t1w_tpl) - REGISTRATION_ANTS ( ch_registration ) - ch_versions = ch_versions.mix(REGISTRATION_ANTS.out.versions) + ch_brain_mask = ch_anat + .map{ meta, _anat -> meta } + .combine(ch_brain_mask.TRUE) + // ** Register the subject to the template space ** // + REGISTRATION( + ch_anat, + ch_template, + Channel.empty(), + ch_brain_mask, + Channel.empty(), + Channel.empty(), + ch_freesurfer_license + ) + ch_versions = ch_versions.mix(REGISTRATION.out.versions) + ch_mqc = ch_mqc.mix(REGISTRATION.out.mqc) // ** Apply the transformation to all files ** // // ** The channel ch_nifti_files contains all the files that ** // @@ -148,49 +164,50 @@ workflow OUTPUT_TEMPLATE_SPACE { // ** [ tuple(meta, [ file1, file2, ... ]) ] ** // // ** Need to unpack the files and apply the transformation to each one ** // ch_files_to_transform = ch_nifti_files - | join(REGISTRATION_ANTS.out.image) - | join(REGISTRATION_ANTS.out.warp) - | join(REGISTRATION_ANTS.out.affine) + .join(REGISTRATION.out.image_warped) + .join(REGISTRATION.out.forward_image_transform) WARPIMAGES ( ch_files_to_transform ) ch_versions = ch_versions.mix(WARPIMAGES.out.versions) + ch_mqc = ch_mqc.mix(WARPIMAGES.out.mqc) // ** Same process for the masks ** // ch_masks_to_transform = ch_mask_files - | join(REGISTRATION_ANTS.out.image) - | join(REGISTRATION_ANTS.out.warp) - | join(REGISTRATION_ANTS.out.affine) + .join(REGISTRATION.out.image_warped) + .join(REGISTRATION.out.forward_image_transform) WARPMASK ( ch_masks_to_transform ) ch_versions = ch_versions.mix(WARPMASK.out.versions) + ch_mqc = ch_mqc.mix(WARPMASK.out.mqc) // ** Same process for the labels ** // ch_labels_to_transform = ch_labels_files - | join(REGISTRATION_ANTS.out.image) - | join(REGISTRATION_ANTS.out.warp) - | join(REGISTRATION_ANTS.out.affine) + .join(REGISTRATION.out.image_warped) + .join(REGISTRATION.out.forward_image_transform) WARPLABELS ( ch_labels_to_transform ) ch_versions = ch_versions.mix(WARPLABELS.out.versions) + ch_mqc = ch_mqc.mix(WARPLABELS.out.mqc) // ** Apply the transformation to the tractograms ** // ch_tractograms_to_transform = ch_trk_files - | join(REGISTRATION_ANTS.out.image) - | join(REGISTRATION_ANTS.out.inverse_warp) - | join(REGISTRATION_ANTS.out.affine) - | map{ meta, trk, image, warp, affine -> - tuple(meta, image, affine, trk, [], warp) + .join(REGISTRATION.out.image_warped) + .join(REGISTRATION.out.backward_affine) + .join(REGISTRATION.out.backward_warp, remainder: true) + .map{ meta, trk, image, affine, warp -> + [meta, image, affine, trk, [], warp ?: []] } REGISTRATION_TRACTOGRAM ( ch_tractograms_to_transform ) ch_versions = ch_versions.mix(REGISTRATION_TRACTOGRAM.out.versions) emit: - ch_t1w_tpl = ch_t1w_tpl // channel: [ tpl-T1w ] - ch_t2w_tpl = ch_t2w_tpl // channel: [ tpl-T2w ] - ch_registered_anat = REGISTRATION_ANTS.out.image // channel: [ val(meta), [ image ] ] - ch_warped_nifti_files = WARPIMAGES.out.warped_image // channel: [ val(meta), [ warped_image ] ] - ch_warped_mask_files = WARPMASK.out.warped_image // channel: [ val(meta), [ warped_mask ] ] - ch_warped_labels_files = WARPLABELS.out.warped_image // channel: [ val(meta), [ warped_labels ] ] - ch_warped_trk_files = REGISTRATION_TRACTOGRAM.out.warped_tractogram // channel: [ val(meta), [ warped_tractogram ] ] - versions = ch_versions // channel: [ versions.yml ] + ch_t1w_tpl = ch_t1w_tpl // channel: [ tpl-T1w ] + ch_t2w_tpl = ch_t2w_tpl // channel: [ tpl-T2w ] + ch_registered_anat = REGISTRATION.out.image_warped // channel: [ val(meta), [ image ] ] + ch_registered_nifti_files = WARPIMAGES.out.warped_image // channel: [ val(meta), [ warped_image ] ] + ch_registered_mask_files = WARPMASK.out.warped_image // channel: [ val(meta), [ warped_mask ] ] + ch_registered_labels_files = WARPLABELS.out.warped_image // channel: [ val(meta), [ warped_labels ] ] + ch_registered_trk_files = REGISTRATION_TRACTOGRAM.out.tractogram // channel: [ val(meta), [ warped_tractogram ] ] + mqc = ch_mqc // channel: [ mqc ] + versions = ch_versions // channel: [ versions.yml ] } diff --git a/subworkflows/nf-neuro/output_template_space/meta.yml b/subworkflows/nf-neuro/output_template_space/meta.yml index bf31c6b9..d51801fc 100644 --- a/subworkflows/nf-neuro/output_template_space/meta.yml +++ b/subworkflows/nf-neuro/output_template_space/meta.yml @@ -15,18 +15,47 @@ description: | - params.template_cohort: the cohort of the template (e.g., 1) An example of how to set the module's ext.args can be find in the - `test/nextflow.config` file. + `test/nextflow.config` file. Additional configuration for local templates + are also available in the `test/local.config` file. + + To select the registration technique used to register the input files, use the + following parameters : + - params.run_easyreg : Use Easyreg any-to-any registration model + - params.run_synthmorph : Use Synthmorph ML registration model + + Refer to the `registration` subworkflow and the modules it uses for configuration options. keywords: - template - TemplateFlow - registration components: - - registration/ants - registration/antsapplytransforms - registration/tractogram - image/applymask - betcrop/fslbetcrop - utils/templateflow + - registration +args: + - template: + type: string + description: Name of the template to use (see https://templateflow.org). + - templateflow_home: + type: string + description: Path to the templateflow local home directory where files are located and/or downloaded. + - template_res: + type: int + description: Template resolution to use or download. + - template_cohort: + type: string + description: Name/type of the cohort to use. + - run_easyreg: + type: boolean + description: Use Easyreg any-to-any registration model. + default: false + - run_synthmorph: + type: boolean + description: Use Synthmorph ML registration model. + default: false input: - ch_anat: type: file @@ -64,6 +93,13 @@ input: Structure: [ val(meta), [path(trk1), path(trk2), path(trk3), ...] ] pattern: "*.trk" mandatory: false + - ch_freesurfer_license: + type: file + description: | + ONLY USED WITH SYNTHMORPH REGISTRATION. The input channel containing the Freesurfer license file. + Structure: [ val(meta), path(license) ] + pattern: "*.txt" + mandatory: false output: - ch_t1w_tpl: type: file @@ -80,21 +116,44 @@ output: - ch_registered_anat: type: file description: | - Channel containing the registered anatomical image into the template space. + Channel containing the anatomical image registered into the template space. Structure: [ val(meta), path(anat) ] pattern: "*.{nii,nii.gz}" - ch_registered_nifti_files: type: file description: | - Channel containing the registered NIfTI files into the template space. + Channel containing the NIfTI files registered into the template space. Structure: [ val(meta), [path(nifti1), path(nifti2), path(nifti3), ...] ] pattern: "*.{nii,nii.gz}" + optional: true + - ch_registered_mask_files: + type: file + description: | + Channel containing the mask files registered into the template space. + Structure: [ val(meta), [path(mask1), path(mask2), path(mask3), ...] ] + pattern: "*.{nii,nii.gz}" + optional: true + - ch_registered_labels_files: + type: file + description: | + Channel containing the label files registered into the template space. + Structure: [ val(meta), [path(label1), path(label2), path(label3), ...] ] + pattern: "*.{nii,nii.gz}" + optional: true - ch_registered_trk_files: type: file description: | - Channel containing the registered TRK files into the template space. + Channel containing the TRK files registered into the template space. Structure: [ val(meta), [path(trk1), path(trk2), path(trk3), ...] ] pattern: "*.trk" + optional: true + - mqc: + type: file + description: | + Channel containing the MultiQC report of the registration. + Structure: [ path(mqc) ] + pattern: "*mqc.*" + optional: true - versions: type: file description: | @@ -103,5 +162,6 @@ output: pattern: "versions.yml" authors: - "@gagnonanthony" + - "@AlexVCaron" maintainers: - "@gagnonanthony" diff --git a/subworkflows/nf-neuro/output_template_space/tests/local.config b/subworkflows/nf-neuro/output_template_space/tests/local.config new file mode 100644 index 00000000..11936ca6 --- /dev/null +++ b/subworkflows/nf-neuro/output_template_space/tests/local.config @@ -0,0 +1,14 @@ +process { + withName: "BET_T1W" { + ext.bet_f = 0.6 + ext.crop = false + ext.dilate = false + } + withName: "BET_T2W" { + ext.bet_f = 0.6 + ext.crop = false + ext.dilate = false + } +} + +params.templateflow_home = "$launchDir/templateflow_home" diff --git a/subworkflows/nf-neuro/output_template_space/tests/main.nf.test b/subworkflows/nf-neuro/output_template_space/tests/main.nf.test index 3ea559b9..a7f635cf 100644 --- a/subworkflows/nf-neuro/output_template_space/tests/main.nf.test +++ b/subworkflows/nf-neuro/output_template_space/tests/main.nf.test @@ -3,22 +3,19 @@ nextflow_workflow { name "Test Subworkflow OUTPUT_TEMPLATE_SPACE" script "../main.nf" workflow "OUTPUT_TEMPLATE_SPACE" + config "./nextflow.config" tag "subworkflows" tag "subworkflows_nfneuro" tag "subworkflows/output_template_space" + tag "subworkflows/registration" + tag "load_test_data" - tag "registration" - tag "registration/ants" tag "registration/antsapplytransforms" tag "registration/tractogram" - tag "image" tag "image/applymask" - tag "betcrop" tag "betcrop/fslbetcrop" - tag "utils" tag "utils/templateflow" - tag "load_test_data" tag "stub" options "-stub-run" @@ -28,15 +25,15 @@ nextflow_workflow { script "../../load_test_data/main.nf" process { """ - input[0] = Channel.from( [ "tractometry.zip" ] ) + input[0] = Channel.from( [ "tractometry.zip", "freesurfer.zip" ] ) input[1] = "test.load-test-data" """ } } } - test("output to template MNI152NLin2009cAsym with local folder") { - config "./nextflow_local.config" + test("Template MNI152NLin2009cAsym - local templates") { + config "./local.config" when { workflow { """ @@ -77,6 +74,7 @@ nextflow_workflow { ] ] } + input[5] = Channel.empty() """ } } @@ -100,12 +98,94 @@ nextflow_workflow { ).match()}, { assert workflow.out .findAll{ channel -> !channel.key.isInteger() } - .every{ channel -> ["ch_registered_anat", - "ch_t1w_tpl", + .every{ channel -> ["ch_t1w_tpl", + "ch_t2w_tpl", + "ch_registered_anat", + "ch_registered_labels_files", + "ch_registered_nifti_files", + "ch_registered_trk_files", + "mqc", + "versions"].contains(channel.key) + ? channel.value.every{ subject -> subject instanceof ArrayList + ? subject.every() + : subject } + : channel.value.size() == 0 } } + ) + } + } + + test("Template MNI152NLin2009aAsym - no brain mask") { + when { + workflow { + """ + ch_split_test_data = LOAD_DATA.out.test_data_directory + .branch{ + tractometry: it.simpleName == "tractometry" + } + input[0] = ch_split_test_data.tractometry.map{ + test_data_directory -> [ + [ id: 'test' ], // meta map + file("\${test_data_directory}/mni_masked.nii.gz") + ] + } + input[1] = ch_split_test_data.tractometry.map{ + test_data_directory -> [ + [ id: 'test' ], // meta map + [ + file("\${test_data_directory}/IFGWM.nii.gz") + ] + ] + } + input[2] = Channel.empty() + input[3] = ch_split_test_data.tractometry.map{ + test_data_directory -> [ + [ id: 'test' ], // meta map + [ + file("\${test_data_directory}/IFGWM_labels_map.nii.gz") + ] + ] + } + input[4] = ch_split_test_data.tractometry.map{ + test_data_directory -> [ + [ id: 'test' ], // meta map + [ + file("\${test_data_directory}/IFGWM.trk"), + file("\${test_data_directory}/IFGWM_color.trk"), + file("\${test_data_directory}/IFGWM_uni.trk") + ] + ] + } + input[5] = Channel.empty() + """ + } + } + then { + assertAll( + { assert workflow.success }, + { assert snapshot( + workflow.out + .findAll{ channel -> !channel.key.isInteger() && channel.value } + .collectEntries{ channel -> + [(channel.key): ["versions"].contains(channel.key) + ? channel.value + : channel.value.collect{ subject -> + ["ch_t1w_tpl", "ch_t2w_tpl"].contains(channel.key) + ? file(subject).name + : [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry + ? file(entry).name + : "" } + } ] + } + ).match()}, + { assert workflow.out + .findAll{ channel -> !channel.key.isInteger() } + .every{ channel -> ["ch_t1w_tpl", "ch_t2w_tpl", - "ch_warped_labels_files", - "ch_warped_nifti_files", - "ch_warped_trk_files", + "ch_registered_anat", + "ch_registered_labels_files", + "ch_registered_nifti_files", + "ch_registered_trk_files", + "mqc", "versions"].contains(channel.key) ? channel.value.every{ subject -> subject instanceof ArrayList ? subject.every() @@ -115,14 +195,15 @@ nextflow_workflow { } } - test("output to template MNI152NLin2009aAsym - without brain mask") { - config "./nextflow.config" + test("Template MNI152NLin2009aAsym - using synthmorph registration") { + config "./synthmorph.config" when { workflow { """ ch_split_test_data = LOAD_DATA.out.test_data_directory .branch{ tractometry: it.simpleName == "tractometry" + freesurfer: it.simpleName == "freesurfer" } input[0] = ch_split_test_data.tractometry.map{ test_data_directory -> [ @@ -157,6 +238,9 @@ nextflow_workflow { ] ] } + input[5] = ch_split_test_data.freesurfer.map{ + test_data_directory -> file("\${test_data_directory}/license.txt") + } """ } } @@ -183,9 +267,10 @@ nextflow_workflow { .every{ channel -> ["ch_registered_anat", "ch_t1w_tpl", "ch_t2w_tpl", - "ch_warped_labels_files", - "ch_warped_nifti_files", - "ch_warped_trk_files", + "ch_registered_labels_files", + "ch_registered_nifti_files", + "ch_registered_trk_files", + "mqc", "versions"].contains(channel.key) ? channel.value.every{ subject -> subject instanceof ArrayList ? subject.every() diff --git a/subworkflows/nf-neuro/output_template_space/tests/main.nf.test.snap b/subworkflows/nf-neuro/output_template_space/tests/main.nf.test.snap index 91e12a29..ec2a521a 100644 --- a/subworkflows/nf-neuro/output_template_space/tests/main.nf.test.snap +++ b/subworkflows/nf-neuro/output_template_space/tests/main.nf.test.snap @@ -1,5 +1,5 @@ { - "output to template MNI152NLin2009aAsym - without brain mask": { + "Template MNI152NLin2009aAsym - using synthmorph registration": { "content": [ { "ch_registered_anat": [ @@ -7,7 +7,33 @@ { "id": "test" }, - "test__t1_warped.nii.gz" + "test_warped.nii.gz" + ] + ], + "ch_registered_labels_files": [ + [ + { + "id": "test" + }, + "test_IFGWM_labels_map_warped.nii.gz" + ] + ], + "ch_registered_nifti_files": [ + [ + { + "id": "test" + }, + "test_IFGWM_warped.nii.gz" + ] + ], + "ch_registered_trk_files": [ + [ + { + "id": "test" + }, + "test_IFGWM.trk", + "test_IFGWM_color.trk", + "test_IFGWM_uni.trk" ] ], "ch_t1w_tpl": [ @@ -16,50 +42,119 @@ "ch_t2w_tpl": [ "template__image_bet.nii.gz" ], - "ch_warped_labels_files": [ + "mqc": [ [ { "id": "test" }, - "test__IFGWM_labels_map__warped.nii.gz" + "test_IFGWM_labels_map_registration_antsapplytransforms_mqc.gif" + ], + [ + { + "id": "test" + }, + "test_IFGWM_registration_antsapplytransforms_mqc.gif" ] ], - "ch_warped_nifti_files": [ + "versions": [ + "versions.yml:md5,08c023826481c9130c3914096cda7f63", + "versions.yml:md5,4e02408f236932635d1e7585d80d16a1", + "versions.yml:md5,5091055c3aacdbd4dccc294cc841b8b3", + "versions.yml:md5,513f314aaedec96c49c67ed0bd3bbf4b", + "versions.yml:md5,53e0a593f56efd67061357b22888233f", + "versions.yml:md5,837bc1b10db6cb6c459b8871725ba254", + "versions.yml:md5,9013974da5ab5a7ae91e580948968e75", + "versions.yml:md5,957fd24bd8b559411e3a565ac7313f40" + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-28T15:51:42.158522262" + }, + "Template MNI152NLin2009aAsym - no brain mask": { + "content": [ + { + "ch_registered_anat": [ [ { "id": "test" }, - "test__IFGWM__warped.nii.gz" + "test_t1_warped.nii.gz" ] ], - "ch_warped_trk_files": [ + "ch_registered_labels_files": [ [ { "id": "test" }, - "test__IFGWM.trk", - "test__IFGWM_color.trk", - "test__IFGWM_uni.trk" + "test_IFGWM_labels_map_warped.nii.gz" + ] + ], + "ch_registered_nifti_files": [ + [ + { + "id": "test" + }, + "test_IFGWM_warped.nii.gz" + ] + ], + "ch_registered_trk_files": [ + [ + { + "id": "test" + }, + "test_IFGWM.trk", + "test_IFGWM_color.trk", + "test_IFGWM_uni.trk" + ] + ], + "ch_t1w_tpl": [ + "template__image_bet.nii.gz" + ], + "ch_t2w_tpl": [ + "template__image_bet.nii.gz" + ], + "mqc": [ + [ + { + "id": "test" + }, + "test_IFGWM_labels_map_registration_antsapplytransforms_mqc.gif" + ], + [ + { + "id": "test" + }, + "test_IFGWM_registration_antsapplytransforms_mqc.gif" + ], + [ + { + "id": "test" + }, + "test__registration_ants_mqc.gif" ] ], "versions": [ "versions.yml:md5,08c023826481c9130c3914096cda7f63", + "versions.yml:md5,2b72cc481686daf9f4a00587bbf1efa3", + "versions.yml:md5,4e02408f236932635d1e7585d80d16a1", + "versions.yml:md5,5091055c3aacdbd4dccc294cc841b8b3", "versions.yml:md5,513f314aaedec96c49c67ed0bd3bbf4b", "versions.yml:md5,53e0a593f56efd67061357b22888233f", - "versions.yml:md5,a8806981919b9cc22e2e1b374119a4e6", - "versions.yml:md5,cc26da22b51bd5b3e2a161a9b30c4f0c", - "versions.yml:md5,d6bae503d4e56ee4c5c8f9f951394aec", - "versions.yml:md5,e9d4549cb032979391719cd59bcfae59" + "versions.yml:md5,957fd24bd8b559411e3a565ac7313f40" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-17T17:39:24.646305544" + "timestamp": "2025-10-28T15:50:40.403026792" }, - "output to template MNI152NLin2009cAsym with local folder": { + "Template MNI152NLin2009cAsym - local templates": { "content": [ { "ch_registered_anat": [ @@ -67,7 +162,33 @@ { "id": "test" }, - "test__t1_warped.nii.gz" + "test_t1_warped.nii.gz" + ] + ], + "ch_registered_labels_files": [ + [ + { + "id": "test" + }, + "test_IFGWM_labels_map_warped.nii.gz" + ] + ], + "ch_registered_nifti_files": [ + [ + { + "id": "test" + }, + "test_IFGWM_warped.nii.gz" + ] + ], + "ch_registered_trk_files": [ + [ + { + "id": "test" + }, + "test_IFGWM.trk", + "test_IFGWM_color.trk", + "test_IFGWM_uni.trk" ] ], "ch_t1w_tpl": [ @@ -76,47 +197,41 @@ "ch_t2w_tpl": [ "template__image_bet.nii.gz" ], - "ch_warped_labels_files": [ + "mqc": [ [ { "id": "test" }, - "test__IFGWM_labels_map__warped.nii.gz" - ] - ], - "ch_warped_nifti_files": [ + "test_IFGWM_labels_map_registration_antsapplytransforms_mqc.gif" + ], [ { "id": "test" }, - "test__IFGWM__warped.nii.gz" - ] - ], - "ch_warped_trk_files": [ + "test_IFGWM_registration_antsapplytransforms_mqc.gif" + ], [ { "id": "test" }, - "test__IFGWM.trk", - "test__IFGWM_color.trk", - "test__IFGWM_uni.trk" + "test__registration_ants_mqc.gif" ] ], "versions": [ "versions.yml:md5,08c023826481c9130c3914096cda7f63", + "versions.yml:md5,2b72cc481686daf9f4a00587bbf1efa3", + "versions.yml:md5,4e02408f236932635d1e7585d80d16a1", + "versions.yml:md5,5091055c3aacdbd4dccc294cc841b8b3", "versions.yml:md5,513f314aaedec96c49c67ed0bd3bbf4b", "versions.yml:md5,53e0a593f56efd67061357b22888233f", - "versions.yml:md5,a8806981919b9cc22e2e1b374119a4e6", - "versions.yml:md5,cc26da22b51bd5b3e2a161a9b30c4f0c", - "versions.yml:md5,d6bae503d4e56ee4c5c8f9f951394aec", - "versions.yml:md5,e9d4549cb032979391719cd59bcfae59" + "versions.yml:md5,957fd24bd8b559411e3a565ac7313f40" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.04.8" }, - "timestamp": "2025-10-17T17:39:09.650176125" + "timestamp": "2025-10-24T14:50:02.140523373" } } \ No newline at end of file diff --git a/subworkflows/nf-neuro/output_template_space/tests/nextflow.config b/subworkflows/nf-neuro/output_template_space/tests/nextflow.config index f35f49f8..5e7ad9ef 100644 --- a/subworkflows/nf-neuro/output_template_space/tests/nextflow.config +++ b/subworkflows/nf-neuro/output_template_space/tests/nextflow.config @@ -1,9 +1,12 @@ process { publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + cpus = 1 + withName: "REGISTRATION_ANTS" { ext.repro_mode = 1 ext.transform = "s" ext.quick = true + ext.run_qc = true } withName: "WARPIMAGES" { ext.interpolation = "Linear" @@ -11,13 +14,15 @@ process { ext.image_type = 0 ext.output_dtype = "float" ext.default_val = 0 + ext.run_qc = true } - withName: "WARPMASKS" { + withName: "WARPMASK" { ext.interpolation = "NearestNeighbor" ext.dimensionality = 3 ext.image_type = 1 ext.output_dtype = "int" ext.default_val = 0 + ext.run_qc = true } withName: "WARPLABELS" { ext.interpolation = "NearestNeighbor" @@ -25,11 +30,11 @@ process { ext.image_type = 1 ext.output_dtype = "int" ext.default_val = 0 + ext.run_qc = true } withName: "REGISTRATION_TRACTOGRAM" { ext.inverse = true ext.force = true - ext.cut_invalid = true ext.remove_single_point = true ext.remove_overlapping_points = true ext.threshold = 0.001 diff --git a/subworkflows/nf-neuro/output_template_space/tests/nextflow_local.config b/subworkflows/nf-neuro/output_template_space/tests/nextflow_local.config deleted file mode 100644 index 856d8cae..00000000 --- a/subworkflows/nf-neuro/output_template_space/tests/nextflow_local.config +++ /dev/null @@ -1,54 +0,0 @@ -process { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - withName: "BET_T1W" { - ext.bet_f = 0.6 - ext.crop = false - ext.dilate = false - } - withName: "BET_T2W" { - ext.bet_f = 0.6 - ext.crop = false - ext.dilate = false - } - withName: "REGISTRATION_ANTS" { - ext.repro_mode = 1 - ext.transform = "s" - ext.quick = true - } - withName: "WARPIMAGES" { - ext.interpolation = "Linear" - ext.dimensionality = 3 - ext.image_type = 0 - ext.output_dtype = "float" - ext.default_val = 0 - } - withName: "WARPMASKS" { - ext.interpolation = "NearestNeighbor" - ext.dimensionality = 3 - ext.image_type = 1 - ext.output_dtype = "int" - ext.default_val = 0 - } - withName: "WARPLABELS" { - ext.interpolation = "NearestNeighbor" - ext.dimensionality = 3 - ext.image_type = 1 - ext.output_dtype = "int" - ext.default_val = 0 - } - withName: "REGISTRATION_TRACTOGRAM" { - ext.inverse = true - ext.force = true - ext.cut_invalid = true - ext.remove_single_point = true - ext.remove_overlapping_points = true - ext.threshold = 0.001 - ext.no_empty = true - } -} - -params.templateflow_home = "$launchDir/templateflow_home" -params.template = "MNI152NLin2009cAsym" -params.templateflow_res = 1 -params.templateflow_cohort = null -params.use_template_t2w = false diff --git a/subworkflows/nf-neuro/output_template_space/tests/synthmorph.config b/subworkflows/nf-neuro/output_template_space/tests/synthmorph.config new file mode 100644 index 00000000..8f8d1d65 --- /dev/null +++ b/subworkflows/nf-neuro/output_template_space/tests/synthmorph.config @@ -0,0 +1,9 @@ +process { + withName: "REGISTRATION_SYNTHMORPH" { + ext.models = ["affine"] + ext.extent = 192 + memory = "2G" + } +} + +params.run_synthmorph = true diff --git a/subworkflows/nf-neuro/registration/main.nf b/subworkflows/nf-neuro/registration/main.nf index e04e0b7f..7177c7ee 100644 --- a/subworkflows/nf-neuro/registration/main.nf +++ b/subworkflows/nf-neuro/registration/main.nf @@ -1,14 +1,16 @@ include { REGISTRATION_ANATTODWI } from '../../../modules/nf-neuro/registration/anattodwi/main' include { REGISTRATION_ANTS } from '../../../modules/nf-neuro/registration/ants/main' include { REGISTRATION_EASYREG } from '../../../modules/nf-neuro/registration/easyreg/main' -include { REGISTRATION_SYNTHREGISTRATION } from '../../../modules/nf-neuro/registration/synthregistration/main' +include { REGISTRATION_SYNTHMORPH } from '../../../modules/nf-neuro/registration/synthmorph/main' +include { REGISTRATION_CONVERT } from '../../../modules/nf-neuro/registration/convert/main' params.run_easyreg = false params.run_synthmorph = false + workflow REGISTRATION { - // The subworkflow requires at least ch_image and ch_ref as inputs to + // The subworkflow requires at least ch_fixed_image and ch_moving_image as inputs to // properly perform the registration. Supplying a ch_metric will select // the REGISTRATION_ANATTODWI module meanwhile NOT supplying a ch_metric // will select the REGISTRATION_ANTS (SyN or SyNQuick) module. Alternatively, @@ -16,16 +18,16 @@ workflow REGISTRATION { // REGISTRATION_EASYREG or REGISTRATION_SYNTHMORPH take: - ch_image // channel: [ val(meta), image ] - ch_ref // channel: [ val(meta), reference ] - ch_metric // channel: [ val(meta), metric ], optional - ch_mask // channel: [ val(meta), mask ], optional - ch_segmentation // channel: [ val(meta), segmentation ], optional - ch_ref_segmentation // channel: [ val(meta), ref-segmentation ], optional - + ch_fixed_image // channel: [ val(meta), image ] + ch_moving_image // channel: [ val(meta), reference ] + ch_metric // channel: [ val(meta), metric ], optional + ch_fixed_mask // channel: [ val(meta), mask ], optional + ch_segmentation // channel: [ val(meta), segmentation ], optional + ch_moving_segmentation // channel: [ val(meta), segmentation ], optional + ch_freesurfer_license // channel: [ license ], optional main: - ch_versions = Channel.empty() + ch_mqc = Channel.empty() if ( params.run_easyreg ) { // ** Registration using Easyreg ** // @@ -35,9 +37,9 @@ workflow REGISTRATION { // - join [ meta, reference, image | null, ref-segmentation | null ] // - join [ meta, reference, image | null, ref-segmentation | null, segmentation | null ] // - map [ meta, reference, image | [], ref-segmentation | [], segmentation | [] ] - ch_register = ch_ref - .join(ch_image, remainder: true) - .join(ch_ref_segmentation, remainder: true) + ch_register = ch_moving_image + .join(ch_fixed_image, remainder: true) + .join(ch_moving_segmentation, remainder: true) .join(ch_segmentation, remainder: true) .map{ it[0..1] + [it[2] ?: [], it[3] ?: [], it[4] ?: []] } @@ -45,31 +47,107 @@ workflow REGISTRATION { ch_versions = ch_versions.mix(REGISTRATION_EASYREG.out.versions.first()) // ** Set compulsory outputs ** // - image_warped = REGISTRATION_EASYREG.out.flo_reg - transfo_image = REGISTRATION_EASYREG.out.fwd_field - transfo_trk = REGISTRATION_EASYREG.out.bak_field - ref_warped = REGISTRATION_EASYREG.out.ref_reg + out_image_warped = REGISTRATION_EASYREG.out.image_warped + out_forward_affine = Channel.empty() + out_forward_warp = REGISTRATION_EASYREG.out.forward_warp + out_backward_affine = Channel.empty() + out_backward_warp = REGISTRATION_EASYREG.out.backward_warp + out_forward_image_transform = REGISTRATION_EASYREG.out.forward_warp + out_backward_image_transform = REGISTRATION_EASYREG.out.backward_warp + out_forward_tractogram_transform = REGISTRATION_EASYREG.out.backward_warp + out_backward_tractogram_transform = REGISTRATION_EASYREG.out.forward_warp // ** Set optional outputs. ** // // If segmentations are not provided as inputs, // easyreg will outputs synthseg segmentations - out_segmentation = ch_segmentation.mix( REGISTRATION_EASYREG.out.flo_seg ) - out_ref_segmentation = ch_ref_segmentation.mix( REGISTRATION_EASYREG.out.ref_seg ) + out_ref_warped = REGISTRATION_EASYREG.out.fixed_warped + out_segmentation = ch_segmentation.mix( REGISTRATION_EASYREG.out.segmentation_warped ) + out_ref_segmentation = ch_moving_segmentation.mix( REGISTRATION_EASYREG.out.fixed_segmentation_warped ) } else if ( params.run_synthmorph ) { // ** Registration using synthmorph ** // - ch_register = ch_image - .join(ch_ref) - - REGISTRATION_SYNTHREGISTRATION ( ch_register ) - ch_versions = ch_versions.mix(REGISTRATION_SYNTHREGISTRATION.out.versions.first()) - - // ** Set outputs ** // - image_warped = REGISTRATION_SYNTHREGISTRATION.out.warped_image - transfo_image = REGISTRATION_SYNTHREGISTRATION.out.warp - .join(REGISTRATION_SYNTHREGISTRATION.out.affine) // FIXME : this is .lta, should be .mat, but we need a custom container for that - transfo_trk = Channel.empty() // FIXME : this transformation should be available - ref_warped = Channel.empty() + ch_register = ch_fixed_image + .join(ch_moving_image) + + REGISTRATION_SYNTHMORPH ( ch_register ) + ch_versions = ch_versions.mix(REGISTRATION_SYNTHMORPH.out.versions.first()) + + // Tag all synthmorph transforms per type, and index if in a chain. This info will be + // used after conversion to sort out the transforms from the conversion module. + ch_convert_forward_affine = REGISTRATION_SYNTHMORPH.out.forward_affine + .map{ meta, forward_affine -> [meta, [tag: "forward_affine"], forward_affine] } + ch_convert_forward_warp = REGISTRATION_SYNTHMORPH.out.forward_warp + .map{ meta, forward_warp -> [meta, [tag: "forward_warp"], forward_warp] } + ch_convert_backward_affine = REGISTRATION_SYNTHMORPH.out.backward_affine + .map{ meta, backward_affine -> [meta, [tag: "backward_affine"], backward_affine] } + ch_convert_backward_warp = REGISTRATION_SYNTHMORPH.out.backward_warp + .map{ meta, backward_warp -> [meta, [tag: "backward_warp"], backward_warp] } + ch_convert_forward_image_transform = REGISTRATION_SYNTHMORPH.out.forward_image_transform + .map{ meta, transforms -> [meta, [tag: "forward_image_transform"], 0.. [meta, tag + [idx: idx], transform]} + ch_convert_backward_image_transform = REGISTRATION_SYNTHMORPH.out.backward_image_transform + .map{ meta, transforms -> [meta, [tag: "backward_image_transform"], 0.. [meta, tag + [idx: idx], transform]} + + // Mix all transforms into a single channel for conversion + ch_convert = ch_convert_forward_affine + .mix(ch_convert_forward_warp) + .mix(ch_convert_backward_affine) + .mix(ch_convert_backward_warp) + .mix(ch_convert_forward_image_transform) + .mix(ch_convert_backward_image_transform) + .combine(ch_fixed_image, by: 0) + .combine(ch_moving_image, by: 0) + .map{ meta, tag, transform, fixed, moving -> + def extension = transform.name.tokenize('.')[1..-1].join(".") + return [ + meta + tag + [cache: meta], + transform, + extension == "lta" ? "lta" : "ras", + "itk", + extension == "lta" ? fixed : moving, + [], + ]} + .combine(ch_freesurfer_license) + + REGISTRATION_CONVERT ( ch_convert ) + ch_versions = ch_versions.mix(REGISTRATION_CONVERT.out.versions.first()) + + // Un-mix conversion outputs using the tags. Save indexes for output sorting + ch_conversion_outputs = REGISTRATION_CONVERT.out.transformation + .branch{ meta, transform -> + forward_affine: meta.tag == "forward_affine" + return [meta.cache, transform] + forward_warp: meta.tag == "forward_warp" + return [meta.cache, transform] + backward_affine: meta.tag == "backward_affine" + return [meta.cache, transform] + backward_warp: meta.tag == "backward_warp" + return [meta.cache, transform] + forward_image_transform: meta.tag == "forward_image_transform" + return [meta.cache, [idx: meta.idx, trans: transform]] + backward_image_transform: meta.tag == "backward_image_transform" + return [meta.cache, [idx: meta.idx, trans: transform]] + } + + // ** Set compulsory outputs ** // + out_image_warped = REGISTRATION_SYNTHMORPH.out.image_warped + out_forward_affine = ch_conversion_outputs.forward_affine + out_forward_warp = ch_conversion_outputs.forward_warp + out_backward_affine = ch_conversion_outputs.backward_affine + out_backward_warp = ch_conversion_outputs.backward_warp + out_forward_image_transform = ch_conversion_outputs.forward_image_transform + .groupTuple() + .map{ meta, trans -> [meta, trans.sort{ t1, t2 -> t1.idx <=> t2.idx }.collect{ it.trans }] } + out_backward_image_transform = ch_conversion_outputs.backward_image_transform + .groupTuple() + .map{ meta, trans -> [meta, trans.sort{ t1, t2 -> t1.idx <=> t2.idx }.collect{ it.trans }] } + out_forward_tractogram_transform = out_backward_image_transform + out_backward_tractogram_transform = out_forward_image_transform + // ** and optional outputs. ** // + out_ref_warped = Channel.empty() out_segmentation = Channel.empty() out_ref_segmentation = Channel.empty() } @@ -83,8 +161,8 @@ workflow REGISTRATION { // Branches : // - anat_to_dwi : has a metric at index 3 // - ants_syn : doesn't have a metric at index 3 ( [] or null ) - ch_register = ch_image - .join(ch_ref) + ch_register = ch_fixed_image + .join(ch_moving_image) .join(ch_metric, remainder: true) .map{ it[0..2] + [it[3] ?: []] } .branch{ @@ -95,13 +173,18 @@ workflow REGISTRATION { // ** Registration using ANAT TO DWI ** // REGISTRATION_ANATTODWI ( ch_register.anat_to_dwi ) ch_versions = ch_versions.mix(REGISTRATION_ANATTODWI.out.versions.first()) + ch_mqc = ch_mqc.mix(REGISTRATION_ANATTODWI.out.mqc) // ** Set compulsory outputs ** // - image_warped = REGISTRATION_ANATTODWI.out.t1_warped - transfo_image = REGISTRATION_ANATTODWI.out.warp - .join(REGISTRATION_ANATTODWI.out.affine) - transfo_trk = REGISTRATION_ANATTODWI.out.affine - .join(REGISTRATION_ANATTODWI.out.inverse_warp) + out_image_warped = REGISTRATION_ANATTODWI.out.anat_warped + out_forward_affine = REGISTRATION_ANATTODWI.out.forward_affine + out_forward_warp = REGISTRATION_ANATTODWI.out.forward_warp + out_backward_affine = REGISTRATION_ANATTODWI.out.backward_affine + out_backward_warp = REGISTRATION_ANATTODWI.out.backward_warp + out_forward_image_transform = REGISTRATION_ANATTODWI.out.forward_image_transform + out_backward_image_transform = REGISTRATION_ANATTODWI.out.backward_image_transform + out_forward_tractogram_transform = REGISTRATION_ANATTODWI.out.forward_tractogram_transform + out_backward_tractogram_transform = REGISTRATION_ANATTODWI.out.backward_tractogram_transform // ** Registration using ANTS SYN SCRIPTS ** // // Registration using antsRegistrationSyN.sh or antsRegistrationSyNQuick.sh, has @@ -111,33 +194,46 @@ workflow REGISTRATION { // - join [ meta, image, metric | [], mask | null ] // - map [ meta, image, mask | [] ] ch_register = ch_register.ants_syn - .join(ch_mask, remainder: true) + .join(ch_fixed_mask, remainder: true) .map{ it[0..2] + [it[4] ?: []] } REGISTRATION_ANTS ( ch_register ) ch_versions = ch_versions.mix(REGISTRATION_ANTS.out.versions.first()) + ch_mqc = ch_mqc.mix(REGISTRATION_ANTS.out.mqc) // ** Set compulsory outputs ** // - image_warped = image_warped.mix(REGISTRATION_ANTS.out.image) - transfo_image = REGISTRATION_ANTS.out.warp - .join(REGISTRATION_ANTS.out.affine) - .mix(transfo_image) - transfo_trk = REGISTRATION_ANTS.out.inverse_affine - .join(REGISTRATION_ANTS.out.inverse_warp) - .mix(transfo_trk) - - // **et optional outputs **// - ref_warped = Channel.empty() + out_image_warped = out_image_warped.mix(REGISTRATION_ANTS.out.image_warped) + out_forward_affine = out_forward_affine.mix(REGISTRATION_ANTS.out.forward_affine) + out_forward_warp = out_forward_warp.mix(REGISTRATION_ANTS.out.forward_warp) + out_backward_affine = out_backward_affine.mix(REGISTRATION_ANTS.out.backward_affine) + out_backward_warp = out_backward_warp.mix(REGISTRATION_ANTS.out.backward_warp) + out_forward_image_transform = out_forward_image_transform.mix(REGISTRATION_ANTS.out.forward_image_transform) + out_backward_image_transform = out_backward_image_transform.mix(REGISTRATION_ANTS.out.backward_image_transform) + out_forward_tractogram_transform = out_forward_tractogram_transform.mix(REGISTRATION_ANTS.out.forward_tractogram_transform) + out_backward_tractogram_transform = out_backward_tractogram_transform.mix(REGISTRATION_ANTS.out.backward_tractogram_transform) + + // **and optional outputs **// + out_ref_warped = Channel.empty() out_segmentation = Channel.empty() out_ref_segmentation = Channel.empty() } - emit: - image_warped = image_warped // channel: [ val(meta), image ] ] - ref_warped = ref_warped // channel: [ val(meta), ref ] - transfo_image = transfo_image // channel: [ val(meta), [ warp ], [ ] ] - transfo_trk = transfo_trk // channel: [ val(meta), [ ], [ inverse-warp ] ] - segmentation = out_segmentation // channel: [ val(meta), segmentation ] - ref_segmentation = out_ref_segmentation // channel: [ val(meta), ref-segmentation ] - versions = ch_versions // channel: [ versions.yml ] + image_warped = out_image_warped // channel: [ val(meta), image ] + reference_warped = out_ref_warped // channel: [ val(meta), ref ] + // Individual transforms + forward_affine = out_forward_affine // channel: [ val(meta), ] + forward_warp = out_forward_warp // channel: [ val(meta), ] + backward_warp = out_backward_warp // channel: [ val(meta), ] + backward_affine = out_backward_affine // channel: [ val(meta), ] + // Combined transforms + forward_image_transform = out_forward_image_transform // channel: [ val(meta), [ , ] ] + backward_image_transform = out_backward_image_transform // channel: [ val(meta), [ , ] ] + forward_tractogram_transform = out_forward_tractogram_transform // channel: [ val(meta), [ , ] ] + backward_tractogram_transform = out_backward_tractogram_transform // channel: [ val(meta), [ , ] ] + // Segmentations + segmentation = out_segmentation // channel: [ val(meta), segmentation ] + reference_segmentation = out_ref_segmentation // channel: [ val(meta), ref-segmentation ] + + mqc = ch_mqc // channel: [ *mqc.*, ... ] + versions = ch_versions // channel: [ versions.yml ] } diff --git a/subworkflows/nf-neuro/registration/meta.yml b/subworkflows/nf-neuro/registration/meta.yml index 55e059e0..552defb2 100644 --- a/subworkflows/nf-neuro/registration/meta.yml +++ b/subworkflows/nf-neuro/registration/meta.yml @@ -2,16 +2,40 @@ name: "registration" description: | Subworkflow to perform registration between a moving and a fixed image (e.g. T1 -> DWI). It requires as input at least a moving (ch_image) and a reference (ch_ref) image to properly - perform registration. Three modes are available: - 1) if a metric file is supplied (ch_metric), the subworkflow will use the REGISTER_ANATTODWI - module calling AntsRegistration, with the metric as additional target. - 2) if NO metric file is supplied, the subworkflow will use the REGISTRATION_ANTS module calling - antsRegistrationSyN.sh or antsRegistrationSyNQuick.sh. - 3) alternatively, if an alternative model parameter is activated, the subworkflow will use the specified module - This subworkflow outputs transformation files that can be used with the ANTSAPPLYTRANSFORMS module - to warp any new image. Simply provide your moving image, reference image, and transformations files - to the module to register a new image in the current space. Similar steps can be used to register - bundles/tractograms using the BUNDLEREGISTRATION module. + perform registration. + + Output transformations curated for images for use with the REGISTRATION_ANTSAPPLYTRANSFORMS module + are available under the names `forward_image_transform` and `backward_image_transform`. The same lists + are also available for tractograms (`forward_tractogram_transform` and `backward_tractogram_transform`). + However, to use the REGISTRATION_TRACTOGRAM module, you have to use the `backward_affine` and `backward_warp` + channels provided by this subworkflow. + + Registration suites: + - EasyReg (ML): params.run_easyreg = true + + module: REGISTRATION_EASYREG + + Run freesurfer EasyReg any-to-any modality registration. + + NOTE: Outputs a combined warp field only, no affine transformations are available. CANNOT BE LINKED WITH THE BUNDLE_SEG SUBWORKFLOW. + + - Synthmorph (ML): params.run_synthmorph = true + + module: REGISTRATION_SYNTHMORPH + + Run SynthMorph any-to-any modality registration. By default, the algorithm is configured to + run a chain of affin+deformable transformations, making it suitable for use with most modules + and subworkflows like BUNDLE_SEG. + + NOTE: the output affine transformation file is in .lta format and needs to be converted to .mat to be used with ANTs using the REGISTRATION_CONVERT module. + + - ANTs (SyN+SyN Quick): params.run_easyreg = false && params.run_synthmorph = false + + module: REGISTRATION_ANTS or REGISTRATION_ANATTODWI + + Run ANTs registration using antsRegistrationSyN.sh (quick version also available). + + NOTE : Transformation to DWI space is available. To trigger it, supply a metric file (ch_metric) in DWI space (e.g. FA map) in addition to the moving (ch_image) and reference (ch_ref) images. keywords: - Registration - Transformation @@ -20,92 +44,147 @@ keywords: components: - registration/anattodwi - registration/ants + - registration/convert - registration/easyreg - - registration/synthregistration + - registration/synthmorph input: - - ch_image: + - ch_fixed_image: type: file description: | - The input channel containing the moving image files. Typically your anatomical image to be - registered to dwi space. - Structure: [ val(meta), path(image) ] + The input channel containing the fixed image files. If performing + registration to DWI space, this is the reference image (e.g. b0 image). + Structure: [ val(meta), path(fixed_image) ] pattern: "*.{nii,nii.gz}" - - ch_ref: + mandatory: true + - ch_moving_image: type: file description: | - The input channel containing the fixed image files. Typically a reference image from the dwi - space (e.g. b0 image, etc.). - Structure: [ val(meta), path(ref) ] + Input channel containing the moving image files. If performing + registration to DWI space, this is the anatomical image to be registered. + Structure: [ val(meta), path(image) ] pattern: "*.{nii,nii.gz}" + mandatory: true - ch_metric: type: file description: | - The input channel containing the metric files. Supplying this channel will make the subworkflow - use the module REGISTER_ANATTODWI calling AntsRegistration using the moving, reference and metric - image. For a T1 -> DWI registration, this is typically a FA map. + FOR USE WITH REGISTRATION TO DWI SPACE ONLY. The input channel containing the metric files, typically a FA map. Structure: [ val(meta), path(metric) ] pattern: "*.{nii,nii.gz}" - - ch_mask: + mandatory: false + - ch_fixed_mask: type: file description: | - The input channel containing the mask files. Supplying this channel only affect the subworkflow - if the ch_metric is NOT supplied. This channel is only being used if the module called is - REGISTRATION_ANTS, see the description section above for more details. + FOR USE WITH ANTS SYN REGISTRATION ONLY. The input channel containing the mask file in fixed image space. Structure: [ val(meta), path(mask) ] pattern: "*.{nii,nii.gz}" + mandatory: false - ch_segmentation: type: file description: | - The input channel containing the the SynthSeg v2 (non-robust) segmentation + parcellation of the moving (floating in Easyreg naming convention) image. - If it does not exist, Easyreg will create it. If it already exists (e.g., from a previous EasyReg run), - then EasyReg will read it from disk (which is faster than segmenting). + FOR USE WITH SYNTHMORPH REGISTRATION ONLY. The input channel containing the SynthSeg v2 (non-robust) + segmentation + parcellation in fixed image space (reference in Easyreg naming convention) image. pattern: "*.{nii,nii.gz}" - - ch_ref_segmentation: + mandatory: false + - ch_moving_segmentation: type: file description: | - The input channel containing the SynthSeg v2 (non-robust) segmentation + parcellation of the fixed (reference in Easyreg naming convention) image. - If it does not exist, Easyreg will create it. If it already exists (e.g., from a previous EasyReg run), - then EasyReg will read it from disk (which is faster than segmenting). + FOR USE WITH SYNTHMORPH REGISTRATION ONLY. The input channel containing the SynthSeg v2 (non-robust) + segmentation + parcellation in moving image space (floating in Easyreg naming convention) image. pattern: "*.{nii,nii.gz}" + mandatory: false output: - image_warped: type: file description: | - Channel containing warped moving images. Typically, this would be the warped T1 in DWI space. + Channel containing images warped in fixed space. + Structure: [ val(meta), path(image) ] + pattern: "*.{nii,nii.gz}" + - reference_warped: + type: file + description: | + ONLY PROVIDED BY REGISTRATION_EASYREG. Channel containing the reference image warped in moving space. Structure: [ val(meta), path(image) ] pattern: "*.{nii,nii.gz}" - - transfo_image: + optional: true + - forward_affine: type: file description: | - Channel containing the image transformation files. This channel contains the necessary transformation - files (warp and affine) to perform the anatomical -> dwi space registration. Those files could be - used in the future to bring anatomical labels into DWI space for connectomics. - Structure: [ val(meta), [ path(warp), path(affine) ] ] - pattern: "*.{nii,nii.gz,mat}" - - transfo_trk: + Channel containing the affine transformation matrix to fixed space. + Structure: [ val(meta), path(forward_affine) ] + pattern: "*__forward*.{lta,mat}" + optional: true + - forward_warp: type: file description: | - Channel containing the tractogram transformation files. This channel contains the necessary transformation - files (inverseAffine, inverseWarp) to perform the dwi -> anatomical space registration. Those files could - be used to register tractograms or bundle in the subject's anatomical space. - Structure: [ val(meta), [ path(inverseAffine), path(inverseWarp) ] - pattern: "*.{nii,nii.gz,mat}" - - ref_warped: + Channel containing the deformation field to fixed space. + Structure: [ val(meta), path(forward_warp) ] + pattern: "*__forward*.{nii,nii.gz}" + optional: true + - backward_warp: type: file description: | - Channel containing warped reference image. Typically, this would be the warped DWI in T1 space. - Structure: [ val(meta), path(ref) ] - pattern: "*.{nii,nii.gz}" - - out_segmentation: + Channel containing the deformation field to moving space. + Structure: [ val(meta), path(backward_warp) ] + pattern: "*__backward*.{nii,nii.gz}" + optional: true + - backward_affine: type: file description: | - Channel containing the file with the SynthSeg v2 (non-robust) segmentation + parcellation of the moving (floating in Easyreg naming convention) image. + Channel containing the affine transformation matrix to moving space. + Structure: [ val(meta), path(backward_affine) ] + pattern: "*__backward*.{lta,mat}" + optional: true + - forward_image_transform: + type: list + description: | + Channel containing the transformation tuples to warp images to fixed space, + in the correct order for REGISTRATION_ANTSAPPLYTRANSFORMS. + Structure: [ val(meta), [ path(forward_warp), path(forward_affine) ] ]. + pattern: "*__forward*.{nii,nii.gz,mat,lta}" + - backward_image_transform: + type: list + description: | + Channel containing the transformation tuples to warp images to moving space, + in the correct order for REGISTRATION_ANTSAPPLYTRANSFORMS. + Structure: [ val(meta), [ path(backward_affine), path(backward_warp) ] ]. + pattern: "*__backward*.{nii,nii.gz,mat,lta}" + - forward_tractogram_transform: + type: list + description: | + Channel containing the transformation tuples to warp tractograms to fixed space, + in the correct order for REGISTRATION_TRANSFORMTRACTOGRAM. + Structure: [ val(meta), [ path(backward_warp), path(backward_affine) ] ]. + pattern: "*__backward*.{nii,nii.gz,mat,lta}" + - backward_tractogram_transform: + type: list + description: | + Channel containing the transformation tuples to warp tractograms to moving space, + in the correct order for REGISTRATION_TRANSFORMTRACTOGRAM. + Structure: [ val(meta), [ path(forward_warp), path(forward_affine) ] ]. + pattern: "*__forward*.{nii,nii.gz,mat,lta}" + - segmentation: + type: file + description: | + ONLY PROVIDED BY REGISTRATION_SYNTHMORPH. Channel containing the SynthSeg v2 (non-robust) + segmentation + parcellation in moving space (floating in Easyreg naming convention). + Structure: [ val(meta), path(segmentation) ] pattern: "*.{nii,nii.gz}" - - out_ref_segmentation: + optional: true + - reference_segmentation: type: file description: | - Channel containing the file with the SynthSeg v2 (non-robust) segmentation + parcellation of the fixed (reference in Easyreg naming convention) image. + ONLY PROVIDED BY REGISTRATION_SYNTHMORPH. Channel containing the SynthSeg v2 (non-robust) + segmentation + parcellation in fixed space (reference in Easyreg naming convention). + Structure: [ val(meta), path(reference_segmentation) ] pattern: "*.{nii,nii.gz}" + optional: true + - mqc: + type: file + description: | + Channel containing the MultiQC report data for the registration. + Structure: [ path(mqc) ] + pattern: "*mqc.*" + optional: true - versions: type: file description: | @@ -114,3 +193,4 @@ output: pattern: "versions.yml" authors: - "@gagnonanthony" + - "@AlexVCaron" diff --git a/subworkflows/nf-neuro/registration/tests/ants.config b/subworkflows/nf-neuro/registration/tests/ants.config new file mode 100644 index 00000000..9841628a --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/ants.config @@ -0,0 +1,14 @@ +process { + withName: "REGISTRATION_ANATTODWI" { + ext.run_qc = true + } + + withName: "REGISTRATION_ANTS" { + ext.quick = true + ext.repro_mode = true + ext.transform = "a" + ext.initial_transform = "intensities" + ext.run_qc = true + ext.random_seed = 44 + } +} diff --git a/subworkflows/nf-neuro/registration/tests/easyreg.config b/subworkflows/nf-neuro/registration/tests/easyreg.config new file mode 100644 index 00000000..29e56dc8 --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/easyreg.config @@ -0,0 +1,8 @@ +process { + withName: "REGISTRATION_EASYREG" { + ext.affine_only = true + memory = '4G' + } +} + +params.run_easyreg = true diff --git a/subworkflows/nf-neuro/registration/tests/main.nf.test b/subworkflows/nf-neuro/registration/tests/main.nf.test index 30b6f1d1..1a941fdc 100644 --- a/subworkflows/nf-neuro/registration/tests/main.nf.test +++ b/subworkflows/nf-neuro/registration/tests/main.nf.test @@ -3,6 +3,7 @@ nextflow_workflow { name "Test Subworkflow REGISTRATION" script "../main.nf" workflow "REGISTRATION" + config "./nextflow.config" tag "subworkflows" tag "subworkflows_nfneuro" @@ -12,8 +13,9 @@ nextflow_workflow { tag "registration/anattodwi" tag "registration" tag "registration/ants" + tag "registration/convert" tag "registration/easyreg" - tag "registration/synthregistration" + tag "registration/synthmorph" tag "load_test_data" @@ -25,15 +27,15 @@ nextflow_workflow { script "../../load_test_data/main.nf" process { """ - input[0] = Channel.from( [ "T1w.zip", "b0.zip", "dti.zip" ] ) + input[0] = Channel.from( [ "freesurfer.zip", "T1w.zip", "b0.zip", "dti.zip", "others.zip" ] ) input[1] = "test.load-test-data" """ } } } - test("registration - antsRegistration") { - config "./nextflow.config" + test("registration - ANTs - Anat to DWI") { + config "./ants.config" when { workflow { """ @@ -43,16 +45,16 @@ nextflow_workflow { b0: it.simpleName == "b0" dti: it.simpleName == "dti" } - input[0] = ch_split_test_data.t1w.map{ + input[0] = ch_split_test_data.b0.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/T1w.nii.gz") + file("\${test_data_directory}/b0.nii.gz") ] } - input[1] = ch_split_test_data.b0.map{ + input[1] = ch_split_test_data.t1w.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz") + file("\${test_data_directory}/T1w.nii.gz") ] } input[2] = ch_split_test_data.dti.map{ @@ -64,6 +66,7 @@ nextflow_workflow { input[3] = Channel.empty() input[4] = Channel.empty() input[5] = Channel.empty() + input[6] = Channel.empty() """ } } @@ -78,7 +81,7 @@ nextflow_workflow { [(channel.key): ["versions"].contains(channel.key) ? channel.value : channel.value.collect{ subject -> - [ subject[0] ] + subject[1..-1].collect{ entry -> entry ? file(entry).name : "" } + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } } ] } ).match() }, @@ -95,31 +98,35 @@ nextflow_workflow { } } - test("registration - SyNQuick") { - config "./nextflow.config" + test("registration - ANTs - SyNQuick") { + config "./ants.config" when { workflow { """ ch_split_test_data = LOAD_DATA.out.test_data_directory .branch{ t1w: it.simpleName == "T1w" - b0: it.simpleName == "b0" - dti: it.simpleName == "dti" + moving: it.simpleName == "others" } input[0] = ch_split_test_data.t1w.map{ test_data_directory -> [ [ id:'test' ], file("\${test_data_directory}/T1w.nii.gz") ]} - input[1] = ch_split_test_data.b0.map{ + input[1] = ch_split_test_data.moving.map{ test_data_directory -> [ [ id:'test' ], - file("\${test_data_directory}/b0.nii.gz") + file("\${test_data_directory}/t1.nii.gz") ]} input[2] = Channel.empty() - input[3] = Channel.empty() + input[3] = ch_split_test_data.t1w.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/T1w_mask.nii.gz") + ]} input[4] = Channel.empty() input[5] = Channel.empty() + input[6] = Channel.empty() """ } } @@ -134,7 +141,7 @@ nextflow_workflow { [(channel.key): ["versions"].contains(channel.key) ? channel.value : channel.value.collect{ subject -> - [ subject[0] ] + subject[1..-1].collect{ entry -> entry ? file(entry).name : "" } + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } } ] } ).match() }, @@ -152,7 +159,7 @@ nextflow_workflow { } test("registration - easyreg") { - config "./nextflow_easyreg.config" + config "./easyreg.config" when { workflow { """ @@ -160,7 +167,6 @@ nextflow_workflow { .branch{ t1w: it.simpleName == "T1w" b0: it.simpleName == "b0" - dti: it.simpleName == "dti" } input[0] = ch_split_test_data.t1w.map{ test_data_directory -> [ @@ -178,6 +184,7 @@ nextflow_workflow { input[3] = Channel.empty() input[4] = Channel.empty() input[5] = Channel.empty() + input[6] = Channel.empty() """ } } @@ -192,15 +199,16 @@ nextflow_workflow { [(channel.key): ["versions"].contains(channel.key) ? channel.value : channel.value.collect{ subject -> - [ subject[0] ] + subject[1..-1].collect{ entry -> entry ? file(entry).name : "" } + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } } ] } ).match() } ) } } - test("registration - synthregistration") { - config "./nextflow_synthregistration.config" + + test("registration - Synthmorph") { + config "./synthmorph.config" when { workflow { """ @@ -208,7 +216,7 @@ nextflow_workflow { .branch{ t1w: it.simpleName == "T1w" b0: it.simpleName == "b0" - dti: it.simpleName == "dti" + freesurfer: it.simpleName == "freesurfer" } input[0] = ch_split_test_data.t1w.map{ test_data_directory -> [ @@ -226,6 +234,9 @@ nextflow_workflow { input[3] = Channel.empty() input[4] = Channel.empty() input[5] = Channel.empty() + input[6] = ch_split_test_data.freesurfer.map{ + test_data_directory -> file("\${test_data_directory}/license.txt") + } """ } } @@ -240,14 +251,13 @@ nextflow_workflow { [(channel.key): ["versions"].contains(channel.key) ? channel.value : channel.value.collect{ subject -> - [ subject[0] ] + subject[1..-1].collect{ entry -> entry ? file(entry).name : "" } + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } } ] } ).match() }, { assert workflow.out .findAll{ channel -> !channel.key.isInteger() } .every{ channel -> ["ref_warped", - "transfo_trk", "segmentation", "ref_segmentation"].contains(channel.key) ? channel.value.size() == 0 diff --git a/subworkflows/nf-neuro/registration/tests/main.nf.test.snap b/subworkflows/nf-neuro/registration/tests/main.nf.test.snap index b42e26eb..bc02436a 100644 --- a/subworkflows/nf-neuro/registration/tests/main.nf.test.snap +++ b/subworkflows/nf-neuro/registration/tests/main.nf.test.snap @@ -1,84 +1,160 @@ { - "registration - synthregistration": { + "registration - Synthmorph": { "content": [ { - "image_warped": [ + "backward_affine": [ + [ + { + "id": "test" + }, + "test_out_affine.txt" + ] + ], + "backward_image_transform": [ + [ + { + "id": "test" + }, + "test_out_affine.txt", + "test_out_warp.nii.gz" + ] + ], + "backward_tractogram_transform": [ + [ + { + "id": "test" + }, + "test_out_warp.nii.gz", + "test_out_affine.txt" + ] + ], + "forward_affine": [ [ { "id": "test" }, - "test__output_warped.nii.gz" + "test_out_affine.txt" ] ], - "transfo_image": [ + "forward_image_transform": [ [ { "id": "test" }, - "test__deform_warp.nii.gz", - "test__affine_warp.lta" + "test_out_warp.nii.gz", + "test_out_affine.txt" + ] + ], + "forward_tractogram_transform": [ + [ + { + "id": "test" + }, + "test_out_affine.txt", + "test_out_warp.nii.gz" + ] + ], + "image_warped": [ + [ + { + "id": "test" + }, + "test_warped.nii.gz" ] ], "versions": [ - "versions.yml:md5,8a5fc770044eef3575fe68a3f7792976" + "versions.yml:md5,300199ef2ea6dd6438553c3ec2615e58", + "versions.yml:md5,5bc0d347809508f4b94ebe433b0f21b9" ] } ], "meta": { "nf-test": "0.9.0", - "nextflow": "25.04.6" + "nextflow": "25.04.8" }, - "timestamp": "2025-07-04T21:00:00.502234107" + "timestamp": "2025-10-16T20:51:47.326447932" }, "registration - easyreg": { "content": [ { - "image_warped": [ + "backward_image_transform": [ [ { "id": "test" }, - "test_floating_registered.nii.gz" + "test_backward0_warp.nii.gz" ] ], - "ref_segmentation": [ + "backward_tractogram_transform": [ [ { "id": "test" }, - "test_reference_segmentation.nii.gz" + "test_forward0_warp.nii.gz" ] ], - "ref_warped": [ + "backward_warp": [ [ { "id": "test" }, - "test_reference_registered.nii.gz" + "test_backward0_warp.nii.gz" ] ], - "segmentation": [ + "forward_image_transform": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz" + ] + ], + "forward_tractogram_transform": [ + [ + { + "id": "test" + }, + "test_backward0_warp.nii.gz" + ] + ], + "forward_warp": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz" + ] + ], + "image_warped": [ + [ + { + "id": "test" + }, + "test_warped.nii.gz" + ] + ], + "reference_segmentation": [ [ { "id": "test" }, - "test_floating_segmentation.nii.gz" + "test_warped_reference_segmentation.nii.gz" ] ], - "transfo_image": [ + "reference_warped": [ [ { "id": "test" }, - "test_forward_field.nii.gz" + "test_warped_reference.nii.gz" ] ], - "transfo_trk": [ + "segmentation": [ [ { "id": "test" }, - "test_backward_field.nii.gz" + "test_warped_segmentation.nii.gz" ] ], "versions": [ @@ -87,89 +163,205 @@ } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.7" + "nf-test": "0.9.0", + "nextflow": "25.04.8" }, - "timestamp": "2025-09-20T13:03:27.206157" + "timestamp": "2025-10-15T18:55:55.830874285" }, - "registration - antsRegistration": { + "registration - ANTs - SyNQuick": { "content": [ { - "image_warped": [ + "backward_affine": [ [ { "id": "test" }, - "test__t1_warped.nii.gz" + "test_backward0_affine.mat" ] ], - "transfo_image": [ + "backward_image_transform": [ [ { "id": "test" }, - "test__output1Warp.nii.gz", - "test__output0GenericAffine.mat" + "test_backward0_affine.mat", + "test_backward1_warp.nii.gz" ] ], - "transfo_trk": [ + "backward_tractogram_transform": [ [ { "id": "test" }, - "test__output0GenericAffine.mat", - "test__output1InverseWarp.nii.gz" + "test_forward0_warp.nii.gz", + "test_forward1_affine.mat" + ] + ], + "backward_warp": [ + [ + { + "id": "test" + }, + "test_backward1_warp.nii.gz" + ] + ], + "forward_affine": [ + [ + { + "id": "test" + }, + "test_forward1_affine.mat" + ] + ], + "forward_image_transform": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz", + "test_forward1_affine.mat" + ] + ], + "forward_tractogram_transform": [ + [ + { + "id": "test" + }, + "test_backward0_affine.mat", + "test_backward1_warp.nii.gz" + ] + ], + "forward_warp": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz" + ] + ], + "image_warped": [ + [ + { + "id": "test" + }, + "test_t1_warped.nii.gz" + ] + ], + "mqc": [ + [ + { + "id": "test" + }, + "test__registration_ants_mqc.gif" ] ], "versions": [ - "versions.yml:md5,0f0af6043cb0549dd850d1f59d0c7190" + "versions.yml:md5,2740f4407177a63180822db9890b82fc" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.04.8" }, - "timestamp": "2025-10-17T17:41:51.560683985" + "timestamp": "2025-10-24T16:25:08.767472444" }, - "registration - SyNQuick": { + "registration - ANTs - Anat to DWI": { "content": [ { - "image_warped": [ + "backward_affine": [ [ { "id": "test" }, - "test__t1_warped.nii.gz" + "test_backward0_affine.mat" ] ], - "transfo_image": [ + "backward_image_transform": [ [ { "id": "test" }, - "test__output1Warp.nii.gz", - "test__output0GenericAffine.mat" + "test_backward0_affine.mat", + "test_backward1_warp.nii.gz" ] ], - "transfo_trk": [ + "backward_tractogram_transform": [ [ { "id": "test" }, - "test__output1InverseAffine.mat", - "test__output0InverseWarp.nii.gz" + "test_forward0_warp.nii.gz", + "test_forward1_affine.mat" + ] + ], + "backward_warp": [ + [ + { + "id": "test" + }, + "test_backward1_warp.nii.gz" + ] + ], + "forward_affine": [ + [ + { + "id": "test" + }, + "test_forward1_affine.mat" + ] + ], + "forward_image_transform": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz", + "test_forward1_affine.mat" + ] + ], + "forward_tractogram_transform": [ + [ + { + "id": "test" + }, + "test_backward0_affine.mat", + "test_backward1_warp.nii.gz" + ] + ], + "forward_warp": [ + [ + { + "id": "test" + }, + "test_forward0_warp.nii.gz" + ] + ], + "image_warped": [ + [ + { + "id": "test" + }, + "test_T1w_warped.nii.gz" + ] + ], + "mqc": [ + [ + { + "id": "test" + }, + "test_registration_anattodwi_mqc.gif" ] ], "versions": [ - "versions.yml:md5,42f2dcb95a2cf29b157dbaebf162d2dd" + "versions.yml:md5,21ba9553a13abb65e62caa26aa079736" ] } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.10.0" }, - "timestamp": "2025-10-17T17:41:57.285829729" + "timestamp": "2025-10-28T16:02:30.217450739" } } \ No newline at end of file diff --git a/subworkflows/nf-neuro/registration/tests/nextflow.config b/subworkflows/nf-neuro/registration/tests/nextflow.config index 8f8c6c2e..d37a9c66 100644 --- a/subworkflows/nf-neuro/registration/tests/nextflow.config +++ b/subworkflows/nf-neuro/registration/tests/nextflow.config @@ -1,12 +1,4 @@ process { - withName: "REGISTRATION_ANATTODWI" { - ext.cpus = 1 - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - } - withName: "REGISTRATION_ANTS" { - ext.quick = true - ext.threads = 1 - ext.random_seed = 44 - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - } + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + cpus = 1 } diff --git a/subworkflows/nf-neuro/registration/tests/nextflow_easyreg.config b/subworkflows/nf-neuro/registration/tests/nextflow_easyreg.config deleted file mode 100644 index a4c0bd3f..00000000 --- a/subworkflows/nf-neuro/registration/tests/nextflow_easyreg.config +++ /dev/null @@ -1,8 +0,0 @@ -process { - withName: "REGISTRATION_EASYREG" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.field = true - } -} - -params.run_easyreg = true diff --git a/subworkflows/nf-neuro/registration/tests/nextflow_synthregistration.config b/subworkflows/nf-neuro/registration/tests/nextflow_synthregistration.config deleted file mode 100644 index b0a76764..00000000 --- a/subworkflows/nf-neuro/registration/tests/nextflow_synthregistration.config +++ /dev/null @@ -1,11 +0,0 @@ -process { - withName: "REGISTRATION_SYNTHREGISTRATION" { - publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } - ext.affine = "affine" - ext.warp = "deform" - ext.lambda = 0.9 - ext.steps = 9 - } -} - -params.run_synthmorph = true diff --git a/subworkflows/nf-neuro/registration/tests/synthmorph.config b/subworkflows/nf-neuro/registration/tests/synthmorph.config new file mode 100644 index 00000000..12eec6f8 --- /dev/null +++ b/subworkflows/nf-neuro/registration/tests/synthmorph.config @@ -0,0 +1,11 @@ +process { + withName: "REGISTRATION_SYNTHMORPH" { + ext.models = ["affine"] + ext.regularization = 0.9 + ext.steps = 9 + ext.extent = 192 + memory = '2G' + } +} + +params.run_synthmorph = true diff --git a/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test.snap b/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test.snap index 5a1990c5..f7682c92 100644 --- a/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test.snap +++ b/subworkflows/nf-neuro/topup_eddy/tests/main.nf.test.snap @@ -206,4 +206,4 @@ }, "timestamp": "2025-09-24T13:08:23.941067656" } -} +} \ No newline at end of file diff --git a/subworkflows/nf-neuro/tractoflow/main.nf b/subworkflows/nf-neuro/tractoflow/main.nf index ca9ed917..89575439 100644 --- a/subworkflows/nf-neuro/tractoflow/main.nf +++ b/subworkflows/nf-neuro/tractoflow/main.nf @@ -1,18 +1,18 @@ // PREPROCESSING -include { PREPROC_DWI } from '../preproc_dwi/main' -include { PREPROC_T1 } from '../preproc_t1/main' -include { REGISTRATION as T1_REGISTRATION } from '../registration/main' -include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_WMPARC } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' -include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_APARC_ASEG } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' -include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_LESION_MASK } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' -include { ANATOMICAL_SEGMENTATION } from '../anatomical_segmentation/main' +include { PREPROC_DWI } from '../preproc_dwi/main' +include { PREPROC_T1 } from '../preproc_t1/main' +include { REGISTRATION as T1_REGISTRATION } from '../registration/main' +include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_WMPARC } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' +include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_APARC_ASEG } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' +include { REGISTRATION_ANTSAPPLYTRANSFORMS as TRANSFORM_LESION_MASK } from '../../../modules/nf-neuro/registration/antsapplytransforms/main' +include { ANATOMICAL_SEGMENTATION } from '../anatomical_segmentation/main' // RECONSTRUCTION -include { RECONST_FRF } from '../../../modules/nf-neuro/reconst/frf/main' -include { RECONST_MEANFRF } from '../../../modules/nf-neuro/reconst/meanfrf/main' -include { RECONST_DTIMETRICS } from '../../../modules/nf-neuro/reconst/dtimetrics/main' -include { RECONST_FODF } from '../../../modules/nf-neuro/reconst/fodf/main' +include { RECONST_FRF } from '../../../modules/nf-neuro/reconst/frf/main' +include { RECONST_MEANFRF } from '../../../modules/nf-neuro/reconst/meanfrf/main' +include { RECONST_DTIMETRICS } from '../../../modules/nf-neuro/reconst/dtimetrics/main' +include { RECONST_FODF } from '../../../modules/nf-neuro/reconst/fodf/main' // TRACKING include { TRACKING_PFTTRACKING } from '../../../modules/nf-neuro/tracking/pfttracking/main' @@ -98,6 +98,7 @@ workflow TRACTOFLOW { RECONST_DTIMETRICS.out.fa, Channel.empty(), Channel.empty(), + Channel.empty(), Channel.empty() ) ch_versions = ch_versions.mix(T1_REGISTRATION.out.versions.first()) @@ -110,7 +111,7 @@ workflow TRACTOFLOW { TRANSFORM_WMPARC( ch_wmparc .join(PREPROC_DWI.out.b0) - .join(T1_REGISTRATION.out.transfo_image) + .join(T1_REGISTRATION.out.forward_image_transform) ) ch_versions = ch_versions.mix(TRANSFORM_WMPARC.out.versions.first()) @@ -120,7 +121,7 @@ workflow TRACTOFLOW { TRANSFORM_APARC_ASEG( ch_aparc_aseg .join(PREPROC_DWI.out.b0) - .join(T1_REGISTRATION.out.transfo_image) + .join(T1_REGISTRATION.out.forward_image_transform) ) ch_versions = ch_versions.mix(TRANSFORM_APARC_ASEG.out.versions.first()) @@ -129,7 +130,7 @@ workflow TRACTOFLOW { TRANSFORM_LESION_MASK( ch_lesion_mask .join(PREPROC_DWI.out.b0) - .join(T1_REGISTRATION.out.transfo_image) + .join(T1_REGISTRATION.out.forward_image_transform) ) ch_versions = ch_versions.mix(TRANSFORM_LESION_MASK.out.versions.first()) @@ -269,8 +270,8 @@ workflow TRACTOFLOW { wmparc = TRANSFORM_WMPARC.out.warped_image // REGISTRATION - anatomical_to_diffusion = T1_REGISTRATION.out.transfo_image - diffusion_to_anatomical = T1_REGISTRATION.out.transfo_trk + anatomical_to_diffusion = T1_REGISTRATION.out.forward_image_transform + diffusion_to_anatomical = T1_REGISTRATION.out.backward_image_transform // IN ANATOMICAL SPACE t1_native = PREPROC_T1.out.t1_final diff --git a/subworkflows/nf-neuro/tractoflow/tests/main.nf.test b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test index 06f4c7c4..63c22e6a 100644 --- a/subworkflows/nf-neuro/tractoflow/tests/main.nf.test +++ b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test @@ -99,7 +99,7 @@ nextflow_workflow { [(channel.key): ["versions"].contains(channel.key) ? channel.value : channel.value.collect{ subject -> - [ subject[0] ] + subject[1..-1].collect{ entry -> entry ? file(entry).name : "" } + [ subject[0] ] + subject[1..-1].flatten().collect{ entry -> entry ? file(entry).name : "" } } ] } ).match() } diff --git a/subworkflows/nf-neuro/tractoflow/tests/main.nf.test.snap b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test.snap index bca3e2e6..39fe6e5c 100644 --- a/subworkflows/nf-neuro/tractoflow/tests/main.nf.test.snap +++ b/subworkflows/nf-neuro/tractoflow/tests/main.nf.test.snap @@ -31,8 +31,8 @@ { "id": "test" }, - "test__output1Warp.nii.gz", - "test__output0GenericAffine.mat" + "test_forward0_warp.nii.gz", + "test_forward1_affine.mat" ] ], "csf_fodf": [ @@ -64,8 +64,8 @@ { "id": "test" }, - "test__output0GenericAffine.mat", - "test__output1InverseWarp.nii.gz" + "test_backward0_affine.mat", + "test_backward1_warp.nii.gz" ] ], "dti_ad": [ @@ -195,9 +195,7 @@ { "id": "test" }, - "test__frf.txt", - "", - "" + "test__frf.txt" ], [ { @@ -365,7 +363,7 @@ { "id": "test" }, - "test__t1_warped.nii.gz" + "test_b0_warped.nii.gz" ] ], "t1_native": [ @@ -383,7 +381,7 @@ "versions.yml:md5,5011203328febedfa9c5af5684bcc20c", "versions.yml:md5,6f86cbe9b96fbefff47fe66fcb287678", "versions.yml:md5,7cabc91ec64b5e824e9b8f60d7dcf930", - "versions.yml:md5,91878ccc28dd256f9f90600fc048c99f", + "versions.yml:md5,91bb720c5b6b219ae9d7e0f671592381", "versions.yml:md5,955e704dfd97848e196333d0cecf0451", "versions.yml:md5,ee0cb6ae187add67175c9997ef1a5fd1" ], @@ -422,8 +420,8 @@ } ], "meta": { - "nf-test": "0.9.0", - "nextflow": "25.04.7" + "nf-test": "0.9.3", + "nextflow": "25.04.8" }, "timestamp": "2025-10-17T17:43:41.279576493" } diff --git a/tests/config/nextflow.config b/tests/config/nextflow.config index 4e825599..20e45ccd 100644 --- a/tests/config/nextflow.config +++ b/tests/config/nextflow.config @@ -49,7 +49,7 @@ profiles { } docker { docker.enabled = true - docker.runOptions = '-u $(id -u):$(id -g) --platform=linux/amd64' + docker.runOptions = '-u $(id -u):$(id -g) --group-add root --platform=linux/amd64' } docker_self_hosted{ docker.enabled = true @@ -62,10 +62,10 @@ profiles { docker.runOptions = '--platform=linux/amd64' } arm { - docker.runOptions = '-u $(id -u):$(id -g) --platform=linux/amd64' + docker.runOptions = '-u $(id -u):$(id -g) --group-add root --platform=linux/amd64' } gpu { - docker.runOptions = '-u $(id -u):$(id -g) --gpus all' + docker.runOptions = '-u $(id -u):$(id -g) --group-add root --gpus all' apptainer.runOptions = '--nv' singularity.runOptions = '--nv' }