Skip to content

Commit 946aae6

Browse files
✨ Add the possibility to draw 3d arrows (#86)
1 parent 1ecfc04 commit 946aae6

5 files changed

Lines changed: 378 additions & 114 deletions

File tree

robot_log_visualizer/file_reader/signal_provider.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ def __init__(self, period: float):
5151
self._3d_trajectories_path = {}
5252
self._3d_trajectories_path_lock = QMutex()
5353

54+
self._3d_arrows = {}
55+
self._3d_arrows_path_lock = QMutex()
56+
57+
self._max_arrow = 0
58+
self._custom_max_arrow = 0
59+
self._is_custom_max_arrow_used = False
60+
self._max_arrow_mutex = QMutex()
61+
5462
self.period = period
5563

5664
self.data = {}
@@ -90,14 +98,16 @@ def __populate_text_logging_data(self, file_object):
9098
# If len(value[text[0]].shape) == 2 then the text contains a string, otherwise it is empty
9199
# We need to manually check the shape to handle the case in which the text is empty
92100
data[key]["data"] = [
93-
TextLoggingMsg(
94-
text="".join(chr(c[0]) for c in value[text[0]]),
95-
level="".join(chr(c[0]) for c in value[level[0]]),
96-
)
97-
if len(value[text[0]].shape) == 2
98-
else TextLoggingMsg(
99-
text="",
100-
level="".join(chr(c[0]) for c in value[level[0]]),
101+
(
102+
TextLoggingMsg(
103+
text="".join(chr(c[0]) for c in value[text[0]]),
104+
level="".join(chr(c[0]) for c in value[level[0]]),
105+
)
106+
if len(value[text[0]].shape) == 2
107+
else TextLoggingMsg(
108+
text="",
109+
level="".join(chr(c[0]) for c in value[level[0]]),
110+
)
101111
)
102112
for text, level in zip(text_ref, level_ref)
103113
]
@@ -214,6 +224,14 @@ def robot_state_path(self):
214224
value = self._robot_state_path
215225
return value
216226

227+
@property
228+
def max_arrow(self):
229+
locker = QMutexLocker(self._max_arrow_mutex)
230+
if self._is_custom_max_arrow_used:
231+
return self._custom_max_arrow
232+
else:
233+
return self._max_arrow
234+
217235
@robot_state_path.setter
218236
def robot_state_path(self, robot_state_path):
219237
locker = QMutexLocker(self.robot_state_path_lock)
@@ -308,6 +326,14 @@ def get_robot_state_at_index(self, index):
308326

309327
return robot_state
310328

329+
def set_custom_max_arrow(self, use_custom_max_arrow: bool, max_arrow: float):
330+
_ = QMutexLocker(self._max_arrow_mutex)
331+
self._is_custom_max_arrow_used = use_custom_max_arrow
332+
if use_custom_max_arrow:
333+
self._custom_max_arrow = max_arrow
334+
else:
335+
self._custom_max_arrow = 0
336+
311337
def get_3d_point_at_index(self, index):
312338
points = {}
313339

@@ -325,6 +351,18 @@ def get_3d_point_at_index(self, index):
325351

326352
return points
327353

354+
def get_3d_arrow_at_index(self, index):
355+
arrows = {}
356+
357+
self._3d_arrows_path_lock.lock()
358+
359+
for key, value in self._3d_arrows.items():
360+
arrows[key] = self.get_item_from_path_at_index(value, index)
361+
362+
self._3d_arrows_path_lock.unlock()
363+
364+
return arrows
365+
328366
def get_3d_trajectory_at_index(self, index):
329367
trajectories = {}
330368

@@ -360,6 +398,24 @@ def unregister_3d_point(self, key):
360398
del self._3d_points_path[key]
361399
self._3d_points_path_lock.unlock()
362400

401+
def register_3d_arrow(self, key, arrow_path):
402+
self._3d_arrows_path_lock.lock()
403+
self._3d_arrows[key] = arrow_path
404+
for _, value in self._3d_arrows.items():
405+
data, _ = self.get_item_from_path(arrow_path)
406+
arrow = data[:, 3:]
407+
self._max_arrow_mutex.lock()
408+
self._max_arrow = max(
409+
np.max(np.linalg.norm(arrow, axis=1)), self._max_arrow
410+
)
411+
self._max_arrow_mutex.unlock()
412+
self._3d_arrows_path_lock.unlock()
413+
414+
def unregister_3d_arrow(self, key):
415+
self._3d_arrows_path_lock.lock()
416+
del self._3d_arrows[key]
417+
self._3d_arrows_path_lock.unlock()
418+
363419
def register_3d_trajectory(self, key, trajectory_path):
364420
self._3d_trajectories_path_lock.lock()
365421
self._3d_trajectories_path[key] = trajectory_path

robot_log_visualizer/robot_visualizer/meshcat_provider.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(self, signal_provider, period):
3737
self.env_list = ["GAZEBO_MODEL_PATH", "ROS_PACKAGE_PATH", "AMENT_PREFIX_PATH"]
3838
self._registered_3d_points = set()
3939
self._registered_3d_trajectories = dict()
40+
self._register_3d_arrow = set()
4041

4142
self.frame_T_base = np.eye(4)
4243

@@ -59,6 +60,14 @@ def register_3d_point(self, point_path, color):
5960
radius=radius, color=color, shape_name=point_path
6061
)
6162

63+
def register_3d_arrow(self, arrow_path, color):
64+
radius = 0.02
65+
locker = QMutexLocker(self.meshcat_visualizer_mutex)
66+
self._register_3d_arrow.add(arrow_path)
67+
self._meshcat_visualizer.load_arrow(
68+
radius=radius, color=color, shape_name=arrow_path
69+
)
70+
6271
def register_3d_trajectory(self, trajectory_path, color):
6372
locker = QMutexLocker(self.meshcat_visualizer_mutex)
6473
self._registered_3d_trajectories[trajectory_path] = (False, color)
@@ -73,6 +82,11 @@ def unregister_3d_trajectory(self, trajectory_path):
7382
self._registered_3d_trajectories.pop(trajectory_path, None)
7483
self._meshcat_visualizer.delete(shape_name=trajectory_path)
7584

85+
def unregister_3d_arrow(self, arrow_path):
86+
locker = QMutexLocker(self.meshcat_visualizer_mutex)
87+
self._register_3d_arrow.remove(arrow_path)
88+
self._meshcat_visualizer.delete(shape_name=arrow_path)
89+
7690
def load_model(self, considered_joints, model_name, base_frame=None):
7791
def get_model_path_from_envs(env_list):
7892
return [
@@ -269,6 +283,19 @@ def run(self):
269283
color=self._registered_3d_trajectories[trajectory_path][1],
270284
)
271285

286+
for (
287+
arrow_path,
288+
arrow,
289+
) in self._signal_provider.get_3d_arrow_at_index(index).items():
290+
if arrow_path not in self._register_3d_arrow:
291+
continue
292+
293+
self._meshcat_visualizer.set_arrow_transform(
294+
origin=arrow[0:3],
295+
vector=arrow[3:6] / self._signal_provider.max_arrow,
296+
shape_name=arrow_path,
297+
)
298+
272299
self.meshcat_visualizer_mutex.unlock()
273300

274301
sleep_time = self._period - (time.time() - start)

robot_log_visualizer/ui/gui.py

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ def __init__(self, meshcat_provider, parent=None, dataset_loaded=False):
7171
self.ui.robotModelToolButton.clicked.connect(self.open_urdf_file)
7272
self.ui.packageDirToolButton.clicked.connect(self.open_package_directory)
7373

74+
# Force the arrowScaling_lineEdit to be a positive float
75+
self.ui.arrowScaling_lineEdit.setValidator(QtGui.QDoubleValidator(0, 100, 2))
76+
77+
# connect the arrowScaling_checkBox to the handle_arrow_scaling method
78+
self.ui.arrowScaling_checkBox.toggled.connect(self.handle_arrow_scaling)
79+
80+
self.clicked_button = None
81+
self.std_button = None
82+
self.ui.buttonBox.clicked.connect(self.buttonBox_on_click)
83+
7484
if dataset_loaded:
7585
frames = meshcat_provider.robot_frames()
7686
self.ui.frameNameComboBox.addItems(frames)
@@ -94,6 +104,33 @@ def get_urdf_path(self):
94104
def get_package_directory(self):
95105
return self.ui.packageDirLineEdit.text()
96106

107+
def buttonBox_on_click(self, button):
108+
self.clicked_button = button
109+
110+
self.std_button = self.ui.buttonBox.standardButton(button)
111+
112+
def get_clicked_button_role(self):
113+
if self.clicked_button is not None:
114+
return self.ui.buttonBox.buttonRole(self.clicked_button)
115+
return None
116+
117+
def get_clicked_button_text(self):
118+
if self.clicked_button is not None:
119+
return self.clicked_button.text()
120+
return None
121+
122+
def get_clicked_standard_button(self):
123+
return self.std_button
124+
125+
def handle_arrow_scaling(self):
126+
# if arrowScaling_checkBox is checked the lineEdit must be disabled else it must be enabled
127+
if self.ui.arrowScaling_checkBox.isChecked():
128+
self.ui.arrowScaling_lineEdit.setText("")
129+
self.ui.arrowScaling_lineEdit.setEnabled(False)
130+
else:
131+
self.ui.arrowScaling_lineEdit.setText("")
132+
self.ui.arrowScaling_lineEdit.setEnabled(True)
133+
97134

98135
class About(QtWidgets.QMainWindow):
99136
def __init__(self):
@@ -172,6 +209,7 @@ def __init__(self, signal_provider, meshcat_provider, animation_period):
172209
self.video_items = []
173210
self.visualized_3d_points = set()
174211
self.visualized_3d_trajectories = set()
212+
self.visualized_3d_arrows = set()
175213
self.visualized_3d_points_colors_palette = ColorPalette()
176214

177215
self.toolButton_on_click()
@@ -690,9 +728,47 @@ def open_set_robot_model(self):
690728
)
691729
outcome = dlg.exec()
692730
if outcome == QDialog.Accepted:
693-
if not self.dataset_loaded:
694-
self.meshcat_provider.model_path = dlg.get_urdf_path()
695-
self.meshcat_provider.custom_package_dir = dlg.get_package_directory()
731+
732+
# check which button was clicked
733+
button_role = dlg.get_clicked_button_role()
734+
button_text = dlg.get_clicked_button_text()
735+
std_button = dlg.get_clicked_standard_button()
736+
737+
if std_button == QtWidgets.QDialogButtonBox.SaveAll:
738+
if not self.dataset_loaded:
739+
self.meshcat_provider.model_path = dlg.get_urdf_path()
740+
self.meshcat_provider.custom_package_dir = (
741+
dlg.get_package_directory()
742+
)
743+
744+
arrow_scaling_value = dlg.ui.arrowScaling_lineEdit.text()
745+
if not arrow_scaling_value:
746+
arrow_scaling_value = "1.0"
747+
else:
748+
arrow_scaling_value = float(arrow_scaling_value)
749+
self.signal_provider.set_custom_max_arrow(
750+
not dlg.ui.arrowScaling_checkBox.isChecked(), arrow_scaling_value
751+
)
752+
if std_button == QtWidgets.QDialogButtonBox.Save:
753+
# we need to check which tab is selected in the dlg
754+
if dlg.ui.tabWidget.currentIndex() == 0:
755+
if not self.dataset_loaded:
756+
self.meshcat_provider.model_path = dlg.get_urdf_path()
757+
self.meshcat_provider.custom_package_dir = (
758+
dlg.get_package_directory()
759+
)
760+
else:
761+
arrow_scaling_value = dlg.ui.arrowScaling_lineEdit.text()
762+
# if it is empty we set it to 1.0
763+
if not arrow_scaling_value:
764+
arrow_scaling_value = "1.0"
765+
else:
766+
arrow_scaling_value = float(arrow_scaling_value)
767+
self.signal_provider.set_custom_max_arrow(
768+
not dlg.ui.arrowScaling_checkBox.isChecked(),
769+
arrow_scaling_value,
770+
)
771+
696772
else:
697773
self.meshcat_provider.load_model(
698774
self.signal_provider.joints_name,
@@ -752,8 +828,10 @@ def variableTreeWidget_on_right_click(self, item_position):
752828

753829
add_3d_point_str = "Show as a 3D point"
754830
add_3d_trajectory_str = "Show as a 3D trajectory"
831+
add_3d_arrow_str = "Show as a 3D arrow"
755832
remove_3d_point_str = "Remove the 3D point"
756833
remove_3d_trajectory_str = "Remove the 3D trajectory"
834+
remove_3d_arrow_str = "Remove the 3D arrow"
757835
use_as_base_position_str = "Use as base position"
758836
use_as_base_orientation_str = "Use as base orientation"
759837
dont_use_as_base_position_str = "Don't use as base position"
@@ -805,14 +883,24 @@ def variableTreeWidget_on_right_click(self, item_position):
805883
else:
806884
menu.addAction(use_as_base_orientation_str + " (xyzw Quaternion)")
807885

886+
if item_size == 6:
887+
if item_key in self.visualized_3d_arrows:
888+
menu.addAction(remove_3d_arrow_str)
889+
else:
890+
menu.addAction(add_3d_arrow_str)
891+
808892
# show the menu
809893
action = menu.exec_(self.ui.variableTreeWidget.mapToGlobal(item_position))
810894
if action is None:
811895
return
812896

813897
item_path = self.get_item_path(item)
814898

815-
if action.text() == add_3d_point_str or action.text() == add_3d_trajectory_str:
899+
if (
900+
action.text() == add_3d_point_str
901+
or action.text() == add_3d_trajectory_str
902+
or action.text() == add_3d_arrow_str
903+
):
816904
color = next(self.visualized_3d_points_colors_palette)
817905

818906
item.setForeground(0, QtGui.QBrush(QtGui.QColor(color.as_hex())))
@@ -823,12 +911,20 @@ def variableTreeWidget_on_right_click(self, item_position):
823911
)
824912
self.signal_provider.register_3d_point(item_key, item_path)
825913
self.visualized_3d_points.add(item_key)
826-
else:
914+
elif action.text() == add_3d_trajectory_str:
827915
self.meshcat_provider.register_3d_trajectory(
828916
item_key, list(color.as_normalized_rgb())
829917
)
830918
self.signal_provider.register_3d_trajectory(item_key, item_path)
831919
self.visualized_3d_trajectories.add(item_key)
920+
elif action.text() == add_3d_arrow_str:
921+
self.meshcat_provider.register_3d_arrow(
922+
item_key, list(color.as_normalized_rgb())
923+
)
924+
self.signal_provider.register_3d_arrow(item_key, item_path)
925+
self.visualized_3d_arrows.add(item_key)
926+
else:
927+
raise ValueError("Unknown action")
832928

833929
if action.text() == remove_3d_point_str:
834930
self.meshcat_provider.unregister_3d_point(item_key)
@@ -842,6 +938,12 @@ def variableTreeWidget_on_right_click(self, item_position):
842938
self.visualized_3d_trajectories.remove(item_key)
843939
item.setForeground(0, QtGui.QBrush(QtGui.QColor(0, 0, 0)))
844940

941+
if action.text() == remove_3d_arrow_str:
942+
self.meshcat_provider.unregister_3d_arrow(item_key)
943+
self.signal_provider.unregister_3d_arrow(item_key)
944+
self.visualized_3d_arrows.remove(item_key)
945+
item.setForeground(0, QtGui.QBrush(QtGui.QColor(0, 0, 0)))
946+
845947
if (
846948
use_as_base_orientation_str in action.text()
847949
or action.text() == use_as_base_position_str

0 commit comments

Comments
 (0)