├── .gitignore ├── ORB Notebook.ipynb ├── README.md ├── brief.pdf ├── corner properties.pdf ├── fast.pdf ├── images ├── ball.jpg ├── ball2.jpg ├── ball3.jpg ├── beach.jpg ├── beach2.jpg ├── beach3.jpg ├── car.jpg ├── car2.jpg ├── car3.jpg ├── chess.jpg ├── chess2.jpg ├── chess3.jpg ├── out-kps.png ├── out-matches.png ├── out-matches2.png ├── pyramids.jpg ├── pyramids2.jpg ├── pyramids3.jpg ├── pyramids4.jpg ├── waffle.jpg ├── waffle2.jpg └── waffle3.jpg ├── orb_final.pdf └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .ipynb_checkpoints/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ORB-feature-matching 2 | Python implementation of ORB feature matching algorithm from scratch. (not using openCV) 3 | 4 | This is a python implementation of the ORB feature extraction/detection and matching without using OpenCV orb functions. It was done as an **exercise of my understanding of the algorithm**. 5 | 6 | ##### Code Structure 7 | Inside the ```utils.py``` file I have created the following functions: 8 | - **```FAST():```** FAST algorithm [***Features from Accelerated Segment Test***] for keypoints (corners) detection. 9 | 10 | - **```corner_orientations():```** a function that computes the orientations of each keypoint based on the ***intensity centroid*** method. 11 | 12 | - **```BRIEF():```** BRIEF algorithm [***Binary Robust Independent Elementary Features***] for keypoints (corners) detection. The function can also utilize the keypoints orientations to compute the descriptors accordingly (steered-BRIEF algorithm). 13 | 14 | - **```match():```** Brute force matching of the BRIEF descriptors based on hamming distance, with the option to perform cross-check, and to remove ambiguous matches (confusing matches) using a distance_ratio. 15 | 16 | 17 | The actual ORB implementation is in the ```ORB Notebook.ipynb``` file where I use all the functions of ```utils.py```. 18 | 19 | ## Examples 20 | ##### Multi-Scale Keypoints Detection 21 | ![keypoints](/images/out-kps.png) 22 | 23 | ##### Scale Test Matching 24 | ![keypoints](/images/out-matches.png) 25 | 26 | ##### Blur Test Matching 27 | ![keypoints](/images/out-matches2.png) 28 | 29 | 30 | #### TODO: 31 | The performance of this implementation is noticably worse (some incorrect matches & overall very slow) than the OpenCV ORB implementation, further improvements could be made in the future. -------------------------------------------------------------------------------- /brief.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/brief.pdf -------------------------------------------------------------------------------- /corner properties.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/corner properties.pdf -------------------------------------------------------------------------------- /fast.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/fast.pdf -------------------------------------------------------------------------------- /images/ball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/ball.jpg -------------------------------------------------------------------------------- /images/ball2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/ball2.jpg -------------------------------------------------------------------------------- /images/ball3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/ball3.jpg -------------------------------------------------------------------------------- /images/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/beach.jpg -------------------------------------------------------------------------------- /images/beach2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/beach2.jpg -------------------------------------------------------------------------------- /images/beach3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/beach3.jpg -------------------------------------------------------------------------------- /images/car.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/car.jpg -------------------------------------------------------------------------------- /images/car2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/car2.jpg -------------------------------------------------------------------------------- /images/car3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/car3.jpg -------------------------------------------------------------------------------- /images/chess.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/chess.jpg -------------------------------------------------------------------------------- /images/chess2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/chess2.jpg -------------------------------------------------------------------------------- /images/chess3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/chess3.jpg -------------------------------------------------------------------------------- /images/out-kps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/out-kps.png -------------------------------------------------------------------------------- /images/out-matches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/out-matches.png -------------------------------------------------------------------------------- /images/out-matches2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/out-matches2.png -------------------------------------------------------------------------------- /images/pyramids.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/pyramids.jpg -------------------------------------------------------------------------------- /images/pyramids2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/pyramids2.jpg -------------------------------------------------------------------------------- /images/pyramids3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/pyramids3.jpg -------------------------------------------------------------------------------- /images/pyramids4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/pyramids4.jpg -------------------------------------------------------------------------------- /images/waffle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/waffle.jpg -------------------------------------------------------------------------------- /images/waffle2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/waffle2.jpg -------------------------------------------------------------------------------- /images/waffle3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/images/waffle3.jpg -------------------------------------------------------------------------------- /orb_final.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedHisham1/ORB-feature-matching/88d4fdfcf5a521f8b6fa594fc57bf78857a3ed80/orb_final.pdf -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.signal import convolve2d 3 | from scipy.spatial.distance import cdist 4 | 5 | 6 | def FAST(img, N=9, threshold=0.15, nms_window=2): 7 | kernel = np.array([[1,2,1], 8 | [2,4,2], 9 | [1,2,1]])/16 # 3x3 Gaussian Window 10 | 11 | img = convolve2d(img, kernel, mode='same') 12 | 13 | cross_idx = np.array([[3,0,-3,0], [0,3,0,-3]]) 14 | circle_idx = np.array([[3,3,2,1,0,-1,-2,-3,-3,-3,-2,-1,0,1,2,3], 15 | [0,1,2,3,3,3,2,1,0,-1,-2,-3,-3,-3,-2,-1]]) 16 | 17 | corner_img = np.zeros(img.shape) 18 | keypoints = [] 19 | for y in range(3, img.shape[0]-3): 20 | for x in range(3, img.shape[1]-3): 21 | Ip = img[y,x] 22 | t = threshold*Ip if threshold < 1 else threshold 23 | # fast checking cross idx only 24 | if np.count_nonzero(Ip+t < img[y+cross_idx[0,:], x+cross_idx[1,:]]) >= 3 or np.count_nonzero(Ip-t > img[y+cross_idx[0,:], x+cross_idx[1,:]]) >= 3: 25 | # detailed check -> full circle 26 | if np.count_nonzero(img[y+circle_idx[0,:], x+circle_idx[1,:]] >= Ip+t) >= N or np.count_nonzero(img[y+circle_idx[0,:], x+circle_idx[1,:]] <= Ip-t) >= N: 27 | # Keypoint [corner] 28 | keypoints.append([x,y]) # Note: keypoint = [col, row] 29 | corner_img[y,x] = np.sum(np.abs(Ip - img[y+circle_idx[0,:], x+circle_idx[1,:]])) 30 | 31 | # NMS - Non Maximal Suppression 32 | if nms_window != 0: 33 | fewer_kps = [] 34 | for [x, y] in keypoints: 35 | window = corner_img[y-nms_window:y+nms_window+1, x-nms_window:x+nms_window+1] 36 | # v_max = window.max() 37 | loc_y_x = np.unravel_index(window.argmax(), window.shape) 38 | x_new = x + loc_y_x[1] - nms_window 39 | y_new = y + loc_y_x[0] - nms_window 40 | new_kp = [x_new, y_new] 41 | if new_kp not in fewer_kps: 42 | fewer_kps.append(new_kp) 43 | else: 44 | fewer_kps = keypoints 45 | 46 | return np.array(fewer_kps) 47 | 48 | def corner_orientations(img, corners): 49 | # mask shape must be odd to have one centre point which is the corner 50 | OFAST_MASK = np.zeros((31, 31), dtype=np.int32) 51 | OFAST_UMAX = [15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 10, 9, 8, 6, 3] 52 | for i in range(-15, 16): 53 | for j in range(-OFAST_UMAX[abs(i)], OFAST_UMAX[abs(i)] + 1): 54 | OFAST_MASK[15 + j, 15 + i] = 1 55 | mrows, mcols = OFAST_MASK.shape 56 | mrows2 = int((mrows - 1) / 2) 57 | mcols2 = int((mcols - 1) / 2) 58 | 59 | # Padding to avoid errors @ corners near image edges. 60 | # Padding value=0 to not affect the orientation calculations 61 | img = np.pad(img, (mrows2, mcols2), mode='constant', constant_values=0) 62 | 63 | # Calculating orientation by the intensity centroid method 64 | orientations = [] 65 | for i in range(corners.shape[0]): 66 | c0, r0 = corners[i, :] 67 | m01, m10 = 0, 0 68 | for r in range(mrows): 69 | m01_temp = 0 70 | for c in range(mcols): 71 | if OFAST_MASK[r,c]: 72 | I = img[r0+r, c0+c] 73 | m10 = m10 + I*(c-mcols2) 74 | m01_temp = m01_temp + I 75 | m01 = m01 + m01_temp*(r-mrows2) 76 | orientations.append(np.arctan2(m01, m10)) 77 | 78 | return np.array(orientations) 79 | 80 | 81 | def BRIEF(img, keypoints, orientations=None, n=256, patch_size=9, sigma=1, mode='uniform', sample_seed=42): 82 | ''' 83 | BRIEF [Binary Robust Independent Elementary Features] keypoint/corner descriptor 84 | ''' 85 | random = np.random.RandomState(seed=sample_seed) 86 | 87 | # kernel = np.array([[1,2,1], 88 | # [2,4,2], 89 | # [1,2,1]])/16 # 3x3 Gaussian Window 90 | 91 | kernel = np.array([[1, 4, 7, 4, 1], 92 | [4, 16, 26, 16, 4], 93 | [7, 26, 41, 26, 7], 94 | [4, 16, 26, 16, 4], 95 | [1, 4, 7, 4, 1]])/273 # 5x5 Gaussian Window 96 | 97 | img = convolve2d(img, kernel, mode='same') 98 | 99 | if mode == 'normal': 100 | samples = (patch_size / 5.0) * random.randn(n*8) 101 | samples = np.array(samples, dtype=np.int32) 102 | samples = samples[(samples < (patch_size // 2)) & (samples > - (patch_size - 2) // 2)] 103 | pos1 = samples[:n * 2].reshape(n, 2) 104 | pos2 = samples[n * 2:n * 4].reshape(n, 2) 105 | elif mode == 'uniform': 106 | samples = random.randint(-(patch_size - 2) // 2 +1, (patch_size // 2), (n * 2, 2)) 107 | samples = np.array(samples, dtype=np.int32) 108 | pos1, pos2 = np.split(samples, 2) 109 | 110 | rows, cols = img.shape 111 | 112 | if orientations is None: 113 | mask = ( ((patch_size//2 - 1) < keypoints[:, 0]) 114 | & (keypoints[:, 0] < (cols - patch_size//2 + 1)) 115 | & ((patch_size//2 - 1) < keypoints[:, 1]) 116 | & (keypoints[:, 1] < (rows - patch_size//2 + 1))) 117 | 118 | keypoints = np.array(keypoints[mask, :], dtype=np.intp, copy=False) 119 | descriptors = np.zeros((keypoints.shape[0], n), dtype=bool) 120 | 121 | for p in range(pos1.shape[0]): 122 | pr0 = pos1[p, 0] 123 | pc0 = pos1[p, 1] 124 | pr1 = pos2[p, 0] 125 | pc1 = pos2[p, 1] 126 | for k in range(keypoints.shape[0]): 127 | kr = keypoints[k, 1] 128 | kc = keypoints[k, 0] 129 | if img[kr + pr0, kc + pc0] < img[kr + pr1, kc + pc1]: 130 | descriptors[k, p] = True 131 | else: 132 | # Using orientations 133 | 134 | # masking the keypoints with a safe distance from borders 135 | # instead of the patch_size//2 distance used in case of no rotations. 136 | distance = int((patch_size//2)*1.5) 137 | mask = ( ((distance - 1) < keypoints[:, 0]) 138 | & (keypoints[:, 0] < (cols - distance + 1)) 139 | & ((distance - 1) < keypoints[:, 1]) 140 | & (keypoints[:, 1] < (rows - distance + 1))) 141 | 142 | keypoints = np.array(keypoints[mask], dtype=np.intp, copy=False) 143 | orientations = np.array(orientations[mask], copy=False) 144 | descriptors = np.zeros((keypoints.shape[0], n), dtype=bool) 145 | 146 | for i in range(descriptors.shape[0]): 147 | angle = orientations[i] 148 | sin_theta = np.sin(angle) 149 | cos_theta = np.cos(angle) 150 | 151 | kr = keypoints[i, 1] 152 | kc = keypoints[i, 0] 153 | for p in range(pos1.shape[0]): 154 | pr0 = pos1[p, 0] 155 | pc0 = pos1[p, 1] 156 | pr1 = pos2[p, 0] 157 | pc1 = pos2[p, 1] 158 | 159 | # Rotation is based on the idea that: 160 | # x` = x*cos(th) - y*sin(th) 161 | # y` = x*sin(th) + y*cos(th) 162 | # c -> x & r -> y 163 | spr0 = round(sin_theta*pr0 + cos_theta*pc0) 164 | spc0 = round(cos_theta*pr0 - sin_theta*pc0) 165 | spr1 = round(sin_theta*pr1 + cos_theta*pc1) 166 | spc1 = round(cos_theta*pr1 - sin_theta*pc1) 167 | 168 | if img[kr + spr0, kc + spc0] < img[kr + spr1, kc + spc1]: 169 | descriptors[i, p] = True 170 | return descriptors 171 | 172 | 173 | def match(descriptors1, descriptors2, max_distance=np.inf, cross_check=True, distance_ratio=None): 174 | distances = cdist(descriptors1, descriptors2, metric='hamming') # distances.shape: [len(d1), len(d2)] 175 | 176 | indices1 = np.arange(descriptors1.shape[0]) # [0, 1, 2, 3, 4, 5, 6, 7, ..., len(d1)] "indices of d1" 177 | indices2 = np.argmin(distances, axis=1) # [12, 465, 23, 111, 123, 45, 67, 2, 265, ..., len(d1)] "list of the indices of d2 points that are closest to d1 points" 178 | # Each d1 point has a d2 point that is the most close to it. 179 | if cross_check: 180 | ''' 181 | Cross check idea: 182 | what d1 matches with in d2 [indices2], should be equal to 183 | what that point in d2 matches with in d1 [matches1] 184 | ''' 185 | matches1 = np.argmin(distances, axis=0) # [15, 37, 283, ..., len(d2)] "list of d1 points closest to d2 points" 186 | # Each d2 point has a d1 point that is closest to it. 187 | # indices2 is the forward matches [d1 -> d2], while matches1 is the backward matches [d2 -> d1]. 188 | mask = indices1 == matches1[indices2] # len(mask) = len(d1) 189 | # we are basically asking does this point in d1 matches with a point in d2 that is also matching to the same point in d1 ? 190 | indices1 = indices1[mask] 191 | indices2 = indices2[mask] 192 | 193 | if max_distance < np.inf: 194 | mask = distances[indices1, indices2] < max_distance 195 | indices1 = indices1[mask] 196 | indices2 = indices2[mask] 197 | 198 | if distance_ratio is not None: 199 | ''' 200 | the idea of distance_ratio is to use this ratio to remove ambigous matches. 201 | ambigous matches: matches where the closest match distance is similar to the second closest match distance 202 | basically, the algorithm is confused about 2 points, and is not sure enough with the closest match. 203 | solution: if the ratio between the distance of the closest match and 204 | that of the second closest match is more than the defined "distance_ratio", 205 | we remove this match entirly. if not, we leave it as is. 206 | ''' 207 | modified_dist = distances 208 | fc = np.min(modified_dist[indices1,:], axis=1) 209 | modified_dist[indices1, indices2] = np.inf 210 | fs = np.min(modified_dist[indices1,:], axis=1) 211 | mask = fc/fs <= 0.5 212 | indices1 = indices1[mask] 213 | indices2 = indices2[mask] 214 | 215 | # sort matches using distances 216 | dist = distances[indices1, indices2] 217 | sorted_indices = dist.argsort() 218 | 219 | matches = np.column_stack((indices1[sorted_indices], indices2[sorted_indices])) 220 | return matches 221 | 222 | 223 | if __name__ == "__main__": 224 | import cv2 225 | import matplotlib.pyplot as plt 226 | from time import time 227 | from skimage.feature import plot_matches 228 | from skimage.transform import pyramid_gaussian 229 | 230 | # Trying multi-scale 231 | N_LAYERS = 4 232 | DOWNSCALE = 2 233 | 234 | img1 = cv2.imread('images/chess3.jpg') 235 | original_img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB) 236 | img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB) 237 | gray1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY) 238 | grays1 = list(pyramid_gaussian(gray1, downscale=DOWNSCALE, max_layer=N_LAYERS, multichannel=False)) 239 | 240 | img2 = cv2.imread('images/chess.jpg') 241 | original_img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) 242 | img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) 243 | gray2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY) 244 | grays2 = list(pyramid_gaussian(gray2, downscale=2, max_layer=4, multichannel=False)) 245 | 246 | scales = [(i*DOWNSCALE if i>0 else 1) for i in range(N_LAYERS)] 247 | features_img1 = np.copy(img1) 248 | features_img2 = np.copy(img2) 249 | 250 | 251 | kps1 = [] 252 | kps2 = [] 253 | ds1 = [] 254 | ds2 = [] 255 | ms = [] 256 | for i in range(len(scales)): 257 | scale_kp1 = FAST(grays1[i], N=9, threshold=0.15, nms_window=3) 258 | kps1.append(scale_kp1*scales[i]) 259 | scale_kp2 = FAST(grays2[i], N=9, threshold=0.15, nms_window=3) 260 | kps2.append(scale_kp2*scales[i]) 261 | for keypoint in scale_kp1: 262 | features_img1 = cv2.circle(features_img1, tuple(keypoint*scales[i]), 3*scales[i], (0,255,0), 1) 263 | for keypoint in scale_kp2: 264 | features_img2 = cv2.circle(features_img2, tuple(keypoint*scales[i]), 3*scales[i], (0,255,0), 1) 265 | plt.figure() 266 | plt.subplot(1,2,1) 267 | plt.imshow(grays1[i], cmap='gray') 268 | plt.subplot(1,2,2) 269 | plt.imshow(features_img1) 270 | 271 | plt.figure() 272 | plt.subplot(1,2,1) 273 | plt.imshow(grays2[i], cmap='gray') 274 | plt.subplot(1,2,2) 275 | plt.imshow(features_img2) 276 | 277 | d1 = BRIEF(grays1[i], scale_kp1, mode='uniform', patch_size=8, n=512) 278 | ds1.append(d1) 279 | d2 = BRIEF(grays2[i], scale_kp2, mode='uniform', patch_size=8, n=512) 280 | ds2.append(d2) 281 | 282 | matches = match(d1,d2, cross_check=True) 283 | ms.append(matches) 284 | print('no. of matches: ', matches.shape[0]) 285 | 286 | fig = plt.figure(figsize=(20, 10)) 287 | ax = fig.add_subplot(1,1,1) 288 | 289 | plot_matches(ax, grays1[i], grays2[i], np.flip(scale_kp1, 1), np.flip(scale_kp2, 1), matches) 290 | plt.show() 291 | 292 | 293 | plt.figure(figsize=(20,10)) 294 | plt.subplot(1,2,1) 295 | plt.imshow(features_img1) 296 | plt.subplot(1,2,2) 297 | plt.imshow(features_img2) 298 | plt.show() --------------------------------------------------------------------------------