Skip to content
This repository was archived by the owner on Jul 10, 2025. It is now read-only.

Commit 117a105

Browse files
authored
Bugfix in MotionBlur (#2529)
* Bugfix in MotionBlur * Bugfix in MotionBlur
1 parent b914077 commit 117a105

3 files changed

Lines changed: 364 additions & 8 deletions

File tree

albumentations/augmentations/blur/functional.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ def create_motion_kernel(
263263
np.ndarray: Motion blur kernel
264264
265265
"""
266+
# Validate direction range to prevent unexpected interpolation results
267+
direction = np.clip(direction, -1.0, 1.0)
268+
266269
kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32)
267270
center = kernel_size // 2
268271

@@ -275,20 +278,34 @@ def create_motion_kernel(
275278

276279
# Create line points with direction bias
277280
line_length = kernel_size // 2
278-
t = np.linspace(-line_length, line_length, kernel_size * 2)
279281

280-
# Apply direction bias
281-
if direction != 0:
282-
t = t * (1 + abs(direction))
283-
if direction < 0:
284-
t = t * -1
282+
# Apply direction bias to control the distribution of blur
283+
if direction < 0:
284+
# Backward bias: interpolate between symmetric and backward-only
285+
# direction = -1: only backward, direction = 0: symmetric
286+
bias_factor = abs(direction)
287+
t_start = float(-line_length)
288+
t_end = line_length * (1 - bias_factor)
289+
elif direction > 0:
290+
# Forward bias: interpolate between symmetric and forward-only
291+
# direction = 1: only forward, direction = 0: symmetric
292+
bias_factor = direction
293+
t_start = -line_length * (1 - bias_factor)
294+
t_end = float(line_length)
295+
else:
296+
# Symmetric case (direction = 0)
297+
t_start = float(-line_length)
298+
t_end = float(line_length)
299+
300+
# Generate points along the biased line
301+
t = np.linspace(t_start, t_end, kernel_size)
285302

286303
# Generate line coordinates
287304
x = center + dx * t
288305
y = center + dy * t
289306

290307
# Apply random shift if allowed
291-
if allow_shifted and random_state is not None:
308+
if allow_shifted:
292309
shift_x = random_state.uniform(-1, 1) * line_length / 2
293310
shift_y = random_state.uniform(-1, 1) * line_length / 2
294311
x += shift_x

tests/functional/test_blur.py

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,307 @@ def test_kernel_visual_comparison():
144144
pil_kernel = create_pil_kernel(sigma)
145145

146146
np.testing.assert_allclose(our_kernel, pil_kernel, rtol=1e-5)
147+
148+
149+
# === Motion Kernel Tests ===
150+
151+
@pytest.mark.parametrize(
152+
"kernel_size, expected_shape",
153+
[
154+
(3, (3, 3)),
155+
(5, (5, 5)),
156+
(7, (7, 7)),
157+
(9, (9, 9)),
158+
]
159+
)
160+
def test_create_motion_kernel_shape(kernel_size, expected_shape):
161+
"""Test that motion kernel has the correct shape."""
162+
random_state = Random(42)
163+
kernel = fblur.create_motion_kernel(
164+
kernel_size=kernel_size,
165+
angle=0.0,
166+
direction=0.0,
167+
allow_shifted=False,
168+
random_state=random_state
169+
)
170+
assert kernel.shape == expected_shape
171+
172+
173+
@pytest.mark.parametrize("kernel_size", [3, 5, 7, 9])
174+
def test_create_motion_kernel_normalization(kernel_size):
175+
"""Test that motion kernel has equal weight distribution along motion line."""
176+
random_state = Random(42)
177+
kernel = fblur.create_motion_kernel(
178+
kernel_size=kernel_size,
179+
angle=0.0,
180+
direction=0.0,
181+
allow_shifted=False,
182+
random_state=random_state
183+
)
184+
# For symmetric horizontal motion, the kernel sum should equal the kernel size
185+
# (each pixel along the horizontal line gets weight 1.0)
186+
np.testing.assert_allclose(kernel.sum(), kernel_size, atol=1e-6)
187+
188+
189+
@pytest.mark.parametrize(
190+
"direction, expected_behavior",
191+
[
192+
(-1.0, "backward"), # Should have more weight toward start of line
193+
(-0.5, "backward"), # Moderate backward bias
194+
(0.0, "symmetric"), # Should be symmetric
195+
(0.5, "forward"), # Moderate forward bias
196+
(1.0, "forward"), # Should have more weight toward end of line
197+
]
198+
)
199+
def test_create_motion_kernel_direction_bias(direction, expected_behavior):
200+
"""Test that direction parameter controls blur direction correctly."""
201+
random_state = Random(42)
202+
kernel_size = 7
203+
kernel = fblur.create_motion_kernel(
204+
kernel_size=kernel_size,
205+
angle=0.0, # Horizontal line for easier testing
206+
direction=direction,
207+
allow_shifted=False,
208+
random_state=random_state
209+
)
210+
211+
# For horizontal motion, check the middle row
212+
middle_row = kernel_size // 2
213+
row = kernel[middle_row, :]
214+
215+
# Find center and calculate weights
216+
center_col = kernel_size // 2
217+
left_weight = np.sum(row[:center_col])
218+
right_weight = np.sum(row[center_col + 1:])
219+
220+
if expected_behavior == "backward":
221+
assert left_weight >= right_weight, f"direction={direction} should have more weight on the left"
222+
assert left_weight > 0, f"direction={direction} should have some weight on the left"
223+
elif expected_behavior == "symmetric":
224+
assert abs(left_weight - right_weight) < 1e-6, f"direction={direction} should be symmetric"
225+
assert left_weight > 0 and right_weight > 0, f"direction={direction} should have weight on both sides"
226+
elif expected_behavior == "forward":
227+
assert right_weight >= left_weight, f"direction={direction} should have more weight on the right"
228+
assert right_weight > 0, f"direction={direction} should have some weight on the right"
229+
230+
231+
@pytest.mark.parametrize(
232+
"angle, expected_orientation",
233+
[
234+
(0.0, "horizontal"),
235+
(90.0, "vertical"),
236+
(45.0, "diagonal"),
237+
(135.0, "diagonal"),
238+
(180.0, "horizontal"),
239+
(270.0, "vertical"),
240+
]
241+
)
242+
def test_create_motion_kernel_angles(angle, expected_orientation):
243+
"""Test that angle parameter controls motion direction correctly."""
244+
random_state = Random(42)
245+
kernel_size = 7
246+
kernel = fblur.create_motion_kernel(
247+
kernel_size=kernel_size,
248+
angle=angle,
249+
direction=0.0, # Symmetric for easier testing
250+
allow_shifted=False,
251+
random_state=random_state
252+
)
253+
254+
# Check that kernel has non-zero values
255+
assert kernel.sum() > 0, f"Kernel should have non-zero values for angle={angle}"
256+
257+
# For horizontal angles (0, 180), expect motion in middle row
258+
if expected_orientation == "horizontal":
259+
middle_row = kernel_size // 2
260+
row_sum = np.sum(kernel[middle_row, :])
261+
assert np.isclose(row_sum, kernel_size, atol=1.5) or row_sum > kernel_size * 0.8, (
262+
f"Horizontal motion should be concentrated in middle row for angle={angle} "
263+
f"(row_sum={row_sum}, expected≈{kernel_size})"
264+
)
265+
266+
# For vertical angles (90, 270), expect motion in middle column
267+
elif expected_orientation == "vertical":
268+
middle_col = kernel_size // 2
269+
col_sum = np.sum(kernel[:, middle_col])
270+
assert np.isclose(col_sum, kernel_size, atol=1.5) or col_sum > kernel_size * 0.8, (
271+
f"Vertical motion should be concentrated in middle column for angle={angle} "
272+
f"(col_sum={col_sum}, expected≈{kernel_size})"
273+
)
274+
275+
276+
@pytest.mark.parametrize("allow_shifted", [True, False])
277+
def test_create_motion_kernel_allow_shifted(allow_shifted):
278+
"""Test that allow_shifted parameter works correctly."""
279+
random_state = Random(42)
280+
kernel_size = 7
281+
282+
# Generate multiple kernels to test shifting behavior
283+
kernels = []
284+
for _ in range(10):
285+
kernel = fblur.create_motion_kernel(
286+
kernel_size=kernel_size,
287+
angle=0.0,
288+
direction=0.0,
289+
allow_shifted=allow_shifted,
290+
random_state=random_state
291+
)
292+
kernels.append(kernel)
293+
294+
if not allow_shifted:
295+
# All kernels should be identical when shifting is disabled
296+
for i in range(1, len(kernels)):
297+
np.testing.assert_array_equal(kernels[0], kernels[i],
298+
"Kernels should be identical when allow_shifted=False")
299+
# Note: With shifting enabled, kernels might be different
300+
# (This is harder to test deterministically due to randomness)
301+
302+
303+
@pytest.mark.parametrize(
304+
"direction1, direction2",
305+
[
306+
(-1.0, 0.0),
307+
(-1.0, 1.0),
308+
(0.0, 1.0),
309+
(-0.5, 0.5),
310+
]
311+
)
312+
def test_create_motion_kernel_different_directions_produce_different_kernels(direction1, direction2):
313+
"""Test that different direction values produce different kernels."""
314+
random_state = Random(42)
315+
kernel_size = 5
316+
317+
kernel1 = fblur.create_motion_kernel(
318+
kernel_size=kernel_size,
319+
angle=0.0,
320+
direction=direction1,
321+
allow_shifted=False,
322+
random_state=random_state
323+
)
324+
325+
# Reset random state to ensure same other parameters
326+
random_state = Random(42)
327+
kernel2 = fblur.create_motion_kernel(
328+
kernel_size=kernel_size,
329+
angle=0.0,
330+
direction=direction2,
331+
allow_shifted=False,
332+
random_state=random_state
333+
)
334+
335+
assert not np.array_equal(kernel1, kernel2), \
336+
f"direction={direction1} and direction={direction2} should produce different kernels"
337+
338+
339+
def test_create_motion_kernel_extreme_directions():
340+
"""Test motion kernel with extreme direction values."""
341+
random_state = Random(42)
342+
kernel_size = 7
343+
344+
# Test direction = -1 (fully backward)
345+
kernel_backward = fblur.create_motion_kernel(
346+
kernel_size=kernel_size,
347+
angle=0.0,
348+
direction=-1.0,
349+
allow_shifted=False,
350+
random_state=random_state
351+
)
352+
353+
# Test direction = 1 (fully forward)
354+
random_state = Random(42) # Reset for consistency
355+
kernel_forward = fblur.create_motion_kernel(
356+
kernel_size=kernel_size,
357+
angle=0.0,
358+
direction=1.0,
359+
allow_shifted=False,
360+
random_state=random_state
361+
)
362+
363+
# For horizontal motion, check middle row
364+
middle_row = kernel_size // 2
365+
center_col = kernel_size // 2
366+
367+
# Backward should have no weight strictly to the right of center
368+
backward_row = kernel_backward[middle_row, :]
369+
assert np.sum(backward_row[center_col+1:]) == 0, "Fully backward motion should have no weight strictly to the right of center"
370+
assert np.sum(backward_row[:center_col+1]) > 0, "Fully backward motion should have weight on left side and center"
371+
372+
# Forward should have no weight strictly to the left of center
373+
forward_row = kernel_forward[middle_row, :]
374+
assert np.sum(forward_row[:center_col]) == 0, "Fully forward motion should have no weight strictly to the left of center"
375+
assert np.sum(forward_row[center_col:]) > 0, "Fully forward motion should have weight on center and right side"
376+
377+
378+
def test_create_motion_kernel_center_pixel_behavior():
379+
"""Test that center pixel is handled correctly for symmetric motion."""
380+
random_state = Random(42)
381+
kernel_size = 5 # Use odd size for clear center
382+
kernel = fblur.create_motion_kernel(
383+
kernel_size=kernel_size,
384+
angle=0.0,
385+
direction=0.0, # Symmetric
386+
allow_shifted=False,
387+
random_state=random_state
388+
)
389+
390+
# For symmetric horizontal motion, center pixel should have weight
391+
center = kernel_size // 2
392+
assert kernel[center, center] > 0, "Center pixel should have weight for symmetric motion"
393+
394+
395+
@pytest.mark.parametrize("kernel_size", [3, 5, 7])
396+
def test_create_motion_kernel_non_empty(kernel_size):
397+
"""Test that kernel always has at least one non-zero pixel."""
398+
random_state = Random(42)
399+
400+
# Test with extreme parameters that might create empty kernels
401+
kernel = fblur.create_motion_kernel(
402+
kernel_size=kernel_size,
403+
angle=0.0,
404+
direction=0.0,
405+
allow_shifted=False,
406+
random_state=random_state
407+
)
408+
409+
assert kernel.sum() > 0, "Kernel should never be completely empty"
410+
assert np.any(kernel > 0), "Kernel should have at least one non-zero pixel"
411+
412+
413+
@pytest.mark.parametrize(
414+
"direction_input, expected_clamped",
415+
[
416+
(-2.0, -1.0), # Should be clamped to -1.0
417+
(-1.5, -1.0), # Should be clamped to -1.0
418+
(1.5, 1.0), # Should be clamped to 1.0
419+
(2.0, 1.0), # Should be clamped to 1.0
420+
(0.5, 0.5), # Should remain unchanged
421+
(-0.5, -0.5), # Should remain unchanged
422+
]
423+
)
424+
def test_create_motion_kernel_direction_validation(direction_input, expected_clamped):
425+
"""Test that direction values are properly clamped to [-1, 1] range."""
426+
random_state = Random(42)
427+
kernel_size = 5
428+
429+
# Create kernel with extreme direction value
430+
kernel = fblur.create_motion_kernel(
431+
kernel_size=kernel_size,
432+
angle=0.0,
433+
direction=direction_input,
434+
allow_shifted=False,
435+
random_state=random_state
436+
)
437+
438+
# Create reference kernel with expected clamped value
439+
random_state = Random(42) # Reset for consistency
440+
reference_kernel = fblur.create_motion_kernel(
441+
kernel_size=kernel_size,
442+
angle=0.0,
443+
direction=expected_clamped,
444+
allow_shifted=False,
445+
random_state=random_state
446+
)
447+
448+
# Kernels should be identical (direction was clamped to valid range)
449+
np.testing.assert_array_equal(kernel, reference_kernel,
450+
f"direction={direction_input} should be clamped to {expected_clamped}")

0 commit comments

Comments
 (0)