Skip to content

crackmnist_eccv.run_experiment

Run CrackMNIST pollution experiments across seeds, intensities, and splits.

Trains a UNet model under controlled input pollution and evaluates average Dice loss on train/val/test splits across multiple random seeds. Results are written incrementally to a CSV file named after the experiment configuration.

Functions:

Name Description
evaluate

Run model in eval mode and return average DiceLoss over the loader.

fix_random_seeds

Fix all relevant random seeds for reproducibility.

get_pollution_config

Return keyword arguments for apply_per_image_pollution.

parse_python_literal

Convert a string like '[0.1,0.2]' or '[(0.1,0.2)]' to a Python object.

train_and_evaluate

Train a UNet and evaluate on val and test sets.

Attributes:

Name Type Description
BATCH_SIZE
INTENSITY_RANGE
LR
NAMING_CONSTANT
NUM_EPOCHS
NUM_SEEDS
PIXELS
RADIUS
SELECTED_POLLUTION_TYPE
SIZE
TEST_PERC_RANGE
TRAIN_PERC_RANGE
args
avg_test_loss
avg_train_loss
avg_val_loss
data_transform
device
df
parser
results
start
std_test_loss
std_train_loss
std_val_loss
target_transform
test_dataset_full
test_loader_full
test_losses_seeds
train_dataset_full
train_loader_full
train_losses_seeds
val_dataset_full
val_loader_full
val_losses_seeds

Attributes

BATCH_SIZE module-attribute

BATCH_SIZE = 32

INTENSITY_RANGE module-attribute

INTENSITY_RANGE = parse_python_literal(args.intensity_range, list)

LR module-attribute

LR = 0.0001

NAMING_CONSTANT module-attribute

NAMING_CONSTANT = f'{PIXELS}_F9_{SELECTED_POLLUTION_TYPE}_radius{RADIUS}'

NUM_EPOCHS module-attribute

NUM_EPOCHS = 10

NUM_SEEDS module-attribute

NUM_SEEDS = 10

PIXELS module-attribute

PIXELS = args.pixels

RADIUS module-attribute

RADIUS = args.radius

SELECTED_POLLUTION_TYPE module-attribute

SELECTED_POLLUTION_TYPE = args.pollution_type

SIZE module-attribute

SIZE = 'S'

TEST_PERC_RANGE module-attribute

TEST_PERC_RANGE = [0.0, 1.0]

TRAIN_PERC_RANGE module-attribute

TRAIN_PERC_RANGE = args.train_range

args module-attribute

args = parser.parse_args()

avg_test_loss module-attribute

avg_test_loss = np.mean(test_losses_seeds)

avg_train_loss module-attribute

avg_train_loss = np.mean(train_losses_seeds)

avg_val_loss module-attribute

avg_val_loss = np.mean(val_losses_seeds)

data_transform module-attribute

data_transform = transforms.Compose([NumpyToTensor(), InputNormalization()])

device module-attribute

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

df module-attribute

df = pd.DataFrame(results)

parser module-attribute

parser = argparse.ArgumentParser(
    description="Run CrackMNIST experiments with flexible settings."
)

results module-attribute

results = []

start module-attribute

start = time.time()

std_test_loss module-attribute

std_test_loss = np.std(test_losses_seeds)

std_train_loss module-attribute

std_train_loss = np.std(train_losses_seeds)

std_val_loss module-attribute

std_val_loss = np.std(val_losses_seeds)

target_transform module-attribute

target_transform = transforms.Compose([NumpyToTensor()])

test_dataset_full module-attribute

test_dataset_full = CrackMNIST(
    split="test",
    pixels=PIXELS,
    size=SIZE,
    transform=data_transform,
    target_transform=target_transform,
)

test_loader_full module-attribute

test_loader_full = data.DataLoader(
    test_dataset_full, batch_size=BATCH_SIZE, shuffle=False, num_workers=4
)

test_losses_seeds module-attribute

test_losses_seeds = []

train_dataset_full module-attribute

train_dataset_full = CrackMNIST(
    split="train",
    pixels=PIXELS,
    size=SIZE,
    transform=data_transform,
    target_transform=target_transform,
)

train_loader_full module-attribute

train_loader_full = data.DataLoader(
    train_dataset_full, batch_size=BATCH_SIZE, shuffle=False, num_workers=4
)

train_losses_seeds module-attribute

train_losses_seeds = []

val_dataset_full module-attribute

val_dataset_full = CrackMNIST(
    split="val",
    pixels=PIXELS,
    size=SIZE,
    transform=data_transform,
    target_transform=target_transform,
)

val_loader_full module-attribute

val_loader_full = data.DataLoader(
    val_dataset_full, batch_size=BATCH_SIZE, shuffle=False, num_workers=4
)

val_losses_seeds module-attribute

val_losses_seeds = []

Functions:

evaluate

evaluate(
    model: Module,
    loader: DataLoader,
    pollution_ratio: float,
    pollution_type: str,
    pollution_kwargs: dict,
) -> float

Run model in eval mode and return average DiceLoss over the loader.

Parameters:

Name Type Description Default

model

Module

Trained PyTorch model.

required

loader

DataLoader

DataLoader to evaluate on.

required

pollution_ratio

float

Fraction of images to pollute.

required

pollution_type

str

Type of pollution to apply.

required

pollution_kwargs

dict

Additional keyword arguments for the pollution function.

required

Returns:

Type Description
float

Average DiceLoss as a float.

Source code in src/crackmnist_eccv/run_experiment.py
def evaluate(
    model: torch.nn.Module,
    loader: data.DataLoader,
    pollution_ratio: float,
    pollution_type: str,
    pollution_kwargs: dict,
) -> float:
    """Run model in eval mode and return average DiceLoss over the loader.

    Args:
        model: Trained PyTorch model.
        loader: DataLoader to evaluate on.
        pollution_ratio: Fraction of images to pollute.
        pollution_type: Type of pollution to apply.
        pollution_kwargs: Additional keyword arguments for the pollution
            function.

    Returns:
        Average DiceLoss as a float.
    """
    model.eval()
    criterion = DiceLoss()
    total_loss, total_count = 0.0, 0

    with torch.no_grad():
        for inputs, targets in loader:
            inputs = apply_per_image_pollution(
                inputs,
                pollution_ratio=pollution_ratio,
                pollution_type=pollution_type,
                **pollution_kwargs,
            )
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = model(inputs)
            loss = criterion(outputs, targets.unsqueeze(1).float() / 255.0)
            batch_size = inputs.size(0)
            total_loss += loss.item() * batch_size
            total_count += batch_size

    return total_loss / total_count if total_count else 0.0

fix_random_seeds

fix_random_seeds(seed: int) -> None

Fix all relevant random seeds for reproducibility.

Parameters:

Name Type Description Default

seed

int

Integer seed value.

required
Source code in src/crackmnist_eccv/run_experiment.py
def fix_random_seeds(seed: int) -> None:
    """Fix all relevant random seeds for reproducibility.

    Args:
        seed: Integer seed value.
    """
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.use_deterministic_algorithms(True)

get_pollution_config

get_pollution_config(pollution_type: str, intensity: float | tuple[float, float]) -> dict

Return keyword arguments for apply_per_image_pollution.

Parameters:

Name Type Description Default

pollution_type

str

One of 'blob', 'gaussian', 'salt_pepper', 'gaussian_blur'.

required

intensity

float | tuple[float, float]

Float or (min, max) tuple depending on pollution type.

required

Returns:

Type Description
dict

Dict with 'pollution_type' and 'pollution_kwargs' keys.

Raises:

Type Description
ValueError

If the pollution type is unsupported or intensity format is wrong.

Source code in src/crackmnist_eccv/run_experiment.py
def get_pollution_config(
    pollution_type: str, intensity: float | tuple[float, float]
) -> dict:
    """Return keyword arguments for apply_per_image_pollution.

    Args:
        pollution_type: One of 'blob', 'gaussian', 'salt_pepper',
            'gaussian_blur'.
        intensity: Float or (min, max) tuple depending on pollution type.

    Returns:
        Dict with 'pollution_type' and 'pollution_kwargs' keys.

    Raises:
        ValueError: If the pollution type is unsupported or intensity format
            is wrong.
    """
    if pollution_type == "blob":
        if not isinstance(intensity, tuple) or len(intensity) != 2:
            raise ValueError("Blob pollution requires intensity as a (min, max) tuple")
        return {
            "pollution_type": pollution_type,
            "pollution_kwargs": {
                "radius": RADIUS,
                "target_min_oar": intensity[0],
                "target_max_oar": intensity[1],
            },
        }
    elif pollution_type in ["gaussian", "salt_pepper", "gaussian_blur"]:
        if not isinstance(intensity, (float, int)):
            raise ValueError(f"{pollution_type} requires intensity as a float or int")
        return {
            "pollution_type": pollution_type,
            "pollution_kwargs": {"intensity": intensity},
        }
    else:
        raise ValueError(f"Unsupported pollution type: {pollution_type}")

parse_python_literal

parse_python_literal(text: str, expected_type: type) -> list

Convert a string like '[0.1,0.2]' or '[(0.1,0.2)]' to a Python object.

Parameters:

Name Type Description Default

text

str

String representation of a Python literal.

required

expected_type

type

The expected Python type (e.g. list).

required

Returns:

Type Description
list

Parsed Python object of the expected type.

Raises:

Type Description
ArgumentTypeError

If parsing fails or type does not match.

TypeError

If the parsed value is not an instance of expected_type.

Source code in src/crackmnist_eccv/run_experiment.py
def parse_python_literal(text: str, expected_type: type) -> list:
    """Convert a string like '[0.1,0.2]' or '[(0.1,0.2)]' to a Python object.

    Args:
        text: String representation of a Python literal.
        expected_type: The expected Python type (e.g. list).

    Returns:
        Parsed Python object of the expected type.

    Raises:
        ArgumentTypeError: If parsing fails or type does not match.
        TypeError: If the parsed value is not an instance of expected_type.
    """
    try:
        value = ast.literal_eval(text)
        if not isinstance(value, expected_type):
            raise TypeError
        return value
    except Exception as err:
        raise argparse.ArgumentTypeError(
            f"Value '{text}' is not a valid {expected_type.__name__}"
        ) from err

train_and_evaluate

Train a UNet and evaluate on val and test sets.

Parameters:

Name Type Description Default

train_perc

float

Fraction of training images to pollute.

required

test_perc

float

Fraction of test images to pollute during evaluation.

required

intensity

float | tuple[float, float]

Pollution intensity (float or tuple for blob).

required

seed

int

Random seed for reproducibility.

required

Returns:

Type Description
tuple[float, float, float]

Tuple of (final_val_loss, final_test_loss, final_train_loss).

Source code in src/crackmnist_eccv/run_experiment.py
def train_and_evaluate(
    train_perc: float,
    test_perc: float,
    intensity: float | tuple[float, float],
    seed: int,
) -> tuple[float, float, float]:
    """Train a UNet and evaluate on val and test sets.

    Args:
        train_perc: Fraction of training images to pollute.
        test_perc: Fraction of test images to pollute during evaluation.
        intensity: Pollution intensity (float or tuple for blob).
        seed: Random seed for reproducibility.

    Returns:
        Tuple of (final_val_loss, final_test_loss, final_train_loss).
    """
    fix_random_seeds(seed)

    model = UNet(in_ch=2, out_ch=1, init_features=128, dropout_prob=0.2).to(device)
    criterion = DiceLoss()
    optimizer = optim.Adam(model.parameters(), lr=LR)

    config = get_pollution_config(SELECTED_POLLUTION_TYPE, intensity)
    pollution_type = config["pollution_type"]
    pollution_kwargs = config["pollution_kwargs"]

    model.train()
    for _epoch in range(NUM_EPOCHS):
        for inputs, targets in train_loader_full:
            inputs = apply_per_image_pollution(
                inputs,
                pollution_ratio=train_perc,
                pollution_type=pollution_type,
                **pollution_kwargs,
            )
            inputs, targets = inputs.to(device), targets.to(device)

            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, targets.unsqueeze(1).float() / 255.0)
            loss.backward()
            optimizer.step()

    final_val_loss = evaluate(
        model,
        val_loader_full,
        pollution_ratio=train_perc,
        pollution_type=pollution_type,
        pollution_kwargs=pollution_kwargs,
    )
    final_test_loss = evaluate(
        model,
        test_loader_full,
        pollution_ratio=test_perc,
        pollution_type=pollution_type,
        pollution_kwargs=pollution_kwargs,
    )
    final_train_loss = evaluate(
        model,
        train_loader_full,
        pollution_ratio=train_perc,
        pollution_type=pollution_type,
        pollution_kwargs=pollution_kwargs,
    )

    return final_val_loss, final_test_loss, final_train_loss