11# -*- coding: utf-8 -*-
2-
3-
42import nibabel as nib
53import numpy as np
64from numpy .testing import (assert_array_equal ,
119 gaussian_filter )
1210
1311
14- from scilpy .image .volume_math import (_validate_imgs ,
15- _validate_imgs_concat ,
12+ from scilpy .image .volume_math import (_validate_imgs_type ,
1613 _validate_length ,
1714 _validate_type ,
1815 _validate_float ,
19- cut_up_cube ,
16+ _get_neighbors ,
2017 lower_threshold_eq , upper_threshold_eq ,
2118 lower_threshold , upper_threshold ,
2219 lower_threshold_otsu ,
3431 difference , invert ,
3532 concatenate , gaussian_blur ,
3633 dilation , erosion ,
37- closing , opening )
34+ closing , opening ,
35+ neighborhood_correlation )
3836
3937
4038EPSILON = np .finfo (float ).eps
4139
4240
43- def test_validate_imgs_matching ():
44- data1 = np .zeros ((5 , 5 , 5 )).astype (float )
45- affine1 = np .eye (4 )
46- img1 = nib .Nifti1Image (data1 , affine1 )
47-
48- data2 = np .zeros ((5 , 5 , 5 )).astype (float )
49- affine2 = np .eye (4 )
50- img2 = nib .Nifti1Image (data2 , affine2 )
51-
52- try :
53- _validate_imgs (img1 , img2 )
54- print ("Test with matching shapes passed." )
55- except ValueError as e :
56- print ("Test with matching shapes failed:" , str (e ))
57-
58-
59- def test_validate_imgs_different ():
60- data1 = np .zeros ((5 , 5 , 5 )).astype (float )
61- affine1 = np .eye (4 )
62- img1 = nib .Nifti1Image (data1 , affine1 )
63-
64- data2 = np .zeros ((6 , 6 , 6 )).astype (float )
65- affine2 = np .eye (4 )
66- img2 = nib .Nifti1Image (data2 , affine2 )
67-
41+ def _assert_failed (function , * args ):
42+ """
43+ Assert that a call fails
44+ """
45+ failed = False
6846 try :
69- _validate_imgs ( img1 , img2 )
70- print ( "Test with different shapes passed." )
71- except ValueError as e :
72- print ( "Test with different shapes failed:" , str ( e ))
47+ function ( * args )
48+ except ValueError : # All our tests raise ValueErrors.
49+ failed = True
50+ assert failed
7351
7452
75- def test_validate_imgs_concat_all ():
53+ def test_validate_imgs_type ():
54+ fake_affine = np .eye (4 )
7655 data1 = np .zeros ((5 , 5 , 5 )).astype (float )
77- affine1 = np .eye (4 )
78- img1 = nib .Nifti1Image (data1 , affine1 )
56+ img1 = nib .Nifti1Image (data1 , fake_affine )
7957
58+ # 1) If they are all images
8059 data2 = np .zeros ((6 , 6 , 6 )).astype (float )
81- affine2 = np .eye (4 )
82- img2 = nib .Nifti1Image (data2 , affine2 )
83-
84- try :
85- _validate_imgs_concat (img1 , img2 )
86- print ("Test with all valid NIFTI images passed." )
87- except ValueError as e :
88- print ("Test with all valid NIFTI images failed:" , str (e ))
60+ img2 = nib .Nifti1Image (data2 , fake_affine )
61+ _validate_imgs_type (img1 , img2 ) # Should pass
8962
63+ # 2) If one input is not an image
64+ _assert_failed (_validate_imgs_type , img1 , data2 )
9065
91- def test_validate_imgs_concat_not_all ():
92- data1 = np .zeros ((5 , 5 , 5 )).astype (float )
93- affine1 = np .eye (4 )
94- img1 = nib .Nifti1Image (data1 , affine1 )
95-
96- not_an_image = "I am not an image"
9766
98- try :
99- _validate_imgs_concat (img1 , not_an_image )
100- print ("Test with one invalid object passed." )
101- except ValueError as e :
102- print ("Test with one invalid object failed:" , str (e ))
67+ def test_validate_length ():
10368
104-
105- def test_validate_length_exact ():
69+ # 1) Exact length
10670 exact_length_list = [1 , 2 , 3 ]
107- shorter_list = [1 , 2 ]
108-
109- try :
110- _validate_length (exact_length_list , 3 )
111- print ("Test with exact length passed." )
112- except ValueError as e :
113- print ("Test with exact length failed:" , str (e ))
114-
115- try :
116- _validate_length (shorter_list , 3 )
117- print ("Test with shorter length passed." )
118- except ValueError as e :
119- print ("Test with shorter length failed:" , str (e ))
71+ _validate_length (exact_length_list , 3 ) # Should pass
12072
73+ shorter_list = [1 , 2 ]
74+ _assert_failed (_validate_length , shorter_list , 3 )
12175
122- def test_validate_length_at_least ():
76+ # 2) With option 'at_least'
12377 at_least_list = [1 , 2 , 3 , 4 ]
124- shorter_list = [ 1 , 2 ]
78+ _validate_length ( at_least_list , 3 , at_least = True ) # Should pass
12579
126- try :
127- _validate_length (at_least_list , 3 , at_least = True )
128- print ("Test with 'at least' length passed." )
129- except ValueError as e :
130- print ("Test with 'at least' length failed:" , str (e ))
80+ shorter_list = [1 , 2 ]
81+ _assert_failed (_validate_length , shorter_list , 3 )
13182
132- try :
133- _validate_length (shorter_list , 3 , at_least = True )
134- print ("Test with shorter 'at least' length passed." )
135- except ValueError as e :
136- print ("Test with shorter 'at least' length failed:" , str (e ))
13783
84+ def test_validate_type ():
13885
139- def test_validate_type_correct ():
14086 correct_type_input = 42 # Integer
87+ _validate_type (correct_type_input , int ) # Should pass
14188
142- try :
143- _validate_type (correct_type_input , int )
144- print ("Test with correct type passed." )
145- except ValueError :
146- print ("Test with correct type failed." )
147-
148-
149- def test_validate_type_incorrect ():
15089 incorrect_type_input = "42" # String
90+ _assert_failed (_validate_type , incorrect_type_input , int )
15191
152- try :
153- _validate_type (incorrect_type_input , int )
154- print ("Test with incorrect type passed." )
155- except ValueError :
156- print ("Test with incorrect type failed." )
15792
93+ def test_validate_float ():
15894
159- def test_validate_float_correct ():
16095 correct_input = 42 # Integer can be cast to float
96+ _validate_float (correct_input ) # Should pass
16197
162- try :
163- _validate_float (correct_input )
164- print ("Test with correct type passed." )
165- except ValueError :
166- print ("Test with correct type failed." )
167-
168-
169- def test_validate_float_incorrect ():
17098 incorrect_input = "not_a_float" # String that can't be cast to float
171-
172- try :
173- _validate_float (incorrect_input )
174- print ("Test with incorrect type passed." )
175- except ValueError :
176- print ("Test with incorrect type failed." )
177-
178-
179- def test_cut_up_cube_with_known_output ():
180- # Input data: smaller 3x3x3 cube
181- data = np .arange (3 * 3 * 3 ).reshape ((3 , 3 , 3 ))
182- blck = (3 , 3 , 3 )
183-
184- # Running the function
185- result = cut_up_cube (data , blck )
186-
187- # Expected output shape
188- expected_shape = (3 , 3 , 3 , 3 , 3 , 3 )
189-
190- # Expected first block
191- expected_first_block = np .array ([[[0 , 0 , 0 ],
192- [0 , 0 , 0 ],
193- [0 , 0 , 0 ]],
194-
195- [[0 , 0 , 0 ],
196- [0 , 0 , 1 ],
197- [0 , 3 , 4 ]],
198-
199- [[0 , 0 , 0 ],
200- [0 , 9 , 10 ],
201- [0 , 12 , 13 ]]])
202-
203- # Expected last block
204- expected_last_block = np .array ([[[13 , 14 , 0 ],
205- [16 , 17 , 0 ],
206- [0 , 0 , 0 ]],
207-
208- [[22 , 23 , 0 ],
209- [25 , 26 , 0 ],
210- [0 , 0 , 0 ]],
211-
212- [[0 , 0 , 0 ],
213- [0 , 0 , 0 ],
214- [0 , 0 , 0 ]]])
215-
216- # Asserting that the output shape matches the expected shape
217- assert result .shape == expected_shape , \
218- f"Expected shape { expected_shape } , got { result .shape } "
219-
220- # Asserting that the first block matches the expected first block
221- assert_array_equal (result [0 , 0 , 0 , :, :, :], expected_first_block )
222-
223- # Asserting that the last block matches the expected last block
224- assert_array_equal (result [- 1 , - 1 , - 1 , :, :, :], expected_last_block )
99+ _assert_failed (_validate_float , incorrect_input )
225100
226101
227102def test_lower_threshold_eq ():
@@ -243,7 +118,6 @@ def test_lower_threshold_eq():
243118 assert_array_equal (output_data , expected_output )
244119
245120
246- # Test function for upper_threshold_eq
247121def test_upper_threshold_eq ():
248122 # Create a sample nib.Nifti1Image object
249123 img_data = np .array ([0 , 1 , 2 , 3 , 4 , 5 ]).astype (float )
@@ -572,6 +446,91 @@ def test_concatenate():
572446 assert_array_almost_equal (img_data_1 .shape + (2 ,), output_data .shape )
573447
574448
449+ def test_get_neighbors ():
450+ # Input data: small data with NOT the same dimension in each direction.
451+ data = np .arange (3 * 4 * 5 ).reshape ((3 , 4 , 5 )) + 1
452+
453+ # Cutting into patches of radius 1, i.e. 3x3x3.
454+ result = _get_neighbors (data , radius = 1 )
455+
456+ # Expected output shape: Should fit 3 x 4 x 5 patches of shape 3x3x3
457+ expected_shape = (3 , 4 , 5 , 3 , 3 , 3 )
458+ assert result .shape == expected_shape , \
459+ f"Expected shape { expected_shape } , got { result .shape } "
460+
461+ # First block: expecting 8 non-zero values (always true with patch_size 3!)
462+ # (draw a cube and check the number of neighbors of the voxel at the
463+ # corner)
464+ assert np .count_nonzero (result [0 , 0 , 0 , :, :, :]) == 8
465+
466+ # Middle blocks: expecting 27 non-zero values
467+ assert np .count_nonzero (result [1 , 1 , 1 , :, :, :]) == 27
468+
469+ # Comparing with values obtained the day we created this test.
470+ # Expected first block:
471+ expected_first_block = np .array ([[[0 , 0 , 0 ],
472+ [0 , 0 , 0 ],
473+ [0 , 0 , 0 ]],
474+ [[0 , 0 , 0 ],
475+ [0 , 1 , 2 ],
476+ [0 , 6 , 7 ]],
477+ [[0 , 0 , 0 ],
478+ [0 , 21 , 22 ],
479+ [0 , 26 , 27 ]]])
480+ assert_array_equal (result [0 , 0 , 0 , :, :, :], expected_first_block )
481+
482+ # Expected last block
483+ expected_last_block = np .array ([[[34 , 35 , 0 ],
484+ [39 , 40 , 0 ],
485+ [0 , 0 , 0 ]],
486+ [[54 , 55 , 0 ],
487+ [59 , 60 , 0 ],
488+ [0 , 0 , 0 ]],
489+ [[0 , 0 , 0 ],
490+ [0 , 0 , 0 ],
491+ [0 , 0 , 0 ]]])
492+ assert_array_equal (result [- 1 , - 1 , - 1 , :, :, :], expected_last_block )
493+
494+
495+ def test_neighborhood_correlation ():
496+ # Note. Not working on 2D data.
497+ affine = np .eye (4 )
498+
499+ # Test 1: Perfect correlation
500+ # Compares uniform patch of ones with a uniform patch of twos.
501+ # No background.
502+ img_data_1 = np .ones ((3 , 3 , 3 ), dtype = float )
503+ img1 = nib .Nifti1Image (img_data_1 , affine )
504+
505+ img_data_2 = np .ones ((3 , 3 , 3 ), dtype = float ) * 2
506+ img2 = nib .Nifti1Image (img_data_2 , affine )
507+ output = neighborhood_correlation ([img1 , img2 ], img1 )
508+ assert np .allclose (output , np .ones ((3 , 3 , 3 ))), \
509+ "Expected a perfect correlation, got: {}" .format (output )
510+
511+ # Test 2: Bad correlation.
512+ # Compares uniform patch of ones with a noisy patch of twos.
513+ # There should be a poor correlation (cloud of points is horizontal).
514+ # But we notice that around the edges of the image, high correlation (~1),
515+ # as explained in the correlation method's docstring
516+ img_data_2 = np .ones ((3 , 3 , 3 ), dtype = float ) * 2 + \
517+ np .random .rand (3 , 3 , 3 ) * 0.001
518+ img2 = nib .Nifti1Image (img_data_2 , affine )
519+ output = neighborhood_correlation ([img1 , img2 ], img1 )
520+ expected = np .ones ((3 , 3 , 3 ))
521+ expected [1 , 1 , 1 ] = 0
522+ assert np .allclose (output , expected ), \
523+ ("Expected a bad correlation at central point, good around the border,"
524+ " got: {}" ).format (output )
525+
526+ # Test 2: Comparing with only background: should be 0 everywhere.
527+ img_data_2 = np .zeros ((3 , 3 , 3 )).astype (float )
528+ img2 = nib .Nifti1Image (img_data_2 , affine )
529+ output = neighborhood_correlation ([img1 , img2 ], img1 )
530+ assert np .allclose (output , np .zeros ((3 , 3 , 3 ))), \
531+ "Expected a 0 correlation everywhere, got {}" .format (output )
532+
533+
575534def test_dilation ():
576535 img_data = np .array ([0 , 1 ]).astype (float )
577536 affine = np .eye (4 )
0 commit comments