Visual Servoing Platform version 3.7.0
Loading...
Searching...
No Matches
XFeatVisualOdometry.py
35import numpy as np
36from enum import Enum
37import torch
38
39from visp.core import Matrix
40from visp.core import CameraParameters, HomogeneousMatrix, PixelMeterConversion, ArrayInt2D
41from visp.core import Color, Display
42from visp.core import ImageGray, ImageRGBa
43from visp.rbt import RBFeatureTrackerInput
44from visp.rbt import RBVisualOdometry, RBVisualOdometryUtils, LevenbergMarquardtParameters
45
46from visp.python.rbt import TrackedDescriptorMap
47from visp.python.rbt.xfeat import XFeatTrackingBackend
48
49
50class XFeatVisualOdometry(RBVisualOdometry):
51 """
52 Visual odometry implementation based on XFeat keypoints.
53 The minimised error is either the reprojection or the 3D error (in meters)
54 Descriptors are associated to 3D points that are stored in a map.
55 This map is updated every iteration, with mismatched points being removed and novel keypoints being added.
56
57 Every iteration, keypoints in the current RGB frame that are matched with points in the map are used to
58 update the camera pose by minimizing the chosen error criterion.
59
60 This implementation requires an RGB-D camera to compute the actual 3D positions of newly detected keypoints.
61
62 The parameters are the following:
63 - "numPoints" Number of points to store in the map
64 - "reprojectionThreshold": Distance (in pixels) between observed keypoints and model keypoints above which a map point is considered an outlier and is removed.
65 - "minDistNewPoint": Minimum distance (in meters) that a new keypoints should have to all other keypoints to be added to the map. This can help ensure that there is no overlapping keypoints in a region.
66 - "maxDepthErrorVisible": Maximum depth error between 3D map point and observed depth value (from the camera) above which a point is considered as not visible (occluded)
67
68 - "gain": The optimizer gain
69 - "maxNumIters": Maximum number of optimizer iterations
70 - "muInit": Initial value of the Levenberg-Marquardt regularization parameter
71 - "muIterFactor": LM regularization scaling factor applied every iteration
72 - "minImprovementFactor": Improvement factor (expressed as a ratio of the previous to current error criterion values) below which optimization is stopped.
73
74
75 An example JSON representation of the available settings is:
76 {
77 "type": "xfeat",
78 "numPoints": 8192,
79 "reprojectionThreshold": 10.0,
80 "minDistNewPoint": 0.0,
81 "maxDepthErrorVisible": 0.02,
82
83 "maxDepthErrorCandidate": 0.0,
84
85 "gain": 0.5,
86 "maxNumIters": 10,
87 "muInit": 0.0,
88 "muIterFactor": 0.0,
89 "minImprovementFactor": 0.001
90 }
91 """
92
93 class DisplayType(Enum):
94 """ Feature display type for visual odometry.
95 Changes the amount of displayed information
96 """
97 SIMPLE = 'simple'
98 SIMPLE_MODEL_AND_PROJ = 'simpleModelAndProj'
99 ERROR = 'error'
100 WEIGHT_AND_ERROR = 'weightAndError'
101
102 def __init__(self, backend: XFeatTrackingBackend):
103 RBVisualOdometry.__init__(self)
104 self.backend = backend
105 self.environment_map = TrackedDescriptorMap(num_points=1024,
106 reprojection_threshold=10.0,
107 min_dist_new_point=0,
108 max_depth_error_visible=2e-2)
109
110 self.display_type = XFeatVisualOdometry.DisplayType.SIMPLE
111 self.use_3d = False
113 # XFeat params
115 self.iter = 0
116
117 # Odometry parameters
118 self.cTw = HomogeneousMatrix()
119 self.cnTc = None
120
121 # Initial optimization pass: Minimize reprojection error, wrt to cTw
122 self.optim_params = LevenbergMarquardtParameters()
123 self.optim_params.gain = 0.5
124 self.optim_params.maxNumIters = 10
125 self.optim_params.muInit = 0.0
126 self.optim_params.muIterFactor = 0.0
127 self.optim_params.minImprovementFactor = 1e-3
128
129
130 def compute_weights(self, obj_descriptors: torch.Tensor):
131 Id = torch.eye(obj_descriptors.size(), device=obj_descriptors.device)
132 cos = obj_descriptors @ obj_descriptors.t()
133 w = 1.0 - (torch.max(cos - Id, dim=-1))
134 return w
135
136 def load_settings(self, d):
137 """Update the VO parameters from a dictionary.
138
139 Args:
140 d (_type_): The dictionary containing the settings
141 """
142 self.environment_map.parse_settings(d)
143 self.use_3d = d.get('use3d', self.use_3d)
144 self.optim_params.gain = d.get('gain', self.optim_params.gain)
145 self.optim_params.maxNumIters = d.get('maxNumIters', self.optim_params.maxNumIters)
146 self.optim_params.muInit = d.get('muInit', self.optim_params.muInit)
147 self.optim_params.muIterFactor = d.get('muIterFactor',self.optim_params.muIterFactor)
148 self.optim_params.minImprovementFactor = d.get('minImprovementFactor', self.optim_params.minImprovementFactor)
149 self.display_type = d.get('displayType', self.display_type)
150
151
152 def compute(self, frame: RBFeatureTrackerInput, previousFrame: RBFeatureTrackerInput) -> None:
153
154 if self.current_representation is not None:
155 removed_indices = self.environment_map.update(frame, self.cTw, None, frame.depth, self.idx_curr_env_matched, self.idx_environment_map,
156 self.current_representation.keypoints, self.current_representation.descriptors)
157
158 self.backend.process_frame(frame, self.iter)
159 self.current_representation = self.backend.get_current_environment_data()
160
161 self.idx_curr_env_matched, self.idx_environment_map = None, None
162 with torch.no_grad():
163 if self.environment_map.has_points() and self.current_representation is not None:
164 h, w = frame.I.getRows(), frame.I.getCols()
165
166 visible_indices = np.ascontiguousarray(self.environment_map.point_map.getVisiblePoints(h, w, frame.cam, self.cTw, frame.depth))
167 # visible_indices = self.environment_map.get_visible_points(self.cTw, frame)
168 if len(visible_indices) > 0:
169 visible_env_descriptors = self.environment_map.descriptors[visible_indices]
170 self.idx_curr_env_matched, self.idx_environment_map = self.backend.match(self.current_representation.descriptors, visible_env_descriptors)
171 self.idx_curr_env_matched = self.idx_curr_env_matched.cpu().numpy()
172 self.idx_environment_map = visible_indices[self.idx_environment_map.cpu().numpy()]
173
174 self.numFeatures = 0
175 self.iter += 1
176
177 if self.idx_curr_env_matched is not None:
178
179 if self.use_3d:
180 valid_indices = []
181 self.Zs = []
182 for i, kp in enumerate(self.current_representation.keypoints[self.idx_curr_env_matched]):
183 Z = frame.depth[int(kp[1]), int(kp[0])]
184 if Z > 0.0:
185 valid_indices.append(i)
186 self.Zs.append(Z)
187 self.Zs = np.ascontiguousarray(self.Zs)
188 self.idx_curr_env_matched = self.idx_curr_env_matched[valid_indices]
189 self.idx_environment_map = self.idx_environment_map[valid_indices]
190
191 self.numFeatures = len(self.idx_curr_env_matched) * (2 if not self.use_3d else 3)
192
193 self.optimize(frame)
194
195 def optimize(self, frame: RBFeatureTrackerInput):
196 self.previous_cTw = HomogeneousMatrix(self.cTw) # Calling constructor is important, we want to keep the value, not the reference as current cTw will be modified!
197
198 if self.numFeatures == 0:
199 self.cnTc = HomogeneousMatrix()
200 return
201
202 if self.idx_curr_env_matched is None or len(self.idx_curr_env_matched) == 0:
203 self.cnTc = HomogeneousMatrix()
204 return
205
206 current_px_matched_env = self.current_representation.keypoints[self.idx_curr_env_matched]
207 xoe, yoe = PixelMeterConversion.convertPoints(frame.cam, current_px_matched_env[:, 0], current_px_matched_env[:, 1])
208 mapX = Matrix()
209 indices = ArrayInt2D.view(np.ascontiguousarray(self.idx_environment_map[:, None].astype(np.int32)))
210 self.environment_map.point_map.getPoints(indices, mapX)
211 c = HomogeneousMatrix(self.cTw)
212 if self.use_3d:
213 current_xyz_env = Matrix.view(np.stack((xoe * self.Zs, yoe * self.Zs, self.Zs), axis=-1))
214 RBVisualOdometryUtils.levenbergMarquardtKeypoints3D(mapX, current_xyz_env, self.optim_params, c)
215 else:
216 current_xy_env = Matrix.view(np.stack((xoe, yoe), axis=-1))
217 RBVisualOdometryUtils.levenbergMarquardtKeypoints2D(mapX, current_xy_env, self.optim_params, c)
218
219 self.cTw = c
220 self.cnTc = self.cTw * self.previous_cTw.inverse()
221
222 def reset(self):
224 self.iter = 0
225 self.current_representation = None
226 self.previous_cTw = HomogeneousMatrix()
227 self.cTw = HomogeneousMatrix()
228 self.cnTc = HomogeneousMatrix()
229 self.idx_curr_env_matched = None
230 self.idx_environment_map = None
231
232 def getCameraMotion(self) -> HomogeneousMatrix:
233 return self.cnTc
234
235 def getCameraPose(self) -> HomogeneousMatrix:
236 return self.cTw
237
238 def display(self, cam: CameraParameters, _I: ImageGray, IRGB: ImageRGBa, _I_depth: ImageGray):
239 """Display the features used for visual odometry.
240 XFeat keypoints are displayed as small disks in the RGB image.
241
242 Depending on the display type, the behavior is different:
243 - DisplayType.SIMPLE: Display the current image keypoints that were matched with the environment map
244 - DisplayType.SIMPLE_MODEL_AND_PROJ: Display current image keypoints along with the map points that they were matched with.
245 - DisplayType.WEIGHT_AND_ERROR: Same as the SIMPLE value, except that the color of the keypoints display the error
246 (on the red component) and their attributed weight in the optimization (in blue)
247
248 Args:
249 cam (CameraParameters): Camera intrinsics
250 _I (ImageGray): Grayscale image
251 IRGB (ImageRGBa): RGB image where to display the points
252 _I_depth (ImageGray): Depth image
253
254 Raises:
255 RuntimeError: if the display type is invalid
256 """
257 if self.idx_curr_env_matched is None or self.idx_environment_map is None:
258 return
259
260 ps = self.current_representation.keypoints[self.idx_curr_env_matched]
261 cX, xs, uvs = Matrix(), Matrix(), Matrix()
262 self.environment_map.point_map.project(cam, ArrayInt2D.view(np.ascontiguousarray(self.idx_environment_map[:, None]).astype(np.int32)), self.cTw, cX, xs, uvs)
263 uvs = uvs.numpy().copy()
264 error = np.linalg.norm(uvs - ps, axis=1)
265 threshold = self.environment_map.point_map.getOutlierReprojectionErrorThreshold()
266 if len(error) == 0:
267 return
268 max_error_display = np.maximum(np.max(error), self.environment_map.point_map.getOutlierReprojectionErrorThreshold())
269 ps = np.rint(ps).astype(np.int32)
270 uvs = np.rint(uvs).astype(np.int32)
271
272 if self.display_type == XFeatVisualOdometry.DisplayType.SIMPLE:
273 Display.displayCrosses(IRGB, ps[:, 1], ps[:, 0], 8, Color.blue, 1)
274
275 elif self.display_type == XFeatVisualOdometry.DisplayType.SIMPLE_MODEL_AND_PROJ:
276 Display.displayCrosses(IRGB, ps[:, 1], ps[:, 0], 8, Color.blue, 1)
277 Display.displayCrosses(IRGB, uvs[:, 1], np.rint(uvs[:, 0]).astype(np.int32), 8, Color.red, 1)
278
279 elif self.display_type == XFeatVisualOdometry.DisplayType.WEIGHT_AND_ERROR:
280 c = Color()
281 for p in range(len(error)):
282 e = np.minimum(error[p], threshold) / threshold
283 c.setColor(
284 np.rint((e) * 255).astype(np.uint8),
285 0,
286 np.rint((1.0 - e) * 255).astype(np.uint8),
287 255
288 )
289
290 Display.displayCircleStatic(IRGB, ps[p, 1], ps[p, 0], 2, c, True, 1)
291 else:
292 raise RuntimeError('Display type not implemented')
None compute(self, RBFeatureTrackerInput frame, RBFeatureTrackerInput previousFrame)
display(self, CameraParameters cam, ImageGray _I, ImageRGBa IRGB, ImageGray _I_depth)