diff --git a/modules/nf-scil/registration/convert/environment.yml b/modules/nf-scil/registration/convert/environment.yml new file mode 100644 index 00000000..f802ec84 --- /dev/null +++ b/modules/nf-scil/registration/convert/environment.yml @@ -0,0 +1,7 @@ +--- +name: "registration_convert" +channels: + - Docker + - Apptainer +dependencies: + - "Freesurfer" diff --git a/modules/nf-scil/registration/convert/main.nf b/modules/nf-scil/registration/convert/main.nf new file mode 100644 index 00000000..4e28f613 --- /dev/null +++ b/modules/nf-scil/registration/convert/main.nf @@ -0,0 +1,90 @@ +process REGISTRATION_CONVERT { + tag "$meta.id" + label 'process_single' + + container "freesurfer/freesurfer:7.4.1" + containerOptions "--entrypoint ''" + containerOptions "--env FSLOUTPUTTYPE='NIFTI_GZ'" + + input: + tuple val(meta), path(affine), path(deform), path(source), path(target) /* optional, value = [] */, path(fs_license) /* optional, value = [] */ + + output: + tuple val(meta), path("*.{txt,lta,mat,dat}"), emit: init_transform + tuple val(meta), path("*.{nii,nii.gz,mgz,m3z}"), emit: deform_transform + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + 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_init = "$source" ? "--src " + "$source" : "" + def target_geometry_init = "$target" ? "--trg " + "$target" : "" + def in_format_init = task.ext.in_format_init ? "--in" + task.ext.in_format_init + " " + "$affine" : "--inlta " + "$affine" + def out_format_init = task.ext.out_format_init ? "--out" + task.ext.out_format_init : "--outitk" + + //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" : "" + + """ + export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=$task.cpus + export OMP_NUM_THREADS=1 + export OPENBLAS_NUM_THREADS=1 + + cp $fs_license \$FREESURFER_HOME/license.txt + + declare -A affine_dictionnary=( ["--outlta"]="lta" \ + ["--outfsl"]="mat" \ + ["--outmni"]="xfm" \ + ["--outreg"]="dat" \ + ["--outniftyreg"]="txt" \ + ["--outitk"]="txt" \ + ["--outvox"]="txt" ) + + ext_affine=\${affine_dictionnary[${out_format_init}]} + + 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}]} + + lta_convert ${invert} ${source_geometry_init} ${target_geometry_init} ${in_format_init} ${out_format_init} ${prefix}__init_warp.\${ext_affine} + mri_warp_convert ${source_geometry_deform} ${downsample} ${in_format_deform} ${out_format_deform} ${prefix}__deform_warp.\${ext_deform} + + rm \$FREESURFER_HOME/license.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + Freesurfer: 7.4.1 + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + lta_convert -h + mri_warp_convert -h + + touch ${prefix}__init_transform.txt + touch ${prefix}__deform_transform.nii.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + Freesurfer: 7.4.1 + END_VERSIONS + """ +} diff --git a/modules/nf-scil/registration/convert/meta.yml b/modules/nf-scil/registration/convert/meta.yml new file mode 100644 index 00000000..21f48c77 --- /dev/null +++ b/modules/nf-scil/registration/convert/meta.yml @@ -0,0 +1,69 @@ +--- +name: "registration_convert" +description: Freesurfer transform conversion tool. Default usage is aim at receiving freesurfer format and converting to ANTs (ITK). See lta_convert --help and mri_warp_convert --help for alternatives. +keywords: + - Registration + - Brain imaging + - MRI +tools: + - "Freesurfer": + description: "Freesurfer lta_convert (affine conversion) and mri_warp_convert (deform) tools for transform conversion" + homepage: "https://surfer.nmr.mgh.harvard.edu/" + documentation: "https://surfer.nmr.mgh.harvard.edu/fswiki/FreeSurferWiki" + +input: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - affine: + type: file + description: Affine transform to convert. Default usage expects Freesurfer .lta format from mri_synthmorph + pattern: "*.{lta,txt,fslmat,xfm,dat}" + + - deform: + type: file + description: Deform transform to convert. Default usage expects Freesurfer .mgz format from mri_synthmorph + pattern: "*.{nii,nii.gz,mgz,m3z}" + + - source: + type: file + description: Moving Nifti volume used for registration. Defines source image geometry + pattern: "*.{nii,nii.gz}" + + - target: + type: file + description: Fixed Nifti volume used for registration. Defines target image geometry. (optional) + pattern: "*.{nii,nii.gz}" + + - fs_license: + type: file + description: The path to your FreeSurfer license. To get one, go to https://surfer.nmr.mgh.harvard.edu/registration.html. 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" + +output: + - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + + - init_transform: + type: file + description: Init transform. Default usage outputs ANTs (ITK) format .txt + pattern: "*.{txt,lta,mat,dat}" + + - deform_transform: + type: file + description: Deform transform. Default usage outputs ANTs (ITK) format .nii.gz + pattern: "*.{nii,nii.gz,mgz,m3z}" + + - versions: + type: file + description: File containing software versions + pattern: "versions.yml" + +authors: + - "@anroy1" diff --git a/modules/nf-scil/registration/convert/tests/main.nf.test b/modules/nf-scil/registration/convert/tests/main.nf.test new file mode 100644 index 00000000..a7b2d84f --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/main.nf.test @@ -0,0 +1,267 @@ +nextflow_process { + + name "Test Process REGISTRATION_CONVERT" + script "../main.nf" + process "REGISTRATION_CONVERT" + + tag "modules" + tag "modules_nfcore" + tag "registration" + tag "registration/convert" + + tag "subworkflows" + tag "subworkflows/load_test_data" + + + setup { + run("LOAD_TEST_DATA", alias: "LOAD_DATA") { + script "../../../../../subworkflows/nf-scil/load_test_data/main.nf" + process { + """ + input[0] = Channel.from( [ "freesurfer.zip" , "freesurfer_reslice.zip" , "freesurfer_transforms.zip"] ) + input[1] = "test.load-test-data" + """ + } + } + } + + test("registration - convert - default") { + + config "./nextflow_default.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}/fs_affine.lta"), + file("\${test_data_directory}/fs_deform.nii.gz"), + ] + } + ch_reslice = 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{ + test_data_directory -> [ + [ id:'test' ], + 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 - fsants") { + + config "./nextflow_fsants.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}/fs_affine.lta"), + file("\${test_data_directory}/fs_deform.nii.gz") + ] + } + 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) + """ + } + } + + 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" + } + ch_transforms = ch_split_test_data.transforms.map{ + test_data_directory -> [ + [ id:'test' ], + file("\${test_data_directory}/fsl_affine.mat"), + file("\${test_data_directory}/fsl_deform.nii.gz") + ] + } + 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) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("registration - convert - fslants") { + + config "./nextflow_fslants.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}/fsl_affine.mat"), + file("\${test_data_directory}/fsl_deform.nii.gz") + ] + } + 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) + """ + } + } + + 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_affine.txt"), + file("\${test_data_directory}/ants_deform.nii.gz") + ] + } + 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) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-scil/registration/convert/tests/main.nf.test.snap b/modules/nf-scil/registration/convert/tests/main.nf.test.snap new file mode 100644 index 00000000..6cf6fd6a --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/main.nf.test.snap @@ -0,0 +1,247 @@ +{ + "registration - convert - fsants": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__init_warp.txt:md5,5f989a979be61faa578ad619377a8a07" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + ] + ], + "2": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ], + "deform_transform": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + ] + ], + "init_transform": [ + [ + { + "id": "test" + }, + "test__init_warp.txt:md5,5f989a979be61faa578ad619377a8a07" + ] + ], + "versions": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ] + } + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-17T18:13:39.632131" + }, + "registration - convert - default": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__init_warp.txt:md5,5f989a979be61faa578ad619377a8a07" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + ] + ], + "2": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ], + "deform_transform": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,69052e6226da946bad1f9466285cbb89" + ] + ], + "init_transform": [ + [ + { + "id": "test" + }, + "test__init_warp.txt:md5,5f989a979be61faa578ad619377a8a07" + ] + ], + "versions": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ] + } + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-17T18:13:34.897607" + }, + "registration - convert - antsfs": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__init_warp.lta:md5,74ec8f54427d766cb525c70c8c288cb9" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,599fbe3a6b85d61c0c67cea8be4972b7" + ] + ], + "2": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ], + "deform_transform": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,599fbe3a6b85d61c0c67cea8be4972b7" + ] + ], + "init_transform": [ + [ + { + "id": "test" + }, + "test__init_warp.lta:md5,74ec8f54427d766cb525c70c8c288cb9" + ] + ], + "versions": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ] + } + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-17T18:14:12.617303" + }, + "registration - convert - fslants": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__init_warp.txt:md5,074e8ac5777a91ba0808cd58c5a0cc44" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,94feb8f0e648256eaa5ae0a47e5702c6" + ] + ], + "2": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ], + "deform_transform": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,94feb8f0e648256eaa5ae0a47e5702c6" + ] + ], + "init_transform": [ + [ + { + "id": "test" + }, + "test__init_warp.txt:md5,074e8ac5777a91ba0808cd58c5a0cc44" + ] + ], + "versions": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ] + } + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-17T18:14:07.85049" + }, + "registration - convert - fslfs": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test__init_warp.lta:md5,4b2d78a1a34240dcf2d0abe0980549bd" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,df432a27c586e57cad93af7509941cd4" + ] + ], + "2": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ], + "deform_transform": [ + [ + { + "id": "test" + }, + "test__deform_warp.nii.gz:md5,df432a27c586e57cad93af7509941cd4" + ] + ], + "init_transform": [ + [ + { + "id": "test" + }, + "test__init_warp.lta:md5,4b2d78a1a34240dcf2d0abe0980549bd" + ] + ], + "versions": [ + "versions.yml:md5,9912ec095965c1ff571f77b447c18f92" + ] + } + ], + "meta": { + "nf-test": "0.9.0-rc1", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-17T18:13:53.506966" + } +} \ No newline at end of file diff --git a/modules/nf-scil/registration/convert/tests/nextflow_antsfs.config b/modules/nf-scil/registration/convert/tests/nextflow_antsfs.config new file mode 100644 index 00000000..da4abf8c --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/nextflow_antsfs.config @@ -0,0 +1,9 @@ +process { + withName: "REGISTRATION_CONVERT" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.in_format_init = "itk" + ext.out_format_init = "lta" + ext.in_format_deform = "itk" + ext.out_format_deform = "ras" + } +} diff --git a/modules/nf-scil/registration/convert/tests/nextflow_default.config b/modules/nf-scil/registration/convert/tests/nextflow_default.config new file mode 100644 index 00000000..07f6f24b --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/nextflow_default.config @@ -0,0 +1,5 @@ +process { + withName: "REGISTRATION_CONVERT" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + } +} diff --git a/modules/nf-scil/registration/convert/tests/nextflow_fsants.config b/modules/nf-scil/registration/convert/tests/nextflow_fsants.config new file mode 100644 index 00000000..793cf577 --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/nextflow_fsants.config @@ -0,0 +1,9 @@ +process { + withName: "REGISTRATION_CONVERT" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.in_format_init = "lta" + ext.out_format_init = "itk" + ext.in_format_deform = "ras" + ext.out_format_deform = "itk" + } +} diff --git a/modules/nf-scil/registration/convert/tests/nextflow_fslants.config b/modules/nf-scil/registration/convert/tests/nextflow_fslants.config new file mode 100644 index 00000000..e989f119 --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/nextflow_fslants.config @@ -0,0 +1,9 @@ +process { + withName: "REGISTRATION_CONVERT" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.in_format_init = "fsl" + ext.out_format_init = "itk" + ext.in_format_deform = "fsl" + ext.out_format_deform = "itk" + } +} diff --git a/modules/nf-scil/registration/convert/tests/nextflow_fslfs.config b/modules/nf-scil/registration/convert/tests/nextflow_fslfs.config new file mode 100644 index 00000000..652ed07d --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/nextflow_fslfs.config @@ -0,0 +1,9 @@ +process { + withName: "REGISTRATION_CONVERT" { + publishDir = { "${params.outdir}/${task.process.tokenize(':')[-1].tokenize('_')[0].toLowerCase()}" } + ext.in_format_init = "fsl" + ext.out_format_init = "lta" + ext.in_format_deform = "fsl" + ext.out_format_deform = "ras" + } +} diff --git a/modules/nf-scil/registration/convert/tests/tags.yml b/modules/nf-scil/registration/convert/tests/tags.yml new file mode 100644 index 00000000..8448388a --- /dev/null +++ b/modules/nf-scil/registration/convert/tests/tags.yml @@ -0,0 +1,2 @@ +registration/convert: + - "modules/nf-scil/registration/convert/**"