1import numpy as np
2import pytest
3from numpy.testing import assert_almost_equal
4
5from skimage import color, data, draw, feature, img_as_float
6from skimage._shared import filters
7from skimage._shared._warnings import expected_warnings
8from skimage._shared.testing import fetch
9from skimage._shared.utils import _supported_float_type
10
11
12def test_hog_output_size():
13    img = img_as_float(data.astronaut()[:256, :].mean(axis=2))
14
15    fd = feature.hog(img, orientations=9, pixels_per_cell=(8, 8),
16                     cells_per_block=(1, 1), block_norm='L1')
17
18    assert len(fd) == 9 * (256 // 8) * (512 // 8)
19
20
21@pytest.mark.parametrize('dtype', [np.float32, np.float64])
22def test_hog_output_correctness_l1_norm(dtype):
23    img = color.rgb2gray(data.astronaut()).astype(dtype=dtype, copy=False)
24    correct_output = np.load(fetch('data/astronaut_GRAY_hog_L1.npy'))
25
26    output = feature.hog(img, orientations=9, pixels_per_cell=(8, 8),
27                         cells_per_block=(3, 3), block_norm='L1',
28                         feature_vector=True, transform_sqrt=False,
29                         visualize=False)
30    float_dtype = _supported_float_type(dtype)
31    assert output.dtype == float_dtype
32    decimal = 7 if float_dtype == np.float64 else 5
33    assert_almost_equal(output, correct_output, decimal=decimal)
34
35
36@pytest.mark.parametrize('dtype', [np.float32, np.float64])
37def test_hog_output_correctness_l2hys_norm(dtype):
38    img = color.rgb2gray(data.astronaut()).astype(dtype=dtype, copy=False)
39    correct_output = np.load(fetch('data/astronaut_GRAY_hog_L2-Hys.npy'))
40
41    output = feature.hog(img, orientations=9, pixels_per_cell=(8, 8),
42                         cells_per_block=(3, 3), block_norm='L2-Hys',
43                         feature_vector=True, transform_sqrt=False,
44                         visualize=False)
45    float_dtype = _supported_float_type(dtype)
46    assert output.dtype == float_dtype
47    decimal = 7 if float_dtype == np.float64 else 5
48    assert_almost_equal(output, correct_output, decimal=decimal)
49
50
51def test_hog_image_size_cell_size_mismatch():
52    image = data.camera()[:150, :200]
53    fd = feature.hog(image, orientations=9, pixels_per_cell=(8, 8),
54                     cells_per_block=(1, 1), block_norm='L1')
55    assert len(fd) == 9 * (150 // 8) * (200 // 8)
56
57
58def test_hog_odd_cell_size():
59    img = np.zeros((3, 3))
60    img[2, 2] = 1
61
62    correct_output = np.zeros((9, ))
63    correct_output[0] = 0.5
64    correct_output[4] = 0.5
65
66    output = feature.hog(img, pixels_per_cell=(3, 3),
67                         cells_per_block=(1, 1), block_norm='L1')
68
69    assert_almost_equal(output, correct_output, decimal=1)
70
71
72def test_hog_basic_orientations_and_data_types():
73    # scenario:
74    #  1) create image (with float values) where upper half is filled by
75    #     zeros, bottom half by 100
76    #  2) create unsigned integer version of this image
77    #  3) calculate feature.hog() for both images, both with 'transform_sqrt'
78    #     option enabled and disabled
79    #  4) verify that all results are equal where expected
80    #  5) verify that computed feature vector is as expected
81    #  6) repeat the scenario for 90, 180 and 270 degrees rotated images
82
83    # size of testing image
84    width = height = 35
85
86    image0 = np.zeros((height, width), dtype='float')
87    image0[height // 2:] = 100
88
89    for rot in range(4):
90        # rotate by 0, 90, 180 and 270 degrees
91        image_float = np.rot90(image0, rot)
92
93        # create uint8 image from image_float
94        image_uint8 = image_float.astype('uint8')
95
96        (hog_float, hog_img_float) = feature.hog(
97            image_float, orientations=4, pixels_per_cell=(8, 8),
98            cells_per_block=(1, 1), visualize=True, transform_sqrt=False,
99            block_norm='L1')
100        (hog_uint8, hog_img_uint8) = feature.hog(
101            image_uint8, orientations=4, pixels_per_cell=(8, 8),
102            cells_per_block=(1, 1), visualize=True, transform_sqrt=False,
103            block_norm='L1')
104        (hog_float_norm, hog_img_float_norm) = feature.hog(
105            image_float, orientations=4, pixels_per_cell=(8, 8),
106            cells_per_block=(1, 1), visualize=True, transform_sqrt=True,
107            block_norm='L1')
108        (hog_uint8_norm, hog_img_uint8_norm) = feature.hog(
109            image_uint8, orientations=4, pixels_per_cell=(8, 8),
110            cells_per_block=(1, 1), visualize=True, transform_sqrt=True,
111            block_norm='L1')
112
113        # set to True to enable manual debugging with graphical output,
114        # must be False for automatic testing
115        if False:
116            import matplotlib.pyplot as plt
117            plt.figure()
118            plt.subplot(2, 3, 1)
119            plt.imshow(image_float)
120            plt.colorbar()
121            plt.title('image')
122            plt.subplot(2, 3, 2)
123            plt.imshow(hog_img_float)
124            plt.colorbar()
125            plt.title('HOG result visualisation (float img)')
126            plt.subplot(2, 3, 5)
127            plt.imshow(hog_img_uint8)
128            plt.colorbar()
129            plt.title('HOG result visualisation (uint8 img)')
130            plt.subplot(2, 3, 3)
131            plt.imshow(hog_img_float_norm)
132            plt.colorbar()
133            plt.title('HOG result (transform_sqrt) visualisation (float img)')
134            plt.subplot(2, 3, 6)
135            plt.imshow(hog_img_uint8_norm)
136            plt.colorbar()
137            plt.title('HOG result (transform_sqrt) visualisation (uint8 img)')
138            plt.show()
139
140        # results (features and visualisation) for float and uint8 images must
141        # be almost equal
142        assert_almost_equal(hog_float, hog_uint8)
143        assert_almost_equal(hog_img_float, hog_img_uint8)
144
145        # resulting features should be almost equal
146        # when 'transform_sqrt' is enabled
147        # or disabled (for current simple testing image)
148        assert_almost_equal(hog_float, hog_float_norm, decimal=4)
149        assert_almost_equal(hog_float, hog_uint8_norm, decimal=4)
150
151        # reshape resulting feature vector to matrix with 4 columns (each
152        # corresponding to one of 4 directions); only one direction should
153        # contain nonzero values (this is manually determined for testing
154        # image)
155        actual = np.max(hog_float.reshape(-1, 4), axis=0)
156
157        if rot in [0, 2]:
158            # image is rotated by 0 and 180 degrees
159            desired = [0, 0, 1, 0]
160        elif rot in [1, 3]:
161            # image is rotated by 90 and 270 degrees
162            desired = [1, 0, 0, 0]
163        else:
164            raise Exception('Result is not determined for this rotation.')
165
166        assert_almost_equal(actual, desired, decimal=2)
167
168
169def test_hog_orientations_circle():
170    # scenario:
171    #  1) create image with blurred circle in the middle
172    #  2) calculate feature.hog()
173    #  3) verify that the resulting feature vector contains uniformly
174    #     distributed values for all orientations, i.e. no orientation is
175    #     lost or emphasized
176    #  4) repeat the scenario for other 'orientations' option
177
178    # size of testing image
179    width = height = 100
180
181    image = np.zeros((height, width))
182    rr, cc = draw.disk((int(height / 2), int(width / 2)), int(width / 3))
183    image[rr, cc] = 100
184    image = filters.gaussian(image, 2, mode='reflect')
185
186    for orientations in range(2, 15):
187        (hog, hog_img) = feature.hog(image, orientations=orientations,
188                                     pixels_per_cell=(8, 8),
189                                     cells_per_block=(1, 1), visualize=True,
190                                     transform_sqrt=False,
191                                     block_norm='L1')
192
193        # set to True to enable manual debugging with graphical output,
194        # must be False for automatic testing
195        if False:
196            import matplotlib.pyplot as plt
197            plt.figure()
198            plt.subplot(1, 2, 1)
199            plt.imshow(image)
200            plt.colorbar()
201            plt.title('image_float')
202            plt.subplot(1, 2, 2)
203            plt.imshow(hog_img)
204            plt.colorbar()
205            plt.title('HOG result visualisation, '
206                      'orientations=%d' % (orientations))
207            plt.show()
208
209        # reshape resulting feature vector to matrix with N columns (each
210        # column corresponds to one direction),
211        hog_matrix = hog.reshape(-1, orientations)
212
213        # compute mean values in the resulting feature vector for each
214        # direction, these values should be almost equal to the global mean
215        # value (since the image contains a circle), i.e., all directions have
216        # same contribution to the result
217        actual = np.mean(hog_matrix, axis=0)
218        desired = np.mean(hog_matrix)
219        assert_almost_equal(actual, desired, decimal=1)
220
221
222def test_hog_visualization_orientation():
223    """Test that the visualization produces a line with correct orientation
224
225    The hog visualization is expected to draw line segments perpendicular to
226    the midpoints of orientation bins.  This example verifies that when
227    orientations=3 and the gradient is entirely in the middle bin (bisected
228    by the y-axis), the line segment drawn by the visualization is horizontal.
229    """
230
231    width = height = 11
232
233    image = np.zeros((height, width), dtype='float')
234    image[height // 2:] = 1
235
236    _, hog_image = feature.hog(
237        image,
238        orientations=3,
239        pixels_per_cell=(width, height),
240        cells_per_block=(1, 1),
241        visualize=True,
242        block_norm='L1'
243    )
244
245    middle_index = height // 2
246    indices_excluding_middle = [x for x in range(height) if x != middle_index]
247
248    assert (hog_image[indices_excluding_middle, :] == 0).all()
249    assert (hog_image[middle_index, 1:-1] > 0).all()
250
251
252def test_hog_block_normalization_incorrect_error():
253    img = np.eye(4)
254    with pytest.raises(ValueError):
255        feature.hog(img, block_norm='Linf')
256
257
258@pytest.mark.parametrize("shape,multichannel", [
259    ((3, 3, 3), False),
260    ((3, 3), True),
261    ((3, 3, 3, 3), True),
262])
263def test_hog_incorrect_dimensions(shape, multichannel):
264    img = np.zeros(shape)
265    with pytest.raises(ValueError):
266        with expected_warnings(["`multichannel` is a deprecated argument"]):
267            feature.hog(img, multichannel=multichannel, block_norm='L1')
268
269
270def test_hog_output_equivariance_deprecated_multichannel():
271    img = data.astronaut()
272    img[:, :, (1, 2)] = 0
273    with expected_warnings(["`multichannel` is a deprecated argument"]):
274        hog_ref = feature.hog(img, multichannel=True, block_norm='L1')
275
276    for n in (1, 2):
277        with expected_warnings(["`multichannel` is a deprecated argument"]):
278            hog_fact = feature.hog(np.roll(img, n, axis=2), multichannel=True,
279                                   block_norm='L1')
280        assert_almost_equal(hog_ref, hog_fact)
281
282        # repeat prior test, but check for positional multichannel warning
283        with expected_warnings(["Providing the `multichannel` argument"]):
284            hog_fact = feature.hog(np.roll(img, n, axis=2), 9, (8, 8), (3, 3),
285                                   'L1', False, False, True, True)
286        assert_almost_equal(hog_ref, hog_fact)
287
288
289@pytest.mark.parametrize('channel_axis', [0, 1, -1, -2])
290def test_hog_output_equivariance_channel_axis(channel_axis):
291    img = data.astronaut()[:64, :32]
292    img[:, :, (1, 2)] = 0
293    img = np.moveaxis(img, -1, channel_axis)
294    hog_ref = feature.hog(img, channel_axis=channel_axis, block_norm='L1')
295
296    for n in (1, 2):
297        hog_fact = feature.hog(np.roll(img, n, axis=channel_axis),
298                               channel_axis=channel_axis, block_norm='L1')
299        assert_almost_equal(hog_ref, hog_fact)
300