Automated L3-level trunk-muscle analysis from CT. The tool takes a full-body abdominal CT, it resamples to the working resolution, localizes the L3 level and crops to it, then runs muscle segmentation on the crop.
full-body CT (.nii.gz)
│ resample to target spacing
▼
L3 localizer (presence-gated sliding window) ──► z-crop
▼
muscle segmentation (nnU-Net, in-Python) ──► raw label map
▼
strip interior fat (intramuscular adipose tissue) ──► TOTAL muscle segmentation (final)
The muscle segmenter downloads weights from HuggingFace and runs inference on the full CT (Computed Tomography) first to get L3 bounds. The segmentation model is then run on the cropped L3-bounded image to label the muscles. The fat step removes solidly-fat interior voxels (< −30 Hounsfield units, HU, inside the eroded muscle core) from each muscle.
| Input CT | Muscle segmentation | Body-composition map |
|---|---|---|
![]() |
![]() |
![]() |
-
Muscle segmentation — psoas (red), quadratus lumborum (green), erector spinae / multifidus (blue).
-
Body-composition map — each muscle is split into four compartments: muscle, low-attenuation ("fat partial-volume") muscle, intramuscular fat, and the partial-volume boundary rim. The muscle / low-attenuation split is a per-scan, per-muscle Gaussian Mixture Model (GMM), interior fat (< −30 HU) is removed to form the total-muscle segmentation.
Gaussian Mixture Model (GMM) — a muscle's HU values form two overlapping populations, denser normal muscle and lower-HU fatty muscle. The GMM fits one bell curve to each and puts the split where they cross, so the threshold adapts to each patient's muscle attenuation instead of a fixed cutoff (a myosteatotic patient's split lands much lower).
Install PyTorch first (pytorch.org), then:
pip install myoL3Or clone and install editable for development:
git clone https://github.com/Manskelab/myoL3 myoL3
cd myoL3
pip install -e .Both checkpoints live in one Hugging Face repo
(YousifKhoury/myoL3):
l3_localizer.pt (L3 bounds localizer) and muscle_seg.pth (UNet muscle
segmenter). Download them once:
myol3-install /path/to/models # downloads both + remembers the pathsor point at local files without downloading:
export MYOL3_LOCALIZER_CKPT=/path/to/l3_localizer.pt
export MYOL3_SEGMENTER_CKPT=/path/to/muscle_seg.pthResolution order per model: --*-ckpt flag → env var → myol3-install path
(~/.myol3/config.json) → Hugging Face download.
- L3 localizer (
l3_localizer.pt) — a Residual Network (ResNet-18) encodes each axial slice, a bidirectional Gated Recurrent Unit (GRU) runs over the superior–inferior axis, a soft-argmax head regresses the top and bottom L3 slice, and a presence head gates out slices that contain no L3. Applied as a sliding window over the full volume. - Muscle segmenter (
muscle_seg.pth) — a 3D UNet (no-new-UNet / nnU-Net, full-resolution configuration) run with sliding-window inference, loaded and driven directly in Python.
# full pipeline: localize L3 -> crop -> segment -> strip fat -> total muscle seg
myol3 -i fullbody_ct.nii.gz -o total_muscle_seg.nii.gz
# also save the crop, the 4-compartment map, and per-muscle/side metrics
myol3 -i fullbody_ct.nii.gz -o total_muscle_seg.nii.gz \
--save-crop l3_crop.nii.gz --save-comp composition.nii.gz --save-metrics metrics.json
# already cropped to L3 yourself -> skip localization
myol3 -i my_l3_crop.nii.gz -o total_muscle_seg.nii.gz --cropped
# localize + crop only (no segmentation model needed)
myol3 -i fullbody_ct.nii.gz --save-crop l3_crop.nii.gz
# overrides
myol3 -i ct.nii.gz -o seg.nii.gz \
--localizer-ckpt /path/l3_localizer.pt \
--segmenter-ckpt /path/muscle_seg.pth \
--pad 2 --device cuda| Flag | Meaning |
|---|---|
-i, --input |
full-body CT (required) |
-o, --output |
total muscle seg (fat stripped); omit to only localize/crop |
--cropped |
input is already cropped to L3; skip the localizer |
--split-drop FRAC |
slice cleaning: an interior slice with muscle area < FRAC × median is a boundary; only the longest piece is averaged, then edge-trimmed (default 0.5; 0 = off) |
--save-crop |
also write the L3-cropped CT |
--save-comp |
also write the 4-compartment map (muscle*10 + compartment) |
--save-metrics |
also write per-muscle/side metrics (.json) |
--localizer-ckpt / --segmenter-ckpt |
checkpoint overrides |
--pad |
extra slices each side of the L3 crop |
--device |
cpu or cuda (auto if unset) |
Python API:
import myol3
myol3.run("fullbody_ct.nii.gz", "total_muscle_seg.nii.gz",
save_crop="l3_crop.nii.gz", save_metrics="metrics.json")Per muscle × side (L/R), in mm² (mean, median, std, n_slices):
muscle_csa— total-muscle cross-sectional areafat_pv_muscle_csa— low-attenuation ("fat partial-volume") muscleintramuscular_fat_csa— solidly-fat interior (intramuscular adipose tissue, IMAT)outer_edge_pv_fat_csa— partial-volume boundary rimslice_range— first/last crop slice averaged over
Slice cleaning. All four metrics are averaged over the same cleaned slices:
- Split at drops — any mid-crop slice whose muscle area falls below 0.5 × the median (e.g. a muscle that disappears at a disc level) is a boundary, widened over the transition slices either side (< 0.75 × median). Only the longest piece is kept.
- Edge trim — under-segmented first/last slices of that piece are dropped (> 3 × MAD, floor 10% of the median, from the piece's central median).
- Shared L/R range — left and right of each muscle use the overlap of their two cleaned ranges, so both sides are measured on the same slices.
--split-drop FRAC (or SPLIT_DROP in the config) sets the step-1 fraction;
0 turns the split off (edge trim and the shared range still apply).
myol3/config.py:
TARGET_SPACING— spacing (mm) to resample the input to; set to what the models were trained at (None= keep native).LOCALIZER_HW / LOCALIZER_WIN / LOCALIZER_STRIDE— localizer input size and sliding-window params.LOCALIZER_MAX_MM— hard cap on the L3 crop length (mm) so a spurious vote can't stretch it.WL / WW— CT window for the localizer.FAT_HU / FAT_ERODE— intramuscular-fat threshold and interior-core erosion for fat stripping.LAMA_HI— muscle / low-attenuation split HU;None= per-scan, per-muscle Gaussian Mixture Model (GMM) (adaptive).SPLIT_DROP— slice cleaning: a mid-crop slice with muscle area <SPLIT_DROP× median splits the range and only the longest piece is averaged (default0.5,0= off).SEG_TILE_STEP / SEG_USE_MIRRORING— nnU-Net sliding-window step and test-time mirroring.


