https://github.com/JiawangBian/GMS-Feature-Matcher
Tip revision: d187b3690ac220d4564db113788ea529e38af79e authored by JiaWang Bian on 10 June 2020, 06:30:43 UTC
Update README.md
Update README.md
Tip revision: d187b36
gms_matcher.py
import math
from enum import Enum
import cv2
cv2.ocl.setUseOpenCL(False)
import numpy as np
THRESHOLD_FACTOR = 6
ROTATION_PATTERNS = [
[1, 2, 3,
4, 5, 6,
7, 8, 9],
[4, 1, 2,
7, 5, 3,
8, 9, 6],
[7, 4, 1,
8, 5, 2,
9, 6, 3],
[8, 7, 4,
9, 5, 1,
6, 3, 2],
[9, 8, 7,
6, 5, 4,
3, 2, 1],
[6, 9, 8,
3, 5, 7,
2, 1, 4],
[3, 6, 9,
2, 5, 8,
1, 4, 7],
[2, 3, 6,
1, 5, 9,
4, 7, 8]]
class Size:
def __init__(self, width, height):
self.width = width
self.height = height
class DrawingType(Enum):
ONLY_LINES = 1
LINES_AND_POINTS = 2
COLOR_CODED_POINTS_X = 3
COLOR_CODED_POINTS_Y = 4
COLOR_CODED_POINTS_XpY = 5
class GmsMatcher:
def __init__(self, descriptor, matcher):
self.scale_ratios = [1.0, 1.0 / 2, 1.0 / math.sqrt(2.0), math.sqrt(2.0), 2.0]
# Normalized vectors of 2D points
self.normalized_points1 = []
self.normalized_points2 = []
# Matches - list of pairs representing numbers
self.matches = []
self.matches_number = 0
# Grid Size
self.grid_size_right = Size(0, 0)
self.grid_number_right = 0
# x : left grid idx
# y : right grid idx
# value : how many matches from idx_left to idx_right
self.motion_statistics = []
self.number_of_points_per_cell_left = []
# Inldex : grid_idx_left
# Value : grid_idx_right
self.cell_pairs = []
# Every Matches has a cell-pair
# first : grid_idx_left
# second : grid_idx_right
self.match_pairs = []
# Inlier Mask for output
self.inlier_mask = []
self.grid_neighbor_right = []
# Grid initialize
self.grid_size_left = Size(20, 20)
self.grid_number_left = self.grid_size_left.width * self.grid_size_left.height
# Initialize the neihbor of left grid
self.grid_neighbor_left = np.zeros((self.grid_number_left, 9))
self.descriptor = descriptor
self.matcher = matcher
self.gms_matches = []
self.keypoints_image1 = []
self.keypoints_image2 = []
def empty_matches(self):
self.normalized_points1 = []
self.normalized_points2 = []
self.matches = []
self.gms_matches = []
def compute_matches(self, img1, img2):
self.keypoints_image1, descriptors_image1 = self.descriptor.detectAndCompute(img1, np.array([]))
self.keypoints_image2, descriptors_image2 = self.descriptor.detectAndCompute(img2, np.array([]))
size1 = Size(img1.shape[1], img1.shape[0])
size2 = Size(img2.shape[1], img2.shape[0])
if self.gms_matches:
self.empty_matches()
all_matches = self.matcher.match(descriptors_image1, descriptors_image2)
self.normalize_points(self.keypoints_image1, size1, self.normalized_points1)
self.normalize_points(self.keypoints_image2, size2, self.normalized_points2)
self.matches_number = len(all_matches)
self.convert_matches(all_matches, self.matches)
self.initialize_neighbours(self.grid_neighbor_left, self.grid_size_left)
mask, num_inliers = self.get_inlier_mask(False, False)
print('Found', num_inliers, 'matches')
for i in range(len(mask)):
if mask[i]:
self.gms_matches.append(all_matches[i])
return self.gms_matches
# Normalize Key points to range (0-1)
def normalize_points(self, kp, size, npts):
for keypoint in kp:
npts.append((keypoint.pt[0] / size.width, keypoint.pt[1] / size.height))
# Convert OpenCV match to list of tuples
def convert_matches(self, vd_matches, v_matches):
for match in vd_matches:
v_matches.append((match.queryIdx, match.trainIdx))
def initialize_neighbours(self, neighbor, grid_size):
for i in range(neighbor.shape[0]):
neighbor[i] = self.get_nb9(i, grid_size)
def get_nb9(self, idx, grid_size):
nb9 = [-1 for _ in range(9)]
idx_x = idx % grid_size.width
idx_y = idx // grid_size.width
for yi in range(-1, 2):
for xi in range(-1, 2):
idx_xx = idx_x + xi
idx_yy = idx_y + yi
if idx_xx < 0 or idx_xx >= grid_size.width or idx_yy < 0 or idx_yy >= grid_size.height:
continue
nb9[xi + 4 + yi * 3] = idx_xx + idx_yy * grid_size.width
return nb9
def get_inlier_mask(self, with_scale, with_rotation):
max_inlier = 0
self.set_scale(0)
if not with_scale and not with_rotation:
max_inlier = self.run(1)
return self.inlier_mask, max_inlier
elif with_scale and with_rotation:
vb_inliers = []
for scale in range(5):
self.set_scale(scale)
for rotation_type in range(1, 9):
num_inlier = self.run(rotation_type)
if num_inlier > max_inlier:
vb_inliers = self.inlier_mask
max_inlier = num_inlier
if vb_inliers != []:
return vb_inliers, max_inlier
else:
return self.inlier_mask, max_inlier
elif with_rotation and not with_scale:
vb_inliers = []
for rotation_type in range(1, 9):
num_inlier = self.run(rotation_type)
if num_inlier > max_inlier:
vb_inliers = self.inlier_mask
max_inlier = num_inlier
if vb_inliers != []:
return vb_inliers, max_inlier
else:
return self.inlier_mask, max_inlier
else:
vb_inliers = []
for scale in range(5):
self.set_scale(scale)
num_inlier = self.run(1)
if num_inlier > max_inlier:
vb_inliers = self.inlier_mask
max_inlier = num_inlier
if vb_inliers != []:
return vb_inliers, max_inlier
else:
return self.inlier_mask, max_inlier
def set_scale(self, scale):
self.grid_size_right.width = self.grid_size_left.width * self.scale_ratios[scale]
self.grid_size_right.height = self.grid_size_left.height * self.scale_ratios[scale]
self.grid_number_right = self.grid_size_right.width * self.grid_size_right.height
# Initialize the neighbour of right grid
self.grid_neighbor_right = np.zeros((int(self.grid_number_right), 9))
self.initialize_neighbours(self.grid_neighbor_right, self.grid_size_right)
def run(self, rotation_type):
self.inlier_mask = [False for _ in range(self.matches_number)]
# Initialize motion statistics
self.motion_statistics = np.zeros((int(self.grid_number_left), int(self.grid_number_right)))
self.match_pairs = [[0, 0] for _ in range(self.matches_number)]
for GridType in range(1, 5):
self.motion_statistics = np.zeros((int(self.grid_number_left), int(self.grid_number_right)))
self.cell_pairs = [-1 for _ in range(self.grid_number_left)]
self.number_of_points_per_cell_left = [0 for _ in range(self.grid_number_left)]
self.assign_match_pairs(GridType)
self.verify_cell_pairs(rotation_type)
# Mark inliers
for i in range(self.matches_number):
if self.cell_pairs[int(self.match_pairs[i][0])] == self.match_pairs[i][1]:
self.inlier_mask[i] = True
return sum(self.inlier_mask)
def assign_match_pairs(self, grid_type):
for i in range(self.matches_number):
lp = self.normalized_points1[self.matches[i][0]]
rp = self.normalized_points2[self.matches[i][1]]
lgidx = self.match_pairs[i][0] = self.get_grid_index_left(lp, grid_type)
if grid_type == 1:
rgidx = self.match_pairs[i][1] = self.get_grid_index_right(rp)
else:
rgidx = self.match_pairs[i][1]
if lgidx < 0 or rgidx < 0:
continue
self.motion_statistics[int(lgidx)][int(rgidx)] += 1
self.number_of_points_per_cell_left[int(lgidx)] += 1
def get_grid_index_left(self, pt, type_of_grid):
x = pt[0] * self.grid_size_left.width
y = pt[1] * self.grid_size_left.height
if type_of_grid == 2:
x += 0.5
elif type_of_grid == 3:
y += 0.5
elif type_of_grid == 4:
x += 0.5
y += 0.5
x = math.floor(x)
y = math.floor(y)
if x >= self.grid_size_left.width or y >= self.grid_size_left.height:
return -1
return x + y * self.grid_size_left.width
def get_grid_index_right(self, pt):
x = int(math.floor(pt[0] * self.grid_size_right.width))
y = int(math.floor(pt[1] * self.grid_size_right.height))
return x + y * self.grid_size_right.width
def verify_cell_pairs(self, rotation_type):
current_rotation_pattern = ROTATION_PATTERNS[rotation_type - 1]
for i in range(self.grid_number_left):
if sum(self.motion_statistics[i]) == 0:
self.cell_pairs[i] = -1
continue
max_number = 0
for j in range(int(self.grid_number_right)):
value = self.motion_statistics[i]
if value[j] > max_number:
self.cell_pairs[i] = j
max_number = value[j]
idx_grid_rt = self.cell_pairs[i]
nb9_lt = self.grid_neighbor_left[i]
nb9_rt = self.grid_neighbor_right[idx_grid_rt]
score = 0
thresh = 0
numpair = 0
for j in range(9):
ll = nb9_lt[j]
rr = nb9_rt[current_rotation_pattern[j] - 1]
if ll == -1 or rr == -1:
continue
score += self.motion_statistics[int(ll), int(rr)]
thresh += self.number_of_points_per_cell_left[int(ll)]
numpair += 1
thresh = THRESHOLD_FACTOR * math.sqrt(thresh/numpair)
if score < thresh:
self.cell_pairs[i] = -2
def draw_matches(self, src1, src2, drawing_type):
height = max(src1.shape[0], src2.shape[0])
width = src1.shape[1] + src2.shape[1]
output = np.zeros((height, width, 3), dtype=np.uint8)
output[0:src1.shape[0], 0:src1.shape[1]] = src1
output[0:src2.shape[0], src1.shape[1]:] = src2[:]
if drawing_type == DrawingType.ONLY_LINES:
for i in range(len(self.gms_matches)):
left = self.keypoints_image1[self.gms_matches[i].queryIdx].pt
right = tuple(sum(x) for x in zip(self.keypoints_image2[self.gms_matches[i].trainIdx].pt, (src1.shape[1], 0)))
cv2.line(output, tuple(map(int, left)), tuple(map(int, right)), (0, 255, 255))
elif drawing_type == DrawingType.LINES_AND_POINTS:
for i in range(len(self.gms_matches)):
left = self.keypoints_image1[self.gms_matches[i].queryIdx].pt
right = tuple(sum(x) for x in zip(self.keypoints_image2[self.gms_matches[i].trainIdx].pt, (src1.shape[1], 0)))
cv2.line(output, tuple(map(int, left)), tuple(map(int, right)), (255, 0, 0))
for i in range(len(self.gms_matches)):
left = self.keypoints_image1[self.gms_matches[i].queryIdx].pt
right = tuple(sum(x) for x in zip(self.keypoints_image2[self.gms_matches[i].trainIdx].pt, (src1.shape[1], 0)))
cv2.circle(output, tuple(map(int, left)), 1, (0, 255, 255), 2)
cv2.circle(output, tuple(map(int, right)), 1, (0, 255, 0), 2)
elif drawing_type == DrawingType.COLOR_CODED_POINTS_X or drawing_type == DrawingType.COLOR_CODED_POINTS_Y or drawing_type == DrawingType.COLOR_CODED_POINTS_XpY :
_1_255 = np.expand_dims( np.array( range( 0, 256 ), dtype='uint8' ), 1 )
_colormap = cv2.applyColorMap(_1_255, cv2.COLORMAP_HSV)
for i in range(len(self.gms_matches)):
left = self.keypoints_image1[self.gms_matches[i].queryIdx].pt
right = tuple(sum(x) for x in zip(self.keypoints_image2[self.gms_matches[i].trainIdx].pt, (src1.shape[1], 0)))
if drawing_type == DrawingType.COLOR_CODED_POINTS_X:
colormap_idx = int(left[0] * 256. / src1.shape[1] ) # x-gradient
if drawing_type == DrawingType.COLOR_CODED_POINTS_Y:
colormap_idx = int(left[1] * 256. / src1.shape[0] ) # y-gradient
if drawing_type == DrawingType.COLOR_CODED_POINTS_XpY:
colormap_idx = int( (left[0] - src1.shape[1]*.5 + left[1] - src1.shape[0]*.5) * 256. / (src1.shape[0]*.5 + src1.shape[1]*.5) ) # manhattan gradient
color = tuple( map(int, _colormap[ colormap_idx,0,: ]) )
cv2.circle(output, tuple(map(int, left)), 1, color, 2)
cv2.circle(output, tuple(map(int, right)), 1, color, 2)
cv2.imshow('show', output)
cv2.waitKey()
if __name__ == '__main__':
img1 = cv2.imread("../data/01.jpg")
img2 = cv2.imread("../data/02.jpg")
orb = cv2.ORB_create(10000)
orb.setFastThreshold(0)
if cv2.__version__.startswith('3'):
matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
else:
matcher = cv2.BFMatcher_create(cv2.NORM_HAMMING)
gms = GmsMatcher(orb, matcher)
matches = gms.compute_matches(img1, img2)
# gms.draw_matches(img1, img2, DrawingType.ONLY_LINES)
gms.draw_matches(img1, img2, DrawingType.COLOR_CODED_POINTS_XpY)