Skip to content

Commit

Permalink
Merge pull request #6 from anroy1/synthseg2
Browse files Browse the repository at this point in the history
[New module] Module Synthseg
  • Loading branch information
AlexVCaron authored Sep 26, 2024
2 parents c463eff + 4839fa8 commit 3a9a336
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/run_checks_suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ jobs:
- runner: scilus-nf-neuro-runners
- runner: scilus-nf-neuro-bigmem-runners
path: modules/nf-neuro/registration/easyreg
- runner: scilus-nf-neuro-bigmem-runners
path: modules/nf-neuro/segmentation/synthseg
exclude:
- path: subworkflows/nf-neuro/load_test_data
uses: ./.github/workflows/test_component.yml
Expand Down
3 changes: 3 additions & 0 deletions modules/nf-neuro/segmentation/synthseg/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
channels: []
dependencies: []
name: segmentation_synthseg
113 changes: 113 additions & 0 deletions modules/nf-neuro/segmentation/synthseg/main.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
process SEGMENTATION_SYNTHSEG {
tag "$meta.id"
label 'process_single'
label 'process_high'

container "freesurfer/freesurfer:7.4.1"

input:
tuple val(meta), path(image), path(lesion) /* optional, input = [] */, path(fs_license) /* optional, input = [] */

output:
tuple val(meta), path("*__seg.nii.gz") , emit: seg
tuple val(meta), path("*__mask_wm.nii.gz") , emit: wm_mask
tuple val(meta), path("*__mask_gm.nii.gz") , emit: gm_mask
tuple val(meta), path("*__mask_csf.nii.gz") , emit: csf_mask
tuple val(meta), path("*__gm_parc.nii.gz") , emit: gm_parc, optional: true
tuple val(meta), path("*__resampled_image.nii.gz") , emit: resample, optional: true
tuple val(meta), path("*__volume.csv") , emit: volume, optional: true
tuple val(meta), path("*__qc_score.csv") , emit: qc_score, optional: true
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}"

def gpu = task.ext.gpu ? "" : "--cpu"
def gm_parc = task.ext.gm_parc ? "--parc" : ""
def robust = task.ext.robust ? "--robust" : ""
def fast = task.ext.fast ? "--fast" : ""
def ct = task.ext.ct ? "--ct" : ""
def output_resample = task.ext.output_resample ? "--resample ${prefix}__resampled_image.nii.gz": ""
def output_volume = task.ext.output_volume ? "--vol ${prefix}__volume.csv" : ""
def output_qc_score = task.ext.output_qc_score ? "--qc ${prefix}__qc_score.csv" : ""
def crop = task.ext.crop ? "--crop " + task.ext.crop: ""

"""
export ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1
export OMP_NUM_THREADS=1
export OPENBLAS_NUM_THREADS=1
cp $fs_license \$FREESURFER_HOME/license.txt
mri_synthseg --i $image --o seg.nii.gz --threads $task.cpus $gpu $robust $fast $ct $output_resample $output_volume $output_qc_score $crop
cp seg.nii.gz ${prefix}__seg.nii.gz
if [[ -n "$gm_parc" ]];
then
# Cortical grey matter parcellation
mri_synthseg --i $image --o ${prefix}__gm_parc.nii.gz --threads $task.cpus $gpu $gm_parc $robust $fast $crop
mri_convert -i ${prefix}__gm_parc.nii.gz --out_data_type uchar -o ${prefix}__gm_parc.nii.gz
fi
# WM Mask
mri_binarize --i seg.nii.gz \
--match 2 7 10 12 13 16 28 41 46 49 51 52 60 \
--o ${prefix}__mask_wm.nii.gz
# GM Mask
mri_binarize --i seg.nii.gz \
--match 3 8 11 17 18 26 42 47 50 52 53 54 58 \
--o ${prefix}__mask_gm.nii.gz
# CSF Mask
mri_binarize --i seg.nii.gz \
--match 4 5 14 15 24 43 44 \
--o ${prefix}__mask_csf.nii.gz
if [[ -f "$lesion" ]];
then
mri_binarize --i ${prefix}__mask_wm.nii.gz --merge $lesion --min 0.5 --o ${prefix}__mask_wm.nii.gz
fi
mri_convert -i ${prefix}__mask_wm.nii.gz --out_data_type uchar -o ${prefix}__mask_wm.nii.gz
mri_convert -i ${prefix}__mask_gm.nii.gz --out_data_type uchar -o ${prefix}__mask_gm.nii.gz
mri_convert -i ${prefix}__mask_csf.nii.gz --out_data_type uchar -o ${prefix}__mask_csf.nii.gz
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}"

"""
mri_synthseg -h
mri_binarize -h
mri_convert -h
touch ${prefix}__seg.nii.gz
touch ${prefix}__mask_wm.nii.gz
touch ${prefix}__mask_gm.nii.gz
touch ${prefix}__mask_csf.nii.gz
touch ${prefix}__gm_parc.nii.gz
touch ${prefix}__resampled_image.nii.gz
touch ${prefix}__volume.csv
touch ${prefix}__qc_score.csv
cat <<-END_VERSIONS > versions.yml
"${task.process}":
Freesurfer: 7.4.1
END_VERSIONS
"""
}
87 changes: 87 additions & 0 deletions modules/nf-neuro/segmentation/synthseg/meta.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: "segmentation_synthseg"
description: Perform Brain Tissues Segmentation using Freesurfer synthseg on a T1 image. Optionally, a binary mask of lesion can be add to correct the white matter mask. Note that tests using synthseg are non-reproductible.
keywords:
- Segmentation
- Freesurfer
- Tissues
- Synthetic
- AI
- CNN
tools:
- "Freesurfer":
description: "An open source neuroimaging toolkit for processing, analyzing, and visualizing human brain MR images."
homepage: "https://surfer.nmr.mgh.harvard.edu/"

input:
- meta:
type: map
description: |
Groovy Map containing sample information
e.g. `[ id:'sample1', single_end:false ]`
- image:
type: file
description: Nifti T1 volume to segment into tissue maps.
pattern: "*.{nii,nii.gz}"

- lesion:
type: file
description: Nifti lesion volume to correct the white matter with a lesion mask. The lesion mask must be a binary mask.
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:'sample1', single_end:false ]`
- wm_mask:
type: file
description: Nifti WM mask volume.
pattern: "*.{nii,nii.gz}"

- gm_mask:
type: file
description: Nifti GM mask volume.
pattern: "*.{nii,nii.gz}"

- csf_mask:
type: file
description: Nifti CSF mask volume.
pattern: "*.{nii,nii.gz}"

- gm_parc:
type: file
description: (optional) Nifti cortical parcellation volume.
pattern: "*.{nii,nii.gz}"

- volume:
type: file
description: (optional) Output CSV file with volumes for all structures and subjects.
pattern: "*.csv"

- qc_score:
type: file
description: (optional) Output CSV file with qc scores for all subjects.
pattern: "*.csv"

- resample:
type: file
description: (optional) in order to return segmentations at 1mm resolution, the input images are internally resampled (except if they already are at 1mm). Use this optional flag to save the resampled images. This must be the same type as --i.
pattern: "*.{nii,nii.gz}"

- versions:
type: file
description: File containing software versions
pattern: "versions.yml"

authors:
- "@anroy1"
maintainers:
- "@anroy1"
160 changes: 160 additions & 0 deletions modules/nf-neuro/segmentation/synthseg/tests/main.nf.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
nextflow_process {

name "Test Process SEGMENTATION_SYNTHSEG"
script "../main.nf"
process "SEGMENTATION_SYNTHSEG"

tag "modules"
tag "modules_nfcore"
tag "segmentation"
tag "segmentation/synthseg"

tag "subworkflows"
tag "subworkflows/load_test_data"

setup {
run("LOAD_TEST_DATA", alias: "LOAD_DATA") {
script "../../../../../subworkflows/nf-neuro/load_test_data/main.nf"
process {
"""
input[0] = Channel.from( [ "heavy.zip", "freesurfer.zip", "processing.zip" ] )
input[1] = "test.load-test-data"
"""
}
}
}

// ** Assertion only uses file name since there is a discrepancy when tests are ran remotly. ** //
// ** Synthseg's direct output was emitted as an output to help assess its non-reproductibility ** //
// ** Synthseg is an AI model and freesurfer does not control for randomness in computation yet, both in terms of seeding and operation ordering.** //

test("segmentation - synthseg - basic") {
config "./nextflow_basic.config"
when {
process {
"""
ch_split_test_data = LOAD_DATA.out.test_data_directory
.branch{
heavy: it.simpleName == "heavy"
freesurfer: it.simpleName == "freesurfer"
}
ch_anat = ch_split_test_data.heavy.map{
test_data_directory -> [
[ id:'test' ],
file("\${test_data_directory}/anat/anat_image.nii.gz"),
[]
]
}
ch_license = ch_split_test_data.freesurfer.map{
test_data_directory -> [
[ id:'test' ],
file("\${test_data_directory}/license.txt")
]
}
input[0] = ch_anat
.join(ch_license)
"""
}
}

then {
assertAll(
{ assert process.success },
{ assert snapshot(
file(process.out.wm_mask.get(0).get(1)).name,
file(process.out.gm_mask.get(0).get(1)).name,
file(process.out.csf_mask.get(0).get(1)).name,
process.out.versions
).match() }
)
}
}

test("segmentation - synthseg - options") {
config "./nextflow_options.config"
when {
process {
"""
ch_split_test_data = LOAD_DATA.out.test_data_directory
.branch{
processing: it.simpleName == "processing"
freesurfer: it.simpleName == "freesurfer"
}
ch_anat = ch_split_test_data.processing.map{
test_data_directory -> [
[ id:'test' ],
file("\${test_data_directory}/mni_masked_2x2x2.nii.gz"),
[]
]
}
ch_license = ch_split_test_data.freesurfer.map{
test_data_directory -> [
[ id:'test' ],
file("\${test_data_directory}/license.txt")
]
}
input[0] = ch_anat
.join(ch_license)
"""
}
}

then {
assertAll(
{ assert process.success },
{ assert snapshot(
file(process.out.wm_mask.get(0).get(1)).name,
file(process.out.gm_mask.get(0).get(1)).name,
file(process.out.csf_mask.get(0).get(1)).name,
file(process.out.gm_parc.get(0).get(1)).name,
niftiMD5SUM(process.out.resample.get(0).get(1)),
process.out.volume,
process.out.qc_score,
process.out.versions
).match() }
)
}
}

test("segmentation - synthseg - lesion") {
config "./nextflow_basic.config"
when {
process {
"""
ch_split_test_data = LOAD_DATA.out.test_data_directory
.branch{
heavy: it.simpleName == "heavy"
freesurfer: it.simpleName == "freesurfer"
}
ch_anat = ch_split_test_data.heavy.map{
test_data_directory -> [
[ id:'test' ],
file("\${test_data_directory}/anat/anat_image.nii.gz"),
file("\${test_data_directory}/anat/anat_mask.nii.gz")
]
}
ch_license = ch_split_test_data.freesurfer.map{
test_data_directory -> [
[ id:'test' ],
file("\${test_data_directory}/license.txt")
]
}
input[0] = ch_anat
.join(ch_license)
"""
}
}

then {
assertAll(
{ assert process.success },
{ assert snapshot(
file(process.out.wm_mask.get(0).get(1)).name,
file(process.out.gm_mask.get(0).get(1)).name,
file(process.out.csf_mask.get(0).get(1)).name,
process.out.versions
).match() }
)
}
}
}
Loading

0 comments on commit 3a9a336

Please sign in to comment.