""" * This file is part of PYSLAM * * Copyright (C) 2016-present Luigi Freda <luigi dot freda at gmail dot com> * * PYSLAM is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PYSLAM is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PYSLAM. If not, see <http://www.gnu.org/licenses/>. """ import numpy as np import time from enum import Enum from collections import defaultdict, Counter from itertools import chain import cv2 import g2o from parameters import Parameters from frame import Frame, match_frames from keyframe import KeyFrame from map_point import MapPoint from map import Map from search_points import propagate_map_point_matches from search_points import search_map_by_projection, search_frame_by_projection from local_mapping import LocalMapping from initializer import Initializer import optimizer_g2o from timer import TimerFps from slam_dynamic_config import SLAMDynamicConfig from motion_model import MotionModel, MotionModelDamping from feature_tracker import FeatureTrackerTypes from utils import Printer, getchar, Logging from utils_draw import draw_feature_matches from utils_geom import triangulate_points, poseRt, normalize_vector, inv_T, triangulate_normalized_points, estimate_pose_ess_mat kVerbose = True kTimerVerbose = False kDebugDrawMatches = False kLocalMappingOnSeparateThread = Parameters.kLocalMappingOnSeparateThread kTrackingWaitForLocalMappingToGetIdle = Parameters.kTrackingWaitForLocalMappingToGetIdle kTrackingWaitForLocalMappingSleepTime = Parameters.kTrackingWaitForLocalMappingSleepTime kLogKFinfoToFile = True kUseDynamicDesDistanceTh = True kRansacThresholdNormalized = 0.0003 # 0.0003 # metric threshold used for normalized image coordinates kRansacProb = 0.999 kNumMinInliersEssentialMat = 8 kUseGroundTruthScale = False kNumMinInliersPoseOptimizationTrackFrame = 10 kNumMinInliersPoseOptimizationTrackLocalMap = 20 kUseMotionModel = Parameters.kUseMotionModel or Parameters.kUseSearchFrameByProjection kUseSearchFrameByProjection = Parameters.kUseSearchFrameByProjection and not Parameters.kUseEssentialMatrixFitting kUseEssentialMatrixFitting = Parameters.kUseEssentialMatrixFitting kNumMinObsForKeyFrameDefault = 3 if not kVerbose: def print(*args, **kwargs): pass class SlamState(Enum): NO_IMAGES_YET=0, NOT_INITIALIZED=1, OK=2, LOST=3 class TrackingHistory(object): def __init__(self): self.relative_frame_poses = [] # list of relative frame poses as g2o.Isometry3d() (see camera_pose.py) self.kf_references = [] # list of reference keyframes self.timestamps = [] # list of frame timestamps self.slam_states = [] # list of slam states # main slam class containing all the required modules class Slam(object): def __init__(self, camera, feature_tracker, groundtruth = None): self.init_feature_tracker(feature_tracker) self.camera = camera self.map = Map() self.local_mapping = LocalMapping(self.map) if kLocalMappingOnSeparateThread: self.local_mapping.start() self.groundtruth = groundtruth # not actually used here; could be used for evaluating performances self.tracking = Tracking(self) def quit(self): if kLocalMappingOnSeparateThread: self.local_mapping.quit() def init_feature_tracker(self, tracker): Frame.set_tracker(tracker) # set the static field of the class if kUseEssentialMatrixFitting: Printer.orange('forcing feature matcher ratio_test to 0.8') tracker.matcher.ratio_test = 0.8 if tracker.tracker_type == FeatureTrackerTypes.LK: raise ValueError("You cannot use Lukas-Kanade tracker in this SLAM approach!") # @ main track method @ def track(self, img, frame_id, timestamp=None): return self.tracking.track(img,frame_id,timestamp) class Tracking(object): def __init__(self, system): if kDebugDrawMatches: Frame.is_store_imgs = True self.system = system self.camera = system.camera self.map = system.map self.local_mapping = system.local_mapping self.intializer = Initializer() self.motion_model = MotionModel() # motion model for current frame pose prediction without damping #self.motion_model = MotionModelDamping() # motion model for current frame pose prediction with damping self.dyn_config = SLAMDynamicConfig() self.descriptor_distance_sigma = Parameters.kMaxDescriptorDistance self.reproj_err_frame_map_sigma = Parameters.kMaxReprojectionDistanceMap self.max_frames_between_kfs = int(system.camera.fps) self.min_frames_between_kfs = 0 self.state = SlamState.NO_IMAGES_YET self.num_matched_kps = None # current number of matched keypoints self.num_inliers = None # current number of matched points self.num_matched_map_points = None # current number of matched map points (matched and found valid in current pose optimization) self.num_kf_ref_tracked_points = None # number of tracked points in k_ref (considering a minimum number of observations) self.mask_match = None self.pose_is_ok = False self.predicted_pose = None self.velocity = None self.f_cur = None self.idxs_cur = None self.f_ref = None self.idxs_ref = None self.kf_ref = None # reference keyframe (in general, different from last keyframe depending on the used approach) self.kf_last = None # last keyframe self.kid_last_BA = -1 # last keyframe id when performed BA self.local_keyframes = [] # local keyframes self.local_points = [] # local points self.tracking_history = TrackingHistory() self.timer_verbose = kTimerVerbose # set this to True if you want to print timings self.timer_main_track = TimerFps('Track', is_verbose = self.timer_verbose) self.timer_pose_opt = TimerFps('Pose optimization', is_verbose = self.timer_verbose) self.timer_seach_frame_proj = TimerFps('Search frame by proj', is_verbose = self.timer_verbose) self.timer_match = TimerFps('Match', is_verbose = self.timer_verbose) self.timer_pose_est = TimerFps('Ess mat pose estimation', is_verbose = self.timer_verbose) self.timer_frame = TimerFps('Frame', is_verbose = self.timer_verbose) self.timer_seach_map = TimerFps('Search map', is_verbose = self.timer_verbose) self.init_history = True self.poses = [] # history of poses self.t0_est = None # history of estimated translations self.t0_gt = None # history of ground truth translations (if available) self.traj3d_est = [] # history of estimated translations centered w.r.t. first one self.traj3d_gt = [] # history of estimated ground truth translations centered w.r.t. first one self.cur_R = None # current rotation w.r.t. world frame self.cur_t = None # current translation w.r.t. world frame self.trueX, self.trueY, self.trueZ = None, None, None self.groundtruth = system.groundtruth # not actually used here; could be used for evaluating performances if kLogKFinfoToFile: self.kf_info_logger = Logging.setup_file_logger('kf_info_logger', 'kf_info.log',formatter=Logging.simple_log_formatter) # estimate a pose from a fitted essential mat; # since we do not have an interframe translation scale, this fitting can be used to detect outliers, estimate interframe orientation and translation direction # N.B. read the NBs of the method estimate_pose_ess_mat(), where the limitations of this method are explained def estimate_pose_by_fitting_ess_mat(self, f_ref, f_cur, idxs_ref, idxs_cur): # N.B.: in order to understand the limitations of fitting an essential mat, read the comments of the method self.estimate_pose_ess_mat() self.timer_pose_est.start() # estimate inter frame camera motion by using found keypoint matches # output of the following function is: Trc = [Rrc, trc] with ||trc||=1 where c=cur, r=ref and pr = Trc * pc Mrc, self.mask_match = estimate_pose_ess_mat(f_ref.kpsn[idxs_ref], f_cur.kpsn[idxs_cur], method=cv2.RANSAC, prob=kRansacProb, threshold=kRansacThresholdNormalized) #Mcr = np.linalg.inv(poseRt(Mrc[:3, :3], Mrc[:3, 3])) Mcr = inv_T(Mrc) estimated_Tcw = np.dot(Mcr, f_ref.pose) self.timer_pose_est.refresh() # remove outliers from keypoint matches by using the mask computed with inter frame pose estimation mask_idxs = (self.mask_match.ravel() == 1) self.num_inliers = sum(mask_idxs) print('# inliers: ', self.num_inliers ) idxs_ref = idxs_ref[mask_idxs] idxs_cur = idxs_cur[mask_idxs] # if there are not enough inliers do not use the estimated pose if self.num_inliers < kNumMinInliersEssentialMat: #f_cur.update_pose(f_ref.pose) # reset estimated pose to previous frame Printer.red('Essential mat: not enough inliers!') else: # use the estimated pose as an initial guess for the subsequent pose optimization # set only the estimated rotation (essential mat computation does not provide a scale for the translation, see above) #f_cur.pose[:3,:3] = estimated_Tcw[:3,:3] # copy only the rotation #f_cur.pose[:,3] = f_ref.pose[:,3].copy() # override translation with ref frame translation Rcw = estimated_Tcw[:3,:3] # copy only the rotation tcw = f_ref.pose[:3,3] # override translation with ref frame translation f_cur.update_rotation_and_translation(Rcw, tcw) return idxs_ref, idxs_cur def pose_optimization(self, f_cur, name=''): print('pose opt %s ' % (name) ) pose_before=f_cur.pose.copy() # f_cur pose optimization 1 (here we use f_cur pose as first guess and exploit the matched map points of f_ref ) self.timer_pose_opt.start() pose_opt_error, self.pose_is_ok, self.num_matched_map_points = optimizer_g2o.pose_optimization(f_cur, verbose=False) self.timer_pose_opt.pause() print(' error^2: %f, ok: %d' % (pose_opt_error, int(self.pose_is_ok)) ) if not self.pose_is_ok: # if current pose optimization failed, reset f_cur pose f_cur.update_pose(pose_before) return self.pose_is_ok # track camera motion of f_cur w.r.t. f_ref def track_previous_frame(self, f_ref, f_cur): print('>>>> tracking previous frame ...') is_search_frame_by_projection_failure = False use_search_frame_by_projection = self.motion_model.is_ok and kUseSearchFrameByProjection and kUseMotionModel if use_search_frame_by_projection: # search frame by projection: match map points observed in f_ref with keypoints of f_cur print('search frame by projection') search_radius = Parameters.kMaxReprojectionDistanceFrame f_cur.reset_points() self.timer_seach_frame_proj.start() idxs_ref, idxs_cur, num_found_map_pts = search_frame_by_projection(f_ref, f_cur, max_reproj_distance=search_radius, max_descriptor_distance=self.descriptor_distance_sigma) self.timer_seach_frame_proj.refresh() self.num_matched_kps = len(idxs_cur) print("# matched map points in prev frame: %d " % self.num_matched_kps) # if not enough map point matches consider a larger search radius if self.num_matched_kps < Parameters.kMinNumMatchedFeaturesSearchFrameByProjection: f_cur.remove_frame_views(idxs_cur) f_cur.reset_points() idxs_ref, idxs_cur, num_found_map_pts = search_frame_by_projection(f_ref, f_cur, max_reproj_distance=2*search_radius, max_descriptor_distance=0.5*self.descriptor_distance_sigma) self.num_matched_kps = len(idxs_cur) Printer.orange("# matched map points in prev frame (wider search): %d " % self.num_matched_kps) if kDebugDrawMatches and True: img_matches = draw_feature_matches(f_ref.img, f_cur.img, f_ref.kps[idxs_ref], f_cur.kps[idxs_cur], f_ref.sizes[idxs_ref], f_cur.sizes[idxs_cur], horizontal=False) cv2.imshow('tracking frame by projection - matches', img_matches) cv2.waitKey(1) if self.num_matched_kps < Parameters.kMinNumMatchedFeaturesSearchFrameByProjection: f_cur.remove_frame_views(idxs_cur) f_cur.reset_points() is_search_frame_by_projection_failure = True Printer.red('Not enough matches in search frame by projection: ', self.num_matched_kps) else: # search frame by projection was successful if kUseDynamicDesDistanceTh: self.descriptor_distance_sigma = self.dyn_config.update_descriptor_stat(f_ref, f_cur, idxs_ref, idxs_cur) # store tracking info (for possible reuse) self.idxs_ref = idxs_ref self.idxs_cur = idxs_cur # f_cur pose optimization 1: # here, we use f_cur pose as first guess and exploit the matched map point of f_ref self.pose_optimization(f_cur,'proj-frame-frame') # update matched map points; discard outliers detected in last pose optimization num_matched_points = f_cur.clean_outlier_map_points() print(' # num_matched_map_points: %d' % (self.num_matched_map_points) ) #print(' # matched points: %d' % (num_matched_points) ) if not self.pose_is_ok or self.num_matched_map_points < kNumMinInliersPoseOptimizationTrackFrame: Printer.red('failure in tracking previous frame, # matched map points: ', self.num_matched_map_points) self.pose_is_ok = False if not use_search_frame_by_projection or is_search_frame_by_projection_failure: self.track_reference_frame(f_ref, f_cur,'match-frame-frame') # track camera motion of f_cur w.r.t. f_ref # estimate motion by matching keypoint descriptors def track_reference_frame(self, f_ref, f_cur, name=''): print('>>>> tracking reference %d ...' %(f_ref.id)) if f_ref is None: return # find keypoint matches between f_cur and kf_ref print('matching keypoints with ', Frame.feature_matcher.type.name) self.timer_match.start() idxs_cur, idxs_ref = match_frames(f_cur, f_ref) self.timer_match.refresh() self.num_matched_kps = idxs_cur.shape[0] print("# keypoints matched: %d " % self.num_matched_kps) if kUseEssentialMatrixFitting: # estimate camera orientation and inlier matches by fitting and essential matrix (see the limitations above) idxs_ref, idxs_cur = self.estimate_pose_by_fitting_ess_mat(f_ref, f_cur, idxs_ref, idxs_cur) if kUseDynamicDesDistanceTh: self.descriptor_distance_sigma = self.dyn_config.update_descriptor_stat(f_ref, f_cur, idxs_ref, idxs_cur) # propagate map point matches from kf_ref to f_cur (do not override idxs_ref, idxs_cur) num_found_map_pts_inter_frame, idx_ref_prop, idx_cur_prop = propagate_map_point_matches(f_ref, f_cur, idxs_ref, idxs_cur, max_descriptor_distance=self.descriptor_distance_sigma) print("# matched map points in prev frame: %d " % num_found_map_pts_inter_frame) if kDebugDrawMatches and True: img_matches = draw_feature_matches(f_ref.img, f_cur.img, f_ref.kps[idx_ref_prop], f_cur.kps[idx_cur_prop], f_ref.sizes[idx_ref_prop], f_cur.sizes[idx_cur_prop], horizontal=False) cv2.imshow('tracking frame (no projection) - matches', img_matches) cv2.waitKey(1) # store tracking info (for possible reuse) self.idxs_ref = idxs_ref self.idxs_cur = idxs_cur # f_cur pose optimization using last matches with kf_ref: # here, we use first guess of f_cur pose and propated map point matches from f_ref (matched keypoints) self.pose_optimization(f_cur, name) # update matched map points; discard outliers detected in last pose optimization num_matched_points = f_cur.clean_outlier_map_points() print(' # num_matched_map_points: %d' % (self.num_matched_map_points) ) #print(' # matched points: %d' % (num_matched_points) ) if not self.pose_is_ok or self.num_matched_map_points < kNumMinInliersPoseOptimizationTrackFrame: f_cur.remove_frame_views(idxs_cur) f_cur.reset_points() Printer.red('failure in tracking reference %d, # matched map points: %d' %(f_ref.id,self.num_matched_map_points)) self.pose_is_ok = False # track camera motion of f_cur w.r.t. given keyframe # estimate motion by matching keypoint descriptors def track_keyframe(self, keyframe, f_cur, name='match-frame-keyframe'): f_cur.update_pose(self.f_ref.pose.copy()) # start pose optimization from last frame pose self.track_reference_frame(keyframe, f_cur, name) def update_local_map(self): self.f_cur.clean_bad_map_points() #self.local_points = self.map.local_map.get_points() self.kf_ref, self.local_keyframes, self.local_points = self.map.local_map.get_frame_covisibles(self.f_cur) self.f_cur.kf_ref = self.kf_ref # track camera motion of f_cur w.r.t. the built local map # find matches between {local map points} (points in the built local map) and {unmatched keypoints of f_cur} def track_local_map(self, f_cur): if self.map.local_map.is_empty(): return print('>>>> tracking local map...') self.timer_seach_map.start() self.update_local_map() num_found_map_pts, reproj_err_frame_map_sigma, matched_points_frame_idxs = search_map_by_projection(self.local_points, f_cur, max_reproj_distance=self.reproj_err_frame_map_sigma, #Parameters.kMaxReprojectionDistanceMap, max_descriptor_distance=self.descriptor_distance_sigma, ratio_test=Parameters.kMatchRatioTestMap) # use the updated local map self.timer_seach_map.refresh() #print('reproj_err_sigma: ', reproj_err_frame_map_sigma, ' used: ', self.reproj_err_frame_map_sigma) print("# new matched map points in local map: %d " % num_found_map_pts) print("# local map points ", self.map.local_map.num_points()) if kDebugDrawMatches and True: img_matched_trails = f_cur.draw_feature_trails(f_cur.img.copy(), matched_points_frame_idxs, trail_max_length=3) cv2.imshow('tracking local map - matched trails', img_matched_trails) cv2.waitKey(1) # f_cur pose optimization 2 with all the matched local map points self.pose_optimization(f_cur,'proj-map-frame') f_cur.update_map_points_statistics() # here we do not reset outliers; we let them reach the keyframe generation # and then bundle adjustment will possible decide if remove them or not; # only after keyframe generation the outliers are cleaned! print(' # num_matched_map_points: %d' % (self.num_matched_map_points) ) if not self.pose_is_ok or self.num_matched_map_points < kNumMinInliersPoseOptimizationTrackLocalMap: Printer.red('failure in tracking local map, # matched map points: ', self.num_matched_map_points) self.pose_is_ok = False #if kUseDynamicDesDistanceTh: # self.reproj_err_frame_map_sigma = self.dyn_config.update_reproj_err_map_stat(reproj_err_frame_map_sigma) # store frame history in order to retrieve the complete camera trajectory def update_tracking_history(self): if self.state==SlamState.OK: isometry3d_Tcr = self.f_cur.isometry3d * self.f_cur.kf_ref.isometry3d.inverse() # pose of current frame w.r.t. current reference keyframe kf_ref self.tracking_history.relative_frame_poses.append(isometry3d_Tcr) self.tracking_history.kf_references.append(self.kf_ref) self.tracking_history.timestamps.append(self.f_cur.timestamp) else: self.tracking_history.relative_frame_poses.append(self.tracking_history.relative_frame_poses[-1]) self.tracking_history.kf_references.append(self.tracking_history.kf_references[-1]) self.tracking_history.timestamps.append(self.tracking_history.timestamps[-1]) self.tracking_history.slam_states.append(self.state) def need_new_keyframe(self, f_cur): num_keyframes = self.map.num_keyframes() nMinObs = kNumMinObsForKeyFrameDefault if num_keyframes <= 2: nMinObs = 2 # if just two keyframes then we can have just two observations num_kf_ref_tracked_points = self.kf_ref.num_tracked_points(nMinObs) # number of tracked points in k_ref num_f_cur_tracked_points = f_cur.num_matched_inlier_map_points() # number of inliers in f_cur Printer.purple('F(%d) #points: %d, KF(%d) #points: %d ' %(f_cur.id, num_f_cur_tracked_points, self.kf_ref.id, num_kf_ref_tracked_points)) if kLogKFinfoToFile: self.kf_info_logger.info('F(%d) #points: %d, KF(%d) #points: %d ' %(f_cur.id, num_f_cur_tracked_points, self.kf_ref.id, num_kf_ref_tracked_points)) self.num_kf_ref_tracked_points = num_kf_ref_tracked_points is_local_mapping_idle = self.local_mapping.is_idle() local_mapping_queue_size = self.local_mapping.queue_size() print('is_local_mapping_idle: ', is_local_mapping_idle,', local_mapping_queue_size: ', local_mapping_queue_size) # condition 1: more than "max_frames_between_kfs" have passed from last keyframe insertion cond1 = f_cur.id >= (self.kf_last.id + self.max_frames_between_kfs) # condition 2: more than "min_frames_between_kfs" have passed and local mapping is idle cond2 = (f_cur.id >= (self.kf_last.id + self.min_frames_between_kfs)) & is_local_mapping_idle #cond2 = (f_cur.id >= (self.kf_last.id + self.min_frames_between_kfs)) # condition 3: few tracked features compared to reference keyframe cond3 = (num_f_cur_tracked_points < num_kf_ref_tracked_points * Parameters.kThNewKfRefRatio) and (num_f_cur_tracked_points > Parameters.kNumMinPointsForNewKf) #print('KF conditions: %d %d %d' % (cond1, cond2, cond3) ) ret = (cond1 or cond2 ) and cond3 if ret: if is_local_mapping_idle: return True else: self.local_mapping.interrupt_optimization() if True: if local_mapping_queue_size <= 3: return True else: return False else: return False else: return False # @ main track method @ def track(self, img, frame_id, timestamp=None): Printer.cyan('@tracking') time_start = time.time() # check image size is coherent with camera params print("img.shape: ", img.shape) print("camera ", self.camera.height," x ", self.camera.width) assert img.shape[0:2] == (self.camera.height, self.camera.width) if timestamp is not None: print('timestamp: ', timestamp) self.timer_main_track.start() # build current frame self.timer_frame.start() f_cur = Frame(img, self.camera, timestamp=timestamp) self.f_cur = f_cur print("frame: ", f_cur.id) self.timer_frame.refresh() # reset indexes of matches self.idxs_ref = [] self.idxs_cur = [] if self.state == SlamState.NO_IMAGES_YET: # push first frame in the inizializer self.intializer.init(f_cur) self.state = SlamState.NOT_INITIALIZED return # EXIT (jump to second frame) if self.state == SlamState.NOT_INITIALIZED: # try to inizialize initializer_output, intializer_is_ok = self.intializer.initialize(f_cur, img) if intializer_is_ok: kf_ref = initializer_output.kf_ref kf_cur = initializer_output.kf_cur # add the two initialized frames in the map self.map.add_frame(kf_ref) # add first frame in map and update its frame id self.map.add_frame(kf_cur) # add second frame in map and update its frame id # add the two initialized frames as keyframes in the map self.map.add_keyframe(kf_ref) # add first keyframe in map and update its kid self.map.add_keyframe(kf_cur) # add second keyframe in map and update its kid kf_ref.init_observations() kf_cur.init_observations() # add points in map new_pts_count,_,_ = self.map.add_points(initializer_output.pts, None, kf_cur, kf_ref, initializer_output.idxs_cur, initializer_output.idxs_ref, img, do_check=False) Printer.green("map: initialized %d new points" % (new_pts_count)) # update covisibility graph connections kf_ref.update_connections() kf_cur.update_connections() # update tracking info self.f_cur = kf_cur self.f_cur.kf_ref = kf_ref self.kf_ref = kf_cur # set reference keyframe self.kf_last = kf_cur # set last added keyframe self.map.local_map.update(self.kf_ref) self.state = SlamState.OK self.update_tracking_history() self.motion_model.update_pose(kf_cur.timestamp,kf_cur.position,kf_cur.quaternion) self.motion_model.is_ok = False # after initialization you cannot use motion model for next frame pose prediction (time ids of initialized poses may not be consecutive) self.intializer.reset() if kUseDynamicDesDistanceTh: self.descriptor_distance_sigma = self.dyn_config.update_descriptor_stat(kf_ref, kf_cur, initializer_output.idxs_ref, initializer_output.idxs_cur) return # EXIT (jump to next frame) # get previous frame in map as reference f_ref = self.map.get_frame(-1) #f_ref_2 = self.map.get_frame(-2) self.f_ref = f_ref # add current frame f_cur to map self.map.add_frame(f_cur) self.f_cur.kf_ref = self.kf_ref # reset pose state flag self.pose_is_ok = False with self.map.update_lock: # check for map point replacements in previous frame f_ref (some points might have been replaced by local mapping during point fusion) self.f_ref.check_replaced_map_points() if kUseDynamicDesDistanceTh: print('descriptor_distance_sigma: ', self.descriptor_distance_sigma) self.local_mapping.descriptor_distance_sigma = self.descriptor_distance_sigma # udpdate (velocity) old motion model # c1=ref_ref, c2=ref, c3=cur; c=cur, r=ref #self.velocity = np.dot(f_ref.pose, inv_T(f_ref_2.pose)) # Tc2c1 = Tc2w * Twc1 (predicted Tcr) #self.predicted_pose = g2o.Isometry3d(np.dot(self.velocity, f_ref.pose)) # Tc3w = Tc2c1 * Tc2w (predicted Tcw) # set intial guess for current pose optimization if kUseMotionModel and self.motion_model.is_ok: print('using motion model for next pose prediction') # update f_ref pose according to its reference keyframe (the pose of the reference keyframe could be updated by local mapping) self.f_ref.update_pose(self.tracking_history.relative_frame_poses[-1] * self.f_ref.kf_ref.isometry3d) # predict pose by using motion model self.predicted_pose,_ = self.motion_model.predict_pose(timestamp, self.f_ref.position, self.f_ref.orientation) f_cur.update_pose(self.predicted_pose) else: print('setting f_cur.pose <-- f_ref.pose') # use reference frame pose as initial guess f_cur.update_pose(f_ref.pose) # track camera motion from f_ref to f_cur self.track_previous_frame(f_ref, f_cur) if not self.pose_is_ok: # if previous track didn't go well then track the camera motion from kf_ref to f_cur self.track_keyframe(self.kf_ref, f_cur) # now, having a better estimate of f_cur pose, we can find more map point matches: # find matches between {local map points} (points in the local map) and {unmatched keypoints of f_cur} if self.pose_is_ok: self.track_local_map(f_cur) # end block {with self.map.update_lock:} # TODO: add relocalization # HACK: since local mapping is not fast enough in python (and tracking is not in real-time) => give local mapping more time to process stuff self.wait_for_local_mapping() # N.B.: this must be outside the `with self.map.update_lock:` block with self.map.update_lock: # update slam state if self.pose_is_ok: self.state=SlamState.OK else: self.state=SlamState.LOST Printer.red('tracking failure') # update motion model state self.motion_model.is_ok = self.pose_is_ok if self.pose_is_ok: # if tracking was successful # update motion model self.motion_model.update_pose(timestamp, f_cur.position, f_cur.quaternion) f_cur.clean_vo_map_points() # do we need a new KeyFrame? need_new_kf = self.need_new_keyframe(f_cur) if need_new_kf: Printer.green('adding new KF with frame id % d: ' %(f_cur.id)) if kLogKFinfoToFile: self.kf_info_logger.info('adding new KF with frame id % d: ' %(f_cur.id)) kf_new = KeyFrame(f_cur, img) self.kf_last = kf_new self.kf_ref = kf_new f_cur.kf_ref = kf_new self.local_mapping.push_keyframe(kf_new) if not kLocalMappingOnSeparateThread: self.local_mapping.do_local_mapping() else: Printer.yellow('NOT KF') # From ORBSLAM2: # Clean outliers once keyframe generation has been managed: # we allow points with high innovation (considered outliers by the Huber Function) # pass to the new keyframe, so that bundle adjustment will finally decide # if they are outliers or not. We don't want next frame to estimate its position # with those points so we discard them in the frame. f_cur.clean_outlier_map_points() if self.f_cur.kf_ref is None: self.f_cur.kf_ref = self.kf_ref self.update_tracking_history() # must stay after having updated slam state (self.state) Printer.green("map: %d points, %d keyframes" % (self.map.num_points(), self.map.num_keyframes())) #self.update_history() self.timer_main_track.refresh() duration = time.time() - time_start print('tracking duration: ', duration) # Since we do not have real-time performances, we can slow-down things and make tracking wait till local mapping gets idle # N.B.: this function must be called outside 'with self.map.update_lock' blocks, # since both self.track() and the local-mapping optimization use the RLock 'map.update_lock' # => they cannot wait for each other once map.update_lock is locked (deadlock) def wait_for_local_mapping(self): if kTrackingWaitForLocalMappingToGetIdle: #while not self.local_mapping.is_idle() or self.local_mapping.queue_size()>0: if not self.local_mapping.is_idle(): print('>>>> waiting for local mapping...') self.local_mapping.wait_idle() else: if not self.local_mapping.is_idle() and kTrackingWaitForLocalMappingSleepTime>0: print('>>>> sleeping for local mapping...') time.sleep(kTrackingWaitForLocalMappingSleepTime) # check again for debug #is_local_mapping_idle = self.local_mapping.is_idle() #local_mapping_queue_size = self.local_mapping.queue_size() #print('is_local_mapping_idle: ', is_local_mapping_idle,', local_mapping_queue_size: ', local_mapping_queue_size) # def update_history(self): # f_cur = self.map.get_frame(-1) # self.cur_R = f_cur.pose[:3,:3].T # self.cur_t = np.dot(-self.cur_R,f_cur.pose[:3,3]) # if (self.init_history is True) and (self.trueX is not None): # self.t0_est = np.array([self.cur_t[0], self.cur_t[1], self.cur_t[2]]) # starting translation # self.t0_gt = np.array([self.trueX, self.trueY, self.trueZ]) # starting translation # if (self.t0_est is not None) and (self.t0_gt is not None): # p = [self.cur_t[0]-self.t0_est[0], self.cur_t[1]-self.t0_est[1], self.cur_t[2]-self.t0_est[2]] # the estimated traj starts at 0 # self.traj3d_est.append(p) # self.traj3d_gt.append([self.trueX-self.t0_gt[0], self.trueY-self.t0_gt[1], self.trueZ-self.t0_gt[2]]) # self.poses.append(poseRt(self.cur_R, p)) # get current translation scale from ground-truth if this is set # def get_absolute_scale(self, frame_id): # if self.groundtruth is not None and kUseGroundTruthScale: # self.trueX, self.trueY, self.trueZ, scale = self.groundtruth.getPoseAndAbsoluteScale(frame_id) # return scale # else: # self.trueX = 0 # self.trueY = 0 # self.trueZ = 0 # return 1