Skip to content

[Enhancement] Replace RandomDownScale with the built-in Downscale #8

Description

@ternaus

Hi,

I am glad Albumentations was useful in FakeSTormer. The RandomDownScale used during SBI source generation is now covered by the built-in Downscale, including the same nearest-neighbor downscale, linear upscale, and discrete 2×/4× choices.

The current RandomDownScale contains the complete operation:

class RandomDownScale(ImageOnlyTransform):
    def __init__(self, 
                 always_apply: bool = False, 
                 p: float = 0.5,
                 ratio_list: list = [2,4]):
        self.ratio_list = ratio_list
        super().__init__(p=p, always_apply=always_apply)

    def apply(self, img: np.ndarray, ratio: int, **params):
        return self.randomdownscale(img, ratio, **params)
    
    def get_params(self):
        ratio = self.ratio_list[np.random.randint(len(self.ratio_list))]
        return {"ratio": ratio}
    
    def randomdownscale(self, img, ratio, **kwargs):
        keep_ratio = True
        keep_input_shape = True
        H, W, C = img.shape
        
        # r = np.random.uniform(2, 4)
        img_ds = cv2.resize(img, (int(W/ratio), int(H/ratio)), interpolation=cv2.INTER_NEAREST)
        if keep_input_shape:
            img_ds = cv2.resize(img_ds, (W,H), interpolation=cv2.INTER_LINEAR)
            
        return img_ds

    def get_transform_init_args_names(self):
        return ("ratio_list",)

Both the image and video builders place it beside Sharpen with equal weight:

A.OneOf([
    RandomDownScale(p=1),
    A.Sharpen(alpha=(0.2, 0.5), lightness=(0.5, 1.0), p=1),
],p=1),

The video path stores the first frame's replay and applies it to later frames:

if data_type == 'video':
    if index == 0:
        transform = get_source_transforms(data_type=data_type)
        data = transform(image=source.astype(np.uint8))
        source = data['image']
        replay_params = data['replay']
        param_store_ins.add_parameters('s_replay_params', replay_params)
    else:
        replay_params = param_store_ins.get_parameters('s_replay_params')
        data = alb.ReplayCompose.replay(replay_params, image=source.astype(np.uint8))
        source = data['image']

The existing distribution has three outcomes: 2× downscale with probability 1/4, 4× downscale with probability 1/4, and sharpening with probability 1/2. OneOf uses each child's p as its selection weight, so the exact discrete distribution can stay flat and explicit:

def source_quality_transform():
    return A.OneOf([
        A.Downscale(scale_range=(0.5, 0.5), interpolation_pair={"downscale": cv2.INTER_NEAREST, "upscale": cv2.INTER_LINEAR}, p=0.25),
        A.Downscale(scale_range=(0.25, 0.25), interpolation_pair={"downscale": cv2.INTER_NEAREST, "upscale": cv2.INTER_LINEAR}, p=0.25),
        A.Sharpen(alpha_range=(0.2, 0.5), lightness_range=(0.5, 1.0), p=0.5),
    ], p=1)

Then both repeated quality blocks become the same small call, and the custom class can be removed:

@@ -241,4 +241 @@
-            A.OneOf([
-                RandomDownScale(p=1),
-                A.Sharpen(alpha=(0.2, 0.5), lightness=(0.5, 1.0), p=1),
-            ],p=1),
+            source_quality_transform(),
@@ -254,4 +251 @@
-            A.OneOf([
-                RandomDownScale(p=1),
-                A.Sharpen(alpha=(0.2, 0.5), lightness=(0.5, 1.0), p=1),
-            ],p=1),
+            source_quality_transform(),

The existing transform(image=...) and ReplayCompose.replay(..., image=...) calls remain unchanged. The replacement also keeps the configured range of Sharpen while using its current 2.3.5 argument names.

I tested the published albumentationsx==2.3.5 wheel and matching tag with RGB uint8 arrays shaped 224×224, 317×317, 319×257, and 768×512. For both fixed scale factors, the old OpenCV code and Downscale matched element-for-element and preserved shape and dtype. A 128-seed ReplayCompose check exercised all three branches and reproduced every result exactly.

The 1/4, 1/4, 1/2 probabilities follow directly from the old two equal choices and the new OneOf weights; they do not depend on that seed check. The random-number stream will change after migration, so saved training runs should not be expected to produce the same sampled sequence.

The recommended environment and Dockerfile pin albumentations==1.1.0, so I did not open a dependency-change PR.

A complete 2.3.5 migration also needs the removed always_apply arguments, the old DualTransform import path, and the *_limit/quality_lower/quality_upper arguments elsewhere in geo_transform.py to be updated and tested through image and video training.

If you want to try the maintained package, the Python import remains import albumentations as A:

pip uninstall albumentations
pip install -U albumentationsx

The packages use different licenses: albumentations==1.1.0 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