From 41eb6112531f1168726f5c86a228def32d0ce50e Mon Sep 17 00:00:00 2001 From: Vladimir Iglovikov Date: Wed, 2 Oct 2024 16:44:17 -0700 Subject: [PATCH 1/3] Empty-Commit From 2ed3dd111e5b9211b568df57b80c597e4a4b54c0 Mon Sep 17 00:00:00 2001 From: Vladimir Iglovikov Date: Mon, 18 Nov 2024 15:41:05 -0800 Subject: [PATCH 2/3] Added angle_range and direction_range to the MotionBlur --- .../augmentations/blur/functional.py | 64 ++++++ .../augmentations/blur/transforms.py | 186 +++++++++++------- 2 files changed, 180 insertions(+), 70 deletions(-) diff --git a/albumentations/augmentations/blur/functional.py b/albumentations/augmentations/blur/functional.py index 4d98afda7..a3109eecf 100644 --- a/albumentations/augmentations/blur/functional.py +++ b/albumentations/augmentations/blur/functional.py @@ -3,6 +3,7 @@ from collections.abc import Sequence from itertools import product from math import ceil +from random import Random from typing import Literal from warnings import warn @@ -160,3 +161,66 @@ def process_blur_limit(value: ScaleIntType, info: ValidationInfo, min_value: int return final_result return result + + +def create_motion_kernel( + kernel_size: int, + angle: float, + direction: float, + allow_shifted: bool, + random_state: Random, +) -> np.ndarray: + """Create a motion blur kernel. + + Args: + kernel_size: Size of the kernel (must be odd) + angle: Angle in degrees (counter-clockwise) + direction: Blur direction (-1.0 to 1.0) + allow_shifted: Allow kernel to be randomly shifted from center + random_state: Python's random.Random instance + + Returns: + Motion blur kernel + """ + kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32) + center = kernel_size // 2 + + # Convert angle to radians + angle_rad = np.deg2rad(angle) + + # Calculate direction vector + dx = np.cos(angle_rad) + dy = np.sin(angle_rad) + + # Create line points with direction bias + line_length = kernel_size // 2 + t = np.linspace(-line_length, line_length, kernel_size * 2) + + # Apply direction bias + if direction != 0: + t = t * (1 + direction) + + # Generate line coordinates + x = center + dx * t + y = center + dy * t + + # Apply random shift if allowed + if allow_shifted and random_state is not None: + shift_x = random_state.uniform(-1, 1) * line_length / 2 + shift_y = random_state.uniform(-1, 1) * line_length / 2 + x += shift_x + y += shift_y + + # Round coordinates and clip to kernel bounds + x = np.clip(np.round(x), 0, kernel_size - 1).astype(int) + y = np.clip(np.round(y), 0, kernel_size - 1).astype(int) + + # Keep only unique points to avoid multiple assignments + points = np.unique(np.column_stack([y, x]), axis=0) + kernel[points[:, 0], points[:, 1]] = 1 + + # Ensure at least one point is set + if not kernel.any(): + kernel[center, center] = 1 + + return kernel diff --git a/albumentations/augmentations/blur/transforms.py b/albumentations/augmentations/blur/transforms.py index 3e49a79df..5b79879f1 100644 --- a/albumentations/augmentations/blur/transforms.py +++ b/albumentations/augmentations/blur/transforms.py @@ -1,11 +1,10 @@ from __future__ import annotations import warnings -from typing import Any, Literal, cast +from typing import Annotated, Any, Literal, cast -import cv2 import numpy as np -from pydantic import Field, ValidationInfo, field_validator, model_validator +from pydantic import AfterValidator, Field, ValidationInfo, field_validator, model_validator from typing_extensions import Self from albumentations.augmentations import functional as fmain @@ -14,6 +13,8 @@ OnePlusFloatRangeType, OnePlusIntRangeType, SymmetricRangeType, + check_range_bounds, + nondecreasing, ) from albumentations.core.transforms_interface import BaseTransformInitSchema, ImageOnlyTransform from albumentations.core.types import ScaleFloatType, ScaleIntType @@ -96,77 +97,141 @@ def get_transform_init_args_names(self) -> tuple[str, ...]: class MotionBlur(Blur): - """Apply motion blur to the input image using a random-sized kernel. + """Apply motion blur to the input image using a directional kernel. - This transform simulates the effect of camera or object motion during image capture, - creating a directional blur. It uses a line-shaped kernel with random orientation - to achieve this effect. + This transform simulates motion blur effects that occur during image capture, + such as camera shake or object movement. It creates a directional blur using + a line-shaped kernel with controllable angle, direction, and position. Args: - blur_limit (int | tuple[int, int]): Maximum kernel size for blurring the input image. + blur_limit (int | tuple[int, int]): Maximum kernel size for blurring. Should be in range [3, inf). - - If a single int is provided, the kernel size will be randomly chosen - between 3 and that value. - - If a tuple of two ints is provided, it defines the inclusive range - of possible kernel sizes. + - If int: kernel size will be randomly chosen from [3, blur_limit] + - If tuple: kernel size will be randomly chosen from [min, max] + Larger values create stronger blur effects. Default: (3, 7) - allow_shifted (bool): If set to True, allows the motion blur kernel to be - randomly shifted from the center. If False, the kernel will always be - centered. Default: True + angle_range (tuple[float, float]): Range of possible angles in degrees. + Controls the rotation of the motion blur line: + - 0°: Horizontal motion blur → + - 45°: Diagonal motion blur ↗ + - 90°: Vertical motion blur ↑ + - 135°: Diagonal motion blur ↖ + Default: (-45, 45) + + direction_range (tuple[float, float]): Range for motion bias. + Controls how the blur extends from the center: + - -1.0: Blur extends only backward (←) + - 0.0: Blur extends equally in both directions (←→) + - 1.0: Blur extends only forward (→) + For example, with angle=0: + - direction=-1.0: ←• + - direction=0.0: ←•→ + - direction=1.0: •→ + Default: (-0.5, 0.5) + + allow_shifted (bool): Allow random kernel position shifts. + - If True: Kernel can be randomly offset from center + - If False: Kernel will always be centered + Default: True p (float): Probability of applying the transform. Default: 0.5 - Targets: - image + Examples of angle vs direction: + 1. Horizontal motion (angle=0°): + - direction=0.0: ←•→ (symmetric blur) + - direction=1.0: •→ (forward blur) + - direction=-1.0: ←• (backward blur) - Image types: - uint8, float32 + 2. Vertical motion (angle=90°): + - direction=0.0: ↑•↓ (symmetric blur) + - direction=1.0: •↑ (upward blur) + - direction=-1.0: ↓• (downward blur) - Number of channels: - Any + 3. Diagonal motion (angle=45°): + - direction=0.0: ↙•↗ (symmetric blur) + - direction=1.0: •↗ (forward diagonal blur) + - direction=-1.0: ↙• (backward diagonal blur) Note: - - The blur kernel is always a straight line, simulating linear motion. - - The angle of the motion blur is randomly chosen for each application. - - Larger kernel sizes result in more pronounced motion blur effects. - - When `allow_shifted` is True, the blur effect can appear more natural and varied, - as it simulates motion that isn't perfectly centered in the frame. - - This transform is particularly useful for: - * Simulating camera shake or motion blur in action scenes - * Data augmentation for object detection or tracking tasks - * Creating more challenging inputs for image stabilization algorithms + - angle controls the orientation of the motion line + - direction controls the distribution of the blur along that line + - Together they can simulate various motion effects: + * Camera shake: Small angle range + direction near 0 + * Object motion: Specific angle + direction=1.0 + * Complex motion: Random angle + random direction Example: - >>> import numpy as np >>> import albumentations as A - >>> image = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8) - >>> transform = A.MotionBlur(blur_limit=7, allow_shifted=True, p=0.5) - >>> result = transform(image=image) - >>> motion_blurred_image = result["image"] + >>> # Horizontal camera shake (symmetric) + >>> transform = A.MotionBlur( + ... angle_range=(-5, 5), # Near-horizontal motion + ... direction_range=(0, 0), # Symmetric blur + ... p=1.0 + ... ) + >>> + >>> # Object moving right + >>> transform = A.MotionBlur( + ... angle_range=(0, 0), # Horizontal motion + ... direction_range=(0.8, 1.0), # Strong forward bias + ... p=1.0 + ... ) References: - - Motion blur: https://en.wikipedia.org/wiki/Motion_blur - - OpenCV filter2D (used internally): + - Motion blur fundamentals: + https://en.wikipedia.org/wiki/Motion_blur + + - Directional blur kernels: + https://www.sciencedirect.com/topics/computer-science/directional-blur + + - OpenCV filter2D (used for convolution): https://docs.opencv.org/master/d4/d86/group__imgproc__filter.html#ga27c049795ce870216ddfb366086b5a04 + + - Research on motion blur simulation: + "Understanding and Evaluating Blind Deconvolution Algorithms" (CVPR 2009) + https://doi.org/10.1109/CVPR.2009.5206815 + + - Motion blur in photography: + "The Manual of Photography", Chapter 7: Motion in Photography + ISBN: 978-0240520377 + + - Kornia's implementation (similar approach): + https://kornia.readthedocs.io/en/latest/augmentation.html#kornia.augmentation.RandomMotionBlur + + See Also: + - GaussianBlur: For uniform blur effects + - MedianBlur: For noise reduction while preserving edges + - RandomRain: Another motion-based effect + - Perspective: For geometric motion-like distortions + """ class InitSchema(BlurInitSchema): allow_shifted: bool + angle_range: Annotated[tuple[float, float], AfterValidator(nondecreasing)] + direction_range: Annotated[ + tuple[float, float], + AfterValidator(nondecreasing), + AfterValidator(check_range_bounds(min_val=-1.0, max_val=1.0)), + ] def __init__( self, blur_limit: ScaleIntType = 7, allow_shifted: bool = True, + angle_range: tuple[float, float] = (-45, 45), + direction_range: tuple[float, float] = (-0.5, 0.5), always_apply: bool | None = None, p: float = 0.5, ): - super().__init__(blur_limit=blur_limit, p=p, always_apply=always_apply) + super().__init__(blur_limit=blur_limit, p=p) self.allow_shifted = allow_shifted self.blur_limit = cast(tuple[int, int], blur_limit) + self.angle_range = angle_range + self.direction_range = direction_range def get_transform_init_args_names(self) -> tuple[str, ...]: - return (*super().get_transform_init_args_names(), "allow_shifted") + return (*super().get_transform_init_args_names(), "allow_shifted", "angle_range", "direction_range") def apply(self, img: np.ndarray, kernel: np.ndarray, **params: Any) -> np.ndarray: return fmain.convolve(img, kernel=kernel) @@ -175,38 +240,19 @@ def get_params(self) -> dict[str, Any]: ksize = self.py_random.choice(list(range(self.blur_limit[0], self.blur_limit[1] + 1, 2))) if ksize <= TWO: raise ValueError(f"ksize must be > 2. Got: {ksize}") - kernel = np.zeros((ksize, ksize), dtype=np.uint8) - x1, x2 = self.py_random.randint(0, ksize - 1), self.py_random.randint(0, ksize - 1) - if x1 == x2: - y1, y2 = self.py_random.sample(range(ksize), 2) - else: - y1, y2 = self.py_random.randint(0, ksize - 1), self.py_random.randint(0, ksize - 1) - - def make_odd_val(v1: int, v2: int) -> tuple[int, int]: - len_v = abs(v1 - v2) + 1 - if len_v % 2 != 1: - if v2 > v1: - v2 -= 1 - else: - v1 -= 1 - return v1, v2 - - if not self.allow_shifted: - x1, x2 = make_odd_val(x1, x2) - y1, y2 = make_odd_val(y1, y2) - - xc = (x1 + x2) / 2 - yc = (y1 + y2) / 2 - - center = ksize / 2 - 0.5 - dx = xc - center - dy = yc - center - x1, x2 = (int(i - dx) for i in [x1, x2]) - y1, y2 = (int(i - dy) for i in [y1, y2]) - - cv2.line(kernel, (x1, y1), (x2, y2), 1, thickness=1) - # Normalize kernel + angle = self.py_random.uniform(*self.angle_range) + direction = self.py_random.uniform(*self.direction_range) + + # Create motion blur kernel + kernel = fblur.create_motion_kernel( + ksize, + angle, + direction, + allow_shifted=self.allow_shifted, + random_state=self.py_random, + ) + return {"kernel": kernel.astype(np.float32) / np.sum(kernel)} From 297daa0589023cb9b85fc6f57f4cb55c8302f109 Mon Sep 17 00:00:00 2001 From: Vladimir Iglovikov Date: Mon, 18 Nov 2024 15:47:14 -0800 Subject: [PATCH 3/3] Added angle_range and direction_range to the MotionBlur --- albumentations/augmentations/blur/transforms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/albumentations/augmentations/blur/transforms.py b/albumentations/augmentations/blur/transforms.py index 5b79879f1..425698b4c 100644 --- a/albumentations/augmentations/blur/transforms.py +++ b/albumentations/augmentations/blur/transforms.py @@ -117,7 +117,7 @@ class MotionBlur(Blur): - 45°: Diagonal motion blur ↗ - 90°: Vertical motion blur ↑ - 135°: Diagonal motion blur ↖ - Default: (-45, 45) + Default: (0, 360) direction_range (tuple[float, float]): Range for motion bias. Controls how the blur extends from the center: @@ -219,8 +219,8 @@ def __init__( self, blur_limit: ScaleIntType = 7, allow_shifted: bool = True, - angle_range: tuple[float, float] = (-45, 45), - direction_range: tuple[float, float] = (-0.5, 0.5), + angle_range: tuple[float, float] = (0, 360), + direction_range: tuple[float, float] = (-1.0, 1.0), always_apply: bool | None = None, p: float = 0.5, ):