@@ -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