YongchengYAO/MedVision

The official codebase of "MedVision: Benchmarking Quantitative Medical Image Analysis"

15

stars

339

commits

Python

primary language

Sep 6, 2026

updated

medvision-vlm.github.io

README

MedVision Logo

Dataset and Benchmark for Quantitative Medical Image Analysis

πŸŽ‰ Paper accepted @ EMNLP 2026 Main Conference EMNLP 2026, Budapest πŸŽ‰

| 🌏 Project | πŸ§‘πŸ»β€πŸ’» GitHub | πŸ“š Docs | 🩻 Dataset | πŸ”Ž Data Explorer | 🐳 Docker | πŸ€— Models | πŸš€ Demo | πŸ“– arXiv | πŸ’Ό LinkedIn |

πŸ”Ž Benchmarking VLMs for medical vision tasks: detection and measurement πŸ“

πŸ’Ώ v1.4.0 | 33.2K 3D images | 12.0M 2D slices | 25.7M single-instance / 50.5M multi-instance annotations | multi-modality | multi-anatomy πŸ’Ώ

πŸ“ Annotation: segmentation mask | landmark coordinate | bounding box | tumor/lesion size | distance | angle πŸ“

🎯 Post-training: SFT, RFT (RL), CoT, LoRA | Framework: TRL, verl 🎯

@misc{yao2026medvisionbenchmarkingquantitativemedical,
      title={MedVision: Benchmarking Quantitative Medical Image Analysis}, 
      author={Yongcheng Yao and Yongshuo Zong and Raman Dutt and Yongxin Yang and Sotirios A Tsaftaris and Timothy Hospedales},
      year={2026},
      eprint={2511.18676},
      archivePrefix={arXiv},
      primaryClass={cs.CV},
      url={https://arxiv.org/abs/2511.18676}, 
}

πŸ“‘ Table of Contents


πŸ† Benchmarked Models

This codebase supports open-weight VLMs and API models. The leaderboard published full scores for 18 VLMs, evaluated with MedVision dataset v1.0.0-v1.1.1 and 1000-sample-per-subtask. Pilot study include scored for API models with the same dataset but with 100-sample-per-subtask.

The live open leaderboard (per-task score tables + a frontier API-model pilot study) lives on the project page.

Ours

MedVision-V0

General-purpose VLMs (open-weight)

Qwen Qwen2.5-VL, Qwen3-VL-Thinking Β Β·Β  InternVL InternVL3 Β Β·Β  Gemma Gemma-3, Gemma-4 Β Β·Β  Meta Llama-3.2-Vision Β Β·Β  LLaVA-OneVision Β Β·Β  GLM GLM-4.6V, GLM-4.6V-Flash

Medical VLMs (open-weight)

Gemma MedGemma Β Β·Β  LLaVA-Med, Lingshu, MedDr, HuatuoGPT-Vision, HealthGPT-L14

Proprietary / API

Claude Claude-Fable-5 Β Β·Β  OpenAI GPT-5.5-Pro Β Β·Β  Gemini Gemini-3.1-Pro Β Β·Β  Kimi Kimi-K2.6

The project-page leaderboard currently publishes full score tables for the 12 off-the-shelf VLMs + MedVision-V0, plus a Claude-Fable-5 / Gemini-3.1-Pro API pilot on tumor/lesion size. Newer entries (Qwen3-VL-Thinking, Gemma-4, GLM-4.6V/-Flash, GPT-5.5-Pro, Kimi-K2.6) have eval scripts wired up and are being rolled into the leaderboard.


πŸ”₯ News

  • [Sep 1, 2026] Update paper, leaderboard, dataset v1.4.0 blog, and RFT recipe

  • [Aug 31, 2026] Released the benchmarking/fine-tuning codebase medvision_bm v1.2.0 β€” release notes

    Details (Click to expand)
    • πŸ§‘β€βš–οΈ New LLM-as-Judge output parsing: a second, independent reading of every benchmark response, so format-following failure is separable from measurement failure.
    • 🩺 New Clinical Decision Agreement (CDA) analysis: re-scores existing outputs into clinical categories (SNA/SNB position bands, AJCC kidney tumor staging) and reports Cohen's and weighted kappa.
    • πŸ› Key evaluation bugfixes: HuatuoGPT-Vision's decoding config never reached generate() (512-token CoT truncation); explicit output-token budgets on every launcher; GLM-4.6V no longer reinstalls medvision_ds mid-run.
    • πŸ“ New metrics: COCO-grid Acc@IoU for Detection, cross-dataset Distance and Angle group rows for A/D.
    • Dataset v1.2.0–v1.4.0 integration with ceiling-based plan resolution.
  • [Aug 19, 2026] πŸš€ Release MedVision dataset v1.4.0 [release-v1.4.0]

    Details (Click to expand)
    • Regenerate Tumor-Lesion-Size annotations for all 12 tumour/lesion datasets: multi-instance annotations 75K --> 3.8M
  • [Aug 9, 2026] πŸš€ Release MedVision dataset v1.3.0 [release-v1.3.0]

    Details (Click to expand)
    • New dataset: MSWAL (484 abdominal CT cases; tumor/lesion labels: liver tumour, kidney tumour, pancreatic cancer, liver cyst, and kidney cyst).
  • [Aug 3, 2026] πŸš€ Release MedVision dataset v1.2.1 [release-v1.2.1]

    Details (Click to expand)
    • ⚠️ Corrects MAMA-MIA and PI-CAI, whose v1.2.0 annotations were recorded in the source orientation instead of RAS+ β€” the loader reoriented the images at load time without renumbering the coordinates. Their v1.2.0 annotations are withdrawn. If you have used either dataset, clear that cache once.
    • No other dataset is affected β€” the other 28 resolve to exactly the same annotation files as at v1.2.0.
    • New: scripts/gen-annotations/ rebuilds the preprocessed images and annotations of any dataset from its original source -- For the record only, you never use it to load data.
  • [Jul 28, 2026] πŸš€ Release MedVision dataset v1.2.0 [release-v1.2.0]

    Details (Click to expand)
    • Highlight: 8 new datasets (130 configs) β€” AFIDs, DEEP-PSMA, LIDC-IDRI, LNQ2023, MAMA-MIA, PDDCA, PI-CAI, VerSe.
    • No existing annotation changed. Annotation versions now resolve per dataset: the version you set is a ceiling, and each dataset loads the newest annotation it published at or before it. Pinning '1.1.1' or older keeps working for every pre-existing dataset (check Annotation Version Control).
    • ⚠️ Fixes a stale-cache defect present in all earlier versions. The cache key used the version you requested rather than the annotation actually loaded, so load_dataset could silently return previously cached rows after the annotations changed β€” which really happened, to the v1.1.0 T/L train/test split. See Fixed: cached data could be stale for who is affected and how to clear it. The data root is now part of the key too, which matters only if your HuggingFace cache is not already co-located with it. Because the cache key changed, existing Arrow caches rebuild once on next use (reads the annotation file, no re-download)
  • [Jul 21, 2026] Updated leaderboard and data explorer

  • [Jul 4, 2026] Released the benchmarking/fine-tuning codebase medvision_bm v1.1.1 β€” release notes

    Details (Click to expand)
    • πŸ“š New documentation site on Read the Docs: installation, dataset, benchmarking, and fine-tuning guides plus the full CLI and Python API reference.
  • [Jun 29, 2026] πŸš€ Released the MedVision dataset (medvision_ds) v1.1.1 β€” release notes

    Details (Click to expand)
    • Highlight: corrected T/L ellipse fit β€” fixes a transposed in-plane voxel-spacing bug (wrong axis lengths and major/minor labelling on anisotropic slices, e.g. sagittal/coronal); ~22% fewer T/L samples on anisotropic data, isotropic data (e.g., axial slices) essentially unchanged
    • Backward compatibility: The codebase medvision_ds will be automatically updated to the latest (v1.1.1). MedVision_PLANNER_VERSION='latest' now resolves to '1.1.1'; pin '1.1.0' or '1.0.0' for earlier annotations.
    • ⚠️ New env var MedVision_ACK_RELEASE: required only when you pin an older version (MedVision_PLANNER_VERSION below the latest) β€” set it to the latest version (1.1.1) to acknowledge you have read this release note and unblock loading legacy data.
    • Always set MedVision_FORCE_INSTALL_CODE='True' to receive notification of future releases. See Environment Variables.
  • [Jun 9, 2026] Released MedVision-V0, RFT code, preprint v2, project page with interactive case viewer.

  • [May 15, 2026] Released the benchmarking/fine-tuning codebase medvision_bm v1.1.0 β€” release notes

  • [May 14, 2026] Released the MedVision dataset (medvision_ds) v1.1.0 β€” release notes

    Details (Click to expand)
    • Highlight: new T/L sample filtering (with ambiguous cases removed), more T/L samples with a single small target (cluster size > 20)
    • Backward compatibility: The codebase medvision_ds will be automatically updated to the latest (v1.1.0). MedVision_PLANNER_VERSION is required (v1.1.0+) to specify the annotation data version. Setting MedVision_PLANNER_VERSION='1.0.0' will fall back to MedVision dataset v1.0.0.
    • πŸ§ͺ Test backward compatibility:
    python unit-test/medvision-ds-planner-version/test_planner_switch_medvision_ds_v1.1.0.py --data_dir <local-data-folder>
    
  • [Dec 10, 2025] Added preprint, training code, docker images, released models, new tasks/models guide

  • [Oct 8, 2025] Released MedVision dataset v1.0.0


🌟 Quick Start

πŸ“š Read the Docs: Installation Β· Quickstart walkthrough

Option 1 β€” run the full pipeline (benchmarking, SFT/RFT). Clone the repo and install from the local copy. Use this when you rely on the repo's folder structure (e.g. script/, tasks_list/, Results/), since the scripts and configs live there.

git clone https://github.com/YongchengYAO/MedVision.git MedVision
cd MedVision
pip install .
pip show medvision_bm

Option 2 β€” import the package in your own project. Install medvision_bm from PyPI. Use this when you only want to import its modules/functions (e.g. from medvision_bm.utils import parse_utils) and do not need the repo's folder structure.

Stable release (PyPI):

# Install the benchmark codebase
pip install medvision-bm
pip show medvision_bm

# Install the dataset codebase
# "Data" is the local dataset folder
mvbm install mvds -d Data

Or the nightly build (latest commit on GitHub master):

pip install "git+https://github.com/YongchengYAO/MedVision.git"
pip show medvision_bm

🐳 Use Docker

πŸ“š Read the Docs: Installation β†’ Docker

Docker images are built from these dockerfiles

  • Base docker: docker pull vincentycyao/medvision:base
  • You can choose model-specific docker and skip env setup in scripts, such as docker pull vincentycyao/medvision:eval_medvision-v0, then in eval_*.sh:
    python -m medvision_bm.benchmark.install_medvision_ds --data_dir "${data_dir}" # always keep
    python -m medvision_bm.benchmark.install_vendored_lmms_eval --lmms_eval_opt_deps medvision_v0 # always keep
    #pip install -r "${benchmark_dir}/requirements/requirements_eval_medvision-v0.txt" --no-deps # can skip if using model-specific docker
    
    python -m medvision_bm.benchmark.eval__medvision-model-rft \
      --skip_env_setup \
      ...
    
    
  1. Choose the docker image for a specific model: https://hub.docker.com/r/vincentycyao/medvision/tags

    docker pull vincentycyao/medvision:<tag>
    
  2. Map local volumes and GPUs, then use the docker image vincentycyao/medvision:<tag>

    # NOTE: replace </path/to/working/folder>, <tag>
    docker run -it --rm \
        --gpus all \
        -v </path/to/working/folder>:/root/Documents/MedVision \
        vincentycyao/medvision:<tag> \
        bash
    
    # In the container
    git clone https://github.com/YongchengYAO/MedVision.git /root/Documents/MedVision
    cd /root/Documents/MedVision
    
    # Check existing Conda env and activate 
    conda env list
    conda activate <env-name>
    
    # Install the latest medvision_bm
    pip install .
    pip show medvision_bm
    
    # Install the latest medvision_ds
    mvbm install mvds -d Data
    pip show medvision_ds
    

[!TIP] Treat the MedVision folder as the working directory for benchmarking and fine-tuning.

File structure: imaging data, benchmark results, and model checkpoints are automatically saved


πŸ’Ώ Data

πŸ“š Read the Docs: Dataset concepts Β· Loading data

[!IMPORTANT] Leaderboard results use annotation v1.0.0. All leaderboard numbers are computed on the v1.0.0 annotations. We removed ambiguous cases (multi-instance targets) in metric calculation as a workaround. For new studies we recommend the latest annotation version (currently v1.4.0).

πŸ’« Quick start: use MedVision Data Explorer

  • Dataset. For the full description of the MedVision dataset (source datasets, modalities, anatomies, annotation types, and returned fields), see the Hugging Face dataset repo.

  • Benchmark subtasks ↔ dataset subsets. Each subtask in this benchmark links to a subset of the MedVision dataset. The per-subtask sample sizes are listed for every dataset version 1.0.0–1.4.0:

    • all_tasks__ds_v1.1.1: we use this for OOD ablation since we fixed the annotation error for sagittal and coronal slices from v1.1.1
    • all_tasks__ds_v1.0.0: we use single instance annotations from v1.0.0 in all benchmarking tasks
  • Pixel size (physical spacing) distribution. Because the quantitative tasks require pixelβ†’mm arithmetic, the distribution of pixel sizes across subtasks is provided in pixel_sizes__ds_v1.0.0.

  • Image size distribution. The distribution of image sizes across subtasks is provided in image_sizes__ds_v1.0.0.

  • Multi-instance and single-instance annotations. Each benchmark sample is a (2D slice, target) pair, several instances of the same target on one slice still count as one annotation.

    • Single-instance βŠ† multi-instance
    • Multi-instance (unfiltered) β€” every target carrying β‰₯ 1 annotation is kept, however many instances (boxes / clusters) it has on the slice and whatever their size.
    • Single-instance (filtered) β€” a target is kept only when it is a single, large-enough instance. Per benchmark task, a sample is dropped when:
    Details (Click to expand) | Benchmark task | Single-instance drops the sample when… | |---|---| | **Box** β€” detection | the slice has **more than one** box for the target (`len(boxes) > 1`), **or** a box is **< 10 px** on any side | | **T/L** β€” tumor / lesion size | the target has **more than one** cluster on the slice (`n_clusters > 1`; `len(biometric_profile) > 1` on the v1.0.0 fallback) | | **A/D** β€” biometrics (angle / distance) | *never dropped* β€” every angle and distance sample is kept (the loader only splits them by `metric_type`) |

[!TIP] To load the unfiltered (multi-instance) samples, set the environment variable MedVision_DISABLE_SAMPLE_FILTERING=true (default off) β€” it bypasses the per-sample quality/size filters and returns every planner sample.

[!WARNING] Multi-instance annotations are not for leaderboard comparison. Do not use multi-instance annotations to compare models on the leaderboad. The current MedVision-V0 SFT/RFT training is not optimized for multi-instance detection and measurement tasks.

Per-dataset statistics

Computed from the local benchmark plans by script/misc/summarize_datasets.sh. The modality / image / slice / segmentation stats are the same for every dataset version; only the benchmark-annotation counts (Box / T/L / A/D) depend on it β€” and among those, only T/L changes (Box and A/D are identical across versions). Each row is version-invariant, but the set of rows is not: the table lists the 31 datasets present in v1.4.0, so its Total row is specific to that release.

Per-dataset stats β€” modality, images, slices, segmentation (version-invariant)
DatasetModality3D Images3D Masks2D SlicesSeg. annotations
ACDCMRI30030043,96294,160
AFIDsMRI7204320
AMOS22CT, MRI360360251,6371,215,776
AbdomenAtlas1.0MiniCT5,1955,1953,778,80513,770,398
AbdomenCT-1KCT1,0001,000711,1551,549,325
BCV15CT606034,472125,870
BraTS24MRI10,6323,0332,019,1183,767,594
CAMUSultrasound1,0001,000670,9641,341,433
Ceph-Biometrics-400X Ray40007,6000
CrossMoDAMRI10510514,11516,623
DEEP-PSMAPET20020050,34150,341
FLARE22CT505034,235152,954
FeTA24MRI808035,776153,599
HNTSMRG24MRI30030056,07862,424
ISLES24MRI29814997,22897,228
KiPA22CT707029,49474,690
KiTS23CT489489190,642291,550
LIDC-IDRICT1,0131,01373,97673,976
LNQ2023CT12012025,33125,331
MAMA-MIAMRI1,5061,506214,408214,408
MSDCT, MRI3,2251,741791,7061,438,472
MSWALCT484484108,246125,126
OAIZIB-CMMRI507507358,728922,989
PDDCACT484815,00337,119
PI-CAIMRI42542542,85742,857
SKM-TEAMRI310155173,690475,828
ToothFairy2CT480480397,5312,131,223
TopCoW24CT, MRI25025087,953251,901
TotalSegmentatorCT, MRI1,8441,8441,091,56316,979,575
VerSeCT325325208,402874,896
autoPET-IIICT, PET2,0761,038360,638360,638
Total (31)β€”33,22422,32711,976,08646,718,304

Benchmark annotations by dataset version. Each block below breaks the annotation counts down by dataset and by task β€” Box (detection), T/L (tumor/lesion size) and A/D (biometrics) β€” under both the single-instance (filtered) and multi-instance (unfiltered) views. Across releases, Box and A/D counts are byte-identical; T/L is the only task ever regenerated, in v1.1.0, v1.1.1 and most recently v1.4.0, which rebuilt all 12 T/L datasets. A total therefore moves for exactly two reasons: a T/L regeneration, or a release that adds datasets (v1.2.0 added 8, v1.3.0 added MSWAL). Each block totals only the datasets its own release shipped β€” 31 for v1.3.0 and v1.4.0, 30 for v1.2.0, 22 for the rest:

MedVision v1.4.0 (default)

T/L regenerated. Clusters are now selected by a physical size floor in millimetres β€” max(2.0 mm, 2 Γ— the coarser in-plane spacing) of the measured plane β€” instead of a raw pixel count; a gate that silently discarded rotated ellipses is removed; and the ellipse fit is guarded against degenerate results. Published T/L landmarks grow 75,840 β†’ 3,801,540 (50Γ—). Every other task and every previously published annotation file is unchanged. See the v1.4.0 release note and the blog post (MedVision v1.4.0: 50Γ— More Tumor/Lesion Measurements and Their Annotation Recall).

Single-instanceMulti-instance
v1.4.0 single-instance donutv1.4.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AFIDs0043200432
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,524427,33903,767,5941,645,5500
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
DEEP-PSMA2,17910,740050,341156,4390
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,02943,920062,42469,0930
ISLES249,7740097,22800
KiPA2237,64713,061074,69013,8640
KiTS23114,49162,2250291,55076,0160
LIDC-IDRI10,37943,488073,97659,1790
LNQ20231,6535,702025,33137,8980
MAMA-MIA67,76790,7250214,408369,4190
MSD277,451115,76401,438,472712,2470
MSWAL60,10678,5860125,126145,1360
OAIZIB-CM648,04800922,98900
PDDCA15,076013237,1190132
PI-CAI5,45522,764042,85728,3240
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
VerSe350,31101,500874,89601,500
autoPET-III31,79451,8750360,638488,3750
Total (31)24,749,253966,1899,98946,718,3043,801,5409,989
Total (all tasks)25,725,43150,529,833
MedVision v1.3.0

Adds MSWAL (CT, 484 volumes). No annotation logic changed β€” every v1.2.0 count is carried over unmodified.

Single-instanceMulti-instance
v1.3.0 single-instance donutv1.3.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AFIDs0043200432
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52415,88003,767,59426,1830
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
DEEP-PSMA2,17994050,3417530
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0292,272062,4243,1880
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,4917,0480291,5508,0340
LIDC-IDRI10,379417073,9765170
LNQ20231,65345025,3312380
MAMA-MIA67,7673,2400214,4085,0710
MSD277,4516,12601,438,47212,9140
MSWAL60,1068,2540125,12612,2600
OAIZIB-CM648,04800922,98900
PDDCA15,076013237,1190132
PI-CAI5,455393042,8574090
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
VerSe350,31101,500874,89601,500
autoPET-III31,7948790360,6383,1180
Total (31)24,749,25347,7259,98946,718,30475,8279,989
Total (all tasks)24,806,96746,804,120
MedVision v1.2.0
Single-instanceMulti-instance
v1.2.0 single-instance donutv1.2.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AFIDs0043200432
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52415,88003,767,59426,1830
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
DEEP-PSMA2,17994050,3417530
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0292,272062,4243,1880
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,4917,0480291,5508,0340
LIDC-IDRI10,379417073,9765170
LNQ20231,65345025,3312380
MAMA-MIA67,7673,3270214,4085,1170
MSD277,4516,12601,438,47212,9140
OAIZIB-CM648,04800922,98900
PDDCA15,076013237,1190132
PI-CAI5,455395042,8574100
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
VerSe350,31101,500874,89601,500
autoPET-III31,7948790360,6383,1180
Total (30)24,689,14739,5609,98946,593,17863,6149,989
Total (all tasks)24,738,69646,666,781
MedVision v1.1.1
Single-instanceMulti-instance
v1.1.1 single-instance donutv1.1.1 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52415,88003,767,59426,1830
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0292,272062,4243,1880
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,4917,0480291,5508,0340
MSD277,4516,12601,438,47212,9140
OAIZIB-CM648,04800922,98900
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
autoPET-III31,7948790360,6383,1180
Total (22)24,236,32735,2827,92545,274,25056,5797,925
Total (all tasks)24,279,53445,338,754
MedVision v1.1.0
Single-instanceMulti-instance
v1.1.0 single-instance donutv1.1.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52419,13903,767,59430,3570
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0293,129062,4244,4750
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,49112,4710291,55014,1480
MSD277,4519,15201,438,47216,6200
OAIZIB-CM648,04800922,98900
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
autoPET-III31,7941,2460360,6383,8690
Total (22)24,236,32748,2147,92545,274,25072,6117,925
Total (all tasks)24,292,46645,354,786
MedVision v1.0.0
Single-instanceMulti-instance
v1.0.0 single-instance donutv1.0.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52411,07103,767,59411,0930
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0291,392062,4241,4160
ISLES249,7740097,22800
KiPA2237,6473,095074,6903,0950
KiTS23114,4918,4840291,5508,5400
MSD277,4517,47201,438,4727,6740
OAIZIB-CM648,04800922,98900
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
autoPET-III31,7947350360,6387490
Total (22)24,236,32732,2497,92545,274,25032,5677,925
Total (all tasks)24,276,50145,314,742

πŸ“Š Benchmark

πŸ“š Read the Docs: Pipeline overview Β· Running evaluations Β· Parsing & summarizing Β· CLI reference

Benchmark Setting

Steps

  1. The scripts in script/benchmark-*/ should be sufficient for dependency installation, data processing, and benchmarking

[!TIP] Set these variables:

  • benchmark_dir: the working directory
  • model_hf_id: Hugging Face ID (<user>/<model>) of the tested model
  • model_name: user-defined identifier for the tested model, used as folder name in Results/MedVision-*/
  • resource-constrained configs, such as batch_size_per_gpu

[!NOTE] Crash-safe resume.

During evaluation each finished output is written immediately to Results/MedVision-*/<model_name>/response_cache/<task>_rank<N>.jsonl, so re-running an interrupted eval skips already-completed samples instead of regenerating them β€” only the in-flight sample is lost. The cache key includes a hash of the prompt, so editing a prompt/config automatically invalidates stale entries (no need to clear the folder). Set the environment variable MEDVISION_RESP_CACHE=0 to disable this layer entirely and reproduce the original (no-cache) behavior.

  1. After evaluating all models in step 1, parse model outputs and calculate metrics (e.g., MRE, MAE, nMAE, IoU, F1, Precision, Recall, Success Rate). Base command:

[!TIP] Command: python -m medvision_bm.benchmark.parse_outputs

Arguments:

  • --task_type: one of ["AD", "TL", "Detection"]
  • --task_dir: task folder
  • --model_dir: model folder
  • --limit: limit sample size in the parsed files
  • --skip_existing: (store_true) skip parsed files
  • --processes, -p: number of processes
  • --rm_old: remove existing parsed folder for each model

Example 1 β€” parse all models for the T/L task:

python -m medvision_bm.benchmark.parse_outputs \
--task_type TL \
--task_dir Results/MedVision-TL \
-p 32

Example 2 β€” parse all models for the A/D task (remove existing parsed folder):

python -m medvision_bm.benchmark.parse_outputs \
--task_type AD \
--task_dir Results/MedVision-AD \
-p 32 \
--rm_old

Example 3 β€” parse one model for the detection task and skip existing parsed files:

python -m medvision_bm.benchmark.parse_outputs \
--task_type Detection \
--model_dir Results/MedVision-detect/Qwen2.5-VL-32B-Instruct \
--skip_existing \
-p 32
  1. Summarize model performance for each task

[!TIP] If medvision_ds is missing, install with:

python -m medvision_bm.benchmark.install_medvision_ds --data_dir

[!TIP] Command:

python -m medvision_bm.benchmark.summarize_{AD,TL,detection}_task

Arguments:

  • --task_dir: task folder
  • --model_dir: model folder
  • --limit: limit sample size in the parsed files
  • --skip_model_wo_parsed_files: skip model directories that don't have a parsed folder
  • --processes, -p: number of processes
  • --removed_samples_dir: (TL task only) root directory with per-dataset removed_samples JSON files, used to filter ambiguous cases

Example 1 β€” summarize all models for the A/D task:

python -m medvision_bm.benchmark.summarize_AD_task \
--task_dir Results/MedVision-AD \
-p 32

Example 2 β€” summarize all models for the T/L task:

python -m medvision_bm.benchmark.summarize_TL_task \
--task_dir Results/MedVision-TL \
--removed_samples_dir <local-data-folder>/Datasets \
-p 32

Example 3 β€” summarize one model for the detection task:

python -m medvision_bm.benchmark.summarize_detection_task \
--model_dir Results/MedVision-detect/Qwen2.5-VL-32B-Instruct \
-p 32
  1. (Recommended) LLM-judge parsing β€” the format-robust second pass

The regex parser in step 2 only accepts answers written inside <answer>…</answer>, so a response that states a correct answer in any other form (\boxed{…}, **Answer:** …, plain prose) is scored as a miss β€” mixing "the model can't measure" with "the model didn't follow the output format". The LLM-judge pipeline re-reads every response with a judge model whose only job is to find and quote the answer, wherever it was written. Regex parsing (steps 2–3) and LLM-judge parsing together are the complete parsing scheme β€” run both: the diff between the two reports is how much of a model's apparent failure was formatting.

  • [File structure] after steps 1-4

    β”œβ”€β”€ MedVision
    β”‚   β”œβ”€β”€ completed_tasks 
    β”‚   β”‚   β”œβ”€β”€ completed_tasks_MedVision-AD.json       # <== tasks status tracker
    β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”œβ”€β”€ Results                                     # <== benchmark results
    β”‚   β”‚   β”œβ”€β”€ MedVision-AD
    β”‚   β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_AD_task.txt                 # <== [step 3] summary
    β”‚   β”‚   β”œβ”€β”€ MedVision-detect
    β”‚   β”‚   β”‚   β”œβ”€β”€ Qwen2.5-VL-32B-Instruct
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ parsed                               
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.jsonl                     # <== [step 2] parsed model outputs
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.json                      # <== [step 2] parsed summary file
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ summary_*                   # <== [step 3] mean metrics, values
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ llm-parsed_<judge>              # <== [step 4] format-robust re-parse; parsed/ is never touched
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.jsonl                     #        re-parsed records: outcome label + quoted answer span
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ summary_*                   #        mean metrics, values (same summarizer as step 3)
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ summary_metrics_judge_Task.json    # per-model failure decomposition
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ response_cache                  # <== [step 1] per-sample resume cache (auto; MEDVISION_RESP_CACHE=0 to disable)
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *_rank*.jsonl               #        one line per finished sample, written as produced
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.jsonl                         # <== [step 1] model outputs
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.json                          # <== [step 1] summary file
    β”‚   β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_detection_task.txt          # <== [step 3] summary
    β”‚   β”‚   β”‚   β”œβ”€β”€ judge-queue_Detection.jsonl         # <== [step 4] work list, one row per response
    β”‚   β”‚   β”‚   β”œβ”€β”€ judge-out_Detection_<judge>.jsonl   # <== [step 4] the judge's raw answers
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_detection_task__llm-parsed_<judge>.txt  # <== [step 4] format-robust summary (diff against summary_detection_task.txt)
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_judge_task__llm-parsed_<judge>.txt      # <== [step 4] wrong-format vs. no-answer breakdown + judge validity
    β”‚   β”‚   β”œβ”€β”€ MedVision-TL
    β”‚   β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_TL_task.txt                 # <== [step 3] summary
    
  • [Analysis & Visualization] (optional) Scripts in script/visualization:

    • Radar charts (viz_radar.sh, viz_radar_batch.sh): cross-model comparison across metrics.
    • Per-model radar grids (viz_radar_grid.sh): one row per model, six radars per row (Detection Recall/Precision/F1, A/D Angle/Distance MRE, T/L MRE), each spoke overlaid with that model's per-sample violin + box plots.
    • Leaderboard timeline (viz_benchmark_leaderboard_timeline.sh): benchmark accuracy against model release date, one panel per task (Detection IoU, plus T/L, Distance and Angle as 1/MRE).
    • Detection label Γ— box-size (viz_detection_sampleSize_per_label_x_boxSize.sh): detection metrics and sample distribution per label Γ— box-to-image ratio group.
    • A/D landmark overlays (viz_ad_landmarks.sh): per-sample GT vs. predicted landmarks and lines.
    • A/D response panels (viz_ad_responses.sh): per-sample prompt/response/GT panels.
    • T/L axis overlays (viz_tl_axes.sh): per-sample predicted vs. GT axes with mask contour.
    • T/L response panels (viz_tl_responses.sh): per-sample prompt/response/GT panels.
    • T/L ellipse fit: image vs. real space (viz_ellipse_fit_comparison.py): overlay the T/L ellipse axes fit in image (pixel) space vs. real (physical) space on one coronal/sagittal slice, illustrating how anisotropic pixel spacing makes the two fits diverge (rendered with and without the physical aspect-ratio correction).
    • Detection box overlays (viz_detection_boxes.sh): per-sample GT vs. predicted bounding boxes.
    • Detection response panels (viz_detection_responses.sh): per-sample prompt/response/GT panels.
    • Ground-truth annotation figure (viz_gt_annotations.sh): GT-only panels rendered straight from the on-disk benchmark plans (no model output, no HuggingFace build), compiled into one figure with a labelled row-block per task.
    • Plane-OOD sample grids (viz_planeOOD_samples.sh): the same volume and target shown in the in-distribution axial plane and in both OOD planes (coronal, sagittal); Detection and T/L only, since A/D has no plane-OOD split.
    • Label clouds (viz_label_cloud.sh): 2Γ—2 target Γ— modality label clouds contrasting each task's in-distribution roster against its target-OOD roster.
    • Comparison grids (viz_compile_grid_batch.sh): tile per-sample overlays across models.
  • [Analysis] (optional) Scripts in script/analyze:

    • Process accuracy (process-accuracy/analyze_process_accuracy_TL.py, process-accuracy/analyze_process_accuracy_AD.py): step-by-step CoT accuracy for T/L (4 steps: major/minor axis endpoint norm-L2 β†’ axis length MRE) and A/D (3 steps: landmark coordinate norm-L2 β†’ scalar MRE), evaluated against ground truth.
    • Equation accuracy (equation-accuracy/analyze_equation_accuracy_TL.py, equation-accuracy/analyze_equation_accuracy_AD.py): arithmetic correctness independent of ground truth β€” extracts the equation the model wrote, evaluates it in Python, and computes MRE between that result and the model's own reported answer.
    • Detection Γ— target size (detection--target-size/run_analysis.sh): detection metrics (F1, IoU, etc.) stratified by box-to-image ratio, revealing performance trends across small, medium, and large targets.
    • Clinical Decision Agreement (CDA) (clinical-decision-analysis/run_CDA_analysis.sh): asks whether a measurement error would change the clinical decision β€” each prediction and its ground truth are pushed through a published cutoff table into a clinical category, and agreement is scored with Cohen's / weighted kappa. Re-reads existing parsed/ records only: no re-inference, no GPU, seconds per model. Check the CDA pipeline.
    • Compare with Segmentation Specialist (script/ablation/biomedparse): Evaludate and fine-tune BiomedParse v2 on our dataset
  • [Troubleshooting] here


🎯 Training: SFT

πŸ“š Read the Docs: Supervised fine-tuning (SFT)

[!TIP] Set these variables in the script:

  • benchmark_dir: the working directory
  • base_model_hf: Hugging Face ID (<user>/<model>) of the base model, or the path to a local model folder.
  • run_name: an identifier for the current training
  • merged_model_hf: Hugging Face model name (<model>) of the merged model
  • resource-constrained configs, such as
    • per_device_train_batch_size
    • gradient_accumulation_steps
    • CUDA_VISIBLE_DEVICES=0,1,2,3 and --num_processes=4

🎯 Training: RFT

πŸ“š Read the Docs: Reinforcement fine-tuning (RFT)

RL fine-tuning uses the verl framework. MedVision provides parquet dataset builders that turn the MedVision tasks into verl-ready parquet datasets.

  • [Data Processing] Build the verl parquet dataset with the scripts in script/rft, which call:

    • medvision_bm.rft.verl.build_parquet_ds: normal parquet dataset builder
    • medvision_bm.rft.verl.build_parquet_ds__checkpointed: checkpointed builder to avoid OOM, recommended for large datasets (e.g. ~1M detection samples)

    Available scripts:

    • build_parquet_ds__verl__D0k-AD5.5k-TL0k__512x512.sh: A/D task only (5.5K train / 45 val)
    • build_parquet_ds__verl__D0k-AD0k-TL5.5k__512x512.sh: T/L task only (5.5K train / 50 val)
    • build_parquet_ds__verl__D110k-AD0k-TL0k__512x512.sh: Detection task only (110K train / 105 val)
    • build_parquet_ds__verl__D110k-AD5.5k-TL5.5k__512x512.sh: all 3 tasks combined (121K train / 200 val)
    • build_parquet_ds__verl__D1000k-AD0k-TL0k__512x512__checkpointed.sh: Detection task only, large scale (1M train / 500 val); uses the checkpointed builder
  • [RFT] RL fine-tuning (GRPO) runs in our verl fork, branch medvision-rl, which implements the RFT stage of the paper:

    • Rewards (verl/utils/reward_score/medvision_rewards/): format, process (per-CoT-step landmark / measurement accuracy) and answer rewards with the exp(-error) mapping; CIoU overlap reward for detection; multiplicative composition r = r_format + r_process * r_answer (default) and the additive ablation r = r_format + r_process + r_answer; options and CLI overrides in REWARDS.md.
    • Temperature-scaled task mixing (verl/utils/dataset/temperature_sampler.py): task probability proportional to count^(1/T) to rebalance the 110K / 5.5K / 5.5K multi-task mixture (T=8).
    • Epoch-level curriculum learning (verl/utils/dataset/curriculum.py, CURRICULUM_FILTERING.md): per-task easy / hard pools, EMA-gated promotion of solved samples, retention mix-in, rotating audits with hysteresis-guarded demotion, and a per-task floor.
    • Recipes (examples/grpo_trainer/): sequential A/D β†’ T/L β†’ detection RFT (train__rft-sequential__{1-AD,2-TL,3-detection}.sh; MedVision-V0) and single-stage multi-task RFT with curriculum (train__rft-multitask.sh; additive-reward twin train__rft-multitask__additive-reward.sh); each script takes DATASET_ROOT plus either BASE_MODEL_PATH (a local checkpoint) or BASE_MODEL_HF (a Hub id, downloaded locally before training).
  • [Evaluation] Evaluate the trained model with eval__MedVision-V0-7B__detect.sh (in script/benchmark-*/).


πŸ“š New Tasks/Models Guide

πŸ“š Read the Docs: Adding a new model Β· Adding a new task

New tasks guide | New models guide

πŸ–ΌοΈ Model Image Processing

For the quantitative tasks (TL/AD), the image size and pixel size stated in each prompt must match the resolution the model's vision encoder actually perceives after its internal resize. Model image processing documents the per-model strategy (fixed perceived size, dynamic processor probe, or API resize formula), with code references, validation status, and known caveats for every supported model.


πŸ€– Agent Skills

MedVision ships Agent Skills β€” plain-Markdown instructions that teach an AI coding agent how to operate this repository. They live in skills/ and use the cross-harness <name>/SKILL.md layout, so the same files work in Claude Code, Codex, OpenCode and Pi.

Installation

Every harness discovers skills at <skills-root>/<name>/SKILL.md. Codex, OpenCode and Pi all read the shared ~/.agents/skills root; Claude Code reads ~/.claude/skills. Pick your root and link the three skills:

git clone https://github.com/YongchengYAO/MedVision.git && cd MedVision

SKILLS_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills"   # Claude Code
# SKILLS_ROOT=~/.agents/skills                             # Codex Β· OpenCode Β· Pi (shared root)
# SKILLS_ROOT=~/.config/opencode/skills                    # OpenCode (native root)
# SKILLS_ROOT=~/.pi/agent/skills                           # Pi (native root)

mkdir -p "$SKILLS_ROOT"
for s in medvision medvision-paper medvision-pipeline; do
  ln -s "$PWD/skills/$s" "$SKILLS_ROOT/$s"    # use `cp -r skills/$s "$SKILLS_ROOT/$s"` to pin a copy
done

To scope the skills to this checkout only, link them into the project instead β€” .claude/skills/ for Claude Code, .agents/skills/ for Codex, OpenCode and Pi:

mkdir -p .claude/skills
for s in medvision medvision-paper medvision-pipeline; do
  ln -s "../../skills/$s" ".claude/skills/$s"
done

Restart the agent afterwards so it re-scans the skills root.

SkillInvokeAnswers
medvision-paperautomaticPaper facts: dataset scale, annotation rules, metric definitions, the MedVision-V0 recipe, the evaluated VLM roster
medvision-pipelineautomaticThe benchmark pipeline end to end, dataset download, environment setup, where the SFT launchers live
medvision/medvisionDeep reference: 10 sub-skills covering every flag, launcher, version pin and failure mode

[!NOTE] medvision sets disable-model-invocation: true, so Claude Code and Pi load it only when you type /medvision. OpenCode ignores that field and will advertise it automatically.

Understand the paper: /medvision-paper

Ask about the technical details behind the benchmark and get the paper's own definitions rather than a guess:

/medvision-paper  How are tumor/lesion sizes annotated, and which ellipse fits are discarded?
/medvision-paper  What is nMAE, how does it differ from MRE, and what is the denominator?
/medvision-paper  What data and hyperparameters produced MedVision-V0?

It is the source of truth for terminology and scale numbers β€” annotation-generation rules, the real-world-units convention (annotations are mm and degrees, never pixels), metric definitions, the 70/30 subject-level split, and the SFT/RFT recipe.

Run the benchmark: /medvision-pipeline

Describe the run you want and the skill returns the ordered steps with the right module names and flags:

/medvision-pipeline  Evaluate Qwen-2.5-VL on the T/L task, then parse and summarize the results.

The pipeline it walks you through:

# 1 β€” evaluate (or use a launcher in script/benchmark-{detect,TL,AD}/)
python -m medvision_bm.benchmark.eval__qwen2_5_vl --task_type TL ...

# 2 β€” parse model outputs into structured predictions
python -m medvision_bm.benchmark.parse_outputs --task_type TL --task_dir <dir> --model_dir <dir>

# 3 β€” summarize into metrics
python -m medvision_bm.benchmark.summarize_TL_task --task_dir <dir>

# 4 β€” optional: re-parse with the LLM judge for format-robust extraction
bash script/llm-parsing/run_llm_parsing.sh

Use it the same way for dataset download, environment setup and the SFT launchers.

Go deeper: /medvision

For maintainer-level work β€” the exact flag, pin or failure mode β€” the repo skill routes to one of ten sub-skills (environment-setup, dataset-and-tasks, benchmark-evaluation, results-parsing-and-metrics, llm-judge-parsing, sft, rft, analysis, extending-models-and-tasks, biomedparse-ablation):

/medvision  How do I add a new model across every site that needs registering?
/medvision  Why did my detection run OOM while building the plan?

See skills/README.md for the routing map, the 39 bundled helper scripts, and how to use the skills as documentation without installing anything.


πŸ“– Essential Dataset Concept

πŸ“š Read the Docs: Dataset concepts

We cover some essential concepts that help you use the MedVision dataset with ease.

Concepts: Dataset & Data Configuration

  • MedVision: the collection of public imaging data and our annotations
  • dataset: name of the public datasets, such as BraTS24, MSD, OAIZIB-CM
  • data-config: name of predefined subsets
    • naming convention: {dataset}_{annotation-type}_{task-ID}_{slice}_{split}
      • dataset: details
      • annotation-type:
        • BoxSize: detection annotations (bounding box)
        • TumorLesionSize: tumor/lesion size annotations
        • BiometricsFromLandmarks: angle/distance annotations
        • MaskSize: area / mask-size annotations
      • task-ID: Task[xx] (Note: this is a local ID in the dataset, not a global ID in MedVision.)
        • For datasets with multiple image-mask pairs, we defined tasks in medvision_ds/datasets/*/preprocess_*.py
        • source: medvision_ds
        • e.g., detection tasks for the BraTS24 dataset are defined in the benchmark_plan in medvision_ds/datasets/BraTS24/preprocess_detection.py
      • slice: [Sagittal, Coronal, Axial]
      • split: [Train, Test]

What's returned from MedVision Dataset?

We only share the annotations (https://huggingface.co/datasets/YongchengYAO/MedVision/tree/main/Datasets). The data loading script MedVision.py will handle raw image downloading and processing. The returned fields in each sample are defined as follows.

In MedVision.py, the class MedVision(GeneratorBasedBuilder) defines the feature dict and the method _generate_examples() builds the dataset.

Code block in `MedVision(GeneratorBasedBuilder)` (Click to expand)
"""
MedVision dataset.

NOTE: To update the features returned by the load_dataset() method, the followings should be updated:
        - the feature dict in this class 
        - the dict yielded by the _generate_examples() method 
"""

# The feature dict for the task:
# - Mask-Size
features_dict_MaskSize = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "mask_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "label": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "pixel_count": Value("uint32"),
    "ROI_area": Value("float16"),
}

# The feature dict for the task:
# - Box-Size
features_dict_BoxSize = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "mask_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "label": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "bounding_boxes": Sequence(
        {
            "min_coords": Sequence(Value("uint16"), length=2),
            "max_coords": Sequence(Value("uint16"), length=2),
            "center_coords": Sequence(Value("uint16"), length=2),
            "dimensions": Sequence(Value("uint16"), length=2),
            "sizes": Sequence(Value("float16"), length=2),
        },
    ),
}

features_dict_BiometricsFromLandmarks = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "landmark_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "biometric_profile": {
        "metric_type": Value("string"),
        "metric_map_name": Value("string"),
        "metric_key": Value("string"),
        "metric_value": Value("float16"),
        "metric_unit": Value("string"),
        "slice_dim": Value("uint8"),
    },
}

features_dict_TumorLesionSize = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "landmark_file": Value("string"),
    "mask_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "label": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "biometric_profile": Sequence(
        {
            "metric_type": Value("string"),
            "metric_map_name": Value("string"),
            "metric_key_major_axis": Value("string"),
            "metric_value_major_axis": Value("float16"),
            "metric_key_minor_axis": Value("string"),
            "metric_value_minor_axis": Value("float16"),
            "metric_unit": Value("string"),
        },
    ),
}
Code block in `_generate_examples` (Click to expand)
# Env var to disable per-sample quality filtering. When true, the size/cluster
# exclusion filters below are bypassed so every sample in the planner is returned.
# The distance/angle metric_type split (task partitioning) is always preserved.
disable_sample_filtering = (
    os.environ.get("MedVision_DISABLE_SAMPLE_FILTERING", "False").lower()
    == "true"
)
if disable_sample_filtering:
    logger.info(
        "MedVision_DISABLE_SAMPLE_FILTERING=true β€” quality/size sample filters bypassed"
    )

# Task type: Mask-Size
if taskType == "Mask-Size":
    flatten_slice_profiles = (
        MedVision_BenchmarkPlannerSegmentation.flatten_slice_profiles_2d
    )
    if imageSliceType.lower() == "sagittal":
        slice_dim = 0
    elif imageSliceType.lower() == "coronal":
        slice_dim = 1
    elif imageSliceType.lower() == "axial":
        slice_dim = 2
    slice_profile_flattened = flatten_slice_profiles(biometricData, slice_dim)
    for idx, case in enumerate(slice_profile_flattened):
        # Skip cases with a mask size smaller than 200 pixels
        if not disable_sample_filtering and case["pixel_count"] < 200:
            continue
        else:
            yield idx, {
                "dataset_name": dataset_name,
                "taskID": taskID,
                "taskType": taskType,
                "image_file": os.path.join(dataset_dir, case["image_file"]),
                "mask_file": os.path.join(dataset_dir, case["mask_file"]),
                "slice_dim": case["slice_dim"],
                "slice_idx": case["slice_idx"],
                "label": case["label"],
                "image_size_2d": case["image_size_2d"],
                "pixel_size": case["pixel_size"],
                "image_size_3d": case["image_size_3d"],
                "voxel_size": case["voxel_size"],
                "pixel_count": case["pixel_count"],
                "ROI_area": case["ROI_area"],
            }

# Task type: Box-Size
if taskType == "Box-Size":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerDetection.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            # Skip cases with multiple bounding boxes in the same slice
            if not disable_sample_filtering and len(case["bounding_boxes"]) > 1:
                continue
            # Skip cases with a bounding box size smaller than 10 pixels in any dimension
            elif not disable_sample_filtering and (
                case["bounding_boxes"][0]["dimensions"][0] < 10
                or case["bounding_boxes"][0]["dimensions"][1] < 10
            ):
                continue
            else:
                yield idx, {
                    "dataset_name": dataset_name,
                    "taskID": taskID,
                    "taskType": taskType,
                    "image_file": os.path.join(dataset_dir, case["image_file"]),
                    "mask_file": os.path.join(dataset_dir, case["mask_file"]),
                    "slice_dim": case["slice_dim"],
                    "slice_idx": case["slice_idx"],
                    "label": case["label"],
                    "image_size_2d": case["image_size_2d"],
                    "pixel_size": case["pixel_size"],
                    "image_size_3d": case["image_size_3d"],
                    "voxel_size": case["voxel_size"],
                    "bounding_boxes": case["bounding_boxes"],
                }

# Task type: Biometrics-From-Landmarks
if taskType == "Biometrics-From-Landmarks":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            yield idx, {
                "dataset_name": dataset_name,
                "taskID": taskID,
                "taskType": taskType,
                "image_file": os.path.join(dataset_dir, case["image_file"]),
                "landmark_file": os.path.join(
                    dataset_dir, case["landmark_file"]
                ),
                "slice_dim": case["slice_dim"],
                "slice_idx": case["slice_idx"],
                "image_size_2d": case["image_size_2d"],
                "pixel_size": case["pixel_size"],
                "image_size_3d": case["image_size_3d"],
                "voxel_size": case["voxel_size"],
                "biometric_profile": case["biometric_profile"],
            }

# Task type: Biometrics-From-Landmarks-Distance
if taskType == "Biometrics-From-Landmarks-Distance":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            if case["biometric_profile"]["metric_type"] == "distance":
                yield idx, {
                    "dataset_name": dataset_name,
                    "taskID": taskID,
                    "taskType": taskType,
                    "image_file": os.path.join(dataset_dir, case["image_file"]),
                    "landmark_file": os.path.join(
                        dataset_dir, case["landmark_file"]
                    ),
                    "slice_dim": case["slice_dim"],
                    "slice_idx": case["slice_idx"],
                    "image_size_2d": case["image_size_2d"],
                    "pixel_size": case["pixel_size"],
                    "image_size_3d": case["image_size_3d"],
                    "voxel_size": case["voxel_size"],
                    "biometric_profile": case["biometric_profile"],
                }

# Task type: Biometrics-From-Landmarks-Angle
if taskType == "Biometrics-From-Landmarks-Angle":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            if case["biometric_profile"]["metric_type"] == "angle":
                yield idx, {
                    "dataset_name": dataset_name,
                    "taskID": taskID,
                    "taskType": taskType,
                    "image_file": os.path.join(dataset_dir, case["image_file"]),
                    "landmark_file": os.path.join(
                        dataset_dir, case["landmark_file"]
                    ),
                    "slice_dim": case["slice_dim"],
                    "slice_idx": case["slice_idx"],
                    "image_size_2d": case["image_size_2d"],
                    "pixel_size": case["pixel_size"],
                    "image_size_3d": case["image_size_3d"],
                    "voxel_size": case["voxel_size"],
                    "biometric_profile": case["biometric_profile"],
                }

# Task type: Tumor-Lesion-Size
if taskType == "Tumor-Lesion-Size":
    if imageType.lower() == "2d":
        # Get the target label for the task
        target_label = benchmark_plan["tasks"][int(taskID) - 1]["target_label"]

        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry_fromSeg.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            if not disable_sample_filtering:
                n_total_clusters = case["n_total_clusters"]
                if n_total_clusters is not None:
                    # New JSON (v1.1.0+): filter on raw cluster count
                    if n_total_clusters > 1:
                        continue
                else:
                    # Old JSON (v1.0.0): fall back to above-threshold cluster count
                    if len(case["biometric_profile"]) > 1:
                        continue
            yield idx, {
                "dataset_name": dataset_name,
                "taskID": taskID,
                "taskType": taskType,
                "image_file": os.path.join(dataset_dir, case["image_file"]),
                "mask_file": os.path.join(dataset_dir, case["mask_file"]),
                "landmark_file": os.path.join(
                    dataset_dir, case["landmark_file"]
                ),
                "slice_dim": case["slice_dim"],
                "slice_idx": case["slice_idx"],
                "label": target_label,
                "image_size_2d": case["image_size_2d"],
                "pixel_size": case["pixel_size"],
                "image_size_3d": case["image_size_3d"],
                "voxel_size": case["voxel_size"],
                "biometric_profile": case["biometric_profile"],
            }


Dataset Building Workflow

Workflow

MedVision Dataset Building Workflow

There are a few ways to control the dataset loading and building behavior:

  • Rebuild Dataset (Arrow files): Use the download_mode argument in load_dataset() (docs).
    • [1] Set download_mode="force_redownload" to ignore the cached Arrow files and trigger the data loading script MedVision.py to rebuild the dataset.
  • Redownload Raw Data:
    • [2] MedVision_FORCE_DOWNLOAD_DATA: Set this environment variable to True to force re-downloading raw images and annotations.
    • [3] .downloaded_datasets.json: This tracker file records downloaded status. Removing a dataset's entry here will trigger a re-download of the raw data for that dataset.

[!TIP] How to properly update/redownload raw data?

If you need to update raw data (images, masks, landmarks) using [2] or [3], you MUST ALSO use [1] (download_mode="force_redownload").

Why? Because if Hugging Face finds a valid cached dataset (Arrow files), it will load it directly and skip running the script entirely. Without running the script, the environment variable [2] or tracker file [3] will never be checked.

Summary:

  • Update Arrow/Fields only: Use [1].
  • Update Raw Data: Use [1] AND ([2] or [3]).

πŸ”₯ We will maintain a change log for essential updates.


Examples

Running this for the first time will download the raw data and build the dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
    )
Running the same script again will use the cached dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
    )
Adding `download_mode="force_redownload"` will skip raw data downloading and rebuild the dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
        download_mode="force_redownload",
    )
Adding `download_mode="force_redownload"` and `os.environ["MedVision_FORCE_DOWNLOAD_DATA"] = "True"` will redownload raw data and rebuild the dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Force redownload
os.environ["MedVision_FORCE_DOWNLOAD_DATA"] = "True"

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
        download_mode="force_redownload",
    )

Download Mode in MedVision Dataset

(Advanced) Understand how the customized dataset loading script `MedVision.py` changes the behavior of `download_mode` in `load_dataset()`
  • download_mode can be one of these: "reuse_dataset_if_exists" (default), "reuse_cache_if_exists", "force_redownload"

  • Default behavior of download_mode in load_dataset():

    DownloadsDataset
    reuse_dataset_if_exists (default)ReuseReuse
    reuse_cache_if_existsReuseFresh
    force_redownloadFreshFresh
  • download_mode in MedVision dataset:

    DownloadsDataset
    reuse_dataset_if_exists (default)ReuseReuse
    reuse_cache_if_existsReuseFresh
    force_redownload (MedVision_FORCE_DOWNLOAD_DATA=False)ReuseFresh
    force_redownload (MedVision_FORCE_DOWNLOAD_DATA=True)FreshFresh

πŸ”₯ Summary: Understanding the download mode of MedVision dataset


πŸ’Ώ Batch Download (Optional)

πŸ“š Read the Docs: Loading data β†’ batch download Β· CLI reference

Since data downloading and processing take time, you can download datasets from the tasks list or configs list in advance.

[!WARNING] You need to set an API token for these datasets (see detailed instructions): FeTA24, SKM-TEA, and ToothFairy2

[!NOTE] Per-slice QC figures are opt-in (MedVision_DOWNLOAD_QC_FIGURES, default False).

export MedVision_DOWNLOAD_QC_FIGURES="True"   # default: "False"

The QC figures are review material β€” nothing in the loader reads them, and no task needs them β€” but they are ~99% of the annotation payload (298 GB of PNG against 3 GB of annotation). Until v1.4.0 they shipped inside Datasets/<dataset>.zip, which pushed BraTS24.zip to 72.6 GB and MSD.zip to 51.4 GB β€” past Hugging Face's 50 GB per-file limit, a hard publish failure. Since v1.4.0 they ship in their own archives: Datasets/<dataset>_fig.zip, or Datasets/<dataset>_fig.partNN.zip where a single archive would again clear 50 GB.

Set the variable to True and the figures are restored to exactly the paths they occupied before v1.4.0 β€” the archives carry the same arcnames, so nothing is relocated. Details worth knowing:

  • Checked on every load. Setting the flag on a machine whose annotations are already present still fetches the figures β€” and only the figures: the image, landmark and planner downloads stay step 3's decision, so nothing else is re-pulled.
  • Tracked per annotation version. .downloaded_datasets.json records "qc_figures_<dataset>": "1.4.0" β€” the biometry version the figures belong to. A release that regenerates a dataset's biometry re-fetches its figures; a release that leaves it alone does not.
  • Never re-downloads what is already on disk. With no usable recorded version β€” any pre-v1.4.0 install already holds every figure β€” the directory itself is read and its version written back. Datasets that publish no figures (roughly half) record the attempt too, so they do not re-query the Hub on every load.
  • Retry with MedVision_FORCE_DOWNLOAD_DATA=True, which also forces figures already on disk to be fetched again. Shards are independent zips (not zip -s volumes), so they extract in any order and a missing one costs only its own figures.

[!TIP] Command:

python -m medvision_bm.benchmark.download_datasets

Arguments:

  • --data_dir: (required) data folder
  • --tasks_json: task json file
  • --configs_csv: config csv file
  • --force_download_data: (store_true) force redownload raw imaging data ⚠️ for debugging only; it will repeatedly download data for tasks/configs of the same dataset

Download from a task-list JSON (replace <task-list-json>, <data-folder>):

python -m medvision_bm.benchmark.download_datasets \
--tasks_json <task-list-json> \
--data_dir <data-folder>

Or from a configs CSV (replace <config-list-csv>, <data-folder>):

python -m medvision_bm.benchmark.download_datasets \
--configs_csv <config-list-csv> \
--data_dir <data-folder>

πŸ“œ License

MedVision is released under the Creative Commons Attribution 4.0 International (CC-BY 4.0) license. Users are permitted to utilize, adapt, and build upon this dataset for both academic and commercial purposes, provided that appropriate credit is given. MedVision is a meta-dataset built upon various publicly available source datasets. While the annotations provided by MedVision are covered by the CC-BY 4.0 license, any downstream application must continue to comply with the specific usage terms and licensing requirements stipulated by the curators of the original raw imaging data. It is the responsibility of the user to ensure that their application of this data aligns with the license agreements of all constituent source datasets.


🩡 Acknowledgement

This work was supported by

  • The United Kingdom Research and Innovation (grant EP/S02431X/1), UKRI Centre for Doctoral Training in Biomedical AI at the University of Edinburgh, School of Informatics
  • The Edinburgh International Data Facility (EIDF) and the Data-Driven Innovation Programme at the University of Edinburgh.

MedVision is based on some open-source projects:


Contributors

YongchengYAO

339 commits

YongchengYAO/MedVision

The official codebase of "MedVision: Benchmarking Quantitative Medical Image Analysis"

15

stars

339

commits

Python

primary language

Sep 6, 2026

updated

medvision-vlm.github.io

README

MedVision Logo

Dataset and Benchmark for Quantitative Medical Image Analysis

πŸŽ‰ Paper accepted @ EMNLP 2026 Main Conference EMNLP 2026, Budapest πŸŽ‰

| 🌏 Project | πŸ§‘πŸ»β€πŸ’» GitHub | πŸ“š Docs | 🩻 Dataset | πŸ”Ž Data Explorer | 🐳 Docker | πŸ€— Models | πŸš€ Demo | πŸ“– arXiv | πŸ’Ό LinkedIn |

πŸ”Ž Benchmarking VLMs for medical vision tasks: detection and measurement πŸ“

πŸ’Ώ v1.4.0 | 33.2K 3D images | 12.0M 2D slices | 25.7M single-instance / 50.5M multi-instance annotations | multi-modality | multi-anatomy πŸ’Ώ

πŸ“ Annotation: segmentation mask | landmark coordinate | bounding box | tumor/lesion size | distance | angle πŸ“

🎯 Post-training: SFT, RFT (RL), CoT, LoRA | Framework: TRL, verl 🎯

@misc{yao2026medvisionbenchmarkingquantitativemedical,
      title={MedVision: Benchmarking Quantitative Medical Image Analysis}, 
      author={Yongcheng Yao and Yongshuo Zong and Raman Dutt and Yongxin Yang and Sotirios A Tsaftaris and Timothy Hospedales},
      year={2026},
      eprint={2511.18676},
      archivePrefix={arXiv},
      primaryClass={cs.CV},
      url={https://arxiv.org/abs/2511.18676}, 
}

πŸ“‘ Table of Contents


πŸ† Benchmarked Models

This codebase supports open-weight VLMs and API models. The leaderboard published full scores for 18 VLMs, evaluated with MedVision dataset v1.0.0-v1.1.1 and 1000-sample-per-subtask. Pilot study include scored for API models with the same dataset but with 100-sample-per-subtask.

The live open leaderboard (per-task score tables + a frontier API-model pilot study) lives on the project page.

Ours

MedVision-V0

General-purpose VLMs (open-weight)

Qwen Qwen2.5-VL, Qwen3-VL-Thinking Β Β·Β  InternVL InternVL3 Β Β·Β  Gemma Gemma-3, Gemma-4 Β Β·Β  Meta Llama-3.2-Vision Β Β·Β  LLaVA-OneVision Β Β·Β  GLM GLM-4.6V, GLM-4.6V-Flash

Medical VLMs (open-weight)

Gemma MedGemma Β Β·Β  LLaVA-Med, Lingshu, MedDr, HuatuoGPT-Vision, HealthGPT-L14

Proprietary / API

Claude Claude-Fable-5 Β Β·Β  OpenAI GPT-5.5-Pro Β Β·Β  Gemini Gemini-3.1-Pro Β Β·Β  Kimi Kimi-K2.6

The project-page leaderboard currently publishes full score tables for the 12 off-the-shelf VLMs + MedVision-V0, plus a Claude-Fable-5 / Gemini-3.1-Pro API pilot on tumor/lesion size. Newer entries (Qwen3-VL-Thinking, Gemma-4, GLM-4.6V/-Flash, GPT-5.5-Pro, Kimi-K2.6) have eval scripts wired up and are being rolled into the leaderboard.


πŸ”₯ News

  • [Sep 1, 2026] Update paper, leaderboard, dataset v1.4.0 blog, and RFT recipe

  • [Aug 31, 2026] Released the benchmarking/fine-tuning codebase medvision_bm v1.2.0 β€” release notes

    Details (Click to expand)
    • πŸ§‘β€βš–οΈ New LLM-as-Judge output parsing: a second, independent reading of every benchmark response, so format-following failure is separable from measurement failure.
    • 🩺 New Clinical Decision Agreement (CDA) analysis: re-scores existing outputs into clinical categories (SNA/SNB position bands, AJCC kidney tumor staging) and reports Cohen's and weighted kappa.
    • πŸ› Key evaluation bugfixes: HuatuoGPT-Vision's decoding config never reached generate() (512-token CoT truncation); explicit output-token budgets on every launcher; GLM-4.6V no longer reinstalls medvision_ds mid-run.
    • πŸ“ New metrics: COCO-grid Acc@IoU for Detection, cross-dataset Distance and Angle group rows for A/D.
    • Dataset v1.2.0–v1.4.0 integration with ceiling-based plan resolution.
  • [Aug 19, 2026] πŸš€ Release MedVision dataset v1.4.0 [release-v1.4.0]

    Details (Click to expand)
    • Regenerate Tumor-Lesion-Size annotations for all 12 tumour/lesion datasets: multi-instance annotations 75K --> 3.8M
  • [Aug 9, 2026] πŸš€ Release MedVision dataset v1.3.0 [release-v1.3.0]

    Details (Click to expand)
    • New dataset: MSWAL (484 abdominal CT cases; tumor/lesion labels: liver tumour, kidney tumour, pancreatic cancer, liver cyst, and kidney cyst).
  • [Aug 3, 2026] πŸš€ Release MedVision dataset v1.2.1 [release-v1.2.1]

    Details (Click to expand)
    • ⚠️ Corrects MAMA-MIA and PI-CAI, whose v1.2.0 annotations were recorded in the source orientation instead of RAS+ β€” the loader reoriented the images at load time without renumbering the coordinates. Their v1.2.0 annotations are withdrawn. If you have used either dataset, clear that cache once.
    • No other dataset is affected β€” the other 28 resolve to exactly the same annotation files as at v1.2.0.
    • New: scripts/gen-annotations/ rebuilds the preprocessed images and annotations of any dataset from its original source -- For the record only, you never use it to load data.
  • [Jul 28, 2026] πŸš€ Release MedVision dataset v1.2.0 [release-v1.2.0]

    Details (Click to expand)
    • Highlight: 8 new datasets (130 configs) β€” AFIDs, DEEP-PSMA, LIDC-IDRI, LNQ2023, MAMA-MIA, PDDCA, PI-CAI, VerSe.
    • No existing annotation changed. Annotation versions now resolve per dataset: the version you set is a ceiling, and each dataset loads the newest annotation it published at or before it. Pinning '1.1.1' or older keeps working for every pre-existing dataset (check Annotation Version Control).
    • ⚠️ Fixes a stale-cache defect present in all earlier versions. The cache key used the version you requested rather than the annotation actually loaded, so load_dataset could silently return previously cached rows after the annotations changed β€” which really happened, to the v1.1.0 T/L train/test split. See Fixed: cached data could be stale for who is affected and how to clear it. The data root is now part of the key too, which matters only if your HuggingFace cache is not already co-located with it. Because the cache key changed, existing Arrow caches rebuild once on next use (reads the annotation file, no re-download)
  • [Jul 21, 2026] Updated leaderboard and data explorer

  • [Jul 4, 2026] Released the benchmarking/fine-tuning codebase medvision_bm v1.1.1 β€” release notes

    Details (Click to expand)
    • πŸ“š New documentation site on Read the Docs: installation, dataset, benchmarking, and fine-tuning guides plus the full CLI and Python API reference.
  • [Jun 29, 2026] πŸš€ Released the MedVision dataset (medvision_ds) v1.1.1 β€” release notes

    Details (Click to expand)
    • Highlight: corrected T/L ellipse fit β€” fixes a transposed in-plane voxel-spacing bug (wrong axis lengths and major/minor labelling on anisotropic slices, e.g. sagittal/coronal); ~22% fewer T/L samples on anisotropic data, isotropic data (e.g., axial slices) essentially unchanged
    • Backward compatibility: The codebase medvision_ds will be automatically updated to the latest (v1.1.1). MedVision_PLANNER_VERSION='latest' now resolves to '1.1.1'; pin '1.1.0' or '1.0.0' for earlier annotations.
    • ⚠️ New env var MedVision_ACK_RELEASE: required only when you pin an older version (MedVision_PLANNER_VERSION below the latest) β€” set it to the latest version (1.1.1) to acknowledge you have read this release note and unblock loading legacy data.
    • Always set MedVision_FORCE_INSTALL_CODE='True' to receive notification of future releases. See Environment Variables.
  • [Jun 9, 2026] Released MedVision-V0, RFT code, preprint v2, project page with interactive case viewer.

  • [May 15, 2026] Released the benchmarking/fine-tuning codebase medvision_bm v1.1.0 β€” release notes

  • [May 14, 2026] Released the MedVision dataset (medvision_ds) v1.1.0 β€” release notes

    Details (Click to expand)
    • Highlight: new T/L sample filtering (with ambiguous cases removed), more T/L samples with a single small target (cluster size > 20)
    • Backward compatibility: The codebase medvision_ds will be automatically updated to the latest (v1.1.0). MedVision_PLANNER_VERSION is required (v1.1.0+) to specify the annotation data version. Setting MedVision_PLANNER_VERSION='1.0.0' will fall back to MedVision dataset v1.0.0.
    • πŸ§ͺ Test backward compatibility:
    python unit-test/medvision-ds-planner-version/test_planner_switch_medvision_ds_v1.1.0.py --data_dir <local-data-folder>
    
  • [Dec 10, 2025] Added preprint, training code, docker images, released models, new tasks/models guide

  • [Oct 8, 2025] Released MedVision dataset v1.0.0


🌟 Quick Start

πŸ“š Read the Docs: Installation Β· Quickstart walkthrough

Option 1 β€” run the full pipeline (benchmarking, SFT/RFT). Clone the repo and install from the local copy. Use this when you rely on the repo's folder structure (e.g. script/, tasks_list/, Results/), since the scripts and configs live there.

git clone https://github.com/YongchengYAO/MedVision.git MedVision
cd MedVision
pip install .
pip show medvision_bm

Option 2 β€” import the package in your own project. Install medvision_bm from PyPI. Use this when you only want to import its modules/functions (e.g. from medvision_bm.utils import parse_utils) and do not need the repo's folder structure.

Stable release (PyPI):

# Install the benchmark codebase
pip install medvision-bm
pip show medvision_bm

# Install the dataset codebase
# "Data" is the local dataset folder
mvbm install mvds -d Data

Or the nightly build (latest commit on GitHub master):

pip install "git+https://github.com/YongchengYAO/MedVision.git"
pip show medvision_bm

🐳 Use Docker

πŸ“š Read the Docs: Installation β†’ Docker

Docker images are built from these dockerfiles

  • Base docker: docker pull vincentycyao/medvision:base
  • You can choose model-specific docker and skip env setup in scripts, such as docker pull vincentycyao/medvision:eval_medvision-v0, then in eval_*.sh:
    python -m medvision_bm.benchmark.install_medvision_ds --data_dir "${data_dir}" # always keep
    python -m medvision_bm.benchmark.install_vendored_lmms_eval --lmms_eval_opt_deps medvision_v0 # always keep
    #pip install -r "${benchmark_dir}/requirements/requirements_eval_medvision-v0.txt" --no-deps # can skip if using model-specific docker
    
    python -m medvision_bm.benchmark.eval__medvision-model-rft \
      --skip_env_setup \
      ...
    
    
  1. Choose the docker image for a specific model: https://hub.docker.com/r/vincentycyao/medvision/tags

    docker pull vincentycyao/medvision:<tag>
    
  2. Map local volumes and GPUs, then use the docker image vincentycyao/medvision:<tag>

    # NOTE: replace </path/to/working/folder>, <tag>
    docker run -it --rm \
        --gpus all \
        -v </path/to/working/folder>:/root/Documents/MedVision \
        vincentycyao/medvision:<tag> \
        bash
    
    # In the container
    git clone https://github.com/YongchengYAO/MedVision.git /root/Documents/MedVision
    cd /root/Documents/MedVision
    
    # Check existing Conda env and activate 
    conda env list
    conda activate <env-name>
    
    # Install the latest medvision_bm
    pip install .
    pip show medvision_bm
    
    # Install the latest medvision_ds
    mvbm install mvds -d Data
    pip show medvision_ds
    

[!TIP] Treat the MedVision folder as the working directory for benchmarking and fine-tuning.

File structure: imaging data, benchmark results, and model checkpoints are automatically saved


πŸ’Ώ Data

πŸ“š Read the Docs: Dataset concepts Β· Loading data

[!IMPORTANT] Leaderboard results use annotation v1.0.0. All leaderboard numbers are computed on the v1.0.0 annotations. We removed ambiguous cases (multi-instance targets) in metric calculation as a workaround. For new studies we recommend the latest annotation version (currently v1.4.0).

πŸ’« Quick start: use MedVision Data Explorer

  • Dataset. For the full description of the MedVision dataset (source datasets, modalities, anatomies, annotation types, and returned fields), see the Hugging Face dataset repo.

  • Benchmark subtasks ↔ dataset subsets. Each subtask in this benchmark links to a subset of the MedVision dataset. The per-subtask sample sizes are listed for every dataset version 1.0.0–1.4.0:

    • all_tasks__ds_v1.1.1: we use this for OOD ablation since we fixed the annotation error for sagittal and coronal slices from v1.1.1
    • all_tasks__ds_v1.0.0: we use single instance annotations from v1.0.0 in all benchmarking tasks
  • Pixel size (physical spacing) distribution. Because the quantitative tasks require pixelβ†’mm arithmetic, the distribution of pixel sizes across subtasks is provided in pixel_sizes__ds_v1.0.0.

  • Image size distribution. The distribution of image sizes across subtasks is provided in image_sizes__ds_v1.0.0.

  • Multi-instance and single-instance annotations. Each benchmark sample is a (2D slice, target) pair, several instances of the same target on one slice still count as one annotation.

    • Single-instance βŠ† multi-instance
    • Multi-instance (unfiltered) β€” every target carrying β‰₯ 1 annotation is kept, however many instances (boxes / clusters) it has on the slice and whatever their size.
    • Single-instance (filtered) β€” a target is kept only when it is a single, large-enough instance. Per benchmark task, a sample is dropped when:
    Details (Click to expand) | Benchmark task | Single-instance drops the sample when… | |---|---| | **Box** β€” detection | the slice has **more than one** box for the target (`len(boxes) > 1`), **or** a box is **< 10 px** on any side | | **T/L** β€” tumor / lesion size | the target has **more than one** cluster on the slice (`n_clusters > 1`; `len(biometric_profile) > 1` on the v1.0.0 fallback) | | **A/D** β€” biometrics (angle / distance) | *never dropped* β€” every angle and distance sample is kept (the loader only splits them by `metric_type`) |

[!TIP] To load the unfiltered (multi-instance) samples, set the environment variable MedVision_DISABLE_SAMPLE_FILTERING=true (default off) β€” it bypasses the per-sample quality/size filters and returns every planner sample.

[!WARNING] Multi-instance annotations are not for leaderboard comparison. Do not use multi-instance annotations to compare models on the leaderboad. The current MedVision-V0 SFT/RFT training is not optimized for multi-instance detection and measurement tasks.

Per-dataset statistics

Computed from the local benchmark plans by script/misc/summarize_datasets.sh. The modality / image / slice / segmentation stats are the same for every dataset version; only the benchmark-annotation counts (Box / T/L / A/D) depend on it β€” and among those, only T/L changes (Box and A/D are identical across versions). Each row is version-invariant, but the set of rows is not: the table lists the 31 datasets present in v1.4.0, so its Total row is specific to that release.

Per-dataset stats β€” modality, images, slices, segmentation (version-invariant)
DatasetModality3D Images3D Masks2D SlicesSeg. annotations
ACDCMRI30030043,96294,160
AFIDsMRI7204320
AMOS22CT, MRI360360251,6371,215,776
AbdomenAtlas1.0MiniCT5,1955,1953,778,80513,770,398
AbdomenCT-1KCT1,0001,000711,1551,549,325
BCV15CT606034,472125,870
BraTS24MRI10,6323,0332,019,1183,767,594
CAMUSultrasound1,0001,000670,9641,341,433
Ceph-Biometrics-400X Ray40007,6000
CrossMoDAMRI10510514,11516,623
DEEP-PSMAPET20020050,34150,341
FLARE22CT505034,235152,954
FeTA24MRI808035,776153,599
HNTSMRG24MRI30030056,07862,424
ISLES24MRI29814997,22897,228
KiPA22CT707029,49474,690
KiTS23CT489489190,642291,550
LIDC-IDRICT1,0131,01373,97673,976
LNQ2023CT12012025,33125,331
MAMA-MIAMRI1,5061,506214,408214,408
MSDCT, MRI3,2251,741791,7061,438,472
MSWALCT484484108,246125,126
OAIZIB-CMMRI507507358,728922,989
PDDCACT484815,00337,119
PI-CAIMRI42542542,85742,857
SKM-TEAMRI310155173,690475,828
ToothFairy2CT480480397,5312,131,223
TopCoW24CT, MRI25025087,953251,901
TotalSegmentatorCT, MRI1,8441,8441,091,56316,979,575
VerSeCT325325208,402874,896
autoPET-IIICT, PET2,0761,038360,638360,638
Total (31)β€”33,22422,32711,976,08646,718,304

Benchmark annotations by dataset version. Each block below breaks the annotation counts down by dataset and by task β€” Box (detection), T/L (tumor/lesion size) and A/D (biometrics) β€” under both the single-instance (filtered) and multi-instance (unfiltered) views. Across releases, Box and A/D counts are byte-identical; T/L is the only task ever regenerated, in v1.1.0, v1.1.1 and most recently v1.4.0, which rebuilt all 12 T/L datasets. A total therefore moves for exactly two reasons: a T/L regeneration, or a release that adds datasets (v1.2.0 added 8, v1.3.0 added MSWAL). Each block totals only the datasets its own release shipped β€” 31 for v1.3.0 and v1.4.0, 30 for v1.2.0, 22 for the rest:

MedVision v1.4.0 (default)

T/L regenerated. Clusters are now selected by a physical size floor in millimetres β€” max(2.0 mm, 2 Γ— the coarser in-plane spacing) of the measured plane β€” instead of a raw pixel count; a gate that silently discarded rotated ellipses is removed; and the ellipse fit is guarded against degenerate results. Published T/L landmarks grow 75,840 β†’ 3,801,540 (50Γ—). Every other task and every previously published annotation file is unchanged. See the v1.4.0 release note and the blog post (MedVision v1.4.0: 50Γ— More Tumor/Lesion Measurements and Their Annotation Recall).

Single-instanceMulti-instance
v1.4.0 single-instance donutv1.4.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AFIDs0043200432
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,524427,33903,767,5941,645,5500
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
DEEP-PSMA2,17910,740050,341156,4390
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,02943,920062,42469,0930
ISLES249,7740097,22800
KiPA2237,64713,061074,69013,8640
KiTS23114,49162,2250291,55076,0160
LIDC-IDRI10,37943,488073,97659,1790
LNQ20231,6535,702025,33137,8980
MAMA-MIA67,76790,7250214,408369,4190
MSD277,451115,76401,438,472712,2470
MSWAL60,10678,5860125,126145,1360
OAIZIB-CM648,04800922,98900
PDDCA15,076013237,1190132
PI-CAI5,45522,764042,85728,3240
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
VerSe350,31101,500874,89601,500
autoPET-III31,79451,8750360,638488,3750
Total (31)24,749,253966,1899,98946,718,3043,801,5409,989
Total (all tasks)25,725,43150,529,833
MedVision v1.3.0

Adds MSWAL (CT, 484 volumes). No annotation logic changed β€” every v1.2.0 count is carried over unmodified.

Single-instanceMulti-instance
v1.3.0 single-instance donutv1.3.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AFIDs0043200432
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52415,88003,767,59426,1830
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
DEEP-PSMA2,17994050,3417530
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0292,272062,4243,1880
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,4917,0480291,5508,0340
LIDC-IDRI10,379417073,9765170
LNQ20231,65345025,3312380
MAMA-MIA67,7673,2400214,4085,0710
MSD277,4516,12601,438,47212,9140
MSWAL60,1068,2540125,12612,2600
OAIZIB-CM648,04800922,98900
PDDCA15,076013237,1190132
PI-CAI5,455393042,8574090
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
VerSe350,31101,500874,89601,500
autoPET-III31,7948790360,6383,1180
Total (31)24,749,25347,7259,98946,718,30475,8279,989
Total (all tasks)24,806,96746,804,120
MedVision v1.2.0
Single-instanceMulti-instance
v1.2.0 single-instance donutv1.2.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AFIDs0043200432
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52415,88003,767,59426,1830
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
DEEP-PSMA2,17994050,3417530
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0292,272062,4243,1880
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,4917,0480291,5508,0340
LIDC-IDRI10,379417073,9765170
LNQ20231,65345025,3312380
MAMA-MIA67,7673,3270214,4085,1170
MSD277,4516,12601,438,47212,9140
OAIZIB-CM648,04800922,98900
PDDCA15,076013237,1190132
PI-CAI5,455395042,8574100
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
VerSe350,31101,500874,89601,500
autoPET-III31,7948790360,6383,1180
Total (30)24,689,14739,5609,98946,593,17863,6149,989
Total (all tasks)24,738,69646,666,781
MedVision v1.1.1
Single-instanceMulti-instance
v1.1.1 single-instance donutv1.1.1 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52415,88003,767,59426,1830
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0292,272062,4243,1880
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,4917,0480291,5508,0340
MSD277,4516,12601,438,47212,9140
OAIZIB-CM648,04800922,98900
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
autoPET-III31,7948790360,6383,1180
Total (22)24,236,32735,2827,92545,274,25056,5797,925
Total (all tasks)24,279,53445,338,754
MedVision v1.1.0
Single-instanceMulti-instance
v1.1.0 single-instance donutv1.1.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52419,13903,767,59430,3570
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0293,129062,4244,4750
ISLES249,7740097,22800
KiPA2237,6473,077074,6903,1420
KiTS23114,49112,4710291,55014,1480
MSD277,4519,15201,438,47216,6200
OAIZIB-CM648,04800922,98900
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
autoPET-III31,7941,2460360,6383,8690
Total (22)24,236,32748,2147,92545,274,25072,6117,925
Total (all tasks)24,292,46645,354,786
MedVision v1.0.0
Single-instanceMulti-instance
v1.0.0 single-instance donutv1.0.0 multi-instance donut
DatasetSingle-instance AnnotationMulti-instance Annotation
BoxT/LA/DBoxT/LA/D
ACDC14,2710094,16000
AMOS22666,532001,215,77600
AbdomenAtlas1.0Mini9,748,2900013,770,39800
AbdomenCT-1K1,041,588001,549,32500
BCV1568,54300125,87000
BraTS241,115,52411,07103,767,59411,0930
CAMUS951,370001,341,43300
Ceph-Biometrics-400007,600007,600
CrossMoDA4,0760016,62300
FLARE22104,21100152,95400
FeTA2449,0870325153,5990325
HNTSMRG2432,0291,392062,4241,4160
ISLES249,7740097,22800
KiPA2237,6473,095074,6903,0950
KiTS23114,4918,4840291,5508,5400
MSD277,4517,47201,438,4727,6740
OAIZIB-CM648,04800922,98900
SKM-TEA262,33800475,82800
ToothFairy21,413,979002,131,22300
TopCoW2441,82900251,90100
TotalSegmentator7,603,4550016,979,57500
autoPET-III31,7947350360,6387490
Total (22)24,236,32732,2497,92545,274,25032,5677,925
Total (all tasks)24,276,50145,314,742

πŸ“Š Benchmark

πŸ“š Read the Docs: Pipeline overview Β· Running evaluations Β· Parsing & summarizing Β· CLI reference

Benchmark Setting

Steps

  1. The scripts in script/benchmark-*/ should be sufficient for dependency installation, data processing, and benchmarking

[!TIP] Set these variables:

  • benchmark_dir: the working directory
  • model_hf_id: Hugging Face ID (<user>/<model>) of the tested model
  • model_name: user-defined identifier for the tested model, used as folder name in Results/MedVision-*/
  • resource-constrained configs, such as batch_size_per_gpu

[!NOTE] Crash-safe resume.

During evaluation each finished output is written immediately to Results/MedVision-*/<model_name>/response_cache/<task>_rank<N>.jsonl, so re-running an interrupted eval skips already-completed samples instead of regenerating them β€” only the in-flight sample is lost. The cache key includes a hash of the prompt, so editing a prompt/config automatically invalidates stale entries (no need to clear the folder). Set the environment variable MEDVISION_RESP_CACHE=0 to disable this layer entirely and reproduce the original (no-cache) behavior.

  1. After evaluating all models in step 1, parse model outputs and calculate metrics (e.g., MRE, MAE, nMAE, IoU, F1, Precision, Recall, Success Rate). Base command:

[!TIP] Command: python -m medvision_bm.benchmark.parse_outputs

Arguments:

  • --task_type: one of ["AD", "TL", "Detection"]
  • --task_dir: task folder
  • --model_dir: model folder
  • --limit: limit sample size in the parsed files
  • --skip_existing: (store_true) skip parsed files
  • --processes, -p: number of processes
  • --rm_old: remove existing parsed folder for each model

Example 1 β€” parse all models for the T/L task:

python -m medvision_bm.benchmark.parse_outputs \
--task_type TL \
--task_dir Results/MedVision-TL \
-p 32

Example 2 β€” parse all models for the A/D task (remove existing parsed folder):

python -m medvision_bm.benchmark.parse_outputs \
--task_type AD \
--task_dir Results/MedVision-AD \
-p 32 \
--rm_old

Example 3 β€” parse one model for the detection task and skip existing parsed files:

python -m medvision_bm.benchmark.parse_outputs \
--task_type Detection \
--model_dir Results/MedVision-detect/Qwen2.5-VL-32B-Instruct \
--skip_existing \
-p 32
  1. Summarize model performance for each task

[!TIP] If medvision_ds is missing, install with:

python -m medvision_bm.benchmark.install_medvision_ds --data_dir

[!TIP] Command:

python -m medvision_bm.benchmark.summarize_{AD,TL,detection}_task

Arguments:

  • --task_dir: task folder
  • --model_dir: model folder
  • --limit: limit sample size in the parsed files
  • --skip_model_wo_parsed_files: skip model directories that don't have a parsed folder
  • --processes, -p: number of processes
  • --removed_samples_dir: (TL task only) root directory with per-dataset removed_samples JSON files, used to filter ambiguous cases

Example 1 β€” summarize all models for the A/D task:

python -m medvision_bm.benchmark.summarize_AD_task \
--task_dir Results/MedVision-AD \
-p 32

Example 2 β€” summarize all models for the T/L task:

python -m medvision_bm.benchmark.summarize_TL_task \
--task_dir Results/MedVision-TL \
--removed_samples_dir <local-data-folder>/Datasets \
-p 32

Example 3 β€” summarize one model for the detection task:

python -m medvision_bm.benchmark.summarize_detection_task \
--model_dir Results/MedVision-detect/Qwen2.5-VL-32B-Instruct \
-p 32
  1. (Recommended) LLM-judge parsing β€” the format-robust second pass

The regex parser in step 2 only accepts answers written inside <answer>…</answer>, so a response that states a correct answer in any other form (\boxed{…}, **Answer:** …, plain prose) is scored as a miss β€” mixing "the model can't measure" with "the model didn't follow the output format". The LLM-judge pipeline re-reads every response with a judge model whose only job is to find and quote the answer, wherever it was written. Regex parsing (steps 2–3) and LLM-judge parsing together are the complete parsing scheme β€” run both: the diff between the two reports is how much of a model's apparent failure was formatting.

  • [File structure] after steps 1-4

    β”œβ”€β”€ MedVision
    β”‚   β”œβ”€β”€ completed_tasks 
    β”‚   β”‚   β”œβ”€β”€ completed_tasks_MedVision-AD.json       # <== tasks status tracker
    β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”œβ”€β”€ Results                                     # <== benchmark results
    β”‚   β”‚   β”œβ”€β”€ MedVision-AD
    β”‚   β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_AD_task.txt                 # <== [step 3] summary
    β”‚   β”‚   β”œβ”€β”€ MedVision-detect
    β”‚   β”‚   β”‚   β”œβ”€β”€ Qwen2.5-VL-32B-Instruct
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ parsed                               
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.jsonl                     # <== [step 2] parsed model outputs
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.json                      # <== [step 2] parsed summary file
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ summary_*                   # <== [step 3] mean metrics, values
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ llm-parsed_<judge>              # <== [step 4] format-robust re-parse; parsed/ is never touched
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.jsonl                     #        re-parsed records: outcome label + quoted answer span
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ summary_*                   #        mean metrics, values (same summarizer as step 3)
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ summary_metrics_judge_Task.json    # per-model failure decomposition
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ response_cache                  # <== [step 1] per-sample resume cache (auto; MEDVISION_RESP_CACHE=0 to disable)
    β”‚   β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *_rank*.jsonl               #        one line per finished sample, written as produced
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.jsonl                         # <== [step 1] model outputs
    β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ *.json                          # <== [step 1] summary file
    β”‚   β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_detection_task.txt          # <== [step 3] summary
    β”‚   β”‚   β”‚   β”œβ”€β”€ judge-queue_Detection.jsonl         # <== [step 4] work list, one row per response
    β”‚   β”‚   β”‚   β”œβ”€β”€ judge-out_Detection_<judge>.jsonl   # <== [step 4] the judge's raw answers
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_detection_task__llm-parsed_<judge>.txt  # <== [step 4] format-robust summary (diff against summary_detection_task.txt)
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_judge_task__llm-parsed_<judge>.txt      # <== [step 4] wrong-format vs. no-answer breakdown + judge validity
    β”‚   β”‚   β”œβ”€β”€ MedVision-TL
    β”‚   β”‚   β”‚   β”œβ”€β”€ ...
    β”‚   β”‚   β”‚   β”œβ”€β”€ summary_TL_task.txt                 # <== [step 3] summary
    
  • [Analysis & Visualization] (optional) Scripts in script/visualization:

    • Radar charts (viz_radar.sh, viz_radar_batch.sh): cross-model comparison across metrics.
    • Per-model radar grids (viz_radar_grid.sh): one row per model, six radars per row (Detection Recall/Precision/F1, A/D Angle/Distance MRE, T/L MRE), each spoke overlaid with that model's per-sample violin + box plots.
    • Leaderboard timeline (viz_benchmark_leaderboard_timeline.sh): benchmark accuracy against model release date, one panel per task (Detection IoU, plus T/L, Distance and Angle as 1/MRE).
    • Detection label Γ— box-size (viz_detection_sampleSize_per_label_x_boxSize.sh): detection metrics and sample distribution per label Γ— box-to-image ratio group.
    • A/D landmark overlays (viz_ad_landmarks.sh): per-sample GT vs. predicted landmarks and lines.
    • A/D response panels (viz_ad_responses.sh): per-sample prompt/response/GT panels.
    • T/L axis overlays (viz_tl_axes.sh): per-sample predicted vs. GT axes with mask contour.
    • T/L response panels (viz_tl_responses.sh): per-sample prompt/response/GT panels.
    • T/L ellipse fit: image vs. real space (viz_ellipse_fit_comparison.py): overlay the T/L ellipse axes fit in image (pixel) space vs. real (physical) space on one coronal/sagittal slice, illustrating how anisotropic pixel spacing makes the two fits diverge (rendered with and without the physical aspect-ratio correction).
    • Detection box overlays (viz_detection_boxes.sh): per-sample GT vs. predicted bounding boxes.
    • Detection response panels (viz_detection_responses.sh): per-sample prompt/response/GT panels.
    • Ground-truth annotation figure (viz_gt_annotations.sh): GT-only panels rendered straight from the on-disk benchmark plans (no model output, no HuggingFace build), compiled into one figure with a labelled row-block per task.
    • Plane-OOD sample grids (viz_planeOOD_samples.sh): the same volume and target shown in the in-distribution axial plane and in both OOD planes (coronal, sagittal); Detection and T/L only, since A/D has no plane-OOD split.
    • Label clouds (viz_label_cloud.sh): 2Γ—2 target Γ— modality label clouds contrasting each task's in-distribution roster against its target-OOD roster.
    • Comparison grids (viz_compile_grid_batch.sh): tile per-sample overlays across models.
  • [Analysis] (optional) Scripts in script/analyze:

    • Process accuracy (process-accuracy/analyze_process_accuracy_TL.py, process-accuracy/analyze_process_accuracy_AD.py): step-by-step CoT accuracy for T/L (4 steps: major/minor axis endpoint norm-L2 β†’ axis length MRE) and A/D (3 steps: landmark coordinate norm-L2 β†’ scalar MRE), evaluated against ground truth.
    • Equation accuracy (equation-accuracy/analyze_equation_accuracy_TL.py, equation-accuracy/analyze_equation_accuracy_AD.py): arithmetic correctness independent of ground truth β€” extracts the equation the model wrote, evaluates it in Python, and computes MRE between that result and the model's own reported answer.
    • Detection Γ— target size (detection--target-size/run_analysis.sh): detection metrics (F1, IoU, etc.) stratified by box-to-image ratio, revealing performance trends across small, medium, and large targets.
    • Clinical Decision Agreement (CDA) (clinical-decision-analysis/run_CDA_analysis.sh): asks whether a measurement error would change the clinical decision β€” each prediction and its ground truth are pushed through a published cutoff table into a clinical category, and agreement is scored with Cohen's / weighted kappa. Re-reads existing parsed/ records only: no re-inference, no GPU, seconds per model. Check the CDA pipeline.
    • Compare with Segmentation Specialist (script/ablation/biomedparse): Evaludate and fine-tune BiomedParse v2 on our dataset
  • [Troubleshooting] here


🎯 Training: SFT

πŸ“š Read the Docs: Supervised fine-tuning (SFT)

[!TIP] Set these variables in the script:

  • benchmark_dir: the working directory
  • base_model_hf: Hugging Face ID (<user>/<model>) of the base model, or the path to a local model folder.
  • run_name: an identifier for the current training
  • merged_model_hf: Hugging Face model name (<model>) of the merged model
  • resource-constrained configs, such as
    • per_device_train_batch_size
    • gradient_accumulation_steps
    • CUDA_VISIBLE_DEVICES=0,1,2,3 and --num_processes=4

🎯 Training: RFT

πŸ“š Read the Docs: Reinforcement fine-tuning (RFT)

RL fine-tuning uses the verl framework. MedVision provides parquet dataset builders that turn the MedVision tasks into verl-ready parquet datasets.

  • [Data Processing] Build the verl parquet dataset with the scripts in script/rft, which call:

    • medvision_bm.rft.verl.build_parquet_ds: normal parquet dataset builder
    • medvision_bm.rft.verl.build_parquet_ds__checkpointed: checkpointed builder to avoid OOM, recommended for large datasets (e.g. ~1M detection samples)

    Available scripts:

    • build_parquet_ds__verl__D0k-AD5.5k-TL0k__512x512.sh: A/D task only (5.5K train / 45 val)
    • build_parquet_ds__verl__D0k-AD0k-TL5.5k__512x512.sh: T/L task only (5.5K train / 50 val)
    • build_parquet_ds__verl__D110k-AD0k-TL0k__512x512.sh: Detection task only (110K train / 105 val)
    • build_parquet_ds__verl__D110k-AD5.5k-TL5.5k__512x512.sh: all 3 tasks combined (121K train / 200 val)
    • build_parquet_ds__verl__D1000k-AD0k-TL0k__512x512__checkpointed.sh: Detection task only, large scale (1M train / 500 val); uses the checkpointed builder
  • [RFT] RL fine-tuning (GRPO) runs in our verl fork, branch medvision-rl, which implements the RFT stage of the paper:

    • Rewards (verl/utils/reward_score/medvision_rewards/): format, process (per-CoT-step landmark / measurement accuracy) and answer rewards with the exp(-error) mapping; CIoU overlap reward for detection; multiplicative composition r = r_format + r_process * r_answer (default) and the additive ablation r = r_format + r_process + r_answer; options and CLI overrides in REWARDS.md.
    • Temperature-scaled task mixing (verl/utils/dataset/temperature_sampler.py): task probability proportional to count^(1/T) to rebalance the 110K / 5.5K / 5.5K multi-task mixture (T=8).
    • Epoch-level curriculum learning (verl/utils/dataset/curriculum.py, CURRICULUM_FILTERING.md): per-task easy / hard pools, EMA-gated promotion of solved samples, retention mix-in, rotating audits with hysteresis-guarded demotion, and a per-task floor.
    • Recipes (examples/grpo_trainer/): sequential A/D β†’ T/L β†’ detection RFT (train__rft-sequential__{1-AD,2-TL,3-detection}.sh; MedVision-V0) and single-stage multi-task RFT with curriculum (train__rft-multitask.sh; additive-reward twin train__rft-multitask__additive-reward.sh); each script takes DATASET_ROOT plus either BASE_MODEL_PATH (a local checkpoint) or BASE_MODEL_HF (a Hub id, downloaded locally before training).
  • [Evaluation] Evaluate the trained model with eval__MedVision-V0-7B__detect.sh (in script/benchmark-*/).


πŸ“š New Tasks/Models Guide

πŸ“š Read the Docs: Adding a new model Β· Adding a new task

New tasks guide | New models guide

πŸ–ΌοΈ Model Image Processing

For the quantitative tasks (TL/AD), the image size and pixel size stated in each prompt must match the resolution the model's vision encoder actually perceives after its internal resize. Model image processing documents the per-model strategy (fixed perceived size, dynamic processor probe, or API resize formula), with code references, validation status, and known caveats for every supported model.


πŸ€– Agent Skills

MedVision ships Agent Skills β€” plain-Markdown instructions that teach an AI coding agent how to operate this repository. They live in skills/ and use the cross-harness <name>/SKILL.md layout, so the same files work in Claude Code, Codex, OpenCode and Pi.

Installation

Every harness discovers skills at <skills-root>/<name>/SKILL.md. Codex, OpenCode and Pi all read the shared ~/.agents/skills root; Claude Code reads ~/.claude/skills. Pick your root and link the three skills:

git clone https://github.com/YongchengYAO/MedVision.git && cd MedVision

SKILLS_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills"   # Claude Code
# SKILLS_ROOT=~/.agents/skills                             # Codex Β· OpenCode Β· Pi (shared root)
# SKILLS_ROOT=~/.config/opencode/skills                    # OpenCode (native root)
# SKILLS_ROOT=~/.pi/agent/skills                           # Pi (native root)

mkdir -p "$SKILLS_ROOT"
for s in medvision medvision-paper medvision-pipeline; do
  ln -s "$PWD/skills/$s" "$SKILLS_ROOT/$s"    # use `cp -r skills/$s "$SKILLS_ROOT/$s"` to pin a copy
done

To scope the skills to this checkout only, link them into the project instead β€” .claude/skills/ for Claude Code, .agents/skills/ for Codex, OpenCode and Pi:

mkdir -p .claude/skills
for s in medvision medvision-paper medvision-pipeline; do
  ln -s "../../skills/$s" ".claude/skills/$s"
done

Restart the agent afterwards so it re-scans the skills root.

SkillInvokeAnswers
medvision-paperautomaticPaper facts: dataset scale, annotation rules, metric definitions, the MedVision-V0 recipe, the evaluated VLM roster
medvision-pipelineautomaticThe benchmark pipeline end to end, dataset download, environment setup, where the SFT launchers live
medvision/medvisionDeep reference: 10 sub-skills covering every flag, launcher, version pin and failure mode

[!NOTE] medvision sets disable-model-invocation: true, so Claude Code and Pi load it only when you type /medvision. OpenCode ignores that field and will advertise it automatically.

Understand the paper: /medvision-paper

Ask about the technical details behind the benchmark and get the paper's own definitions rather than a guess:

/medvision-paper  How are tumor/lesion sizes annotated, and which ellipse fits are discarded?
/medvision-paper  What is nMAE, how does it differ from MRE, and what is the denominator?
/medvision-paper  What data and hyperparameters produced MedVision-V0?

It is the source of truth for terminology and scale numbers β€” annotation-generation rules, the real-world-units convention (annotations are mm and degrees, never pixels), metric definitions, the 70/30 subject-level split, and the SFT/RFT recipe.

Run the benchmark: /medvision-pipeline

Describe the run you want and the skill returns the ordered steps with the right module names and flags:

/medvision-pipeline  Evaluate Qwen-2.5-VL on the T/L task, then parse and summarize the results.

The pipeline it walks you through:

# 1 β€” evaluate (or use a launcher in script/benchmark-{detect,TL,AD}/)
python -m medvision_bm.benchmark.eval__qwen2_5_vl --task_type TL ...

# 2 β€” parse model outputs into structured predictions
python -m medvision_bm.benchmark.parse_outputs --task_type TL --task_dir <dir> --model_dir <dir>

# 3 β€” summarize into metrics
python -m medvision_bm.benchmark.summarize_TL_task --task_dir <dir>

# 4 β€” optional: re-parse with the LLM judge for format-robust extraction
bash script/llm-parsing/run_llm_parsing.sh

Use it the same way for dataset download, environment setup and the SFT launchers.

Go deeper: /medvision

For maintainer-level work β€” the exact flag, pin or failure mode β€” the repo skill routes to one of ten sub-skills (environment-setup, dataset-and-tasks, benchmark-evaluation, results-parsing-and-metrics, llm-judge-parsing, sft, rft, analysis, extending-models-and-tasks, biomedparse-ablation):

/medvision  How do I add a new model across every site that needs registering?
/medvision  Why did my detection run OOM while building the plan?

See skills/README.md for the routing map, the 39 bundled helper scripts, and how to use the skills as documentation without installing anything.


πŸ“– Essential Dataset Concept

πŸ“š Read the Docs: Dataset concepts

We cover some essential concepts that help you use the MedVision dataset with ease.

Concepts: Dataset & Data Configuration

  • MedVision: the collection of public imaging data and our annotations
  • dataset: name of the public datasets, such as BraTS24, MSD, OAIZIB-CM
  • data-config: name of predefined subsets
    • naming convention: {dataset}_{annotation-type}_{task-ID}_{slice}_{split}
      • dataset: details
      • annotation-type:
        • BoxSize: detection annotations (bounding box)
        • TumorLesionSize: tumor/lesion size annotations
        • BiometricsFromLandmarks: angle/distance annotations
        • MaskSize: area / mask-size annotations
      • task-ID: Task[xx] (Note: this is a local ID in the dataset, not a global ID in MedVision.)
        • For datasets with multiple image-mask pairs, we defined tasks in medvision_ds/datasets/*/preprocess_*.py
        • source: medvision_ds
        • e.g., detection tasks for the BraTS24 dataset are defined in the benchmark_plan in medvision_ds/datasets/BraTS24/preprocess_detection.py
      • slice: [Sagittal, Coronal, Axial]
      • split: [Train, Test]

What's returned from MedVision Dataset?

We only share the annotations (https://huggingface.co/datasets/YongchengYAO/MedVision/tree/main/Datasets). The data loading script MedVision.py will handle raw image downloading and processing. The returned fields in each sample are defined as follows.

In MedVision.py, the class MedVision(GeneratorBasedBuilder) defines the feature dict and the method _generate_examples() builds the dataset.

Code block in `MedVision(GeneratorBasedBuilder)` (Click to expand)
"""
MedVision dataset.

NOTE: To update the features returned by the load_dataset() method, the followings should be updated:
        - the feature dict in this class 
        - the dict yielded by the _generate_examples() method 
"""

# The feature dict for the task:
# - Mask-Size
features_dict_MaskSize = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "mask_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "label": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "pixel_count": Value("uint32"),
    "ROI_area": Value("float16"),
}

# The feature dict for the task:
# - Box-Size
features_dict_BoxSize = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "mask_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "label": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "bounding_boxes": Sequence(
        {
            "min_coords": Sequence(Value("uint16"), length=2),
            "max_coords": Sequence(Value("uint16"), length=2),
            "center_coords": Sequence(Value("uint16"), length=2),
            "dimensions": Sequence(Value("uint16"), length=2),
            "sizes": Sequence(Value("float16"), length=2),
        },
    ),
}

features_dict_BiometricsFromLandmarks = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "landmark_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "biometric_profile": {
        "metric_type": Value("string"),
        "metric_map_name": Value("string"),
        "metric_key": Value("string"),
        "metric_value": Value("float16"),
        "metric_unit": Value("string"),
        "slice_dim": Value("uint8"),
    },
}

features_dict_TumorLesionSize = {
    "dataset_name": Value("string"),
    "taskID": Value("string"),
    "taskType": Value("string"),
    "image_file": Value("string"),
    "landmark_file": Value("string"),
    "mask_file": Value("string"),
    "slice_dim": Value("uint8"),
    "slice_idx": Value("uint16"),
    "label": Value("uint16"),
    "image_size_2d": Sequence(Value("uint16"), length=2),
    "pixel_size": Sequence(Value("float16"), length=2),
    "image_size_3d": Sequence(Value("uint16"), length=3),
    "voxel_size": Sequence(Value("float16"), length=3),
    "biometric_profile": Sequence(
        {
            "metric_type": Value("string"),
            "metric_map_name": Value("string"),
            "metric_key_major_axis": Value("string"),
            "metric_value_major_axis": Value("float16"),
            "metric_key_minor_axis": Value("string"),
            "metric_value_minor_axis": Value("float16"),
            "metric_unit": Value("string"),
        },
    ),
}
Code block in `_generate_examples` (Click to expand)
# Env var to disable per-sample quality filtering. When true, the size/cluster
# exclusion filters below are bypassed so every sample in the planner is returned.
# The distance/angle metric_type split (task partitioning) is always preserved.
disable_sample_filtering = (
    os.environ.get("MedVision_DISABLE_SAMPLE_FILTERING", "False").lower()
    == "true"
)
if disable_sample_filtering:
    logger.info(
        "MedVision_DISABLE_SAMPLE_FILTERING=true β€” quality/size sample filters bypassed"
    )

# Task type: Mask-Size
if taskType == "Mask-Size":
    flatten_slice_profiles = (
        MedVision_BenchmarkPlannerSegmentation.flatten_slice_profiles_2d
    )
    if imageSliceType.lower() == "sagittal":
        slice_dim = 0
    elif imageSliceType.lower() == "coronal":
        slice_dim = 1
    elif imageSliceType.lower() == "axial":
        slice_dim = 2
    slice_profile_flattened = flatten_slice_profiles(biometricData, slice_dim)
    for idx, case in enumerate(slice_profile_flattened):
        # Skip cases with a mask size smaller than 200 pixels
        if not disable_sample_filtering and case["pixel_count"] < 200:
            continue
        else:
            yield idx, {
                "dataset_name": dataset_name,
                "taskID": taskID,
                "taskType": taskType,
                "image_file": os.path.join(dataset_dir, case["image_file"]),
                "mask_file": os.path.join(dataset_dir, case["mask_file"]),
                "slice_dim": case["slice_dim"],
                "slice_idx": case["slice_idx"],
                "label": case["label"],
                "image_size_2d": case["image_size_2d"],
                "pixel_size": case["pixel_size"],
                "image_size_3d": case["image_size_3d"],
                "voxel_size": case["voxel_size"],
                "pixel_count": case["pixel_count"],
                "ROI_area": case["ROI_area"],
            }

# Task type: Box-Size
if taskType == "Box-Size":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerDetection.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            # Skip cases with multiple bounding boxes in the same slice
            if not disable_sample_filtering and len(case["bounding_boxes"]) > 1:
                continue
            # Skip cases with a bounding box size smaller than 10 pixels in any dimension
            elif not disable_sample_filtering and (
                case["bounding_boxes"][0]["dimensions"][0] < 10
                or case["bounding_boxes"][0]["dimensions"][1] < 10
            ):
                continue
            else:
                yield idx, {
                    "dataset_name": dataset_name,
                    "taskID": taskID,
                    "taskType": taskType,
                    "image_file": os.path.join(dataset_dir, case["image_file"]),
                    "mask_file": os.path.join(dataset_dir, case["mask_file"]),
                    "slice_dim": case["slice_dim"],
                    "slice_idx": case["slice_idx"],
                    "label": case["label"],
                    "image_size_2d": case["image_size_2d"],
                    "pixel_size": case["pixel_size"],
                    "image_size_3d": case["image_size_3d"],
                    "voxel_size": case["voxel_size"],
                    "bounding_boxes": case["bounding_boxes"],
                }

# Task type: Biometrics-From-Landmarks
if taskType == "Biometrics-From-Landmarks":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            yield idx, {
                "dataset_name": dataset_name,
                "taskID": taskID,
                "taskType": taskType,
                "image_file": os.path.join(dataset_dir, case["image_file"]),
                "landmark_file": os.path.join(
                    dataset_dir, case["landmark_file"]
                ),
                "slice_dim": case["slice_dim"],
                "slice_idx": case["slice_idx"],
                "image_size_2d": case["image_size_2d"],
                "pixel_size": case["pixel_size"],
                "image_size_3d": case["image_size_3d"],
                "voxel_size": case["voxel_size"],
                "biometric_profile": case["biometric_profile"],
            }

# Task type: Biometrics-From-Landmarks-Distance
if taskType == "Biometrics-From-Landmarks-Distance":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            if case["biometric_profile"]["metric_type"] == "distance":
                yield idx, {
                    "dataset_name": dataset_name,
                    "taskID": taskID,
                    "taskType": taskType,
                    "image_file": os.path.join(dataset_dir, case["image_file"]),
                    "landmark_file": os.path.join(
                        dataset_dir, case["landmark_file"]
                    ),
                    "slice_dim": case["slice_dim"],
                    "slice_idx": case["slice_idx"],
                    "image_size_2d": case["image_size_2d"],
                    "pixel_size": case["pixel_size"],
                    "image_size_3d": case["image_size_3d"],
                    "voxel_size": case["voxel_size"],
                    "biometric_profile": case["biometric_profile"],
                }

# Task type: Biometrics-From-Landmarks-Angle
if taskType == "Biometrics-From-Landmarks-Angle":
    if imageType.lower() == "2d":
        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            if case["biometric_profile"]["metric_type"] == "angle":
                yield idx, {
                    "dataset_name": dataset_name,
                    "taskID": taskID,
                    "taskType": taskType,
                    "image_file": os.path.join(dataset_dir, case["image_file"]),
                    "landmark_file": os.path.join(
                        dataset_dir, case["landmark_file"]
                    ),
                    "slice_dim": case["slice_dim"],
                    "slice_idx": case["slice_idx"],
                    "image_size_2d": case["image_size_2d"],
                    "pixel_size": case["pixel_size"],
                    "image_size_3d": case["image_size_3d"],
                    "voxel_size": case["voxel_size"],
                    "biometric_profile": case["biometric_profile"],
                }

# Task type: Tumor-Lesion-Size
if taskType == "Tumor-Lesion-Size":
    if imageType.lower() == "2d":
        # Get the target label for the task
        target_label = benchmark_plan["tasks"][int(taskID) - 1]["target_label"]

        flatten_slice_profiles = (
            MedVision_BenchmarkPlannerBiometry_fromSeg.flatten_slice_profiles_2d
        )
        if imageSliceType.lower() == "sagittal":
            slice_dim = 0
        elif imageSliceType.lower() == "coronal":
            slice_dim = 1
        elif imageSliceType.lower() == "axial":
            slice_dim = 2
        slice_profile_flattened = flatten_slice_profiles(
            biometricData, slice_dim
        )
        for idx, case in enumerate(slice_profile_flattened):
            if not disable_sample_filtering:
                n_total_clusters = case["n_total_clusters"]
                if n_total_clusters is not None:
                    # New JSON (v1.1.0+): filter on raw cluster count
                    if n_total_clusters > 1:
                        continue
                else:
                    # Old JSON (v1.0.0): fall back to above-threshold cluster count
                    if len(case["biometric_profile"]) > 1:
                        continue
            yield idx, {
                "dataset_name": dataset_name,
                "taskID": taskID,
                "taskType": taskType,
                "image_file": os.path.join(dataset_dir, case["image_file"]),
                "mask_file": os.path.join(dataset_dir, case["mask_file"]),
                "landmark_file": os.path.join(
                    dataset_dir, case["landmark_file"]
                ),
                "slice_dim": case["slice_dim"],
                "slice_idx": case["slice_idx"],
                "label": target_label,
                "image_size_2d": case["image_size_2d"],
                "pixel_size": case["pixel_size"],
                "image_size_3d": case["image_size_3d"],
                "voxel_size": case["voxel_size"],
                "biometric_profile": case["biometric_profile"],
            }


Dataset Building Workflow

Workflow

MedVision Dataset Building Workflow

There are a few ways to control the dataset loading and building behavior:

  • Rebuild Dataset (Arrow files): Use the download_mode argument in load_dataset() (docs).
    • [1] Set download_mode="force_redownload" to ignore the cached Arrow files and trigger the data loading script MedVision.py to rebuild the dataset.
  • Redownload Raw Data:
    • [2] MedVision_FORCE_DOWNLOAD_DATA: Set this environment variable to True to force re-downloading raw images and annotations.
    • [3] .downloaded_datasets.json: This tracker file records downloaded status. Removing a dataset's entry here will trigger a re-download of the raw data for that dataset.

[!TIP] How to properly update/redownload raw data?

If you need to update raw data (images, masks, landmarks) using [2] or [3], you MUST ALSO use [1] (download_mode="force_redownload").

Why? Because if Hugging Face finds a valid cached dataset (Arrow files), it will load it directly and skip running the script entirely. Without running the script, the environment variable [2] or tracker file [3] will never be checked.

Summary:

  • Update Arrow/Fields only: Use [1].
  • Update Raw Data: Use [1] AND ([2] or [3]).

πŸ”₯ We will maintain a change log for essential updates.


Examples

Running this for the first time will download the raw data and build the dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
    )
Running the same script again will use the cached dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
    )
Adding `download_mode="force_redownload"` will skip raw data downloading and rebuild the dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
        download_mode="force_redownload",
    )
Adding `download_mode="force_redownload"` and `os.environ["MedVision_FORCE_DOWNLOAD_DATA"] = "True"` will redownload raw data and rebuild the dataset
import os
from datasets import load_dataset

# Set data folder
wd = os.path.join(os.getcwd(), "Data-testing")
os.makedirs(wd, exist_ok=True)
os.environ["MedVision_DATA_DIR"] = wd

# Pick a dataset config name and split
config = "OAIZIB-CM_BoxSize_Task01_Axial_Test"
split_name = "test" # use "test" for testing set config; use "train" for training set config 

# Force redownload
os.environ["MedVision_FORCE_DOWNLOAD_DATA"] = "True"

# Get dataset
ds = load_dataset(
        "YongchengYAO/MedVision",
        name=config,
        trust_remote_code=True,
        split=split_name,
        download_mode="force_redownload",
    )

Download Mode in MedVision Dataset

(Advanced) Understand how the customized dataset loading script `MedVision.py` changes the behavior of `download_mode` in `load_dataset()`
  • download_mode can be one of these: "reuse_dataset_if_exists" (default), "reuse_cache_if_exists", "force_redownload"

  • Default behavior of download_mode in load_dataset():

    DownloadsDataset
    reuse_dataset_if_exists (default)ReuseReuse
    reuse_cache_if_existsReuseFresh
    force_redownloadFreshFresh
  • download_mode in MedVision dataset:

    DownloadsDataset
    reuse_dataset_if_exists (default)ReuseReuse
    reuse_cache_if_existsReuseFresh
    force_redownload (MedVision_FORCE_DOWNLOAD_DATA=False)ReuseFresh
    force_redownload (MedVision_FORCE_DOWNLOAD_DATA=True)FreshFresh

πŸ”₯ Summary: Understanding the download mode of MedVision dataset


πŸ’Ώ Batch Download (Optional)

πŸ“š Read the Docs: Loading data β†’ batch download Β· CLI reference

Since data downloading and processing take time, you can download datasets from the tasks list or configs list in advance.

[!WARNING] You need to set an API token for these datasets (see detailed instructions): FeTA24, SKM-TEA, and ToothFairy2

[!NOTE] Per-slice QC figures are opt-in (MedVision_DOWNLOAD_QC_FIGURES, default False).

export MedVision_DOWNLOAD_QC_FIGURES="True"   # default: "False"

The QC figures are review material β€” nothing in the loader reads them, and no task needs them β€” but they are ~99% of the annotation payload (298 GB of PNG against 3 GB of annotation). Until v1.4.0 they shipped inside Datasets/<dataset>.zip, which pushed BraTS24.zip to 72.6 GB and MSD.zip to 51.4 GB β€” past Hugging Face's 50 GB per-file limit, a hard publish failure. Since v1.4.0 they ship in their own archives: Datasets/<dataset>_fig.zip, or Datasets/<dataset>_fig.partNN.zip where a single archive would again clear 50 GB.

Set the variable to True and the figures are restored to exactly the paths they occupied before v1.4.0 β€” the archives carry the same arcnames, so nothing is relocated. Details worth knowing:

  • Checked on every load. Setting the flag on a machine whose annotations are already present still fetches the figures β€” and only the figures: the image, landmark and planner downloads stay step 3's decision, so nothing else is re-pulled.
  • Tracked per annotation version. .downloaded_datasets.json records "qc_figures_<dataset>": "1.4.0" β€” the biometry version the figures belong to. A release that regenerates a dataset's biometry re-fetches its figures; a release that leaves it alone does not.
  • Never re-downloads what is already on disk. With no usable recorded version β€” any pre-v1.4.0 install already holds every figure β€” the directory itself is read and its version written back. Datasets that publish no figures (roughly half) record the attempt too, so they do not re-query the Hub on every load.
  • Retry with MedVision_FORCE_DOWNLOAD_DATA=True, which also forces figures already on disk to be fetched again. Shards are independent zips (not zip -s volumes), so they extract in any order and a missing one costs only its own figures.

[!TIP] Command:

python -m medvision_bm.benchmark.download_datasets

Arguments:

  • --data_dir: (required) data folder
  • --tasks_json: task json file
  • --configs_csv: config csv file
  • --force_download_data: (store_true) force redownload raw imaging data ⚠️ for debugging only; it will repeatedly download data for tasks/configs of the same dataset

Download from a task-list JSON (replace <task-list-json>, <data-folder>):

python -m medvision_bm.benchmark.download_datasets \
--tasks_json <task-list-json> \
--data_dir <data-folder>

Or from a configs CSV (replace <config-list-csv>, <data-folder>):

python -m medvision_bm.benchmark.download_datasets \
--configs_csv <config-list-csv> \
--data_dir <data-folder>

πŸ“œ License

MedVision is released under the Creative Commons Attribution 4.0 International (CC-BY 4.0) license. Users are permitted to utilize, adapt, and build upon this dataset for both academic and commercial purposes, provided that appropriate credit is given. MedVision is a meta-dataset built upon various publicly available source datasets. While the annotations provided by MedVision are covered by the CC-BY 4.0 license, any downstream application must continue to comply with the specific usage terms and licensing requirements stipulated by the curators of the original raw imaging data. It is the responsibility of the user to ensure that their application of this data aligns with the license agreements of all constituent source datasets.


🩡 Acknowledgement

This work was supported by

  • The United Kingdom Research and Innovation (grant EP/S02431X/1), UKRI Centre for Doctoral Training in Biomedical AI at the University of Edinburgh, School of Informatics
  • The Edinburgh International Data Facility (EIDF) and the Data-Driven Innovation Programme at the University of Edinburgh.

MedVision is based on some open-source projects:


Contributors

YongchengYAO

339 commits

Languages

Python

73.6%

Shell

22.1%

Jupyter Notebook

4.3%