SLAM μ λ¬Έ (Visual SLAM Introduction)
SLAM μ λ¬Έ (Visual SLAM Introduction)¶
κ°μ¶
SLAM (Simultaneous Localization and Mapping)μ λ‘λ΄μ΄λ μμ¨μ£Όν μμ€ν μ΄ λ―Έμ§μ νκ²½μμ μ§λλ₯Ό μμ±νλ©΄μ λμμ μμ μ μμΉλ₯Ό μΆμ νλ κΈ°μ μ λλ€. Visual SLAM, LiDAR SLAM, Loop Closureμ κΈ°μ΄λ₯Ό λ€λ£Ήλλ€.
λμ΄λ: ββββ
μ μ μ§μ: 3D λΉμ , νΉμ§μ κ²μΆ/λ§€μΉ, μΉ΄λ©λΌ μΊλ¦¬λΈλ μ΄μ , κΈ°λ³Έ νλ₯ λ‘
λͺ©μ°¨¶
1. SLAM κ°μ¶
SLAMμ΄λ?¶
SLAM (Simultaneous Localization and Mapping):
λμμ μμΉμΆμ λ° μ§λμμ±
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β ν΅μ¬ μ§λ¬Έ: β
β "μ§λ μμ΄ μ΄λ»κ² μμΉλ₯Ό μ μ μλκ°?" β
β "μμΉλ₯Ό λͺ¨λ₯΄λ©΄μ μ΄λ»κ² μ§λλ₯Ό λ§λ€ μ μλκ°?" β
β β
β β λμ λμμ ν΄κ²°! (λκ³Ό λ¬κ± λ¬Έμ ) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β μΌμ λ°μ΄ν° β β
β β (μΉ΄λ©λΌ, LiDAR, IMU) β β
β β β β β
β β βΌ β β
β β ββββββββββββββββ β β
β β β SLAM β β β
β β β μκ³ λ¦¬μ¦ β β β
β β ββββββββ¬ββββββββ β β
β β β β β
β β ββββββββ΄ββββββββ β β
β β β β β β
β β βΌ βΌ β β
β β βββββββββββ βββββββββββ β β
β β β μ§λ β β μμΉ β β β
β β β (Map) β β (Pose) β β β
β β βββββββββββ βββββββββββ β β
β β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
μμ© λΆμΌ:
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β λΆμΌ β μμ β
βββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ€
β μμ¨μ£Όν β μλμ°¨, λλ‘ , λ°°λ¬ λ‘λ΄ β
β μ¦κ°νμ€ β ARKit, ARCore, HoloLens β
β λ‘λ΄ μ²μκΈ° β Roomba, Roborock β
β 3D μ€μΊλ β 건μΆ, λ¬Ένμ¬ λ³΅μ β
β λ΄λΉκ²μ΄μ
β μ€λ΄ μμΉ μΈμ β
βββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββ
SLAM λΆλ₯¶
SLAM λ°©μ λΆλ₯:
1. μΌμ κΈ°λ° λΆλ₯
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β Visual SLAM (V-SLAM) β
β - μΉ΄λ©λΌ (λ¨μ, μ€ν
λ μ€, RGB-D) β
β - νΉμ§μ κΈ°λ° λλ μ§μ λ°©μ β
β - μ: ORB-SLAM, LSD-SLAM, DSO β
β β
β LiDAR SLAM β
β - λ μ΄μ μ€μΊλ β
β - ν¬μΈνΈ ν΄λΌμ°λ λ§€μΉ β
β - μ: Cartographer, LOAM, LeGO-LOAM β
β β
β Visual-Inertial SLAM β
β - μΉ΄λ©λΌ + IMU μ΅ν© β
β - μ: VINS-Mono, OKVIS, MSCKF β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. λ°©λ²λ‘ κΈ°λ° λΆλ₯
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β νν° κΈ°λ° (Filter-based) β
β - EKF-SLAM, UKF-SLAM β
β - μ€μκ° μ
λ°μ΄νΈ β
β - μ νν μ€λ₯ λμ λ¬Έμ β
β β
β κ·Έλν κΈ°λ° (Graph-based) β
β - ν¬μ¦ κ·Έλν μ΅μ ν β
β - λ²λ€ μ‘°μ β
β - λ μ ννμ§λ§ κ³μ° λΉμ© λμ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3. νλ‘ νΈμλ/λ°±μλ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β νλ‘ νΈμλ (Front-end) β
β - μΌμ λ°μ΄ν° μ²λ¦¬ β
β - νΉμ§ μΆμΆ λ° λ§€μΉ β
β - μ΄κΈ° ν¬μ¦ μΆμ β
β - 루ν ν΄λ‘μ νμ§ β
β β
β λ°±μλ (Back-end) β
β - μ μ μ΅μ ν β
β - κ·Έλν μ΅μ ν β
β - λΆνμ€μ± μΆμ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Visual Odometry¶
Visual Odometry κ°λ ¶
Visual Odometry (VO):
μ°μλ μ΄λ―Έμ§λ‘λΆν° μΉ΄λ©λΌ μμ§μ μΆμ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β νλ μ t-1 νλ μ t νλ μ t+1 β
β βββββββββ βββββββββ βββββββββ β
β β π· βββTβββββΆβ π· βββTβββββΆβ π· β β
β βββββββββ βββββββββ βββββββββ β
β β
β λμ ν¬μ¦: P_t = Tβ * Tβ * ... * T_t β
β β
β λ¬Έμ μ : β
β - λμ μ€μ°¨ (drift) β
β - μ€μΌμΌ λͺ¨νΈμ± (λ¨μ μΉ΄λ©λΌ) β
β - λΉ λ₯Έ μμ§μμ μ·¨μ½ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
VO νμ΄νλΌμΈ:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β 1. μ΄λ―Έμ§ νλ β
β βΌ β
β 2. νΉμ§ μΆμΆ (ORB, SIFT, Harris corners) β
β βΌ β
β 3. νΉμ§ λ§€μΉ/μΆμ (BF Matcher, Optical Flow) β
β βΌ β
β 4. λͺ¨μ
μΆμ (Essential Matrix, PnP) β
β βΌ β
β 5. μ§μ μ΅μ ν (Local BA) β
β βΌ β
β 6. ν¬μ¦ μ
λ°μ΄νΈ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
λ¨μ Visual Odometry ꡬν¶
import cv2
import numpy as np
class MonocularVO:
"""λ¨μ Visual Odometry"""
def __init__(self, K, detector='ORB'):
"""
K: μΉ΄λ©λΌ λ΄λΆ νλΌλ―Έν° νλ ¬
detector: νΉμ§μ κ²μΆκΈ° ('ORB', 'SIFT', 'FAST')
"""
self.K = K
self.focal = K[0, 0]
self.pp = (K[0, 2], K[1, 2]) # principal point
# νΉμ§μ κ²μΆκΈ°
if detector == 'ORB':
self.detector = cv2.ORB_create(3000)
elif detector == 'SIFT':
self.detector = cv2.SIFT_create(3000)
else:
self.detector = cv2.FastFeatureDetector_create(threshold=25)
# κ΄ν νλ¦ νλΌλ―Έν°
self.lk_params = dict(
winSize=(21, 21),
maxLevel=3,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 30, 0.01)
)
# μν
self.prev_frame = None
self.prev_pts = None
self.cur_R = np.eye(3)
self.cur_t = np.zeros((3, 1))
self.trajectory = []
def detect_features(self, img):
"""νΉμ§μ κ²μΆ"""
if hasattr(self.detector, 'detectAndCompute'):
kp, _ = self.detector.detectAndCompute(img, None)
else:
kp = self.detector.detect(img, None)
pts = np.array([p.pt for p in kp], dtype=np.float32)
return pts.reshape(-1, 1, 2)
def track_features(self, prev_img, cur_img, prev_pts):
"""κ΄ν νλ¦μΌλ‘ νΉμ§μ μΆμ """
cur_pts, status, err = cv2.calcOpticalFlowPyrLK(
prev_img, cur_img, prev_pts, None, **self.lk_params
)
status = status.reshape(-1)
prev_pts = prev_pts[status == 1]
cur_pts = cur_pts[status == 1]
return prev_pts, cur_pts
def estimate_pose(self, pts1, pts2):
"""Essential Matrixλ‘ ν¬μ¦ μΆμ """
E, mask = cv2.findEssentialMat(
pts1, pts2, self.K,
method=cv2.RANSAC,
prob=0.999,
threshold=1.0
)
_, R, t, mask = cv2.recoverPose(E, pts1, pts2, self.K)
return R, t
def process_frame(self, frame):
"""νλ μ μ²λ¦¬"""
# κ·Έλ μ΄μ€μΌμΌ λ³ν
if len(frame.shape) == 3:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
else:
gray = frame
if self.prev_frame is None:
# 첫 νλ μ
self.prev_frame = gray
self.prev_pts = self.detect_features(gray)
return self.cur_R, self.cur_t
# νΉμ§μ μΆμ
if self.prev_pts is not None and len(self.prev_pts) > 0:
prev_pts, cur_pts = self.track_features(
self.prev_frame, gray, self.prev_pts
)
if len(prev_pts) >= 8:
# ν¬μ¦ μΆμ
R, t = self.estimate_pose(
prev_pts.reshape(-1, 2),
cur_pts.reshape(-1, 2)
)
# ν¬μ¦ λμ
self.cur_t = self.cur_t + self.cur_R @ t
self.cur_R = R @ self.cur_R
# μ νΉμ§μ μ΄ νμνλ©΄ κ²μΆ
if len(cur_pts) < 1000:
new_pts = self.detect_features(gray)
if len(cur_pts) > 0:
self.prev_pts = np.vstack([
cur_pts.reshape(-1, 1, 2),
new_pts
])
else:
self.prev_pts = new_pts
else:
self.prev_pts = cur_pts.reshape(-1, 1, 2)
else:
self.prev_pts = self.detect_features(gray)
else:
self.prev_pts = self.detect_features(gray)
self.prev_frame = gray
# κΆ€μ μ μ₯
self.trajectory.append(self.cur_t.copy())
return self.cur_R, self.cur_t
def get_trajectory(self):
"""κΆ€μ λ°ν"""
return np.array([t.ravel() for t in self.trajectory])
# μ¬μ© μ
K = np.array([
[718.856, 0, 607.1928],
[0, 718.856, 185.2157],
[0, 0, 1]
], dtype=np.float32)
vo = MonocularVO(K)
cap = cv2.VideoCapture('driving.mp4')
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
R, t = vo.process_frame(frame)
# νμ¬ μμΉ μΆλ ₯
x, y, z = t.ravel()
print(f"Position: x={x:.2f}, y={y:.2f}, z={z:.2f}")
cap.release()
# κΆ€μ μκ°ν
trajectory = vo.get_trajectory()
μ€ν λ μ€ Visual Odometry¶
class StereoVO:
"""μ€ν
λ μ€ Visual Odometry"""
def __init__(self, K, baseline, detector='ORB'):
self.K = K
self.baseline = baseline
self.focal = K[0, 0]
self.detector = cv2.ORB_create(3000)
self.bf = cv2.BFMatcher(cv2.NORM_HAMMING)
# μ€ν
λ μ€ λ§€μ²
self.stereo = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=128,
blockSize=5,
P1=8 * 3 * 5 ** 2,
P2=32 * 3 * 5 ** 2
)
self.prev_pts_3d = None
self.prev_kp = None
self.prev_desc = None
self.cur_R = np.eye(3)
self.cur_t = np.zeros((3, 1))
def compute_depth(self, left, right):
"""μ€ν
λ μ€ λ§€μΉμΌλ‘ κΉμ΄ κ³μ°"""
disparity = self.stereo.compute(left, right).astype(np.float32) / 16.0
# μμ°¨ β κΉμ΄
depth = np.zeros_like(disparity)
valid = disparity > 0
depth[valid] = self.focal * self.baseline / disparity[valid]
return depth
def get_3d_points(self, kp, depth):
"""2D ν€ν¬μΈνΈλ₯Ό 3Dλ‘ λ³ν"""
fx = self.K[0, 0]
fy = self.K[1, 1]
cx = self.K[0, 2]
cy = self.K[1, 2]
pts_3d = []
valid_indices = []
for i, pt in enumerate(kp):
x, y = int(pt.pt[0]), int(pt.pt[1])
if 0 <= x < depth.shape[1] and 0 <= y < depth.shape[0]:
z = depth[y, x]
if z > 0 and z < 100: # μ ν¨ν κΉμ΄
X = (pt.pt[0] - cx) * z / fx
Y = (pt.pt[1] - cy) * z / fy
pts_3d.append([X, Y, z])
valid_indices.append(i)
return np.array(pts_3d), valid_indices
def process_frame(self, left, right):
"""μ€ν
λ μ€ νλ μ μ²λ¦¬"""
# κ·Έλ μ΄μ€μΌμΌ
gray_left = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY)
gray_right = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY)
# κΉμ΄ κ³μ°
depth = self.compute_depth(gray_left, gray_right)
# νΉμ§ κ²μΆ
kp, desc = self.detector.detectAndCompute(gray_left, None)
# 3D μ κ³μ°
pts_3d, valid_idx = self.get_3d_points(kp, depth)
if self.prev_pts_3d is None:
self.prev_pts_3d = pts_3d
self.prev_kp = [kp[i] for i in valid_idx]
self.prev_desc = desc[valid_idx]
return self.cur_R, self.cur_t
# μ΄μ νλ μκ³Ό λ§€μΉ
matches = self.bf.knnMatch(self.prev_desc, desc[valid_idx], k=2)
good_matches = []
for m, n in matches:
if m.distance < 0.7 * n.distance:
good_matches.append(m)
if len(good_matches) >= 6:
# 3D-2D λμμ
obj_points = np.array([
self.prev_pts_3d[m.queryIdx] for m in good_matches
])
img_points = np.array([
kp[valid_idx[m.trainIdx]].pt for m in good_matches
])
# PnPλ‘ ν¬μ¦ μΆμ
success, rvec, tvec, inliers = cv2.solvePnPRansac(
obj_points, img_points, self.K, None
)
if success and inliers is not None and len(inliers) > 10:
R, _ = cv2.Rodrigues(rvec)
# ν¬μ¦ λμ
self.cur_t = self.cur_t + self.cur_R @ tvec
self.cur_R = R @ self.cur_R
# μν μ
λ°μ΄νΈ
self.prev_pts_3d = pts_3d
self.prev_kp = [kp[i] for i in valid_idx]
self.prev_desc = desc[valid_idx]
return self.cur_R, self.cur_t
3. ORB-SLAM¶
ORB-SLAM κ°μ¶
ORB-SLAM μν€ν
μ²:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β ORB-SLAM: κ°μ₯ λ리 μ¬μ©λλ Visual SLAM μμ€ν
β
β β
β λ²μ : β
β - ORB-SLAM (2015): λ¨μ β
β - ORB-SLAM2 (2017): λ¨μ/μ€ν
λ μ€/RGB-D β
β - ORB-SLAM3 (2021): Visual-Inertial, λ€μ€ λ§΅ β
β β
β 3κ°μ λ³λ ¬ μ€λ λ: β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β βββββββββββββββ βββββββββββββββ βββββββββββββββ β β
β β β Tracking β βLocal Mappingβ βLoop Closing β β β
β β β Thread β β Thread β β Thread β β β
β β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββ¬βββββββ β β
β β β β β β β
β β β Keyframes β β β β
β β βββββββββββββββββΆβ β β β
β β β Keyframes β β β
β β βββββββββββββββββΆβ β β
β β β β β
β β ββββββββββββββββββββββββββββββββββββββββββ β β
β β β Map (MapPoints) ββ β β
β β β & Covisibility Graph ββ β β
β β ββββββββββββββββββββββββββββββββββββββββββ β β
β β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tracking μ€λ λ:
- λ§€ νλ μ μ²λ¦¬
- ORB νΉμ§ μΆμΆ
- μ΄μ νλ μ λλ λ§΅κ³Ό λ§€μΉ
- μ΄κΈ° ν¬μ¦ μΆμ
- ν€νλ μ κ²°μ
Local Mapping μ€λ λ:
- μ ν€νλ μ μ½μ
- μ΅κ·Ό MapPoint 컬λ§
- μ MapPoint μμ±
- Local Bundle Adjustment
- μ€λ³΅ ν€νλ μ μ κ±°
Loop Closing μ€λ λ:
- 루ν ν보 κ²μΆ (DBoW2)
- 루ν κ²μ¦ λ° λ³΄μ
- Essential Graph μ΅μ ν
- μ μ Bundle Adjustment
ORB νΉμ§κ³Ό Bag of Words¶
import cv2
import numpy as np
class ORBVocabulary:
"""ORB κΈ°λ° Bag of Words"""
def __init__(self, num_words=1000):
self.orb = cv2.ORB_create(1000)
self.num_words = num_words
self.vocabulary = None
self.bf = cv2.BFMatcher(cv2.NORM_HAMMING)
def train(self, images):
"""μ΄λ―Έμ§λ‘λΆν° vocabulary νμ΅"""
all_descriptors = []
for img in images:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, desc = self.orb.detectAndCompute(gray, None)
if desc is not None:
all_descriptors.append(desc)
all_desc = np.vstack(all_descriptors)
# K-means ν΄λ¬μ€ν°λ§
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
100, 0.2)
_, labels, centers = cv2.kmeans(
all_desc.astype(np.float32),
self.num_words,
None,
criteria,
10,
cv2.KMEANS_RANDOM_CENTERS
)
self.vocabulary = centers.astype(np.uint8)
print(f"Vocabulary μμ± μλ£: {self.num_words} words")
def compute_bow(self, img):
"""μ΄λ―Έμ§μ BoW λ²‘ν° κ³μ°"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, desc = self.orb.detectAndCompute(gray, None)
if desc is None:
return np.zeros(self.num_words)
# κ° λμ€ν¬λ¦½ν°λ₯Ό κ°μ₯ κ°κΉμ΄ vocabulary wordμ ν λΉ
matches = self.bf.match(desc, self.vocabulary)
bow = np.zeros(self.num_words)
for m in matches:
bow[m.trainIdx] += 1
# μ κ·ν
bow = bow / (np.linalg.norm(bow) + 1e-6)
return bow
def compute_similarity(self, bow1, bow2):
"""λ BoW 벑ν°μ μ μ¬λ"""
return np.dot(bow1, bow2)
class SimpleSLAM:
"""κ°λ¨ν SLAM μμ€ν
(ORB-SLAM 컨μ
)"""
def __init__(self, K):
self.K = K
self.orb = cv2.ORB_create(2000)
self.bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
# λ§΅
self.keyframes = [] # ν€νλ μ λͺ©λ‘
self.map_points = [] # 3D ν¬μΈνΈ
self.poses = [] # ν€νλ μ ν¬μ¦
# νμ¬ μν
self.cur_R = np.eye(3)
self.cur_t = np.zeros((3, 1))
self.prev_frame = None
self.prev_kp = None
self.prev_desc = None
# ν€νλ μ κΈ°μ€
self.kf_threshold = 30 # μ΅μ λ§€μΉ μ
def is_keyframe(self, num_matches, motion):
"""ν€νλ μ μ¬λΆ κ²°μ """
# κ°λ¨ν κΈ°μ€: λ§€μΉ μκ° μ κ±°λ μμ§μμ΄ ν¬λ©΄ ν€νλ μ
translation = np.linalg.norm(motion)
if num_matches < self.kf_threshold or translation > 0.5:
return True
return False
def add_keyframe(self, frame, kp, desc, pose):
"""ν€νλ μ μΆκ°"""
keyframe = {
'frame': frame.copy(),
'keypoints': kp,
'descriptors': desc,
'pose': pose.copy()
}
self.keyframes.append(keyframe)
self.poses.append(pose)
print(f"Keyframe μΆκ°: μ΄ {len(self.keyframes)}κ°")
def process_frame(self, frame):
"""νλ μ μ²λ¦¬ (Tracking)"""
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
kp, desc = self.orb.detectAndCompute(gray, None)
if self.prev_frame is None:
# 첫 νλ μ β ν€νλ μ
pose = {'R': np.eye(3), 't': np.zeros((3, 1))}
self.add_keyframe(gray, kp, desc, pose)
self.prev_frame = gray
self.prev_kp = kp
self.prev_desc = desc
return self.cur_R, self.cur_t
# μ΄μ νλ μκ³Ό λ§€μΉ
matches = self.bf.match(self.prev_desc, desc)
matches = sorted(matches, key=lambda x: x.distance)[:500]
if len(matches) >= 8:
# λ§€μΉμ μΆμΆ
pts1 = np.float32([self.prev_kp[m.queryIdx].pt for m in matches])
pts2 = np.float32([kp[m.trainIdx].pt for m in matches])
# Essential Matrixλ‘ ν¬μ¦ μΆμ
E, mask = cv2.findEssentialMat(pts1, pts2, self.K)
_, R, t, mask = cv2.recoverPose(E, pts1, pts2, self.K)
# ν¬μ¦ λμ
self.cur_t = self.cur_t + self.cur_R @ t
self.cur_R = R @ self.cur_R
# ν€νλ μ 체ν¬
if self.is_keyframe(len(matches), t):
pose = {'R': self.cur_R.copy(), 't': self.cur_t.copy()}
self.add_keyframe(gray, kp, desc, pose)
# μν μ
λ°μ΄νΈ
self.prev_frame = gray
self.prev_kp = kp
self.prev_desc = desc
return self.cur_R, self.cur_t
def get_camera_trajectory(self):
"""μΉ΄λ©λΌ κΆ€μ λ°ν"""
trajectory = []
for pose in self.poses:
R = pose['R']
t = pose['t']
# μΉ΄λ©λΌ μμΉ = -R^T * t
pos = -R.T @ t
trajectory.append(pos.ravel())
return np.array(trajectory)
4. LiDAR SLAM¶
LiDAR SLAM κ°μ¶
LiDAR SLAM:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β LiDAR μΌμ νΉμ§: β
β - 360λ μ€μΊ β
β - μ νν 거리 μΈ‘μ β
β - μ‘°λͺ
쑰건μ κ°κ±΄ β
β - νλΆν 3D ν¬μΈνΈ ν΄λΌμ°λ β
β β
β LiDAR μ’
λ₯: β
β ββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
β β 2D LiDAR β νλ©΄ μ€μΊ, μ λ ΄, λ‘λ΄ μ²μκΈ° β β
β β (μ: RPLiDAR) β β β
β ββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββ€ β
β β 3D LiDAR β 3D ν¬μΈνΈ ν΄λΌμ°λ, μμ¨μ£Όν β β
β β (μ: Velodyne) β β β
β ββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββ€ β
β β Solid-State β 무νμ , μν, μ΅μ νΈλ λ β β
β β (μ: Livox) β β β
β ββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββ β
β β
β μ£Όμ μκ³ λ¦¬μ¦: β
β - ICP (Iterative Closest Point) β
β - NDT (Normal Distributions Transform) β
β - LOAM (LiDAR Odometry and Mapping) β
β - LeGO-LOAM (Lightweight Ground-Optimized) β
β - Cartographer (Google) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ICP (Iterative Closest Point)¶
import numpy as np
from scipy.spatial import KDTree
def icp(source, target, max_iterations=50, tolerance=1e-6):
"""
ICP μκ³ λ¦¬μ¦μΌλ‘ λ ν¬μΈνΈ ν΄λΌμ°λ μ ν©
Parameters:
source: μμ€ ν¬μΈνΈ ν΄λΌμ°λ (N x 3)
target: νκ² ν¬μΈνΈ ν΄λΌμ°λ (M x 3)
Returns:
R: νμ νλ ¬ (3 x 3)
t: νν μ΄λ λ²‘ν° (3,)
transformed: λ³νλ μμ€ ν¬μΈνΈ
"""
src = source.copy()
prev_error = float('inf')
R_total = np.eye(3)
t_total = np.zeros(3)
# KD-Treeλ‘ ν¨μ¨μ μΈ μ΅κ·Όμ νμ
tree = KDTree(target)
for i in range(max_iterations):
# 1. μ΅κ·Όμ λμμ μ°ΎκΈ°
distances, indices = tree.query(src)
correspondences = target[indices]
# 2. λ³ν μΆμ (SVD)
src_centroid = np.mean(src, axis=0)
tgt_centroid = np.mean(correspondences, axis=0)
src_centered = src - src_centroid
tgt_centered = correspondences - tgt_centroid
H = src_centered.T @ tgt_centered
U, _, Vt = np.linalg.svd(H)
R = Vt.T @ U.T
# λ°μ¬ 보μ
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
t = tgt_centroid - R @ src_centroid
# 3. λ³ν μ μ©
src = (R @ src.T).T + t
# λμ λ³ν
R_total = R @ R_total
t_total = R @ t_total + t
# 4. μλ ΄ νμΈ
mean_error = np.mean(distances)
if abs(prev_error - mean_error) < tolerance:
print(f"ICP μλ ΄: {i+1} λ°λ³΅, μ€μ°¨: {mean_error:.6f}")
break
prev_error = mean_error
return R_total, t_total, src
class LiDARSLAM:
"""κ°λ¨ν 2D LiDAR SLAM"""
def __init__(self, map_resolution=0.05):
self.resolution = map_resolution
self.pose = np.array([0.0, 0.0, 0.0]) # x, y, theta
self.trajectory = [self.pose.copy()]
# μ μ 격μ λ§΅
self.map_size = 1000
self.occupancy_map = np.ones((self.map_size, self.map_size)) * 0.5
self.map_origin = np.array([self.map_size // 2, self.map_size // 2])
def scan_to_points(self, scan_ranges, scan_angles):
"""μ€μΊ λ°μ΄ν°λ₯Ό 2D ν¬μΈνΈλ‘ λ³ν"""
valid = (scan_ranges > 0.1) & (scan_ranges < 30.0)
ranges = scan_ranges[valid]
angles = scan_angles[valid]
x = ranges * np.cos(angles)
y = ranges * np.sin(angles)
return np.column_stack([x, y])
def transform_points(self, points, pose):
"""ν¬μΈνΈλ₯Ό μλ μ’νλ‘ λ³ν"""
x, y, theta = pose
R = np.array([
[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]
])
transformed = (R @ points.T).T + np.array([x, y])
return transformed
def point_to_grid(self, points):
"""ν¬μΈνΈλ₯Ό 그리λ μ’νλ‘ λ³ν"""
grid_x = (points[:, 0] / self.resolution + self.map_origin[0]).astype(int)
grid_y = (points[:, 1] / self.resolution + self.map_origin[1]).astype(int)
# λ§΅ λ²μ λ΄λ‘ μ ν
valid = (grid_x >= 0) & (grid_x < self.map_size) & \
(grid_y >= 0) & (grid_y < self.map_size)
return grid_x[valid], grid_y[valid], valid
def update_map(self, scan_points, pose):
"""μ μ 격μ λ§΅ μ
λ°μ΄νΈ"""
world_points = self.transform_points(scan_points, pose)
gx, gy, valid = self.point_to_grid(world_points)
# μ μ νλ₯ μ
λ°μ΄νΈ (λ‘κ·Έ μ€μ¦)
self.occupancy_map[gy, gx] = np.clip(
self.occupancy_map[gy, gx] + 0.1, 0, 1
)
def match_scan(self, current_points, previous_points):
"""μ€μΊ λ§€μΉμΌλ‘ μλ μ΄λ μΆμ """
if len(previous_points) < 10 or len(current_points) < 10:
return np.array([0, 0, 0])
# ICP μ μ©
R, t, _ = icp(current_points, previous_points)
# 2Dμμ theta μΆμΆ
theta = np.arctan2(R[1, 0], R[0, 0])
return np.array([t[0], t[1], theta])
def process_scan(self, scan_ranges, scan_angles, prev_scan=None):
"""μ€μΊ μ²λ¦¬"""
current_points = self.scan_to_points(scan_ranges, scan_angles)
if prev_scan is not None:
prev_points = self.scan_to_points(prev_scan[0], prev_scan[1])
# μ€μΊ λ§€μΉ
delta_pose = self.match_scan(current_points, prev_points)
# ν¬μ¦ μ
λ°μ΄νΈ
self.pose[2] += delta_pose[2]
R = np.array([
[np.cos(self.pose[2]), -np.sin(self.pose[2])],
[np.sin(self.pose[2]), np.cos(self.pose[2])]
])
self.pose[:2] += R @ delta_pose[:2]
# λ§΅ μ
λ°μ΄νΈ
self.update_map(current_points, self.pose)
# κΆ€μ μ μ₯
self.trajectory.append(self.pose.copy())
return self.pose
def get_occupancy_map(self):
"""μ μ λ§΅ λ°ν"""
return self.occupancy_map
def get_trajectory(self):
"""κΆ€μ λ°ν"""
return np.array(self.trajectory)
5. Loop Closure¶
Loop Closure κ°λ ¶
Loop Closure (루ν νν©):
μ΄μ μ λ°©λ¬Έν μ₯μλ₯Ό μ¬μΈμνμ¬ λμ μ€μ°¨ 보μ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β λ¬Έμ : Drift (λμ μ€μ°¨) β
β β
β μ€μ κ²½λ‘ μΆμ κ²½λ‘ (drift μμ) β
β βββββββββββ βββββββββββ β
β β β β β² β
β β β β β² β
β β β β β² β
β βββββββββββ ββββββββββββββ² β
β (ν곑μ ) (μ΄λ¦° 곑μ ) β
β β
β ν΄κ²°: Loop Closure β
β 1. νμ¬ μμΉκ° μ΄μ μ λ°©λ¬Έν κ³³μΈμ§ νμ§ β
β 2. 루ν μ μ½ μ‘°κ±΄ μΆκ° β
β 3. ν¬μ¦ κ·Έλν μ΅μ ν β
β β
β βββββββββββ β
β β ββββββ β 루ν νμ§ β
β β β β β
β β β β β κ·Έλν μ΅μ ν β
β β ββββββ β
β βββββββββββ β
β (보μ λ κ²½λ‘) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Loop Closure ꡬν¶
import cv2
import numpy as np
from collections import deque
class LoopClosureDetector:
"""Bag of Words κΈ°λ° λ£¨ν ν΄λ‘μ νμ§"""
def __init__(self, vocabulary_size=1000, min_score=0.3):
self.orb = cv2.ORB_create(2000)
self.bf = cv2.BFMatcher(cv2.NORM_HAMMING)
self.vocabulary = None
self.vocabulary_size = vocabulary_size
self.min_score = min_score
# ν€νλ μ λ°μ΄ν°λ² μ΄μ€
self.keyframe_bows = []
self.keyframe_descs = []
self.keyframe_kps = []
# μ΅κ·Ό Nκ° ν€νλ μμ 루ν ν보μμ μ μΈ
self.temporal_window = 30
def build_vocabulary(self, training_images):
"""Vocabulary ꡬμΆ"""
all_descriptors = []
for img in training_images:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, desc = self.orb.detectAndCompute(gray, None)
if desc is not None:
all_descriptors.append(desc)
all_desc = np.vstack(all_descriptors).astype(np.float32)
# K-means
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
100, 0.2)
_, _, self.vocabulary = cv2.kmeans(
all_desc, self.vocabulary_size, None,
criteria, 10, cv2.KMEANS_RANDOM_CENTERS
)
self.vocabulary = self.vocabulary.astype(np.uint8)
def compute_bow(self, descriptors):
"""BoW λ²‘ν° κ³μ°"""
if self.vocabulary is None or descriptors is None:
return None
matches = self.bf.match(descriptors, self.vocabulary)
bow = np.zeros(self.vocabulary_size)
for m in matches:
bow[m.trainIdx] += 1
# L2 μ κ·ν
norm = np.linalg.norm(bow)
if norm > 0:
bow = bow / norm
return bow
def add_keyframe(self, frame):
"""ν€νλ μ μΆκ°"""
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
kp, desc = self.orb.detectAndCompute(gray, None)
if desc is None:
return -1
bow = self.compute_bow(desc)
self.keyframe_bows.append(bow)
self.keyframe_descs.append(desc)
self.keyframe_kps.append(kp)
return len(self.keyframe_bows) - 1
def detect_loop(self, query_idx):
"""루ν ν보 νμ§"""
if query_idx < self.temporal_window + 1:
return None, 0
query_bow = self.keyframe_bows[query_idx]
best_match = -1
best_score = 0
# μκ°μ μΌλ‘ λ©λ¦¬ λ¨μ΄μ§ ν€νλ μλ§ κ²μ
for i in range(query_idx - self.temporal_window):
score = np.dot(query_bow, self.keyframe_bows[i])
if score > best_score and score > self.min_score:
best_score = score
best_match = i
if best_match >= 0:
return best_match, best_score
return None, 0
def verify_loop(self, query_idx, candidate_idx, min_inliers=50):
"""κΈ°ννμ κ²μ¦μΌλ‘ 루ν νμΈ"""
desc1 = self.keyframe_descs[query_idx]
desc2 = self.keyframe_descs[candidate_idx]
kp1 = self.keyframe_kps[query_idx]
kp2 = self.keyframe_kps[candidate_idx]
# νΉμ§μ λ§€μΉ
matches = self.bf.knnMatch(desc1, desc2, k=2)
good_matches = []
for m, n in matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
if len(good_matches) < 8:
return False, None
# Fundamental Matrixλ‘ κΈ°ννμ κ²μ¦
pts1 = np.float32([kp1[m.queryIdx].pt for m in good_matches])
pts2 = np.float32([kp2[m.trainIdx].pt for m in good_matches])
F, mask = cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC)
if mask is None:
return False, None
num_inliers = np.sum(mask)
if num_inliers >= min_inliers:
return True, {
'query_idx': query_idx,
'match_idx': candidate_idx,
'inliers': num_inliers,
'pts1': pts1[mask.ravel() == 1],
'pts2': pts2[mask.ravel() == 1]
}
return False, None
class PoseGraphOptimizer:
"""κ°λ¨ν ν¬μ¦ κ·Έλν μ΅μ ν"""
def __init__(self):
self.poses = [] # λ
Έλ (ν¬μ¦)
self.edges = [] # μ£μ§ (μλ λ³ν)
self.loop_constraints = [] # 루ν μ μ½
def add_pose(self, pose):
"""ν¬μ¦ λ
Έλ μΆκ°"""
self.poses.append(pose.copy())
return len(self.poses) - 1
def add_odometry_edge(self, i, j, relative_pose, info_matrix=None):
"""μ€λλ©νΈλ¦¬ μ£μ§ μΆκ°"""
if info_matrix is None:
info_matrix = np.eye(3)
self.edges.append({
'from': i,
'to': j,
'measurement': relative_pose,
'info': info_matrix
})
def add_loop_constraint(self, i, j, relative_pose, info_matrix=None):
"""루ν μ μ½ μΆκ°"""
if info_matrix is None:
# 루ν μ μ½μ λμ κ°μ€μΉ
info_matrix = np.eye(3) * 100
self.loop_constraints.append({
'from': i,
'to': j,
'measurement': relative_pose,
'info': info_matrix
})
def optimize(self, num_iterations=10):
"""κ·Έλν μ΅μ ν (Gauss-Newton)"""
# κ°λ¨ν ꡬν (μ€μ λ‘λ g2o, Ceres λ± μ¬μ©)
print("ν¬μ¦ κ·Έλν μ΅μ νλ g2o λ± μ λ¬Έ λΌμ΄λΈλ¬λ¦¬ κΆμ₯")
# 루ν μ μ½μ μ΄μ©ν κ°λ¨ν 보μ
for constraint in self.loop_constraints:
i = constraint['from']
j = constraint['to']
# λμ λ리ννΈ κ³μ°
drift = self.poses[j][:2] - self.poses[i][:2]
drift -= constraint['measurement'][:2]
# μ ν 보κ°μΌλ‘ λ리ννΈ λΆλ°°
for k in range(i, j + 1):
alpha = (k - i) / (j - i) if j > i else 0
self.poses[k][:2] -= alpha * drift
return self.poses
6. SLAM ꡬν μ€μ΅¶
κ°λ¨ν SLAM μμ€ν ¶
import cv2
import numpy as np
class SimpleVSLAM:
"""κ°λ¨ν Visual SLAM μμ€ν
"""
def __init__(self, K):
self.K = K
# λͺ¨λ
self.vo = MonocularVO(K)
self.loop_detector = LoopClosureDetector()
self.pose_graph = PoseGraphOptimizer()
# μν
self.frame_count = 0
self.keyframe_interval = 10
def process_frame(self, frame):
"""νλ μ μ²λ¦¬"""
self.frame_count += 1
# Visual Odometry
R, t = self.vo.process_frame(frame)
# ν€νλ μ μΆκ°
if self.frame_count % self.keyframe_interval == 0:
kf_idx = self.loop_detector.add_keyframe(frame)
# ν¬μ¦ κ·Έλνμ λ
Έλ μΆκ°
pose = np.array([t[0, 0], t[1, 0], 0]) # 2D κ·Όμ¬
node_idx = self.pose_graph.add_pose(pose)
# μ΄μ ν€νλ μκ³Ό μ£μ§ μ°κ²°
if node_idx > 0:
prev_pose = self.pose_graph.poses[node_idx - 1]
relative = pose - prev_pose
self.pose_graph.add_odometry_edge(
node_idx - 1, node_idx, relative
)
# 루ν νμ§
if kf_idx > 30: # μΆ©λΆν ν€νλ μ ν
candidate, score = self.loop_detector.detect_loop(kf_idx)
if candidate is not None:
verified, loop_info = self.loop_detector.verify_loop(
kf_idx, candidate
)
if verified:
print(f"Loop detected: {kf_idx} -> {candidate}")
# 루ν μ μ½ μΆκ°
relative = pose - self.pose_graph.poses[candidate]
self.pose_graph.add_loop_constraint(
candidate, node_idx, relative
)
# μ΅μ ν
self.pose_graph.optimize()
return R, t
def get_map(self):
"""λ§΅ λ°ν"""
return self.vo.get_trajectory()
def get_optimized_trajectory(self):
"""μ΅μ νλ κΆ€μ λ°ν"""
return np.array(self.pose_graph.poses)
μκ°ν¶
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def visualize_slam_result(trajectory, loop_closures=None):
"""SLAM κ²°κ³Ό μκ°ν"""
fig = plt.figure(figsize=(12, 5))
# 2D κΆ€μ
ax1 = fig.add_subplot(121)
ax1.plot(trajectory[:, 0], trajectory[:, 1], 'b-', linewidth=1)
ax1.scatter(trajectory[0, 0], trajectory[0, 1],
c='green', s=100, marker='o', label='Start')
ax1.scatter(trajectory[-1, 0], trajectory[-1, 1],
c='red', s=100, marker='x', label='End')
if loop_closures:
for lc in loop_closures:
i, j = lc['from'], lc['to']
ax1.plot([trajectory[i, 0], trajectory[j, 0]],
[trajectory[i, 1], trajectory[j, 1]],
'g--', linewidth=2, alpha=0.5)
ax1.set_xlabel('X')
ax1.set_ylabel('Y')
ax1.set_title('2D Trajectory')
ax1.legend()
ax1.axis('equal')
ax1.grid(True)
# 3D κΆ€μ
ax2 = fig.add_subplot(122, projection='3d')
ax2.plot(trajectory[:, 0], trajectory[:, 1], trajectory[:, 2],
'b-', linewidth=1)
ax2.set_xlabel('X')
ax2.set_ylabel('Y')
ax2.set_zlabel('Z')
ax2.set_title('3D Trajectory')
plt.tight_layout()
plt.show()
def visualize_occupancy_map(occupancy_map, trajectory=None):
"""μ μ λ§΅ μκ°ν"""
plt.figure(figsize=(10, 10))
# λ§΅ νμ
plt.imshow(occupancy_map, cmap='gray', origin='lower')
# κΆ€μ μ€λ²λ μ΄
if trajectory is not None:
# λ§΅ μ’νλ‘ λ³ν
map_center = occupancy_map.shape[0] // 2
resolution = 0.05
traj_map = trajectory / resolution + map_center
plt.plot(traj_map[:, 0], traj_map[:, 1], 'r-', linewidth=2)
plt.scatter(traj_map[0, 0], traj_map[0, 1], c='green', s=100)
plt.scatter(traj_map[-1, 0], traj_map[-1, 1], c='blue', s=100)
plt.title('Occupancy Grid Map')
plt.xlabel('X')
plt.ylabel('Y')
plt.colorbar(label='Occupancy Probability')
plt.show()
7. μ°μ΅ λ¬Έμ ¶
λ¬Έμ 1: Visual Odometry ꡬν¶
λ¨μ Visual Odometryλ₯Ό ꡬννμΈμ.
μꡬμ¬ν: - ORB νΉμ§ κ²μΆ - κ΄ν νλ¦ λλ λμ€ν¬λ¦½ν° λ§€μΉ - Essential Matrixλ‘ ν¬μ¦ μΆμ - κΆ€μ μκ°ν
ννΈ
# Essential Matrix
E, mask = cv2.findEssentialMat(pts1, pts2, K)
_, R, t, _ = cv2.recoverPose(E, pts1, pts2, K)
# ν¬μ¦ λμ
cur_t = cur_t + cur_R @ t
cur_R = R @ cur_R
λ¬Έμ 2: 루ν ν΄λ‘μ νμ§¶
BoW κΈ°λ° λ£¨ν ν΄λ‘μ λ₯Ό ꡬννμΈμ.
μꡬμ¬ν: - ORB vocabulary κ΅¬μΆ - BoW λ²‘ν° κ³μ° - μ μ¬λ κΈ°λ° ν보 νμ§ - κΈ°ννμ κ²μ¦
ννΈ
# BoW μ μ¬λ
score = np.dot(bow1, bow2)
# κΈ°ννμ κ²μ¦
F, mask = cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC)
inliers = np.sum(mask)
λ¬Έμ 3: ICP ꡬν¶
ICP μκ³ λ¦¬μ¦μ ꡬννμΈμ.
μꡬμ¬ν: - μ΅κ·Όμ λμμ κ²μ - SVDλ‘ λ³ν μΆμ - λ°λ³΅ μ΅μ ν - μλ ΄ 쑰건
ννΈ
# SVDλ‘ R, t κ³μ°
H = src_centered.T @ tgt_centered
U, _, Vt = np.linalg.svd(H)
R = Vt.T @ U.T
t = tgt_centroid - R @ src_centroid
λ¬Έμ 4: μ μ 격μ λ§΅¶
LiDAR λ°μ΄ν°λ‘ μ μ 격μ λ§΅μ μμ±νμΈμ.
μꡬμ¬ν: - μ€μΊ λ°μ΄ν°λ₯Ό ν¬μΈνΈλ‘ λ³ν - 격μ μ’ν λ³ν - μ μ νλ₯ μ λ°μ΄νΈ - λ§΅ μκ°ν
ννΈ
# λ‘κ·Έ μ€μ¦ μ
λ°μ΄νΈ
log_odds = np.log(p / (1 - p))
log_odds[occupied] += 0.5
log_odds[free] -= 0.2
p = 1 / (1 + np.exp(-log_odds))
λ¬Έμ 5: μμ ν SLAM μμ€ν ¶
VO, 루ν ν΄λ‘μ , λ§΅νμ ν΅ν©ν SLAMμ ꡬννμΈμ.
μꡬμ¬ν: - ν€νλ μ κ΄λ¦¬ - 루ν νμ§ λ° κ²μ¦ - ν¬μ¦ κ·Έλν μ΅μ ν - 3D λ§΅ μμ±
ννΈ
# ν΅ν© μμ€ν
class SLAM:
def process(self, frame):
# 1. νΈλνΉ
pose = self.track(frame)
# 2. ν€νλ μμ΄λ©΄ λ§΅ μ
λ°μ΄νΈ
if self.is_keyframe():
self.local_mapping()
# 3. 루ν νμ§
if self.detect_loop():
self.optimize_graph()
λ€μ λ¨κ³¶
- μ€μ SLAM λΌμ΄λΈλ¬λ¦¬ μ¬μ© (ORB-SLAM3, RTAB-Map)
- ROS μ°λ
- Visual-Inertial SLAM
- λ₯λ¬λ κΈ°λ° SLAM