1
1
"""Main code for Multi-Template-Matching (MTM)."""
2
+ import os
3
+ import warnings
4
+ from concurrent .futures import ThreadPoolExecutor , as_completed
5
+
2
6
import cv2
3
- import numpy as np
7
+ import numpy as np
4
8
import pandas as pd
5
- import warnings
9
+ from scipy . signal import find_peaks
6
10
from skimage .feature import peak_local_max
7
- from scipy .signal import find_peaks
8
- from .version import __version__
9
11
10
12
from .NMS import NMS
13
+ from .version import __version__
11
14
12
15
__all__ = ['NMS' ]
13
16
@@ -33,7 +36,7 @@ def _findLocalMax_(corrMap, score_threshold=0.6):
33
36
peaks = [[i ,0 ] for i in peaks [0 ]]
34
37
35
38
36
- else : # Correlatin map is 2D
39
+ else : # Correlation map is 2D
37
40
peaks = peak_local_max (corrMap , threshold_abs = score_threshold , exclude_border = False ).tolist ()
38
41
39
42
return peaks
@@ -116,82 +119,111 @@ def findMatches(listTemplates, image, method=cv2.TM_CCOEFF_NORMED, N_object=floa
116
119
-------
117
120
- Pandas DataFrame with 1 row per hit and column "TemplateName"(string), "BBox":(X, Y, Width, Height), "Score":float
118
121
"""
119
- if N_object != float ("inf" ) and type (N_object ) != int :
122
+ if N_object != float ("inf" ) and not isinstance (N_object , int ) :
120
123
raise TypeError ("N_object must be an integer" )
121
124
122
125
## Crop image to search region if provided
123
126
if searchBox is not None :
124
127
xOffset , yOffset , searchWidth , searchHeight = searchBox
125
128
image = image [yOffset : yOffset + searchHeight , xOffset : xOffset + searchWidth ]
126
-
127
129
else :
128
130
xOffset = yOffset = 0
129
-
131
+
130
132
# Check that the template are all smaller are equal to the image (original, or cropped if there is a search region)
131
133
for index , tempTuple in enumerate (listTemplates ):
132
-
134
+
133
135
if not isinstance (tempTuple , tuple ) or len (tempTuple )== 1 :
134
136
raise ValueError ("listTemplates should be a list of tuples as ('name','array') or ('name', 'array', 'mask')" )
135
-
137
+
136
138
templateSmallerThanImage = all (templateDim <= imageDim for templateDim , imageDim in zip (tempTuple [1 ].shape , image .shape ))
137
-
139
+
138
140
if not templateSmallerThanImage :
139
141
fitIn = "searchBox" if (searchBox is not None ) else "image"
140
142
raise ValueError ("Template '{}' at index {} in the list of templates is larger than {}." .format (tempTuple [0 ], index , fitIn ) )
141
-
143
+
142
144
listHit = []
143
- for tempTuple in listTemplates :
145
+ ## Use multi-threading to iterate through all templates, using half the number of cpu cores available.
146
+ with ThreadPoolExecutor (max_workers = round (os .cpu_count ()* .5 )) as executor :
147
+ futures = [executor .submit (_multi_compute , tempTuple , image , method , N_object , score_threshold , xOffset , yOffset , listHit ) for tempTuple in listTemplates ]
148
+ for future in as_completed (futures ):
149
+ _ = future .result ()
144
150
145
- templateName , template = tempTuple [:2 ]
146
- mask = None
151
+ if listHit :
152
+ return pd .DataFrame (listHit ) # All possible hits before Non-Maxima Supression
153
+ else :
154
+ return pd .DataFrame (columns = ["TemplateName" , "BBox" , "Score" ])
147
155
148
- if len (tempTuple )>= 3 : # ie a mask is also provided
149
- if method in (0 ,3 ):
150
- mask = tempTuple [2 ]
151
- else :
152
- warnings .warn ("Template matching method not supporting the use of Mask. Use 0/TM_SQDIFF or 3/TM_CCORR_NORMED." )
153
156
154
- #print('\nSearch with template : ',templateName)
155
- corrMap = computeScoreMap (template , image , method , mask = mask )
157
+ def _multi_compute (tempTuple , image , method , N_object , score_threshold , xOffset , yOffset , listHit ):
158
+ """
159
+ Find all possible template locations satisfying the score threshold provided a template to search and an image.
160
+ Add the hits in the list of hits.
161
+
162
+ Parameters
163
+ ----------
164
+ - tempTuple : a tuple (LabelString, template, mask (optional))
165
+ template to search in each image, associated to a label
166
+ labelstring : string
167
+ template : numpy array (grayscale or RGB)
168
+ mask (optional): numpy array, should have the same dimensions and type than the template
156
169
157
- ## Find possible location of the object
158
- if N_object == 1 : # Detect global Min/Max
159
- minVal , maxVal , minLoc , maxLoc = cv2 .minMaxLoc (corrMap )
170
+ - image : Grayscale or RGB numpy array
171
+ image in which to perform the search, it should be the same bitDepth and number of channels than the templates
160
172
161
- if method in ( 0 , 1 ):
162
- peaks = [ minLoc [:: - 1 ]] # opposite sorting than in the multiple detection
173
+ - method : int
174
+ one of OpenCV template matching method (0 to 5), default 5=0-mean cross-correlation
163
175
164
- else :
165
- peaks = [ maxLoc [:: - 1 ]]
176
+ - N_object: int or float("inf")
177
+ expected number of objects in the image, default to infinity if unknown
166
178
179
+ - score_threshold: float in range [0,1]
180
+ if N_object>1, returns local minima/maxima respectively below/above the score_threshold
167
181
168
- else :# Detect local max or min
169
- if method in (0 ,1 ): # Difference => look for local minima
170
- peaks = _findLocalMin_ (corrMap , score_threshold )
182
+ - xOffset : int
183
+ optional the x offset if the search area is provided
171
184
172
- else :
173
- peaks = _findLocalMax_ ( corrMap , score_threshold )
185
+ - yOffset : int
186
+ optional the y offset if the search area is provided
174
187
188
+ - listHit : the list of hits which we want to add the discovered hit
189
+ expected array of hits
190
+ """
191
+ templateName , template = tempTuple [:2 ]
192
+ mask = None
175
193
176
- #print('Initially found',len(peaks),'hit with this template')
194
+ if len (tempTuple )>= 3 : # ie a mask is also provided
195
+ if method in (0 ,3 ):
196
+ mask = tempTuple [2 ]
197
+ else :
198
+ warnings .warn ("Template matching method not supporting the use of Mask. Use 0/TM_SQDIFF or 3/TM_CCORR_NORMED." )
177
199
200
+ #print('\nSearch with template : ',templateName)
201
+ corrMap = computeScoreMap (template , image , method , mask = mask )
178
202
179
- # Once every peak was detected for this given template
180
- ## Create a dictionnary for each hit with {'TemplateName':, 'BBox': (x,y,Width, Height), 'Score':coeff}
203
+ ## Find possible location of the object
204
+ if N_object == 1 : # Detect global Min/Max
205
+ _ , _ , minLoc , maxLoc = cv2 .minMaxLoc (corrMap )
206
+ if method in (0 ,1 ):
207
+ peaks = [minLoc [::- 1 ]] # opposite sorting than in the multiple detection
208
+ else :
209
+ peaks = [maxLoc [::- 1 ]]
210
+ else :# Detect local max or min
211
+ if method in (0 ,1 ): # Difference => look for local minima
212
+ peaks = _findLocalMin_ (corrMap , score_threshold )
213
+ else :
214
+ peaks = _findLocalMax_ (corrMap , score_threshold )
181
215
182
- height , width = template . shape [ 0 : 2 ] # slicing make sure it works for RGB too
216
+ #print('Initially found',len(peaks),'hit with this template')
183
217
184
- for peak in peaks :
185
- coeff = corrMap [tuple (peak )]
186
- newHit = {'TemplateName' :templateName , 'BBox' : ( int (peak [1 ])+ xOffset , int (peak [0 ])+ yOffset , width , height ) , 'Score' :coeff }
218
+ # Once every peak was detected for this given template
219
+ ## Create a dictionnary for each hit with {'TemplateName':, 'BBox': (x,y,Width, Height), 'Score':coeff}
187
220
188
- # append to list of potential hit before Non maxima suppression
189
- listHit .append (newHit )
221
+ height , width = template .shape [0 :2 ] # slicing make sure it works for RGB too
190
222
191
- if listHit :
192
- return pd . DataFrame ( listHit ) # All possible hits before Non-Maxima Supression
193
- else :
194
- return pd . DataFrame ( columns = [ " TemplateName" , " BBox" , " Score" ] ) # empty df with correct column header
223
+ for peak in peaks :
224
+ # append to list of potential hit before Non maxima suppression
225
+ # no need to lock the list, append is thread-safe
226
+ listHit . append ({ ' TemplateName' : templateName , ' BBox' : ( int ( peak [ 1 ]) + xOffset , int ( peak [ 0 ]) + yOffset , width , height ) , ' Score' : corrMap [ tuple ( peak )]} ) # empty df with correct column header
195
227
196
228
197
229
def matchTemplates (listTemplates , image , method = cv2 .TM_CCOEFF_NORMED , N_object = float ("inf" ), score_threshold = 0.5 , maxOverlap = 0.25 , searchBox = None ):
@@ -239,7 +271,7 @@ def matchTemplates(listTemplates, image, method=cv2.TM_CCOEFF_NORMED, N_object=f
239
271
tableHit = findMatches (listTemplates , image , method , N_object , score_threshold , searchBox )
240
272
241
273
if method == 0 : raise ValueError ("The method TM_SQDIFF is not supported. Use TM_SQDIFF_NORMED instead." )
242
- sortAscending = True if method == 1 else False
274
+ sortAscending = ( method == 1 )
243
275
244
276
return NMS (tableHit , score_threshold , sortAscending , N_object , maxOverlap )
245
277
@@ -275,7 +307,7 @@ def drawBoxesOnRGB(image, tableHit, boxThickness=2, boxColor=(255, 255, 00), sho
275
307
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
276
308
else : outImage = image .copy ()
277
309
278
- for index , row in tableHit .iterrows ():
310
+ for _ , row in tableHit .iterrows ():
279
311
x ,y ,w ,h = row ['BBox' ]
280
312
cv2 .rectangle (outImage , (x , y ), (x + w , y + h ), color = boxColor , thickness = boxThickness )
281
313
if showLabel : cv2 .putText (outImage , text = row ['TemplateName' ], org = (x , y ), fontFace = cv2 .FONT_HERSHEY_SIMPLEX , fontScale = labelScale , color = labelColor , lineType = cv2 .LINE_AA )
@@ -315,9 +347,9 @@ def drawBoxesOnGray(image, tableHit, boxThickness=2, boxColor=255, showLabel=Fal
315
347
if image .ndim == 3 : outImage = cv2 .cvtColor (image , cv2 .COLOR_RGB2GRAY ) # convert to RGB to be able to show detections as color box on grayscale image
316
348
else : outImage = image .copy ()
317
349
318
- for index , row in tableHit .iterrows ():
350
+ for _ , row in tableHit .iterrows ():
319
351
x ,y ,w ,h = row ['BBox' ]
320
352
cv2 .rectangle (outImage , (x , y ), (x + w , y + h ), color = boxColor , thickness = boxThickness )
321
353
if showLabel : cv2 .putText (outImage , text = row ['TemplateName' ], org = (x , y ), fontFace = cv2 .FONT_HERSHEY_SIMPLEX , fontScale = labelScale , color = labelColor , lineType = cv2 .LINE_AA )
322
354
323
- return outImage
355
+ return outImage
0 commit comments