diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 5961407d..735bffa9 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -108,3 +108,6 @@ Adaptive thresholding Implementation of Matlab's `regionprops` Estimation of geometric transformation parameters + +- Pavel Campr + Fixes and tests for Histograms of Oriented Gradients. diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index 5206034e..ce7db462 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -59,7 +59,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), shadowing and illumination variations. """ - if image.ndim > 3: + if image.ndim > 2: raise ValueError("Currently only supports grey-level images") if normalise: @@ -75,6 +75,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), e.g. bar like structures in bicycles and limbs in humans. """ + if image.dtype.kind == 'u': + # convert uint image to float + # to avoid problems with subtracting unsigned numbers in np.diff() + image = image.astype('float') + gx = np.zeros(image.shape) gy = np.zeros(image.shape) gx[:, :-1] = np.diff(image, n=1, axis=1) @@ -96,7 +101,7 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), """ magnitude = sqrt(gx**2 + gy**2) - orientation = arctan2(gy, (gx + 1e-15)) * (180 / pi) + 90 + orientation = arctan2(gy, gx) * (180 / pi) % 180 sy, sx = image.shape cx, cy = pixels_per_cell @@ -113,11 +118,11 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), # isolate orientations in this range temp_ori = np.where(orientation < 180 / orientations * (i + 1), - orientation, 0) + orientation, -1) temp_ori = np.where(orientation >= 180 / orientations * i, - temp_ori, 0) + temp_ori, -1) # select magnitudes for those orientations - cond2 = temp_ori > 0 + cond2 = temp_ori > -1 temp_mag = np.where(cond2, magnitude, 0) temp_filt = uniform_filter(temp_mag, size=(cy, cx)) @@ -137,8 +142,8 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), centre = tuple([y * cy + cy // 2, x * cx + cx // 2]) dx = radius * cos(float(o) / orientations * np.pi) dy = radius * sin(float(o) / orientations * np.pi) - rr, cc = draw.bresenham(centre[0] - dx, centre[1] - dy, - centre[0] + dx, centre[1] + dy) + rr, cc = draw.bresenham(centre[0] - dy, centre[1] - dx, + centre[0] + dy, centre[1] + dx) hog_image[rr, cc] += orientation_histogram[y, x, o] """ diff --git a/skimage/feature/tests/test_hog.py b/skimage/feature/tests/test_hog.py index 90e08105..c9449028 100644 --- a/skimage/feature/tests/test_hog.py +++ b/skimage/feature/tests/test_hog.py @@ -1,6 +1,10 @@ +import numpy as np +from scipy import ndimage from skimage import data from skimage import feature from skimage import img_as_float +from skimage.draw import draw +from numpy.testing import * def test_histogram_of_oriented_gradients(): img = img_as_float(data.lena()[:256, :].mean(axis=2)) @@ -16,6 +20,115 @@ def test_hog_image_size_cell_size_mismatch(): cells_per_block=(1, 1)) assert len(fd) == 9 * (150 // 8) * (200 // 8) +def test_hog_color_image_unsupported_error(): + image = np.zeros((20, 20, 3)) + assert_raises(ValueError, feature.hog, image) + +def test_hog_basic_orientations_and_data_types(): + # scenario: + # 1) create image (with float values) where upper half is filled by zeros, bottom half by 100 + # 2) create unsigned integer version of this image + # 3) calculate feature.hog() for both images, both with 'normalise' option enabled and disabled + # 4) verify that all results are equal where expected + # 5) verify that computed feature vector is as expected + # 6) repeat the scenario for 90, 180 and 270 degrees rotated images + + # size of testing image + width = height = 35 + + image0 = np.zeros((height, width), dtype='float') + image0[height / 2:] = 100 + + for rot in range(4): + # rotate by 0, 90, 180 and 270 degrees + image_float = np.rot90(image0, rot) + + # create uint8 image from image_float + image_uint8 = image_float.astype('uint8') + + (hog_float, hog_img_float) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + (hog_uint8, hog_img_uint8) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + (hog_float_norm, hog_img_float_norm) = feature.hog(image_float, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=True) + (hog_uint8_norm, hog_img_uint8_norm) = feature.hog(image_uint8, orientations=4, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=True) + + # set to True to enable manual debugging with graphical output, + # must be False for automatic testing + if False: + import matplotlib.pyplot as plt + plt.figure() + plt.subplot(2, 3, 1); plt.imshow(image_float); plt.colorbar(); plt.title('image') + plt.subplot(2, 3, 2); plt.imshow(hog_img_float); plt.colorbar(); plt.title('HOG result visualisation (float img)') + plt.subplot(2, 3, 5); plt.imshow(hog_img_uint8); plt.colorbar(); plt.title('HOG result visualisation (uint8 img)') + plt.subplot(2, 3, 3); plt.imshow(hog_img_float_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (float img)') + plt.subplot(2, 3, 6); plt.imshow(hog_img_uint8_norm); plt.colorbar(); plt.title('HOG result (normalise) visualisation (uint8 img)') + plt.show() + + # results (features and visualisation) for float and uint8 images must be almost equal + assert_almost_equal(hog_float, hog_uint8) + assert_almost_equal(hog_img_float, hog_img_uint8) + + # resulting features should be almost equal when 'normalise' is enabled or disabled (for current simple testing image) + assert_almost_equal(hog_float, hog_float_norm, decimal=4) + assert_almost_equal(hog_float, hog_uint8_norm, decimal=4) + + # reshape resulting feature vector to matrix with 4 columns (each corresponding to one of 4 directions), + # only one direction should contain nonzero values (this is manually determined for testing image) + actual = np.max(hog_float.reshape(-1, 4), axis=0) + + if rot in [0, 2]: + # image is rotated by 0 and 180 degrees + desired = [0, 0, 1, 0] + elif rot in [1, 3]: + # image is rotated by 90 and 270 degrees + desired = [1, 0, 0, 0] + else: + raise Exception('Result is not determined for this rotation.') + + assert_almost_equal(actual, desired, decimal=2) + +def test_hog_orientations_circle(): + # scenario: + # 1) create image with blurred circle in the middle + # 2) calculate feature.hog() + # 3) verify that the resulting feature vector contains uniformly distributed values for all orientations, + # i.e. no orientation is lost or emphasized + # 4) repeat the scenario for other 'orientations' option + + # size of testing image + width = height = 100 + + image = np.zeros((height, width)) + rr, cc = draw.circle(height/2, width/2, width/3) + image[rr, cc] = 100 + image = ndimage.gaussian_filter(image, 2) + + for orientations in range(2, 15): + (hog, hog_img) = feature.hog(image, orientations=orientations, pixels_per_cell=(8, 8), + cells_per_block=(1, 1), visualise=True, normalise=False) + + # set to True to enable manual debugging with graphical output, + # must be False for automatic testing + if False: + import matplotlib.pyplot as plt + plt.figure() + plt.subplot(1, 2, 1); plt.imshow(image); plt.colorbar(); plt.title('image_float') + plt.subplot(1, 2, 2); plt.imshow(hog_img); plt.colorbar(); plt.title('HOG result visualisation, orientations=%d' % (orientations)) + plt.show() + + # reshape resulting feature vector to matrix with N columns (each column corresponds to one direction), + hog_matrix = hog.reshape(-1, orientations) + + # compute mean values in the resulting feature vector for each direction, + # these values should be almost equal to the global mean value (since the image contains a circle), + # i.e. all directions have same contribution to the result + actual = np.mean(hog_matrix, axis=0) + desired = np.mean(hog_matrix) + assert_almost_equal(actual, desired, decimal=1) + if __name__ == '__main__': from numpy.testing import run_module_suite run_module_suite()