├── .gitignore ├── README.md ├── hog ├── __init__.py ├── example_histogram.py ├── histogram.py └── tests │ ├── __init__.py │ └── test_histogram.py ├── images └── interpolation_illustration.jpg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-hog 2 | 3 | Vectorised implementation of the Histogram of Oriented Gradients 4 | 5 | ## How to install 6 | 7 | ### Option 1: clone the repository and install 8 | 9 | Clone the repository and cd there: 10 | ```bash 11 | git clone https://github.com/JeanKossaifi/python-hog 12 | cd python-hog 13 | ``` 14 | Then install the package (here in editable mode with `-e` or equivalently `--editable`: 15 | ```bash 16 | pip install -e . 17 | ``` 18 | 19 | ### Option 2: install with pip 20 | 21 | Simply run 22 | ```bash 23 | pip install git+https://github.com/JeanKossaifi/python-hog 24 | ``` 25 | 26 | ## Idea behind the implementation 27 | 28 | Because of time-constraints the code might not be as clear as it should be but here is the idea behind it: 29 | 30 | ### Vectorising the tri-linear interpolation 31 | Assuming we have a gray-scale image represented as an ndarray of shape `(sy, sx)`. 32 | We want to compute the HOG features of that image with `nbins` orientation bins. 33 | 34 | First, we interpolate between the bins, resulting in a `(sy, sy, nbins)` array. 35 | 36 | We then interpolate spatially. The key observation is that in the end (after interpolation), we do not care about the position of the orientation vectors since all orientation vector for a given cell are going to be summed to obtain only one histogram per cell. 37 | We can thus virtually divide each cell in 4, each part being interpolated in the 4 diagonally adjacent sub-cells. 38 | As a result, each of the 4 sub-cell will be interpolated once in the same cell, and once in the 3 adjacent cells (which is exactly what interpolation is). 39 | The only thing to do is to multiply by the right coefficient. 40 | 41 | To illustrated: We sum 4 times in the 4 diagonal directions. The coefficient for the sum can be represented by a single matrix which is turned. 42 | ![Illustration](./images/interpolation_illustration.jpg) 43 | 44 | Finally the histograms in each cell are summed to obtain the `(n_cells_x, n_cells_y, nbins)` desired orientation_histogram (which can be further normalise block-wise). 45 | -------------------------------------------------------------------------------- /hog/__init__.py: -------------------------------------------------------------------------------- 1 | from .histogram import hog 2 | -------------------------------------------------------------------------------- /hog/example_histogram.py: -------------------------------------------------------------------------------- 1 | # Author: Jean KOSSAIFI 2 | 3 | from histogram import gradient, magnitude_orientation, hog, visualise_histogram 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | 7 | def octagon(): 8 | return np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 9 | [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], 10 | [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], 11 | [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], 12 | [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], 13 | [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], 14 | [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], 15 | [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], 16 | [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], 17 | [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], 18 | [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], 19 | [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], 20 | [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], 21 | [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], 22 | [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], 23 | [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], 24 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) 25 | 26 | 27 | img = octagon() 28 | gx, gy = gradient(img, same_size=False) 29 | mag, ori = magnitude_orientation(gx, gy) 30 | 31 | # Show gradient and magnitude 32 | plt.figure() 33 | plt.title('gradients and magnitude') 34 | plt.subplot(141) 35 | plt.imshow(img, cmap=plt.cm.Greys_r) 36 | plt.subplot(142) 37 | plt.imshow(gx, cmap=plt.cm.Greys_r) 38 | plt.subplot(143) 39 | plt.imshow(gy, cmap=plt.cm.Greys_r) 40 | plt.subplot(144) 41 | plt.imshow(mag, cmap=plt.cm.Greys_r) 42 | 43 | 44 | # Show the orientation deducted from gradient 45 | plt.figure() 46 | plt.title('orientations') 47 | plt.imshow(ori) 48 | plt.pcolor(ori) 49 | plt.colorbar() 50 | 51 | 52 | # Plot histogram 53 | from scipy.ndimage.interpolation import zoom 54 | # make the image bigger to compute the histogram 55 | im1 = zoom(octagon(), 3) 56 | h = hog(im1, cell_size=(2, 2), cells_per_block=(1, 1), visualise=False, nbins=9, signed_orientation=False, normalise=True) 57 | im2 = visualise_histogram(h, 8, 8, False) 58 | 59 | plt.figure() 60 | plt.title('HOG features') 61 | plt.imshow(im2, cmap=plt.cm.Greys_r) 62 | 63 | plt.show() 64 | -------------------------------------------------------------------------------- /hog/histogram.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | # Author: Jean KOSSAIFI 4 | 5 | import numpy as np 6 | from numpy import arctan2, fliplr, flipud 7 | 8 | 9 | def gradient(image, same_size=False): 10 | """ Computes the Gradients of the image separated pixel difference 11 | 12 | Gradient of X is computed using the filter 13 | [-1, 0, 1] 14 | Gradient of X is computed using the filter 15 | [[1, 16 | 0, 17 | -1]] 18 | 19 | Parameters 20 | ---------- 21 | image: image of shape (imy, imx) 22 | same_size: boolean, optional, default is True 23 | If True, boundaries are duplicated so that the gradients 24 | has the same size as the original image. 25 | Otherwise, the gradients will have shape (imy-2, imx-2) 26 | 27 | Returns 28 | ------- 29 | (Gradient X, Gradient Y), two numpy array with the same shape as image 30 | (if same_size=True) 31 | """ 32 | sy, sx = image.shape 33 | if same_size: 34 | gx = np.zeros(image.shape) 35 | gx[:, 1:-1] = -image[:, :-2] + image[:, 2:] 36 | gx[:, 0] = -image[:, 0] + image[:, 1] 37 | gx[:, -1] = -image[:, -2] + image[:, -1] 38 | 39 | gy = np.zeros(image.shape) 40 | gy[1:-1, :] = image[:-2, :] - image[2:, :] 41 | gy[0, :] = image[0, :] - image[1, :] 42 | gy[-1, :] = image[-2, :] - image[-1, :] 43 | 44 | else: 45 | gx = np.zeros((sy-2, sx-2)) 46 | gx[:, :] = -image[1:-1, :-2] + image[1:-1, 2:] 47 | 48 | gy = np.zeros((sy-2, sx-2)) 49 | gy[:, :] = image[:-2, 1:-1] - image[2:, 1:-1] 50 | 51 | return gx, gy 52 | 53 | 54 | def magnitude_orientation(gx, gy): 55 | """ Computes the magnitude and orientation matrices from the gradients gx gy 56 | 57 | Parameters 58 | ---------- 59 | gx: gradient following the x axis of the image 60 | gy: gradient following the y axis of the image 61 | 62 | Returns 63 | ------- 64 | (magnitude, orientation) 65 | 66 | Warning 67 | ------- 68 | The orientation is in degree, NOT radian!! 69 | """ 70 | 71 | magnitude = np.sqrt(gx**2 + gy**2) 72 | orientation = (arctan2(gy, gx) * 180 / np.pi) % 360 73 | 74 | return magnitude, orientation 75 | 76 | 77 | def compute_coefs(csx, csy, dx, dy, n_cells_x, n_cells_y): 78 | """ 79 | Computes the coefficients for the bilinear (spatial) interpolation 80 | 81 | Parameters 82 | ---------- 83 | csx: int 84 | number of columns of the cells 85 | csy: int 86 | number of raws dimension of the cells 87 | sx: int 88 | number of colums of the image (x axis) 89 | sy: int 90 | number of raws of the image (y axis) 91 | n_cells_x: int 92 | number of cells in the x axis 93 | n_cells_y: int 94 | number of cells in the y axis 95 | 96 | Notes 97 | ----- 98 | We consider an image: image[y, x] (NOT image[x, y]!!!) 99 | 100 | /!\ csx and csy must be even number 101 | 102 | Using the coefficients 103 | ---------------------- 104 | The coefficient correspond to the interpolation in direction of the upper left corner of the image. 105 | In other words, if you interpolate img, and res is the result of your interpolation, you should do 106 | 107 | res = zeros(n_cells_y*pixels_per_cell, n_cells_x*pixels_per_cell) 108 | with (csx, csy) the number of pixels per cell 109 | and dx, dy = csx//2, csy//2 110 | res[:-dx, :-dy] += img[dx:, dy:]*coefs 111 | 112 | then you rotate the coefs and do the same thing for every part of the image 113 | """ 114 | if csx != csy: 115 | raise NotImplementedError("For now compute_coefs is only implemented for squared cells (csx == csy)") 116 | 117 | ################################ 118 | ##### /!\ TODO /!| ##### 119 | ################################ 120 | 121 | else: # Squared cells 122 | # Note: in this case, dx = dy, we differentiate them only to make the code clearer 123 | 124 | # We want a squared coefficients matrix so that it can be rotated to interpolate in every direction 125 | n_cells = max(n_cells_x, n_cells_y) 126 | 127 | # Every cell of this matrix corresponds to (x - x_1)/dx 128 | x = (np.arange(dx)+0.5)/csx 129 | 130 | # Every cell of this matrix corresponds to (y - y_1)/dy 131 | y = (np.arange(dy)+0.5)/csy 132 | 133 | y = y[np.newaxis, :] 134 | x = x[:, np.newaxis] 135 | 136 | # CENTRAL COEFFICIENT 137 | ccoefs = np.zeros((csy, csx)) 138 | 139 | ccoefs[:dy, :dx] = (1 - x)*(1 - y) 140 | ccoefs[:dy, -dx:] = fliplr(y)*(1 - x) 141 | ccoefs[-dy:, :dx] = (1 - y)*flipud(x) 142 | ccoefs[-dy:, -dx:] = fliplr(y)*flipud(x) 143 | 144 | coefs = np.zeros((csx*n_cells - dx, csy*n_cells - dy)) 145 | coefs[:-dy, :-dx] = np.tile(ccoefs, (n_cells - 1, n_cells - 1)) 146 | 147 | # REST OF THE BORDER 148 | coefs[:-dy, -dx:] = np.tile(np.concatenate(((1 - x), np.flipud(x))), (n_cells - 1, dy)) 149 | coefs[-dy:, :-dx] = np.tile(np.concatenate(((1 - y), np.fliplr(y)), axis=1), (dx, n_cells - 1)) 150 | coefs[-dy:, -dx:] = 1 151 | 152 | return coefs 153 | 154 | 155 | def interpolate_orientation(orientation, sx, sy, nbins, signed_orientation): 156 | """ interpolates linearly the orientations to their corresponding bins 157 | 158 | Parameters 159 | ---------- 160 | sx: int 161 | number of columns of the image (x axis) 162 | sy: int 163 | number of raws of the image (y axis) 164 | nbins : int, optional, default is 9 165 | Number of orientation bins. 166 | signed_orientation: bool, default is True 167 | if True, sign information of the orientation is preserved, 168 | ie orientation angles are between 0 and 360 degree. 169 | if False, the angles are between 0 and 180 degree. 170 | 171 | Returns 172 | ------- 173 | pre-histogram: array of shape (sx, sy, nbins) 174 | contains the pre histogram of orientation built using linear interpolation 175 | to interpolate the orientations to their bins 176 | """ 177 | 178 | if signed_orientation: 179 | max_angle = 360 180 | else: 181 | max_angle = 180 182 | 183 | b_step = max_angle/nbins 184 | b0 = (orientation % max_angle) // b_step 185 | b0[np.where(b0>=nbins)]=0 186 | b1 = b0 + 1 187 | b1[np.where(b1>=nbins)]=0 188 | b = np.abs(orientation % b_step) / b_step 189 | 190 | #linear interpolation between the bins 191 | # Coefficients corresponding to the bin interpolation 192 | # We go from an image to a higher dimension representation of size (sizex, sizey, nbins) 193 | temp_coefs = np.zeros((sy, sx, nbins)) 194 | for i in range(nbins): 195 | temp_coefs[:, :, i] += np.where(b0==i, (1 - b), 0) 196 | temp_coefs[:, :, i] += np.where(b1==i, b, 0) 197 | 198 | return temp_coefs 199 | 200 | 201 | def per_pixel_hog(image, dy=2, dx=2, signed_orientation=False, nbins=9, flatten=False, normalise=True): 202 | """ builds a histogram of orientation for a cell centered around each pixel of the image 203 | 204 | Parameters 205 | --------- 206 | image: numpy array of shape (sizey, sizex) 207 | dx : the cell around each pixel in the x axis will have size 2*dx+1 208 | dy : the cell around each pixel in the y axis will have size 2*dy+1 209 | signed_orientation: bool, default is True 210 | if True, sign information of the orientation is preserved, 211 | ie orientation angles are between 0 and 360 degree. 212 | if False, the angles are between 0 and 180 degree. 213 | nbins : int, optional, default is 9 214 | Number of orientation bins. 215 | 216 | Returns 217 | ------- 218 | if visualise if True: (histogram of oriented gradient, visualisation image) 219 | 220 | histogram of oriented gradient: 221 | numpy array of shape (n_cells_y, n_cells_x, nbins), flattened if flatten is True 222 | """ 223 | gx, gy = gradient(image, same_size=True) 224 | magnitude, orientation = magnitude_orientation(gx, gy) 225 | sy, sx = image.shape 226 | orientations_image = interpolate_orientation(orientation, sx, sy, nbins, signed_orientation) 227 | for j in range(1, dy): 228 | for i in range(1, dx): 229 | orientations_image[:-j, :-i, :] += orientations_image[j:, i:, :] 230 | 231 | if normalise: 232 | normalised_blocks = normalise_histogram(orientations_image, 1, 1, 1, 1, nbins) 233 | else: 234 | normalised_blocks = orientations_image 235 | 236 | if flatten: 237 | normalised_blocks = normalised_blocks.flatten() 238 | 239 | return normalised_blocks 240 | 241 | 242 | def interpolate(magnitude, orientation, csx, csy, sx, sy, n_cells_x, n_cells_y, signed_orientation=False, nbins=9): 243 | """ Returns a matrix of size (cell_size_x, cell_size_y, nbins) corresponding 244 | to the trilinear interpolation of the pixels magnitude and orientation 245 | 246 | Parameters 247 | ---------- 248 | csx: int 249 | number of columns of the cells 250 | csy: int 251 | number of raws dimension of the cells 252 | sx: int 253 | number of colums of the image (x axis) 254 | sy: int 255 | number of raws of the image (y axis) 256 | n_cells_x: int 257 | number of cells in the x axis 258 | n_cells_y: int 259 | number of cells in the y axis 260 | signed_orientation: bool, default is True 261 | if True, sign information of the orientation is preserved, 262 | ie orientation angles are between 0 and 360 degree. 263 | if False, the angles are between 0 and 180 degree. 264 | nbins : int, optional, default is 9 265 | Number of orientation bins. 266 | 267 | Returns 268 | ------- 269 | orientation_histogram: array of shape (n_cells_x, n_cells_y, nbins) 270 | contains the histogram of orientation built using tri-linear interpolation 271 | """ 272 | 273 | dx = csx//2 274 | dy = csy//2 275 | 276 | temp_coefs = interpolate_orientation(orientation, sx, sy, nbins, signed_orientation) 277 | 278 | 279 | # Coefficients of the spatial interpolation in every direction 280 | coefs = compute_coefs(csx, csy, dx, dy, n_cells_x, n_cells_y) 281 | 282 | temp = np.zeros((sy, sx, nbins)) 283 | # hist(y0, x0) 284 | temp[:-dy, :-dx, :] += temp_coefs[dy:, dx:, :]*\ 285 | (magnitude[dy:, dx:]*coefs[-(n_cells_y*csy - dy):, -(n_cells_x*csx - dx):])[:, :, np.newaxis] 286 | 287 | # hist(y1, x0) 288 | coefs = np.rot90(coefs) 289 | temp[dy:, :-dx, :] += temp_coefs[:-dy, dx:, :]*\ 290 | (magnitude[:-dy, dx:]*coefs[:(n_cells_y*csy - dy), -(n_cells_x*csx - dx):])[:, :, np.newaxis] 291 | 292 | # hist(y1, x1) 293 | coefs = np.rot90(coefs) 294 | temp[dy:, dx:, :] += temp_coefs[:-dy, :-dx, :]*\ 295 | (magnitude[:-dy, :-dx]*coefs[:(n_cells_y*csy - dy), :(n_cells_x*csx - dx)])[:, :, np.newaxis] 296 | 297 | # hist(y0, x1) 298 | coefs = np.rot90(coefs) 299 | temp[:-dy, dx:, :] += temp_coefs[dy:, :-dx, :]*\ 300 | (magnitude[dy:, :-dx]*coefs[-(n_cells_y*csy - dy):, :(n_cells_x*csx - dx)])[:, :, np.newaxis] 301 | 302 | # Compute the histogram: sum over the cells 303 | orientation_histogram = temp.reshape((n_cells_y, csy, n_cells_x, csx, nbins)).sum(axis=3).sum(axis=1) 304 | 305 | return orientation_histogram 306 | 307 | 308 | def draw_histogram(hist, csx, csy, signed_orientation=False): 309 | """ simple function to draw an orientation histogram 310 | with arrows 311 | """ 312 | import matplotlib.pyplot as plt 313 | 314 | if signed_orientation: 315 | max_angle = 2*np.pi 316 | else: 317 | max_angle = np.pi 318 | 319 | n_cells_y, n_cells_x, nbins = hist.shape 320 | sx, sy = n_cells_x*csx, n_cells_y*csy 321 | plt.close() 322 | plt.figure()#figsize=(sx/2, sy/2))#, dpi=1) 323 | plt.xlim(0, sx) 324 | plt.ylim(sy, 0) 325 | center = csx//2, csy//2 326 | b_step = max_angle / nbins 327 | 328 | for i in range(n_cells_y): 329 | for j in range(n_cells_x): 330 | for k in range(nbins): 331 | if hist[i, j, k] != 0: 332 | width = 1*hist[i, j, k] 333 | plt.arrow((center[0] + j*csx) - np.cos(b_step*k)*(center[0] - 1), 334 | (center[1] + i*csy) + np.sin(b_step*k)*(center[1] - 1), 335 | 2*np.cos(b_step*k)*(center[0] - 1), -2*np.sin(b_step*k)*(center[1] - 1), 336 | width=width, color=str(width), #'black', 337 | head_width=2.2*width, head_length=2.2*width, 338 | length_includes_head=True) 339 | 340 | plt.show() 341 | 342 | 343 | def visualise_histogram(hist, csx, csy, signed_orientation=False): 344 | """ Create an image visualisation of the histogram of oriented gradient 345 | 346 | Parameters 347 | ---------- 348 | hist: numpy array of shape (n_cells_y, n_cells_x, nbins) 349 | histogram of oriented gradient 350 | csx: int 351 | number of columns of the cells 352 | csy: int 353 | number of raws dimension of the cells 354 | signed_orientation: bool, default is True 355 | if True, sign information of the orientation is preserved, 356 | ie orientation angles are between 0 and 360 degree. 357 | if False, the angles are between 0 and 180 degree. 358 | 359 | Return 360 | ------ 361 | Image of shape (hist.shape[0]*csy, hist.shape[1]*csx) 362 | """ 363 | from skimage import draw 364 | 365 | if signed_orientation: 366 | max_angle = 2*np.pi 367 | else: 368 | max_angle = np.pi 369 | 370 | n_cells_y, n_cells_x, nbins = hist.shape 371 | sx, sy = n_cells_x*csx, n_cells_y*csy 372 | center = csx//2, csy//2 373 | b_step = max_angle / nbins 374 | 375 | radius = min(csx, csy) // 2 - 1 376 | hog_image = np.zeros((sy, sx), dtype=float) 377 | for x in range(n_cells_x): 378 | for y in range(n_cells_y): 379 | for o in range(nbins): 380 | centre = tuple([y * csy + csy // 2, x * csx + csx // 2]) 381 | dx = radius * np.cos(o*nbins) 382 | dy = radius * np.sin(o*nbins) 383 | rr, cc = draw.line(int(centre[0] - dy), 384 | int(centre[1] - dx), 385 | int(centre[0] + dy), 386 | int(centre[1] + dx)) 387 | hog_image[rr, cc] += hist[y, x, o] 388 | return hog_image 389 | 390 | 391 | def normalise_histogram(orientation_histogram, bx, by, n_cells_x, n_cells_y, nbins): 392 | """ normalises a histogram by blocks 393 | 394 | Parameters 395 | ---------- 396 | bx: int 397 | number of blocks on the x axis 398 | by: int 399 | number of blocks on the y axis 400 | n_cells_x: int 401 | number of cells in the x axis 402 | n_cells_y: int 403 | number of cells in the y axis 404 | nbins : int, optional, default is 9 405 | Number of orientation bins. 406 | 407 | The normalisation is done according to Dalal's original thesis, using L2-Hys. 408 | In other words the histogram is first normalised block-wise using l2 norm, before clipping it by 409 | limiting the values between 0 and 0.02 and finally normalising again with l2 norm 410 | """ 411 | eps = 1e-7 412 | 413 | if bx==1 and by==1: #faster version 414 | normalised_blocks = np.clip( 415 | orientation_histogram / np.sqrt((orientation_histogram**2).sum(axis=-1) + eps)[:, :, np.newaxis], 0, 0.2) 416 | normalised_blocks /= np.sqrt((normalised_blocks**2).sum(axis=-1) + eps)[:, :, np.newaxis] 417 | 418 | else: 419 | n_blocksx = (n_cells_x - bx) + 1 420 | n_blocksy = (n_cells_y - by) + 1 421 | normalised_blocks = np.zeros((n_blocksy, n_blocksx, nbins)) 422 | 423 | for x in range(n_blocksx): 424 | for y in range(n_blocksy): 425 | block = orientation_histogram[y:y + by, x:x + bx, :] 426 | normalised_blocks[y, x, :] = np.clip(block[0, 0, :] / np.sqrt((block**2).sum() + eps), 0, 0.2) 427 | normalised_blocks[y, x, :] /= np.sqrt((normalised_blocks[y, x, :]**2).sum() + eps) 428 | 429 | return normalised_blocks 430 | 431 | 432 | def build_histogram(magnitude, orientation, cell_size=(8, 8), signed_orientation=False, 433 | nbins=9, cells_per_block=(1, 1), visualise=False, flatten=False, normalise=True): 434 | """ builds a histogram of orientation using the provided magnitude and orientation matrices 435 | 436 | Parameters 437 | --------- 438 | magnitude: np-array of size (sy, sx) 439 | matrix of magnitude 440 | orientation: np-array of size (sy, sx) 441 | matrix of orientations 442 | csx: int 443 | number of columns of the cells 444 | MUST BE EVEN 445 | csy: int 446 | number of raws dimension of the cells 447 | MUST BE EVEN 448 | sx: int 449 | number of colums of the image (x axis) 450 | sy: int 451 | number of raws of the image (y axis) 452 | n_cells_x: int 453 | number of cells in the x axis 454 | n_cells_y: int 455 | number of cells in the y axis 456 | signed_orientation: bool, default is True 457 | if True, sign information of the orientation is preserved, 458 | ie orientation angles are between 0 and 360 degree. 459 | if False, the angles are between 0 and 180 degree. 460 | nbins : int, optional, default is 9 461 | Number of orientation bins. 462 | 463 | Returns 464 | ------- 465 | if visualise if True: (histogram of oriented gradient, visualisation image) 466 | 467 | histogram of oriented gradient: 468 | numpy array of shape (n_cells_y, n_cells_x, nbins), flattened if flatten is True 469 | visualisation image: 470 | Image of shape (hist.shape[0]*csy, hist.shape[1]*csx) 471 | """ 472 | sy, sx = magnitude.shape 473 | csy, csx = cell_size 474 | 475 | # checking that the cell size are even 476 | if csx % 2 != 0: 477 | csx += 1 478 | print("WARNING: the cell_size must be even, incrementing cell_size_x of 1") 479 | if csy % 2 != 0: 480 | csy += 1 481 | print("WARNING: the cell_size must be even, incrementing cell_size_y of 1") 482 | 483 | # Consider only the right part of the image 484 | # (if the rest doesn't fill a whole cell, just drop it) 485 | sx -= sx % csx 486 | sy -= sy % csy 487 | n_cells_x = sx//csx 488 | n_cells_y = sy//csy 489 | magnitude = magnitude[:sy, :sx] 490 | orientation = orientation[:sy, :sx] 491 | by, bx = cells_per_block 492 | 493 | orientation_histogram = interpolate(magnitude, orientation, csx, csy, sx, sy, n_cells_x, n_cells_y, signed_orientation, nbins) 494 | 495 | if normalise: 496 | normalised_blocks = normalise_histogram(orientation_histogram, bx, by, n_cells_x, n_cells_y, nbins) 497 | else: 498 | normalised_blocks = orientation_histogram 499 | 500 | if flatten: 501 | normalised_blocks = normalised_blocks.flatten() 502 | 503 | if visualise: 504 | #draw_histogram(normalised_blocks, csx, csy, signed_orientation) 505 | return normalised_blocks, visualise_histogram(normalised_blocks, csx, csy, signed_orientation) 506 | else: 507 | return normalised_blocks 508 | 509 | 510 | def histogram_from_gradients(gradientx, gradienty, cell_size=(8, 8), cells_per_block=(1, 1), signed_orientation=False, 511 | nbins=9, visualise=False, normalise=True, flatten=False, same_size=False): 512 | """ builds a histogram of oriented gradient from the provided gradients 513 | 514 | Parameters 515 | ---------- 516 | gradientx : (M, N) ndarray 517 | Gradient following the x axis 518 | gradienty: (M, N) ndarray 519 | Gradient following the y axis 520 | nbins : int, optional, default is 9 521 | Number of orientation bins. 522 | cell_size : 2 tuple (int, int), optional, default is (8, 8) 523 | Size (in pixels) of a cell. 524 | cells_per_block : 2 tuple (int,int), optional, default is (2, 2) 525 | Number of cells in each block. 526 | visualise : bool, optional, default is False 527 | Also return an image of the HOG. 528 | flatten: bool, optional, default is True 529 | signed_orientation: bool, default is True 530 | if True, sign information of the orientation is preserved, 531 | ie orientation angles are between 0 and 360 degree. 532 | if False, the angles are between 0 and 180 degree. 533 | normalise: bool, optional, default is True 534 | if True, the histogram is normalised block-wise 535 | same_size: bool, optional, default is False 536 | if True, the boundaries are duplicated when computing the gradients of the image 537 | so that these have the same size as the original image 538 | 539 | Returns 540 | ------- 541 | if visualise if True: (histogram of oriented gradient, visualisation image) 542 | 543 | histogram of oriented gradient: 544 | numpy array of shape (n_cells_y, n_cells_x, nbins), flattened if flatten is True 545 | visualisation image: 546 | Image of shape (hist.shape[0]*csy, hist.shape[1]*csx) 547 | 548 | References 549 | ---------- 550 | * http://en.wikipedia.org/wiki/Histogram_of_oriented_gradients 551 | 552 | * Dalal, N and Triggs, B, Histograms of Oriented Gradients for 553 | Human Detection, IEEE Computer Society Conference on Computer 554 | Vision and Pattern Recognition 2005 San Diego, CA, USA 555 | """ 556 | magnitude, orientation = magnitude_orientation(gradientx, gradienty) 557 | return build_histogram(magnitude, orientation, cell_size=cell_size, 558 | signed_orientation=signed_orientation, cells_per_block=cells_per_block, 559 | nbins=nbins, visualise=visualise, normalise=normalise, flatten=flatten) 560 | 561 | 562 | def hog(image, cell_size=(4, 4), cells_per_block=(1, 1), signed_orientation=False, 563 | nbins=9, visualise=False, normalise=True, flatten=False, same_size=True): 564 | """ builds a histogram of oriented gradient (HoG) from the provided image 565 | 566 | Compute a Histogram of Oriented Gradients (HOG) by 567 | 568 | 1. computing the gradient image in x and y and deduce from them the magnitude and orientation 569 | of each pixel 570 | 2. computing gradient histograms (vectorised version) 571 | 3. normalising across blocks 572 | 4. flattening into a feature vector if flatten=True 573 | 574 | Parameters 575 | ---------- 576 | image : (M, N) ndarray 577 | Input image (greyscale). 578 | nbins : int, optional, default is 9 579 | Number of orientation bins. 580 | cell_size : 2 tuple (int, int), optional, default is (8, 8) 581 | Size (in pixels) of a cell. 582 | cells_per_block : 2 tuple (int,int), optional, default is (2, 2) 583 | Number of cells in each block. 584 | visualise : bool, optional, default is False 585 | Also return an image of the HOG. 586 | flatten: bool, optional, default is True 587 | signed_orientation: bool, default is True 588 | if True, sign information of the orientation is preserved, 589 | ie orientation angles are between 0 and 360 degree. 590 | if False, the angles are between 0 and 180 degree. 591 | normalise: bool, optional, default is True 592 | if True, the histogram is normalised block-wise 593 | same_size: bool, optional, default is True 594 | if True, the boundaries are duplicated when computing the gradients of the image 595 | so that these have the same size as the original image 596 | 597 | Returns 598 | ------- 599 | if visualise if True: (histogram of oriented gradient, visualisation image) 600 | 601 | histogram of oriented gradient: 602 | numpy array of shape (n_cells_y, n_cells_x, nbins), flattened if flatten is True 603 | visualisation image: 604 | Image of shape (hist.shape[0]*csy, hist.shape[1]*csx) 605 | 606 | References 607 | ---------- 608 | * http://en.wikipedia.org/wiki/Histogram_of_oriented_gradients 609 | 610 | * Dalal, N and Triggs, B, Histograms of Oriented Gradients for 611 | Human Detection, IEEE Computer Society Conference on Computer 612 | Vision and Pattern Recognition 2005 San Diego, CA, USA 613 | """ 614 | gx, gy = gradient(image, same_size=same_size) 615 | return histogram_from_gradients(gx, gy, cell_size=cell_size, 616 | signed_orientation=signed_orientation, cells_per_block=cells_per_block, 617 | nbins=nbins, visualise=visualise, normalise=normalise, flatten=flatten) 618 | -------------------------------------------------------------------------------- /hog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeanKossaifi/python-hog/0a4496c3ae5516951f7357d4e88a326c8700f2cd/hog/tests/__init__.py -------------------------------------------------------------------------------- /hog/tests/test_histogram.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | # Author: Jean KOSSAIFI 4 | 5 | import numpy as np 6 | from numpy import rot90, sqrt 7 | from numpy.testing import assert_array_equal, assert_array_almost_equal 8 | from ..histogram import gradient, magnitude_orientation, compute_coefs, interpolate, normalise_histogram, interpolate_orientation 9 | 10 | 11 | def test_gradient(): 12 | """ tests for the gradient function 13 | """ 14 | # No duplication of the border 15 | image = np.array([[-1, 0, 1, 1], 16 | [1, 0, 1, 1], 17 | [1, 1, 1, 1], 18 | [-1, 0, 1, 1], 19 | [-1, 0, 1, 1]]) 20 | gradx = np.array([[0, 1], [0, 0], [2, 1]]) 21 | grady = np.array([[-1, 0], [0, 0], [1, 0]]) 22 | resx, resy = gradient(image, same_size=False) 23 | assert_array_equal(gradx, resx) 24 | assert_array_equal(grady, resy) 25 | # With duplication of the border 26 | assert(np.shape(gradient(image, same_size=True)[0])==(5, 4)) 27 | image = np.array([[1, 1], 28 | [-1, 1]]) 29 | gradx = np.array([[0, 0], [2, 2]]) 30 | grady = np.array([[2, 0], [2, 0]]) 31 | resx, resy = gradient(image, same_size=True) 32 | assert_array_equal(gradx, resx) 33 | assert_array_equal(grady, resy) 34 | 35 | 36 | def test_magnitude_orientation(): 37 | """ test for the magnitude_orientation function 38 | """ 39 | gx = np.array([[1, 0], 40 | [0, 1]]) 41 | gy = np.array([[0, 1], 42 | [0, 1]]) 43 | magnitude, orientation = magnitude_orientation(gx, gy) 44 | res_orientation = np.array([[0, 90], [0, 45]]) 45 | res_magnitude = np.array([[1, 1], [0, sqrt(2)]]) 46 | assert_array_equal(res_orientation, orientation) 47 | assert_array_equal(res_magnitude, magnitude) 48 | 49 | gx = np.array([[0, 1], [0, 1]]) 50 | gy = np.array([[-1, 0], [1, 1]]) 51 | magnitude, orientation = magnitude_orientation(gx, gy) 52 | assert(orientation[0, 0] == 270) 53 | assert(orientation[0, 1] == 0) 54 | assert(orientation[1, 0] == 90) 55 | assert(orientation[1, 1] == 45) 56 | 57 | 58 | def test_compute_coefs(): 59 | """ tests for the compute coefs function 60 | """ 61 | csx, csy = (4, 4) 62 | dx = csx//2 63 | dy = csy//2 64 | n_cells_y, n_cells_x = (6, 4) 65 | coefs = compute_coefs(csy, csx, dy, dx, n_cells_y, n_cells_x) 66 | 67 | # Create an image to store the results 68 | res = np.tile(np.zeros((csx, csy)), (n_cells_x, n_cells_y)) 69 | 70 | # We check that the sum of the coefficient for a given pixel is one 71 | res[dy:, dx:] += coefs[-(n_cells_x*csx - dx):, -(n_cells_y*csy - dy):] 72 | res[:-dy, dx:] += rot90(coefs[-(n_cells_y*csy - dy):, -(n_cells_x*csx - dx):]) 73 | res[:-dy, :-dx] += rot90(rot90(coefs[-(n_cells_x*csx - dx):, -(n_cells_y*csy - dy):])) 74 | res[dy:, :-dx] += rot90(rot90(rot90(coefs[-(n_cells_y*csy - dy):, -(n_cells_x*csx - dx):]))) 75 | 76 | assert np.all(res==1) 77 | 78 | 79 | def test_interpolate_orientation(): 80 | sy, sx = 4, 9 81 | nbins = 9 82 | orientation = np.reshape(np.arange(36)*10, (4, 9)) 83 | res = interpolate_orientation(orientation, sx, sy, nbins, signed_orientation=False) 84 | count = 0 85 | even = 0 86 | for i in res: 87 | for j in i: 88 | if even == 0: 89 | a = np.zeros(9) 90 | a[count] = 1 91 | assert_array_equal(j, a) 92 | even = 1 93 | else: 94 | a = np.zeros(9) 95 | a[count] = 0.5 96 | count += 1 97 | if count >= nbins: 98 | count = 0 99 | a[count] = 0.5 100 | assert_array_equal(j, a) 101 | even = 0 102 | 103 | orientation = np.reshape(np.arange(36)*10, (4, 9)) 104 | res = interpolate_orientation(orientation, sx, sy, nbins, signed_orientation=True) 105 | assert_array_equal(res[0, 0, :], np.array([1, 0, 0, 0, 0, 0, 0, 0, 0])) 106 | assert_array_equal(res[0, 1, :], np.array([0.75, 0.25, 0, 0, 0, 0, 0, 0, 0])) 107 | assert_array_equal(res[-1, -1, :], np.array([0.75, 0, 0, 0, 0, 0, 0, 0, 0.25])) 108 | 109 | 110 | def test_interpolate(): 111 | """ tests for the interpolate function 112 | """ 113 | csx, csy = (4, 4) 114 | n_cells_x, n_cells_y = (3, 2) 115 | magnitude = np.zeros((csy*n_cells_y, csx*n_cells_x)) 116 | magnitude[1, 1] = 1 117 | magnitude[3, 3] = 1 118 | magnitude[-1, -1] = 1 119 | magnitude[3, -1] = 1 120 | orientation = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 121 | [0, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 122 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 123 | [0, 0, 0, 160, 0, 0, 0, 0, 0, 0, 0, 55], 124 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 125 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 126 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 127 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15]]) 128 | sy, sx = magnitude.shape 129 | hist = interpolate(magnitude, orientation, csx, csy, sx, sy, n_cells_x, n_cells_y) 130 | 131 | assert(np.sum(hist[:, :, 8])==1) # there is only one value in the bin 100 to 119 degree 132 | assert(hist[0, 0, 5]==1) #the 100 in the upper left corner is interpolated only to its own cell 133 | assert(hist[-1, -1, 0] == 0.25) 134 | assert(hist[-1, -1, 1] == 0.75) # interpolation only between the two bins 135 | assert(np.sum(hist[:, 2, 2] + hist[:, 2, 3]) == 1) 136 | 137 | 138 | def test_normalise_histogram(): 139 | """ tests for the normalise_histogram function 140 | """ 141 | bx, by = (1, 1) 142 | n_cells_x, n_cells_y = (2, 2) 143 | nbins = 9 144 | 145 | orientation_histogram = np.array([[[0, 0, 0, 0, 0, 0, 0, 1, 0], 146 | [0, 0, 0, 0, 0, 0, 0, 0, 0]], 147 | 148 | [[1, 1, 0, 0, 0, 0, 0, 0, 0], 149 | [1, 1, 0, 0, 0, 0, 0, 0, 0]]]) 150 | hist = normalise_histogram(orientation_histogram, bx, by, n_cells_x, n_cells_y, nbins) 151 | assert_array_almost_equal(hist[0, 0], np.array([ 0, 0, 0, 0, 0, 0, 0, 1, 0])) 152 | assert_array_equal(hist[0, 1], np.zeros(nbins)) 153 | assert_array_almost_equal(hist[1, 0], np.array([ 0.5, 0.5, 0, 0, 0, 0, 0, 0, 0])) 154 | assert_array_almost_equal(hist[1, 1], np.array([ 0.5, 0.5, 0, 0, 0, 0, 0, 0, 0])) 155 | -------------------------------------------------------------------------------- /images/interpolation_illustration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeanKossaifi/python-hog/0a4496c3ae5516951f7357d4e88a326c8700f2cd/images/interpolation_illustration.jpg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup, find_packages 3 | except ImportError: 4 | from distutils.core import setup, find_packages 5 | 6 | config = { 7 | 'description': 'Vectorised HOG descriptor in Python', 8 | 'author': 'Jean Kossaifi', 9 | 'author_email': 'jean [dot] kossaifi [at] gmail [dot] com', 10 | 'version': '0.1', 11 | 'install_requires': ['numpy'], 12 | 'packages': find_packages(), 13 | 'scripts': [], 14 | 'name': 'hog' 15 | } 16 | 17 | setup(**config) 18 | --------------------------------------------------------------------------------