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.
- q3dviewer/__init__.py +5 -0
- q3dviewer/base_glwidget.py +256 -0
- q3dviewer/base_item.py +57 -0
- q3dviewer/custom_items/__init__.py +9 -0
- q3dviewer/custom_items/axis_item.py +148 -0
- q3dviewer/custom_items/cloud_io_item.py +79 -0
- q3dviewer/custom_items/cloud_item.py +314 -0
- q3dviewer/custom_items/frame_item.py +194 -0
- q3dviewer/custom_items/gaussian_item.py +254 -0
- q3dviewer/custom_items/grid_item.py +88 -0
- q3dviewer/custom_items/image_item.py +172 -0
- q3dviewer/custom_items/line_item.py +120 -0
- q3dviewer/custom_items/text_item.py +63 -0
- q3dviewer/gau_io.py +0 -0
- q3dviewer/glwidget.py +131 -0
- q3dviewer/shaders/cloud_frag.glsl +28 -0
- q3dviewer/shaders/cloud_vert.glsl +72 -0
- q3dviewer/shaders/gau_frag.glsl +42 -0
- q3dviewer/shaders/gau_prep.glsl +249 -0
- q3dviewer/shaders/gau_vert.glsl +77 -0
- q3dviewer/shaders/sort_by_key.glsl +56 -0
- q3dviewer/tools/__init__.py +1 -0
- q3dviewer/tools/cloud_viewer.py +123 -0
- q3dviewer/tools/example_viewer.py +47 -0
- q3dviewer/tools/gaussian_viewer.py +60 -0
- q3dviewer/tools/lidar_calib.py +294 -0
- q3dviewer/tools/lidar_cam_calib.py +314 -0
- q3dviewer/tools/ros_viewer.py +85 -0
- q3dviewer/utils/__init__.py +4 -0
- q3dviewer/utils/cloud_io.py +323 -0
- q3dviewer/utils/convert_ros_msg.py +49 -0
- q3dviewer/utils/gl_helper.py +40 -0
- q3dviewer/utils/maths.py +168 -0
- q3dviewer/utils/range_slider.py +86 -0
- q3dviewer/viewer.py +58 -0
- q3dviewer-1.0.5.dist-info/LICENSE +21 -0
- {q3dviewer-1.0.3.dist-info → q3dviewer-1.0.5.dist-info}/METADATA +7 -4
- q3dviewer-1.0.5.dist-info/RECORD +41 -0
- q3dviewer-1.0.5.dist-info/top_level.txt +1 -0
- q3dviewer-1.0.3.dist-info/RECORD +0 -5
- q3dviewer-1.0.3.dist-info/top_level.txt +0 -1
- {q3dviewer-1.0.3.dist-info → q3dviewer-1.0.5.dist-info}/WHEEL +0 -0
- {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()
|