q3dviewer 1.0.8__py3-none-any.whl → 1.1.0__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,421 @@
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
+ def recover_center_euler(Twc, dist):
21
+ Rwc = Twc[:3, :3] # Extract rotation
22
+ twc = Twc[:3, 3] # Extract translation
23
+ tco = np.array([0, 0, dist]) # Camera frame origin
24
+ two = twc - Rwc @ tco # Compute center
25
+ euler = q3d.matrix_to_euler(Rwc)
26
+ return two, euler
27
+
28
+
29
+ class KeyFrame:
30
+ def __init__(self, Twc, lin_vel=10, ang_vel=np.pi/3, stop_time=0):
31
+ self.Twc = Twc
32
+ self.lin_vel = lin_vel
33
+ self.ang_vel = ang_vel # rad/s
34
+ self.stop_time = stop_time
35
+ self.item = q3d.FrameItem(Twc, width=3, color='#0000FF')
36
+
37
+
38
+ class CustomGLWidget(GLWidget):
39
+ def __init__(self, viewer):
40
+ super().__init__()
41
+ self.viewer = viewer # Add a viewer handle
42
+
43
+ def keyPressEvent(self, ev: QKeyEvent):
44
+ if ev.key() == QtCore.Qt.Key_Space:
45
+ self.viewer.add_key_frame()
46
+ elif ev.key() == QtCore.Qt.Key_Delete:
47
+ self.viewer.del_key_frame()
48
+ elif ev.key() == QtCore.Qt.Key_C:
49
+ self.viewer.dock.show()
50
+ super().keyPressEvent(ev)
51
+
52
+ class CMMViewer(q3d.Viewer):
53
+ """
54
+ This class is a subclass of Viewer, which is used to create a cloud movie maker.
55
+ """
56
+ def __init__(self, **kwargs):
57
+ self.key_frames = []
58
+ self.video_path = os.path.join(os.path.expanduser("~"), "output.mp4")
59
+ super().__init__(**kwargs, gl_widget_class=lambda: CustomGLWidget(self))
60
+ # for drop cloud file
61
+ self.setAcceptDrops(True)
62
+
63
+ def add_control_panel(self, main_layout):
64
+ """
65
+ Add a control panel to the viewer.
66
+ """
67
+ # Create a vertical layout for the settings
68
+ setting_layout = QVBoxLayout()
69
+
70
+ # Buttons to add and delete key frames
71
+ add_button = QPushButton("Add Key Frame (Key Space)")
72
+ add_button.clicked.connect(self.add_key_frame)
73
+ setting_layout.addWidget(add_button)
74
+ del_button = QPushButton("Delete Key Frame (Key Delete)")
75
+ del_button.clicked.connect(self.del_key_frame)
76
+ setting_layout.addWidget(del_button)
77
+
78
+ # Add play/stop button
79
+ self.play_button = QPushButton("Play")
80
+ self.play_button.clicked.connect(self.toggle_playback)
81
+ setting_layout.addWidget(self.play_button)
82
+
83
+ # add a timer to play the frames
84
+ self.timer = QTimer()
85
+ self.timer.timeout.connect(self.play_frames)
86
+ self.current_frame_index = 0
87
+ self.is_playing = False
88
+ self.is_recording = False
89
+
90
+ # Add record checkbox
91
+ self.record_checkbox = QCheckBox("Record")
92
+ self.record_checkbox.stateChanged.connect(self.toggle_recording)
93
+ setting_layout.addWidget(self.record_checkbox)
94
+
95
+ # Add video path setting
96
+ video_path_layout = QHBoxLayout()
97
+ label_video_path = QLabel("Video Path:")
98
+ video_path_layout.addWidget(label_video_path)
99
+ self.video_path_edit = QLineEdit()
100
+ self.video_path_edit.setText(self.video_path)
101
+ self.video_path_edit.textChanged.connect(self.update_video_path)
102
+ video_path_layout.addWidget(self.video_path_edit)
103
+ setting_layout.addLayout(video_path_layout)
104
+
105
+ # Add codec setting
106
+ codec_layout = QHBoxLayout()
107
+ label_codec = QLabel("Codec:")
108
+ codec_layout.addWidget(label_codec)
109
+ self.codec_combo = QComboBox()
110
+ self.codec_combo.addItems(["mjpeg", "mpeg4", "libx264", "libx265"])
111
+ codec_layout.addWidget(self.codec_combo)
112
+ setting_layout.addLayout(codec_layout)
113
+
114
+ # Add a list of key frames
115
+ self.frame_list = QListWidget()
116
+ setting_layout.addWidget(self.frame_list)
117
+ self.frame_list.itemSelectionChanged.connect(self.on_select_frame)
118
+ self.frame_list.itemDoubleClicked.connect(self.on_double_click_frame)
119
+ self.installEventFilter(self)
120
+
121
+ # Add spin boxes for linear / angular velocity and stop time
122
+ self.lin_vel_spinbox = QDoubleSpinBox()
123
+ self.lin_vel_spinbox.setPrefix("Linear Velocity (m/s): ")
124
+ self.lin_vel_spinbox.setRange(0, 1000)
125
+ self.lin_vel_spinbox.valueChanged.connect(self.set_frame_lin_vel)
126
+ setting_layout.addWidget(self.lin_vel_spinbox)
127
+
128
+ self.lin_ang_spinbox = QDoubleSpinBox()
129
+ self.lin_ang_spinbox.setPrefix("Angular Velocity (deg/s): ")
130
+ self.lin_ang_spinbox.setRange(0, 360)
131
+ self.lin_ang_spinbox.valueChanged.connect(self.set_frame_ang_vel)
132
+ setting_layout.addWidget(self.lin_ang_spinbox)
133
+
134
+ self.stop_time_spinbox = QDoubleSpinBox()
135
+ self.stop_time_spinbox.setPrefix("Stop Time: ")
136
+ self.stop_time_spinbox.setRange(0, 100)
137
+ self.stop_time_spinbox.valueChanged.connect(self.set_frame_stop_time)
138
+ setting_layout.addWidget(self.stop_time_spinbox)
139
+
140
+ setting_layout.setAlignment(QtCore.Qt.AlignTop)
141
+
142
+ # Create a dock widget for the settings
143
+ dock_widget = QDockWidget("Settings", self)
144
+ dock_widget.setWidget(QWidget())
145
+ dock_widget.widget().setLayout(setting_layout)
146
+ self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock_widget)
147
+
148
+ # Add the dock widget to the main layout
149
+ main_layout.addWidget(dock_widget)
150
+ self.dock = dock_widget
151
+
152
+
153
+ def update_video_path(self, path):
154
+ self.video_path = path
155
+
156
+ def add_key_frame(self):
157
+ view_matrix = self.glwidget.view_matrix
158
+ # Get camera pose in world frame
159
+ Twc = np.linalg.inv(view_matrix)
160
+ if self.key_frames:
161
+ prev = self.key_frames[-1]
162
+ key_frame = KeyFrame(Twc,
163
+ lin_vel=prev.lin_vel,
164
+ ang_vel=prev.ang_vel,
165
+ stop_time=prev.stop_time)
166
+ else:
167
+ key_frame = KeyFrame(Twc)
168
+ self.key_frames.append(key_frame)
169
+ # visualize this key frame using FrameItem
170
+ self.glwidget.add_item(key_frame.item)
171
+ # move the camera back to 0.5 meter, let the user see the frame
172
+ # self.glwidget.update_dist(0.5)
173
+ # Add the key frame to the Qt ListWidget
174
+ item = QListWidgetItem(f"Frame {len(self.key_frames)}")
175
+ self.frame_list.addItem(item)
176
+ self.frame_list.setCurrentRow(len(self.key_frames) - 1)
177
+
178
+ def del_key_frame(self):
179
+ current_index = self.frame_list.currentRow()
180
+ if current_index < 0:
181
+ return
182
+ self.glwidget.remove_item(self.key_frames[current_index].item)
183
+ self.key_frames.pop(current_index)
184
+ self.frame_list.itemSelectionChanged.disconnect(self.on_select_frame)
185
+ self.frame_list.takeItem(current_index)
186
+ self.frame_list.itemSelectionChanged.connect(self.on_select_frame)
187
+ self.on_select_frame()
188
+ # Update frame labels
189
+ for i in range(len(self.key_frames)):
190
+ self.frame_list.item(i).setText(f"Frame {i + 1}")
191
+
192
+ def on_select_frame(self):
193
+ current = self.frame_list.currentRow()
194
+ if current < 0:
195
+ return
196
+ for i, frame in enumerate(self.key_frames):
197
+ if i == current:
198
+ # Highlight the selected frame
199
+ frame.item.set_color('#FF0000')
200
+ frame.item.set_line_width(5)
201
+ # show current frame's parameters in the spinboxes
202
+ self.lin_vel_spinbox.setValue(frame.lin_vel)
203
+ self.lin_ang_spinbox.setValue(np.rad2deg(frame.ang_vel))
204
+ self.stop_time_spinbox.setValue(frame.stop_time)
205
+ else:
206
+ frame.item.set_color('#0000FF')
207
+ frame.item.set_line_width(3)
208
+
209
+ def set_frame_lin_vel(self, value):
210
+ current_index = self.frame_list.currentRow()
211
+ if current_index < 0:
212
+ return
213
+ self.key_frames[current_index].lin_vel = value
214
+
215
+ def set_frame_ang_vel(self, value):
216
+ current_index = self.frame_list.currentRow()
217
+ if current_index < 0:
218
+ return
219
+ self.key_frames[current_index].ang_vel = np.deg2rad(value)
220
+
221
+ def set_frame_stop_time(self, value):
222
+ current_index = self.frame_list.currentRow()
223
+ if current_index < 0:
224
+ return
225
+ self.key_frames[current_index].stop_time = value
226
+
227
+ def on_double_click_frame(self, item):
228
+ current_index = self.frame_list.row(item)
229
+ if current_index < 0:
230
+ return
231
+ Twc = self.key_frames[current_index].Twc
232
+ center, euler = recover_center_euler(Twc, self.glwidget.dist)
233
+ self.glwidget.set_cam_position(center=center,
234
+ euler=euler)
235
+
236
+
237
+ def create_frames(self):
238
+ """
239
+ Create the frames for playback by interpolating between key frames.
240
+ """
241
+ self.frames = []
242
+ dt = 1 / float(self.update_interval)
243
+ for i in range(len(self.key_frames) - 1):
244
+ current_frame = self.key_frames[i]
245
+ if current_frame.stop_time > 0:
246
+ num_steps = int(current_frame.stop_time / dt)
247
+ for j in range(num_steps):
248
+ self.frames.append([i, current_frame.Twc])
249
+ next_frame = self.key_frames[i + 1]
250
+ Ts = q3d.interpolate_pose(current_frame.Twc, next_frame.Twc,
251
+ current_frame.lin_vel,
252
+ current_frame.ang_vel,
253
+ dt)
254
+ for T in Ts:
255
+ self.frames.append([i, T])
256
+
257
+ print(f"Total frames: {len(self.frames)}")
258
+ print(f"Total time: {len(self.frames) * dt:.2f} seconds")
259
+
260
+ def toggle_playback(self):
261
+ if self.is_playing:
262
+ self.stop_playback()
263
+ else:
264
+ self.start_playback()
265
+
266
+ def start_playback(self):
267
+ if self.key_frames:
268
+ self.create_frames()
269
+ self.current_frame_index = 0
270
+ self.timer.start(self.update_interval) # Adjust the interval as needed
271
+ self.is_playing = True
272
+ self.play_button.setStyleSheet("")
273
+ self.play_button.setText("Stop")
274
+ self.record_checkbox.setEnabled(False)
275
+ if self.is_recording is True:
276
+ self.start_recording()
277
+
278
+ def stop_playback(self):
279
+ self.timer.stop()
280
+ self.is_playing = False
281
+ self.play_button.setStyleSheet("")
282
+ self.play_button.setText("Play")
283
+ self.record_checkbox.setEnabled(True)
284
+ self.frame_list.setCurrentRow(len(self.key_frames) - 1)
285
+ if self.is_recording:
286
+ self.stop_recording()
287
+
288
+ def play_frames(self):
289
+ """
290
+ callback function for the timer to play the frames
291
+ """
292
+ # play the frames
293
+ if self.current_frame_index < len(self.frames):
294
+ key_id, Tcw = self.frames[self.current_frame_index]
295
+ self.glwidget.set_view_matrix(np.linalg.inv(Tcw))
296
+ self.frame_list.setCurrentRow(key_id)
297
+ self.current_frame_index += 1
298
+ if self.is_recording:
299
+ self.record_frame()
300
+ else:
301
+ self.stop_playback()
302
+
303
+ def toggle_recording(self, state):
304
+ if state == 2:
305
+ self.is_recording = True
306
+ else:
307
+ self.is_recording = False
308
+
309
+ def start_recording(self):
310
+ self.is_recording = True
311
+ self.frames_to_record = []
312
+ video_path = self.video_path_edit.text()
313
+ codec = self.codec_combo.currentText()
314
+ self.play_button.setStyleSheet("background-color: red")
315
+ self.play_button.setText("Recording")
316
+ self.writer = imageio.get_writer(video_path,
317
+ fps=self.update_interval,
318
+ codec=codec) # quality=10
319
+ # disable the all the frame_item while recording
320
+ for frame in self.key_frames:
321
+ frame.item.hide()
322
+ self.dock.hide() # Hide the dock while recording
323
+
324
+ def stop_recording(self, save_movie=True):
325
+ self.is_recording = False
326
+ self.record_checkbox.setChecked(False)
327
+ # enable the all the frame_item after recording
328
+ for frame in self.key_frames:
329
+ frame.item.show()
330
+ if hasattr(self, 'writer') and save_movie:
331
+ self.writer.close()
332
+ self.show_save_message()
333
+ self.dock.show() # Show the dock when recording stops
334
+
335
+ def show_save_message(self):
336
+ msg_box = QMessageBox()
337
+ msg_box.setIcon(QMessageBox.Information)
338
+ msg_box.setWindowTitle("Video Saved")
339
+ msg_box.setText(f"Video saved to {self.video_path_edit.text()}")
340
+ msg_box.setStandardButtons(QMessageBox.Ok)
341
+ msg_box.exec()
342
+
343
+ def record_frame(self):
344
+ frame = self.glwidget.capture_frame()
345
+ # make sure the frame size is multiple of 16
346
+ height, width, _ = frame.shape
347
+ if height % 16 != 0 or width % 16 != 0:
348
+ frame = frame[:-(height % 16), :-(width % 16), :]
349
+ frame = np.ascontiguousarray(frame)
350
+ self.frames_to_record.append(frame)
351
+ try:
352
+ self.writer.append_data(frame)
353
+ except Exception as e:
354
+ print("Don't change the window size during recording.")
355
+ self.stop_recording(False) # Stop recording without saving
356
+ self.stop_playback()
357
+
358
+ def eventFilter(self, obj, event):
359
+ if event.type() == QtCore.QEvent.KeyPress:
360
+ if event.key() == QtCore.Qt.Key_Delete:
361
+ self.del_key_frame()
362
+ return True
363
+ return super().eventFilter(obj, event)
364
+
365
+ def dragEnterEvent(self, event):
366
+ if event.mimeData().hasUrls():
367
+ event.accept()
368
+ else:
369
+ event.ignore()
370
+
371
+ def dropEvent(self, event):
372
+ """
373
+ Overwrite the drop event to open the cloud file.
374
+ """
375
+ self.progress_dialog = ProgressDialog(self)
376
+ self.progress_dialog.show()
377
+ files = event.mimeData().urls()
378
+ self.progress_thread = FileLoaderThread(self, files)
379
+ self['cloud'].load(files[0].toLocalFile(), append=False)
380
+ self.progress_thread.progress.connect(self.file_loading_progress)
381
+ self.progress_thread.finished.connect(self.file_loading_finished)
382
+ self.progress_thread.start()
383
+
384
+ def file_loading_progress(self, value):
385
+ self.progress_dialog.set_value(value)
386
+
387
+ def file_loading_finished(self):
388
+ self.progress_dialog.close()
389
+
390
+ def open_cloud_file(self, file, append=False):
391
+ cloud_item = self['cloud']
392
+ if cloud_item is None:
393
+ print("Can't find clouditem.")
394
+ return
395
+ cloud = cloud_item.load(file, append=append)
396
+ center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
397
+ self.glwidget.set_cam_position(center=center)
398
+
399
+ def main():
400
+ import argparse
401
+ parser = argparse.ArgumentParser()
402
+ parser.add_argument("--path", help="the cloud file path")
403
+ args = parser.parse_args()
404
+ app = q3d.QApplication(['Film Maker'])
405
+ viewer = CMMViewer(name='Film Maker', update_interval=30)
406
+ cloud_item = q3d.CloudIOItem(size=0.1, point_type='SPHERE', alpha=0.5, depth_test=True)
407
+ grid_item = q3d.GridItem(size=1000, spacing=20)
408
+
409
+ viewer.add_items(
410
+ {'cloud': cloud_item, 'grid': grid_item})
411
+
412
+ if args.path:
413
+ pcd_fn = args.path
414
+ viewer.open_cloud_file(pcd_fn)
415
+
416
+ viewer.show()
417
+ app.exec()
418
+
419
+
420
+ if __name__ == '__main__':
421
+ 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})