-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathtinysfm.py
57 lines (48 loc) · 2.6 KB
/
tinysfm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import sys
import glob
import cv2
import numpy as np
import matplotlib.pyplot as plt
data_path = sys.argv[1] # Data directory path. Should contain camera intrinsics in `K.txt`.
fnames = glob.glob(f'{data_path}/*.jpg')
fnames.sort() # Order of capture is important for incremental SFM!
K = np.loadtxt(f'{data_path}/K.txt', dtype=np.float) # Load intrinsics
kp_prev, desc_prev = None, None
Rt_prev = np.append(np.eye(3), np.zeros((3, 1)), axis=-1) # Init extrinsics for current frame
Rt = np.zeros((3, 4)) # Init extrinsics for next frame
P_prev = K @ Rt_prev # Init projection matrix for current frame
P = np.zeros((3, 4)) # Init projection matrix for next frame
pt_cld = np.empty((3, 1))
def lowes_filter(matches, kp1, kp2, thresh=0.8):
pts1, pts2 = [], []
for m in matches:
if m[0].distance / m[1].distance < thresh:
pts1.append(kp1[m[0].queryIdx].pt)
pts2.append(kp2[m[0].trainIdx].pt)
return np.array(pts1), np.array(pts2)
for i in range(len(fnames)):
img = cv2.imread(fnames[i])
det = cv2.xfeatures2d.SIFT_create() # Init SIFT detector
kp, desc = det.detectAndCompute(img, None) # Extract keypoints & their descriptors
if i == 0: # If first frame, update and skip
kp_prev, desc_prev = kp, desc
continue
matcher = cv2.BFMatcher_create() # Use a simple bruteforce matcher
matches = matcher.knnMatch(desc_prev, desc, k=2) # Get top 2 matches for Lowe's test
pts_prev, pts = lowes_filter(matches, kp_prev, kp)
F, mask = cv2.findFundamentalMat(pts_prev, pts, cv2.RANSAC) # Fundamental Matrix for the two frames
mask = mask.ravel() == 1
pts_prev, pts = pts_prev[mask], pts[mask] # Exploit Epipolar constraint and keep only useful points
E = K.T @ F @ K # Find Essential Matrix
_, R, t, _ = cv2.recoverPose(E, pts_prev, pts, K) # Get current camera rotation + translation
Rt[:, :3] = R @ Rt_prev[:, :3] # Update rotational params
Rt[:, 3] = Rt_prev[:, 3] + Rt_prev[:, :3] @ t.ravel() # Update translation params
P = K @ Rt # Derive projection matrix for triangulation
pts_3d = cv2.triangulatePoints(P_prev, P, pts_prev.T, pts.T) # Find 3D coords from 2D points
pts_3d = cv2.convertPointsFromHomogeneous(pts_3d.T)[:, 0, :].T # Homogenous (4D) -> Euclidean (3D)
pt_cld = np.concatenate([pt_cld, pts_3d], axis=-1) # Add 3D points to point cloud
P_prev, Rt_prev, kp_prev, desc_prev = np.copy(P), np.copy(Rt), kp, desc # Updates for next iteration
fig = plt.figure(1, (10, 10)) # Plot the point cloud
ax = fig.add_subplot(111, projection='3d')
ax.scatter(pt_cld[0], pt_cld[1], pt_cld[2], s=1)
plt.show()