Skip to content

Commit 7f2863a

Browse files
authored
Merge pull request #460 from effigies/enh/fslr_surfs
feat: Output fsLR meshes on subject surfaces
2 parents 0bf8f89 + 7e93927 commit 7f2863a

File tree

4 files changed

+90
-21
lines changed

4 files changed

+90
-21
lines changed

.circleci/ds005_outputs.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ smriprep/sub-01/anat/sub-01_hemi-L_curv.shape.gii
2525
smriprep/sub-01/anat/sub-01_hemi-L_inflated.surf.gii
2626
smriprep/sub-01/anat/sub-01_hemi-L_midthickness.surf.gii
2727
smriprep/sub-01/anat/sub-01_hemi-L_pial.surf.gii
28+
smriprep/sub-01/anat/sub-01_hemi-L_space-fsLR_den-32k_midthickness.surf.gii
29+
smriprep/sub-01/anat/sub-01_hemi-L_space-fsLR_den-32k_pial.surf.gii
30+
smriprep/sub-01/anat/sub-01_hemi-L_space-fsLR_den-32k_white.surf.gii
2831
smriprep/sub-01/anat/sub-01_hemi-L_space-fsLR_desc-msmsulc_sphere.surf.gii
2932
smriprep/sub-01/anat/sub-01_hemi-L_space-fsLR_desc-reg_sphere.surf.gii
3033
smriprep/sub-01/anat/sub-01_hemi-L_space-fsaverage_desc-reg_sphere.surf.gii
@@ -36,6 +39,9 @@ smriprep/sub-01/anat/sub-01_hemi-R_curv.shape.gii
3639
smriprep/sub-01/anat/sub-01_hemi-R_inflated.surf.gii
3740
smriprep/sub-01/anat/sub-01_hemi-R_midthickness.surf.gii
3841
smriprep/sub-01/anat/sub-01_hemi-R_pial.surf.gii
42+
smriprep/sub-01/anat/sub-01_hemi-R_space-fsLR_den-32k_midthickness.surf.gii
43+
smriprep/sub-01/anat/sub-01_hemi-R_space-fsLR_den-32k_pial.surf.gii
44+
smriprep/sub-01/anat/sub-01_hemi-R_space-fsLR_den-32k_white.surf.gii
3945
smriprep/sub-01/anat/sub-01_hemi-R_space-fsLR_desc-msmsulc_sphere.surf.gii
4046
smriprep/sub-01/anat/sub-01_hemi-R_space-fsLR_desc-reg_sphere.surf.gii
4147
smriprep/sub-01/anat/sub-01_hemi-R_space-fsaverage_desc-reg_sphere.surf.gii

src/smriprep/workflows/anatomical.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
init_morph_grayords_wf,
8585
init_msm_sulc_wf,
8686
init_refinement_wf,
87-
init_resample_midthickness_wf,
87+
init_resample_surfaces_wf,
8888
init_surface_derivatives_wf,
8989
init_surface_recon_wf,
9090
)
@@ -379,11 +379,23 @@ def init_anat_preproc_wf(
379379

380380
if cifti_output:
381381
hcp_morphometrics_wf = init_hcp_morphometrics_wf(omp_nthreads=omp_nthreads)
382-
resample_midthickness_wf = init_resample_midthickness_wf(grayord_density=cifti_output)
382+
resample_surfaces_wf = init_resample_surfaces_wf(
383+
surfaces=['white', 'pial', 'midthickness'],
384+
grayord_density=cifti_output,
385+
)
383386
morph_grayords_wf = init_morph_grayords_wf(
384387
grayord_density=cifti_output, omp_nthreads=omp_nthreads
385388
)
386389

390+
ds_fsLR_surfaces_wf = init_ds_surfaces_wf(
391+
output_dir=output_dir,
392+
surfaces=['white', 'pial', 'midthickness'],
393+
entities={
394+
'space': 'fsLR',
395+
'density': '32k' if cifti_output == '91k' else '59k',
396+
},
397+
name='ds_fsLR_surfaces_wf',
398+
)
387399
ds_grayord_metrics_wf = init_ds_grayord_metrics_wf(
388400
bids_root=bids_root,
389401
output_dir=output_dir,
@@ -401,7 +413,9 @@ def init_anat_preproc_wf(
401413
(surface_derivatives_wf, hcp_morphometrics_wf, [
402414
('outputnode.curv', 'inputnode.curv'),
403415
]),
404-
(anat_fit_wf, resample_midthickness_wf, [
416+
(anat_fit_wf, resample_surfaces_wf, [
417+
('outputnode.white', 'inputnode.white'),
418+
('outputnode.pial', 'inputnode.pial'),
405419
('outputnode.midthickness', 'inputnode.midthickness'),
406420
(
407421
f"outputnode.sphere_reg_{'msm' if msm_sulc else 'fsLR'}",
@@ -421,12 +435,20 @@ def init_anat_preproc_wf(
421435
('outputnode.thickness', 'inputnode.thickness'),
422436
('outputnode.roi', 'inputnode.roi'),
423437
]),
424-
(resample_midthickness_wf, morph_grayords_wf, [
438+
(resample_surfaces_wf, morph_grayords_wf, [
425439
('outputnode.midthickness_fsLR', 'inputnode.midthickness_fsLR'),
426440
]),
441+
(anat_fit_wf, ds_fsLR_surfaces_wf, [
442+
('outputnode.t1w_valid_list', 'inputnode.source_files'),
443+
]),
427444
(anat_fit_wf, ds_grayord_metrics_wf, [
428445
('outputnode.t1w_valid_list', 'inputnode.source_files'),
429446
]),
447+
(resample_surfaces_wf, ds_fsLR_surfaces_wf, [
448+
('outputnode.white_fsLR', 'inputnode.white'),
449+
('outputnode.pial_fsLR', 'inputnode.pial'),
450+
('outputnode.midthickness_fsLR', 'inputnode.midthickness'),
451+
]),
430452
(morph_grayords_wf, ds_grayord_metrics_wf, [
431453
('outputnode.curv_fsLR', 'inputnode.curv'),
432454
('outputnode.curv_metadata', 'inputnode.curv_metadata'),

src/smriprep/workflows/outputs.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ def init_ds_surfaces_wf(
708708
*,
709709
output_dir: str,
710710
surfaces: list[str],
711+
entities: dict[str, str] | None = None,
711712
name='ds_surfaces_wf',
712713
) -> Workflow:
713714
"""
@@ -721,6 +722,8 @@ def init_ds_surfaces_wf(
721722
Directory in which to save derivatives
722723
surfaces : :class:`str`
723724
List of surfaces to generate DataSinks for
725+
entities : :class:`dict` of :class:`str`
726+
Entities to include in outputs
724727
name : :class:`str`
725728
Workflow name (default: ds_surfaces_wf)
726729
@@ -739,6 +742,9 @@ def init_ds_surfaces_wf(
739742
"""
740743
workflow = Workflow(name=name)
741744

745+
if entities is None:
746+
entities = {}
747+
742748
inputnode = pe.Node(
743749
niu.IdentityInterface(fields=['source_files'] + surfaces),
744750
name='inputnode',
@@ -766,6 +772,8 @@ def init_ds_surfaces_wf(
766772
elif surf == 'sphere_reg_msm':
767773
ds_surf.inputs.space, ds_surf.inputs.desc = 'fsLR', 'msmsulc'
768774

775+
ds_surf.inputs.trait_set(**entities)
776+
769777
# fmt:off
770778
workflow.connect([
771779
(inputnode, ds_surf, [(surf, 'in_file'), ('source_files', 'source_file')]),

src/smriprep/workflows/surfaces.py

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,39 +1316,45 @@ def init_anat_ribbon_wf(name='anat_ribbon_wf'):
13161316
return workflow
13171317

13181318

1319-
def init_resample_midthickness_wf(
1319+
def init_resample_surfaces_wf(
1320+
surfaces: list[str],
13201321
grayord_density: ty.Literal['91k', '170k'],
1321-
name: str = 'resample_midthickness_wf',
1322+
name: str = 'resample_surfaces_wf',
13221323
):
13231324
"""
1324-
Resample subject midthickness surface to specified density.
1325+
Resample subject surfaces surface to specified density.
13251326
13261327
Workflow Graph
13271328
.. workflow::
13281329
:graph2use: colored
13291330
:simple_form: yes
13301331
1331-
from smriprep.workflows.surfaces import init_resample_midthickness_wf
1332-
wf = init_resample_midthickness_wf(grayord_density="91k")
1332+
from smriprep.workflows.surfaces import init_resample_surfaces_wf
1333+
wf = init_resample_surfaces_wf(
1334+
surfaces=['white', 'pial', 'midthickness'],
1335+
grayord_density='91k',
1336+
)
13331337
13341338
Parameters
13351339
----------
1336-
grayord_density : :obj:`str`
1340+
surfaces : :class:`list` of :class:`str`
1341+
Names of surfaces (e.g., ``'white'``) to resample. Both hemispheres will be resampled.
1342+
grayord_density : :class:`str`
13371343
Either `91k` or `170k`, representing the total of vertices or *grayordinates*.
1338-
name : :obj:`str`
1339-
Unique name for the subworkflow (default: ``"resample_midthickness_wf"``)
1344+
name : :class:`str`
1345+
Unique name for the subworkflow (default: ``"resample_surfaces_wf"``)
13401346
13411347
Inputs
13421348
------
1343-
midthickness
1344-
GIFTI surface mesh corresponding to the midthickness surface
1349+
``<surface>``
1350+
Left and right GIFTIs for each surface name passed to ``surfaces``
13451351
sphere_reg_fsLR
13461352
GIFTI surface mesh corresponding to the subject's fsLR registration sphere
13471353
13481354
Outputs
13491355
-------
1350-
midthickness
1351-
GIFTI surface mesh corresponding to the midthickness surface, resampled to fsLR
1356+
``<surface>``
1357+
Left and right GIFTI surface mesh corresponding to the input surface, resampled to fsLR
13521358
"""
13531359
import templateflow.api as tf
13541360
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
@@ -1358,11 +1364,19 @@ def init_resample_midthickness_wf(
13581364
fslr_density = '32k' if grayord_density == '91k' else '59k'
13591365

13601366
inputnode = pe.Node(
1361-
niu.IdentityInterface(fields=['midthickness', 'sphere_reg_fsLR']),
1367+
niu.IdentityInterface(fields=[*surfaces, 'sphere_reg_fsLR']),
13621368
name='inputnode',
13631369
)
13641370

1365-
outputnode = pe.Node(niu.IdentityInterface(fields=['midthickness_fsLR']), name='outputnode')
1371+
outputnode = pe.Node(
1372+
niu.IdentityInterface(fields=[f'{surf}_fsLR' for surf in surfaces]), name='outputnode'
1373+
)
1374+
1375+
surface_list = pe.Node(
1376+
niu.Merge(len(surfaces), ravel_inputs=True),
1377+
name='surface_list',
1378+
run_without_submitting=True,
1379+
)
13661380

13671381
resampler = pe.MapNode(
13681382
SurfaceResample(method='BARYCENTRIC'),
@@ -1380,15 +1394,30 @@ def init_resample_midthickness_wf(
13801394
extension='.surf.gii',
13811395
)
13821396
)
1397+
# Order matters. Iterate over surfaces, then hemis to get L R L R L R
1398+
for _surf in surfaces
13831399
for hemi in ['L', 'R']
13841400
]
13851401

1402+
surface_groups = pe.Node(
1403+
niu.Split(splits=[2] * len(surfaces)),
1404+
name='surface_groups',
1405+
run_without_submitting=True,
1406+
)
1407+
13861408
workflow.connect([
1409+
(inputnode, surface_list, [
1410+
((surf, _sorted_by_basename), f'in{i}')
1411+
for i, surf in enumerate(surfaces, start=1)
1412+
]),
13871413
(inputnode, resampler, [
1388-
('midthickness', 'surface_in'),
1389-
('sphere_reg_fsLR', 'current_sphere'),
1414+
(('sphere_reg_fsLR', _repeat, len(surfaces)), 'current_sphere'),
1415+
]),
1416+
(surface_list, resampler, [('out', 'surface_in')]),
1417+
(resampler, surface_groups, [('surface_out', 'inlist')]),
1418+
(surface_groups, outputnode, [
1419+
(f'out{i}', f'{surf}_fsLR') for i, surf in enumerate(surfaces, start=1)
13901420
]),
1391-
(resampler, outputnode, [('surface_out', 'midthickness_fsLR')]),
13921421
]) # fmt:skip
13931422

13941423
return workflow
@@ -1678,3 +1707,7 @@ def _select_seg(in_files, segmentation):
16781707
if segmentation in fl:
16791708
return fl
16801709
raise FileNotFoundError(f'No segmentation containing "{segmentation}" was found.')
1710+
1711+
1712+
def _repeat(seq: list, count: int) -> list:
1713+
return seq * count

0 commit comments

Comments
 (0)