Skip to content

Commit 18a3238

Browse files
authored
Merge pull request #32 from nimh-dsst/feature/integrate-generate-renders
integrating generate renders to pipeline
2 parents b674e3c + 1d6e8cd commit 18a3238

File tree

5 files changed

+133
-302
lines changed

5 files changed

+133
-302
lines changed

README.md

Lines changed: 41 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -46,74 +46,62 @@ Once conda finishes creating the virtual environment, activate `dsstdeface`.
4646
conda activate dsstdeface
4747
```
4848

49-
## Using `dsst_defacing_wf.py`
49+
## Usage
5050

51-
To deface anatomical scans in the dataset, run the `src/dsst_defacing_wf.py` script. From within the `dsst-defacing-pipeline` cloned directory, run the following command to see the help message.
51+
To deface anatomical scans in the dataset, run the `src/run.py` script. From within the `dsst-defacing-pipeline` cloned directory, run the following command to see the help message.
5252

5353
```text
54-
% python src/dsst_defacing_wf.py -h
54+
% python src/run.py -h
5555
56-
usage: dsst_defacing_wf.py [-h] [-n N_CPUS]
57-
[-p PARTICIPANT_LABEL [PARTICIPANT_LABEL ...]]
58-
[-s SESSION_ID [SESSION_ID ...]]
59-
[--no-clean]
60-
bids_dir output_dir
56+
usage: run.py [-h] [-n N_CPUS] [-p PARTICIPANT_LABEL [PARTICIPANT_LABEL ...]]
57+
[-s SESSION_ID [SESSION_ID ...]] [--no-clean]
58+
bids_dir output_dir
6159
62-
Deface anatomical scans for a given BIDS dataset or a subject
63-
directory in BIDS format.
60+
Deface anatomical scans for a given BIDS dataset or a subject directory in
61+
BIDS format.
6462
6563
positional arguments:
66-
bids_dir The directory with the input dataset
67-
formatted according to the BIDS standard.
68-
output_dir The directory where the output files should
69-
be stored.
64+
bids_dir The directory with the input dataset formatted
65+
according to the BIDS standard.
66+
output_dir The directory where the output files should be stored.
7067
71-
options:
68+
optional arguments:
7269
-h, --help show this help message and exit
7370
-n N_CPUS, --n-cpus N_CPUS
74-
Number of parallel processes to run when
75-
there is more than one folder. Defaults to
76-
1, meaning "serial processing".
71+
Number of parallel processes to run when there is more
72+
than one folder. Defaults to 1, meaning "serial
73+
processing".
7774
-p PARTICIPANT_LABEL [PARTICIPANT_LABEL ...], --participant-label PARTICIPANT_LABEL [PARTICIPANT_LABEL ...]
78-
The label(s) of the participant(s) that
79-
should be defaced. The label corresponds to
80-
sub-<participant_label> from the BIDS spec
81-
(so it does not include "sub-"). If this
82-
parameter is not provided all subjects
83-
should be analyzed. Multiple participants
84-
can be specified with a space separated
85-
list.
86-
-s SESSION_ID [SESSION_ID ...], --session-id SESSION_ID [SESSION_ID ...]
87-
The ID(s) of the session(s) that should be
75+
The label(s) of the participant(s) that should be
8876
defaced. The label corresponds to
89-
ses-<session_id> from the BIDS spec (so it
90-
does not include "ses-"). If this parameter
91-
is not provided all subjects should be
92-
analyzed. Multiple sessions can be specified
93-
with a space separated list.
94-
--no-clean If this argument is provided, then AFNI
95-
intermediate files are preserved.
96-
```
97-
98-
The script can be run serially on a BIDS dataset or in parallel at subject/session level. The three methods of running
99-
the script have been described below with example commands:
100-
101-
For readability of example commands, the following bash variables have been defined as follows:
102-
103-
```bash
104-
INPUT_DIR="<path/to/BIDS/input/dataset>"
105-
OUTPUT_DIR="<path/to/desired/defacing/output/directory>"
77+
sub-<participant_label> from the BIDS spec (so it does
78+
not include "sub-"). If this parameter is not provided
79+
all subjects should be analyzed. Multiple participants
80+
can be specified with a space separated list.
81+
-s SESSION_ID [SESSION_ID ...], --session-id SESSION_ID [SESSION_ID ...]
82+
The ID(s) of the session(s) that should be defaced.
83+
The label corresponds to ses-<session_id> from the
84+
BIDS spec (so it does not include "ses-"). If this
85+
parameter is not provided all subjects should be
86+
analyzed. Multiple sessions can be specified with a
87+
space separated list.
88+
--no-clean If this argument is provided, then AFNI intermediate
89+
files are preserved.
10690
```
10791

108-
**NOTE:** In the example commands below, `<path/to/BIDS/input/dataset>` and `<path/to/desired/output/directory>` are
109-
placeholders for paths to input and output directories, respectively.
92+
The script can be run serially on a BIDS dataset or in parallel at subject/session level. Both these methods of running
93+
the script have been described below with example commands.
11094

11195
### Option 1: Serial defacing
11296

11397
If you have a small dataset with less than 10 subjects, then it might be easiest to run the defacing algorithm serially.
11498

11599
```bash
116-
python src/dsst_defacing_wf.py ${INPUT_DIR} ${OUTPUT_DIR}
100+
# activate your conda environment
101+
conda activate dsstdeface
102+
103+
# once your conda environment is active, execute the following
104+
python src/run.py ${INPUT_DIR} ${OUTPUT_DIR}
117105
```
118106

119107
### Option 2: Parallel defacing
@@ -122,60 +110,14 @@ If you have dataset with over 10 subjects and since each defacing job is indepen
122110
subject/session in the dataset using the `-n/--n-cpus` option. The following example command will run the pipeline occupying 10 processors at a time.
123111

124112
```bash
125-
python src/dsst_defacing_wf.py ${INPUT_DIR} ${OUTPUT_DIR} -n 10
126-
```
127-
128-
### Option 3: Parallel defacing using `swarm`
129-
130-
131-
Assuming these scripts are run on the NIH HPC system, you can create a `swarm` file:
132-
133-
```bash
134-
135-
for i in `ls -d ${INPUT_DIR}/sub-*`; do \
136-
SUBJ=$(echo $i | sed "s|${INPUT_DIR}/||g" ); \
137-
echo "python dsst-defacing-pipeline/src/dsst_defacing_wf.py -i ${INPUT_DIR} -o ${OUTPUT_DIR} -p ${SUBJ}"; \
138-
done > defacing_parallel_subject_level.swarm
139-
```
140-
141-
The above BASH "for loop" crawls through the dataset and finds all subject directories to construct `dsst_defacing_wf.py` commands
142-
with the `-p/--participant-label` option.
143-
144-
Next you can run the swarm file with the following command:
145-
146-
```bash
147-
swarm -f defacing_parallel_subject_level.swarm --merge-output --logdir ${OUTPUT_DIR}/swarm_log
148-
```
149-
150-
### Option 4: In parallel at session level
151-
152-
If the input dataset has multiple sessions per subject, then run the pipeline on every session in the dataset
153-
in parallel. Similar to Option 2, the following commands loop through the dataset to find subject and session IDs to
154-
create a `swarm` file to be run on NIH HPC systems.
155-
156-
```bash
157-
for i in `ls -d ${INPUT_DIR}/sub-*`; do
158-
SUBJ=$(echo $i | sed "s|${INPUT_DIR}/||g" );
159-
for j in `ls -d ${INPUT_DIR}/${SUBJ}/ses-*`; do
160-
SESS=$(echo $j | sed "s|${INPUT_DIR}/${SUBJ}/||g" )
161-
echo "python dsst-defacing-pipeline/src/dsst_defacing_wf.py -i ${INPUT_DIR} -o ${OUTPUT_DIR} -p ${SUBJ} -s ${SESS}";
162-
done;
163-
done > defacing_parallel_session_level.swarm
164-
```
165-
166-
To run the swarm file, once created, use the following command:
113+
# activate your conda environment
114+
conda activate dsstdeface
167115

168-
```bash
169-
swarm -f defacing_parallel_session_level.swarm --merge-output --logdir ${OUTPUT_DIR}/swarm_log
116+
# once your conda environment is active, execute the following
117+
python src/run.py ${INPUT_DIR} ${OUTPUT_DIR} -n 10
170118
```
171119

172-
## Using `generate_renders.py`
173-
174-
Generate 3D renders for every defaced image in the output directory.
175-
176-
```bash
177-
python dsst-defacing-pipeline/src/generate_renders.py -o ${OUTPUT_DIR}
178-
```
120+
Additionally, the pipeline can be run on a single subject or session using the `-p/--participant-label` and `-s/--session-id`, respectively.
179121

180122
## Visual Inspection
181123

src/deface.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import gzip
2+
import os
23
import re
34
import shutil
4-
import os
55
import subprocess
66
from os import fspath
77
from pathlib import Path
@@ -69,8 +69,25 @@ def copy_over_sidecar(scan_filepath, input_anat_dir, output_anat_dir):
6969
shutil.copy2(json_sidecar, output_anat_dir / filename)
7070

7171

72+
def generate_3d_renders(defaced_img, render_outdir):
73+
rotations = [(45, 5, 10), (-45, 5, 10)]
74+
for idx, rot in enumerate(rotations):
75+
yaw, pitch, roll = rot[0], rot[1], rot[2]
76+
outfile = render_outdir.joinpath('defaced_render_0' + str(idx) + '.png')
77+
if not outfile.exists():
78+
if 'T2w' in render_outdir.parts:
79+
fsleyes_render_cmd = f"fsleyes render --scene 3d -rot {yaw} {pitch} {roll} --outfile {outfile} {defaced_img} -dr 80 1000 -in spline -cm render1 -bf 0.3 -r 100 -ns 500;"
80+
else:
81+
fsleyes_render_cmd = f"fsleyes render --scene 3d -rot {yaw} {pitch} {roll} --outfile {outfile} {defaced_img} -dr 20 250 -in spline -cm render1 -bf 0.3 -r 100 -ns 500;"
82+
cmd = f"export TMP_DISPLAY=$DISPLAY; unset DISPLAY; {fsleyes_render_cmd} export DISPLAY=$TMP_DISPLAY"
83+
84+
print(cmd)
85+
run_command(cmd, "")
86+
print(f"Has the render been created? {outfile.exists()}")
87+
88+
7289
def vqcdeface_prep(bids_input_dir, defaced_anat_dir, bids_defaced_outdir):
73-
defacing_qc_dir = bids_defaced_outdir.parent / 'QC_prep' / 'defacing_QC'
90+
defacing_qc_dir = bids_defaced_outdir.parent / 'defacing_QC'
7491
interested_files = [f for f in defaced_anat_dir.rglob('*.nii.gz') if
7592
'work_dir' not in str(f).split('/')]
7693
print(interested_files)
@@ -119,7 +136,10 @@ def reorganize_into_bids(input_bids_dir, subj_dir, sess_dir, primary_t1, bids_de
119136
intermediate_files_dir = anat_dir / 'work_dir'
120137
intermediate_files_dir.mkdir(parents=True, exist_ok=True)
121138
for dirpath in anat_dir.glob('*'):
122-
if dirpath.name.startswith('workdir') or dirpath.name.endswith('QC'):
139+
if dirpath.name.startswith('workdir'):
140+
new_name = '_'.join(['afni', dirpath.name])
141+
shutil.move(str(dirpath), str(intermediate_files_dir / new_name))
142+
elif dirpath.name.endswith('QC'):
123143
shutil.move(str(dirpath), str(intermediate_files_dir))
124144

125145
vqcdeface_prep(input_bids_dir, anat_dir, bids_defaced_outdir)
@@ -209,6 +229,14 @@ def deface_primary_scan(input_bids_dir, subj_input_dir, sess_dir, mapping_dict,
209229
# reorganizing the directory with defaced images into BIDS tree
210230
reorganize_into_bids(input_bids_dir, subj_input_dir, sess_dir, primary_t1, output_dir, no_clean)
211231

232+
# prep for visual inspection using visualqc deface
233+
print(f"Preparing for QC by visual inspection...\n")
234+
defaced_imgs = list(output_dir.parent.rglob('defaced.nii.gz'))
235+
for img in defaced_imgs:
236+
generate_3d_renders(img, img.parent)
237+
238+
# print(f"All set to start visual inspection of defaced images!")
239+
212240
return missing_refacer_outputs
213241

214242

src/generate_mappings.py

Lines changed: 9 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -32,64 +32,6 @@ def run_command(cmdstr, logfile):
3232
subprocess.run(cmdstr, stdout=logfile, stderr=subprocess.STDOUT, encoding='utf8', shell=True)
3333

3434

35-
def primary_scans_qc_prep(mapping_dict, qc_prep):
36-
"""Prepares a directory tree with symbolic links to primary scans and an id_list of primary scans to be used in the
37-
visualqc_t1_mri command.
38-
39-
:param defaultdict mapping_dict: A dictionary mapping each subject's and session's primary and other scans.
40-
:param Path() outdir: Path to output directory provided by the user. The output directory contains this scripts output files and
41-
directories.
42-
:return str vqc_t1_mri_cmd: A visualqc T1 MRI command string.
43-
"""
44-
45-
interested_keys = ['primary_t1', 'others']
46-
primaries = []
47-
for subjid in mapping_dict.keys():
48-
49-
# check existence of sessions to query mapping dict
50-
if list(mapping_dict[subjid].keys()) != interested_keys:
51-
for sessid in mapping_dict[subjid].keys():
52-
primary = mapping_dict[subjid][sessid]['primary_t1']
53-
primaries.append(primary)
54-
else:
55-
primary = mapping_dict[subjid]['primary_t1']
56-
primaries.append(primary)
57-
# remove empty strings from primaries list
58-
primaries = [p for p in primaries if p != '']
59-
60-
vqc_t1_mri = qc_prep / 't1_mri_QC'
61-
vqc_t1_mri.mkdir(parents=True, exist_ok=True)
62-
63-
id_list = []
64-
for primary in primaries:
65-
entities = Path(primary).name.split('_')
66-
subjid = entities[0]
67-
68-
# check existence of session to construct destination path
69-
sessid = ""
70-
for e in entities:
71-
if e.startswith('ses-'):
72-
sessid = e
73-
74-
dest = vqc_t1_mri / subjid / sessid / 'anat'
75-
dest.mkdir(parents=True, exist_ok=True)
76-
77-
id_list.append(dest)
78-
primary_link = dest / 'primary.nii.gz'
79-
if not primary_link.is_symlink():
80-
try:
81-
primary_link.symlink_to(primary)
82-
except:
83-
pass
84-
85-
with open(vqc_t1_mri / 't1_mri_id_list.txt', 'w') as f:
86-
f.write('\n'.join([str(i) for i in id_list]))
87-
88-
vqc_t1_mri_cmd = f"visualqc_t1_mri -u {vqc_t1_mri} -i {vqc_t1_mri / 't1_mri_id_list.txt'} -m primary.nii.gz"
89-
90-
return vqc_t1_mri_cmd
91-
92-
9335
def sort_by_acq_time(sidecars):
9436
"""Sorting a list of scans' JSON sidecars based on their acquisition time.
9537
@@ -187,9 +129,9 @@ def update_mapping_dict(mapping_dict, anat_dir, is_sessions, sidecars, t1_unavai
187129
sessid = anat_dir.parent.name
188130
if sessid not in mapping_dict[subjid]:
189131
mapping_dict[subjid][sessid] = {
190-
'primary_t1': str(primary_t1),
191-
'others': others
192-
}
132+
'primary_t1': str(primary_t1),
133+
'others': others
134+
}
193135

194136
else:
195137
mapping_dict[subjid][sessid] = {
@@ -199,14 +141,14 @@ def update_mapping_dict(mapping_dict, anat_dir, is_sessions, sidecars, t1_unavai
199141

200142
else:
201143
mapping_dict[subjid] = {
202-
'primary_t1': str(primary_t1),
203-
'others': others
204-
}
144+
'primary_t1': str(primary_t1),
145+
'others': others
146+
}
205147

206148
return mapping_dict, t1_unavailable, t1_available
207149

208150

209-
def summary_to_stdout(vqc_t1_cmd, sess_ct, t1s_found, t1s_not_found, no_anat_dirs, output):
151+
def summary_to_stdout(sess_ct, t1s_found, t1s_not_found, output):
210152
readable_path_list = ['/'.join([path.parent.name, path.name]) for path in t1s_not_found]
211153
print(f"====================")
212154
print(f"Dataset Summary")
@@ -222,7 +164,7 @@ def summary_to_stdout(vqc_t1_cmd, sess_ct, t1s_found, t1s_not_found, no_anat_dir
222164

223165
def crawl(input_dir, output):
224166
# make dir for log files and visualqc prep
225-
dir_names = ['logs', 'QC_prep']
167+
dir_names = ['logs', 'defacing_QC']
226168
for dir_name in dir_names:
227169
output.joinpath(dir_name).mkdir(parents=True, exist_ok=True)
228170

@@ -253,10 +195,5 @@ def crawl(input_dir, output):
253195
with open(output / 'logs' / 'anat_unavailable.txt', 'w') as f3:
254196
f3.write('\n'.join([str(p) for p in no_anat_dirs]))
255197

256-
# write vqc command to file
257-
vqc_t1_mri_cmd = primary_scans_qc_prep(mapping_dict, output / 'QC_prep')
258-
with open(output / 'QC_prep' / 't1_mri_qc_cmd', 'w') as f4:
259-
f4.write(f"{vqc_t1_mri_cmd}\n")
260-
261-
summary_to_stdout(vqc_t1_mri_cmd, total_sessions, t1s_found, t1s_not_found, no_anat_dirs, output)
198+
summary_to_stdout(total_sessions, t1s_found, t1s_not_found, output)
262199
return mapping_dict

0 commit comments

Comments
 (0)