q3dviewer 1.0.0__py3-none-any.whl → 1.0.2__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.
- q3dviewer/__init__.py +15 -0
- q3dviewer/tools/__init__.py +1 -0
- q3dviewer/tools/cloud_viewer.py +75 -0
- q3dviewer/tools/example_viewer.py +38 -0
- q3dviewer/tools/gaussian_viewer.py +55 -0
- q3dviewer/tools/lidar_calib.py +274 -0
- q3dviewer/tools/lidar_cam_calib.py +299 -0
- q3dviewer/tools/mesh_viewer.py +57 -0
- q3dviewer/tools/ros_viewer.py +102 -0
- {q3dviewer-1.0.0.dist-info → q3dviewer-1.0.2.dist-info}/METADATA +1 -1
- {q3dviewer-1.0.0.dist-info → q3dviewer-1.0.2.dist-info}/RECORD +14 -6
- {q3dviewer-1.0.0.dist-info → q3dviewer-1.0.2.dist-info}/WHEEL +0 -0
- {q3dviewer-1.0.0.dist-info → q3dviewer-1.0.2.dist-info}/entry_points.txt +0 -0
- {q3dviewer-1.0.0.dist-info → q3dviewer-1.0.2.dist-info}/top_level.txt +0 -0
q3dviewer/__init__.py
CHANGED
|
@@ -2,3 +2,18 @@ from q3dviewer.utils import *
|
|
|
2
2
|
from q3dviewer.custom_items import *
|
|
3
3
|
from q3dviewer.viewer_widget import *
|
|
4
4
|
from q3dviewer.basic_viewer import *
|
|
5
|
+
from q3dviewer.tools.cloud_viewer import main as cloud_viewer
|
|
6
|
+
from q3dviewer.tools.ros_viewer import main as ros_viewer
|
|
7
|
+
from q3dviewer.tools.mesh_viewer import main as mesh_viewer
|
|
8
|
+
from q3dviewer.tools.gaussian_viewer import main as gaussian_viewer
|
|
9
|
+
from q3dviewer.tools.lidar_cam_calib import main as lidar_cam_calib
|
|
10
|
+
from q3dviewer.tools.lidar_calib import main as lidar_calib
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'cloud_viewer',
|
|
14
|
+
'ros_viewer',
|
|
15
|
+
'mesh_viewer',
|
|
16
|
+
'gaussian_viewer',
|
|
17
|
+
'lidar_cam_calib',
|
|
18
|
+
'lidar_calib'
|
|
19
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# tools/__init__.py
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pypcd4 import PointCloud
|
|
5
|
+
import q3dviewer as q3d
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CloudViewer(q3d.Viewer):
|
|
9
|
+
def __init__(self, **kwargs):
|
|
10
|
+
super(CloudViewer, self).__init__(**kwargs)
|
|
11
|
+
self.setAcceptDrops(True)
|
|
12
|
+
|
|
13
|
+
def dragEnterEvent(self, event):
|
|
14
|
+
if event.mimeData().hasUrls():
|
|
15
|
+
event.accept()
|
|
16
|
+
else:
|
|
17
|
+
event.ignore()
|
|
18
|
+
|
|
19
|
+
def dropEvent(self, event):
|
|
20
|
+
for url in event.mimeData().urls():
|
|
21
|
+
file_path = url.toLocalFile()
|
|
22
|
+
self.openCloudFile(file_path)
|
|
23
|
+
|
|
24
|
+
def openCloudFile(self, file):
|
|
25
|
+
cloud_item = self['cloud']
|
|
26
|
+
if cloud_item is None:
|
|
27
|
+
print("Can't find clouditem.")
|
|
28
|
+
return
|
|
29
|
+
if file.endswith('.pcd'):
|
|
30
|
+
pc = PointCloud.from_path(file).pc_data
|
|
31
|
+
if 'rgb' in pc.dtype.names:
|
|
32
|
+
color = pc["rgb"].astype(np.uint32)
|
|
33
|
+
cloud_item.setColorMode('RGB')
|
|
34
|
+
elif 'intensity' in pc.dtype.names:
|
|
35
|
+
color = pc["intensity"].astype(np.uint32)
|
|
36
|
+
cloud_item.setColorMode('I')
|
|
37
|
+
else:
|
|
38
|
+
color = pc['z'].astype(np.uint32)
|
|
39
|
+
cloud_item.setColorMode('#FFFFFF')
|
|
40
|
+
cloud = np.rec.fromarrays(
|
|
41
|
+
[np.stack([pc["x"], pc["y"], pc["z"]], axis=1), color],
|
|
42
|
+
dtype=cloud_item.data_type)
|
|
43
|
+
elif file.endswith('.npy'):
|
|
44
|
+
pc = np.load(file)
|
|
45
|
+
cloud = np.rec.fromarrays(
|
|
46
|
+
[np.stack([pc[:, 0], pc[:, 1], pc[:, 2]], axis=1),
|
|
47
|
+
pc[:, 3].astype(np.uint32)],
|
|
48
|
+
dtype=cloud_item.data_type)
|
|
49
|
+
cloud_item.setData(data=cloud)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
import argparse
|
|
54
|
+
parser = argparse.ArgumentParser()
|
|
55
|
+
parser.add_argument("--pcd", help="the pcd path")
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
app = q3d.QApplication(['Cloud Viewer'])
|
|
58
|
+
viewer = CloudViewer(name='Cloud Viewer')
|
|
59
|
+
cloud_item = q3d.CloudIOItem(size=1, alpha=0.1)
|
|
60
|
+
axis_item = q3d.GLAxisItem(size=0.5, width=5)
|
|
61
|
+
grid_item = q3d.GridItem(size=1000, spacing=20)
|
|
62
|
+
|
|
63
|
+
viewer.addItems(
|
|
64
|
+
{'cloud': cloud_item, 'grid': grid_item, 'axis': axis_item})
|
|
65
|
+
|
|
66
|
+
if args.pcd:
|
|
67
|
+
pcd_fn = args.pcd
|
|
68
|
+
viewer.openCloudFile(pcd_fn)
|
|
69
|
+
|
|
70
|
+
viewer.show()
|
|
71
|
+
app.exec_()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == '__main__':
|
|
75
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import q3dviewer as q3d
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def update(viewer):
|
|
10
|
+
i = 0.0
|
|
11
|
+
while True:
|
|
12
|
+
i += 0.05
|
|
13
|
+
time.sleep(0.1)
|
|
14
|
+
viewer['traj'].setData(np.array([np.sin(i) * i, np.cos(i) * i, i]))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
app = q3d.QApplication([])
|
|
19
|
+
|
|
20
|
+
axis_item = q3d.GLAxisItem(size=0.5, width=5)
|
|
21
|
+
grid_item = q3d.GridItem(size=10, spacing=1)
|
|
22
|
+
traj_item = q3d.TrajectoryItem(width=2)
|
|
23
|
+
|
|
24
|
+
viewer = q3d.Viewer(name='example')
|
|
25
|
+
th = threading.Thread(target=update, args=(viewer, ))
|
|
26
|
+
th.start()
|
|
27
|
+
|
|
28
|
+
viewer.addItems({
|
|
29
|
+
'grid': grid_item,
|
|
30
|
+
'axis': axis_item,
|
|
31
|
+
'traj': traj_item})
|
|
32
|
+
|
|
33
|
+
viewer.show()
|
|
34
|
+
app.exec_()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == '__main__':
|
|
38
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import q3dviewer as q3d
|
|
5
|
+
from q3dviewer.gau_io import load_gs, rotate_gaussian
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GuassianViewer(q3d.Viewer):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super(GuassianViewer, self).__init__(name="Guassian Viewer")
|
|
11
|
+
self.setAcceptDrops(True)
|
|
12
|
+
|
|
13
|
+
def dragEnterEvent(self, event):
|
|
14
|
+
if event.mimeData().hasUrls():
|
|
15
|
+
event.accept()
|
|
16
|
+
else:
|
|
17
|
+
event.ignore()
|
|
18
|
+
|
|
19
|
+
def dropEvent(self, event):
|
|
20
|
+
for url in event.mimeData().urls():
|
|
21
|
+
file_path = url.toLocalFile()
|
|
22
|
+
self.openGuassianFile(file_path)
|
|
23
|
+
|
|
24
|
+
def openGuassianFile(self, file):
|
|
25
|
+
gau_item = self['gaussian']
|
|
26
|
+
if gau_item is None:
|
|
27
|
+
print("Can't find gaussianitem")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
print("Try to load %s ..." % file)
|
|
31
|
+
gs = load_gs(file)
|
|
32
|
+
# convert camera optical frame (b) to camera frame (c).
|
|
33
|
+
Rcb = np.array([[0, -1, 0],
|
|
34
|
+
[0, 0, -1],
|
|
35
|
+
[1, 0, 0]]).T
|
|
36
|
+
gs = rotate_gaussian(Rcb, gs)
|
|
37
|
+
gs_data = gs.view(np.float32).reshape(gs.shape[0], -1)
|
|
38
|
+
gau_item.setData(gs_data=gs_data)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
app = q3d.QApplication(['Guassian Viewer'])
|
|
43
|
+
viewer = GuassianViewer(name='Guassian Viewer')
|
|
44
|
+
|
|
45
|
+
grid_item = q3d.GridItem(size=1000, spacing=20)
|
|
46
|
+
gau_item = q3d.GaussianItem()
|
|
47
|
+
|
|
48
|
+
viewer.addItems({'grid': grid_item, 'gaussian': gau_item})
|
|
49
|
+
|
|
50
|
+
viewer.show()
|
|
51
|
+
app.exec_()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == '__main__':
|
|
55
|
+
main()
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from sensor_msgs.msg import PointCloud2
|
|
4
|
+
from q3dviewer import *
|
|
5
|
+
import rospy
|
|
6
|
+
import numpy as np
|
|
7
|
+
import argparse
|
|
8
|
+
import open3d as o3d
|
|
9
|
+
|
|
10
|
+
viewer = None
|
|
11
|
+
cloud0_accum = None
|
|
12
|
+
clouds0 = []
|
|
13
|
+
cloud1_accum = None
|
|
14
|
+
clouds1 = []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CustomDoubleSpinBox(QDoubleSpinBox):
|
|
18
|
+
def __init__(self, decimals=4, *args, **kwargs):
|
|
19
|
+
self.decimals = decimals
|
|
20
|
+
super().__init__(*args, **kwargs)
|
|
21
|
+
self.setDecimals(self.decimals) # Set the number of decimal places
|
|
22
|
+
|
|
23
|
+
def textFromValue(self, value):
|
|
24
|
+
return f"{value:.{self.decimals}f}"
|
|
25
|
+
|
|
26
|
+
def valueFromText(self, text):
|
|
27
|
+
return float(text)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ViewerWithPanel(Viewer):
|
|
31
|
+
def __init__(self, **kwargs):
|
|
32
|
+
self.t01 = np.array([0, 0, 0])
|
|
33
|
+
self.R01 = np.eye(3)
|
|
34
|
+
self.cloud_num = 10
|
|
35
|
+
self.radius = 0.2
|
|
36
|
+
super().__init__(**kwargs)
|
|
37
|
+
|
|
38
|
+
def initUI(self):
|
|
39
|
+
centerWidget = QWidget()
|
|
40
|
+
self.setCentralWidget(centerWidget)
|
|
41
|
+
main_layout = QHBoxLayout()
|
|
42
|
+
centerWidget.setLayout(main_layout)
|
|
43
|
+
|
|
44
|
+
# Create a vertical layout for the settings
|
|
45
|
+
setting_layout = QVBoxLayout()
|
|
46
|
+
|
|
47
|
+
# Add XYZ spin boxes
|
|
48
|
+
label_xyz = QLabel("Set XYZ:")
|
|
49
|
+
setting_layout.addWidget(label_xyz)
|
|
50
|
+
self.box_x = CustomDoubleSpinBox()
|
|
51
|
+
self.box_x.setSingleStep(0.01)
|
|
52
|
+
setting_layout.addWidget(self.box_x)
|
|
53
|
+
self.box_y = CustomDoubleSpinBox()
|
|
54
|
+
self.box_y.setSingleStep(0.01)
|
|
55
|
+
setting_layout.addWidget(self.box_y)
|
|
56
|
+
self.box_z = CustomDoubleSpinBox()
|
|
57
|
+
self.box_z.setSingleStep(0.01)
|
|
58
|
+
setting_layout.addWidget(self.box_z)
|
|
59
|
+
self.box_x.setRange(-100.0, 100.0)
|
|
60
|
+
self.box_y.setRange(-100.0, 100.0)
|
|
61
|
+
self.box_z.setRange(-100.0, 100.0)
|
|
62
|
+
|
|
63
|
+
# Add RPY spin boxes
|
|
64
|
+
label_rpy = QLabel("Set Roll-Pitch-Yaw:")
|
|
65
|
+
setting_layout.addWidget(label_rpy)
|
|
66
|
+
self.box_roll = CustomDoubleSpinBox()
|
|
67
|
+
self.box_roll.setSingleStep(0.01)
|
|
68
|
+
setting_layout.addWidget(self.box_roll)
|
|
69
|
+
self.box_pitch = CustomDoubleSpinBox()
|
|
70
|
+
self.box_pitch.setSingleStep(0.01)
|
|
71
|
+
setting_layout.addWidget(self.box_pitch)
|
|
72
|
+
self.box_yaw = CustomDoubleSpinBox()
|
|
73
|
+
self.box_yaw.setSingleStep(0.01)
|
|
74
|
+
setting_layout.addWidget(self.box_yaw)
|
|
75
|
+
self.box_roll.setRange(-np.pi, np.pi)
|
|
76
|
+
self.box_pitch.setRange(-np.pi, np.pi)
|
|
77
|
+
self.box_yaw.setRange(-np.pi, np.pi)
|
|
78
|
+
|
|
79
|
+
label_cloud_num = QLabel("Set cloud frame number:")
|
|
80
|
+
setting_layout.addWidget(label_cloud_num)
|
|
81
|
+
self.box_cloud_num = QSpinBox()
|
|
82
|
+
self.box_cloud_num.setValue(self.cloud_num)
|
|
83
|
+
self.box_cloud_num.setRange(1, 100)
|
|
84
|
+
self.box_cloud_num.valueChanged.connect(self.update_cloud_num)
|
|
85
|
+
setting_layout.addWidget(self.box_cloud_num)
|
|
86
|
+
|
|
87
|
+
label_res = QLabel("The lidar0-lidar1 trans and quat:")
|
|
88
|
+
setting_layout.addWidget(label_res)
|
|
89
|
+
self.line_trans = QLineEdit()
|
|
90
|
+
self.line_trans.setReadOnly(True)
|
|
91
|
+
setting_layout.addWidget(self.line_trans)
|
|
92
|
+
self.line_quat = QLineEdit()
|
|
93
|
+
self.line_quat.setReadOnly(True)
|
|
94
|
+
setting_layout.addWidget(self.line_quat)
|
|
95
|
+
|
|
96
|
+
label_matching_setting = QLabel("Set matching radius:")
|
|
97
|
+
setting_layout.addWidget(label_matching_setting)
|
|
98
|
+
self.box_radius = CustomDoubleSpinBox(decimals=2)
|
|
99
|
+
self.box_radius.setSingleStep(0.1)
|
|
100
|
+
self.box_radius.setRange(0.1, 3.0)
|
|
101
|
+
self.box_radius.setValue(self.radius)
|
|
102
|
+
setting_layout.addWidget(self.box_radius)
|
|
103
|
+
|
|
104
|
+
self.icp_button = QPushButton("Auto Scan Matching")
|
|
105
|
+
self.icp_button.clicked.connect(self.perform_icp)
|
|
106
|
+
setting_layout.addWidget(self.icp_button)
|
|
107
|
+
|
|
108
|
+
self.line_trans.setText(np.array2string(self.t01, formatter={
|
|
109
|
+
'float_kind': lambda x: "%.4f" % x}, separator=', '))
|
|
110
|
+
quat = matrix_to_quaternion(self.R01)
|
|
111
|
+
self.line_quat.setText(np.array2string(
|
|
112
|
+
quat, formatter={'float_kind': lambda x: "%.4f" % x}, separator=', '))
|
|
113
|
+
|
|
114
|
+
# Connect spin boxes to methods
|
|
115
|
+
self.box_x.valueChanged.connect(self.update_xyz)
|
|
116
|
+
self.box_y.valueChanged.connect(self.update_xyz)
|
|
117
|
+
self.box_z.valueChanged.connect(self.update_xyz)
|
|
118
|
+
self.box_roll.valueChanged.connect(self.update_rpy)
|
|
119
|
+
self.box_pitch.valueChanged.connect(self.update_rpy)
|
|
120
|
+
self.box_yaw.valueChanged.connect(self.update_rpy)
|
|
121
|
+
|
|
122
|
+
# Add a stretch to push the widgets to the top
|
|
123
|
+
setting_layout.addStretch(1)
|
|
124
|
+
|
|
125
|
+
self.viewerWidget = self.vw()
|
|
126
|
+
main_layout.addLayout(setting_layout)
|
|
127
|
+
main_layout.addWidget(self.viewerWidget, 1)
|
|
128
|
+
|
|
129
|
+
timer = QtCore.QTimer(self)
|
|
130
|
+
timer.setInterval(20) # period, in milliseconds
|
|
131
|
+
timer.timeout.connect(self.update)
|
|
132
|
+
self.viewerWidget.setCameraPosition(distance=5)
|
|
133
|
+
self.viewerWidget.setBKColor('#ffffff')
|
|
134
|
+
timer.start()
|
|
135
|
+
|
|
136
|
+
def update_radius(self):
|
|
137
|
+
self.radius = self.box_radius.value()
|
|
138
|
+
|
|
139
|
+
def update_cloud_num(self):
|
|
140
|
+
self.cloud_num = self.box_cloud_num.value()
|
|
141
|
+
|
|
142
|
+
def update_xyz(self):
|
|
143
|
+
x = self.box_x.value()
|
|
144
|
+
y = self.box_y.value()
|
|
145
|
+
z = self.box_z.value()
|
|
146
|
+
self.t01 = np.array([x, y, z])
|
|
147
|
+
self.line_trans.setText(f"[{x:.6f}, {y:.6f}, {z:.6f}]")
|
|
148
|
+
|
|
149
|
+
def update_rpy(self):
|
|
150
|
+
roll = self.box_roll.value()
|
|
151
|
+
pitch = self.box_pitch.value()
|
|
152
|
+
yaw = self.box_yaw.value()
|
|
153
|
+
self.R01 = euler_to_matrix(np.array([roll, pitch, yaw]))
|
|
154
|
+
quat = matrix_to_quaternion(self.R01)
|
|
155
|
+
self.line_quat.setText(f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
|
|
156
|
+
|
|
157
|
+
def perform_icp(self):
|
|
158
|
+
global cloud0_accum, cloud1_accum
|
|
159
|
+
|
|
160
|
+
if cloud0_accum is not None and cloud1_accum is not None:
|
|
161
|
+
# Convert to Open3D point clouds
|
|
162
|
+
cloud0_o3d = o3d.geometry.PointCloud()
|
|
163
|
+
cloud0_o3d.points = o3d.utility.Vector3dVector(cloud0_accum['xyz'])
|
|
164
|
+
cloud1_o3d = o3d.geometry.PointCloud()
|
|
165
|
+
cloud1_o3d.points = o3d.utility.Vector3dVector(cloud1_accum['xyz'])
|
|
166
|
+
voxel_size = 0.1
|
|
167
|
+
cloud0_o3d = cloud0_o3d.voxel_down_sample(voxel_size)
|
|
168
|
+
cloud1_o3d = cloud1_o3d.voxel_down_sample(voxel_size)
|
|
169
|
+
cloud0_o3d.estimate_normals(
|
|
170
|
+
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=self.radius, max_nn=30))
|
|
171
|
+
trans_init = np.eye(4)
|
|
172
|
+
trans_init[:3, :3] = self.R01
|
|
173
|
+
trans_init[:3, 3] = self.t01
|
|
174
|
+
|
|
175
|
+
criteria = o3d.pipelines.registration.ICPConvergenceCriteria(
|
|
176
|
+
relative_fitness=1e-4,
|
|
177
|
+
max_iteration=50)
|
|
178
|
+
|
|
179
|
+
# Auto Scan Matching
|
|
180
|
+
reg_p2p = o3d.pipelines.registration.registration_icp(
|
|
181
|
+
cloud1_o3d, cloud0_o3d, self.radius, trans_init,
|
|
182
|
+
o3d.pipelines.registration.TransformationEstimationPointToPlane(),
|
|
183
|
+
criteria)
|
|
184
|
+
|
|
185
|
+
transformation_icp = reg_p2p.transformation
|
|
186
|
+
# print("ICP Transformation:", transformation_icp)
|
|
187
|
+
|
|
188
|
+
# Update R01 and t01 with ICP result
|
|
189
|
+
R01 = transformation_icp[:3, :3]
|
|
190
|
+
t01 = transformation_icp[:3, 3]
|
|
191
|
+
|
|
192
|
+
# Update the UI with new values
|
|
193
|
+
quat = matrix_to_quaternion(R01)
|
|
194
|
+
rpy = matrix_to_euler(R01)
|
|
195
|
+
self.box_roll.setValue(rpy[0])
|
|
196
|
+
self.box_pitch.setValue(rpy[1])
|
|
197
|
+
self.box_yaw.setValue(rpy[2])
|
|
198
|
+
self.box_x.setValue(t01[0])
|
|
199
|
+
self.box_y.setValue(t01[1])
|
|
200
|
+
self.box_z.setValue(t01[2])
|
|
201
|
+
self.line_trans.setText(f"[{t01[0]:.6f}, {t01[1]:.6f}, {t01[2]:.6f}]")
|
|
202
|
+
self.line_quat.setText(f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
|
|
203
|
+
self.t01 = t01
|
|
204
|
+
self.R01 = R01
|
|
205
|
+
print("Matching results")
|
|
206
|
+
print(f"translation: [{t01[0]:.6f}, {t01[1]:.6f}, {t01[2]:.6f}]")
|
|
207
|
+
print(f"Roll-Pitch-Yaw: [{rpy[0]:.6f}, {rpy[1]:.6f}, {rpy[2]:.6f}]")
|
|
208
|
+
print(f"Quaternion: [{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
|
|
209
|
+
|
|
210
|
+
def msg2Cloud(data):
|
|
211
|
+
pc = PointCloud.from_msg(data).pc_data
|
|
212
|
+
data_type = [('xyz', '<f4', (3,)), ('color', '<u4')]
|
|
213
|
+
color = pc['intensity'].astype(np.uint32)
|
|
214
|
+
cloud = np.rec.fromarrays(
|
|
215
|
+
[np.stack([pc['x'], pc['y'], pc['z']], axis=1), color],
|
|
216
|
+
dtype=data_type)
|
|
217
|
+
return cloud
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def scan0CB(data):
|
|
221
|
+
global viewer, clouds0, cloud0_accum
|
|
222
|
+
cloud = msg2Cloud(data)
|
|
223
|
+
while len(clouds0) > viewer.cloud_num:
|
|
224
|
+
clouds0.pop(0)
|
|
225
|
+
clouds0.append(cloud)
|
|
226
|
+
cloud0_accum = np.concatenate(clouds0)
|
|
227
|
+
viewer['scan0'].setData(data=cloud0_accum)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def scan1CB(data):
|
|
231
|
+
global viewer, clouds1, cloud1_accum
|
|
232
|
+
cloud = msg2Cloud(data)
|
|
233
|
+
while len(clouds1) > viewer.cloud_num:
|
|
234
|
+
clouds1.pop(0)
|
|
235
|
+
clouds1.append(cloud)
|
|
236
|
+
cloud1_accum = np.concatenate(clouds1)
|
|
237
|
+
cloud0_accum_new = cloud1_accum.copy()
|
|
238
|
+
cloud0_accum_new['xyz'] = (
|
|
239
|
+
viewer.R01 @ cloud1_accum['xyz'].T + viewer.t01[:, np.newaxis]).T
|
|
240
|
+
|
|
241
|
+
viewer['scan1'].setData(data=cloud0_accum_new)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main():
|
|
245
|
+
global viewer
|
|
246
|
+
# Set up argument parser
|
|
247
|
+
parser = argparse.ArgumentParser(
|
|
248
|
+
description="Configure topic names for LiDAR, image, and camera info.")
|
|
249
|
+
parser.add_argument('--lidar0', type=str, default='/lidar0',
|
|
250
|
+
help="Topic name for LiDAR data")
|
|
251
|
+
parser.add_argument('--lidar1', type=str, default='/lidar1',
|
|
252
|
+
help="Topic name for camera image data")
|
|
253
|
+
args = parser.parse_args()
|
|
254
|
+
|
|
255
|
+
app = QApplication(["LiDAR Calib"])
|
|
256
|
+
viewer = ViewerWithPanel(name='LiDAR Calib')
|
|
257
|
+
grid_item = GridItem(size=10, spacing=1, color=(0, 0, 0, 70))
|
|
258
|
+
scan0_item = CloudItem(size=2, alpha=1, color_mode='#FF0000')
|
|
259
|
+
scan1_item = CloudItem(size=2, alpha=1, color_mode='#00FF00')
|
|
260
|
+
viewer.addItems(
|
|
261
|
+
{'scan0': scan0_item, 'scan1': scan1_item, 'grid': grid_item})
|
|
262
|
+
|
|
263
|
+
rospy.init_node('lidar_calib', anonymous=True)
|
|
264
|
+
|
|
265
|
+
# Use topic names from arguments
|
|
266
|
+
rospy.Subscriber(args.lidar0, PointCloud2, scan0CB, queue_size=1)
|
|
267
|
+
rospy.Subscriber(args.lidar1, PointCloud2, scan1CB, queue_size=1)
|
|
268
|
+
|
|
269
|
+
viewer.show()
|
|
270
|
+
app.exec_()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == '__main__':
|
|
274
|
+
main()
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from sensor_msgs.msg import PointCloud2, Image, CameraInfo
|
|
4
|
+
from q3dviewer import *
|
|
5
|
+
import rospy
|
|
6
|
+
import numpy as np
|
|
7
|
+
import argparse
|
|
8
|
+
import cv2
|
|
9
|
+
|
|
10
|
+
viewer = None
|
|
11
|
+
cloud_accum = None
|
|
12
|
+
cloud_accum_color = None
|
|
13
|
+
clouds = []
|
|
14
|
+
remap_info = None
|
|
15
|
+
K = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CustomDoubleSpinBox(QDoubleSpinBox):
|
|
19
|
+
def __init__(self, decimals=4, *args, **kwargs):
|
|
20
|
+
self.decimals = decimals
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
self.setDecimals(self.decimals) # Set the number of decimal places
|
|
23
|
+
|
|
24
|
+
def textFromValue(self, value):
|
|
25
|
+
return f"{value:.{self.decimals}f}"
|
|
26
|
+
|
|
27
|
+
def valueFromText(self, text):
|
|
28
|
+
return float(text)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ViewerWithPanel(Viewer):
|
|
32
|
+
def __init__(self, **kwargs):
|
|
33
|
+
# b: camera body frame
|
|
34
|
+
# c: camera image frame
|
|
35
|
+
# l: lidar frame
|
|
36
|
+
self.Rbl = np.eye(3)
|
|
37
|
+
self.Rcb = np.array([[0, -1, 0],
|
|
38
|
+
[0, 0, -1],
|
|
39
|
+
[1, 0, 0]])
|
|
40
|
+
self.tcl = np.array([0, 0, 0])
|
|
41
|
+
self.Rcl = self.Rcb @ self.Rbl
|
|
42
|
+
self.psize = 2
|
|
43
|
+
self.cloud_num = 1
|
|
44
|
+
self.en_rgb = False
|
|
45
|
+
super().__init__(**kwargs)
|
|
46
|
+
|
|
47
|
+
def initUI(self):
|
|
48
|
+
centerWidget = QWidget()
|
|
49
|
+
self.setCentralWidget(centerWidget)
|
|
50
|
+
main_layout = QHBoxLayout()
|
|
51
|
+
centerWidget.setLayout(main_layout)
|
|
52
|
+
|
|
53
|
+
# Create a vertical layout for the settings
|
|
54
|
+
setting_layout = QVBoxLayout()
|
|
55
|
+
|
|
56
|
+
# Add a checkbox for RGB
|
|
57
|
+
self.checkbox_rgb = QCheckBox("Enable RGB Cloud")
|
|
58
|
+
self.checkbox_rgb.setChecked(False)
|
|
59
|
+
setting_layout.addWidget(self.checkbox_rgb)
|
|
60
|
+
self.checkbox_rgb.stateChanged.connect(
|
|
61
|
+
self.checkbox_changed)
|
|
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_y.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_psize = QLabel("Set point size:")
|
|
96
|
+
setting_layout.addWidget(label_psize)
|
|
97
|
+
self.box_psize = QSpinBox()
|
|
98
|
+
self.box_psize.setValue(self.psize)
|
|
99
|
+
self.box_psize.setRange(0, 5)
|
|
100
|
+
self.box_psize.valueChanged.connect(self.update_psize)
|
|
101
|
+
setting_layout.addWidget(self.box_psize)
|
|
102
|
+
|
|
103
|
+
label_cloud_num = QLabel("Set cloud frame number:")
|
|
104
|
+
setting_layout.addWidget(label_cloud_num)
|
|
105
|
+
self.box_cloud_num = QSpinBox()
|
|
106
|
+
self.box_cloud_num.setValue(self.cloud_num)
|
|
107
|
+
self.box_cloud_num.setRange(1, 100)
|
|
108
|
+
self.box_cloud_num.valueChanged.connect(self.update_cloud_num)
|
|
109
|
+
setting_layout.addWidget(self.box_cloud_num)
|
|
110
|
+
|
|
111
|
+
label_res = QLabel("The cam-lidar quat and translation:")
|
|
112
|
+
setting_layout.addWidget(label_res)
|
|
113
|
+
self.line_quat = QLineEdit()
|
|
114
|
+
self.line_quat.setReadOnly(True)
|
|
115
|
+
setting_layout.addWidget(self.line_quat)
|
|
116
|
+
self.line_trans = QLineEdit()
|
|
117
|
+
self.line_trans.setReadOnly(True)
|
|
118
|
+
setting_layout.addWidget(self.line_trans)
|
|
119
|
+
|
|
120
|
+
self.line_trans.setText(f"[{self.tcl[0]:.6f}, {self.tcl[1]:.6f}, {self.tcl[2]:.6f}]")
|
|
121
|
+
quat = matrix_to_quaternion(self.Rcl)
|
|
122
|
+
self.line_quat.setText(f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
|
|
123
|
+
|
|
124
|
+
# Connect spin boxes to methods
|
|
125
|
+
self.box_x.valueChanged.connect(self.update_xyz)
|
|
126
|
+
self.box_y.valueChanged.connect(self.update_xyz)
|
|
127
|
+
self.box_z.valueChanged.connect(self.update_xyz)
|
|
128
|
+
self.box_roll.valueChanged.connect(self.update_rpy)
|
|
129
|
+
self.box_pitch.valueChanged.connect(self.update_rpy)
|
|
130
|
+
self.box_yaw.valueChanged.connect(self.update_rpy)
|
|
131
|
+
|
|
132
|
+
# Add a stretch to push the widgets to the top
|
|
133
|
+
setting_layout.addStretch(1)
|
|
134
|
+
|
|
135
|
+
self.viewerWidget = self.vw()
|
|
136
|
+
main_layout.addLayout(setting_layout)
|
|
137
|
+
main_layout.addWidget(self.viewerWidget, 1)
|
|
138
|
+
|
|
139
|
+
timer = QtCore.QTimer(self)
|
|
140
|
+
timer.setInterval(20) # period, in milliseconds
|
|
141
|
+
timer.timeout.connect(self.update)
|
|
142
|
+
self.viewerWidget.setCameraPosition(distance=5)
|
|
143
|
+
self.viewerWidget.setBKColor('#ffffff')
|
|
144
|
+
timer.start()
|
|
145
|
+
|
|
146
|
+
def update_psize(self):
|
|
147
|
+
self.psize = self.box_psize.value()
|
|
148
|
+
|
|
149
|
+
def update_cloud_num(self):
|
|
150
|
+
self.cloud_num = self.box_cloud_num.value()
|
|
151
|
+
|
|
152
|
+
def update_xyz(self):
|
|
153
|
+
x = self.box_x.value()
|
|
154
|
+
y = self.box_y.value()
|
|
155
|
+
z = self.box_z.value()
|
|
156
|
+
self.tcl = np.array([x, y, z])
|
|
157
|
+
self.line_trans.setText(f"[{x:.6f}, {y:.6f}, {x:.6f}]")
|
|
158
|
+
|
|
159
|
+
def update_rpy(self):
|
|
160
|
+
roll = self.box_roll.value()
|
|
161
|
+
pitch = self.box_pitch.value()
|
|
162
|
+
yaw = self.box_yaw.value()
|
|
163
|
+
self.Rbl = euler_to_matrix(np.array([roll, pitch, yaw]))
|
|
164
|
+
self.Rcl = self.Rcb @ self.Rbl
|
|
165
|
+
quat = matrix_to_quaternion(self.Rcl)
|
|
166
|
+
self.line_quat.setText(f"[{quat[0]:.6f}, {quat[1]:.6f}, {quat[2]:.6f}, {quat[3]:.6f}]")
|
|
167
|
+
|
|
168
|
+
def checkbox_changed(self, state):
|
|
169
|
+
if state == QtCore.Qt.Checked:
|
|
170
|
+
self.en_rgb = True
|
|
171
|
+
else:
|
|
172
|
+
self.en_rgb = False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def cameraInfoCB(data):
|
|
176
|
+
global remap_info, K
|
|
177
|
+
if remap_info is None: # Initialize only once
|
|
178
|
+
K = np.array(data.K).reshape(3, 3)
|
|
179
|
+
D = np.array(data.D)
|
|
180
|
+
rospy.loginfo("Camera intrinsic parameters set")
|
|
181
|
+
height = data.height
|
|
182
|
+
width = data.width
|
|
183
|
+
mapx, mapy = cv2.initUndistortRectifyMap(
|
|
184
|
+
K, D, None, K, (width, height), cv2.CV_32FC1)
|
|
185
|
+
remap_info = [mapx, mapy]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def scanCB(data):
|
|
189
|
+
global viewer, clouds, cloud_accum, cloud_accum_color
|
|
190
|
+
pc = PointCloud.from_msg(data).pc_data
|
|
191
|
+
data_type = viewer['scan'].data_type
|
|
192
|
+
color = pc['intensity'].astype(np.uint32)
|
|
193
|
+
cloud = np.rec.fromarrays(
|
|
194
|
+
[np.stack([pc['x'], pc['y'], pc['z']], axis=1), color],
|
|
195
|
+
dtype=data_type)
|
|
196
|
+
while len(clouds) > viewer.cloud_num:
|
|
197
|
+
clouds.pop(0)
|
|
198
|
+
clouds.append(cloud)
|
|
199
|
+
cloud_accum = np.concatenate(clouds)
|
|
200
|
+
|
|
201
|
+
if cloud_accum_color is not None and viewer.en_rgb:
|
|
202
|
+
viewer['scan'].setData(data=cloud_accum_color)
|
|
203
|
+
viewer['scan'].setColorMode('RGB')
|
|
204
|
+
else:
|
|
205
|
+
viewer['scan'].setData(data=cloud_accum)
|
|
206
|
+
viewer['scan'].setColorMode('I')
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def draw_larger_points(image, points, colors, radius):
|
|
210
|
+
for dx in range(-radius + 1, radius):
|
|
211
|
+
for dy in range(-radius + 1, radius):
|
|
212
|
+
distance = np.sqrt(dx**2 + dy**2)
|
|
213
|
+
if distance <= radius:
|
|
214
|
+
x_indices = points[:, 0] + dx
|
|
215
|
+
y_indices = points[:, 1] + dy
|
|
216
|
+
image[y_indices, x_indices] = colors
|
|
217
|
+
return image
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def imageCB(data):
|
|
221
|
+
global cloud_accum, cloud_accum_color, remap_info, K
|
|
222
|
+
if remap_info is None:
|
|
223
|
+
rospy.logwarn("Camera parameters not yet received.")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
image = np.frombuffer(data.data, dtype=np.uint8).reshape(
|
|
227
|
+
data.height, data.width, -1)
|
|
228
|
+
if data.encoding == 'bgr8':
|
|
229
|
+
image = image[:, :, ::-1] # convert BGR to RGB
|
|
230
|
+
|
|
231
|
+
# Undistort the image
|
|
232
|
+
image_un = cv2.remap(image, remap_info[0], remap_info[1], cv2.INTER_LINEAR)
|
|
233
|
+
|
|
234
|
+
if cloud_accum is not None:
|
|
235
|
+
cloud_local = cloud_accum.copy()
|
|
236
|
+
tcl = viewer.tcl
|
|
237
|
+
Rcl = viewer.Rcl
|
|
238
|
+
pl = cloud_local['xyz']
|
|
239
|
+
po = (Rcl @ pl.T + tcl[:, np.newaxis]).T
|
|
240
|
+
u = (K @ po.T).T
|
|
241
|
+
u_mask = u[:, 2] != 0
|
|
242
|
+
u = u[:, :2][u_mask] / u[:, 2][u_mask][:, np.newaxis]
|
|
243
|
+
|
|
244
|
+
radius = viewer.psize # Radius of the points to be drawn
|
|
245
|
+
u = np.round(u).astype(np.int32)
|
|
246
|
+
valid_x = (u[:, 0] >= radius) & (u[:, 0] < image_un.shape[1]-radius)
|
|
247
|
+
valid_y = (u[:, 1] >= radius) & (u[:, 1] < image_un.shape[0]-radius)
|
|
248
|
+
valid_points = valid_x & valid_y
|
|
249
|
+
u = u[valid_points]
|
|
250
|
+
|
|
251
|
+
intensity = cloud_local['color'][u_mask][valid_points]
|
|
252
|
+
intensity_color = rainbow(intensity).astype(np.uint8)
|
|
253
|
+
draw_image = image_un.copy()
|
|
254
|
+
draw_image = draw_larger_points(draw_image, u, intensity_color, radius)
|
|
255
|
+
rgb = image_un[u[:, 1], u[:, 0]]
|
|
256
|
+
xyz = cloud_local['xyz'][u_mask][valid_points]
|
|
257
|
+
color = (intensity.astype(np.uint32) << 24) | \
|
|
258
|
+
(rgb[:, 0].astype(np.uint32) << 16) | \
|
|
259
|
+
(rgb[:, 1].astype(np.uint32) << 8) | \
|
|
260
|
+
(rgb[:, 2].astype(np.uint32) << 0)
|
|
261
|
+
cloud_accum_color = np.rec.fromarrays(
|
|
262
|
+
[xyz, color],
|
|
263
|
+
dtype=viewer['scan'].data_type)
|
|
264
|
+
viewer['img'].setData(data=draw_image)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main():
|
|
268
|
+
global viewer
|
|
269
|
+
# Set up argument parser
|
|
270
|
+
parser = argparse.ArgumentParser(
|
|
271
|
+
description="Configure topic names for LiDAR, image, and camera info.")
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
'--lidar', type=str, default='lidar', help="Topic of LiDAR data")
|
|
274
|
+
parser.add_argument('--camera', type=str, default='image_raw',
|
|
275
|
+
help="Topic of camera image data")
|
|
276
|
+
parser.add_argument('--camera_info', type=str,
|
|
277
|
+
default='camera_info', help="Topic of camera info")
|
|
278
|
+
args = parser.parse_args()
|
|
279
|
+
|
|
280
|
+
app = QApplication(['LiDAR Cam Calib'])
|
|
281
|
+
viewer = ViewerWithPanel(name='LiDAR Cam Calib')
|
|
282
|
+
grid_item = GridItem(size=10, spacing=1, color=(0, 0, 0, 70))
|
|
283
|
+
scan_item = CloudItem(size=2, alpha=1, color_mode='I')
|
|
284
|
+
img_item = ImageItem(pos=np.array([0, 0]), size=np.array([800, 600]))
|
|
285
|
+
viewer.addItems({'scan': scan_item, 'grid': grid_item, 'img': img_item})
|
|
286
|
+
|
|
287
|
+
rospy.init_node('lidar_cam_calib', anonymous=True)
|
|
288
|
+
|
|
289
|
+
# Use topic names from arguments
|
|
290
|
+
rospy.Subscriber(args.lidar, PointCloud2, scanCB, queue_size=1)
|
|
291
|
+
rospy.Subscriber(args.camera, Image, imageCB, queue_size=1)
|
|
292
|
+
rospy.Subscriber(args.camera_info, CameraInfo, cameraInfoCB, queue_size=1)
|
|
293
|
+
|
|
294
|
+
viewer.show()
|
|
295
|
+
app.exec_()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == '__main__':
|
|
299
|
+
main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import q3dviewer as q3d
|
|
5
|
+
from pyqtgraph.opengl import GLMeshItem
|
|
6
|
+
from stl import mesh
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MeshViewer(q3d.Viewer):
|
|
10
|
+
def __init__(self):
|
|
11
|
+
super(MeshViewer, self).__init__(name="Mesh Viewer")
|
|
12
|
+
self.setAcceptDrops(True)
|
|
13
|
+
|
|
14
|
+
def dragEnterEvent(self, event):
|
|
15
|
+
if event.mimeData().hasUrls():
|
|
16
|
+
event.accept()
|
|
17
|
+
else:
|
|
18
|
+
event.ignore()
|
|
19
|
+
|
|
20
|
+
def dropEvent(self, event):
|
|
21
|
+
for url in event.mimeData().urls():
|
|
22
|
+
file_path = url.toLocalFile()
|
|
23
|
+
self.openMeshFile(file_path)
|
|
24
|
+
|
|
25
|
+
def openMeshFile(self, file):
|
|
26
|
+
mesh_item = self['mesh']
|
|
27
|
+
if mesh_item is None:
|
|
28
|
+
print("Can't find meshitem")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
print("Try to load %s ..." % file)
|
|
32
|
+
stl_mesh = mesh.Mesh.from_file(file)
|
|
33
|
+
|
|
34
|
+
vertices = stl_mesh.points.reshape(-1, 3)
|
|
35
|
+
faces = np.arange(vertices.shape[0]).reshape(-1, 3)
|
|
36
|
+
mesh_item.setMeshData(vertexes=vertices, faces=faces)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
app = q3d.QApplication([])
|
|
41
|
+
viewer = MeshViewer()
|
|
42
|
+
|
|
43
|
+
grid_item = q3d.GridItem(size=1000, spacing=20)
|
|
44
|
+
# 'glOptions', 'opaque', 'additive' 'translucent'
|
|
45
|
+
mesh_item = GLMeshItem(smooth=True, drawFaces=True, drawEdges=True,
|
|
46
|
+
color=(0, 1, 0, 0.2),
|
|
47
|
+
edgeColor=(1, 1, 1, 1),
|
|
48
|
+
glOptions='translucent')
|
|
49
|
+
|
|
50
|
+
viewer.addItems({'grid': grid_item, 'mesh': mesh_item})
|
|
51
|
+
|
|
52
|
+
viewer.show()
|
|
53
|
+
app.exec_()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == '__main__':
|
|
57
|
+
main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from q3dviewer.utils import make_transform
|
|
5
|
+
import q3dviewer as q3d
|
|
6
|
+
import rospy
|
|
7
|
+
from nav_msgs.msg import Odometry
|
|
8
|
+
from sensor_msgs.msg import PointCloud2
|
|
9
|
+
import numpy as np
|
|
10
|
+
import random
|
|
11
|
+
from pypcd4 import PointCloud
|
|
12
|
+
from sensor_msgs.msg import Image
|
|
13
|
+
|
|
14
|
+
viewer = None
|
|
15
|
+
point_num_per_scan = None
|
|
16
|
+
color_mode = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def odomCB(data):
|
|
20
|
+
global viewer
|
|
21
|
+
pose = np.array(
|
|
22
|
+
[data.pose.pose.position.x,
|
|
23
|
+
data.pose.pose.position.y,
|
|
24
|
+
data.pose.pose.position.z])
|
|
25
|
+
rotation = np.array([
|
|
26
|
+
data.pose.pose.orientation.x,
|
|
27
|
+
data.pose.pose.orientation.y,
|
|
28
|
+
data.pose.pose.orientation.z,
|
|
29
|
+
data.pose.pose.orientation.w])
|
|
30
|
+
transform = make_transform(pose, rotation)
|
|
31
|
+
viewer['odom'].setTransform(transform)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scanCB(data):
|
|
35
|
+
global viewer
|
|
36
|
+
global point_num_per_scan
|
|
37
|
+
global color_mode
|
|
38
|
+
pc = PointCloud.from_msg(data).pc_data
|
|
39
|
+
data_type = viewer['scan'].data_type
|
|
40
|
+
if 'rgb' in pc.dtype.names:
|
|
41
|
+
color = pc['rgb'].view(np.uint32)
|
|
42
|
+
else:
|
|
43
|
+
color = pc['intensity']
|
|
44
|
+
|
|
45
|
+
if color_mode is None:
|
|
46
|
+
if 'rgb' in pc.dtype.names:
|
|
47
|
+
color_mode = 'RGB'
|
|
48
|
+
else:
|
|
49
|
+
color_mode = 'I'
|
|
50
|
+
viewer['map'].setColorMode(color_mode)
|
|
51
|
+
|
|
52
|
+
cloud = np.rec.fromarrays(
|
|
53
|
+
[np.stack([pc['x'], pc['y'], pc['z']], axis=1), color],
|
|
54
|
+
dtype=data_type)
|
|
55
|
+
if (cloud.shape[0] > point_num_per_scan):
|
|
56
|
+
idx = random.sample(range(cloud.shape[0]), point_num_per_scan)
|
|
57
|
+
cloud = cloud[idx]
|
|
58
|
+
viewer['map'].setData(data=cloud, append=True)
|
|
59
|
+
viewer['scan'].setData(data=cloud)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def imageCB(data):
|
|
63
|
+
image = np.frombuffer(data.data, dtype=np.uint8).reshape(
|
|
64
|
+
data.height, data.width, -1)
|
|
65
|
+
if (data.encoding == 'bgr8'):
|
|
66
|
+
image = image[:, :, ::-1] # convert bgr to rgb
|
|
67
|
+
viewer['img'].setData(data=image)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
rospy.init_node('ros_viewer', anonymous=True)
|
|
72
|
+
|
|
73
|
+
global viewer
|
|
74
|
+
global point_num_per_scan
|
|
75
|
+
point_num_per_scan = 10000
|
|
76
|
+
map_item = q3d.CloudIOItem(size=1, alpha=0.1, color_mode='RGB')
|
|
77
|
+
scan_item = q3d.CloudItem(size=2, alpha=1, color_mode='#FFFFFF')
|
|
78
|
+
odom_item = q3d.GLAxisItem(size=0.5, width=5)
|
|
79
|
+
grid_item = q3d.GridItem(size=1000, spacing=20)
|
|
80
|
+
img_item = q3d.ImageItem(pos=np.array([0, 0]), size=np.array([800, 600]))
|
|
81
|
+
|
|
82
|
+
app = q3d.QApplication(['ROS Viewer'])
|
|
83
|
+
viewer = q3d.Viewer(name='ROS Viewer')
|
|
84
|
+
|
|
85
|
+
viewer.addItems({'map': map_item, 'scan': scan_item,
|
|
86
|
+
'odom': odom_item, 'grid': grid_item, 'img': img_item})
|
|
87
|
+
|
|
88
|
+
point_num_per_scan = rospy.get_param("scan_num", 100000)
|
|
89
|
+
print("point_num_per_scan: %d" % point_num_per_scan)
|
|
90
|
+
rospy.Subscriber(
|
|
91
|
+
"/cloud_registered", PointCloud2, scanCB,
|
|
92
|
+
queue_size=1, buff_size=2**24)
|
|
93
|
+
rospy.Subscriber(
|
|
94
|
+
"/odometry", Odometry, odomCB, queue_size=1, buff_size=2**24)
|
|
95
|
+
rospy.Subscriber('/image', Image, imageCB)
|
|
96
|
+
|
|
97
|
+
viewer.show()
|
|
98
|
+
app.exec_()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
q3dviewer/__init__.py,sha256=
|
|
1
|
+
q3dviewer/__init__.py,sha256=sWX8RiMSEBX4Ebi5SN9Oyij_ruLX1FdY-ZM56_55_Ak,654
|
|
2
2
|
q3dviewer/basic_viewer.py,sha256=A7bMxutP1grnqE5A4tjODGrzNfFq6-c6MlNYj8t54WU,1609
|
|
3
3
|
q3dviewer/gau_io.py,sha256=fKry1eyeFdw_JO5JZqU3l66dzt43NGpHhX89ER_y38A,5900
|
|
4
4
|
q3dviewer/utils.py,sha256=xzUAXUhgxoJyPrnivnyrJWoQQg9LrDPhxgZo1Cp35kQ,4596
|
|
@@ -13,8 +13,16 @@ q3dviewer/custom_items/grid_item.py,sha256=WuNzIHBhjAQ6e4fP5aBp5jSgYbrY7wrbP9PhR
|
|
|
13
13
|
q3dviewer/custom_items/image_item.py,sha256=qOT0r6p8_NcF5GVeJrrR8rJNe68U_jW1qrUDZ05O5hI,5312
|
|
14
14
|
q3dviewer/custom_items/text_item.py,sha256=lbJaXfpeyyxr6lhlMjypk4a079-aUpsrXFjGi-BTzPU,1872
|
|
15
15
|
q3dviewer/custom_items/trajectory_item.py,sha256=9gM5voiYk0j4JMEp3drF7VFYSPsJcEK7mibdx45OQDs,2778
|
|
16
|
-
q3dviewer
|
|
17
|
-
q3dviewer
|
|
18
|
-
q3dviewer
|
|
19
|
-
q3dviewer
|
|
20
|
-
q3dviewer
|
|
16
|
+
q3dviewer/tools/__init__.py,sha256=01wG7BGM6VX0QyFBKsqPmyf2e-vrmV_N3-mo-VQ1VBg,20
|
|
17
|
+
q3dviewer/tools/cloud_viewer.py,sha256=qqOLulsVBRnykbWxatHN0eto52sMwF9yOHz35-Ye_SQ,2322
|
|
18
|
+
q3dviewer/tools/example_viewer.py,sha256=aKydEeM_h9AUvgeVvaWqGsL4l7YHzgxvXhXibQhsuqQ,747
|
|
19
|
+
q3dviewer/tools/gaussian_viewer.py,sha256=o_Vu-kKGN6A0VffPcOBZCCDFGfg_56v94IoC0r7mv1Y,1489
|
|
20
|
+
q3dviewer/tools/lidar_calib.py,sha256=f57xgFD2A-YqEwxCo0fgEAtiPnEvhT2NhM0b4lrTaFU,10238
|
|
21
|
+
q3dviewer/tools/lidar_cam_calib.py,sha256=8LHe7rgr1WCXgcyy1dnFoENdfkANf_CDrwVUmZ85HfQ,10794
|
|
22
|
+
q3dviewer/tools/mesh_viewer.py,sha256=QjahZzNy71xIwV-RzlxAtZs8cO57QlOSmMeqJnylS8g,1542
|
|
23
|
+
q3dviewer/tools/ros_viewer.py,sha256=PZ6kod7QT5DdRXudYw_olFkd072GD8GTszovBjTL3Cs,2984
|
|
24
|
+
q3dviewer-1.0.2.dist-info/METADATA,sha256=IicP6A0SV7jW_yVFO-Ejb610ZEgzPR30NPS2qf3U_U0,189
|
|
25
|
+
q3dviewer-1.0.2.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
|
|
26
|
+
q3dviewer-1.0.2.dist-info/entry_points.txt,sha256=6zSxtJ5cjcpc32sR_wkRqOaeiJFpEuLnJQckZE3niU0,316
|
|
27
|
+
q3dviewer-1.0.2.dist-info/top_level.txt,sha256=HFFDCbGu28txcGe2HPc46A7EPaguBa_b5oH7bufmxHM,10
|
|
28
|
+
q3dviewer-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|