Skip to content

Commit e4c4982

Browse files
committed
Initial commit
1 parent 407bac4 commit e4c4982

File tree

4 files changed

+371
-0
lines changed

4 files changed

+371
-0
lines changed

MultiTemplateMatching.knwf

126 KB
Binary file not shown.

NonMaximaSupression.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
-Python3- (otherwise Python2 -> import division from future and correct print statements)
4+
5+
Non maxima supression for match template
6+
7+
Let say we have a correlation map and we want to detect the N best location in this map, while we want to make sure the detected location do not overlap too much
8+
The threshold for the overlap used is a maximal value of Intersection over Union (IoU). Large overlap will have large IoU.
9+
The IoU is conveneient as it is normalised between 0 and 1 and is a value normalised by the intial area.
10+
11+
To keep the N best hit/bounding-box without overlap, we first take the N best hit as returned by the maxima detector
12+
Then we loop over the best hit (by decreasing order of score, best score first) and compute the IoU with each remaning bounding box
13+
If the IoU is too high, the second bounding box is deleted and replaced by the N+1 th (or N + offset) bounding box in the intiial list of hit
14+
15+
@author: Laurent Thomas
16+
"""
17+
18+
def Point_in_Rectangle(Point, Rectangle):
19+
'''Return True if a point (x,y) is contained in a Rectangle(x, y, width, height)'''
20+
# unpack variables
21+
Px, Py = Point
22+
Rx, Ry, w, h = Rectangle
23+
24+
return (Rx <= Px) and (Px <= Rx + w -1) and (Ry <= Py) and (Py <= Ry + h -1) # simply test if x_Point is in the range of x for the rectangle
25+
26+
27+
def computeIoU(BBox1,BBox2):
28+
'''
29+
Compute the IoU (Intersection over Union) between 2 rectangular bounding boxes defined by the top left (Xtop,Ytop) and bottom right (Xbot, Ybot) pixel coordinates
30+
Code adapted from https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/
31+
'''
32+
print('BBox1 : ', BBox1)
33+
print('BBox2 : ', BBox2)
34+
35+
# Unpack input (python3 - tuple are no more supported as input in function definition - PEP3113 - Tuple can be used in as argument in a call but the function will not unpack it automatically)
36+
Xleft1, Ytop1, Width1, Height1 = BBox1
37+
Xleft2, Ytop2, Width2, Height2 = BBox2
38+
39+
# Compute bottom coordinates
40+
Xright1 = Xleft1 + Width1 -1 # we remove -1 from the width since we start with 1 pixel already (the top one)
41+
Ybot1 = Ytop1 + Height1 -1 # idem for the height
42+
43+
Xright2 = Xleft2 + Width2 -1
44+
Ybot2 = Ytop2 + Height2 -1
45+
46+
# determine the (x, y)-coordinates of the top left and bottom right points of the intersection rectangle
47+
Xleft = max(Xleft1, Xleft2)
48+
Ytop = max(Ytop1, Ytop2)
49+
Xright = min(Xright1, Xright2)
50+
Ybot = min(Ybot1, Ybot2)
51+
52+
# Compute boolean for inclusion
53+
BBox1_in_BBox2 = Point_in_Rectangle((Xleft1, Ytop1), BBox2) and Point_in_Rectangle((Xleft1, Ybot1), BBox2) and Point_in_Rectangle((Xright1, Ytop1), BBox2) and Point_in_Rectangle((Xright1, Ybot1), BBox2)
54+
BBox2_in_BBox1 = Point_in_Rectangle((Xleft2, Ytop2), BBox1) and Point_in_Rectangle((Xleft2, Ybot2), BBox1) and Point_in_Rectangle((Xright2, Ytop2), BBox1) and Point_in_Rectangle((Xright2, Ybot2), BBox1)
55+
56+
# Check that for the intersection box, Xtop,Ytop is indeed on the top left of Xbot,Ybot
57+
if BBox1_in_BBox2 or BBox2_in_BBox1:
58+
print('One BBox is included within the other')
59+
IoU = 1
60+
61+
elif Xright<Xleft or Ybot<Ytop : # it means that there is no intersection (bbox is inverted)
62+
print('No overlap')
63+
IoU = 0
64+
65+
else:
66+
# Compute area of the intersecting box
67+
Inter = (Xright - Xleft + 1) * (Ybot - Ytop + 1) # +1 since we are dealing with pixels. See a 1D example with 3 pixels for instance
68+
#print('Intersection area : ', Inter)
69+
70+
# Compute area of the union as Sum of the 2 BBox area - Intersection
71+
Union = Width1 * Height1 + Width2 * Height2 - Inter
72+
#print('Union : ', Union)
73+
74+
# Compute Intersection over union
75+
IoU = Inter/Union
76+
77+
print('IoU : ',IoU)
78+
return IoU
79+
80+
81+
82+
def NMS(List_Hit, scoreThreshold=None, sortDescending=True, N=1000, maxOverlap=0.7):
83+
'''
84+
Perform Non-Maxima supression : it compares the hits after maxima/minima detection, and removes the ones that are too close (too large overlap)
85+
This function works both with an optionnal threshold on the score, and number of detected bbox
86+
87+
if a scoreThreshold is specified, we first discard any hit below/above the threshold (depending on sortDescending)
88+
if sortDescending = True, the hit with score below the treshold are discarded (ie when high score means better prediction ex : Correlation)
89+
if sortDescending = False, the hit with score above the threshold are discared (ie when low score means better prediction ex : Distance measure)
90+
91+
Then the hit are ordered so that we have the best hits first.
92+
Then we iterate over the list of hits, taking one hit at a time and checking for overlap with the previous validated hit (the Final Hit list is directly iniitialised with the first best hit as there is no better hit with which to compare overlap)
93+
94+
This iteration is terminate once we have collected N best hit, or if there are no more hit left to test for overlap
95+
96+
INPUT
97+
- ListHit : a list of dictionnary, with each dictionnary being a hit following the formating {'TemplateIdx'= (int),'BBox'=(x,y,width,height),'Score'=(float)}
98+
the TemplateIdx is the row index in the panda/Knime table
99+
100+
- scoreThreshold : Float (or None), used to remove hit with too low prediction score.
101+
If sortDescending=True (ie we use a correlation measure so we want to keep large scores) the scores above that threshold are kept
102+
While if we use sortDescending=False (we use a difference measure ie we want to keep low score), the scores below that threshold are kept
103+
104+
- N : number of best hit to return (by increasing score). Min=1, eventhough it does not really make sense to do NMS with only 1 hit
105+
- maxOverlap : float between 0 and 1, the maximal overlap authorised between 2 bounding boxes, above this value, the bounding box of lower score is deleted
106+
- sortDescending : use True when high score means better prediction, False otherwise (ex : if score is a difference measure, then the best prediction are low difference and we sort by ascending order)
107+
108+
OUTPUT
109+
List_nHit : List of the best detection after NMS, it contains max N detection (but potentially less)
110+
'''
111+
112+
# Apply threshold on prediction score
113+
if scoreThreshold==None :
114+
List_ThreshHit = List_Hit[:] # copy to avoid modifying the input list in place
115+
116+
elif sortDescending : # We keep hit above the threshold
117+
List_ThreshHit = [dico for dico in List_Hit if dico['Score']>=scoreThreshold]
118+
119+
elif not sortDescending : # We keep hit below the threshold
120+
List_ThreshHit = [dico for dico in List_Hit if dico['Score']<=scoreThreshold]
121+
122+
123+
# Sort score to have best predictions first (important as we loop testing the best boxes against the other boxes)
124+
if sortDescending:
125+
List_ThreshHit.sort(key=lambda dico: dico['Score'], reverse=True) # Hit = [list of (x,y),score] - sort according to descending (best = high correlation)
126+
else:
127+
List_ThreshHit.sort(key=lambda dico: dico['Score']) # sort according to ascending score (best = small difference)
128+
129+
130+
# Split the inital pool into Final Hit that are kept and restHit that can be tested
131+
# Initialisation : 1st keep is kept for sure, restHit is the rest of the list
132+
print("\nInitialise final hit list with first best hit")
133+
FinalHit = [List_ThreshHit[0]]
134+
restHit = List_ThreshHit[1:]
135+
136+
137+
# Loop to compute overlap
138+
while len(FinalHit)<N and restHit : # second condition is restHit is not empty
139+
140+
# Report state of the loop
141+
print("\n\n\nNext while iteration")
142+
143+
print("-> Final hit list")
144+
for hit in FinalHit: print(hit)
145+
146+
print("\n-> Remaining hit list")
147+
for hit in restHit: print(hit)
148+
149+
# pick the next best peak in the rest of peak
150+
test_hit = restHit[0]
151+
test_bbox = test_hit['BBox']
152+
print("\nTest BBox:{} for overlap against higher score bboxes".format(test_bbox))
153+
154+
# Loop over hit in FinalHit to compute successively overlap with test_peak
155+
for hit in FinalHit:
156+
157+
# Recover Bbox from hit
158+
bbox2 = hit['BBox']
159+
160+
# Compute the Intersection over Union between test_peak and current peak
161+
IoU = computeIoU(test_bbox, bbox2)
162+
163+
# Initialise the boolean value to true before test of overlap
164+
ToAppend = True
165+
166+
if IoU>maxOverlap:
167+
ToAppend = False
168+
print("IoU above threshold\n")
169+
break # no need to test overlap with the other peaks
170+
171+
else:
172+
print("IoU below threshold\n")
173+
# no overlap for this particular (test_peak,peak) pair, keep looping to test the other (test_peak,peak)
174+
continue
175+
176+
177+
# After testing against all peaks (for loop is over), append or not the peak to final
178+
if ToAppend:
179+
# Move the test_hit from restHit to FinalHit
180+
print("Append {} to list of final hits, remove it from Remaining hit list".format(test_hit))
181+
FinalHit.append(test_hit)
182+
restHit.remove(test_hit)
183+
184+
else:
185+
# only remove the test_peak from restHit
186+
print("Remove {} from Remaining hit list".format(test_hit))
187+
restHit.remove(test_hit)
188+
189+
190+
# Once function execution is done, return list of hit without overlap
191+
print("\nCollected N expected hit, or no hit left to test")
192+
print("NMS over\n")
193+
return FinalHit
194+
195+
196+
if __name__ == "__main__":
197+
Hit1 = {'TemplateIdx':1,'BBox':(780, 350, 700, 480), 'Score':0.8}
198+
Hit2 = {'TemplateIdx':1,'BBox':(806, 416, 716, 442), 'Score':0.6}
199+
Hit3 = {'TemplateIdx':1,'BBox':(1074, 530, 680, 390), 'Score':0.4}
200+
201+
ListHit = [Hit1, Hit2, Hit3]
202+
203+
ListFinalHit = NMS(ListHit)
204+
205+
print(ListFinalHit)

ScriptWorkflow-NMS.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
'''
2+
PYTHON3
3+
Template matching and maxima/hit detection (above/below the score threshold depending on the detection method)
4+
Followed by Non-Maxima Supression based on the overlap between detected bbox
5+
6+
input_table_1 : Template
7+
input_table_2 : Target Image (only one since we are looping out of python)
8+
9+
output_table_1 : Hits before NMS
10+
output_table_2 : Hits after NMS
11+
Store the detection result as a dictionnary {TemplateIdx:int, BBox:(x,y,width,height), Score:float}
12+
13+
NB :
14+
Peak local max do not return the peaks by order of height by default.
15+
'''
16+
from skimage.feature import peak_local_max
17+
from scipy.signal import argrelmax, find_peaks
18+
import numpy as np
19+
import pandas as pd
20+
import cv2
21+
22+
# For NMS
23+
import sys
24+
sys.path.append(flow_variables['context.workflow.absolute-path']) # to have access to the NonMaximaSuppressionModule stored there
25+
from NonMaximaSupression import NMS
26+
27+
28+
# Recover parameters
29+
MatchMethod = flow_variables['MatchMethod_CV']
30+
N = flow_variables['NumHit'] # Expected number of hit (different from the experimental number of hit found)
31+
Thresh = flow_variables['ScoreThresh'] # min correlation score to retain a hit
32+
maxOverlap = flow_variables['maxOverlap']
33+
34+
# get Image and Name
35+
Name = input_table_2.index[0]
36+
Image = input_table_2['Image'][0].array
37+
38+
# Convert image to 32-bit Gray
39+
if Image.ndim == 3: # conversion from RGB to Gray
40+
Image = np.mean(Image, axis=0, dtype=np.float32)
41+
42+
else: # we still convert to float 32 (needed for match template)
43+
Image = np.float32(Image)
44+
45+
46+
47+
48+
# Do the template matching for each template (initial + flipped + rotated) for that image
49+
50+
# Get templates as images and convert to 32-bit for match template
51+
ListTemplate = [np.float32(image.array) for image in input_table_1['Template']]
52+
53+
ListHit = [] # contains n dictionnaries : 1 per template with {'TemplateIdx'= (int),'BBox'=(x,y,width,height),'Score'=(float)}
54+
55+
# Loop over templates
56+
for j in range(len(ListTemplate)):
57+
58+
TemplateName = input_table_1['TemplateName'][j]
59+
print('\nSearch with template : ',TemplateName)
60+
61+
# Get template
62+
Template = ListTemplate[j]
63+
Height,Width = Template.shape
64+
65+
# Compute correlation map
66+
CorrMap = cv2.matchTemplate(Template,Image, method = MatchMethod) # correlation map
67+
68+
69+
# Get coordinates of the peaks in the correaltion map
70+
# IF depending on the shape of the correlation map
71+
if CorrMap.shape == (1,1): # Correlation map is a simple digit, when template size = image size
72+
print('Template size = Image size -> Correlation map is a single digit')
73+
74+
if (MatchMethod==1 and CorrMap[0,0]<=minScore) or (MatchMethod in [3,5] and CorrMap[0,0]>=minScore):
75+
Peaks = np.array([[0,0]])
76+
else:
77+
Peaks = []
78+
79+
# use scipy findpeaks for the 1D cases (would allow to specify the relative threshold for the score directly here rather than in the NMS
80+
elif CorrMap.shape[0] == 1: # Template is as high as the image
81+
print('Template is as high as the image, the correlation map is a 1D-array')
82+
#Peaks = argrelmax(CorrMap[0], mode="wrap") # CorrMap[0] to have content of CorrMap as a proper line array
83+
84+
if MatchMethod==1:
85+
Peaks = find_peaks(-CorrMap[0], height=-Thresh) # find minima as maxima of inverted corr map
86+
87+
elif MatchMethod in [3,5]:
88+
Peaks = find_peaks(CorrMap[0], height=Thresh) # find minima as maxima of inverted corr map
89+
90+
Peaks = [[0,i] for i in Peaks[0]] # 0,i since one coordinate is fixed (the one for which Template = Image)
91+
92+
93+
94+
elif CorrMap.shape[1] == 1: # Template is as wide as the image
95+
print('Template is as wide as the image, the correlation map is a 1D-array')
96+
#Peaks = argrelmax(CorrMap, mode="wrap")
97+
if MatchMethod==1:
98+
Peaks = find_peaks(-CorrMap[:,0], height=-Thresh) # find minima as maxima of inverted corr map, height define the minimum height (threshold)
99+
100+
elif MatchMethod in [3,5]:
101+
Peaks = find_peaks(CorrMap[:,0], height=Thresh)
102+
103+
Peaks = [[i,0] for i in Peaks[0]]
104+
105+
106+
else: # Correlatin map is 2D
107+
# use threshold_abs to have something reproducible (if relative then case dependant)
108+
if MatchMethod==1:
109+
Peaks = peak_local_max(-CorrMap, threshold_abs=-Thresh, exclude_border=False).tolist() #BEWARE DO NOT RETURN IN ORDER OF MAXIMUM BY DEFAULT
110+
111+
elif MatchMethod in [3,5]:
112+
Peaks = peak_local_max(CorrMap, threshold_abs=Thresh, exclude_border=False).tolist() #BEWARE DO NOT RETURN IN ORDER OF MAXIMUM BY DEFAULT
113+
114+
print('Initially found',len(Peaks),'hit with this template')
115+
116+
# Once every peak was detected for this given template
117+
# Create a dictionnary for each hit with {'TemplateName':, 'BBox': (x,y,Width, Height), 'Score':coeff}
118+
for peak in Peaks :
119+
coeff = CorrMap[tuple(peak)]
120+
newHit = {'ImageName':Name, 'TemplateName':TemplateName, 'BBox': [int(peak[1]), int(peak[0]), Width, Height], 'Score':coeff}
121+
122+
# append to list of potential hit before Non maxima suppression
123+
ListHit.append(newHit)
124+
125+
126+
## Output table 1 : Hits before NMS
127+
output_table_1 = pd.DataFrame(ListHit)
128+
129+
## NMS
130+
if len(ListHit)>1: # More than one hit, so we need to do NMS
131+
132+
print('Start NMS')
133+
134+
if MatchMethod==1: # Difference : Best score = low values
135+
Hits_AfterNMS = NMS(ListHit, sortDescending=False, N=N, maxOverlap=maxOverlap) # remove scoreThreshold=Thres since done already at peak detection
136+
137+
elif MatchMethod in [3,5]: # Correlation : Best score = high values
138+
Hits_AfterNMS = NMS(ListHit, sortDescending=True, N=N, maxOverlap=maxOverlap)
139+
140+
# Generate output table
141+
output_table_2 = pd.DataFrame(Hits_AfterNMS)
142+
143+
144+
else: # only one or 0 hit so no NMS to do
145+
output_table_2 = output_table_1

environnement.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: KNIME-TemplateMatching
2+
channels:
3+
- defaults
4+
dependencies:
5+
- python=3.6 # Python
6+
- pandas=0.23 # Table data structures
7+
- jedi=0.13 # Python script autocompletion
8+
- python-dateutil=2.7 # Date and Time utilities
9+
- numpy=1.15 # N-dimensional arrays
10+
- cairo=1.14 # SVG support
11+
- pillow=5.3 # Image inputs/outputs
12+
- matplotlib=3.0 # Plotting
13+
- pyarrow=0.11 # Arrow serialization
14+
- IPython=7.1 # Notebook support
15+
- nbformat=4.4 # Notebook support
16+
- scipy=1.1 # Notebook support
17+
- scikit-image
18+
- pip:
19+
- opencv-contrib-python
20+
- pysimplegui
21+
prefix: C:\Anaconda\envs\KNIME-TM

0 commit comments

Comments
 (0)