Skip to content

Commit

Permalink
Added angle_range and direction_range to the MotionBlur
Browse files Browse the repository at this point in the history
  • Loading branch information
ternaus committed Nov 18, 2024
1 parent 41eb611 commit 2ed3dd1
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 70 deletions.
64 changes: 64 additions & 0 deletions albumentations/augmentations/blur/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Check warning on line 224 in albumentations/augmentations/blur/functional.py

View check run for this annotation

Codecov / codecov/patch

albumentations/augmentations/blur/functional.py#L224

Added line #L224 was not covered by tests

return kernel
186 changes: 116 additions & 70 deletions albumentations/augmentations/blur/transforms.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)}


Expand Down

0 comments on commit 2ed3dd1

Please sign in to comment.