Skip to content

Commit 63e59c1

Browse files
Update batch_single_subject.sh for the 2025 SCT Course (#33)
* Temporarily turn off PR trigger on workflows * Add GHA workflow to diff slides with batch script * Replace `sct_label_vertebrae` with disc labels from `totalspineseg` * Replace `sct_deepseg_gm` with `sct_deepseg graymatter` * Replace `sct_deepseg_sc` with `sct_deepseg spinalcord` * Add missing lumbar `sct_qc` command * Uncomment `sct_analyze_lesion -f` command * Add space to commented-out `sct_label_utils` command * Add new `sct_compute_ascor` command * Temporarily commit course text to make testing easier * Revert "Temporarily commit course text to make testing easier" This reverts commit 53665c3. * Revert "Temporarily turn off PR trigger on workflows" This reverts commit 5ca1c2a. * Fix `-step1-only` syntax * `gmseg` -> `gm_seg` to match new `graymatter` output * `batch_single_subject.sh`: Fix `aSCOR` typo (`-1` -> `1`) * `batch_single_subject.sh`: Revert back to `_gmseg` Keeping the old syntax will be better for ensuring backwards compatibility with the course data .zips, since we package up the _gmseg and _wmseg files to be used in other tutorials. * `batch_single_subject.sh`: Add cmd to generate `t2_seg_labeled.nii.gz` CSV generation will fail due to missing labeled seg file. * `batch_single_subject.sh`: Remove sct_label_vertebrae comments Superseded by totalspineseg. * `batch_single_subject.sh`: Update totalspineseg syntax/output fname * `batch_single_subject.sh`: Update tutorial-specific command * `batch_single_subject.sh`: Add cropping step to improve `sc_epi` * `run_script_and_create_release.yml`: Upload QC artifact
1 parent 31eac55 commit 63e59c1

4 files changed

Lines changed: 140 additions & 26 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Compare SCT Commands
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
text_url:
7+
description: "URL to text file (e.g. GitHub raw gist link)"
8+
required: true
9+
type: string
10+
11+
jobs:
12+
compare:
13+
runs-on: macos-latest
14+
env:
15+
YDIFF_OPTIONS: "--unified --pager=cat --color=always --width=120 --nowrap"
16+
17+
steps:
18+
- name: Check out repo
19+
uses: actions/checkout@v4
20+
21+
- name: Install Python (for parsing script)
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.11"
25+
26+
- name: Install `ydiff` # https://github.com/ymattw/ydiff
27+
run: brew install ydiff
28+
29+
- name: Download remote text file
30+
run: |
31+
curl -L ${{ inputs.text_url }} -o remote.txt
32+
echo "✅ Downloaded remote file:"
33+
wc -l remote.txt
34+
35+
- name: Extract commands from remote file
36+
run: |
37+
python3 .github/workflows/scripts/extract_sct.py remote.txt -o remote_cmds.txt
38+
sort -u remote_cmds.txt > remote_cmds_sorted.txt
39+
echo "✅ Extracted $(wc -l < remote_cmds_sorted.txt) commands from remote file"
40+
41+
- name: Extract commands from local batch script
42+
run: |
43+
python3 .github/workflows/scripts/extract_sct.py single_subject/batch_single_subject.sh -o local_cmds.txt
44+
sort -u local_cmds.txt > local_cmds_sorted.txt
45+
echo "✅ Extracted $(wc -l < local_cmds_sorted.txt) commands from local script"
46+
47+
- name: Diff commands
48+
run: |
49+
echo "🔍 Diffing remote vs local..."
50+
diff -u local_cmds_sorted.txt remote_cmds_sorted.txt > diff.txt || true
51+
ydiff < diff.txt
52+
53+
- name: Upload results as artifacts
54+
uses: actions/upload-artifact@v4
55+
with:
56+
name: command-diff-output
57+
path: |
58+
remote_cmds_sorted.txt
59+
local_cmds_sorted.txt

.github/workflows/run_script_and_create_release.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ jobs:
6060
cd "${{ github.event.repository.name }}/single_subject"
6161
./batch_single_subject.sh
6262
63+
- name: "Upload QC report for easier output verification"
64+
uses: actions/upload-artifact@v4
65+
with:
66+
name: batch_single_subject QC (${{ runner.os }})
67+
path: "~/qc_singleSubj"
68+
6369
- name: "Upload CSV files for easier tutorial updating"
6470
uses: actions/upload-artifact@v4
6571
with:
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import argparse
2+
from pathlib import Path
3+
4+
5+
def extract_sct_commands(paths, output=None):
6+
results = []
7+
8+
for path in paths:
9+
with open(path, "r", encoding="utf-8") as f:
10+
for line in f:
11+
stripped = line.lstrip()
12+
if stripped.startswith("# sct_"):
13+
stripped = stripped[2:]
14+
# Find relavent SCT commands to compare
15+
if (stripped.startswith("sct_")
16+
# sct commands must have command + arg + value (3)
17+
# this excludes slide subtitles like "sct_slide ..."
18+
and len(stripped.split(" ")) >= 3
19+
# exclude lines with <> which are likely placeholders
20+
and not ("<" in stripped and ">" in stripped)
21+
# exclude sct_download_data (data already present)
22+
and not stripped.startswith("sct_download_data")
23+
# exclude sct_run_batch (handled in .yml workflow)
24+
and not stripped.startswith("sct_run_batch")):
25+
results.append(stripped.rstrip())
26+
27+
if output:
28+
Path(output).write_text("\n".join(results), encoding="utf-8")
29+
else:
30+
print("\n".join(results))
31+
32+
33+
if __name__ == "__main__":
34+
parser = argparse.ArgumentParser(description="Extract SCT commands "
35+
"from TXT files.")
36+
parser.add_argument("files", nargs="+", help="Input text files")
37+
parser.add_argument("-o", "--output", help="Optional output file")
38+
args = parser.parse_args()
39+
40+
extract_sct_commands(args.files, args.output)

single_subject/batch_single_subject.sh

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -63,28 +63,32 @@ sct_deepseg -h
6363
# Vertebral labeling
6464
# ======================================================================================================================
6565

66-
# Vertebral labeling
67-
sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -qc ~/qc_singleSubj
66+
# Vertebral disc labeling
67+
sct_deepseg spine -i t2.nii.gz -label-vert 1 -qc ~/qc_singleSubj
68+
69+
# Full spinal segmentation (Vertebrae, Intervertebral discs, Spinal cord and Spinal canal)
70+
# Segment using totalspineseg
71+
sct_deepseg spine -i t2.nii.gz -qc ~/qc_singleSubj
72+
# Check results using FSLeyes
73+
fsleyes t2.nii.gz -cm greyscale t2_step1_canal.nii.gz -cm YlOrRd -a 70.0 t2_step1_cord.nii.gz -cm YlOrRd -a 70.0 t2_totalspineseg_discs.nii.gz -cm subcortical -a 70.0 t2_step1_output.nii.gz -cm subcortical -a 70.0 t2_step2_output.nii.gz -cm subcortical -a 70.0 &
6874
# Check QC report: Go to your browser and do "refresh".
69-
# Note: Here, two files are output: t2_seg_labeled, which represents the labeled segmentation (i.e., the value
70-
# corresponds to the vertebral level), and t2_seg_labeled_discs, which only has a single point for each
71-
# inter-vertebral disc level. The convention is: Value 3 —> C2-C3 disc, Value 4 —> C3-C4 disc, etc.
7275

73-
# OPTIONAL: If automatic labeling did not work, you can initialize with manual identification of C2-C3 disc:
74-
#sct_label_utils -i t2.nii.gz -create-viewer 3 -o label_c2c3.nii.gz -msg "Click at the posterior tip of C2/C3 inter-vertebral disc"
75-
#sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -initlabel label_c2c3.nii.gz -qc ~/qc_singleSubj
76+
# Optionally, you can use the generated disc labels to create a labeled segmentation
77+
# Note: This approach is no longer recommended. Instead, use the disc labels directly in subsequent commands (e.g. `sct_process_segmentation`).
78+
sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -discfile t2_totalspineseg_discs.nii.gz
79+
# FIXME: Remove this command once the web tutorials are updated to no longer use labeled segmentations
7680

7781

7882

7983
# Shape-based analysis
8084
# ======================================================================================================================
8185

8286
# Compute cross-sectional area (CSA) of spinal cord and average it across levels C3 and C4
83-
sct_process_segmentation -i t2_seg.nii.gz -vert 3:4 -vertfile t2_seg_labeled.nii.gz -o csa_c3c4.csv
87+
sct_process_segmentation -i t2_seg.nii.gz -vert 3:4 -discfile t2_totalspineseg_discs.nii.gz -o csa_c3c4.csv
8488
# Aggregate CSA value per level
85-
sct_process_segmentation -i t2_seg.nii.gz -vert 3:4 -vertfile t2_seg_labeled.nii.gz -perlevel 1 -o csa_perlevel.csv
89+
sct_process_segmentation -i t2_seg.nii.gz -vert 3:4 -discfile t2_totalspineseg_discs.nii.gz -perlevel 1 -o csa_perlevel.csv
8690
# Aggregate CSA value per slices
87-
sct_process_segmentation -i t2_seg.nii.gz -z 30:35 -vertfile t2_seg_labeled.nii.gz -perslice 1 -o csa_perslice.csv
91+
sct_process_segmentation -i t2_seg.nii.gz -z 30:35 -discfile t2_totalspineseg_discs.nii.gz -perslice 1 -o csa_perslice.csv
8892

8993
# A drawback of vertebral level-based CSA is that it doesn’t consider neck flexion and extension.
9094
# To overcome this limitation, the CSA can instead be computed using the distance to a reference point.
@@ -96,7 +100,7 @@ sct_process_segmentation -i t2_seg.nii.gz -pmj t2_pmj.nii.gz -pmj-distance 64 -p
96100

97101
# The above commands will output the metrics in the subject space (with the original image's slice numbers)
98102
# However, you can get the corresponding slice number in the PAM50 space by using the flag `-normalize-PAM50 1`
99-
sct_process_segmentation -i t2_seg.nii.gz -vertfile t2_seg_labeled.nii.gz -perslice 1 -normalize-PAM50 1 -o csa_PAM50.csv
103+
sct_process_segmentation -i t2_seg.nii.gz -discfile t2_totalspineseg_discs.nii.gz -perslice 1 -normalize-PAM50 1 -o csa_PAM50.csv
100104

101105

102106

@@ -127,24 +131,24 @@ sct_compute_compression -i t2_compressed_seg.nii.gz -vertfile t2_compressed_seg_
127131
cd ../t2
128132

129133
# Create labels at C3 and T2 mid-vertebral levels. These labels are needed for template registration.
130-
sct_label_utils -i t2_seg_labeled.nii.gz -vert-body 3,9 -o t2_labels_vert.nii.gz
134+
sct_label_utils -i t2_totalspineseg_discs.nii.gz -keep 3,9 -o t2_labels_vert.nii.gz
131135
# Generate a QC report to visualize the two selected labels on the anatomical image
132136
sct_qc -i t2.nii.gz -s t2_labels_vert.nii.gz -p sct_label_utils -qc ~/qc_singleSubj
133137

134138
# OPTIONAL: You might want to completely bypass sct_label_vertebrae and do the labeling manually. In that case, we
135139
# provide a viewer to do so conveniently. In the example command below, we will create labels at the inter-vertebral
136140
# discs C2-C3 (value=3), C3-C4 (value=4) and C4-C5 (value=5).
137-
#sct_label_utils -i t2.nii.gz -create-viewer 3,4,5 -o labels_disc.nii.gz -msg "Place labels at the posterior tip of each inter-vertebral disc. E.g. Label 3: C2/C3, Label 4: C3/C4, etc."
141+
# sct_label_utils -i t2.nii.gz -create-viewer 3,4,5 -o labels_disc.nii.gz -msg "Place labels at the posterior tip of each inter-vertebral disc. E.g. Label 3: C2/C3, Label 4: C3/C4, etc."
138142

139143
# Register t2->template.
140-
sct_register_to_template -i t2.nii.gz -s t2_seg.nii.gz -l t2_labels_vert.nii.gz -c t2 -qc ~/qc_singleSubj
144+
sct_register_to_template -i t2.nii.gz -s t2_seg.nii.gz -ldisc t2_labels_vert.nii.gz -c t2 -qc ~/qc_singleSubj
141145
# Note: By default the PAM50 template is selected. You can also select your own template using flag -t.
142146

143147
# Register t2->template with modified parameters (advanced usage of `-param`)
144-
sct_register_to_template -i t2.nii.gz -s t2_seg.nii.gz -l t2_labels_vert.nii.gz -qc ~/qc_singleSubj -ofolder advanced_param -c t2 -param step=1,type=seg,algo=rigid:step=2,type=seg,metric=CC,algo=bsplinesyn,slicewise=1,iter=3:step=3,type=im,metric=CC,algo=syn,slicewise=1,iter=2
148+
sct_register_to_template -i t2.nii.gz -s t2_seg.nii.gz -ldisc t2_labels_vert.nii.gz -qc ~/qc_singleSubj -ofolder advanced_param -c t2 -param step=1,type=seg,algo=rigid:step=2,type=seg,metric=CC,algo=bsplinesyn,slicewise=1,iter=3:step=3,type=im,metric=CC,algo=syn,slicewise=1,iter=2
145149

146150
# Register t2->template with large FOV (e.g. C2-L1) using `-ldisc` option
147-
# sct_register_to_template -i t2.nii.gz -s t2_seg.nii.gz -ldisc t2_seg_labeled_discs.nii.gz -c t2
151+
# sct_register_to_template -i t2.nii.gz -s t2_seg.nii.gz -ldisc t2_totalspineseg_discs.nii.gz -c t2
148152

149153
# Register t2->template in compressed cord (example command)
150154
# In case of highly compressed cord, the algo columnwise can be used, which allows for more deformation than bsplinesyn.
@@ -226,6 +230,9 @@ sct_deepseg sc_lumbar_t2 -i t2_lumbar.nii.gz -qc ~/qc_singleSubj
226230
# sake of reproducing the results in the tutorial.
227231
sct_label_utils -i t2_lumbar.nii.gz -create 27,76,187,17:27,79,80,60 -o t2_lumbar_labels.nii.gz -qc ~/qc_singleSubj
228232

233+
# generate a QC report for the lumbar labels
234+
sct_qc -i t2_lumbar.nii.gz -s t2_lumbar_labels.nii.gz -p sct_label_utils -qc ~/qc_singleSubj
235+
229236
# Register the image to the template using segmentation and labels
230237
sct_register_to_template -i t2_lumbar.nii.gz -s t2_lumbar_seg.nii.gz -ldisc t2_lumbar_labels.nii.gz -c t2 -qc ~/qc_singleSubj -param step=1,type=seg,algo=centermassrot:step=2,type=seg,algo=bsplinesyn,metric=MeanSquares,iter=3,slicewise=0:step=3,type=im,algo=syn,metric=CC,iter=3,slicewise=0
231238

@@ -237,7 +244,7 @@ sct_register_to_template -i t2_lumbar.nii.gz -s t2_lumbar_seg.nii.gz -ldisc t2_l
237244
# Go to T2*-weighted data, which has good GM/WM contrast and high in-plane resolution
238245
cd ../t2s
239246
# Segment gray matter (check QC report afterwards)
240-
sct_deepseg_gm -i t2s.nii.gz -qc ~/qc_singleSubj
247+
sct_deepseg graymatter -i t2s.nii.gz -o t2s_gmseg.nii.gz -qc ~/qc_singleSubj
241248
# Spinal cord segmentation
242249
sct_deepseg spinalcord -i t2s.nii.gz -qc ~/qc_singleSubj
243250
# Subtract GM segmentation from cord segmentation to obtain WM segmentation
@@ -387,7 +394,7 @@ sct_smooth_spinalcord -i t1.nii.gz -s t1_seg.nii.gz
387394
# Tips: use flag "-sigma" to specify smoothing kernel size (in mm)
388395

389396
# Second-pass segmentation using the smoothed anatomical image
390-
sct_deepseg_sc -i t1_smooth.nii.gz -c t1 -qc ~/qc_singleSubj
397+
sct_deepseg spinalcord -i t1_smooth.nii.gz -qc ~/qc_singleSubj
391398

392399
# Align the spinal cord in the right-left direction using slice-wise translations.
393400
sct_flatten_sagittal -i t1.nii.gz -s t1_seg.nii.gz
@@ -414,23 +421,25 @@ sct_analyze_lesion -m t2_lesion_seg.nii.gz -s t2_sc_seg.nii.gz -qc ~/qc_singleSu
414421
# Lesion analysis using PAM50 (the -f flag is used to specify the folder containing the atlas/template)
415422
# Note: You must go through the "Register to Template" steps (labeling, registration) first
416423
# This is because `sct_warp_template` is required to generate the `label` folder used for `-f`
417-
# sct_analyze_lesion -m t2_lesion_seg.nii.gz -s t2_sc_seg.nii.gz -f label -qc ~/qc_singleSubj
424+
sct_warp_template -d t2.nii.gz -w ../t2/warp_template2anat.nii.gz
425+
sct_analyze_lesion -m t2_lesion_seg.nii.gz -s t2_sc_seg.nii.gz -f label -qc ~/qc_singleSubj
418426

419427
# Segment the spinal cord on gradient echo EPI data
420428
cd ../fmri/
421-
sct_deepseg sc_epi -i fmri_moco_mean.nii.gz -qc ~/qc_singleSubj
429+
# Crop extraneous tissue using the t2-based mask generated earlier
430+
sct_crop_image -i fmri_moco_mean.nii.gz -m mask_fmri.nii.gz -b 0
431+
# Segment the cord using the cropped image
432+
sct_deepseg sc_epi -i fmri_moco_mean_crop.nii.gz -qc ~/qc_singleSubj
422433

423434
# Canal segmentation
424435
cd ../t2
425436
sct_deepseg sc_canal_t2 -i t2.nii.gz -qc ~/qc_singleSubj
426437
# Check results using FSLeyes
427438
fsleyes t2.nii.gz -cm greyscale t2_canal_seg_seg.nii.gz -cm red -a 70.0 &
428439

429-
# Full spinal segmentation (Vertebrae, Intervertebral discs, Spinal cord and Spinal canal)
430-
# Segment using totalspineseg
431-
sct_deepseg totalspineseg -i t2.nii.gz -qc ~/qc_singleSubj
432-
# Check results using FSLeyes
433-
fsleyes t2.nii.gz -cm greyscale t2_step1_canal.nii.gz -cm YlOrRd -a 70.0 t2_step1_cord.nii.gz -cm YlOrRd -a 70.0 t2_step1_levels.nii.gz -cm subcortical -a 70.0 t2_step1_output.nii.gz -cm subcortical -a 70.0 t2_step2_output.nii.gz -cm subcortical -a 70.0 &
440+
# Compute aSCOR (Adapted Spinal Cord Occupation Ratio)
441+
# i.e. Spinal cord to canal ratio using the canal seg
442+
sct_compute_ascor -i-SC t2_seg.nii.gz -i-canal t2_canal_seg.nii.gz -perlevel 1 -o ascor.csv
434443

435444
# Segment the spinal nerve rootlets
436445
sct_deepseg rootlets -i t2.nii.gz -qc ~/qc_singleSubj

0 commit comments

Comments
 (0)