diff --git a/digital_image_processing/edge_detection/canny.py b/digital_image_processing/edge_detection/canny.py index 7fde75a90a48..759abbc45223 100644 --- a/digital_image_processing/edge_detection/canny.py +++ b/digital_image_processing/edge_detection/canny.py @@ -1,3 +1,6 @@ +""" +Implementation of canny edge-detection algorithm +""" import cv2 import numpy as np from digital_image_processing.filters.convolve import img_convolve diff --git a/digital_image_processing/feature/__init__.py b/digital_image_processing/feature/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/digital_image_processing/feature/hog.py b/digital_image_processing/feature/hog.py new file mode 100644 index 000000000000..a94bd6bb1c39 --- /dev/null +++ b/digital_image_processing/feature/hog.py @@ -0,0 +1,115 @@ +""" +Implementation hog(Histogram of Oriented Gradients) feature of image according +the paper[https://lear.inrialpes.fr/people/triggs/pubs/Dalal-cvpr05.pdf]. +""" + +import math +import cv2 +import numpy as np +from digital_image_processing.filters.convolve import img_convolve + + +def hog_feature(image, gamma=0.4, cell_size=6, bin_size=18, block_size=4): + rows, cols = image.shape[0], image.shape[1] + + """ + Gamma normalization, however, the author point out this step can be omitted in HOG descriptor computation. + """ + norm = (image + 0.5)/255 + norm = np.power(norm, 1/gamma) + img_norm = 255 * norm - 0.5 + + # Get gradient and angle, the kernel below perform better than others(3x3 Sobel). + kernel_x = np.array([[-1, 0, 1]]) + kernel_y = np.array([[-1], [0], [1]]) + + dst_x = img_convolve(img_norm, kernel_x) + dst_y = img_convolve(img_norm, kernel_y) + + dst_xy = np.sqrt((np.square(dst_x)) + (np.square(dst_y))) + dst_xy = dst_xy * 255 / np.max(dst_xy) + gradient_magnitude_global = dst_xy + + # Get the angles and convert them in range (0, 360) + theta = np.arctan2(dst_y, dst_x) + gradient_angle_global = np.rad2deg(theta) # range(-180, 180) + gradient_angle_global = np.where(gradient_angle_global < 0, + gradient_angle_global + 360, + gradient_angle_global) # range(0, 360) + + """ + Orientation binning, The second step of calculation is creating the cell histograms. Each pixel within the cell + casts a weighted vote for an orientation-based histogram channel based on the values found in the gradient + computation. In tests, the gradient magnitude itself generally produces the best results. + """ + angle_unit = 360 / bin_size + cell_gradient_mtx = np.zeros((rows // cell_size, cols // cell_size, bin_size)) + for i in range(cell_gradient_mtx.shape[0]): + for j in range(cell_gradient_mtx.shape[1]): + pixes_grad_per_cell = gradient_magnitude_global[i * cell_size:(i + 1) * cell_size, + j * cell_size:(j + 1) * cell_size] + pixes_angle_per_cell = gradient_angle_global[i * cell_size:(i + 1) * cell_size, + j * cell_size:(j + 1) * cell_size] + + orientation_centers = [0] * bin_size + for cell_i in range(pixes_grad_per_cell.shape[0]): + for cell_j in range(pixes_grad_per_cell.shape[1]): + gradient_strength = pixes_grad_per_cell[cell_i][cell_j] + gradient_angle = pixes_angle_per_cell[cell_i][cell_j] + + bin_index = int(gradient_angle / angle_unit) + if gradient_angle == 360: + bin_index = 0 + orientation_centers[bin_index] += gradient_strength + + cell_gradient_mtx[i][j] = orientation_centers + + """ + Descriptor blocks. Grouping cells into larger spatial blocks and contrast normalizing each + block separately. The final descriptor is then the vector of all components of the normalized cell + responses from all of the blocks in the detection window. + """ + hog_descriptor_mtx = [] + out_rows, out_cols = cell_gradient_mtx.shape[1] - block_size + 1, cell_gradient_mtx.shape[0] - block_size + 1 + for i in range(0, out_rows): + for j in range(0, out_cols): + block_vector = np.ravel(cell_gradient_mtx[i:i + block_size, j:j + block_size, :]) + + # Block L2 normalization + eps = 1e-5 + block_vector = block_vector / np.sqrt(np.sum(block_vector ** 2) + eps ** 2) + hog_descriptor_mtx.append(block_vector) + + # showing hog + hog_dst_image = np.zeros([rows, cols]) + cell_gradient = cell_gradient_mtx + cell_width = cell_size // 2 + max_mag = np.array(cell_gradient).max() + for x in range(cell_gradient.shape[0]): + for y in range(cell_gradient.shape[1]): + cell_grad = cell_gradient[x][y] + cell_grad /= max_mag + angle = 0 + angle_gap = angle_unit + for magnitude in cell_grad: + angle_radian = math.radians(angle) + x1 = int(x * cell_size + magnitude * cell_width * math.cos(angle_radian)) + y1 = int(y * cell_size + magnitude * cell_width * math.sin(angle_radian)) + x2 = int(x * cell_size - magnitude * cell_width * math.cos(angle_radian)) + y2 = int(y * cell_size - magnitude * cell_width * math.sin(angle_radian)) + + strength = int(255 * math.sqrt(magnitude)) + cv2.line(hog_dst_image, (y1, x1), (y2, x2), strength) + + angle += angle_gap + + return hog_descriptor_mtx, hog_dst_image + + +if __name__ == '__main__': + # read original image in gray model + img = cv2.imread('../image_data/lena.jpg', 0) + # extract hog feature + hog_dsp, hog_img = hog_feature(img) + cv2.imshow('hog', hog_img.astype(np.uint8)) + cv2.waitKey(0) diff --git a/digital_image_processing/filters/convolve.py b/digital_image_processing/filters/convolve.py index b7600d74c294..f1865117895f 100644 --- a/digital_image_processing/filters/convolve.py +++ b/digital_image_processing/filters/convolve.py @@ -1,39 +1,104 @@ -# @Author : lightXu -# @File : convolve.py -# @Time : 2019/7/8 0008 下午 16:13 +""" +Implementation of image convolve algorithm +""" from cv2 import imread, cvtColor, COLOR_BGR2GRAY, imshow, waitKey from numpy import array, zeros, ravel, pad, dot, uint8 +import math -def im2col(image, block_size): - rows, cols = image.shape - dst_height = cols - block_size[1] + 1 - dst_width = rows - block_size[0] + 1 - image_array = zeros((dst_height * dst_width, block_size[1] * block_size[0])) +def im2col(image, block_size, row_stride, col_stride, dst_rows, dst_cols): + """ + :param image: padded image array + :param block_size: filter shape tuple + :param row_stride: stride in row channels + :param col_stride: stride in row channels + :param dst_rows: the rows of the filtered input image + :param dst_cols: the cols of the filtered input image + :return: the reshape array with shape(dst_rows*dst_cols, block_size[1] * block_size[0]) + """ + + image_array = zeros((dst_rows * dst_cols, block_size[1] * block_size[0])) row = 0 - for i in range(0, dst_height): - for j in range(0, dst_width): - window = ravel(image[i:i + block_size[0], j:j + block_size[1]]) + for i in range(0, dst_rows): + for j in range(0, dst_cols): + window = ravel(image[i * row_stride:i * row_stride + block_size[0], + j * col_stride:j * col_stride + block_size[1]]) image_array[row, :] = window row += 1 return image_array -def img_convolve(image, filter_kernel): +def img_convolve(image, kernel, row_stride=1, col_stride=1): + """ + :param image: input image array + :param kernel: filter kernel array + :param row_stride: stride in row channels + :param col_stride: stride in row channels + :return: the filter result + + Example: + >>> image = array([[1,2,3,4,5],[2,3,4,5,6], [1,2,3,4,5]]) + >>> kernel = array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]) + >>> img_convolve(image, kernel, 1, 1) + array([[3., 6., 6., 6., 3.], + [3., 6., 6., 6., 3.], + [3., 6., 6., 6., 3.]]) + + >>> img_convolve(image, kernel, 2, 1) + array([[3., 6., 6., 6., 3.], + [3., 6., 6., 6., 3.]]) + + >>> img_convolve(image, kernel, 2, 2) + array([[3., 6., 3.], + [3., 6., 3.]]) + + >>> kernel_1_3 = array([[-1, 2, -1]]) + >>> img_convolve(image, kernel_1_3, 1, 1) + array([[-1., 0., 0., 0., 1.], + [-1., 0., 0., 0., 1.], + [-1., 0., 0., 0., 1.]]) + + >>> kernel_3_1 = array([[-2], [1], [-2]]) + >>> img_convolve(image, kernel_3_1, 1, 1) + array([[ -5., -8., -11., -14., -17.], + [ -2., -5., -8., -11., -14.], + [ -5., -8., -11., -14., -17.]]) + + >>> kernel_3_1 = array([[-2], [1], [-2]]) + >>> img_convolve(image, kernel_3_1, 2, 1) + array([[ -5., -8., -11., -14., -17.], + [ -5., -8., -11., -14., -17.]]) + + + >>> kernel_3_1 = array([[-2], [1], [-2]]) + >>> img_convolve(image, kernel_3_1, 2, 2) + array([[ -5., -11., -17.], + [ -5., -11., -17.]]) + """ + height, width = image.shape[0], image.shape[1] - k_size = filter_kernel.shape[0] - pad_size = k_size//2 - # Pads image with the edge values of array. - image_tmp = pad(image, pad_size, mode='edge') + k_size_row, k_size_col = kernel.shape[0], kernel.shape[1] - # im2col, turn the k_size*k_size pixels into a row and np.vstack all rows - image_array = im2col(image_tmp, (k_size, k_size)) + # "SAME" convolve mode + dst_rows = math.ceil(height / row_stride) # ceil + dst_cols = math.ceil(width / col_stride) # ceil + + pad_h = max((dst_rows - 1) * row_stride + k_size_row - height, 0) + pad_top = pad_h // 2 # floor + pad_bottom = pad_h - pad_top + pad_w = max((dst_cols - 1) * col_stride + k_size_col - width, 0) + pad_left = pad_w // 2 # floor + pad_right = pad_w - pad_left + + # Pads image with the edge values of array. + image_tmp = pad(array=image, + pad_width=((pad_top, pad_bottom), (pad_left, pad_right)), + mode='edge') - # turn the kernel into shape(k*k, 1) - kernel_array = ravel(filter_kernel) - # reshape and get the dst image - dst = dot(image_array, kernel_array).reshape(height, width) + image_array = im2col(image_tmp, (k_size_row, k_size_col), row_stride, col_stride, dst_rows, dst_cols) + kernel_array = ravel(kernel) + dst = dot(image_array, kernel_array).reshape(dst_rows, dst_cols) return dst diff --git a/digital_image_processing/filters/sobel_filter.py b/digital_image_processing/filters/sobel_filter.py index f3ef407d49e5..c624bcee9934 100644 --- a/digital_image_processing/filters/sobel_filter.py +++ b/digital_image_processing/filters/sobel_filter.py @@ -1,6 +1,6 @@ -# @Author : lightXu -# @File : sobel_filter.py -# @Time : 2019/7/8 0008 下午 16:26 +""" +Implementation of sobel filter algorithm +""" import numpy as np from cv2 import imread, cvtColor, COLOR_BGR2GRAY, imshow, waitKey from digital_image_processing.filters.convolve import img_convolve