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,314 @@
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, Image, CameraInfo
9
+ import rospy
10
+ import numpy as np
11
+ import q3dviewer as q3d
12
+ from PySide6.QtWidgets import QLabel, QLineEdit, QDoubleSpinBox, \
13
+ QSpinBox, QWidget, QVBoxLayout, QHBoxLayout, QCheckBox
14
+ from PySide6 import QtCore
15
+ import rospy
16
+ import cv2
17
+ import argparse
18
+ from q3dviewer.utils.convert_ros_msg import convert_pointcloud2_msg, convert_image_msg
19
+
20
+ clouds = []
21
+ remap_info = None
22
+ K = None
23
+ cloud_accum_color = None
24
+ cloud_accum = None
25
+
26
+ class CustomDoubleSpinBox(QDoubleSpinBox):
27
+ def __init__(self, decimals=4, *args, **kwargs):
28
+ self.decimals = decimals
29
+ super().__init__(*args, **kwargs)
30
+ self.setDecimals(self.decimals) # Set the number of decimal places
31
+
32
+ def textFromValue(self, value):
33
+ return f"{value:.{self.decimals}f}"
34
+
35
+ def valueFromText(self, text):
36
+ return float(text)
37
+
38
+
39
+ class ViewerWithPanel(q3d.Viewer):
40
+ def __init__(self, **kwargs):
41
+ # b: camera body frame
42
+ # c: camera image frame
43
+ # l: lidar frame
44
+ self.Rbl = np.eye(3)
45
+ self.Rbl = q3d.euler_to_matrix(np.array([0.0, 0.0, 0.0]))
46
+ self.Rcb = np.array([[0, -1, 0],
47
+ [0, 0, -1],
48
+ [1, 0, 0]])
49
+ self.tbl = np.array([0.0, 0.0, 0.0])
50
+ self.tcl = self.Rcb @ self.tbl
51
+ self.Rcl = self.Rcb @ self.Rbl
52
+ self.psize = 2
53
+ self.cloud_num = 1
54
+ self.en_rgb = False
55
+ super().__init__(**kwargs)
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)
62
+
63
+ # Create a vertical layout for the settings
64
+ setting_layout = QVBoxLayout()
65
+
66
+ # Add a checkbox for RGB
67
+ self.checkbox_rgb = QCheckBox("Enable RGB Cloud")
68
+ self.checkbox_rgb.setChecked(False)
69
+ setting_layout.addWidget(self.checkbox_rgb)
70
+ self.checkbox_rgb.stateChanged.connect(
71
+ self.checkbox_changed)
72
+
73
+ # Add XYZ spin boxes
74
+ label_xyz = QLabel("Set XYZ:")
75
+ setting_layout.addWidget(label_xyz)
76
+ self.box_x = CustomDoubleSpinBox()
77
+ self.box_x.setSingleStep(0.01)
78
+ setting_layout.addWidget(self.box_x)
79
+ self.box_y = CustomDoubleSpinBox()
80
+ self.box_y.setSingleStep(0.01)
81
+ setting_layout.addWidget(self.box_y)
82
+ self.box_z = CustomDoubleSpinBox()
83
+ self.box_z.setSingleStep(0.01)
84
+ setting_layout.addWidget(self.box_z)
85
+ self.box_x.setRange(-100.0, 100.0)
86
+ self.box_y.setRange(-100.0, 100.0)
87
+ self.box_z.setRange(-100.0, 100.0)
88
+ self.box_x.setValue(self.tbl[0])
89
+ self.box_y.setValue(self.tbl[1])
90
+ self.box_z.setValue(self.tbl[2])
91
+
92
+ # Add RPY spin boxes
93
+ label_rpy = QLabel("Set Roll-Pitch-Yaw:")
94
+ setting_layout.addWidget(label_rpy)
95
+ self.box_roll = CustomDoubleSpinBox()
96
+ self.box_roll.setSingleStep(0.01)
97
+ setting_layout.addWidget(self.box_roll)
98
+ self.box_pitch = CustomDoubleSpinBox()
99
+ self.box_pitch.setSingleStep(0.01)
100
+ setting_layout.addWidget(self.box_pitch)
101
+ self.box_yaw = CustomDoubleSpinBox()
102
+ self.box_yaw.setSingleStep(0.01)
103
+ setting_layout.addWidget(self.box_yaw)
104
+ self.box_roll.setRange(-np.pi, np.pi)
105
+ self.box_pitch.setRange(-np.pi, np.pi)
106
+ self.box_yaw.setRange(-np.pi, np.pi)
107
+ rpy = q3d.matrix_to_euler(self.Rbl)
108
+ self.box_roll.setValue(rpy[0])
109
+ self.box_pitch.setValue(rpy[1])
110
+ self.box_yaw.setValue(rpy[2])
111
+
112
+ label_psize = QLabel("Set point size:")
113
+ setting_layout.addWidget(label_psize)
114
+ self.box_psize = QSpinBox()
115
+ self.box_psize.setValue(self.psize)
116
+ self.box_psize.setRange(0, 5)
117
+ self.box_psize.valueChanged.connect(self.update_point_size)
118
+ setting_layout.addWidget(self.box_psize)
119
+
120
+ label_cloud_num = QLabel("Set cloud frame number:")
121
+ setting_layout.addWidget(label_cloud_num)
122
+ self.box_cloud_num = QSpinBox()
123
+ self.box_cloud_num.setValue(self.cloud_num)
124
+ self.box_cloud_num.setRange(1, 100)
125
+ self.box_cloud_num.valueChanged.connect(self.update_cloud_num)
126
+ setting_layout.addWidget(self.box_cloud_num)
127
+
128
+ label_res = QLabel("The cam-lidar quat and translation:")
129
+ setting_layout.addWidget(label_res)
130
+ self.line_quat = QLineEdit()
131
+ self.line_quat.setReadOnly(True)
132
+ setting_layout.addWidget(self.line_quat)
133
+ self.line_trans = QLineEdit()
134
+ self.line_trans.setReadOnly(True)
135
+ setting_layout.addWidget(self.line_trans)
136
+
137
+ self.line_trans.setText(
138
+ f"[{self.tcl[0]:.6f}, {self.tcl[1]:.6f}, {self.tcl[2]:.6f}]")
139
+ quat = q3d.matrix_to_quaternion(self.Rcl)
140
+ self.line_quat.setText(
141
+ f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
142
+
143
+ # Connect spin boxes to methods
144
+ self.box_x.valueChanged.connect(self.update_xyz)
145
+ self.box_y.valueChanged.connect(self.update_xyz)
146
+ self.box_z.valueChanged.connect(self.update_xyz)
147
+ self.box_roll.valueChanged.connect(self.update_rpy)
148
+ self.box_pitch.valueChanged.connect(self.update_rpy)
149
+ self.box_yaw.valueChanged.connect(self.update_rpy)
150
+
151
+ # Add a stretch to push the widgets to the top
152
+ setting_layout.addStretch(1)
153
+
154
+ self.glwidget = q3d.GLWidget()
155
+ main_layout.addLayout(setting_layout)
156
+ main_layout.addWidget(self.glwidget, 1)
157
+
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
+
165
+ def update_point_size(self):
166
+ self.psize = self.box_psize.value()
167
+
168
+ def update_cloud_num(self):
169
+ self.cloud_num = self.box_cloud_num.value()
170
+
171
+ def update_xyz(self):
172
+ x = self.box_x.value()
173
+ y = self.box_y.value()
174
+ z = self.box_z.value()
175
+ self.tbl = np.array([x, y, z])
176
+ self.tcl = self.Rcb @ self.tbl
177
+ x, y, z = self.tcl
178
+ self.line_trans.setText(f"[{x:.6f}, {y:.6f}, {x:.6f}]")
179
+
180
+ def update_rpy(self):
181
+ roll = self.box_roll.value()
182
+ pitch = self.box_pitch.value()
183
+ yaw = self.box_yaw.value()
184
+ self.Rbl = q3d.euler_to_matrix(np.array([roll, pitch, yaw]))
185
+ self.Rcl = self.Rcb @ self.Rbl
186
+ quat = q3d.matrix_to_quaternion(self.Rcl)
187
+ self.line_quat.setText(
188
+ f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
189
+
190
+ def checkbox_changed(self, state):
191
+ if state == QtCore.Qt.Checked:
192
+ self.en_rgb = True
193
+ else:
194
+ self.en_rgb = False
195
+
196
+
197
+ def camera_info_cb(data):
198
+ global remap_info, K
199
+ if remap_info is None: # Initialize only once
200
+ K = np.array(data.K).reshape(3, 3)
201
+ D = np.array(data.D)
202
+ rospy.loginfo("Camera intrinsic parameters set")
203
+ height = data.height
204
+ width = data.width
205
+ mapx, mapy = cv2.initUndistortRectifyMap(
206
+ K, D, None, K, (width, height), cv2.CV_32FC1)
207
+ remap_info = [mapx, mapy]
208
+
209
+
210
+ def scan_cb(data):
211
+ global viewer, clouds, cloud_accum, cloud_accum_color
212
+ cloud, _, _ = convert_pointcloud2_msg(data)
213
+ while len(clouds) > viewer.cloud_num:
214
+ clouds.pop(0)
215
+ clouds.append(cloud)
216
+ cloud_accum = np.concatenate(clouds)
217
+
218
+ if cloud_accum_color is not None and viewer.en_rgb:
219
+ viewer['scan'].set_data(data=cloud_accum_color)
220
+ viewer['scan'].set_color_mode('RGB')
221
+ else:
222
+ viewer['scan'].set_data(data=cloud_accum)
223
+ viewer['scan'].set_color_mode('I')
224
+
225
+
226
+ def draw_larger_points(image, points, colors, radius):
227
+ for dx in range(-radius + 1, radius):
228
+ for dy in range(-radius + 1, radius):
229
+ distance = np.sqrt(dx**2 + dy**2)
230
+ if distance <= radius:
231
+ x_indices = points[:, 0] + dx
232
+ y_indices = points[:, 1] + dy
233
+ image[y_indices, x_indices] = colors
234
+ return image
235
+
236
+
237
+ def image_cb(data):
238
+ global cloud_accum, cloud_accum_color, remap_info, K
239
+ if remap_info is None:
240
+ rospy.logwarn("Camera parameters not yet received.")
241
+ return
242
+ image, _ = convert_image_msg(data)
243
+
244
+ # Undistort the image
245
+ image_un = cv2.remap(image, remap_info[0], remap_info[1], cv2.INTER_LINEAR)
246
+ if cloud_accum is not None:
247
+ cloud_local = cloud_accum.copy()
248
+ tcl = viewer.tcl
249
+ Rcl = viewer.Rcl
250
+ pl = cloud_local['xyz']
251
+ pc = (Rcl @ pl.T + tcl[:, np.newaxis]).T
252
+ u = (K @ pc.T).T
253
+ u_mask = (u[:, 2] != 0) & (pc[:, 2] > 0.2)
254
+ u = u[:, :2][u_mask] / u[:, 2][u_mask][:, np.newaxis]
255
+
256
+ radius = viewer.psize # Radius of the points to be drawn
257
+ u = np.round(u).astype(np.int32)
258
+ valid_x = (u[:, 0] >= radius) & (u[:, 0] < image_un.shape[1]-radius)
259
+ valid_y = (u[:, 1] >= radius) & (u[:, 1] < image_un.shape[0]-radius)
260
+ valid_points = valid_x & valid_y
261
+ u = u[valid_points]
262
+
263
+ intensity = cloud_local['irgb'][u_mask][valid_points] >> 24
264
+ vmin = viewer['scan'].vmin
265
+ vmax = viewer['scan'].vmax
266
+ intensity_color = q3d.rainbow(intensity, scalar_min=vmin, scalar_max=vmax).astype(np.uint8)
267
+ draw_image = image_un.copy()
268
+ draw_image = draw_larger_points(draw_image, u, intensity_color, radius)
269
+ rgb = image_un[u[:, 1], u[:, 0]]
270
+ xyz = cloud_local['xyz'][u_mask][valid_points]
271
+ color = (intensity.astype(np.uint32) << 24) | \
272
+ (rgb[:, 0].astype(np.uint32) << 16) | \
273
+ (rgb[:, 1].astype(np.uint32) << 8) | \
274
+ (rgb[:, 2].astype(np.uint32) << 0)
275
+ cloud_accum_color = np.rec.fromarrays(
276
+ [xyz, color],
277
+ dtype=viewer['scan'].data_type)
278
+ viewer['img'].set_data(data=draw_image)
279
+
280
+
281
+ def main():
282
+ global viewer
283
+ # Set up argument parser
284
+ parser = argparse.ArgumentParser(
285
+ description="Configure topic names for LiDAR, image, and camera info.")
286
+ parser.add_argument(
287
+ '--lidar', type=str, default='lidar', help="Topic of LiDAR data")
288
+ parser.add_argument('--camera', type=str, default='image_raw',
289
+ help="Topic of camera image data")
290
+ parser.add_argument('--camera_info', type=str,
291
+ default='camera_info', help="Topic of camera info")
292
+ args = parser.parse_args()
293
+
294
+ 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))
297
+ scan_item = q3d.CloudItem(size=2, alpha=1, color_mode='I')
298
+ img_item = q3d.ImageItem(pos=np.array([0, 0]), size=np.array([800, 600]))
299
+ viewer.add_items({'scan': scan_item, 'grid': grid_item, 'img': img_item})
300
+
301
+ rospy.init_node('lidar_cam_calib', anonymous=True)
302
+
303
+ # Use topic names from arguments
304
+ rospy.Subscriber(args.lidar, PointCloud2, scan_cb, queue_size=1)
305
+ rospy.Subscriber(args.camera, Image, image_cb, queue_size=1)
306
+ rospy.Subscriber(args.camera_info, CameraInfo,
307
+ camera_info_cb, queue_size=1)
308
+
309
+ viewer.show()
310
+ app.exec()
311
+
312
+
313
+ if __name__ == '__main__':
314
+ main()
@@ -0,0 +1,85 @@
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 q3dviewer.utils.maths import make_transform
9
+ import q3dviewer as q3d
10
+ import rospy
11
+ from nav_msgs.msg import Odometry
12
+ from sensor_msgs.msg import PointCloud2
13
+ import numpy as np
14
+ import random
15
+ from sensor_msgs.msg import Image
16
+ from q3dviewer.utils.convert_ros_msg import convert_pointcloud2_msg, convert_odometry_msg, convert_image_msg
17
+
18
+ viewer = None
19
+ point_num_per_scan = None
20
+ color_mode = None
21
+
22
+
23
+ def odom_cb(data):
24
+ global viewer
25
+ transform, _ = convert_odometry_msg(data)
26
+ viewer['odom'].set_transform(transform)
27
+
28
+
29
+ def scan_cb(data):
30
+ global viewer
31
+ global point_num_per_scan
32
+ cloud, fields, _ = convert_pointcloud2_msg(data)
33
+ if (cloud.shape[0] > point_num_per_scan):
34
+ idx = random.sample(range(cloud.shape[0]), point_num_per_scan)
35
+ cloud = cloud[idx]
36
+ color_mode = 'FLAT'
37
+ if 'intensity' in fields:
38
+ color_mode = 'I'
39
+ if 'rgb' in fields:
40
+ color_mode = 'RGB'
41
+ viewer['map'].set_color_mode(color_mode)
42
+ viewer['map'].set_data(data=cloud, append=True)
43
+ viewer['scan'].set_data(data=cloud)
44
+
45
+
46
+ def image_cb(data):
47
+ image, _ = convert_image_msg(data)
48
+ viewer['img'].set_data(data=image)
49
+
50
+
51
+ def main():
52
+ rospy.init_node('ros_viewer', anonymous=True)
53
+
54
+ global viewer
55
+ global point_num_per_scan
56
+ point_num_per_scan = 10000
57
+ map_item = q3d.CloudIOItem(size=1, alpha=0.1, color_mode='RGB')
58
+ scan_item = q3d.CloudItem(
59
+ size=2, alpha=1, color_mode='FLAT', color='#ffffff')
60
+ odom_item = q3d.AxisItem(size=0.5, width=5)
61
+ grid_item = q3d.GridItem(size=1000, spacing=20)
62
+ img_item = q3d.ImageItem(pos=np.array([0, 0]), size=np.array([800, 600]))
63
+
64
+ app = q3d.QApplication(['ROS Viewer'])
65
+ viewer = q3d.Viewer(name='ROS Viewer')
66
+
67
+ viewer.add_items({'map': map_item, 'scan': scan_item,
68
+ 'odom': odom_item, 'grid': grid_item,
69
+ 'img': img_item})
70
+
71
+ point_num_per_scan = rospy.get_param("scan_num", 100000)
72
+ print("point_num_per_scan: %d" % point_num_per_scan)
73
+ rospy.Subscriber(
74
+ "/cloud_registered", PointCloud2, scan_cb,
75
+ queue_size=1, buff_size=2**24)
76
+ rospy.Subscriber(
77
+ "/odometry", Odometry, odom_cb, queue_size=1, buff_size=2**24)
78
+ rospy.Subscriber('/image', Image, image_cb)
79
+
80
+ viewer.show()
81
+ app.exec()
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,4 @@
1
+ from q3dviewer.utils.maths import *
2
+ from q3dviewer.utils.gl_helper import *
3
+ from q3dviewer.utils.range_slider import *
4
+ from q3dviewer.utils.cloud_io import *