Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5ca1c2a
Temporarily turn off PR trigger on workflows
joshuacwnewton Nov 5, 2025
bd48328
Add GHA workflow to diff slides with batch script
joshuacwnewton Nov 5, 2025
33fcc22
Replace `sct_label_vertebrae` with disc labels from `totalspineseg`
joshuacwnewton Nov 19, 2025
a6d80ca
Replace `sct_deepseg_gm` with `sct_deepseg graymatter`
joshuacwnewton Nov 19, 2025
06c0161
Replace `sct_deepseg_sc` with `sct_deepseg spinalcord`
joshuacwnewton Nov 19, 2025
b31a5bd
Add missing lumbar `sct_qc` command
joshuacwnewton Nov 19, 2025
bd6c4c1
Uncomment `sct_analyze_lesion -f` command
joshuacwnewton Nov 19, 2025
0eb1aec
Add space to commented-out `sct_label_utils` command
joshuacwnewton Nov 19, 2025
4ab8bbc
Add new `sct_compute_ascor` command
joshuacwnewton Nov 19, 2025
53665c3
Temporarily commit course text to make testing easier
joshuacwnewton Nov 5, 2025
3c42572
Revert "Temporarily commit course text to make testing easier"
joshuacwnewton Nov 19, 2025
bfe9286
Revert "Temporarily turn off PR trigger on workflows"
joshuacwnewton Nov 19, 2025
c98f50b
Fix `-step1-only` syntax
joshuacwnewton Nov 19, 2025
8812617
`gmseg` -> `gm_seg` to match new `graymatter` output
joshuacwnewton Nov 19, 2025
cd149da
`batch_single_subject.sh`: Fix `aSCOR` typo (`-1` -> `1`)
joshuacwnewton Nov 19, 2025
60ebdeb
`batch_single_subject.sh`: Revert back to `_gmseg`
joshuacwnewton Nov 19, 2025
e51c8fb
`batch_single_subject.sh`: Add cmd to generate `t2_seg_labeled.nii.gz`
joshuacwnewton Nov 20, 2025
dc8264a
`batch_single_subject.sh`: Remove sct_label_vertebrae comments
joshuacwnewton Nov 20, 2025
aa88fb2
`batch_single_subject.sh`: Update totalspineseg syntax/output fname
joshuacwnewton Nov 28, 2025
8719e48
`batch_single_subject.sh`: Update tutorial-specific command
joshuacwnewton Nov 28, 2025
fd6d63d
`batch_single_subject.sh`: Add cropping step to improve `sc_epi`
joshuacwnewton Dec 2, 2025
93cddf5
`run_script_and_create_release.yml`: Upload QC artifact
joshuacwnewton Dec 1, 2025
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
59 changes: 59 additions & 0 deletions .github/workflows/compare_script_to_course.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Compare SCT Commands

on:
workflow_dispatch:
inputs:
text_url:
description: "URL to text file (e.g. GitHub raw gist link)"
required: true
type: string

jobs:
compare:
runs-on: macos-latest
env:
YDIFF_OPTIONS: "--unified --pager=cat --color=always --width=120 --nowrap"

steps:
- name: Check out repo
uses: actions/checkout@v4

- name: Install Python (for parsing script)
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install `ydiff` # https://github.com/ymattw/ydiff
run: brew install ydiff

- name: Download remote text file
run: |
curl -L ${{ inputs.text_url }} -o remote.txt
echo "✅ Downloaded remote file:"
wc -l remote.txt

- name: Extract commands from remote file
run: |
python3 .github/workflows/scripts/extract_sct.py remote.txt -o remote_cmds.txt
sort -u remote_cmds.txt > remote_cmds_sorted.txt
echo "✅ Extracted $(wc -l < remote_cmds_sorted.txt) commands from remote file"

- name: Extract commands from local batch script
run: |
python3 .github/workflows/scripts/extract_sct.py single_subject/batch_single_subject.sh -o local_cmds.txt
sort -u local_cmds.txt > local_cmds_sorted.txt
echo "✅ Extracted $(wc -l < local_cmds_sorted.txt) commands from local script"

- name: Diff commands
run: |
echo "🔍 Diffing remote vs local..."
diff -u local_cmds_sorted.txt remote_cmds_sorted.txt > diff.txt || true
ydiff < diff.txt

- name: Upload results as artifacts
uses: actions/upload-artifact@v4
with:
name: command-diff-output
path: |
remote_cmds_sorted.txt
local_cmds_sorted.txt
6 changes: 6 additions & 0 deletions .github/workflows/run_script_and_create_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ jobs:
cd "${{ github.event.repository.name }}/single_subject"
./batch_single_subject.sh

- name: "Upload QC report for easier output verification"
uses: actions/upload-artifact@v4
with:
name: batch_single_subject QC (${{ runner.os }})
path: "~/qc_singleSubj"

- name: "Upload CSV files for easier tutorial updating"
uses: actions/upload-artifact@v4
with:
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/scripts/extract_sct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import argparse
from pathlib import Path


def extract_sct_commands(paths, output=None):
results = []

for path in paths:
with open(path, "r", encoding="utf-8") as f:
for line in f:
stripped = line.lstrip()
if stripped.startswith("# sct_"):
stripped = stripped[2:]
# Find relavent SCT commands to compare
if (stripped.startswith("sct_")
# sct commands must have command + arg + value (3)
# this excludes slide subtitles like "sct_slide ..."
and len(stripped.split(" ")) >= 3
# exclude lines with <> which are likely placeholders
and not ("<" in stripped and ">" in stripped)
# exclude sct_download_data (data already present)
and not stripped.startswith("sct_download_data")
# exclude sct_run_batch (handled in .yml workflow)
and not stripped.startswith("sct_run_batch")):
results.append(stripped.rstrip())

if output:
Path(output).write_text("\n".join(results), encoding="utf-8")
else:
print("\n".join(results))


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Extract SCT commands "
"from TXT files.")
parser.add_argument("files", nargs="+", help="Input text files")
parser.add_argument("-o", "--output", help="Optional output file")
args = parser.parse_args()

extract_sct_commands(args.files, args.output)
61 changes: 35 additions & 26 deletions single_subject/batch_single_subject.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,32 @@ sct_deepseg -h
# Vertebral labeling
# ======================================================================================================================

# Vertebral labeling
sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -qc ~/qc_singleSubj
# Vertebral disc labeling
sct_deepseg spine -i t2.nii.gz -label-vert 1 -qc ~/qc_singleSubj

# Full spinal segmentation (Vertebrae, Intervertebral discs, Spinal cord and Spinal canal)
# Segment using totalspineseg
sct_deepseg spine -i t2.nii.gz -qc ~/qc_singleSubj
# Check results using FSLeyes
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 &
# Check QC report: Go to your browser and do "refresh".
# Note: Here, two files are output: t2_seg_labeled, which represents the labeled segmentation (i.e., the value
# corresponds to the vertebral level), and t2_seg_labeled_discs, which only has a single point for each
# inter-vertebral disc level. The convention is: Value 3 —> C2-C3 disc, Value 4 —> C3-C4 disc, etc.

# OPTIONAL: If automatic labeling did not work, you can initialize with manual identification of C2-C3 disc:
#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"
#sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -initlabel label_c2c3.nii.gz -qc ~/qc_singleSubj
# Optionally, you can use the generated disc labels to create a labeled segmentation
# Note: This approach is no longer recommended. Instead, use the disc labels directly in subsequent commands (e.g. `sct_process_segmentation`).
sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -discfile t2_totalspineseg_discs.nii.gz
# FIXME: Remove this command once the web tutorials are updated to no longer use labeled segmentations



# Shape-based analysis
# ======================================================================================================================

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

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

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



Expand Down Expand Up @@ -127,24 +131,24 @@ sct_compute_compression -i t2_compressed_seg.nii.gz -vertfile t2_compressed_seg_
cd ../t2

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

# OPTIONAL: You might want to completely bypass sct_label_vertebrae and do the labeling manually. In that case, we
# provide a viewer to do so conveniently. In the example command below, we will create labels at the inter-vertebral
# discs C2-C3 (value=3), C3-C4 (value=4) and C4-C5 (value=5).
#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."
# 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."

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

# Register t2->template with modified parameters (advanced usage of `-param`)
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
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

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

# Register t2->template in compressed cord (example command)
# In case of highly compressed cord, the algo columnwise can be used, which allows for more deformation than bsplinesyn.
Expand Down Expand Up @@ -226,6 +230,9 @@ sct_deepseg sc_lumbar_t2 -i t2_lumbar.nii.gz -qc ~/qc_singleSubj
# sake of reproducing the results in the tutorial.
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

# generate a QC report for the lumbar labels
sct_qc -i t2_lumbar.nii.gz -s t2_lumbar_labels.nii.gz -p sct_label_utils -qc ~/qc_singleSubj

# Register the image to the template using segmentation and labels
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

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

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

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

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

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

# Full spinal segmentation (Vertebrae, Intervertebral discs, Spinal cord and Spinal canal)
# Segment using totalspineseg
sct_deepseg totalspineseg -i t2.nii.gz -qc ~/qc_singleSubj
# Check results using FSLeyes
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 &
# Compute aSCOR (Adapted Spinal Cord Occupation Ratio)
# i.e. Spinal cord to canal ratio using the canal seg
sct_compute_ascor -i-SC t2_seg.nii.gz -i-canal t2_canal_seg.nii.gz -perlevel 1 -o ascor.csv

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