From ebe1862249a3f9eefe9da2d53b76d85a3553bccf Mon Sep 17 00:00:00 2001 From: Joshua Newton Date: Mon, 27 Apr 2026 15:13:24 -0400 Subject: [PATCH 1/5] `tutorial-datasets.csv`: Fix 'shape metric' before-starting files --- tutorial-datasets.csv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tutorial-datasets.csv b/tutorial-datasets.csv index 90962f6..83804ae 100644 --- a/tutorial-datasets.csv +++ b/tutorial-datasets.csv @@ -1,8 +1,9 @@ data_spinalcord-segmentation,single_subject/data/t2/t2.nii.gz data_vertebral-labeling,single_subject/data/t2/t2.nii.gz data_vertebral-labeling,single_subject/data/t2/t2_seg.nii.gz +data_shape-metric-computation,single_subject/data/t2/t2.nii.gz data_shape-metric-computation,single_subject/data/t2/t2_seg.nii.gz -data_shape-metric-computation,single_subject/data/t2/t2_seg_labeled.nii.gz +data_shape-metric-computation,single_subject/data/t2/t2_totalspineseg_discs.nii.gz data_compression,single_subject/data/t2_compression/t2_compressed.nii.gz data_normalizing-morphometrics-compression,single_subject/data/t2_compression/t2_compressed_seg.nii.gz data_normalizing-morphometrics-compression,single_subject/data/t2_compression/t2_compressed_seg_labeled.nii.gz From 40d792b563d03cdc2ece1bf3f19a9f8e7598ed79 Mon Sep 17 00:00:00 2001 From: Joshua Newton Date: Fri, 1 May 2026 14:37:40 -0400 Subject: [PATCH 2/5] `tutorial-datasets.csv`: Add missing `warp_template2anat` file --- tutorial-datasets.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorial-datasets.csv b/tutorial-datasets.csv index 83804ae..ef890db 100644 --- a/tutorial-datasets.csv +++ b/tutorial-datasets.csv @@ -9,6 +9,7 @@ data_normalizing-morphometrics-compression,single_subject/data/t2_compression/t2 data_normalizing-morphometrics-compression,single_subject/data/t2_compression/t2_compressed_seg_labeled.nii.gz data_normalizing-morphometrics-compression,single_subject/data/t2_compression/t2_compressed_labels-compression.nii.gz data_lesion-analysis,single_subject/data/t2_lesion/t2.nii.gz +data_lesion-analysis,single_subject/data/t2/warp_template2anat.nii.gz data_ms-lesion-segmentation,single_subject/data/t2_ms/t2.nii.gz data_template-registration,single_subject/data/t2/t2.nii.gz data_template-registration,single_subject/data/t2/t2_seg.nii.gz From 0175acb4da1e6de380f836550712ae07c90cce7d Mon Sep 17 00:00:00 2001 From: Joshua Newton Date: Mon, 4 May 2026 13:02:48 -0400 Subject: [PATCH 3/5] Add first draft of CSV postprocessing script This should save us from having to ever manually update CSVs again. --- .github/workflows/scripts/process_csvs.py | 291 ++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 .github/workflows/scripts/process_csvs.py diff --git a/.github/workflows/scripts/process_csvs.py b/.github/workflows/scripts/process_csvs.py new file mode 100644 index 0000000..4927508 --- /dev/null +++ b/.github/workflows/scripts/process_csvs.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +"""process_csvs.py — Transform SCT output CSVs into tutorial documentation CSVs. + +Usage: + python process_csvs.py [--input-dir DIR] [--output-dir DIR] + + --input-dir Root of the CI-downloaded CSV tree. + Default: ./Single Subject CSV Files/ (relative to CWD) + --output-dir Root of the tutorials documentation tree. + Default: $SCT_DIR/documentation/source/user_section/tutorials/ + +What this script does +--------------------- +For each known input CSV it: selects a subset of columns, optionally drops +VertLevel when it contains no meaningful data, strips the absolute path prefix +from the Filename column, optionally renames columns, and writes the result to +its corresponding tutorial documentation path. + +The Slice (I->S) values in the documentation CSV may differ from the input CSV +because SCT sometimes regenerates slightly different slice ranges between runs. +This script does NOT attempt to normalise those — it simply carries the input +value through unchanged. Update the documentation manually if the slice range +matters for the tutorial narrative. +""" + +import argparse +import os +import sys +from pathlib import Path + +import pandas as pd + + +FILENAME_ANCHOR = "sct_tutorial_data/" + + +def strip_filename_prefix(series: pd.Series) -> pd.Series: + """Strip everything up to and including the last 'sct_tutorial_data/' segment.""" + def _strip(value: str) -> str: + if not isinstance(value, str): + return value + idx = value.rfind(FILENAME_ANCHOR) + return value[idx + len(FILENAME_ANCHOR):] if idx != -1 else value + return series.map(_strip) + + +def vertlevel_has_data(df: pd.DataFrame) -> bool: + """Return True if VertLevel contains at least one non-null, non-empty, non-zero value.""" + if "VertLevel" not in df.columns: + return False + non_null = df["VertLevel"].dropna() + if non_null.empty: + return False + as_str = non_null.astype(str).str.strip() + return any(v not in ("", "0", "nan", "None") for v in as_str) + + +def process( + input_path: Path, + output_path: Path, + keep_cols: list[str], + rename_cols: dict[str, str] | None = None, + conditional_vertlevel: bool = False, +) -> None: + df = pd.read_csv(input_path) + + if conditional_vertlevel and "VertLevel" in keep_cols: + if not vertlevel_has_data(df): + keep_cols = [c for c in keep_cols if c != "VertLevel"] + + missing = [c for c in keep_cols if c not in df.columns] + if missing: + sys.exit( + f"ERROR: Columns missing in '{input_path}':\n" + + "\n".join(f" {c}" for c in missing) + + f"\nAvailable: {list(df.columns)}" + ) + + df = df[keep_cols].copy() + + if "Filename" in df.columns: + df["Filename"] = strip_filename_prefix(df["Filename"]) + + if rename_cols: + df = df.rename(columns=rename_cols) + + output_path.parent.mkdir(parents=True, exist_ok=True) + df.to_csv(output_path, index=False) + print(f"Written: {output_path} ({len(df)} row(s), {len(df.columns)} column(s))") + + +# --------------------------------------------------------------------------- +# Per-file processing functions (one per input CSV, called by run_all) +# --------------------------------------------------------------------------- + +def process_ap_ratio(ci_root: Path, tutorials_root: Path) -> None: + out = tutorials_root / "shape-analysis/normalizing-morphometrics-compressions" + process( + ci_root / "t2_compression/ap_ratio.csv", + out / "ap_ratio.csv", + keep_cols=["filename", "compression_level", "Slice (I->S)", + "diameter_AP_ratio", "diameter_AP_ratio_PAM50", "diameter_AP_ratio_PAM50_normalized"], + rename_cols={"Slice (I->S)": "slice(I->S)"}, + ) + + +def process_ap_ratio_norm_pam50(ci_root: Path, tutorials_root: Path) -> None: + out = tutorials_root / "shape-analysis/normalizing-morphometrics-compressions" + process( + ci_root / "t2_compression/ap_ratio_norm_PAM50.csv", + out / "ap_ratio_norm_PAM50.csv", + keep_cols=["filename", "compression_level", "Slice (I->S)", + "diameter_AP_ratio", "diameter_AP_ratio_PAM50", "diameter_AP_ratio_PAM50_normalized"], + rename_cols={"Slice (I->S)": "slice(I->S)"}, + ) + + +def process_csa_c3c4(ci_root: Path, tutorials_root: Path) -> None: + out = tutorials_root / "shape-analysis/compute-csa-and-other-shape-metrics" + process( + ci_root / "t2/csa_c3c4.csv", + out / "csa_c3c4.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", "MEAN(area)", "STD(area)"], + conditional_vertlevel=True, + ) + + +def process_csa_perlevel(ci_root: Path, tutorials_root: Path) -> None: + out = tutorials_root / "shape-analysis/compute-csa-and-other-shape-metrics" + # Primary output: cross-sectional area + process( + ci_root / "t2/csa_perlevel.csv", + out / "csa_perlevel.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", "MEAN(area)", "STD(area)"], + conditional_vertlevel=True, + ) + # Secondary outputs: angle, diameter, and other shape metrics + process( + ci_root / "t2/csa_perlevel.csv", + out / "angle-ap-rl.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", + "MEAN(angle_AP)", "STD(angle_AP)", "MEAN(angle_RL)", "STD(angle_RL)"], + conditional_vertlevel=True, + ) + process( + ci_root / "t2/csa_perlevel.csv", + out / "other-shape-metrics-1.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", + "MEAN(diameter_AP)", "STD(diameter_AP)", "MEAN(diameter_RL)", "STD(diameter_RL)"], + conditional_vertlevel=True, + ) + process( + ci_root / "t2/csa_perlevel.csv", + out / "other-shape-metrics-2.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", + "MEAN(eccentricity)", "STD(eccentricity)", "MEAN(orientation)", "STD(orientation)", + "MEAN(solidity)", "STD(solidity)", "SUM(length)"], + conditional_vertlevel=True, + ) + + +def process_csa_perslice(ci_root: Path, tutorials_root: Path) -> None: + out = tutorials_root / "shape-analysis/compute-csa-and-other-shape-metrics" + process( + ci_root / "t2/csa_perslice.csv", + out / "csa_perslice.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", "MEAN(area)", "STD(area)"], + conditional_vertlevel=True, + ) + + +def process_csa_pmj(ci_root: Path, tutorials_root: Path) -> None: + out = tutorials_root / "shape-analysis/compute-csa-and-other-shape-metrics" + process( + ci_root / "t2/csa_pmj.csv", + out / "csa_pmj.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", "DistancePMJ", "MEAN(area)", "STD(area)"], + conditional_vertlevel=True, + ) + + +def process_fa_in_wm(ci_root: Path, tutorials_root: Path) -> None: + process( + ci_root / "dmri/fa_in_wm.csv", + tutorials_root / "diffusion-weighted-mri/fa_in_wm.csv", + keep_cols=["Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], + conditional_vertlevel=True, + ) + + +def process_mtr_in_cst(ci_root: Path, tutorials_root: Path) -> None: + process( + ci_root / "mt/mtr_in_cst.csv", + tutorials_root / "atlas-based-analysis/mtr_in_cst.csv", + keep_cols=["Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], + conditional_vertlevel=True, + ) + + +def process_mtr_in_dc(ci_root: Path, tutorials_root: Path) -> None: + # atlas-based-analysis output: no Filename column + process( + ci_root / "mt/mtr_in_dc.csv", + tutorials_root / "atlas-based-analysis/mtr_in_dc.csv", + keep_cols=["Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], + conditional_vertlevel=True, + ) + # analysis-pipelines output: includes Filename column + process( + ci_root / "mt/mtr_in_dc.csv", + tutorials_root / "analysis-pipelines-with-sct/MTR_in_DC.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], + conditional_vertlevel=True, + ) + + +def process_mtr_in_wm(ci_root: Path, tutorials_root: Path) -> None: + process( + ci_root / "mt/mtr_in_wm.csv", + tutorials_root / "atlas-based-analysis/mtr_in_wm.csv", + keep_cols=["Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], + conditional_vertlevel=True, + ) + + +def process_t2s_value(ci_root: Path, tutorials_root: Path) -> None: + process( + ci_root / "t2s/t2s_value.csv", + tutorials_root / "gray-matter-segmentation/gm-wm-metric-computation/t2s_value.csv", + keep_cols=["Slice (I->S)", "VertLevel", "Label", "Size [vox]", "BIN()", "STD()"], + conditional_vertlevel=True, + ) + + +# --------------------------------------------------------------------------- +# Run-all driver +# --------------------------------------------------------------------------- + +def run_all(ci_root: Path, tutorials_root: Path) -> None: + """Process all tutorial CSVs. Each call below handles one input file.""" + process_ap_ratio(ci_root, tutorials_root) + process_ap_ratio_norm_pam50(ci_root, tutorials_root) + process_csa_c3c4(ci_root, tutorials_root) + process_csa_perlevel(ci_root, tutorials_root) + process_csa_perslice(ci_root, tutorials_root) + process_csa_pmj(ci_root, tutorials_root) + process_fa_in_wm(ci_root, tutorials_root) + process_mtr_in_cst(ci_root, tutorials_root) + process_mtr_in_dc(ci_root, tutorials_root) + process_mtr_in_wm(ci_root, tutorials_root) + process_t2s_value(ci_root, tutorials_root) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Transform SCT CI output CSVs into tutorial documentation CSVs.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--input-dir", + default=None, + metavar="DIR", + help="Root of the CI-downloaded CSV tree (default: './Single Subject CSV Files/' in CWD).", + ) + parser.add_argument( + "--output-dir", + default=None, + metavar="DIR", + help="Root of the tutorials tree (default: $SCT_DIR/documentation/source/user_section/tutorials/).", + ) + args = parser.parse_args() + + ci_root = Path(args.input_dir) if args.input_dir else Path.cwd() / "Single Subject CSV Files" + + if args.output_dir: + tutorials_root = Path(args.output_dir) + else: + sct_dir = os.environ.get("SCT_DIR") + if not sct_dir: + sys.exit("ERROR: $SCT_DIR is not set. Pass --output-dir or set $SCT_DIR.") + tutorials_root = Path(sct_dir) / "documentation/source/user_section/tutorials" + + run_all(ci_root, tutorials_root) + + +if __name__ == "__main__": + main() From 9313791501f2f7f10d5241cbe7858a755082435d Mon Sep 17 00:00:00 2001 From: Joshua Newton Date: Wed, 6 May 2026 12:48:18 -0400 Subject: [PATCH 4/5] `process_csvs.py`: Update to fix PR review bugs - mismatches between single-subject and multi-subject CSVs - missing truncation on mtr_in_wm.csv --- .github/workflows/scripts/process_csvs.py | 93 ++++++++++++++--------- 1 file changed, 58 insertions(+), 35 deletions(-) diff --git a/.github/workflows/scripts/process_csvs.py b/.github/workflows/scripts/process_csvs.py index 4927508..55a5400 100644 --- a/.github/workflows/scripts/process_csvs.py +++ b/.github/workflows/scripts/process_csvs.py @@ -5,22 +5,22 @@ python process_csvs.py [--input-dir DIR] [--output-dir DIR] --input-dir Root of the CI-downloaded CSV tree. - Default: ./Single Subject CSV Files/ (relative to CWD) + Default: ./csvs/ (relative to CWD) --output-dir Root of the tutorials documentation tree. Default: $SCT_DIR/documentation/source/user_section/tutorials/ -What this script does ---------------------- -For each known input CSV it: selects a subset of columns, optionally drops -VertLevel when it contains no meaningful data, strips the absolute path prefix -from the Filename column, optionally renames columns, and writes the result to -its corresponding tutorial documentation path. - -The Slice (I->S) values in the documentation CSV may differ from the input CSV -because SCT sometimes regenerates slightly different slice ranges between runs. -This script does NOT attempt to normalise those — it simply carries the input -value through unchanged. Update the documentation manually if the slice range -matters for the tutorial narrative. +Instructions: + +1. Run the CI for `sct_tutorial_data`. +2. Navigate to the latest runs of the CI: + - https://github.com/spinalcordtoolbox/sct_tutorial_data/actions/workflows/run_script_and_create_release.yml + - https://github.com/spinalcordtoolbox/sct_tutorial_data/actions/workflows/run_batch_script.yml +3. Fetch the zips containing the metric CSVs for both runs. +4. Create a folder and put both extracted zips into the folder. It should look like: + - ./csvs/Single Subject CSV Files/... (t2, dmri, etc.) + - ./csvs/Multi Subject CSV Files/results/... (CSA.csv, MTR_in_DC.csv) +5. Run this script, making sure to point `--input-dir` at the right directory. +6. Commit the updated CSV files (in the docs dir) to your PR. """ import argparse @@ -30,7 +30,6 @@ import pandas as pd - FILENAME_ANCHOR = "sct_tutorial_data/" @@ -61,6 +60,8 @@ def process( keep_cols: list[str], rename_cols: dict[str, str] | None = None, conditional_vertlevel: bool = False, + head: int = -1, + tail: int = -1, ) -> None: df = pd.read_csv(input_path) @@ -78,6 +79,11 @@ def process( df = df[keep_cols].copy() + if head >= 0 and tail >= 0 and len(df) > head + tail: + sentinel = pd.DataFrame([["..."] + [float("nan")] * (len(keep_cols) - 1)], + columns=keep_cols) + df = pd.concat([df.iloc[:head], sentinel, df.iloc[-tail:]], ignore_index=True) + if "Filename" in df.columns: df["Filename"] = strip_filename_prefix(df["Filename"]) @@ -205,13 +211,6 @@ def process_mtr_in_dc(ci_root: Path, tutorials_root: Path) -> None: keep_cols=["Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], conditional_vertlevel=True, ) - # analysis-pipelines output: includes Filename column - process( - ci_root / "mt/mtr_in_dc.csv", - tutorials_root / "analysis-pipelines-with-sct/MTR_in_DC.csv", - keep_cols=["Filename", "Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], - conditional_vertlevel=True, - ) def process_mtr_in_wm(ci_root: Path, tutorials_root: Path) -> None: @@ -220,6 +219,8 @@ def process_mtr_in_wm(ci_root: Path, tutorials_root: Path) -> None: tutorials_root / "atlas-based-analysis/mtr_in_wm.csv", keep_cols=["Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], conditional_vertlevel=True, + head=7, + tail=2 ) @@ -232,23 +233,45 @@ def process_t2s_value(ci_root: Path, tutorials_root: Path) -> None: ) +def process_multi_subject(ci_root: Path, tutorials_root: Path) -> None: + process( + ci_root / "results/CSA.csv", + tutorials_root / "analysis-pipelines-with-sct/CSA.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", "MEAN(area)", "STD(area)"], + conditional_vertlevel=True + ) + process( + ci_root / "results/MTR_in_DC.csv", + tutorials_root / "analysis-pipelines-with-sct/MTR_in_DC.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", "Label", "Size [vox]", "MAP()", "STD()"], + conditional_vertlevel=True + ) + + # --------------------------------------------------------------------------- # Run-all driver # --------------------------------------------------------------------------- def run_all(ci_root: Path, tutorials_root: Path) -> None: """Process all tutorial CSVs. Each call below handles one input file.""" - process_ap_ratio(ci_root, tutorials_root) - process_ap_ratio_norm_pam50(ci_root, tutorials_root) - process_csa_c3c4(ci_root, tutorials_root) - process_csa_perlevel(ci_root, tutorials_root) - process_csa_perslice(ci_root, tutorials_root) - process_csa_pmj(ci_root, tutorials_root) - process_fa_in_wm(ci_root, tutorials_root) - process_mtr_in_cst(ci_root, tutorials_root) - process_mtr_in_dc(ci_root, tutorials_root) - process_mtr_in_wm(ci_root, tutorials_root) - process_t2s_value(ci_root, tutorials_root) + + # Single-subject CSV files + ci_root_single = ci_root / "Single Subject CSV Files" + process_ap_ratio(ci_root_single, tutorials_root) + process_ap_ratio_norm_pam50(ci_root_single, tutorials_root) + process_csa_c3c4(ci_root_single, tutorials_root) + process_csa_perlevel(ci_root_single, tutorials_root) + process_csa_perslice(ci_root_single, tutorials_root) + process_csa_pmj(ci_root_single, tutorials_root) + process_fa_in_wm(ci_root_single, tutorials_root) + process_mtr_in_cst(ci_root_single, tutorials_root) + process_mtr_in_dc(ci_root_single, tutorials_root) + process_mtr_in_wm(ci_root_single, tutorials_root) + process_t2s_value(ci_root_single, tutorials_root) + + # Multi-subject CSV files + ci_root_multi = ci_root / "Multi Subject CSV Files" + process_multi_subject(ci_root_multi, tutorials_root) # --------------------------------------------------------------------------- @@ -264,7 +287,7 @@ def main() -> None: "--input-dir", default=None, metavar="DIR", - help="Root of the CI-downloaded CSV tree (default: './Single Subject CSV Files/' in CWD).", + help="Root of the CI-downloaded CSV tree (default: './csvs/' in CWD).", ) parser.add_argument( "--output-dir", @@ -274,7 +297,7 @@ def main() -> None: ) args = parser.parse_args() - ci_root = Path(args.input_dir) if args.input_dir else Path.cwd() / "Single Subject CSV Files" + ci_root = Path(args.input_dir) if args.input_dir else Path.cwd() / "csvs" if args.output_dir: tutorials_root = Path(args.output_dir) @@ -288,4 +311,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file From 4101c0f390243de427d880f03bb929ddf5f3c30c Mon Sep 17 00:00:00 2001 From: Joshua Newton Date: Wed, 6 May 2026 13:21:41 -0400 Subject: [PATCH 5/5] Generate symmetry metrics in output CSVs --- .github/workflows/scripts/process_csvs.py | 11 ++++++++++- single_subject/batch_single_subject.sh | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scripts/process_csvs.py b/.github/workflows/scripts/process_csvs.py index 55a5400..e0be780 100644 --- a/.github/workflows/scripts/process_csvs.py +++ b/.github/workflows/scripts/process_csvs.py @@ -163,6 +163,15 @@ def process_csa_perlevel(ci_root: Path, tutorials_root: Path) -> None: "MEAN(solidity)", "STD(solidity)", "SUM(length)"], conditional_vertlevel=True, ) + process( + ci_root / "t2/csa_perlevel.csv", + out / "other-shape-metrics-3.csv", + keep_cols=["Filename", "Slice (I->S)", "VertLevel", + "MEAN(area_quadrant_anterior_left)", "MEAN(area_quadrant_anterior_right)", + "MEAN(symmetry_dice_RL)", "MEAN(symmetry_dice_AP)", + "MEAN(symmetry_hausdorff_RL)", "MEAN(symmetry_hausdorff_AP)"], + conditional_vertlevel=True, + ) def process_csa_perslice(ci_root: Path, tutorials_root: Path) -> None: @@ -311,4 +320,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/single_subject/batch_single_subject.sh b/single_subject/batch_single_subject.sh index b0ed0bc..f752a2f 100755 --- a/single_subject/batch_single_subject.sh +++ b/single_subject/batch_single_subject.sh @@ -85,8 +85,8 @@ sct_label_vertebrae -i t2.nii.gz -s t2_seg.nii.gz -c t2 -discfile t2_totalspines # 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 -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 -discfile t2_totalspineseg_discs.nii.gz -perlevel 1 -o csa_perlevel.csv +# Aggregate CSA value per level (including new anat-based symmetry metrics) +sct_process_segmentation -i t2_seg.nii.gz -anat t2.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 -discfile t2_totalspineseg_discs.nii.gz -perslice 1 -o csa_perslice.csv