mscard
The longitudinal multiple sclerosis MRI report that grades itself.
Two (or more) MRI visits in, one page out: new, enlarging, shrinking and resolved lesions, slowly expanding lesion (SEL) candidates, lesion volume change and brain volume change. Next to every number, a grade: before the report is written, the same pipeline is run on the subject's own baseline scan with known changes injected (a known brain contraction, a known set of new lesions, a small repositioning, gain drift and noise), and the page states how much of that truth it recovered. A number the pipeline cannot recover on this scan is marked as such.
Nothing in this repository is evidence about multiple sclerosis. It is research software. It is not a medical device, it has not been validated against clinical outcome, and it makes no diagnostic claim.
Status: first public build, 2026-09-29. Results on the public MSLesSeg cohort (24 subjects) are in the Results section; the gallery of reports is at https://cedricconday.github.io/mscard/.
Why
Every clinic that pays for longitudinal MS MRI analysis receives counts of new and enlarging lesions and a brain volume change, and none of those reports says how accurate the pipeline was on that scanner, that protocol, that head. The open pieces exist: LST-AI segments, lesiontrack registers, tracks and finds SEL candidates, bidsgate injects known lesions and atrophy and scores their recovery. mscard is the page that joins them, and the rule that the calibration is not optional.
Install
pip install mscard
Python 3.10 or later. Registration comes with lesiontrack (greedy as a wheel,
x86-64 and arm64, CPU only). LST-AI is optional and runs in its container
(docker on PATH); without it, give your own lesion masks.
Use
A manifest, one row per scan, tab separated:
subject session time_years t1 flair mask brainmask
P1 T1 28.09 P1/T1/T1.nii.gz P1/T1/FLAIR.nii.gz P1/T1/MASK.nii.gz P1/T1/brain.nii.gz
P1 T2 28.51 P1/T2/T1.nii.gz P1/T2/FLAIR.nii.gz P1/T2/MASK.nii.gz P1/T2/brain.nii.gz
time_years is any monotone clock (age, or years since the first scan).
mask and brainmask are optional: with --segmenter lst-ai the masks come
from LST-AI, and a missing brain mask is estimated (skull-stripped input: the
nonzero support; whole head: bidsgate's morphological estimate). T1w, FLAIR and
masks of one scan must share a grid.
mscard run manifest.tsv --out derivatives/mscard # measure, calibrate, report
mscard run manifest.tsv --out derivatives/mscard --segmenter lst-ai
mscard run manifest.tsv --out derivatives/mscard --no-calibrate # measure and report only
mscard calibrate manifest.tsv --out derivatives/mscard # calibration only
mscard report derivatives/mscard # re-render pages
Output per subject: report.html (self-contained), measure/mscard.json
(every number), measure/lesiontrack/ (masks, label maps, Jacobian, tables),
calibration/synthetic/ (the synthetic follow-up and its truth),
calibration/grade.json. Over the tree: index.html, one row per subject.
What is measured
- Registration, tracking, SEL candidates: lesiontrack. Halfway space, greedy stationary-velocity deformable registration on T1w and FLAIR jointly, Jacobian determinant, lesion matching on the overlap graph; classes follow the Vanden Bulcke et al. 2025 bands (enlarging or shrinking beyond ±9 %/yr, stable within ±4 %/yr, trend between); SEL candidates by the Elliott et al. 2019 rule. A definite SEL is a cohort-relative ranking and is not claimed for one subject.
- Brain volume change: the mean Jacobian determinant of the same deformation field over the baseline brain mask, minus one. When a brain mask exists for both visits the mask-volume ratio is shown as a second, ungraded number.
How the grade is made
For each subject, its baseline scan becomes a synthetic follow-up one modelled
year later (mscard/calibrate.py, parameters in mscard/config.py):
- the whole brain contracted by a known factor (default 0.98, i.e. −2 %) with
bidsgate
inject-atrophy; every image and mask follows the same field; - a known set of new lesions (default 12, 30 to 1500 mm³) with bidsgate
inject-lesions, placed at least 6 mm from every baseline lesion so each is truly new; - a rigid repositioning (up to 1 degree, 1 voxel), per-image gain (0.9 to 1.1) and 2 % noise.
Every baseline lesion is otherwise unchanged. The same pipeline measures baseline against this synthetic scan and each report line is compared with its truth:
| Report line | Truth | Grade |
|---|---|---|
| new lesions | the injected set | sensitivity; false new |
| enlarging / shrinking | none | share of unchanged lesions called changed |
| resolved | none | share called resolved |
| SEL candidates | none | candidates per baseline lesion |
| brain volume change | the injected factor | recovery = measured / injected |
| segmentation (LST-AI mode only) | the injected lesions | sensitivity, false positives, Dice |
The traffic-light bands are mscard's own and are printed on every page so the reader can disagree with them. Green: read the number at face value. Amber: read it with the calibration table. Red: the pipeline could not recover that kind of change on this scan; do not use the number.
What the calibration does not cover: real new lesions are not ellipsoids and can sit next to old ones; real atrophy is not uniform; a synthetic year has no scanner change. It is a floor, not a validation.
Results
MSLesSeg cohort, 2026-09-29, this box (4 CPU threads, greedy engine), expert lesion masks supplied
(--segmenter given), calibration factor 0.98, 12 injected lesions per subject. Every report is
in the gallery: https://cedricconday.github.io/mscard/ (one self-contained page per subject).
| Subject | Years | Baseline lesions | New | Enlarging | Shrinking | Resolved | SEL cand. | Lesion vol %/yr | Brain vol %/yr (eroded) | Boundary | Injected found | False new | Unchanged called changed | SEL/lesion on unchanged | Atrophy recovery |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| P1 | 1.40 | 18 | 4 | 3 | 4 | 7 | 20 | -18.0 | 1.30 (0.01) | amber | 12/12 | 0 | 0/18 | 1.33 | 1.15 |
| P2 | 4.59 | 15 | 2 | 6 | 1 | 0 | 1 | 6.5 | -0.77 (-0.73) | green | 12/12 | 0 | 0/15 | 1.07 | 1.07 |
| P3 | 4.41 | 41 | 5 | 4 | 3 | 21 | 3 | -8.3 | -0.26 (-0.39) | green | 12/12 | 0 | 0/37 | 0.34 | 1.26 |
| P4 | 2.19 | 19 | 4 | 2 | 4 | 5 | 77 | 6.6 | -0.90 (-1.39) | green | 12/12 | 0 | 0/18 | 7.21 | 1.26 |
| P5 | 1.59 | 22 | 4 | 5 | 3 | 9 | 4 | -6.2 | -0.15 (-0.73) | amber | 12/12 | 0 | 0/18 | 0.73 | 1.20 |
| P6 | 2.01 | 33 | 18 | 9 | 6 | 13 | 40 | 14.8 | -2.38 (-2.33) | green | 12/12 | 0 | 0/32 | 1.85 | 1.17 |
| P7 | 0.97 | 21 | 11 | 4 | 3 | 8 | 40 | 30.5 | 0.08 (-0.21) | green | 12/12 | 0 | 0/20 | 3.67 | 1.23 |
| P8 | 1.02 | 17 | 7 | 4 | 7 | 4 | 15 | 10.9 | -3.47 (-2.53) | amber | 12/12 | 0 | 0/16 | 0.41 | 1.15 |
| P9 | 2.15 | 14 | 3 | 4 | 7 | 2 | 4 | 0.6 | -2.46 (-1.23) | amber | 12/12 | 0 | 0/12 | 1.21 | 1.23 |
| P10 | 1.00 | 34 | 11 | 9 | 7 | 12 | 20 | 13.9 | -3.33 (-1.90) | amber | 12/12 | 0 | 0/33 | 0.41 | 1.13 |
| P11 | 0.58 | 14 | 1 | 3 | 4 | 4 | 11 | -44.7 | -3.64 (-5.69) | red | 12/12 | 0 | 0/13 | 0.79 | 1.20 |
| P12 | 3.16 | 32 | 13 | 5 | 8 | 9 | 39 | 5.7 | -1.84 (-1.87) | green | 12/12 | 0 | 0/28 | 2.88 | 1.13 |
| P13 | 1.39 | 38 | 22 | 14 | 14 | 5 | 40 | 4.4 | -2.16 (-2.14) | green | 12/12 | 0 | 0/37 | 1.16 | 1.17 |
| P14 | 3.75 | 16 | 17 | 1 | 2 | 4 | 11 | -0.8 | 0.39 (-0.38) | amber | 12/12 | 0 | 0/16 | 1.31 | 1.10 |
| P19 | 6.03 | 58 | 8 | 3 | 0 | 36 | 19 | 2.5 | 0.54 (0.20) | green | 12/12 | 0 | 1/56 | 1.35 | 1.19 |
| P20 | 4.37 | 8 | 10 | 0 | 1 | 7 | 3 | 24.5 | 0.65 (1.19) | amber | 12/12 | 0 | 0/8 | 0.12 | 1.18 |
| P22 | 0.59 | 21 | 3 | 7 | 9 | 4 | 4 | -14.2 | 0.69 (-1.41) | red | 12/12 | 0 | 0/19 | 0.38 | 1.17 |
| P28 | 0.97 | 8 | 14 | 1 | 3 | 2 | 2 | 25.2 | -8.18 (-6.25) | red | 12/12 | 0 | 0/8 | 0.00 | 1.18 |
| P31 | 1.48 | 28 | 7 | 15 | 4 | 2 | 12 | 34.8 | -0.58 (0.36) | amber | 12/12 | 0 | 0/24 | 0.85 | 1.20 |
| P33 | 2.42 | 41 | 2 | 3 | 8 | 21 | 9 | -16.3 | -0.81 (-0.54) | green | 12/12 | 0 | 0/37 | 1.00 | 1.31 |
| P50 | 1.00 | 47 | 8 | 14 | 10 | 13 | 35 | 298.5 | -0.38 (0.39) | amber | –/– | – | –/– | – | – |
| P51 | 3.01 | 43 | 18 | 6 | 2 | 26 | 33 | 9.5 | -1.29 (-1.72) | green | 12/12 | 0 | 0/40 | 1.70 | 1.34 |
| P52 | 0.16 | 26 | 3 | 5 | 4 | 15 | 24 | -87.5 | 29.48 (16.12) | red | 12/12 | 0 | 0/25 | 1.00 | 1.25 |
| P53 | 1.17 | 22 | 21 | 0 | 1 | 21 | 21 | -7.6 | -0.97 (-1.41) | green | 12/12 | 0 | 0/21 | 1.14 | 1.13 |
Subjects: 24. Injected new lesions reported: 276 of 276; false new: 0. Unchanged lesions called enlarging or shrinking: 1 of 551; called resolved: 0. SEL candidates per unchanged lesion: median 1.07 (range 0.00 to 7.21). Atrophy recovery: median 1.18 (range 1.07 to 1.34). Boundary check on the real pairs: {'green': 11, 'amber': 9, 'red': 4}.
Two of 26 longitudinal subjects are not in the table: P49's released follow-up T1w is empty (lesiontrack refuses it; reported upstream as MSLesSeg-2024 issue #1) and P50's baseline gives bidsgate too little white matter to place lesions after the 6 mm exclusion around its existing ones.
What the numbers say, and what they do not:
- New lesions, supplied masks: matching only. With masks drawn by an annotator, the synthetic follow-up mask contains the injected lesions by construction, so 276 of 276 says that lesiontrack's tracking calls an injected lesion "new" rather than a fragment or a merge, and nothing about detection. That is why the badge is grey. The LST-AI mode grades detection; its numbers follow below as they land.
- Enlarging, shrinking, resolved: 1 false call in 551 lesions, 0 false resolved. Each baseline lesion is judged against its own truth volume in the synthetic mask, with lesiontrack's ±9 %/yr and ±4 %/yr bands. Registration and tracking do not invent change on a one-year synthetic interval. The real pairs still show many resolved and shrinking lesions (P1: 7 resolved of 18); the calibration cannot tell annotation drift between visits from biology, and the page says so.
- Brain volume change: recovery 1.07 to 1.34, median 1.18. The deformation field over-reads a uniform 2 % contraction by about a fifth, consistently (lesiontrack's lesion-expansion backtest gave 1.16). On the real pairs the boundary check is green for 11, amber for 9, red for 4: where it is red, the full-mask and eroded-mask numbers disagree by more than 1.5 points per year and the page calls the number rim-dominated.
- SEL candidates: red for 17 of 23. With no lesion expanding, the Elliott et al. 2019 candidate rule still marks a median 1.07 candidates per baseline lesion. That is the noise floor of the candidate stage at this registration; the definite/possible split is a cohort ranking on top of it. This is the finding a reader of any SEL count should have next to it.
Citations
- Elliott C, et al. Slowly expanding/evolving lesions as a magnetic resonance imaging marker of chronic active multiple sclerosis lesions. Mult Scler 2019.
- Vanden Bulcke C, et al. 2025 (lesion volume change bands, see lesiontrack).
- Wiltgen T, et al. LST-AI: a deep learning ensemble for accurate MS lesion segmentation. NeuroImage Clin 2024.
- Guarnera C, et al. MSLesSeg: baseline and benchmarking of a new multiple sclerosis lesion segmentation dataset. 2024/2025.
Licence
MIT.
Metadata
Release files for mscard 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mscard-0.1.0.tar.gz | 36.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mscard-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.4 kB
Release files / mscard-0.1.0.tar.gz
| Download URL | mscard-0.1.0.tar.gz |
|---|---|
| Size | 36.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e6650d2d636d1b33f79fb9d46c34813005e3ad006c0c37a60434f26d02a58438
|
|
BLAKE2b-256 checksum How to use checksums |
06e48eb4ae8f0d1e79f3922152e895142b169b0f2c7da38378d275f670d22360
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / mscard-0.1.0-py3-none-any.whl
| Download URL | mscard-0.1.0-py3-none-any.whl |
|---|---|
| Size | 33.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d730bf743817a79b0b4f928f9a280ebc4a1eb3ff12c8c9a2bc72c663162c0bb2
|
|
BLAKE2b-256 checksum How to use checksums |
949957b4e77d8d6b31ae4abbb8c04e68673ee11c955110a99e495d40dc6b3ec8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|