Skip to content

Commit b315e19

Browse files
Merge pull request #923 from soumya997/master
added monocular slam blog code
2 parents b1f589a + 05496ef commit b315e19

24 files changed

+2983
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
### Implementation of Monocular Visual SLAM in Python:
2+
3+
4+
## Setup pangolin for python:
5+
6+
#### Install pangolin python:
7+
The [original library](https://github.com/stevenlovegrove/Pangolin) is written in c++, but there is [python binding](https://github.com/uoip/pangolin) available.
8+
9+
- **Install dependency:** For Ubuntu/Debian execute the below commands to install library dependencies,
10+
11+
```
12+
sudo apt-get install libglew-dev
13+
sudo apt-get install cmake
14+
sudo apt-get install ffmpeg libavcodec-dev libavutil-dev libavformat-dev libswscale-dev
15+
sudo apt-get install libdc1394-22-dev libraw1394-dev
16+
sudo apt-get install libjpeg-dev libpng-dev libtiff5-dev libopenexr-dev
17+
```
18+
19+
- Don't need to follow the [Very Optional Dependencies](https://github.com/uoip/pangolin?tab=readme-ov-file#very-optional-dependencies) from the repository.
20+
21+
- **Install the Library:** Execute the below commands to install *pangolin*,
22+
```
23+
git clone https://github.com/uoip/pangolin.git
24+
cd pangolin
25+
mkdir build
26+
cd build
27+
cmake ..
28+
make -j8
29+
cd ..
30+
python setup.py install
31+
```
32+
33+
In the `make -j8` you might get some error, just follow the comment mentioned in this [github issue](https://github.com/uoip/pangolin/issues/33#issuecomment-717655495). Running the `python setup.py install` might throw an silly error, use this [comment](https://github.com/uoip/pangolin/issues/20#issuecomment-498211997) from the exact issue to solve this.
34+
35+
- Other dependencies are pip installable.
36+
37+
38+
## How to run?
39+
40+
```bash
41+
python main.py
42+
```
43+
44+
## Code structure:
45+
```bash
46+
├── display.py
47+
├── extractor.py
48+
├── pointmap.py
49+
├── main.py
50+
├── notebooks
51+
│   ├── bundle_adjustment.ipynb
52+
│   ├── mapping.ipynb
53+
│   └── SLAM_pipeline_step_by_step.ipynb
54+
55+
```
56+
57+
In the notebook section we have shown how to run all the components of a monocular slam,
58+
- `SLAM_pipeline_step_by_step.ipynb` Describes the entire pipeline
59+
- `mapping.ipynb` is another resource for mapping [source](https://github.com/SiddhantNadkarni/Parallel_SFM)
60+
- `bundle_adjustment.ipynb` another great resource to understand g2o and bundle adjustment. [source](https://github.com/maxcrous/multiview_notebooks)
61+
62+
1st notebook uses the kitti dataset (grayscale, 22 GB), [download it from here](https://www.cvlibs.net/datasets/kitti/eval_odometry.php).
1.03 KB
Binary file not shown.
3.54 KB
Binary file not shown.
3.71 KB
Binary file not shown.
1.3 KB
Binary file not shown.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import sdl2
2+
import sdl2.ext
3+
import cv2
4+
5+
class Display(object):
6+
def __init__(self, W, H):
7+
sdl2.ext.init()
8+
self.window = sdl2.ext.Window("Tim Slam", size=(W, H))
9+
self.window.show()
10+
self.W, self.H = W, H
11+
12+
def paint(self, img):
13+
img = cv2.resize(img, (self.W, self.H))
14+
# Retrieves a list of SDL2 events.
15+
events = sdl2.ext.get_events()
16+
for event in events:
17+
# Checks if the event type is SDL_QUIT (window close event).
18+
if event.type == sdl2.SDL_QUIT:
19+
exit(0)
20+
# Retrieves a 3D numpy array that represents the pixel data of the window's surface.
21+
surf = sdl2.ext.pixels3d(self.window.get_surface())
22+
# Updates the pixel data of the window's surface with the resized image.
23+
# img.swapaxes(0, 1) swaps the axes of the image array to match the expected format of the SDL surface.
24+
surf[:, :, 0:3] = img.swapaxes(0, 1)
25+
# Refreshes the window to display the updated surface.
26+
self.window.refresh()
27+
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import cv2
2+
import numpy as np
3+
from skimage.measure import ransac
4+
from skimage.transform import FundamentalMatrixTransform
5+
import g2o
6+
7+
def add_ones(x):
8+
# concatenates the original array x with the column of ones along the second axis (columns).
9+
# This converts the N×2 array to an N×3 array where each point is represented
10+
# in homogeneous coordinates as [x,y,1].
11+
return np.concatenate([x, np.ones((x.shape[0], 1))], axis=1)
12+
13+
14+
IRt = np.eye(4)
15+
16+
def extractPose(F):
17+
W = np.mat([[0,-1,0],[1,0,0],[0,0,1]])
18+
U,d,Vt = np.linalg.svd(F)
19+
assert np.linalg.det(U) > 0
20+
if np.linalg.det(Vt) < 0:
21+
Vt *= -1
22+
R = np.dot(np.dot(U, W), Vt)
23+
if np.sum(R.diagonal()) < 0:
24+
R = np.dot(np.dot(U, W.T), Vt)
25+
t = U[:, 2]
26+
ret = np.eye(4)
27+
ret[:3, :3] = R
28+
ret[:3, 3] = t
29+
print(d)
30+
return ret
31+
32+
def extract(img):
33+
orb = cv2.ORB_create()
34+
35+
# Convert to grayscale
36+
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
37+
38+
# Detection
39+
pts = cv2.goodFeaturesToTrack(gray_img, 8000, qualityLevel=0.01, minDistance=10)
40+
41+
if pts is None:
42+
return np.array([]), None
43+
44+
# Extraction
45+
kps = [cv2.KeyPoint(f[0][0], f[0][1], 20) for f in pts]
46+
kps, des = orb.compute(gray_img, kps)
47+
48+
return np.array([(kp.pt[0], kp.pt[1]) for kp in kps]), des
49+
50+
def normalize(Kinv, pts):
51+
# The inverse camera intrinsic matrix 𝐾 − 1 transforms 2D homogeneous points
52+
# from pixel coordinates to normalized image coordinates. This transformation centers
53+
# the points based on the principal point (𝑐𝑥 , 𝑐𝑦) and scales them
54+
# according to the focal lengths 𝑓𝑥 and 𝑓𝑦, effectively mapping the points
55+
# to a normalized coordinate system where the principal point becomes the origin and
56+
# the distances are scaled by the focal lengths.
57+
return np.dot(Kinv, add_ones(pts).T).T[:, 0:2]
58+
# `[:, 0:2]` selects the first two columns of the resulting array, which are the normalized x and y coordinates.
59+
# `.T` transposes the result back to N x 3.
60+
61+
62+
def denormalize(K, pt):
63+
ret = np.dot(K, [pt[0], pt[1], 1.0])
64+
ret /= ret[2]
65+
return int(round(ret[0])), int(round(ret[1]))
66+
67+
68+
class Matcher(object):
69+
def __init__(self):
70+
self.last = None
71+
72+
73+
def match_frames(f1, f2):
74+
bf = cv2.BFMatcher(cv2.NORM_HAMMING)
75+
matches = bf.knnMatch(f1.des, f2.des, k=2)
76+
77+
# Lowe's ratio test
78+
ret = []
79+
idx1, idx2 = [], []
80+
for m, n in matches:
81+
if m.distance < 0.75*n.distance:
82+
p1 = f1.pts[m.queryIdx]
83+
p2 = f2.pts[m.trainIdx]
84+
85+
# Distance test
86+
# dditional distance test, ensuring that the
87+
# Euclidean distance between p1 and p2 is less than 0.1
88+
if np.linalg.norm((p1-p2)) < 0.1:
89+
# Keep idxs
90+
idx1.append(m.queryIdx)
91+
idx2.append(m.trainIdx)
92+
ret.append((p1, p2))
93+
pass
94+
95+
96+
assert len(ret) >= 8
97+
ret = np.array(ret)
98+
idx1 = np.array(idx1)
99+
idx2 = np.array(idx2)
100+
101+
# Fit matrix
102+
model, inliers = ransac((ret[:, 0],
103+
ret[:, 1]), FundamentalMatrixTransform,
104+
min_samples=8, residual_threshold=0.005,
105+
max_trials=200)
106+
107+
# Ignore outliers
108+
ret = ret[inliers]
109+
Rt = extractPose(model.params)
110+
111+
return idx1[inliers], idx2[inliers], Rt
112+
113+
114+
class Frame(object):
115+
def __init__(self, mapp, img, K):
116+
self.K = K
117+
self.Kinv = np.linalg.inv(self.K)
118+
self.pose = IRt
119+
120+
self.id = len(mapp.frames)
121+
mapp.frames.append(self)
122+
123+
pts, self.des = extract(img)
124+
125+
if self.des.any()!=None:
126+
self.pts = normalize(self.Kinv, pts)
3.27 MB
Loading
3.02 MB
Loading
2.77 MB
Loading

0 commit comments

Comments
 (0)