Adding a new task#
A “task” in MedVision is one evaluation configuration: a dataset split, the prompt
that gets shown to the model, and the metrics computed on its answer. Tasks are
defined declaratively as YAML files (the benchmark harness is a vendored fork of
lmms-eval) that point at Python hook functions. Adding a task therefore means
writing a couple of YAML files and, when the behaviour is genuinely new, a few
hook functions.
See also
Full code-level guide in the repository: docs/New-Tasks-Guide.md.
Before starting, it helps to understand what a sample actually contains — see Dataset concepts. If you are wiring up a new model rather than a new task, go to Adding a new model instead.
Folder layout#
Every dataset gets its own folder under the harness task tree:
src/medvision_bm/medvision_lmms_eval/lmms_eval/tasks/
├── <dataset>/
│ ├── utils.py # per-dataset entry points
│ ├── <dataset>_<task_type>_base-CoT.yaml # base config (shared knobs)
│ └── <dataset>_<task_type>_<num>_<slice>-CoT.yaml # concrete task instances
└── medvision/
├── medvision_utils.py # all shared hook logic
├── lmms_eval_specific_kwargs.yaml # shared prompt kwargs
└── metadata.yaml
Two YAML layers keep things DRY:
Base config (
<dataset>_<task_type>_base-CoT.yaml) holds everything shared across variants of the same task type:dataset_path(the HF ID, alwaysYongchengYAO/MedVision),output_type: generate_until, the four hook bindings, and themetric_list. It pulls in the shared kwargs and metadata viainclude:.Task instance (
<dataset>_<task_type>_<num>_<slice>-CoT.yaml) is tiny — itinclude:s the base config and sets justtask:(the unique task ID) anddataset_name:(the concrete HF subset/split to load).
The per-dataset utils.py is a thin shim: it imports the real implementations
from medvision_utils.py and, for the factory-style prompt builders, binds the
dataset’s own preprocessing module. YAML !function utils.<name> references
resolve against that file.
The four hook functions#
Each base YAML binds four hooks (plus the metric aggregators). All live in
medvision_utils.py:
doc_to_visual— loads the requested NIfTI slice for a sample and returns it as a PIL image (the visual input).doc_to_text— builds the text prompt. These are produced by factory functions namedcreate_doc_to_text_<TaskType>(...)that take the dataset’s preprocessing module and return the actual closure. Each task type has a family of factories, for example:create_doc_to_text_BoxCoordinate_CoTfor detection;create_doc_to_text_TumorLesionSize_CoTfor size.doc_to_target— returns the ground-truth string for the sample (doc_to_target_BoxCoordinate,doc_to_target_TumorLesionSize,doc_to_target_BiometricsFromLandmarks,doc_to_target_MaskSize).process_results— parses the model’s raw generation and computes the per-sample metrics (process_results_BoxCoordinate,process_results_TumorLesionSize,process_results_BiometricsFromLandmarks).
The metric_list then names aggregators that reduce the per-sample values across
the split, e.g. aggregate_results_MAE, aggregate_results_MRE,
aggregate_results_avgMAE, aggregate_results_avgMRE,
aggregate_results_SuccessRate, and aggregate_results_NMAE. Pick the ones that
match the task: detection uses the avgMAE / avgMRE / SuccessRate family,
while the metric-geometry tasks use MAE / MRE (and NMAE for size).
Task-type labels and naming#
The <task_type> token in every filename must be one of the fixed labels — the
harness and the downstream parse/summarize steps key off it:
Label |
Task |
|---|---|
|
Detection (bounding boxes) |
|
Tumour/lesion size estimation (mm) |
|
Angle / distance measurement |
|
Area estimation (new / preview) |
Follow the two-tier naming exactly:
base config →
<dataset>_<task_type>_base-CoT.yamltask instance →
<dataset>_<task_type>_<num>_<slice>-CoT.yaml(for exampleBraTS24_BoxCoordinate_Task01_Axial-CoT.yaml)
Note
A task instance’s dataset_name: can read differently from its task-type label —
for example a BoxCoordinate task may load a ..._BoxSize_..._Test subset. That
naming bridge is expected; keep the file’s <task_type> label canonical.