|
1 | 1 | import cv2
|
2 | 2 | import numpy as np
|
3 | 3 | from skimage.feature import peak_local_max
|
4 |
| -from scipy.signal import find_peaks |
| 4 | +from scipy.signal import find_peaks |
5 | 5 |
|
6 | 6 | from MTM.NMS import NMS
|
7 | 7 |
|
8 | 8 | __all__ = ['NMS']
|
9 | 9 |
|
10 | 10 |
|
11 | 11 | def _findLocalMax_(corrMap, score_threshold=0.6):
|
12 |
| - ''' |
13 |
| - Get coordinates of the local maximas with values above a threshold in the image of the correlation map |
14 |
| - ''' |
15 |
| - |
16 |
| - # IF depending on the shape of the correlation map |
17 |
| - if corrMap.shape == (1,1): ## Template size = Image size -> Correlation map is a single digit') |
18 |
| - |
19 |
| - if corrMap[0,0]>=score_threshold: |
20 |
| - Peaks = np.array([[0,0]]) |
21 |
| - else: |
22 |
| - Peaks = [] |
| 12 | + ''' |
| 13 | + Get coordinates of the local maximas with values above a threshold in the image of the correlation map |
| 14 | + ''' |
| 15 | + |
| 16 | + # IF depending on the shape of the correlation map |
| 17 | + if corrMap.shape == (1,1): ## Template size = Image size -> Correlation map is a single digit') |
| 18 | + |
| 19 | + if corrMap[0,0]>=score_threshold: |
| 20 | + Peaks = np.array([[0,0]]) |
| 21 | + else: |
| 22 | + Peaks = [] |
23 | 23 |
|
24 |
| - # use scipy findpeaks for the 1D cases (would allow to specify the relative threshold for the score directly here rather than in the NMS |
25 |
| - elif corrMap.shape[0] == 1: ## Template is as high as the image, the correlation map is a 1D-array |
26 |
| - Peaks = find_peaks(corrMap[0], height=score_threshold) # corrMap[0] to have a proper 1D-array |
27 |
| - Peaks = [[0,i] for i in Peaks[0]] # 0,i since one coordinate is fixed (the one for which Template = Image) |
28 |
| - |
| 24 | + # use scipy findpeaks for the 1D cases (would allow to specify the relative threshold for the score directly here rather than in the NMS |
| 25 | + elif corrMap.shape[0] == 1: ## Template is as high as the image, the correlation map is a 1D-array |
| 26 | + Peaks = find_peaks(corrMap[0], height=score_threshold) # corrMap[0] to have a proper 1D-array |
| 27 | + Peaks = [[0,i] for i in Peaks[0]] # 0,i since one coordinate is fixed (the one for which Template = Image) |
| 28 | + |
29 | 29 |
|
30 |
| - elif corrMap.shape[1] == 1: ## Template is as wide as the image, the correlation map is a 1D-array |
31 |
| - #Peaks = argrelmax(corrMap, mode="wrap") |
32 |
| - Peaks = find_peaks(corrMap[:,0], height=score_threshold) |
33 |
| - Peaks = [[i,0] for i in Peaks[0]] |
| 30 | + elif corrMap.shape[1] == 1: ## Template is as wide as the image, the correlation map is a 1D-array |
| 31 | + #Peaks = argrelmax(corrMap, mode="wrap") |
| 32 | + Peaks = find_peaks(corrMap[:,0], height=score_threshold) |
| 33 | + Peaks = [[i,0] for i in Peaks[0]] |
34 | 34 |
|
35 | 35 |
|
36 |
| - else: # Correlatin map is 2D |
37 |
| - Peaks = peak_local_max(corrMap, threshold_abs=score_threshold, exclude_border=False).tolist() |
| 36 | + else: # Correlatin map is 2D |
| 37 | + Peaks = peak_local_max(corrMap, threshold_abs=score_threshold, exclude_border=False).tolist() |
38 | 38 |
|
39 |
| - return Peaks |
| 39 | + return Peaks |
40 | 40 |
|
41 | 41 |
|
42 | 42 |
|
43 | 43 | def _findLocalMin_(corrMap, score_threshold=0.4):
|
44 |
| - '''Find coordinates of local minimas with values below a threshold in the image of the correlation map''' |
45 |
| - return _findLocalMax_(-corrMap, -score_threshold) |
| 44 | + '''Find coordinates of local minimas with values below a threshold in the image of the correlation map''' |
| 45 | + return _findLocalMax_(-corrMap, -score_threshold) |
46 | 46 |
|
47 | 47 |
|
48 | 48 |
|
49 | 49 | def findMatches(listTemplates, image, method=cv2.TM_CCOEFF_NORMED, N_object=float("inf"), score_threshold=0.5, searchBox=None):
|
50 |
| - ''' |
51 |
| - Find all possible templates locations provided a list of template to search and an image |
52 |
| - - listTemplate : list of tuples [(templateName, templateImage), (templateName2, templateImage2) ] |
53 |
| - - method : one of OpenCV template matching method (0 to 5) |
54 |
| - - N_object: expected number of object in the image |
55 |
| - - score_threshold: if N>1, returns local minima/maxima respectively below/above the score_threshold |
56 |
| - - searchBox : optional search region as a tuple (X, Y, Width, Height) |
57 |
| - ''' |
58 |
| - if N_object!=float("inf") and type(N_object)!=int: |
59 |
| - raise TypeError("N_object must be an integer") |
60 |
| - |
61 |
| - elif N_object<1: |
62 |
| - raise ValueError("At least one object should be expected in the image") |
63 |
| - |
64 |
| - ## Crop image to search region if provided |
65 |
| - if searchBox != None: |
66 |
| - xOffset, yOffset, searchWidth, searchHeight = searchBox |
67 |
| - image = image[yOffset:yOffset+searchHeight, xOffset:xOffset+searchWidth] |
68 |
| - else: |
69 |
| - xOffset=yOffset=0 |
70 |
| - |
71 |
| - ## 16-bit image are converted to 32-bit for matchTemplate |
72 |
| - if image.dtype == 'uint16': image = np.float32(image) |
73 |
| - |
74 |
| - listHit = [] |
75 |
| - for templateName, template in listTemplates: |
76 |
| - |
77 |
| - #print('\nSearch with template : ',templateName) |
78 |
| - ## 16-bit image are converted to 32-bit for matchTemplate |
79 |
| - if template.dtype == 'uint16': template = np.float32(template) |
80 |
| - |
81 |
| - ## Compute correlation map |
82 |
| - corrMap = cv2.matchTemplate(template, image, method) |
83 |
| - |
84 |
| - ## Find possible location of the object |
85 |
| - if N_object==1: # Detect global Min/Max |
86 |
| - minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(corrMap) |
87 |
| - |
88 |
| - if method==1: |
89 |
| - Peaks = [minLoc[::-1]] # opposite sorting than in the multiple detection |
90 |
| - |
91 |
| - elif method in (3,5): |
92 |
| - Peaks = [maxLoc[::-1]] |
93 |
| - |
94 |
| - |
95 |
| - else:# Detect local max or min |
96 |
| - if method==1: # Difference => look for local minima |
97 |
| - Peaks = _findLocalMin_(corrMap, score_threshold) |
98 |
| - |
99 |
| - elif method in (3,5): |
100 |
| - Peaks = _findLocalMax_(corrMap, score_threshold) |
101 |
| - |
102 |
| - |
103 |
| - #print('Initially found',len(Peaks),'hit with this template') |
104 |
| - |
105 |
| - |
106 |
| - # Once every peak was detected for this given template |
107 |
| - ## Create a dictionnary for each hit with {'TemplateName':, 'BBox': (x,y,Width, Height), 'Score':coeff} |
108 |
| - |
109 |
| - height, width = template.shape[0:2] # slicing make sure it works for RGB too |
110 |
| - |
111 |
| - for peak in Peaks : |
112 |
| - coeff = corrMap[tuple(peak)] |
113 |
| - newHit = {'TemplateName':templateName, 'BBox': [int(peak[1])+xOffset, int(peak[0])+yOffset, width, height], 'Score':coeff} |
114 |
| - |
115 |
| - # append to list of potential hit before Non maxima suppression |
116 |
| - listHit.append(newHit) |
117 |
| - |
118 |
| - |
119 |
| - return listHit # All possible hit before Non-Maxima Supression |
120 |
| - |
| 50 | + ''' |
| 51 | + Find all possible templates locations provided a list of template to search and an image |
| 52 | + Parameters |
| 53 | + ---------- |
| 54 | + - listTemplates : list of tuples (LabelString, Grayscale or RGB numpy array) |
| 55 | + templates to search in each image, associated to a label |
| 56 | + - image : Grayscale or RGB numpy array |
| 57 | + image in which to perform the search, it should be the same bitDepth and number of channels than the templates |
| 58 | + - method : int |
| 59 | + one of OpenCV template matching method (0 to 5), default 5=0-mean cross-correlation |
| 60 | + - N_object: int |
| 61 | + expected number of objects in the image |
| 62 | + - score_threshold: float in range [0,1] |
| 63 | + if N>1, returns local minima/maxima respectively below/above the score_threshold |
| 64 | + - searchBox : tuple (X, Y, Width, Height) in pixel unit |
| 65 | + optional rectangular search region as a tuple |
| 66 | + |
| 67 | + Returns |
| 68 | + ------- |
| 69 | + - listHit: list of match as dictionaries {"TemplateName":string, "BBox":(X, Y, Width, Height), "Score":float} |
| 70 | + ''' |
| 71 | + if N_object!=float("inf") and type(N_object)!=int: |
| 72 | + raise TypeError("N_object must be an integer") |
| 73 | + |
| 74 | + elif N_object<1: |
| 75 | + raise ValueError("At least one object should be expected in the image") |
| 76 | + |
| 77 | + ## Crop image to search region if provided |
| 78 | + if searchBox != None: |
| 79 | + xOffset, yOffset, searchWidth, searchHeight = searchBox |
| 80 | + image = image[yOffset:yOffset+searchHeight, xOffset:xOffset+searchWidth] |
| 81 | + else: |
| 82 | + xOffset=yOffset=0 |
| 83 | + |
| 84 | + ## 16-bit image are converted to 32-bit for matchTemplate |
| 85 | + if image.dtype == 'uint16': image = np.float32(image) |
| 86 | + |
| 87 | + listHit = [] |
| 88 | + for templateName, template in listTemplates: |
| 89 | + |
| 90 | + #print('\nSearch with template : ',templateName) |
| 91 | + ## 16-bit image are converted to 32-bit for matchTemplate |
| 92 | + if template.dtype == 'uint16': template = np.float32(template) |
| 93 | + |
| 94 | + ## Compute correlation map |
| 95 | + corrMap = cv2.matchTemplate(template, image, method) |
| 96 | + |
| 97 | + ## Find possible location of the object |
| 98 | + if N_object==1: # Detect global Min/Max |
| 99 | + minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(corrMap) |
| 100 | + |
| 101 | + if method==1: |
| 102 | + Peaks = [minLoc[::-1]] # opposite sorting than in the multiple detection |
| 103 | + |
| 104 | + elif method in (3,5): |
| 105 | + Peaks = [maxLoc[::-1]] |
| 106 | + |
| 107 | + |
| 108 | + else:# Detect local max or min |
| 109 | + if method==1: # Difference => look for local minima |
| 110 | + Peaks = _findLocalMin_(corrMap, score_threshold) |
| 111 | + |
| 112 | + elif method in (3,5): |
| 113 | + Peaks = _findLocalMax_(corrMap, score_threshold) |
| 114 | + |
| 115 | + |
| 116 | + #print('Initially found',len(Peaks),'hit with this template') |
| 117 | + |
| 118 | + |
| 119 | + # Once every peak was detected for this given template |
| 120 | + ## Create a dictionnary for each hit with {'TemplateName':, 'BBox': (x,y,Width, Height), 'Score':coeff} |
| 121 | + |
| 122 | + height, width = template.shape[0:2] # slicing make sure it works for RGB too |
| 123 | + |
| 124 | + for peak in Peaks : |
| 125 | + coeff = corrMap[tuple(peak)] |
| 126 | + newHit = {'TemplateName':templateName, 'BBox': [int(peak[1])+xOffset, int(peak[0])+yOffset, width, height], 'Score':coeff} |
| 127 | + |
| 128 | + # append to list of potential hit before Non maxima suppression |
| 129 | + listHit.append(newHit) |
| 130 | + |
| 131 | + return listHit # All possible hit before Non-Maxima Supression |
| 132 | + |
121 | 133 |
|
122 | 134 | def matchTemplates(listTemplates, image, method=cv2.TM_CCOEFF_NORMED, N_object=float("inf"), score_threshold=0.5, maxOverlap=0.25, searchBox=None):
|
123 |
| - ''' |
124 |
| - Search each template in the image, and return the best N_object location which offer the best score and which do not overlap |
125 |
| - - listTemplate : list of tuples (templateName, templateImage) |
126 |
| - - method : one of OpenCV template matching method (0 to 5) |
127 |
| - - N_object: expected number of object in the image |
128 |
| - - score_threshold: if N>1, returns local minima/maxima respectively below/above the score_threshold |
129 |
| - ''' |
130 |
| - if maxOverlap<0 or maxOverlap>1: |
131 |
| - raise ValueError("Maximal overlap between bounding box is in range [0-1]") |
132 |
| - |
133 |
| - listHit = findMatches(listTemplates, image, method, N_object, score_threshold, searchBox) |
134 |
| - |
135 |
| - if method == 1: bestHits = NMS(listHit, N_object=N_object, maxOverlap=maxOverlap, sortDescending=False) |
136 |
| - |
137 |
| - elif method in (3,5): bestHits = NMS(listHit, N_object=N_object, maxOverlap=maxOverlap, sortDescending=True) |
138 |
| - |
139 |
| - return bestHits |
| 135 | + ''' |
| 136 | + Search each template in the image, and return the best N_object location which offer the best score and which do not overlap |
| 137 | + Parameters |
| 138 | + ---------- |
| 139 | + - listTemplates : list of tuples (LabelString, Grayscale or RGB numpy array) |
| 140 | + templates to search in each image, associated to a label |
| 141 | + - image : Grayscale or RGB numpy array |
| 142 | + image in which to perform the search, it should be the same bitDepth and number of channels than the templates |
| 143 | + - method : int |
| 144 | + one of OpenCV template matching method (0 to 5), default 5=0-mean cross-correlation |
| 145 | + - N_object: int |
| 146 | + expected number of objects in the image |
| 147 | + - score_threshold: float in range [0,1] |
| 148 | + if N>1, returns local minima/maxima respectively below/above the score_threshold |
| 149 | + - maxOverlap: float in range [0,1] |
| 150 | + This is the maximal value for the ratio of the Intersection Over Union (IoU) area between a pair of bounding boxes. |
| 151 | + If the ratio is over the maxOverlap, the lower score bounding box is discarded. |
| 152 | + - searchBox : tuple (X, Y, Width, Height) in pixel unit |
| 153 | + optional rectangular search region as a tuple |
| 154 | + |
| 155 | + Returns |
| 156 | + ------- |
| 157 | + - bestHits: list of match as dictionaries {"TemplateName":string, "BBox":(X, Y, Width, Height), "Score":float} |
| 158 | + if N=1, return the best matches independently of the score_threshold |
| 159 | + if N<inf, returns up to N best matches that passed the score_threshold |
| 160 | + if N=inf, returns all matches that passed the score_threshold |
| 161 | + ''' |
| 162 | + if maxOverlap<0 or maxOverlap>1: |
| 163 | + raise ValueError("Maximal overlap between bounding box is in range [0-1]") |
| 164 | + |
| 165 | + listHit = findMatches(listTemplates, image, method, N_object, score_threshold, searchBox) |
| 166 | + |
| 167 | + if method == 1: bestHits = NMS(listHit, N_object=N_object, maxOverlap=maxOverlap, sortDescending=False) |
| 168 | + |
| 169 | + elif method in (3,5): bestHits = NMS(listHit, N_object=N_object, maxOverlap=maxOverlap, sortDescending=True) |
| 170 | + |
| 171 | + return bestHits |
140 | 172 |
|
141 | 173 |
|
142 | 174 | def drawBoxes(image, listHit, boxThickness=2, boxColor=(255, 255, 00), showLabel=True, labelColor=(255, 255, 0) ):
|
143 |
| - """ |
144 |
| - Return a copy of the image with results of template matching drawn as yellow rectangle and name of the template on top |
145 |
| - """ |
146 |
| - outImage = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) # convert to RGB to be able to show detections as color box on grayscale image |
147 |
| - |
148 |
| - for hit in listHit: |
149 |
| - x,y,w,h = hit['BBox'] |
150 |
| - cv2.rectangle(outImage, (x, y), (x+w, y+h), color=boxColor, thickness=boxThickness) |
151 |
| - if showLabel: cv2.putText(outImage, text=hit['TemplateName'], org=(x, y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.5, color=labelColor, lineType=cv2.LINE_AA) |
152 |
| - |
153 |
| - return outImage |
| 175 | + ''' |
| 176 | + Return a copy of the image with predicted template locations as bounding boxes overlaid on the image |
| 177 | + The name of the template can also be displayed on top of the bounding box with showLabel=True |
| 178 | + Parameters |
| 179 | + ---------- |
| 180 | + - image : image in which the search was performed |
| 181 | + - listHit: list of hit as returned by matchTemplates or findMatches |
| 182 | + - boxThickness: int |
| 183 | + thickness of bounding box contour in pixels |
| 184 | + - boxColor: (int, int, int) |
| 185 | + RGB color for the bounding box |
| 186 | + - showLabel: Boolean |
| 187 | + Display label of the bounding box (field TemplateName) |
| 188 | + - labelColor: (int, int, int) |
| 189 | + RGB color for the label |
| 190 | + |
| 191 | + Returns |
| 192 | + ------- |
| 193 | + outImage: RGB image |
| 194 | + original image with predicted template locations depicted as bounding boxes |
| 195 | + ''' |
| 196 | + # Convert Grayscale to RGB to be able to see the color bboxes |
| 197 | + if image.ndim == 2: outImage = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) # convert to RGB to be able to show detections as color box on grayscale image |
| 198 | + else: outImage = image.copy() |
| 199 | + |
| 200 | + for hit in listHit: |
| 201 | + x,y,w,h = hit['BBox'] |
| 202 | + cv2.rectangle(outImage, (x, y), (x+w, y+h), color=boxColor, thickness=boxThickness) |
| 203 | + if showLabel: cv2.putText(outImage, text=hit['TemplateName'], org=(x, y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.5, color=labelColor, lineType=cv2.LINE_AA) |
| 204 | + |
| 205 | + return outImage |
154 | 206 |
|
155 | 207 |
|
156 | 208 | if __name__ == '__main__':
|
157 |
| - |
158 |
| - from skimage.data import coins |
159 |
| - import matplotlib.pyplot as plt |
160 |
| - |
161 |
| - ## Get image and template |
162 |
| - smallCoin = coins()[37:37+38, 80:80+41] |
163 |
| - bigCoin = coins()[14:14+59,302:302+65] |
164 |
| - image = coins() |
165 |
| - |
166 |
| - ## Perform matching |
167 |
| - listHit = matchTemplates([('small', smallCoin), ('big', bigCoin)], image, score_threshold=0.4, method=cv2.TM_CCOEFF_NORMED, maxOverlap=0) |
168 |
| - #listHit = matchTemplates([('small', smallCoin), ('big', bigCoin)], image, N_object=1, score_threshold=0.4, method=cv2.TM_CCOEFF_NORMED, maxOverlap=0) |
| 209 | + |
| 210 | + from skimage.data import coins |
| 211 | + import matplotlib.pyplot as plt |
| 212 | + |
| 213 | + ## Get image and template |
| 214 | + smallCoin = coins()[37:37+38, 80:80+41] |
| 215 | + bigCoin = coins()[14:14+59,302:302+65] |
| 216 | + image = coins() |
| 217 | + |
| 218 | + ## Perform matching |
| 219 | + listHit = matchTemplates([('small', smallCoin), ('big', bigCoin)], image, score_threshold=0.4, method=cv2.TM_CCOEFF_NORMED, maxOverlap=0) |
| 220 | + #listHit = matchTemplates([('small', smallCoin), ('big', bigCoin)], image, N_object=1, score_threshold=0.4, method=cv2.TM_CCOEFF_NORMED, maxOverlap=0) |
169 | 221 |
|
170 |
| - print("Found {} coins".format(len(listHit))) |
171 |
| - |
172 |
| - for hit in listHit: |
173 |
| - print(hit) |
174 |
| - |
175 |
| - ## Display matches |
176 |
| - Overlay = drawBoxes(image, listHit) |
177 |
| - plt.imshow(Overlay) |
| 222 | + print("Found {} coins".format(len(listHit))) |
| 223 | + |
| 224 | + for hit in listHit: |
| 225 | + print(hit) |
| 226 | + |
| 227 | + ## Display matches |
| 228 | + Overlay = drawBoxes(image, listHit) |
| 229 | + plt.imshow(Overlay) |
0 commit comments