q3dviewer 1.0.8__py3-none-any.whl → 1.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
5
+ Distributed under MIT license. See LICENSE for more information.
6
+ """
7
+
8
+ import numpy as np
9
+ import q3dviewer as q3d
10
+ from PySide6.QtWidgets import QVBoxLayout, QListWidget, QListWidgetItem, QPushButton, QDoubleSpinBox, QCheckBox, QLineEdit, QMessageBox, QLabel, QHBoxLayout, QDockWidget, QWidget, QComboBox
11
+ from PySide6.QtCore import QTimer
12
+ from q3dviewer.tools.cloud_viewer import ProgressDialog, FileLoaderThread
13
+ from PySide6 import QtCore
14
+ from PySide6.QtGui import QKeyEvent
15
+ from q3dviewer import GLWidget
16
+ import imageio.v2 as imageio
17
+ import os
18
+
19
+
20
+ class KeyFrame:
21
+ def __init__(self, Twc, lin_vel=10, ang_vel=np.pi/3, stop_time=0):
22
+ self.Twc = Twc
23
+ self.lin_vel = lin_vel
24
+ self.ang_vel = ang_vel # rad/s
25
+ self.stop_time = stop_time
26
+ self.item = q3d.FrameItem(Twc, width=3, color='#0000FF')
27
+
28
+
29
+ class CustomGLWidget(GLWidget):
30
+ def __init__(self, viewer):
31
+ super().__init__()
32
+ self.viewer = viewer # Add a viewer handle
33
+
34
+ def keyPressEvent(self, ev: QKeyEvent):
35
+ if ev.key() == QtCore.Qt.Key_Space:
36
+ self.viewer.add_key_frame()
37
+ elif ev.key() == QtCore.Qt.Key_Delete:
38
+ self.viewer.del_key_frame()
39
+ elif ev.key() == QtCore.Qt.Key_C:
40
+ self.viewer.dock.show()
41
+ super().keyPressEvent(ev)
42
+
43
+ class CMMViewer(q3d.Viewer):
44
+ """
45
+ This class is a subclass of Viewer, which is used to create a cloud movie maker.
46
+ """
47
+ def __init__(self, **kwargs):
48
+ self.key_frames = []
49
+ self.video_path = os.path.join(os.path.expanduser("~"), "output.mp4")
50
+ super().__init__(**kwargs, gl_widget_class=lambda: CustomGLWidget(self))
51
+ # for drop cloud file
52
+ self.setAcceptDrops(True)
53
+
54
+ def add_control_panel(self, main_layout):
55
+ """
56
+ Add a control panel to the viewer.
57
+ """
58
+ # Create a vertical layout for the settings
59
+ setting_layout = QVBoxLayout()
60
+
61
+ # Buttons to add and delete key frames
62
+ add_button = QPushButton("Add Key Frame (Key Space)")
63
+ add_button.clicked.connect(self.add_key_frame)
64
+ setting_layout.addWidget(add_button)
65
+ del_button = QPushButton("Delete Key Frame (Key Delete)")
66
+ del_button.clicked.connect(self.del_key_frame)
67
+ setting_layout.addWidget(del_button)
68
+
69
+ # Add play/stop button
70
+ self.play_button = QPushButton("Play")
71
+ self.play_button.clicked.connect(self.toggle_playback)
72
+ setting_layout.addWidget(self.play_button)
73
+
74
+ # add a timer to play the frames
75
+ self.timer = QTimer()
76
+ self.timer.timeout.connect(self.play_frames)
77
+ self.current_frame_index = 0
78
+ self.is_playing = False
79
+ self.is_recording = False
80
+
81
+ # Add record checkbox
82
+ self.record_checkbox = QCheckBox("Record")
83
+ self.record_checkbox.stateChanged.connect(self.toggle_recording)
84
+ setting_layout.addWidget(self.record_checkbox)
85
+
86
+ # Add video path setting
87
+ video_path_layout = QHBoxLayout()
88
+ label_video_path = QLabel("Video Path:")
89
+ video_path_layout.addWidget(label_video_path)
90
+ self.video_path_edit = QLineEdit()
91
+ self.video_path_edit.setText(self.video_path)
92
+ self.video_path_edit.textChanged.connect(self.update_video_path)
93
+ video_path_layout.addWidget(self.video_path_edit)
94
+ setting_layout.addLayout(video_path_layout)
95
+
96
+ # Add codec setting
97
+ codec_layout = QHBoxLayout()
98
+ label_codec = QLabel("Codec:")
99
+ codec_layout.addWidget(label_codec)
100
+ self.codec_combo = QComboBox()
101
+ self.codec_combo.addItems(["mjpeg", "mpeg4", "libx264", "libx265"])
102
+ codec_layout.addWidget(self.codec_combo)
103
+ setting_layout.addLayout(codec_layout)
104
+
105
+ # Add a list of key frames
106
+ self.frame_list = QListWidget()
107
+ setting_layout.addWidget(self.frame_list)
108
+ self.frame_list.itemSelectionChanged.connect(self.on_select_frame)
109
+ self.installEventFilter(self)
110
+
111
+ # Add spin boxes for linear / angular velocity and stop time
112
+ self.lin_vel_spinbox = QDoubleSpinBox()
113
+ self.lin_vel_spinbox.setPrefix("Linear Velocity (m/s): ")
114
+ self.lin_vel_spinbox.setRange(0, 1000)
115
+ self.lin_vel_spinbox.valueChanged.connect(self.set_frame_lin_vel)
116
+ setting_layout.addWidget(self.lin_vel_spinbox)
117
+
118
+ self.lin_ang_spinbox = QDoubleSpinBox()
119
+ self.lin_ang_spinbox.setPrefix("Angular Velocity (deg/s): ")
120
+ self.lin_ang_spinbox.setRange(0, 360)
121
+ self.lin_ang_spinbox.valueChanged.connect(self.set_frame_ang_vel)
122
+ setting_layout.addWidget(self.lin_ang_spinbox)
123
+
124
+ self.stop_time_spinbox = QDoubleSpinBox()
125
+ self.stop_time_spinbox.setPrefix("Stop Time: ")
126
+ self.stop_time_spinbox.setRange(0, 100)
127
+ self.stop_time_spinbox.valueChanged.connect(self.set_frame_stop_time)
128
+ setting_layout.addWidget(self.stop_time_spinbox)
129
+
130
+ setting_layout.setAlignment(QtCore.Qt.AlignTop)
131
+
132
+ # Create a dock widget for the settings
133
+ dock_widget = QDockWidget("Settings", self)
134
+ dock_widget.setWidget(QWidget())
135
+ dock_widget.widget().setLayout(setting_layout)
136
+ self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock_widget)
137
+
138
+ # Add the dock widget to the main layout
139
+ main_layout.addWidget(dock_widget)
140
+ self.dock = dock_widget
141
+
142
+
143
+ def update_video_path(self, path):
144
+ self.video_path = path
145
+
146
+ def add_key_frame(self):
147
+ view_matrix = self.glwidget.view_matrix
148
+ # Get camera pose in world frame
149
+ Twc = np.linalg.inv(view_matrix)
150
+ # Add the key frame to the end of the list
151
+ if self.key_frames:
152
+ prev = self.key_frames[-1]
153
+ key_frame = KeyFrame(Twc,
154
+ lin_vel=prev.lin_vel,
155
+ ang_vel=prev.ang_vel)
156
+ else:
157
+ key_frame = KeyFrame(Twc)
158
+ self.key_frames.append(key_frame)
159
+ # visualize this key frame using FrameItem
160
+ self.glwidget.add_item(key_frame.item)
161
+ # move the camera back to 0.5 meter, let the user see the frame
162
+ self.glwidget.update_dist(0.5)
163
+ # Add the key frame to the Qt ListWidget
164
+ item = QListWidgetItem(f"Frame {len(self.key_frames)}")
165
+ self.frame_list.addItem(item)
166
+ self.frame_list.setCurrentRow(len(self.key_frames) - 1)
167
+
168
+ def del_key_frame(self):
169
+ current_index = self.frame_list.currentRow()
170
+ if current_index >= 0:
171
+ self.glwidget.remove_item(self.key_frames[current_index].item)
172
+ self.key_frames.pop(current_index)
173
+ self.frame_list.itemSelectionChanged.disconnect(self.on_select_frame)
174
+ self.frame_list.takeItem(current_index)
175
+ self.frame_list.itemSelectionChanged.connect(self.on_select_frame)
176
+ self.on_select_frame()
177
+ # Update frame labels
178
+ for i in range(len(self.key_frames)):
179
+ self.frame_list.item(i).setText(f"Frame {i + 1}")
180
+
181
+ def on_select_frame(self):
182
+ current = self.frame_list.currentRow()
183
+ for i, frame in enumerate(self.key_frames):
184
+ if i == current:
185
+ # Highlight the selected frame
186
+ frame.item.set_color('#FF0000')
187
+ frame.item.set_line_width(5)
188
+ # show current frame's parameters in the spinboxes
189
+ self.lin_vel_spinbox.setValue(frame.lin_vel)
190
+ self.lin_ang_spinbox.setValue(np.rad2deg(frame.ang_vel))
191
+ self.stop_time_spinbox.setValue(frame.stop_time)
192
+ else:
193
+ frame.item.set_color('#0000FF')
194
+ frame.item.set_line_width(3)
195
+
196
+ def set_frame_lin_vel(self, value):
197
+ current_index = self.frame_list.currentRow()
198
+ if current_index >= 0:
199
+ self.key_frames[current_index].lin_vel = value
200
+
201
+ def set_frame_ang_vel(self, value):
202
+ current_index = self.frame_list.currentRow()
203
+ if current_index >= 0:
204
+ self.key_frames[current_index].ang_vel = np.deg2rad(value)
205
+
206
+ def set_frame_stop_time(self, value):
207
+ current_index = self.frame_list.currentRow()
208
+ if current_index >= 0:
209
+ self.key_frames[current_index].stop_time = value
210
+
211
+ def create_frames(self):
212
+ """
213
+ Create the frames for playback by interpolating between key frames.
214
+ """
215
+ self.frames = []
216
+ dt = 1 / float(self.update_interval)
217
+ for i in range(len(self.key_frames) - 1):
218
+ current_frame = self.key_frames[i]
219
+ if current_frame.stop_time > 0:
220
+ num_steps = int(current_frame.stop_time / dt)
221
+ for j in range(num_steps):
222
+ self.frames.append([i, current_frame.Twc])
223
+ next_frame = self.key_frames[i + 1]
224
+ Ts = q3d.interpolate_pose(current_frame.Twc, next_frame.Twc,
225
+ current_frame.lin_vel,
226
+ current_frame.ang_vel,
227
+ dt)
228
+ for T in Ts:
229
+ self.frames.append([i, T])
230
+
231
+ print(f"Total frames: {len(self.frames)}")
232
+ print(f"Total time: {len(self.frames) * dt:.2f} seconds")
233
+
234
+ def toggle_playback(self):
235
+ if self.is_playing:
236
+ self.stop_playback()
237
+ else:
238
+ self.start_playback()
239
+
240
+ def start_playback(self):
241
+ if self.key_frames:
242
+ self.create_frames()
243
+ self.current_frame_index = 0
244
+ self.timer.start(self.update_interval) # Adjust the interval as needed
245
+ self.is_playing = True
246
+ self.play_button.setStyleSheet("")
247
+ self.play_button.setText("Stop")
248
+ self.record_checkbox.setEnabled(False)
249
+ if self.is_recording is True:
250
+ self.start_recording()
251
+
252
+ def stop_playback(self):
253
+ self.timer.stop()
254
+ self.is_playing = False
255
+ self.play_button.setStyleSheet("")
256
+ self.play_button.setText("Play")
257
+ self.record_checkbox.setEnabled(True)
258
+ self.frame_list.setCurrentRow(len(self.key_frames) - 1)
259
+ if self.is_recording:
260
+ self.stop_recording()
261
+
262
+ def play_frames(self):
263
+ """
264
+ callback function for the timer to play the frames
265
+ """
266
+ # play the frames
267
+ if self.current_frame_index < len(self.frames):
268
+ key_id, Tcw = self.frames[self.current_frame_index]
269
+ self.glwidget.set_view_matrix(np.linalg.inv(Tcw))
270
+ self.frame_list.setCurrentRow(key_id)
271
+ self.current_frame_index += 1
272
+ if self.is_recording:
273
+ self.record_frame()
274
+ else:
275
+ self.stop_playback()
276
+
277
+ def toggle_recording(self, state):
278
+ if state == 2:
279
+ self.is_recording = True
280
+ else:
281
+ self.is_recording = False
282
+
283
+ def start_recording(self):
284
+ self.is_recording = True
285
+ self.frames_to_record = []
286
+ video_path = self.video_path_edit.text()
287
+ codec = self.codec_combo.currentText()
288
+ self.play_button.setStyleSheet("background-color: red")
289
+ self.play_button.setText("Recording")
290
+ self.writer = imageio.get_writer(video_path,
291
+ fps=self.update_interval,
292
+ codec=codec) # quality=10
293
+ # disable the all the frame_item while recording
294
+ for frame in self.key_frames:
295
+ frame.item.hide()
296
+ self.dock.hide() # Hide the dock while recording
297
+
298
+ def stop_recording(self, save_movie=True):
299
+ self.is_recording = False
300
+ self.record_checkbox.setChecked(False)
301
+ # enable the all the frame_item after recording
302
+ for frame in self.key_frames:
303
+ frame.item.show()
304
+ if hasattr(self, 'writer') and save_movie:
305
+ self.writer.close()
306
+ self.show_save_message()
307
+ self.dock.show() # Show the dock when recording stops
308
+
309
+ def show_save_message(self):
310
+ msg_box = QMessageBox()
311
+ msg_box.setIcon(QMessageBox.Information)
312
+ msg_box.setWindowTitle("Video Saved")
313
+ msg_box.setText(f"Video saved to {self.video_path_edit.text()}")
314
+ msg_box.setStandardButtons(QMessageBox.Ok)
315
+ msg_box.exec()
316
+
317
+ def record_frame(self):
318
+ frame = self.glwidget.capture_frame()
319
+ # make sure the frame size is multiple of 16
320
+ height, width, _ = frame.shape
321
+ if height % 16 != 0 or width % 16 != 0:
322
+ frame = frame[:-(height % 16), :-(width % 16), :]
323
+ frame = np.ascontiguousarray(frame)
324
+ self.frames_to_record.append(frame)
325
+ try:
326
+ self.writer.append_data(frame)
327
+ except Exception as e:
328
+ print("Don't change the window size during recording.")
329
+ self.stop_recording(False) # Stop recording without saving
330
+ self.stop_playback()
331
+
332
+ def eventFilter(self, obj, event):
333
+ if event.type() == QtCore.QEvent.KeyPress:
334
+ if event.key() == QtCore.Qt.Key_Delete:
335
+ self.del_key_frame()
336
+ return True
337
+ return super().eventFilter(obj, event)
338
+
339
+ def dragEnterEvent(self, event):
340
+ if event.mimeData().hasUrls():
341
+ event.accept()
342
+ else:
343
+ event.ignore()
344
+
345
+ def dropEvent(self, event):
346
+ """
347
+ Overwrite the drop event to open the cloud file.
348
+ """
349
+ self.progress_dialog = ProgressDialog(self)
350
+ self.progress_dialog.show()
351
+ files = event.mimeData().urls()
352
+ self.progress_thread = FileLoaderThread(self, files)
353
+ self['cloud'].load(files[0].toLocalFile(), append=False)
354
+ self.progress_thread.progress.connect(self.file_loading_progress)
355
+ self.progress_thread.finished.connect(self.file_loading_finished)
356
+ self.progress_thread.start()
357
+
358
+ def file_loading_progress(self, value):
359
+ self.progress_dialog.set_value(value)
360
+
361
+ def file_loading_finished(self):
362
+ self.progress_dialog.close()
363
+
364
+ def open_cloud_file(self, file, append=False):
365
+ cloud_item = self['cloud']
366
+ if cloud_item is None:
367
+ print("Can't find clouditem.")
368
+ return
369
+ cloud = cloud_item.load(file, append=append)
370
+ center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
371
+ self.glwidget.set_cam_position(pos=center)
372
+
373
+ def main():
374
+ import argparse
375
+ parser = argparse.ArgumentParser()
376
+ parser.add_argument("--path", help="the cloud file path")
377
+ args = parser.parse_args()
378
+ app = q3d.QApplication(['Film Maker'])
379
+ viewer = CMMViewer(name='Film Maker', update_interval=30)
380
+ cloud_item = q3d.CloudIOItem(size=0.1, point_type='SPHERE', alpha=0.5, depth_test=True)
381
+ grid_item = q3d.GridItem(size=1000, spacing=20)
382
+
383
+ viewer.add_items(
384
+ {'cloud': cloud_item, 'grid': grid_item})
385
+
386
+ if args.path:
387
+ pcd_fn = args.path
388
+ viewer.open_cloud_file(pcd_fn)
389
+
390
+ viewer.show()
391
+ app.exec()
392
+
393
+
394
+ if __name__ == '__main__':
395
+ main()
@@ -43,7 +43,7 @@ class CustomDoubleSpinBox(QDoubleSpinBox):
43
43
  return float(text)
44
44
 
45
45
 
46
- class ViewerWithPanel(q3d.Viewer):
46
+ class LiDARCalibViewer(q3d.Viewer):
47
47
  def __init__(self, **kwargs):
48
48
  self.t01 = np.array([0, 0, 0])
49
49
  self.R01 = np.eye(3)
@@ -51,15 +51,15 @@ class ViewerWithPanel(q3d.Viewer):
51
51
  self.radius = 0.2
52
52
  super().__init__(**kwargs)
53
53
 
54
- def init_ui(self):
55
- center_widget = QWidget()
56
- self.setCentralWidget(center_widget)
57
- main_layout = QHBoxLayout()
58
- center_widget.setLayout(main_layout)
59
-
54
+ def default_gl_setting(self, glwidget):
55
+ # Set camera position and background color
56
+ glwidget.set_bg_color('#ffffff')
57
+ glwidget.set_cam_position(distance=5)
58
+
59
+ def add_control_panel(self, main_layout):
60
60
  # Create a vertical layout for the settings
61
61
  setting_layout = QVBoxLayout()
62
-
62
+ setting_layout.setAlignment(QtCore.Qt.AlignTop)
63
63
  # Add XYZ spin boxes
64
64
  label_xyz = QLabel("Set XYZ:")
65
65
  setting_layout.addWidget(label_xyz)
@@ -135,19 +135,8 @@ class ViewerWithPanel(q3d.Viewer):
135
135
  self.box_pitch.valueChanged.connect(self.update_rpy)
136
136
  self.box_yaw.valueChanged.connect(self.update_rpy)
137
137
 
138
- # Add a stretch to push the widgets to the top
139
- setting_layout.addStretch(1)
140
-
141
- self.glwidget = q3d.GLWidget()
142
138
  main_layout.addLayout(setting_layout)
143
- main_layout.addWidget(self.glwidget, 1)
144
-
145
- timer = QtCore.QTimer(self)
146
- timer.setInterval(20) # period, in milliseconds
147
- timer.timeout.connect(self.update)
148
- self.glwidget.set_cam_position(distance=5)
149
- self.glwidget.set_bg_color('#ffffff')
150
- timer.start()
139
+
151
140
 
152
141
  def update_radius(self):
153
142
  self.radius = self.box_radius.value()
@@ -271,8 +260,8 @@ def main():
271
260
  args = parser.parse_args()
272
261
 
273
262
  app = q3d.QApplication(["LiDAR Calib"])
274
- viewer = ViewerWithPanel(name='LiDAR Calib')
275
- grid_item = q3d.GridItem(size=10, spacing=1, color=(0, 0, 0, 70))
263
+ viewer = LiDARCalibViewer(name='LiDAR Calib')
264
+ grid_item = q3d.GridItem(size=10, spacing=1, color='#00000040')
276
265
  scan0_item = q3d.CloudItem(
277
266
  size=2, alpha=1, color_mode='FLAT', color='#ff0000')
278
267
  scan1_item = q3d.CloudItem(
@@ -36,7 +36,7 @@ class CustomDoubleSpinBox(QDoubleSpinBox):
36
36
  return float(text)
37
37
 
38
38
 
39
- class ViewerWithPanel(q3d.Viewer):
39
+ class LidarCamViewer(q3d.Viewer):
40
40
  def __init__(self, **kwargs):
41
41
  # b: camera body frame
42
42
  # c: camera image frame
@@ -54,15 +54,15 @@ class ViewerWithPanel(q3d.Viewer):
54
54
  self.en_rgb = False
55
55
  super().__init__(**kwargs)
56
56
 
57
- def init_ui(self):
58
- center_widget = QWidget()
59
- self.setCentralWidget(center_widget)
60
- main_layout = QHBoxLayout()
61
- center_widget.setLayout(main_layout)
57
+ def default_gl_setting(self, glwidget):
58
+ # Set camera position and background color
59
+ glwidget.set_bg_color('#ffffff')
60
+ glwidget.set_cam_position(distance=5)
62
61
 
62
+ def add_control_panel(self, main_layout):
63
63
  # Create a vertical layout for the settings
64
64
  setting_layout = QVBoxLayout()
65
-
65
+ setting_layout.setAlignment(QtCore.Qt.AlignTop)
66
66
  # Add a checkbox for RGB
67
67
  self.checkbox_rgb = QCheckBox("Enable RGB Cloud")
68
68
  self.checkbox_rgb.setChecked(False)
@@ -148,19 +148,8 @@ class ViewerWithPanel(q3d.Viewer):
148
148
  self.box_pitch.valueChanged.connect(self.update_rpy)
149
149
  self.box_yaw.valueChanged.connect(self.update_rpy)
150
150
 
151
- # Add a stretch to push the widgets to the top
152
- setting_layout.addStretch(1)
153
-
154
- self.glwidget = q3d.GLWidget()
155
151
  main_layout.addLayout(setting_layout)
156
- main_layout.addWidget(self.glwidget, 1)
157
152
 
158
- timer = QtCore.QTimer(self)
159
- timer.setInterval(20) # period, in milliseconds
160
- timer.timeout.connect(self.update)
161
- self.glwidget.set_cam_position(distance=5)
162
- self.glwidget.set_bg_color('#ffffff')
163
- timer.start()
164
153
 
165
154
  def update_point_size(self):
166
155
  self.psize = self.box_psize.value()
@@ -292,8 +281,8 @@ def main():
292
281
  args = parser.parse_args()
293
282
 
294
283
  app = q3d.QApplication(['LiDAR Cam Calib'])
295
- viewer = ViewerWithPanel(name='LiDAR Cam Calib')
296
- grid_item = q3d.GridItem(size=10, spacing=1, color=(0, 0, 0, 70))
284
+ viewer = LidarCamViewer(name='LiDAR Cam Calib')
285
+ grid_item = q3d.GridItem(size=10, spacing=1, color='#00000040')
297
286
  scan_item = q3d.CloudItem(size=2, alpha=1, color_mode='I')
298
287
  img_item = q3d.ImageItem(pos=np.array([0, 0]), size=np.array([800, 600]))
299
288
  viewer.add_items({'scan': scan_item, 'grid': grid_item, 'img': img_item})
q3dviewer/utils/maths.py CHANGED
@@ -3,9 +3,145 @@ Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
3
3
  Distributed under MIT license. See LICENSE for more information.
4
4
  """
5
5
 
6
+ """
7
+ Math proofs and implementations:
8
+ https://github.com/scomup/MathematicalRobotics.git
9
+ """
10
+
11
+
6
12
  import numpy as np
7
13
 
8
14
 
15
+ _epsilon_ = 1e-5
16
+
17
+
18
+ def skew(vector):
19
+ return np.array([[0, -vector[2], vector[1]],
20
+ [vector[2], 0, -vector[0]],
21
+ [-vector[1], vector[0], 0]])
22
+
23
+
24
+ def expSO3(omega):
25
+ """
26
+ Exponential map of SO3
27
+ The proof is shown in 3d_rotation_group.md (10)
28
+ """
29
+ theta2 = omega.dot(omega)
30
+ theta = np.sqrt(theta2)
31
+ nearZero = theta2 <= _epsilon_
32
+ W = skew(omega)
33
+ if (nearZero):
34
+ return np.eye(3) + W
35
+ else:
36
+ K = W/theta
37
+ KK = K.dot(K)
38
+ sin_theta = np.sin(theta)
39
+ one_minus_cos = 1 - np.cos(theta)
40
+ R = np.eye(3) + sin_theta * K + one_minus_cos * KK # rotation.md (10)
41
+ return R
42
+
43
+
44
+ def logSO3(R):
45
+ """
46
+ Logarithm map of SO3
47
+ The proof is shown in rotation.md (14)
48
+ """
49
+ R11, R12, R13 = R[0, :]
50
+ R21, R22, R23 = R[1, :]
51
+ R31, R32, R33 = R[2, :]
52
+ tr = np.trace(R)
53
+ omega = np.zeros(3)
54
+ v = np.array([R32 - R23, R13 - R31, R21 - R12])
55
+ # when trace == -1, i.e., the theta is approx to +-pi, +-3pi, +-5pi, etc.
56
+ # we do something special
57
+ if (tr + 1.0 < 1e-3):
58
+ if (R33 > R22 and R33 > R11):
59
+ # R33 is largest
60
+ # sin(theta) approx to sgn_w*pi-theta, cos(theta) approx to -1
61
+ W = R21 - R12 # 2*r3*sin(theta) = 2*r3*(sgn_w*pi-theta)
62
+ Q1 = R31 + R13 # 4 * r1*r3
63
+ Q2 = R23 + R32 # 4 * r2*r3
64
+ Q3 = 2.0 + 2.0 * R33 # 4 * r3*r3
65
+ r = np.sqrt(Q3) # 2 * r3
66
+ one_over_r = 1 / r # 1 / (2*r3)
67
+ norm = np.sqrt(Q1*Q1 + Q2*Q2 + Q3*Q3 + W*W) # 4*r3
68
+ sgn_w = np.sign(W) # get the sgn of theta
69
+ mag = np.pi - (2 * sgn_w * W) / norm # theta*sgn_w
70
+ scale = 0.5 * mag * one_over_r # theta * sgn_w / (4*r3)
71
+ # omega = theta * [4*r1*r3, 4*r2*r3, 4*r3*r3]/ (4*r3)
72
+ omega = sgn_w * scale * np.array([Q1, Q2, Q3])
73
+ elif (R22 > R11):
74
+ # R22 is the largest
75
+ W = R13 - R31 # 2*r2*sin(theta) = 2*r2*(sgn_w*pi-theta)
76
+ Q1 = R12 + R21 # 4 * r2*r1
77
+ Q2 = 2.0 + 2.0 * R22 # 4 * r2*r2
78
+ Q3 = R23 + R32 # 4 * r2*r3
79
+ r = np.sqrt(Q2)
80
+ one_over_r = 1 / r
81
+ norm = np.sqrt(Q1*Q1 + Q2*Q2 + Q3*Q3 + W*W)
82
+ sgn_w = np.sign(W)
83
+ mag = np.pi - (2 * sgn_w * W) / norm
84
+ scale = 0.5 * one_over_r * mag
85
+ omega = sgn_w * scale * np.array([Q1, Q2, Q3])
86
+ else:
87
+ # R11 is the largest
88
+ W = R32 - R23 # 2*r1*sin(theta) = 2*r1*(sgn_w*pi-theta)
89
+ Q1 = 2.0 + 2.0 * R11 # 4 * r1*r1
90
+ Q2 = R12 + R21 # 4 * r1*r2
91
+ Q3 = R31 + R13 # 4 * r1*r3
92
+ r = np.sqrt(Q1)
93
+ one_over_r = 1 / r
94
+ norm = np.sqrt(Q1*Q1 + Q2*Q2 + Q3*Q3 + W*W)
95
+ sgn_w = np.sign(W)
96
+ mag = np.pi - (2 * sgn_w * W) / norm
97
+ scale = 0.5 * one_over_r * mag
98
+ omega = sgn_w * scale * np.array([Q1, Q2, Q3])
99
+ else:
100
+ magnitude = 0
101
+ tr_3 = tr - 3.0
102
+ if (tr_3 < -1e-6):
103
+ # this is the normal case -1 < trace < 3
104
+ theta = np.arccos((tr - 1.0) / 2.0)
105
+ magnitude = theta / (2.0 * np.sin(theta))
106
+ else:
107
+ # when theta near 0, +-2pi, +-4pi, etc. (trace near 3.0)
108
+ # use Taylor expansion: theta \approx 1/2-(t-3)/12 + O((t-3)^2)
109
+ # see https://github.com/borglab/gtsam/issues/746 for details
110
+ magnitude = 0.5 - tr_3 / 12.0 + tr_3*tr_3/60.0
111
+ omega = magnitude * np.array([R32 - R23, R13 - R31, R21 - R12])
112
+ return omega
113
+
114
+
115
+ def interpolate_pose(T1, T2, v_max, omega_max, dt=0.02):
116
+ R1, t1 = makeRt(T1)
117
+ R2, t2 = makeRt(T2)
118
+
119
+ # Get transfrom time based on linear velocity
120
+ d = np.linalg.norm(t2 - t1)
121
+ t_lin = d / v_max
122
+
123
+ # Get transform time based on angular velocity
124
+ omega = logSO3(R2 @ R1.T)
125
+ theta = np.linalg.norm(omega)
126
+ t_ang = theta / omega_max
127
+
128
+ # Get total time based on the linear and angular time.
129
+ t_total = max(t_lin, t_ang)
130
+ num_steps = int(np.ceil(t_total / dt))
131
+
132
+ # Generate interpolated transforms
133
+ interpolated_Ts = []
134
+ for i in range(num_steps):
135
+ s = i / num_steps
136
+ t_interp = (1 - s) * t1 + s * t2
137
+ # Interpolate rotation using SO3.
138
+ R_interp = expSO3(s * omega) @ R1
139
+ T_interp = makeT(R_interp, t_interp)
140
+ interpolated_Ts.append(T_interp)
141
+
142
+ return interpolated_Ts
143
+
144
+
9
145
  def frustum(left, right, bottom, top, near, far):
10
146
  # see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
11
147
  if near <= 0 or far <= 0 or near >= far or left == right or bottom == top:
@@ -157,11 +293,25 @@ def makeRt(T):
157
293
  return R, t
158
294
 
159
295
  def hex_to_rgba(hex_color):
160
- color_flat = int(hex_color[1:], 16)
161
- red = (color_flat >> 16) & 0xFF
162
- green = (color_flat >> 8) & 0xFF
163
- blue = color_flat & 0xFF
164
- return (red / 255.0, green / 255.0, blue / 255.0, 1.0)
296
+ if not hex_color.startswith("#"):
297
+ print("Invalid hex color string.")
298
+ return (1.0, 1.0, 1.0, 1.0)
299
+ if len(hex_color) == 7:
300
+ color_flat = int(hex_color[1:], 16)
301
+ red = (color_flat >> 16) & 0xFF
302
+ green = (color_flat >> 8) & 0xFF
303
+ blue = color_flat & 0xFF
304
+ return (red / 255.0, green / 255.0, blue / 255.0, 1.0)
305
+ elif len(hex_color) == 9:
306
+ color_flat = int(hex_color[1:], 16)
307
+ red = (color_flat >> 24) & 0xFF
308
+ green = (color_flat >> 16) & 0xFF
309
+ blue = (color_flat >> 8) & 0xFF
310
+ alpha = color_flat & 0xFF
311
+ return (red / 255.0, green / 255.0, blue / 255.0, alpha / 255.0)
312
+ else:
313
+ print("Invalid hex color string.")
314
+ return (1.0, 1.0, 1.0, 1.0)
165
315
 
166
316
  # euler = np.array([1, 0.1, 0.1])
167
317
  # euler_angles = matrix_to_euler(euler_to_matrix(euler))