Skip to content

Commit e5fb6ac

Browse files
committed
First commit
1 parent a367979 commit e5fb6ac

File tree

9 files changed

+1517
-0
lines changed

9 files changed

+1517
-0
lines changed

aligner.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#from rh_renderer import models
2+
#from rh_aligner.common import ransac
3+
import sys
4+
import os
5+
import glob
6+
import yaml
7+
import cv2
8+
import h5py
9+
import numpy as np
10+
from rh_logger.api import logger
11+
import logging
12+
import rh_logger
13+
import time
14+
from detector import FeaturesDetector
15+
from matcher import FeaturesMatcher
16+
import multiprocessing as mp
17+
18+
class StackAligner(object):
19+
20+
def __init__(self, conf, processes_num=1):
21+
self._conf = conf
22+
23+
# Initialize the detector, amtcher and optimizer objects
24+
detector_params = conf.get('detector_params', {})
25+
matcher_params = conf.get('matcher_params', {})
26+
self._detector = FeaturesDetector(conf['detector_type'], **detector_params)
27+
self._matcher = FeaturesMatcher(self._detector, **matcher_params)
28+
29+
self._processes_num = processes_num
30+
31+
32+
33+
@staticmethod
34+
def read_imgs(folder):
35+
img_fnames = sorted(glob.glob(os.path.join(folder, '*')))
36+
print("Loading {} images from {}.".format(len(img_fnames), folder))
37+
imgs = [cv2.imread(img_fname, 0) for img_fname in img_fnames]
38+
return img_fnames, imgs
39+
40+
41+
@staticmethod
42+
def load_conf_from_file(conf_fname):
43+
'''
44+
Loads a given configuration file from a yaml file
45+
'''
46+
print("Using config file: {}.".format(conf_fname))
47+
with open(conf_fname, 'r') as stream:
48+
conf = yaml.load(stream)
49+
return conf
50+
51+
52+
@staticmethod
53+
def _compute_l2_distance(pts1, pts2):
54+
delta = pts1 - pts2
55+
s = np.sum(delta**2, axis=1)
56+
return np.sqrt(s)
57+
58+
@staticmethod
59+
def _compute_features(detector, img, i):
60+
result = detector.detect(img)
61+
logger.report_event("Img {}, found {} features.".format(i, len(result[0])), log_level=logging.INFO)
62+
return result
63+
64+
@staticmethod
65+
def _match_features(features_result1, features_result2, i, j):
66+
transform_model, filtered_matches = self._matcher.match_and_filter(features_result1[0], features_result1[1], features_result2[0], features_result2[1])
67+
assert(transform_model is not None)
68+
transform_matrix = transform_model.get_matrix()
69+
logger.report_event("Imgs {} -> {}, found the following transformations\n{}\nAnd the average displacement: {} px".format(i, j, transform_matrix, np.mean(StackAligner._compute_l2_distance(transform_model.apply(filtered_matches[1]), filtered_matches[0]))), log_level=logging.INFO)
70+
return transform_matrix
71+
72+
73+
def align_imgs(self, imgs):
74+
'''
75+
Receives a stack of images to align and aligns that stack using the first image as an anchor
76+
'''
77+
78+
#pool = mp.Pool(processes=processes_num)
79+
80+
# Compute features
81+
logger.start_process('align_imgs', 'aligner.py', [len(imgs), self._conf])
82+
logger.report_event("Computing features...", log_level=logging.INFO)
83+
st_time = time.time()
84+
all_features = []
85+
pool_results = []
86+
for i, img in enumerate(imgs):
87+
#res = pool.apply_async(StackAligner._compute_features, (self._detector, img, i))
88+
#pool_results.append(res)
89+
all_features.append(self._detector.detect(img))
90+
logger.report_event("Img {}, found {} features.".format(i, len(all_features[-1][0])), log_level=logging.INFO)
91+
for res in pool_results:
92+
all_features.append(res.get())
93+
logger.report_event("Features computation took {} seconds.".format(time.time() - st_time), log_level=logging.INFO)
94+
95+
# match features of adjacent images
96+
logger.report_event("Pair-wise feature mathcing...", log_level=logging.INFO)
97+
st_time = time.time()
98+
pairwise_transforms = []
99+
for i in range(len(imgs) - 1):
100+
transform_model, filtered_matches = self._matcher.match_and_filter(all_features[i + 1][0], all_features[i+1][1], all_features[i][0], all_features[i][1])
101+
assert(transform_model is not None)
102+
transform_matrix = transform_model.get_matrix()
103+
pairwise_transforms.append(transform_matrix)
104+
logger.report_event("Imgs {} -> {}, found the following transformations\n{}\nAnd the average displacement: {} px".format(i, i+1, transform_matrix, np.mean(StackAligner._compute_l2_distance(transform_model.apply(filtered_matches[1]), filtered_matches[0]))), log_level=logging.INFO)
105+
logger.report_event("Feature matching took {} seconds.".format(time.time() - st_time), log_level=logging.INFO)
106+
107+
# Compute the per-image transformation (all images will be aligned to the first section)
108+
logger.report_event("Computing transformations...", log_level=logging.INFO)
109+
st_time = time.time()
110+
transforms = []
111+
cur_transform = np.eye(3)
112+
transforms.append(cur_transform)
113+
114+
for pair_transform in pairwise_transforms:
115+
cur_transform = np.dot(cur_transform, pair_transform)
116+
transforms.append(cur_transform)
117+
logger.report_event("Transformations computation took {} seconds.".format(time.time() - st_time), log_level=logging.INFO)
118+
119+
assert(len(imgs) == len(transforms))
120+
121+
#pool.close()
122+
#pool.join()
123+
124+
logger.end_process('align_imgs ending', rh_logger.ExitCode(0))
125+
return transforms
126+
127+
128+
129+
130+
@staticmethod
131+
def align_img_files(imgs_dir, conf, processes_num):
132+
# Read the files
133+
img_names, imgs = StackAligner.read_imgs(imgs_dir)
134+
135+
aligner = StackAligner(conf, processes_num)
136+
return img_names, imgs, aligner.align_imgs(imgs)
137+
138+
139+
def get_transforms(imgs_dir='gt-4x6x6_image.h5', conf_fname='conf.yaml', processes_num = 8):
140+
141+
conf = StackAligner.load_conf_from_file(conf_fname)
142+
if imgs_dir.endswith('.h5'):
143+
with h5py.File(imgs_dir, "r") as fd:
144+
imgs = fd[fd.keys()[0]][:].astype(np.uint8)
145+
146+
img_names = None
147+
aligner = StackAligner(conf, processes_num)
148+
transforms = aligner.align_imgs(imgs)
149+
150+
else:
151+
img_names, imgs, transforms = StackAligner.align_img_files(imgs_dir, conf, processes_num)
152+
return img_names, imgs, np.asarray(transforms)
153+

conf.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#detector_type: SIFT
2+
#detector_params:
3+
# sigma: 1.8
4+
detector_type: ORB
5+
detector_params:
6+
nfeatures: 10000
7+
#detector_type: SURF
8+
#detector_type: BRISK
9+
#detector_type: AKAZE
10+
11+
#descriptor:
12+
# type: SIFT
13+
14+
matcher_params:
15+
ROD_cutoff: 0.9
16+
model_index: 3 # 0 - Translation, 1 - Rigid, 3 - Affine
17+
#num_filtered_percent: 0.1
18+
#filter_rate_cutoff: 0.1
19+
min_features_num: 10
20+
iterations: 5000
21+
max_epsilon: 20
22+
min_inlier_ratio: 0.01
23+
min_num_inlier: 0.01
24+
max_trust: 3
25+
det_delta: 0.95
26+
max_stretch: 0.95
27+
#use_regularizer: True
28+
#regularizer_model_index: 1
29+
#regularizer_lambda: 0.1
30+
31+
out_path: ./analysis/AKAZE

demo.ipynb

Lines changed: 301 additions & 0 deletions
Large diffs are not rendered by default.

detector.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import cv2
2+
from enum import Enum
3+
import numpy as np
4+
5+
class FeaturesDetector(object):
6+
7+
class Type(Enum):
8+
SIFT = 1
9+
ORB = 2
10+
SURF = 3
11+
BRISK = 4
12+
AKAZE = 5
13+
14+
def __init__(self, detector_type_name, **kwargs):
15+
detector_type = FeaturesDetector.Type[detector_type_name]
16+
if detector_type == FeaturesDetector.Type.SIFT:
17+
self._detector = cv2.xfeatures2d.SIFT_create(**kwargs)
18+
self._matcher = cv2.BFMatcher(cv2.NORM_L2)
19+
elif detector_type == FeaturesDetector.Type.ORB:
20+
cv2.ocl.setUseOpenCL(False) # Avoiding a bug in OpenCV 3.1
21+
self._detector = cv2.ORB_create(**kwargs)
22+
self._matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
23+
elif detector_type == FeaturesDetector.Type.SURF:
24+
self._detector = cv2.xfeatures2d.SURF_create(**kwargs)
25+
self._matcher = cv2.BFMatcher(cv2.NORM_L2)
26+
elif detector_type == FeaturesDetector.Type.BRISK:
27+
self._detector = cv2.BRISK_create(**kwargs)
28+
self._matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
29+
elif detector_type == FeaturesDetector.Type.AKAZE:
30+
self._detector = cv2.AKAZE_create(**kwargs)
31+
self._matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
32+
else:
33+
raise("Unknown feature detector algorithm given")
34+
35+
36+
def detect(self, img):
37+
return self._detector.detectAndCompute(img, None)
38+
39+
def match(self, features_descs1, features_descs2):
40+
return self._matcher.knnMatch(features_descs1, features_descs2, k=2)

mask.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
import cv2
3+
import h5py
4+
import numpy as np
5+
6+
def compute_mask(imgs, n_imgs, nTransforms):
7+
8+
#with h5py.File("output/transforms.h5", "r") as fd:
9+
# nTransforms = fd["nTransforms"][:]
10+
#print(nTransforms.shape)
11+
masks = []
12+
for img, n_img, transform in zip(imgs, n_imgs, nTransforms):
13+
nH, nW = n_img.shape
14+
tmp = np.ones_like(img)
15+
img_transformed = cv2.warpAffine(tmp, transform[:2,:], (nW, nH), flags=cv2.INTER_NEAREST)
16+
masks.append(img_transformed)
17+
18+
return np.asarray(masks)
19+

matcher.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import numpy as np
2+
import ransac
3+
#from scipy.spatial import KDTree
4+
from collections import defaultdict
5+
import models
6+
7+
#NEIGHBORHOOD_RADIUS = 70
8+
GRID_SIZE = 50
9+
10+
class FeaturesMatcher(object):
11+
12+
def __init__(self, detector, **kwargs):
13+
self._detector = detector
14+
15+
self._params = {}
16+
# get default values if no value is present in kwargs
17+
#self._params["num_filtered_percent"] = kwargs.get("num_filtered_percent", 0.25)
18+
#self._params["filter_rate_cutoff"] = kwargs.get("filter_rate_cutoff", 0.25)
19+
self._params["ROD_cutoff"] = kwargs.get("ROD_cutoff", 0.92)
20+
self._params["min_features_num"] = kwargs.get("min_features_num", 40)
21+
22+
# Parameters for the RANSAC
23+
self._params["model_index"] = kwargs.get("model_index", 3)
24+
self._params["iterations"] = kwargs.get("iterations", 5000)
25+
self._params["max_epsilon"] = kwargs.get("max_epsilon", 30.0)
26+
self._params["min_inlier_ratio"] = kwargs.get("min_inlier_ratio", 0.01)
27+
self._params["min_num_inlier"] = kwargs.get("min_num_inliers", 7)
28+
self._params["max_trust"] = kwargs.get("max_trust", 3)
29+
self._params["det_delta"] = kwargs.get("det_delta", 0.9)
30+
self._params["max_stretch"] = kwargs.get("max_stretch", 0.25)
31+
32+
self._params["use_regularizer"] = True if "use_regularizer" in kwargs.keys() else False
33+
self._params["regularizer_lambda"] = kwargs.get("regularizer_lambda", 0.1)
34+
self._params["regularizer_model_index"] = kwargs.get("regularizer_model_index", 1)
35+
36+
37+
def match(self, features_kps1, features_descs1, features_kps2, features_descs2):
38+
features_kps2 = np.asarray(features_kps2)
39+
40+
# because the sections were already pre-aligned, we only match a feature in section1 to its neighborhood in section2 (according to a grid)
41+
grid = defaultdict(set)
42+
43+
# build the grid of the feature locations in the second section
44+
for i, kp2 in enumerate(features_kps2):
45+
pt_grid = (np.array(kp2.pt) / GRID_SIZE).astype(np.int)
46+
grid[tuple(pt_grid)].add(i)
47+
48+
match_points = [[], [], []]
49+
for kp1, desc1 in zip(features_kps1, features_descs1):
50+
# For each kp1 find the closest points in section2
51+
pt_grid = (np.array(kp1.pt) / GRID_SIZE).astype(np.int)
52+
close_kps2_idxs = set()
53+
# search in a [-1, -1] -> [1, 1] delta windows (3*3)
54+
for delta_y in range(-1, 2):
55+
for delta_x in range(-1, 2):
56+
delta = np.array([delta_x, delta_y], dtype=np.int)
57+
delta_grid_loc = tuple(pt_grid + delta)
58+
if delta_grid_loc in grid.keys():
59+
close_kps2_idxs |= grid[delta_grid_loc]
60+
61+
close_kps2_indices = list(close_kps2_idxs)
62+
close_descs2 = features_descs2[close_kps2_indices]
63+
matches = self._detector.match(desc1.reshape(1, len(desc1)), close_descs2)
64+
if len(matches[0]) == 2:
65+
if matches[0][0].distance < self._params["ROD_cutoff"] * matches[0][1].distance:
66+
match_points[0].append(kp1.pt)
67+
match_points[1].append(features_kps2[close_kps2_indices][matches[0][0].trainIdx].pt)
68+
match_points[2].append(matches[0][0].distance)
69+
70+
71+
72+
73+
# # because the sections were already pre-aligned, we only match a feature in section1 to its neighborhood in section2
74+
# features_kps2_pts = [kp.pt for kp in features_kps2]
75+
# kps2_pts_tree = KDTree(features_kps2_pts)
76+
#
77+
# match_points = [[], [], []]
78+
# for kp1, desc1 in zip(features_kps1, features_descs1):
79+
# # For each kp1 find the closest points in section2
80+
# close_kps2_indices = kps2_pts_tree.query_ball_point(kp1.pt, NEIGHBORHOOD_RADIUS)
81+
# close_descs2 = features_descs2[close_kps2_indices]
82+
# matches = self._detector.match(desc1.reshape(1, len(desc1)), close_descs2)
83+
# if len(matches[0]) == 2:
84+
# if matches[0][0].distance < self._params["ROD_cutoff"] * matches[0][1].distance:
85+
# match_points[0].append(kp1.pt)
86+
# match_points[1].append(features_kps2[close_kps2_indices][matches[0][0].trainIdx].pt)
87+
# match_points[2].append(matches[0][0].distance)
88+
89+
match_points = (np.array(match_points[0]), np.array(match_points[1]), np.array(match_points[2]))
90+
91+
92+
# matches = self._detector.match(features_descs1, features_descs2)
93+
#
94+
# good_matches = []
95+
# for m, n in matches:
96+
# #if (n.distance == 0 and m.distance == 0) or (m.distance / n.distance < actual_params["ROD_cutoff"]):
97+
# if m.distance < self._params["ROD_cutoff"] * n.distance:
98+
# good_matches.append(m)
99+
#
100+
# match_points = (
101+
# np.array([features_kps1[m.queryIdx].pt for m in good_matches]),
102+
# np.array([features_kps2[m.trainIdx].pt for m in good_matches]),
103+
# np.array([m.distance for m in good_matches])
104+
# )
105+
106+
return match_points
107+
108+
def match_and_filter(self, features_kps1, features_descs1, features_kps2, features_descs2):
109+
match_points = self.match(features_kps1, features_descs1, features_kps2, features_descs2)
110+
111+
model, filtered_matches = ransac.filter_matches(match_points, match_points, self._params['model_index'],
112+
self._params['iterations'], self._params['max_epsilon'], self._params['min_inlier_ratio'],
113+
self._params['min_num_inlier'], self._params['max_trust'], self._params['det_delta'], self._params['max_stretch'])
114+
115+
if model is None:
116+
return None, None
117+
118+
if self._params["use_regularizer"]:
119+
regularizer_model, _ = ransac.filter_matches(match_points, match_points, self._params['regularizer_model_index'],
120+
self._params['iterations'], self._params['max_epsilon'], self._params['min_inlier_ratio'],
121+
self._params['min_num_inlier'], self._params['max_trust'], self._params['det_delta'], self._params['max_stretch'])
122+
123+
if regularizer_model is None:
124+
return None, None
125+
126+
result = model.get_matrix() * (1 - self._params["regularizer_lambda"]) + regularizer_model.get_matrix() * self._params["regularizer_lambda"]
127+
model = models.AffineModel(result)
128+
129+
return model, filtered_matches
130+

0 commit comments

Comments
 (0)