Swarm-SLAM  1.0.0
C-SLAM Framework
global_descriptor_loop_closure_detection.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 from cslam.algebraic_connectivity_maximization import EdgeInterRobot
3 import numpy as np
4 
5 import os
6 from os.path import join, exists, isfile, realpath, dirname
7 
8 from cslam.loop_closure_sparse_matching import LoopClosureSparseMatching
9 from cslam.broker import Broker
10 
11 from cslam_common_interfaces.msg import KeyframeRGB, KeyframePointCloud
12 from cslam_common_interfaces.msg import (
13  GlobalDescriptor, GlobalDescriptors, InterRobotLoopClosure,
14  LocalDescriptorsRequest, LocalKeyframeMatch, InterRobotMatch,
15  InterRobotMatches)
16 from diagnostic_msgs.msg import KeyValue
17 import time
18 from sortedcontainers import SortedDict
19 
20 import rclpy
21 from rclpy.node import Node
22 from rclpy.clock import Clock
23 
24 from cslam.neighbors_manager import NeighborManager
25 from cslam.utils.misc import dict_to_list_chunks
26 
28  """ Global descriptor matching """
29 
30  def __init__(self, params, node):
31  """Initialization
32 
33  Args:
34  params (dict): parameters
35  node (ROS 2 node handle): node handle
36  """
37  self.paramsparams = params
38  self.nodenode = node
39  self.lcmlcm = LoopClosureSparseMatching(params)
40 
41  # Place Recognition network setup
42  if self.paramsparams['frontend.global_descriptor_technique'].lower(
43  ) == 'netvlad':
44  from cslam.vpr.netvlad import NetVLAD
45  self.nodenode.get_logger().info('Using NetVLAD.')
46  self.global_descriptorglobal_descriptor = NetVLAD(self.paramsparams, self.nodenode)
47  self.keyframe_typekeyframe_type = "rgb"
48  elif self.paramsparams['frontend.global_descriptor_technique'].lower(
49  ) == 'scancontext':
50  from cslam.lidar_pr.scancontext import ScanContext
51  global icp_utils
52  import cslam.lidar_pr.icp_utils as icp_utils
53  self.nodenode.get_logger().info('Using ScanContext.')
54  self.global_descriptorglobal_descriptor = ScanContext(self.paramsparams, self.nodenode)
55  self.keyframe_typekeyframe_type = "pointcloud"
56  else:
57  from cslam.vpr.cosplace import CosPlace
58  self.nodenode.get_logger().info('Using CosPlace. (default)')
59  self.global_descriptorglobal_descriptor = CosPlace(self.paramsparams, self.nodenode)
60  self.keyframe_typekeyframe_type = "rgb"
61 
62  # ROS 2 objects setup
63  self.paramsparams[
64  'frontend.global_descriptors_topic'] = '/cslam/' + self.nodenode.get_parameter(
65  'frontend.global_descriptors_topic').value
66  self.global_descriptor_publisherglobal_descriptor_publisher = self.nodenode.create_publisher(
67  GlobalDescriptors,
68  self.paramsparams['frontend.global_descriptors_topic'], 100)
69  self.global_descriptor_subscriberglobal_descriptor_subscriber = self.nodenode.create_subscription(
70  GlobalDescriptors,
71  self.paramsparams['frontend.global_descriptors_topic'],
72  self.global_descriptor_callbackglobal_descriptor_callback, 100)
73 
74  self.paramsparams[
75  'frontend.inter_robot_matches_topic'] = '/cslam/' + self.nodenode.get_parameter(
76  'frontend.inter_robot_matches_topic').value
77  self.inter_robot_matches_publisherinter_robot_matches_publisher = self.nodenode.create_publisher(
78  InterRobotMatches,
79  self.paramsparams['frontend.inter_robot_matches_topic'], 100)
80  self.inter_robot_matches_subscriberinter_robot_matches_subscriber = self.nodenode.create_subscription(
81  InterRobotMatches,
82  self.paramsparams['frontend.inter_robot_matches_topic'],
83  self.inter_robot_matches_callbackinter_robot_matches_callback, 100)
84 
85  if self.keyframe_typekeyframe_type == "rgb":
86  self.receive_keyframe_subscriberreceive_keyframe_subscriber = self.nodenode.create_subscription(
87  KeyframeRGB, 'cslam/keyframe_data', self.receive_keyframereceive_keyframe, 100)
88  elif self.keyframe_typekeyframe_type == "pointcloud":
89  self.receive_keyframe_subscriberreceive_keyframe_subscriber = self.nodenode.create_subscription(
90  KeyframePointCloud, 'cslam/keyframe_data', self.receive_keyframereceive_keyframe,
91  100)
92  else:
93  self.nodenode.get_logger().error("Unknown keyframe type")
94 
95  self.local_match_publisherlocal_match_publisher = self.nodenode.create_publisher(
96  LocalKeyframeMatch, 'cslam/local_keyframe_match', 100)
97 
98  self.receive_inter_robot_loop_closure_subscriberreceive_inter_robot_loop_closure_subscriber = self.nodenode.create_subscription(
99  InterRobotLoopClosure, '/cslam/inter_robot_loop_closure',
100  self.receive_inter_robot_loop_closurereceive_inter_robot_loop_closure, 100)
101 
102  self.local_descriptors_request_publisherslocal_descriptors_request_publishers = {}
103  for i in range(self.paramsparams['max_nb_robots']):
104  self.local_descriptors_request_publisherslocal_descriptors_request_publishers[
105  i] = self.nodenode.create_publisher(
106  LocalDescriptorsRequest,
107  '/r' + str(i) + '/cslam/local_descriptors_request', 100)
108 
109  # Listen for changes in node liveliness
110  self.neighbor_managerneighbor_manager = NeighborManager(
111  self.nodenode, self.paramsparams)
112 
113  self.global_descriptors_bufferglobal_descriptors_buffer = SortedDict()
114  self.global_descriptors_timerglobal_descriptors_timer = self.nodenode.create_timer(
115  self.paramsparams['frontend.detection_publication_period_sec'],
116  self.global_descriptors_timer_callbackglobal_descriptors_timer_callback,
117  clock=Clock()
118  ) # Note: It is important to use the system clock instead of ROS clock for timers since we are within a TimerAction
119 
120  self.inter_robot_matches_bufferinter_robot_matches_buffer = SortedDict()
121  self.nb_inter_robot_matchesnb_inter_robot_matches = 0
122  self.inter_robot_matches_timerinter_robot_matches_timer = self.nodenode.create_timer(
123  self.paramsparams['frontend.detection_publication_period_sec'],
124  self.inter_robot_matches_timer_callbackinter_robot_matches_timer_callback,
125  clock=Clock()
126  ) # Note: It is important to use the system clock instead of ROS clock for timers since we are within a TimerAction
127 
128  if self.paramsparams["evaluation.enable_logs"]:
129  self.log_publisherlog_publisher = self.nodenode.create_publisher(
130  KeyValue, 'cslam/log_info', 100)
131  self.log_matches_publisherlog_matches_publisher = self.nodenode.create_publisher(
132  InterRobotMatches, 'cslam/log_matches', 100)
133  self.log_total_successful_matcheslog_total_successful_matches = 0
134  self.log_total_failed_matcheslog_total_failed_matches = 0
135  self.log_total_vertices_transmittedlog_total_vertices_transmitted = 0
136  self.log_total_matches_selectedlog_total_matches_selected = 0
137  self.log_detection_cumulative_communicationlog_detection_cumulative_communication = 0
138  self.log_total_sparsification_computation_timelog_total_sparsification_computation_time = 0.0
139 
140  # Import OpenCV at the end to avoid issue on NVIDIA Jetson Xavier: https://github.com/opencv/opencv/issues/14884#issuecomment-599852128
141  global cv2
142  import cv2
143  global CvBridge
144  from cv_bridge import CvBridge
145 
146  self.gpu_start_timegpu_start_time = time.time()
147 
148  def add_global_descriptor_to_map(self, embedding, kf_id):
149  """ Add global descriptor to matching list
150 
151  Args:
152  embedding (np.array): descriptor
153  kf_id (int): keyframe ID
154  """
155  # Add for matching
156  matches = self.lcmlcm.add_local_global_descriptor(embedding, kf_id)
157  # Local matching
158  self.detect_intradetect_intra(embedding, kf_id)
159 
160  # Store global descriptor
161  msg = GlobalDescriptor()
162  msg.keyframe_id = kf_id
163  msg.robot_id = self.paramsparams['robot_id']
164  msg.descriptor = embedding.tolist()
165  self.global_descriptors_bufferglobal_descriptors_buffer[kf_id] = msg
166 
167  # Store matches
168  for match in matches:
169  self.inter_robot_matches_bufferinter_robot_matches_buffer[
170  self.nb_inter_robot_matchesnb_inter_robot_matches] = match
171  self.nb_inter_robot_matchesnb_inter_robot_matches += 1
172 
174  """Deletes global descriptors
175  because all other robots have already received them.
176  """
177  from_kf_id = self.neighbor_managerneighbor_manager.useless_descriptors(
178  self.global_descriptors_bufferglobal_descriptors_buffer.peekitem(-1)[0])
179  if from_kf_id >= self.global_descriptors_bufferglobal_descriptors_buffer.peekitem(0)[0]:
180  for k in self.global_descriptors_bufferglobal_descriptors_buffer.keys():
181  if k < from_kf_id:
182  del self.global_descriptors_bufferglobal_descriptors_buffer[k]
183 
185  """Deletes inter_robot_matches
186  because all other robots have already received them.
187  """
188  from_match_id = self.neighbor_managerneighbor_manager.useless_matches(
189  self.inter_robot_matches_bufferinter_robot_matches_buffer.peekitem(-1)[0])
190  if from_match_id >= self.inter_robot_matches_bufferinter_robot_matches_buffer.peekitem(0)[0]:
191  for k in self.inter_robot_matches_bufferinter_robot_matches_buffer.keys():
192  if k < from_match_id:
193  del self.inter_robot_matches_bufferinter_robot_matches_buffer[k]
194 
196  """Publish global descriptors message periodically
197  Doesn't publish if the descriptors are already known by neighboring robots
198  """
199  if len(self.global_descriptors_bufferglobal_descriptors_buffer) > 0:
200  from_kf_id = self.neighbor_managerneighbor_manager.select_from_which_kf_to_send(
201  self.global_descriptors_bufferglobal_descriptors_buffer.peekitem(-1)[0])
202 
203  msgs = dict_to_list_chunks(
204  self.global_descriptors_bufferglobal_descriptors_buffer,
205  from_kf_id - self.global_descriptors_bufferglobal_descriptors_buffer.peekitem(0)[0],
206  self.paramsparams['frontend.detection_publication_max_elems_per_msg']
207  )
208 
209  for m in msgs:
210  global_descriptors = GlobalDescriptors()
211  global_descriptors.descriptors = m
212  self.global_descriptor_publisherglobal_descriptor_publisher.publish(global_descriptors)
213  if self.paramsparams["evaluation.enable_logs"]:
214  self.log_detection_cumulative_communicationlog_detection_cumulative_communication += len(
215  global_descriptors.descriptors) * len(
216  global_descriptors.descriptors[0].descriptor
217  ) * 4 # bytes
218 
219  self.delete_useless_descriptorsdelete_useless_descriptors()
220  if self.paramsparams["evaluation.enable_logs"]:
221  self.log_publisherlog_publisher.publish(
222  KeyValue(key="detection_cumulative_communication",
223  value=str(
224  self.log_detection_cumulative_communicationlog_detection_cumulative_communication)))
225 
226  def edge_to_match(self, edge):
227  """Converts an InterRobotEdge to a InterRobotMatch message
228  Args: edge (InterRobotEdge)
229  """
230  msg = InterRobotMatch()
231  msg.robot0_id = edge.robot0_id
232  msg.robot0_keyframe_id = edge.robot0_keyframe_id
233  msg.robot1_id = edge.robot1_id
234  msg.robot1_keyframe_id = edge.robot1_keyframe_id
235  msg.weight = edge.weight
236  return msg
237 
239  """Publish inter-robot matches message periodically
240  Doesn't publish if the inter-robot matches are already known by neighboring robots
241  """
242  if len(self.inter_robot_matches_bufferinter_robot_matches_buffer) > 0:
243  from_match_idx = self.neighbor_managerneighbor_manager.select_from_which_match_to_send(
244  self.inter_robot_matches_bufferinter_robot_matches_buffer.peekitem(-1)[0])
245 
246  chuncks = dict_to_list_chunks(
247  self.inter_robot_matches_bufferinter_robot_matches_buffer, from_match_idx -
248  self.inter_robot_matches_bufferinter_robot_matches_buffer.peekitem(0)[0], self.
249  params['frontend.detection_publication_max_elems_per_msg'])
250 
251  # Don't transmit matches that should have already been detected by the other robot
252  _, neighbors_in_range_list = self.neighbor_managerneighbor_manager.check_neighbors_in_range()
253  if len(neighbors_in_range_list) == 2:
254  self.nodenode.get_logger().info("Transmitting matches {}".format(neighbors_in_range_list))
255  for c in chuncks:
256  for match in c:
257  if match.robot0_id in neighbors_in_range_list and match.robot1_id in neighbors_in_range_list:
258  c.remove(match)
259  if len(c) <= 0:
260  chuncks.remove(c)
261 
262  # Convert to ROS message
263  msgs = []
264  for c in chuncks:
265  m = []
266  for match in c:
267  msg = self.edge_to_matchedge_to_match(match)
268  m.append(msg)
269  msgs.append(m)
270 
271  # Transmit the rest
272  for m in msgs:
273  inter_robot_matches = InterRobotMatches()
274  inter_robot_matches.robot_id = self.paramsparams['robot_id']
275  inter_robot_matches.matches = m
276  self.inter_robot_matches_publisherinter_robot_matches_publisher.publish(inter_robot_matches)
277  if self.paramsparams["evaluation.enable_logs"]:
278  self.log_detection_cumulative_communicationlog_detection_cumulative_communication += len(
279  inter_robot_matches.matches) * 20 # bytes
280 
281  self.delete_useless_inter_robot_matchesdelete_useless_inter_robot_matches()
282  if self.paramsparams["evaluation.enable_logs"]:
283  self.log_publisherlog_publisher.publish(
284  KeyValue(key="detection_cumulative_communication",
285  value=str(
286  self.log_detection_cumulative_communicationlog_detection_cumulative_communication)))
287 
288  def detect_intra(self, embedding, kf_id):
289  """ Detect intra-robot loop closures
290 
291  Args:
292  embedding (np.array): descriptor
293  kf_id (int): keyframe ID
294 
295  Returns:
296  list(int): matched keyframes
297  """
298  if self.paramsparams['frontend.enable_intra_robot_loop_closures']:
299  kf_match, _ = self.lcmlcm.match_local_loop_closures(embedding, kf_id)
300  if kf_match is not None:
301  msg = LocalKeyframeMatch()
302  msg.keyframe0_id = kf_id
303  msg.keyframe1_id = kf_match
304  self.local_match_publisherlocal_match_publisher.publish(msg)
305 
306  def detect_inter(self):
307  """ Detect inter-robot loop closures
308 
309  Returns:
310  list(int): selected keyframes from other robots to match
311  """
312  neighbors_is_in_range, neighbors_in_range_list = self.neighbor_managerneighbor_manager.check_neighbors_in_range(
313  )
314  #self.node.get_logger().info('Neighbors in range: ' + str(neighbors_in_range_list))
315  # Check if the robot is the broker
316  if len(neighbors_in_range_list
317  ) > 0 and self.neighbor_managerneighbor_manager.local_robot_is_broker():
318  if self.paramsparams["evaluation.enable_logs"]: start_time = time.time()
319  # Find matches that maximize the algebraic connectivity
320  selection = self.lcmlcm.select_candidates(
321  self.paramsparams["frontend.inter_robot_loop_closure_budget"],
322  neighbors_is_in_range)
323 
324  # Extract and publish local descriptors
325  vertices_info = self.edge_list_to_verticesedge_list_to_vertices(selection)
326  broker = Broker(selection, neighbors_in_range_list)
327  for selected_vertices_set in broker.brokerage(
328  self.paramsparams["frontend.use_vertex_cover_selection"]):
329  for v in selected_vertices_set:
330  # Call to send publish local descriptors
331  msg = LocalDescriptorsRequest()
332  msg.keyframe_id = v[1]
333  msg.matches_robot_id = vertices_info[v][0]
334  msg.matches_keyframe_id = vertices_info[v][1]
335  self.local_descriptors_request_publisherslocal_descriptors_request_publishers[v[0]].publish(
336  msg)
337  if self.paramsparams["evaluation.enable_logs"]:
338  self.log_total_vertices_transmittedlog_total_vertices_transmitted += len(
339  selected_vertices_set)
340  if self.paramsparams["evaluation.enable_logs"]:
341  stop_time = time.time()
342  self.log_total_sparsification_computation_timelog_total_sparsification_computation_time += stop_time - start_time
343  self.log_total_matches_selectedlog_total_matches_selected += len(selection)
344  self.log_publisherlog_publisher.publish(
345  KeyValue(
346  key="sparsification_cumulative_computation_time",
347  value=str(
348  self.log_total_sparsification_computation_timelog_total_sparsification_computation_time)))
349  self.log_publisherlog_publisher.publish(
350  KeyValue(key="nb_vertices_transmitted",
351  value=str(self.log_total_vertices_transmittedlog_total_vertices_transmitted)))
352  self.log_publisherlog_publisher.publish(
353  KeyValue(key="nb_matches_selected",
354  value=str(self.log_total_matches_selectedlog_total_matches_selected)))
355  if self.paramsparams["evaluation.enable_sparsification_comparison"]:
356  matches = InterRobotMatches()
357  matches.robot_id = self.paramsparams["robot_id"]
358  for e in self.lcmlcm.candidate_selector.log_mac_edges:
359  matches.matches.append(self.edge_to_matchedge_to_match(e))
360  self.log_matches_publisherlog_matches_publisher.publish(matches)
361 
362  def edge_list_to_vertices(self, selection):
363  """Extracts the vertices in a list of edges
364  Args:
365  selection list(EdgeInterRobot): selection of edges
366  Returns:
367  dict((int, int), list(int), list(int)): Vertices indices with their related vertices
368  """
369  vertices = {}
370  for s in selection:
371  key0 = (s.robot0_id, s.robot0_keyframe_id)
372  key1 = (s.robot1_id, s.robot1_keyframe_id)
373  if key0 in vertices:
374  vertices[key0][0].append(s.robot1_id)
375  vertices[key0][1].append(s.robot1_keyframe_id)
376  else:
377  vertices[key0] = [[s.robot1_id], [s.robot1_keyframe_id]]
378  if key1 in vertices:
379  vertices[key1][0].append(s.robot0_id)
380  vertices[key1][1].append(s.robot0_keyframe_id)
381  else:
382  vertices[key1] = [[s.robot0_id], [s.robot0_keyframe_id]]
383  return vertices
384 
385  def receive_keyframe(self, msg):
386  """Callback to add a keyframe
387 
388  Args:
389  msg (cslam_common_interfaces::msg::KeyframeRGB or KeyframePointCloud): Keyframe data
390  """
391  # Place recognition descriptor processing
392  embedding = []
393  if self.keyframe_typekeyframe_type == "rgb":
394  bridge = CvBridge()
395  cv_image = bridge.imgmsg_to_cv2(msg.image,
396  desired_encoding='passthrough')
397  embedding = self.global_descriptorglobal_descriptor.compute_embedding(cv_image)
398  elif self.keyframe_typekeyframe_type == "pointcloud":
399  embedding = self.global_descriptorglobal_descriptor.compute_embedding(
400  icp_utils.ros_pointcloud_to_points(msg.pointcloud))
401 
402  self.add_global_descriptor_to_mapadd_global_descriptor_to_map(embedding, msg.id)
403 
405  """Callback for descriptors received from other robots.
406 
407  Args:
408  msg (cslam_common_interfaces::msg::GlobalDescriptors): descriptors
409  """
410  if msg.descriptors[0].robot_id != self.paramsparams['robot_id']:
411  unknown_range = self.neighbor_managerneighbor_manager.get_unknown_range(
412  msg.descriptors)
413  for i in unknown_range:
414  match = self.lcmlcm.add_other_robot_global_descriptor(
415  msg.descriptors[i])
416  if match is not None:
417  self.inter_robot_matches_bufferinter_robot_matches_buffer[
418  self.nb_inter_robot_matchesnb_inter_robot_matches] = match
419  self.nb_inter_robot_matchesnb_inter_robot_matches += 1
420 
422  """Callback for inter-robot matches received from other robots.
423 
424  Args:
425  msg (cslam_common_interfaces::msg::InterRobotMatches): matches
426  """
427  if msg.robot_id != self.paramsparams['robot_id']:
428  for match in msg.matches:
429  edge = EdgeInterRobot(match.robot0_id, match.robot0_keyframe_id, match.robot1_id, match.robot1_keyframe_id, match.weight)
430  self.lcmlcm.candidate_selector.add_match(edge)
431 
433  """ Convert a inter-robot loop closure to an edge
434  for algebraic connectivity maximization
435 
436  Args:
437  msg (cslam_common_interfaces::msg::InterRobotLoopClosure): Inter-robot loop closure
438 
439  Returns:
440  EdgeInterRobot: inter-robot edge
441  """
442  return EdgeInterRobot(msg.robot0_id, msg.robot0_keyframe_id,
443  msg.robot1_id, msg.robot1_keyframe_id,
444  self.lcmlcm.candidate_selector.fixed_weight)
445 
447  """Receive computed inter-robot loop closure
448 
449  Args:
450  msg (cslam_common_interfaces::msg::InterRobotLoopClosure): Inter-robot loop closure
451  """
452  if msg.success:
453  self.nodenode.get_logger().info(
454  'New inter-robot loop closure measurement: (' +
455  str(msg.robot0_id) + ',' + str(msg.robot0_keyframe_id) +
456  ') -> (' + str(msg.robot1_id) + ',' +
457  str(msg.robot1_keyframe_id) + ')')
458  # If geo verif succeeds, move from candidate to fixed edge in the graph
459  self.lcmlcm.candidate_selector.candidate_edges_to_fixed(
460  [self.inter_robot_loop_closure_msg_to_edgeinter_robot_loop_closure_msg_to_edge(msg)])
461 
462  if self.paramsparams["evaluation.enable_logs"]:
463  self.log_total_successful_matcheslog_total_successful_matches += 1
464  self.log_publisherlog_publisher.publish(
465  KeyValue(key="nb_matches",
466  value=str(self.log_total_successful_matcheslog_total_successful_matches)))
467  else:
468  # If geo verif fails, remove candidate
469  self.nodenode.get_logger().info(
470  'Failed inter-robot loop closure measurement: (' +
471  str(msg.robot0_id) + ',' + str(msg.robot0_keyframe_id) +
472  ') -> (' + str(msg.robot1_id) + ',' +
473  str(msg.robot1_keyframe_id) + ')')
474  self.lcmlcm.candidate_selector.remove_candidate_edges(
475  [self.inter_robot_loop_closure_msg_to_edgeinter_robot_loop_closure_msg_to_edge(msg)], failed=True)
476 
477  if self.paramsparams["evaluation.enable_logs"]:
478  self.log_total_failed_matcheslog_total_failed_matches += 1
479  self.log_publisherlog_publisher.publish(
480  KeyValue(key="nb_failed_matches",
481  value=str(self.log_total_failed_matcheslog_total_failed_matches)))
def ros_pointcloud_to_points(pc_msg)
Definition: icp_utils.py:166
def dict_to_list_chunks(d, start, chunk_size)
Definition: misc.py:21