Skip to content

The custom DefocusBlur is now built in as Defocus #18

Description

@ternaus

Hi,

The custom DefocusBlur in this fourth-place HuBMAP solution implements the transform that later became the built-in Defocus. This class made sense when it was added in 2021: its docstring links the still-open Albumentations feature tracker at that time. AlbumentationsX 2.3.5 now provides the same disk kernels directly, so the custom class and its three-channel loop can be removed. I am glad Albumentations was useful in this solution.

The current helper defines the disk kernel, converts the image to floating point, filters the three RGB channels separately, and then places the custom transform in OneOf:

def disk(radius, alias_blur=0.1, dtype=np.float32):
    """
    From https://github.com/hendrycks/robustness/blob/master/ImageNet-C/create_c/make_imagenet_c.py
    and https://github.com/albumentations-team/albumentations/issues/477
    """
    if radius <= 8:
        L = np.arange(-8, 8 + 1)
        ksize = (3, 3)
    else:
        L = np.arange(-radius, radius + 1)
        ksize = (5, 5)
    X, Y = np.meshgrid(L, L)
    aliased_disk = np.array((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype)
    aliased_disk /= np.sum(aliased_disk)

    # supersample disk to antialias
    return cv2.GaussianBlur(aliased_disk, ksize=ksize, sigmaX=alias_blur)


class DefocusBlur(ImageOnlyTransform):
    """
    From https://github.com/hendrycks/robustness/blob/master/ImageNet-C/create_c/make_imagenet_c.py
    and https://github.com/albumentations-team/albumentations/issues/477
    """
    def __init__(
        self,
        severity=1,
        always_apply=False,
        p=1.0,
    ):
        super(DefocusBlur, self).__init__(always_apply, p)
        self.severity = severity
        self.radius, self.blur = [(3, 0.1), (4, 0.5), (6, 0.5), (8, 0.5), (10, 0.5)][
            self.severity - 1
        ]

    def apply(self, image, **params):
        image = np.array(image) / 255.0
        kernel = disk(radius=self.radius, alias_blur=self.blur)
        channels = []
        for d in range(3):
            channels.append(cv2.filter2D(image[:, :, d], -1, kernel))
        channels = np.array(channels).transpose((1, 2, 0))
        return np.clip(channels, 0, 1) * 255

    def get_transform_init_args_names(self):
        return "severty"


def blur_transforms(p=0.5, blur_limit=5, gaussian_limit=(5, 7), severity=1):
    return albu.OneOf(
        [
            DefocusBlur(severity=severity, always_apply=True),
            albu.MotionBlur(blur_limit=blur_limit, always_apply=True),
            albu.GaussianBlur(blur_limit=gaussian_limit, always_apply=True),
        ],
        p=p,
    )

This helper is part of the checked-in training path. HE_preprocess includes it after the geometric and color transforms:

if augment:
    return albu.Compose(
        [
            albu.VerticalFlip(p=0.5),
            albu.HorizontalFlip(p=0.5),
            albu.ShiftScaleRotate(
                scale_limit=0.1,
                shift_limit=0.1,
                rotate_limit=90,
                p=0.5,
            ),
            deformation_transform(p=0.5),
            color_transforms(p=0.5),
            blur_transforms(p=0.5),
            normalizer,
        ]
    )

The fold trainer passes that pipeline into the in-memory dataset:

in_mem_dataset = InMemoryTrainDataset(
    train_img_names,
    df_rle,
    train_tile_size=config.tile_size,
    reduce_factor=config.reduce_factor,
    train_transfo=HE_preprocess(size=config.tile_size),
    valid_transfo=HE_preprocess(augment=False, size=config.tile_size),
    # ...
)

The dataset then applies it to each image and segmentation mask:

if self.transforms:
    augmented = self.transforms(image=img, mask=mask)
    img = augmented["image"]
    mask = augmented["mask"]

With the 2.3.5 argument names, the complete replacement for blur_transforms is:

DEFOCUS_BY_SEVERITY = [
    (3, 0.1),
    (4, 0.5),
    (6, 0.5),
    (8, 0.5),
    (10, 0.5),
]


def blur_transforms(p=0.5, blur_limit=5, gaussian_limit=(5, 7), severity=1):
    radius, alias_blur = DEFOCUS_BY_SEVERITY[severity - 1]

    return albu.OneOf(
        [
            albu.Defocus(
                radius_range=(radius, radius),
                alias_blur_range=(alias_blur, alias_blur),
                p=1,
            ),
            albu.MotionBlur(
                blur_range=(3, blur_limit),
                p=1,
            ),
            albu.GaussianBlur(
                blur_range=gaussian_limit,
                p=1,
            ),
        ],
        p=p,
    )

The fixed one-value ranges preserve all five existing severity settings without introducing new parameter sampling. The three children still have equal weight inside OneOf, and the outer p keeps the existing probability.

I compared the custom disk() function with the published albumentationsx==2.3.5 implementation for (radius, alias_blur) equal to (3, 0.1), (4, 0.5), (6, 0.5), (8, 0.5), and (10, 0.5). All five kernels were exactly equal element by element. On a deterministic RGB uint8 image, the built-in output differed from the custom output by less than 0.5 on the 0..255 intensity scale because the built-in transform preserves uint8 and rounds the convolution result, while the custom transform returns floating-point values. The replacement preserved shape and dtype for every severity.

The repository does not pin an Albumentations version—the requirements section is still a TODO—so I did not open a dependency-change PR. This helper is an isolated 2.3.5 replacement; a complete migration also needs the old argument names in the rest of transforms.py updated and the full image/mask training path tested. AlbumentationsX 2.3.5 requires Python 3.10 or newer.

pip uninstall albumentations
pip install -U albumentationsx

The Python module name remains albumentations, so the existing import albumentations as albu import keeps the same module path.

The packages use different licenses: the legacy albumentations package used by this repository is MIT, while albumentationsx==2.3.5 is AGPL-3.0-only. The license guide explains the terms. Albumentations, LLC also offers commercial licenses with alternative terms.

If you have feedback, complaints, or proposals for AlbumentationsX, please open an issue. I read the tracker every day.

If this note is useful, a star or sponsorship would mean a lot.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions