diff --git a/.github/workflows/compare_script_to_course.yml b/.github/workflows/compare_script_to_course.yml new file mode 100644 index 0000000..7493697 --- /dev/null +++ b/.github/workflows/compare_script_to_course.yml @@ -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 diff --git a/.github/workflows/run_script_and_create_release.yml b/.github/workflows/run_script_and_create_release.yml index 2851ad2..805c58e 100644 --- a/.github/workflows/run_script_and_create_release.yml +++ b/.github/workflows/run_script_and_create_release.yml @@ -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: diff --git a/.github/workflows/scripts/extract_sct.py b/.github/workflows/scripts/extract_sct.py new file mode 100644 index 0000000..7cca3c7 --- /dev/null +++ b/.github/workflows/scripts/extract_sct.py @@ -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) diff --git a/single_subject/batch_single_subject.sh b/single_subject/batch_single_subject.sh index 3dd0ab3..b0ed0bc 100755 --- a/single_subject/batch_single_subject.sh +++ b/single_subject/batch_single_subject.sh @@ -63,16 +63,20 @@ 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 @@ -80,11 +84,11 @@ sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -qc ~/qc_singleSubj # ====================================================================================================================== # 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. @@ -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 @@ -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. @@ -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 @@ -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 @@ -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 @@ -414,11 +421,15 @@ 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 @@ -426,11 +437,9 @@ 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