# CMDM
**Repository Path**: Louispine/cmdm
## Basic Information
- **Project Name**: CMDM
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-14
- **Last Updated**: 2026-09-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# CMDM: Controllable Mask Diffusion Model for Medical Annotation Synthesis with Semantic Information Extraction
[[Paper on ScienceDirect](https://www.sciencedirect.com/science/article/pii/S0010482525011588)]
This is the official implementation of the paper “Controllable Mask Diffusion Model for Medical Annotation Synthesis with Semantic Information Extraction”, published in **Computers in Biology and Medicine**.
## Table of Contents
- [Requirements](#requirements)
- [Dataset Preparation](#dataset-preparation)
- [Training Your Own CMDM](#training-your-own-cmdm)
- [Sampling with CMDM](#sampling-with-cmdm)
- [Acknowledgement](#acknowledgement)
- [Citations](#citations)
## Requirements
```bash
conda create -n CMDM python=3.8.10
conda activate CMDM
conda install pytorch==1.11.0 torchvision==0.12.0 torchaudio==0.11.0 cudatoolkit=11.3 -c pytorch
```
## Dataset Preparation
The proposed framework requires medical annotatoin data.
Please organize the dataset with the following structure:
```angular2
├── ${data_root}
│ ├── ${train_maskdataset_dir}
│ │ ├── mask_1.png
│ │ ├── mask_2.png
│ │ ├── mask_3.png
```
## Training Your Own CMDM
To train your own CMDM, Run the following command:
```bash
python train.py --data_path ./TrainDataset \
--image_size 256 \
--n_epoch 2000 \
--n_T 500 \
--batch_size 2 \
```
### Mask-stage ablation variants
**Correction:** the historical `--variant original_cmdm` below is a four-stage,
shared-backbone CMDM-conditioning comparator. It is NOT the official default
256px CMDM network. It is retained unchanged so old checkpoints/results remain
reproducible. Use the separate upstream entry below for original-model training.
Use one `--variant` value for each independent training run. Named variants
override the related low-level flags and are stored in the checkpoint.
| Variant | Heatmap | Morphology conditioning | Foreground loss | Controls |
| --- | --- | --- | --- | --- |
| `original_cmdm` | no | original size/location multiplication | 1 | area, centroid x/y |
| `ours_wo_heatmap` | no | full-network FiLM | 10 | paper 8-D descriptor |
| `ours_wo_film` | yes | none | 10 | heatmap position only |
| `ours_wo_fg` | yes | full-network FiLM | 1 | paper 8-D descriptor |
| `ours_full` | yes | full-network FiLM | 10 | paper 8-D descriptor |
| `legacy` | CLI flags | historical repository path | CLI flag | shape ID + descriptor |
### Pinned official CMDM training (separate from Ours)
`train_original_cmdm.py` directly imports the upstream model, diffusion, dataset,
preprocessing and defaults from `official_cmdm/`, pinned to
[DSLab-MJU/CMDM commit 15f69578e6de74b26b447da2d0209340d6e84044](https://github.com/DSLab-MJU/CMDM/tree/15f69578e6de74b26b447da2d0209340d6e84044).
`official_cmdm/UPSTREAM.json` records normalized-source SHA256 hashes; these are
checked before every invocation. No downloads are needed on the cloud server.
Do not run the vendored `train.py` directly: the upstream script does not save
checkpoints. Deploy the entire `official_cmdm/` directory with the wrapper.
The default model is 256px, base channels 128, two residual blocks, channel
multipliers `1,1,2,2,4,4`, and attention resolutions `16,8`. Controls are
`[area/(H*W), x/W, y/H]`. The upstream size/location multiplication, ordinary
noise MSE, Adam at `1e-4`, linear epoch learning-rate decay, batch size 2,
500 diffusion steps and condition-drop probability 0.1 are retained. No Ours
FiLM/heatmap/foreground weighting, augmentation or validation-based selection
is inserted. The wrapper only adds seeding, preflight, provenance, epoch-boundary
resume and atomic checkpoint writes. Runtime versions are recorded; identical
results across different CUDA/PyTorch versions or seeds are not promised.
The upstream `range(n_epoch+1)` is intentionally retained: `--n_epoch 2000`
prints 2001 epochs, of which the last has LR=0. `latest.pt` is the terminal model;
`best.pt` is an additional epoch-mean-training-loss choice, not an upstream
validated controllability selection. Report which checkpoint you evaluate.
**Data gate:** upstream produces one label per valid connected component but
indexes images by image number. For this published single-condition training
entry to have aligned pairs, every image must contain exactly one component
with area >= `st=100`, AFTER upstream processing (BGR green channel, bilinear
resize, threshold 127). The wrapper rejects zero/multiple valid components,
without dropping images, selecting a largest component, or changing masks.
Additional sub-threshold foreground remains exactly as upstream. This is a
safety gate, NOT a claim that the whole CMDM paper cannot handle multiple objects.
If preflight fails, agree on and disclose a data adaptation before training;
do not reproduce the upstream flattening/misalignment bug as an experiment.
One additional dependency relative to the current project is `blobfile`,
imported by the unchanged upstream preprocessing module:
```bash
python -m pip install blobfile
python train_original_cmdm.py --data_path dataset/scratch \
--out_dir checkpoints/scratch_official_cmdm_seed0 --check_only
```
Only after preflight passes, start the official-size model on GPU 0:
```bash
CUDA_VISIBLE_DEVICES=0 python -u train_original_cmdm.py \
--data_path dataset/scratch \
--out_dir checkpoints/scratch_official_cmdm_seed0 --seed 0
```
To test one full epoch without changing the 2000-epoch LR budget, add
`--stop_after_epoch 1`, then continue in the same directory with
`--resume_path checkpoints/scratch_official_cmdm_seed0/latest.pt` and remove
`--stop_after_epoch`. For a second independent seed, use GPU 1, `--seed 1` and a
different output directory. Never resume the historical four-stage comparator.
`--smoke` is a separate tiny 64px/32-channel/2-diffusion-step integration test;
it is NOT an original-model result and cannot resume into the full protocol.
Only `latest.pt`, `best.pt`, and `provenance.json` are retained by default.
Saving uses a temporary file followed by replacement; budget space for these
checkpoints PLUS one temporary checkpoint per running job. A failed write keeps
the previous complete target. If no epoch was successfully saved, no recovery
checkpoint exists. Interruptions lose progress since the last completed epoch.
The existing `experiment_cont_sweep.py` can load these checkpoints and use the
saved upstream three-dimensional training bank (no nearest-neighbor relabeling
of training images). Evaluate only `area,centroid_x,centroid_y`. This tests the
generator; it does NOT reproduce the paper's medical datasets, downstream
segmentation experiments or KDE/GPR population sampling protocol.
For the FiLM variants, each descriptor dimension is fitted on the training set
and mapped to `[0, 1]` before it enters the FiLM MLP. The canonical
centroid coordinates are kept unchanged for Gaussian-heatmap construction, and
classifier-free conditioning dropout explicitly replaces the model condition
with an all-zero vector after normalization. The
fitted ranges are stored in `condition_norm_stats` inside every checkpoint.
Checkpoints trained before this change still load in their original unscaled
condition space, but must not be resumed as a newly normalized run.
The paper descriptor is ordered as `centroid_x, centroid_y, area,
eccentricity, solidity, circularity, orientation_sin, orientation_cos`.
`aspect_ratio`, `convexity`, `extent`, and `perimeter` are still extracted for
post-generation diagnostics but are not model conditions. Named variants also
fix a four-stage U-Net (`channel_mult=1,2,3,4`) with attention in the bottleneck,
matching the mask-generator diagram in the paper.
Example:
```bash
python train.py --data_path dataset/scratch \
--variant ours_full --seed 0 \
--image_size 256 --n_epoch 2000 --n_T 500 --batch_size 2 \
--save_dir checkpoints/scratch/ours_full_seed0.pt
```
For repeated experiments, run every named variant independently with seeds
`0`, `1`, and `2`; use a unique checkpoint filename for every pair.
The controllability sweep reads the variant, normalization mode, image size,
and component threshold from the checkpoint:
```bash
python experiment_cont_sweep.py \
--checkpoint checkpoints/scratch/ours_full_seed0.pt \
--data_path dataset/scratch \
--sweep_features area,centroid_x,centroid_y,eccentricity,orientation \
--n_steps 7 --n_repeats 50 --seed 42 --paired_noise \
--out_dir paper_figures/scratch/ours_full_seed0
```
The sweep uses an observed training descriptor medoid as its baseline by
default, avoiding the potentially invalid synthetic vector produced by taking
each descriptor's mean independently. `--paired_noise` reuses the same initial
and reverse-process noise for each column across condition values, making the
conditional effect easier to isolate.
For `original_cmdm`, only sweep `area,centroid_x,centroid_y`.
## Corrected labels and controllability training protocol
For revision work, the augmentation/validation recipe below is **optional** and
does not reproduce historical experimental settings. The two-GPU launcher in
the next section leaves those additions off. Both launchers use the current
corrected implementation; neither makes old 12-D results equivalent to new 8-D results.
The model still consumes the paper's eight values. Perimeter is only an
intermediate measurement for circularity (and an optional diagnostic). Feature
extraction version 2 computes all quantities on the same largest pixel-connected
object. Binary masks are resized with nearest-neighbor interpolation.
Choose the target explicitly: `--component_policy main` trains only the largest
object, matching its descriptor; `--component_policy all` retains all foreground
but the descriptor still describes the largest object. The default is `all` for
existing command-line compatibility. Original input files are never rewritten.
Validation reports both total area and largest-object area, and component counts.
Orientation is an unoriented major axis in `[-90, 90)` degrees. Its error uses a
180-degree period. Sampling perturbs the angle and reconstructs its sin/cos pair.
Angle perturbations outside fitted sin/cos support fall back to the source angle
instead of clipping the two components independently. Linear trend diagnostics
use one shared angular branch, avoiding a false slope from per-target wrapping;
axial-angle MAE is the primary direction-accuracy measure.
Sweep plots default to the observed training angle P10-P90 (override with
`--orientation_range=-60,60`). Generated shapes with eccentricity below
`--orientation_min_ecc 0.1` have unstable axes and are reported separately.
Training can split sources before augmentation using `--val_fraction 0.2` and
`--split_seed 0`. Identical preprocessed masks stay in the same split. If files
already contain crops, rotations, or related acquisitions, supply
`--split_groups groups.json`: a JSON mapping **every filename** to its original
source/family ID. Related images cannot be inferred reliably from arbitrary
filenames. Keep a separate final test set outside this training directory.
`--augment_copies 4` retains originals and creates four deterministic affine
copies per training source. Controls are remeasured after nearest-neighbor
rotation, isotropic scaling, and translation. Transforms that crop foreground
are retried; an untransformable sample falls back to its original, and the
fallback count is recorded. This is a fixed bank, not a newly sampled transform
each epoch. FiLM min/max statistics are fitted **only on this training bank**;
validation sources are never used. The bank, split, hashes, feature order, and
normalization contract are stored in the checkpoint. Split and augmentation
seeds are independent of the model seed, allowing identical data across ablations.
New checkpoints supply their stored training bank to automatic sampling and
exploratory sweeps even without `--data_path`; this prevents accidental use of
held-out files when recomputing descriptor ranges.
For a first full-model pilot on a single GPU (change the dataset path):
```bash
python train.py --data_path dataset/scratch --variant ours_full --seed 0 \
--component_policy main --image_size 256 --batch_size 2 --n_T 500 \
--val_fraction 0.2 --split_seed 0 \
--augment_copies 4 --augment_seed 0 \
--augment_scale_min 0.7 --augment_scale_max 1.3 \
--augment_rotate_deg 90 --augment_translate_frac 0.2 \
--max_steps 50000 --lrate 1e-4 --dp 0.1 \
--val_every_steps 5000 --val_baselines 1 --val_steps 3 --val_repeats 1 \
--val_guide_w 1 --best_metric control --early_stop_patience 0 \
--save_dir checkpoints/scratch_v2/ours_full_seed0.pt
```
This enables a **small, noisy pilot validation**: one held-out baseline, three
values for each of seven physical controls, and one paired-noise repeat (22
masks per check for paper-8). Increase `--val_baselines` and `--val_repeats` when
selecting the final experimental protocol; validation cost grows proportionally
and each mask uses the full trained diffusion schedule. P10-P90 one-factor
sweeps can still form out-of-distribution *joint* combinations; the observed
held-out descriptors are evaluated separately and range violations are logged.
With `--max_steps`, the optimizer-update budget and step-based linear LR decay
override the epoch limit. Without it, epoch-based training is retained. Training
loss is now a sample-weighted epoch mean; the progress-bar EMA is diagnostic only.
Validation saves/restores RNG states and model mode, so running validation does
not change later training noise or conditional-dropout draws. Checkpoints store
optimizer step, epoch/batch position, loader state, and RNG states for resuming.
`--save_every_steps N` additionally saves periodic step checkpoints. Resuming
requires unchanged data, normalization, training budget, and validation settings.
Outputs for `ours_full_seed0.pt`:
- `ours_full_seed0.pt`: latest training state.
- `ours_full_seed0_best_control.pt`: lowest validation control score.
- `ours_full_seed0_best.pt`: the selected criterion (`--best_metric control`
uses the same checkpoint as `best_control`; default `train_loss` uses epoch loss).
- `ours_full_seed0_data_manifest.json`: source split, hashes, augmentation settings.
- `ours_full_seed0_validation/step_*.json`: targets, measured descriptors, errors,
total/main area, failed masks, and control summaries.
- `ours_full_seed0_validation/step_*_masks/`: generated validation masks, without
component filtering or position correction.
The selection score is an **evaluation-only heuristic**, not an additional loss:
average capped per-feature absolute error divided by training min/max span,
with axial-angle error divided by 90 degrees and invalid masks penalized by 1;
observed and each sweep group receive equal weight. Constant scalar dimensions
are effectively exact-match constraints. Record this rule in the experiment
protocol; scores with different descriptor sets (Original CMDM versus Ours) are
not directly comparable. Sidecar files also report target MAE, trend, off-target
errors, failures, and the rate within 10% of the normalization scale. The old
P10-P90 range-occupancy metric is **not** target success. Reference lines now
default to `y=x`. `--early_stop_patience`, if used with control selection, counts
validation checks; with train-loss selection it counts epochs.
Compare existing CFG weights without retraining, using exactly the checkpoint's
held-out conditions and noise seeds:
```bash
python experiment_cfg_sweep.py \
--checkpoint checkpoints/scratch_v2/ours_full_seed0_best_control.pt \
--guide_weights 0,0.5,1,2,3 --out_dir paper_figures/scratch_v2/cfg_seed0
```
`guide_w=0` is conditional sampling; the effective conventional CFG scale is
`1 + guide_w`. The runner writes `cfg_summary.csv`, detailed JSON, and masks.
Use validation to choose a weight and report performance on untouched test data.
To prepare five one-factor pilot jobs (base LR/dropout, two alternative LRs, two
alternative dropout probabilities), first preview commands:
```bash
python run_control_experiments.py --mode pilot --data_path dataset/scratch \
--component_policy main --seeds 0 --out_dir checkpoints/scratch_v2/pilot
```
Add `--execute` to run sequentially. After fixing the shared protocol, use
`--mode ablation --seeds 0,1,2` for the 15 variant/seed jobs, supplying the chosen
`--lrate`, `--dp`, and `--val_guide_w`. Every job uses the same split, augmentation
bank, batch size, and optimizer-update budget; output names include variant,
seed, LR, and dropout. Existing checkpoints are not overwritten by the launcher.
Corrected feature extraction changes some labels even for an eight-dimensional
checkpoint. Older checkpoints remain loadable for inference, but resuming them
under the new data protocol is rejected; start a new training run and output
directory. Structural/route tests do not establish achieved controllability.
## One-seed controlled improvement pilot
For exactly one new `ours_full / seed 0` run with unchanged model and loss,
use `run_single_seed_pilot.py`, not the five-job hyperparameter pilot above.
It verifies the reference checkpoint and source-mask hashes, performs CPU
preflight, preserves model/optimizer settings, enables train-only relabeled
augmentation and held-out selection, and disables periodic checkpoint archives.
See [SINGLE_SEED_PILOT.md](SINGLE_SEED_PILOT.md) for the frozen protocol,
cloud commands, output paths, and same-plan old/new CFG comparison caveats.
## Two-GPU revision experiments
Use one independent training job per GPU; no DDP or global-batch change is
required. `train.py` respects external `CUDA_VISIBLE_DEVICES`. For example,
`CUDA_VISIBLE_DEVICES=1 python train.py ...` uses physical/assigned GPU 1 as
logical `cuda:0`. Use the IDs/UUIDs assigned by your cloud environment.
The revision launcher defaults to two short jobs: Full seed 0 and Original CMDM
seed 0, each for 10 epochs. Model size, diffusion steps, and per-job batch match
the normal run. These only test GPU memory, training, and checkpoint I/O.
```bash
python run_revision_experiments.py --data_path dataset/scratch \
--out_dir checkpoints/scratch_revision_smoke --gpus 0,1 --preset smoke
```
This previews the commands. Add `--execute` to start both jobs. The launcher
checks GPU visibility, writes a log per experiment under `logs/`, and schedules
at most one training process per GPU. Failed jobs stop further dispatch; jobs
already running on other GPUs are allowed to finish. Use a new/empty output
directory to prevent overwriting older experiments. `experiment_plan.json`
records the commands, and `experiment_status.json` records completion states.
After the smoke tests, run only the first two full-length jobs if desired:
```bash
python run_revision_experiments.py --data_path dataset/scratch \
--out_dir checkpoints/scratch_revision_first_pair --gpus 0,1 \
--preset ablation --variants ours_full,original_cmdm --seeds 0 --execute
```
The ablation preset defaults to 2000 epochs. Without `--variants`/`--seeds`, it
queues all five variants and seeds 0,1,2 (15 jobs). Do not run that full queue
automatically if the first pair still needs inspection. To run only the remaining
13 jobs after accepting that pair, finish seed 0's three missing variants in one
fresh directory, then all five variants at seeds 1,2 in another:
```bash
python run_revision_experiments.py --data_path dataset/scratch \
--out_dir checkpoints/scratch_revision_remaining_seed0 --gpus 0,1 \
--preset ablation --variants ours_wo_heatmap,ours_wo_film,ours_wo_fg \
--seeds 0 --execute
python run_revision_experiments.py --data_path dataset/scratch \
--out_dir checkpoints/scratch_revision_seeds12 --gpus 0,1 \
--preset ablation --seeds 1,2 --execute
```
Every revision-launcher job explicitly disables new augmentation, validation
splitting, and the step-budget override. It retains all mask components by
default, trains by epoch, and selects `best.pt` using the current epoch-mean
training loss. This is a **controlled supplementary run of corrected code**,
not exact reproduction of the historical EMA-selected/12-D experiment. Keep
existing image-generation and downstream results separate until their model
provenance is checked. Short smoke checkpoints cannot resume as 2000-epoch
runs because their LR schedules differ; use the new full-length output directory.
Monitor on Linux with `nvidia-smi` and, for example,
`tail -f checkpoints/scratch_revision_smoke/logs/ours_full_seed0.log`.
GPU utilization and speed must be verified on the cloud host; local tests of the
launcher do not validate the availability of two physical cloud GPUs.
## Sampling with CMDM
To sample with CMDM, set the number of samples you need via ```n_samples``` in ```sampling_si.py```, then run:
```bash
python sampling_mask.py
```
As proposed in the paper, ```sampling_si.py``` first analyzes the correlations among semantic information (e.g., size and location) within the given annotation masks, and ```sampling_mask.py``` then generates the corresponding annotation masks.
## Acknowledgement
This repository is based on [LDM](https://github.com/CompVis/latent-diffusion), [guided-diffusion](https://github.com/openai/guided-diffusion), [CFG](https://github.com/TeaPearce/Conditional_Diffusion_MNIST) and [SDM](https://github.com/WeilunWang/semantic-diffusion-model). We sincerely thank the original authors for their valuable contributions and outstanding work.
## Citations
```
@article{heo2025controllable,
title={Controllable Mask Diffusion Model for medical annotation synthesis with semantic information extraction},
author={Heo, Chanyeong and Jung, Jaehee},
journal={Computers in Biology and Medicine},
volume={196},
pages={110807},
year={2025},
publisher={Elsevier}
doi={https://doi.org/10.1016/j.compbiomed.2025.110807}}
```