q3dviewer 1.0.3__py3-none-any.whl → 1.0.5__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.
Files changed (43) hide show
  1. q3dviewer/__init__.py +5 -0
  2. q3dviewer/base_glwidget.py +256 -0
  3. q3dviewer/base_item.py +57 -0
  4. q3dviewer/custom_items/__init__.py +9 -0
  5. q3dviewer/custom_items/axis_item.py +148 -0
  6. q3dviewer/custom_items/cloud_io_item.py +79 -0
  7. q3dviewer/custom_items/cloud_item.py +314 -0
  8. q3dviewer/custom_items/frame_item.py +194 -0
  9. q3dviewer/custom_items/gaussian_item.py +254 -0
  10. q3dviewer/custom_items/grid_item.py +88 -0
  11. q3dviewer/custom_items/image_item.py +172 -0
  12. q3dviewer/custom_items/line_item.py +120 -0
  13. q3dviewer/custom_items/text_item.py +63 -0
  14. q3dviewer/gau_io.py +0 -0
  15. q3dviewer/glwidget.py +131 -0
  16. q3dviewer/shaders/cloud_frag.glsl +28 -0
  17. q3dviewer/shaders/cloud_vert.glsl +72 -0
  18. q3dviewer/shaders/gau_frag.glsl +42 -0
  19. q3dviewer/shaders/gau_prep.glsl +249 -0
  20. q3dviewer/shaders/gau_vert.glsl +77 -0
  21. q3dviewer/shaders/sort_by_key.glsl +56 -0
  22. q3dviewer/tools/__init__.py +1 -0
  23. q3dviewer/tools/cloud_viewer.py +123 -0
  24. q3dviewer/tools/example_viewer.py +47 -0
  25. q3dviewer/tools/gaussian_viewer.py +60 -0
  26. q3dviewer/tools/lidar_calib.py +294 -0
  27. q3dviewer/tools/lidar_cam_calib.py +314 -0
  28. q3dviewer/tools/ros_viewer.py +85 -0
  29. q3dviewer/utils/__init__.py +4 -0
  30. q3dviewer/utils/cloud_io.py +323 -0
  31. q3dviewer/utils/convert_ros_msg.py +49 -0
  32. q3dviewer/utils/gl_helper.py +40 -0
  33. q3dviewer/utils/maths.py +168 -0
  34. q3dviewer/utils/range_slider.py +86 -0
  35. q3dviewer/viewer.py +58 -0
  36. q3dviewer-1.0.5.dist-info/LICENSE +21 -0
  37. {q3dviewer-1.0.3.dist-info → q3dviewer-1.0.5.dist-info}/METADATA +7 -4
  38. q3dviewer-1.0.5.dist-info/RECORD +41 -0
  39. q3dviewer-1.0.5.dist-info/top_level.txt +1 -0
  40. q3dviewer-1.0.3.dist-info/RECORD +0 -5
  41. q3dviewer-1.0.3.dist-info/top_level.txt +0 -1
  42. {q3dviewer-1.0.3.dist-info → q3dviewer-1.0.5.dist-info}/WHEEL +0 -0
  43. {q3dviewer-1.0.3.dist-info → q3dviewer-1.0.5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,123 @@
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, QProgressBar, QDialog, QLabel
11
+ from PySide6.QtCore import QThread, Signal
12
+
13
+
14
+ class ProgressDialog(QDialog):
15
+ def __init__(self, parent=None):
16
+ super().__init__(parent)
17
+ self.setWindowTitle("Loading Cloud File")
18
+ self.setModal(True)
19
+ self.progress_bar = QProgressBar(self)
20
+ self.file_label = QLabel(self)
21
+ layout = QVBoxLayout()
22
+ layout.addWidget(self.file_label)
23
+ layout.addWidget(self.progress_bar)
24
+ self.setLayout(layout)
25
+
26
+ def set_value(self, value):
27
+ self.progress_bar.setValue(value)
28
+
29
+ def set_file_name(self, file_name):
30
+ self.file_label.setText(f"Loading: {file_name}")
31
+
32
+ def closeEvent(self, event):
33
+ if self.parent().progress_thread and self.parent().progress_thread.isRunning():
34
+ event.ignore()
35
+ else:
36
+ event.accept()
37
+
38
+
39
+ class FileLoaderThread(QThread):
40
+ progress = Signal(int)
41
+ finished = Signal()
42
+
43
+ def __init__(self, viewer, files):
44
+ super().__init__()
45
+ self.viewer = viewer
46
+ self.files = files
47
+
48
+ def run(self):
49
+ cloud_item = self.viewer['cloud']
50
+ for i, url in enumerate(self.files):
51
+ file_path = url.toLocalFile()
52
+ self.viewer.progress_dialog.set_file_name(file_path)
53
+ cloud = cloud_item.load(file_path, append=(i > 0))
54
+ center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
55
+ self.viewer.glwidget.set_cam_position(pos=center)
56
+ self.progress.emit(int((i + 1) / len(self.files) * 100))
57
+ self.finished.emit()
58
+
59
+
60
+ class CloudViewer(q3d.Viewer):
61
+ def __init__(self, **kwargs):
62
+ super(CloudViewer, self).__init__(**kwargs)
63
+ self.setAcceptDrops(True)
64
+
65
+ def dragEnterEvent(self, event):
66
+ if event.mimeData().hasUrls():
67
+ event.accept()
68
+ else:
69
+ event.ignore()
70
+
71
+ def dropEvent(self, event):
72
+ """
73
+ Overwrite the drop event to open the cloud file.
74
+ """
75
+ self.progress_dialog = ProgressDialog(self)
76
+ self.progress_dialog.show()
77
+ files = event.mimeData().urls()
78
+ self.progress_thread = FileLoaderThread(self, files)
79
+ self['cloud'].load(files[0].toLocalFile(), append=False)
80
+ self.progress_thread.progress.connect(self.file_loading_progress)
81
+ self.progress_thread.finished.connect(self.file_loading_finished)
82
+ self.progress_thread.start()
83
+
84
+ def file_loading_progress(self, value):
85
+ self.progress_dialog.set_value(value)
86
+
87
+ def file_loading_finished(self):
88
+ self.progress_dialog.close()
89
+
90
+ def open_cloud_file(self, file, append=False):
91
+ cloud_item = self['cloud']
92
+ if cloud_item is None:
93
+ print("Can't find clouditem.")
94
+ return
95
+ cloud = cloud_item.load(file, append=append)
96
+ center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
97
+ self.glwidget.set_cam_position(pos=center)
98
+
99
+
100
+ def main():
101
+ import argparse
102
+ parser = argparse.ArgumentParser()
103
+ parser.add_argument("--path", help="the cloud file path")
104
+ args = parser.parse_args()
105
+ app = q3d.QApplication(['Cloud Viewer'])
106
+ viewer = CloudViewer(name='Cloud Viewer')
107
+ cloud_item = q3d.CloudIOItem(size=1, alpha=0.1)
108
+ axis_item = q3d.AxisItem(size=0.5, width=5)
109
+ grid_item = q3d.GridItem(size=1000, spacing=20)
110
+
111
+ viewer.add_items(
112
+ {'cloud': cloud_item, 'grid': grid_item, 'axis': axis_item})
113
+
114
+ if args.path:
115
+ pcd_fn = args.path
116
+ viewer.open_cloud_file(pcd_fn)
117
+
118
+ viewer.show()
119
+ app.exec()
120
+
121
+
122
+ if __name__ == '__main__':
123
+ main()
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import numpy as np
4
+ import q3dviewer as q3d
5
+ from PySide6.QtCore import QTimer
6
+
7
+ i = 0.0
8
+
9
+ def update(viewer):
10
+ global i
11
+ i += 0.05
12
+ new = np.array([np.sin(i) * i, np.cos(i) * i, i])
13
+ viewer['traj'].set_data(new, append=True)
14
+ viewer.update()
15
+ return i
16
+
17
+
18
+ def main():
19
+ # add a qt application
20
+ app = q3d.QApplication([])
21
+
22
+ # create opengl items
23
+ axis_item = q3d.AxisItem(size=0.5, width=5)
24
+ grid_item = q3d.GridItem(size=10, spacing=1)
25
+ traj_item = q3d.LineItem(width=2)
26
+
27
+ # create viewer
28
+ viewer = q3d.Viewer(name='example')
29
+
30
+ # add all items to viewer
31
+ viewer.add_items({
32
+ 'grid': grid_item,
33
+ 'axis': axis_item,
34
+ 'traj': traj_item})
35
+
36
+ # dynamic update by timer
37
+ timer = QTimer()
38
+ timer.timeout.connect(lambda: update(viewer))
39
+ timer.start(100)
40
+
41
+ # show viewer and start application
42
+ viewer.show()
43
+ app.exec()
44
+
45
+
46
+ if __name__ == '__main__':
47
+ main()
@@ -0,0 +1,60 @@
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 q3dviewer.utils.cloud_io import load_gs, rotate_gaussian
11
+
12
+
13
+ class GuassianViewer(q3d.Viewer):
14
+ def __init__(self, **kwds):
15
+ super(GuassianViewer, self).__init__(**kwds)
16
+ self.setAcceptDrops(True)
17
+
18
+ def dragEnterEvent(self, event):
19
+ if event.mimeData().hasUrls():
20
+ event.accept()
21
+ else:
22
+ event.ignore()
23
+
24
+ def dropEvent(self, event):
25
+ for url in event.mimeData().urls():
26
+ file_path = url.toLocalFile()
27
+ self.open_gs_file(file_path)
28
+
29
+ def open_gs_file(self, file):
30
+ gau_item = self['gaussian']
31
+ if gau_item is None:
32
+ print("Can't find gaussianitem")
33
+ return
34
+
35
+ print("Try to load %s ..." % file)
36
+ gs = load_gs(file)
37
+ # convert camera optical frame (b) to camera frame (c).
38
+ Rcb = np.array([[0, -1, 0],
39
+ [0, 0, -1],
40
+ [1, 0, 0]]).T
41
+ gs = rotate_gaussian(Rcb, gs)
42
+ gs_data = gs.view(np.float32).reshape(gs.shape[0], -1)
43
+ gau_item.set_data(gs_data=gs_data)
44
+
45
+
46
+ def main():
47
+ app = q3d.QApplication(['Guassian Viewer'])
48
+ viewer = GuassianViewer(name='Guassian Viewer')
49
+
50
+ grid_item = q3d.GridItem(size=1000, spacing=20)
51
+ gau_item = q3d.GaussianItem()
52
+
53
+ viewer.add_items({'grid': grid_item, 'gaussian': gau_item})
54
+
55
+ viewer.show()
56
+ app.exec()
57
+
58
+
59
+ if __name__ == '__main__':
60
+ main()
@@ -0,0 +1,294 @@
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
+ from sensor_msgs.msg import PointCloud2
9
+ import rospy
10
+ import numpy as np
11
+ import argparse
12
+ import q3dviewer as q3d
13
+ from PySide6.QtWidgets import QLabel, QLineEdit, QDoubleSpinBox, \
14
+ QSpinBox, QWidget, QVBoxLayout, QHBoxLayout, QPushButton
15
+ from PySide6 import QtCore
16
+ from q3dviewer.utils.convert_ros_msg import convert_pointcloud2_msg
17
+
18
+ try:
19
+ import open3d as o3d
20
+ except ImportError:
21
+ print("\033[91mWarning: open3d is not installed. Please install it to use this tool.\033[0m")
22
+ print("\033[93mYou can install it using: pip install open3d\033[0m")
23
+ exit(1)
24
+
25
+
26
+ viewer = None
27
+ cloud0_accum = None
28
+ clouds0 = []
29
+ cloud1_accum = None
30
+ clouds1 = []
31
+
32
+
33
+ class CustomDoubleSpinBox(QDoubleSpinBox):
34
+ def __init__(self, decimals=4, *args, **kwargs):
35
+ super().__init__(*args, **kwargs)
36
+ self.decimals = decimals
37
+ self.setDecimals(self.decimals)
38
+
39
+ def textFromValue(self, value):
40
+ return f"{value:.{self.decimals}f}"
41
+
42
+ def valueFromText(self, text):
43
+ return float(text)
44
+
45
+
46
+ class ViewerWithPanel(q3d.Viewer):
47
+ def __init__(self, **kwargs):
48
+ self.t01 = np.array([0, 0, 0])
49
+ self.R01 = np.eye(3)
50
+ self.cloud_num = 10
51
+ self.radius = 0.2
52
+ super().__init__(**kwargs)
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
+
60
+ # Create a vertical layout for the settings
61
+ setting_layout = QVBoxLayout()
62
+
63
+ # Add XYZ spin boxes
64
+ label_xyz = QLabel("Set XYZ:")
65
+ setting_layout.addWidget(label_xyz)
66
+ self.box_x = CustomDoubleSpinBox()
67
+ self.box_x.setSingleStep(0.01)
68
+ setting_layout.addWidget(self.box_x)
69
+ self.box_y = CustomDoubleSpinBox()
70
+ self.box_y.setSingleStep(0.01)
71
+ setting_layout.addWidget(self.box_y)
72
+ self.box_z = CustomDoubleSpinBox()
73
+ self.box_z.setSingleStep(0.01)
74
+ setting_layout.addWidget(self.box_z)
75
+ self.box_x.setRange(-100.0, 100.0)
76
+ self.box_y.setRange(-100.0, 100.0)
77
+ self.box_z.setRange(-100.0, 100.0)
78
+
79
+ # Add RPY spin boxes
80
+ label_rpy = QLabel("Set Roll-Pitch-Yaw:")
81
+ setting_layout.addWidget(label_rpy)
82
+ self.box_roll = CustomDoubleSpinBox()
83
+ self.box_roll.setSingleStep(0.01)
84
+ setting_layout.addWidget(self.box_roll)
85
+ self.box_pitch = CustomDoubleSpinBox()
86
+ self.box_pitch.setSingleStep(0.01)
87
+ setting_layout.addWidget(self.box_pitch)
88
+ self.box_yaw = CustomDoubleSpinBox()
89
+ self.box_yaw.setSingleStep(0.01)
90
+ setting_layout.addWidget(self.box_yaw)
91
+ self.box_roll.setRange(-np.pi, np.pi)
92
+ self.box_pitch.setRange(-np.pi, np.pi)
93
+ self.box_yaw.setRange(-np.pi, np.pi)
94
+
95
+ label_cloud_num = QLabel("Set cloud frame number:")
96
+ setting_layout.addWidget(label_cloud_num)
97
+ self.box_cloud_num = QSpinBox()
98
+ self.box_cloud_num.setValue(self.cloud_num)
99
+ self.box_cloud_num.setRange(1, 100)
100
+ self.box_cloud_num.valueChanged.connect(self.update_cloud_num)
101
+ setting_layout.addWidget(self.box_cloud_num)
102
+
103
+ label_trans_quat = QLabel("The lidar0-lidar1 trans and quat:")
104
+ setting_layout.addWidget(label_trans_quat)
105
+ self.line_trans = QLineEdit()
106
+ self.line_trans.setReadOnly(True)
107
+ setting_layout.addWidget(self.line_trans)
108
+ self.line_quat = QLineEdit()
109
+ self.line_quat.setReadOnly(True)
110
+ setting_layout.addWidget(self.line_quat)
111
+
112
+ label_matching_setting = QLabel("Set matching radius:")
113
+ setting_layout.addWidget(label_matching_setting)
114
+ self.box_radius = CustomDoubleSpinBox(decimals=2)
115
+ self.box_radius.setSingleStep(0.1)
116
+ self.box_radius.setRange(0.1, 3.0)
117
+ self.box_radius.setValue(self.radius)
118
+ setting_layout.addWidget(self.box_radius)
119
+
120
+ self.icp_button = QPushButton("Auto Scan Matching")
121
+ self.icp_button.clicked.connect(self.perform_matching)
122
+ setting_layout.addWidget(self.icp_button)
123
+
124
+ self.line_trans.setText(
125
+ f"[{self.t01[0]:.6f}, {self.t01[1]:.6f}, {self.t01[2]:.6f}]")
126
+ quat = q3d.matrix_to_quaternion(self.R01)
127
+ self.line_quat.setText(
128
+ f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
129
+
130
+ # Connect spin boxes to methods
131
+ self.box_x.valueChanged.connect(self.update_xyz)
132
+ self.box_y.valueChanged.connect(self.update_xyz)
133
+ self.box_z.valueChanged.connect(self.update_xyz)
134
+ self.box_roll.valueChanged.connect(self.update_rpy)
135
+ self.box_pitch.valueChanged.connect(self.update_rpy)
136
+ self.box_yaw.valueChanged.connect(self.update_rpy)
137
+
138
+ # Add a stretch to push the widgets to the top
139
+ setting_layout.addStretch(1)
140
+
141
+ self.glwidget = q3d.GLWidget()
142
+ 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()
151
+
152
+ def update_radius(self):
153
+ self.radius = self.box_radius.value()
154
+
155
+ def update_cloud_num(self):
156
+ self.cloud_num = self.box_cloud_num.value()
157
+
158
+ def update_xyz(self):
159
+ x = self.box_x.value()
160
+ y = self.box_y.value()
161
+ z = self.box_z.value()
162
+ self.t01 = np.array([x, y, z])
163
+ self.line_trans.setText(f"[{x:.6f}, {y:.6f}, {z:.6f}]")
164
+
165
+ def update_rpy(self):
166
+ roll = self.box_roll.value()
167
+ pitch = self.box_pitch.value()
168
+ yaw = self.box_yaw.value()
169
+ self.R01 = q3d.euler_to_matrix(np.array([roll, pitch, yaw]))
170
+ quat = q3d.matrix_to_quaternion(self.R01)
171
+ self.line_quat.setText(
172
+ f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
173
+
174
+ def perform_matching(self):
175
+ global cloud0_accum, cloud1_accum
176
+
177
+ if o3d is None:
178
+ print("Error: open3d is not available. Cannot perform matching.")
179
+ return
180
+
181
+ if cloud0_accum is not None and cloud1_accum is not None:
182
+ # Convert to Open3D point clouds
183
+ cloud0_o3d = o3d.geometry.PointCloud()
184
+ cloud0_o3d.points = o3d.utility.Vector3dVector(cloud0_accum['xyz'])
185
+ cloud1_o3d = o3d.geometry.PointCloud()
186
+ cloud1_o3d.points = o3d.utility.Vector3dVector(cloud1_accum['xyz'])
187
+ voxel_size = 0.1
188
+ cloud0_o3d = cloud0_o3d.voxel_down_sample(voxel_size)
189
+ cloud1_o3d = cloud1_o3d.voxel_down_sample(voxel_size)
190
+ cloud0_o3d.estimate_normals(
191
+ search_param=o3d.geometry.KDTreeSearchParamHybrid
192
+ (radius=self.radius, max_nn=30))
193
+ trans_init = np.eye(4)
194
+ trans_init[:3, :3] = self.R01
195
+ trans_init[:3, 3] = self.t01
196
+
197
+ criteria = o3d.pipelines.registration.ICPConvergenceCriteria(
198
+ relative_fitness=1e-4,
199
+ max_iteration=50)
200
+
201
+ # Auto Scan Matching
202
+ reg_p2p = o3d.pipelines.registration.registration_icp(
203
+ cloud1_o3d, cloud0_o3d, self.radius, trans_init,
204
+ o3d.pipelines.registration.
205
+ TransformationEstimationPointToPlane(),
206
+ criteria)
207
+
208
+ transformation_icp = reg_p2p.transformation
209
+ # print("ICP Transformation:", transformation_icp)
210
+
211
+ # Update R01 and t01 with ICP result
212
+ R01 = transformation_icp[:3, :3]
213
+ t01 = transformation_icp[:3, 3]
214
+
215
+ # Update the UI with new values
216
+ quat = q3d.matrix_to_quaternion(R01)
217
+ rpy = q3d.matrix_to_euler(R01)
218
+ self.box_roll.setValue(rpy[0])
219
+ self.box_pitch.setValue(rpy[1])
220
+ self.box_yaw.setValue(rpy[2])
221
+ self.box_x.setValue(t01[0])
222
+ self.box_y.setValue(t01[1])
223
+ self.box_z.setValue(t01[2])
224
+ self.line_trans.setText(
225
+ f"[{t01[0]:.6f}, {t01[1]:.6f}, {t01[2]:.6f}]")
226
+ self.line_quat.setText(
227
+ f"[{quat[0]: .6f}, {quat[1]: .6f}, {quat[2]: .6f}, {quat[3]: .6f}]")
228
+ self.t01 = t01
229
+ self.R01 = R01
230
+ print("Matching results")
231
+ print(f"translation: [{t01[0]:.6f}, {t01[1]:.6f}, {t01[2]:.6f}]")
232
+ print(
233
+ f"Roll-Pitch-Yaw: [{rpy[0]:.6f}, {rpy[1]:.6f}, {rpy[2]:.6f}]")
234
+ print(
235
+ f"Quaternion: [{quat[0]: .6f}, {quat[1]: .6f}, {quat[2]: .6f}, {quat[3]: .6f}]")
236
+
237
+
238
+ def scan0_cb(data):
239
+ global viewer, clouds0, cloud0_accum
240
+ cloud, _, _ = convert_pointcloud2_msg(data)
241
+ while len(clouds0) > viewer.cloud_num:
242
+ clouds0.pop(0)
243
+ clouds0.append(cloud)
244
+ cloud0_accum = np.concatenate(clouds0)
245
+ viewer['scan0'].set_data(data=cloud0_accum)
246
+
247
+
248
+ def scan1_cb(data):
249
+ global viewer, clouds1, cloud1_accum
250
+ cloud, _, _ = convert_pointcloud2_msg(data)
251
+ while len(clouds1) > viewer.cloud_num:
252
+ clouds1.pop(0)
253
+ clouds1.append(cloud)
254
+ cloud1_accum = np.concatenate(clouds1)
255
+ cloud0_accum_new = cloud1_accum.copy()
256
+ cloud0_accum_new['xyz'] = (
257
+ viewer.R01 @ cloud1_accum['xyz'].T + viewer.t01[:, np.newaxis]).T
258
+
259
+ viewer['scan1'].set_data(data=cloud0_accum_new)
260
+
261
+
262
+ def main():
263
+ global viewer
264
+ # Set up argument parser
265
+ parser = argparse.ArgumentParser(
266
+ description="Configure topic names for LiDAR, image, and camera info.")
267
+ parser.add_argument('--lidar0', type=str, default='/lidar0',
268
+ help="Topic name for LiDAR data")
269
+ parser.add_argument('--lidar1', type=str, default='/lidar1',
270
+ help="Topic name for camera image data")
271
+ args = parser.parse_args()
272
+
273
+ 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))
276
+ scan0_item = q3d.CloudItem(
277
+ size=2, alpha=1, color_mode='FLAT', color='#ff0000')
278
+ scan1_item = q3d.CloudItem(
279
+ size=2, alpha=1, color_mode='FLAT', color='#00ff00')
280
+ viewer.add_items(
281
+ {'scan0': scan0_item, 'scan1': scan1_item, 'grid': grid_item})
282
+
283
+ rospy.init_node('lidar_calib', anonymous=True)
284
+
285
+ # Use topic names from arguments
286
+ rospy.Subscriber(args.lidar0, PointCloud2, scan0_cb, queue_size=1)
287
+ rospy.Subscriber(args.lidar1, PointCloud2, scan1_cb, queue_size=1)
288
+
289
+ viewer.show()
290
+ app.exec()
291
+
292
+
293
+ if __name__ == '__main__':
294
+ main()