Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New module] Module Synthseg #6

Merged
merged 10 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .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
anroy1 marked this conversation as resolved.
Show resolved Hide resolved
- 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
Loading