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,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
|
|
3
|
+
Distributed under MIT license. See LICENSE for more information.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import meshio
|
|
8
|
+
from pypcd4 import PointCloud, MetaData
|
|
9
|
+
from pye57 import E57
|
|
10
|
+
import laspy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_ply(cloud, save_path):
|
|
14
|
+
xyz = cloud['xyz']
|
|
15
|
+
i = (cloud['irgb'] & 0xFF000000) >> 24
|
|
16
|
+
rgb = cloud['irgb'] & 0x00FFFFFF
|
|
17
|
+
mesh = meshio.Mesh(points=xyz, cells=[], point_data={
|
|
18
|
+
"rgb": rgb, "intensity": i})
|
|
19
|
+
mesh.write(save_path, file_format="ply")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_ply(file):
|
|
23
|
+
mesh = meshio.read(file)
|
|
24
|
+
xyz = mesh.points
|
|
25
|
+
rgb = np.zeros([xyz.shape[0]], dtype=np.uint32)
|
|
26
|
+
intensity = np.zeros([xyz.shape[0]], dtype=np.uint32)
|
|
27
|
+
color_mode = 'FLAT'
|
|
28
|
+
if "intensity" in mesh.point_data:
|
|
29
|
+
intensity = mesh.point_data["intensity"]
|
|
30
|
+
color_mode = 'I'
|
|
31
|
+
if "rgb" in mesh.point_data:
|
|
32
|
+
rgb = mesh.point_data["rgb"]
|
|
33
|
+
color_mode = 'RGB'
|
|
34
|
+
irgb = (intensity << 24) | rgb
|
|
35
|
+
dtype = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
36
|
+
cloud = np.rec.fromarrays([xyz, irgb], dtype=dtype)
|
|
37
|
+
return cloud, color_mode
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def save_pcd(cloud, save_path):
|
|
41
|
+
fields = ('x', 'y', 'z', 'intensity', 'rgb')
|
|
42
|
+
metadata = MetaData.model_validate(
|
|
43
|
+
{
|
|
44
|
+
"fields": fields,
|
|
45
|
+
"size": [4, 4, 4, 4, 4],
|
|
46
|
+
"type": ['F', 'F', 'F', 'U', 'U'],
|
|
47
|
+
"count": [1, 1, 1, 1, 1],
|
|
48
|
+
"width": cloud.shape[0],
|
|
49
|
+
"points": cloud.shape[0],
|
|
50
|
+
})
|
|
51
|
+
i = (cloud['irgb'] & 0xFF000000) >> 24
|
|
52
|
+
rgb = cloud['irgb'] & 0x00FFFFFF
|
|
53
|
+
|
|
54
|
+
dtype = [('xyz', '<f4', (3,)), ('intensity', '<u4'), ('rgb', '<u4')]
|
|
55
|
+
tmp = np.rec.fromarrays([cloud['xyz'], i, rgb], dtype=dtype)
|
|
56
|
+
|
|
57
|
+
PointCloud(metadata, tmp).save(save_path)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_pcd(file):
|
|
61
|
+
dtype = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
62
|
+
pc = PointCloud.from_path(file).pc_data
|
|
63
|
+
rgb = np.zeros([pc.shape[0]], dtype=np.uint32)
|
|
64
|
+
intensity = np.zeros([pc.shape[0]], dtype=np.uint32)
|
|
65
|
+
color_mode = 'FLAT'
|
|
66
|
+
if 'intensity' in pc.dtype.names:
|
|
67
|
+
intensity = pc['intensity'].astype(np.uint32)
|
|
68
|
+
color_mode = 'I'
|
|
69
|
+
if 'rgb' in pc.dtype.names:
|
|
70
|
+
rgb = pc['rgb'].astype(np.uint32)
|
|
71
|
+
color_mode = 'RGB'
|
|
72
|
+
irgb = (intensity << 24) | rgb
|
|
73
|
+
xyz = np.stack([pc['x'], pc['y'], pc['z']], axis=1)
|
|
74
|
+
cloud = np.rec.fromarrays([xyz, irgb], dtype=dtype)
|
|
75
|
+
return cloud, color_mode
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def save_e57(cloud, save_path):
|
|
79
|
+
e57 = E57(save_path, mode='w')
|
|
80
|
+
x = cloud['xyz'][:, 0]
|
|
81
|
+
y = cloud['xyz'][:, 1]
|
|
82
|
+
z = cloud['xyz'][:, 2]
|
|
83
|
+
i = (cloud['irgb'] & 0xFF000000) >> 24
|
|
84
|
+
r = (cloud['irgb'] & 0x00FF0000) >> 16
|
|
85
|
+
g = (cloud['irgb'] & 0x0000FF00) >> 8
|
|
86
|
+
b = (cloud['irgb'] & 0x000000ff)
|
|
87
|
+
data = {"cartesianX": x, "cartesianY": y, "cartesianZ": z,
|
|
88
|
+
"intensity": i,
|
|
89
|
+
"colorRed": r, "colorGreen": g, "colorBlue": b}
|
|
90
|
+
e57.write_scan_raw(data)
|
|
91
|
+
e57.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_e57(file_path):
|
|
95
|
+
e57 = E57(file_path, mode="r")
|
|
96
|
+
scans = e57.read_scan(0, ignore_missing_fields=True,
|
|
97
|
+
intensity=True, colors=True)
|
|
98
|
+
x = scans["cartesianX"]
|
|
99
|
+
y = scans["cartesianY"]
|
|
100
|
+
z = scans["cartesianZ"]
|
|
101
|
+
rgb = np.zeros([x.shape[0]], dtype=np.uint32)
|
|
102
|
+
intensity = np.zeros([x.shape[0]], dtype=np.uint32)
|
|
103
|
+
color_mode = 'FLAT'
|
|
104
|
+
if "intensity" in scans:
|
|
105
|
+
intensity = scans["intensity"].astype(np.uint32)
|
|
106
|
+
color_mode = 'I'
|
|
107
|
+
if all([x in scans for x in ["colorRed", "colorGreen", "colorBlue"]]):
|
|
108
|
+
r = scans["colorRed"].astype(np.uint32)
|
|
109
|
+
g = scans["colorGreen"].astype(np.uint32)
|
|
110
|
+
b = scans["colorBlue"].astype(np.uint32)
|
|
111
|
+
rgb = (r << 16) | (g << 8) | b
|
|
112
|
+
color_mode = 'RGB'
|
|
113
|
+
irgb = (intensity << 24) | rgb
|
|
114
|
+
dtype = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
115
|
+
cloud = np.rec.fromarrays(
|
|
116
|
+
[np.stack([x, y, z], axis=1), irgb],
|
|
117
|
+
dtype=dtype)
|
|
118
|
+
e57.close()
|
|
119
|
+
return cloud, color_mode
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_las(file):
|
|
123
|
+
with laspy.open(file) as f:
|
|
124
|
+
las = f.read()
|
|
125
|
+
xyz = np.vstack((las.x, las.y, las.z)).transpose()
|
|
126
|
+
dimensions = list(las.point_format.dimension_names)
|
|
127
|
+
color_mode = 'FLAT'
|
|
128
|
+
rgb = np.zeros([las.x.shape[0]], dtype=np.uint32)
|
|
129
|
+
intensity = np.zeros([las.x.shape[0]], dtype=np.uint32)
|
|
130
|
+
if 'intensity' in dimensions:
|
|
131
|
+
intensity = las.intensity.astype(np.uint32)
|
|
132
|
+
color_mode = 'I'
|
|
133
|
+
if 'red' in dimensions and 'green' in dimensions and 'blue' in dimensions:
|
|
134
|
+
red = las.red
|
|
135
|
+
green = las.green
|
|
136
|
+
blue = las.blue
|
|
137
|
+
max_val = np.max([red, green, blue])
|
|
138
|
+
if red.dtype == np.dtype('uint16') and max_val > 255:
|
|
139
|
+
red = (red / 65535.0 * 255).astype(np.uint32)
|
|
140
|
+
green = (green / 65535.0 * 255).astype(np.uint32)
|
|
141
|
+
blue = (blue / 65535.0 * 255).astype(np.uint32)
|
|
142
|
+
rgb = (red << 16) | (green << 8) | blue
|
|
143
|
+
color_mode = 'RGB'
|
|
144
|
+
color = (intensity << 24) | rgb
|
|
145
|
+
dtype = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
146
|
+
cloud = np.rec.fromarrays([xyz, color], dtype=dtype)
|
|
147
|
+
return cloud, color_mode
|
|
148
|
+
|
|
149
|
+
def save_las(cloud, save_path):
|
|
150
|
+
header = laspy.LasHeader(point_format=3, version="1.2")
|
|
151
|
+
las = laspy.LasData(header)
|
|
152
|
+
las.x = cloud['xyz'][:, 0]
|
|
153
|
+
las.y = cloud['xyz'][:, 1]
|
|
154
|
+
las.z = cloud['xyz'][:, 2]
|
|
155
|
+
las.red = (cloud['irgb'] >> 16) & 0xFF
|
|
156
|
+
las.green = (cloud['irgb'] >> 8) & 0xFF
|
|
157
|
+
las.blue = cloud['irgb'] & 0xFF
|
|
158
|
+
las.intensity = cloud['irgb'] >> 24
|
|
159
|
+
las.write(save_path)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def gsdata_type(sh_dim):
|
|
163
|
+
return [('pw', '<f4', (3,)),
|
|
164
|
+
('rot', '<f4', (4,)),
|
|
165
|
+
('scale', '<f4', (3,)),
|
|
166
|
+
('alpha', '<f4'),
|
|
167
|
+
('sh', '<f4', (sh_dim))]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def matrix_to_quaternion_wxyz(matrices):
|
|
171
|
+
m00, m01, m02 = matrices[:, 0, 0], matrices[:, 0, 1], matrices[:, 0, 2]
|
|
172
|
+
m10, m11, m12 = matrices[:, 1, 0], matrices[:, 1, 1], matrices[:, 1, 2]
|
|
173
|
+
m20, m21, m22 = matrices[:, 2, 0], matrices[:, 2, 1], matrices[:, 2, 2]
|
|
174
|
+
t = 1 + m00 + m11 + m22
|
|
175
|
+
s = np.ones_like(m00)
|
|
176
|
+
w = np.ones_like(m00)
|
|
177
|
+
x = np.ones_like(m00)
|
|
178
|
+
y = np.ones_like(m00)
|
|
179
|
+
z = np.ones_like(m00)
|
|
180
|
+
|
|
181
|
+
t_positive = t > 0.0000001
|
|
182
|
+
s[t_positive] = 0.5 / np.sqrt(t[t_positive])
|
|
183
|
+
w[t_positive] = 0.25 / s[t_positive]
|
|
184
|
+
x[t_positive] = (m21[t_positive] - m12[t_positive]) * s[t_positive]
|
|
185
|
+
y[t_positive] = (m02[t_positive] - m20[t_positive]) * s[t_positive]
|
|
186
|
+
z[t_positive] = (m10[t_positive] - m01[t_positive]) * s[t_positive]
|
|
187
|
+
|
|
188
|
+
c1 = np.logical_and(m00 > m11, m00 > m22)
|
|
189
|
+
cond1 = np.logical_and(np.logical_not(t_positive),
|
|
190
|
+
np.logical_and(m00 > m11, m00 > m22))
|
|
191
|
+
|
|
192
|
+
s[cond1] = 2.0 * np.sqrt(1.0 + m00[cond1] - m11[cond1] - m22[cond1])
|
|
193
|
+
w[cond1] = (m21[cond1] - m12[cond1]) / s[cond1]
|
|
194
|
+
x[cond1] = 0.25 * s[cond1]
|
|
195
|
+
y[cond1] = (m01[cond1] + m10[cond1]) / s[cond1]
|
|
196
|
+
z[cond1] = (m02[cond1] + m20[cond1]) / s[cond1]
|
|
197
|
+
|
|
198
|
+
c2 = np.logical_and(np.logical_not(c1), m11 > m22)
|
|
199
|
+
cond2 = np.logical_and(np.logical_not(t_positive), c2)
|
|
200
|
+
s[cond2] = 2.0 * np.sqrt(1.0 + m11[cond2] - m00[cond2] - m22[cond2])
|
|
201
|
+
w[cond2] = (m02[cond2] - m20[cond2]) / s[cond2]
|
|
202
|
+
x[cond2] = (m01[cond2] + m10[cond2]) / s[cond2]
|
|
203
|
+
y[cond2] = 0.25 * s[cond2]
|
|
204
|
+
z[cond2] = (m12[cond2] + m21[cond2]) / s[cond2]
|
|
205
|
+
|
|
206
|
+
c3 = np.logical_and(np.logical_not(c1), np.logical_not(c2))
|
|
207
|
+
cond3 = np.logical_and(np.logical_not(t_positive), c3)
|
|
208
|
+
s[cond3] = 2.0 * np.sqrt(1.0 + m22[cond3] - m00[cond3] - m11[cond3])
|
|
209
|
+
w[cond3] = (m10[cond3] - m01[cond3]) / s[cond3]
|
|
210
|
+
x[cond3] = (m02[cond3] + m20[cond3]) / s[cond3]
|
|
211
|
+
y[cond3] = (m12[cond3] + m21[cond3]) / s[cond3]
|
|
212
|
+
z[cond3] = 0.25 * s[cond3]
|
|
213
|
+
return np.array([w, x, y, z]).T
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def load_gs_ply(path, T=None):
|
|
217
|
+
mesh = meshio.read(path)
|
|
218
|
+
vertices = mesh.points
|
|
219
|
+
pws = vertices[:, :3]
|
|
220
|
+
|
|
221
|
+
alphas = mesh.point_data['opacity']
|
|
222
|
+
alphas = 1 / (1 + np.exp(-alphas))
|
|
223
|
+
|
|
224
|
+
scales = np.vstack((mesh.point_data['scale_0'],
|
|
225
|
+
mesh.point_data['scale_1'],
|
|
226
|
+
mesh.point_data['scale_2'])).T
|
|
227
|
+
|
|
228
|
+
rots = np.vstack((mesh.point_data['rot_0'],
|
|
229
|
+
mesh.point_data['rot_1'],
|
|
230
|
+
mesh.point_data['rot_2'],
|
|
231
|
+
mesh.point_data['rot_3'])).T
|
|
232
|
+
rots /= np.linalg.norm(rots, axis=1)[:, np.newaxis]
|
|
233
|
+
|
|
234
|
+
sh_dim = len(mesh.point_data) - 11
|
|
235
|
+
shs = np.zeros([pws.shape[0], sh_dim])
|
|
236
|
+
shs[:, 0] = mesh.point_data['f_dc_0']
|
|
237
|
+
shs[:, 1] = mesh.point_data['f_dc_1']
|
|
238
|
+
shs[:, 2] = mesh.point_data['f_dc_2']
|
|
239
|
+
|
|
240
|
+
sh_rest_dim = sh_dim - 3
|
|
241
|
+
if sh_rest_dim > 0:
|
|
242
|
+
for i in range(sh_rest_dim):
|
|
243
|
+
name = f"f_rest_{i}"
|
|
244
|
+
shs[:, 3 + i] = mesh.point_data[name]
|
|
245
|
+
shs[:, 3:] = shs[:, 3:].reshape(-1, 3, sh_rest_dim // 3).transpose([0, 2, 1]).reshape(-1, sh_rest_dim)
|
|
246
|
+
|
|
247
|
+
pws = pws.astype(np.float32)
|
|
248
|
+
rots = rots.astype(np.float32)
|
|
249
|
+
scales = np.exp(scales).astype(np.float32)
|
|
250
|
+
alphas = alphas.astype(np.float32)
|
|
251
|
+
shs = shs.astype(np.float32)
|
|
252
|
+
|
|
253
|
+
dtypes = gsdata_type(sh_dim)
|
|
254
|
+
|
|
255
|
+
gs = np.rec.fromarrays(
|
|
256
|
+
[pws, rots, scales, alphas, shs], dtype=dtypes)
|
|
257
|
+
|
|
258
|
+
return gs
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def rotate_gaussian(T, gs):
|
|
262
|
+
# Transform to world
|
|
263
|
+
pws = (T @ gs['pw'].T).T
|
|
264
|
+
w = gs['rot'][:, 0]
|
|
265
|
+
x = gs['rot'][:, 1]
|
|
266
|
+
y = gs['rot'][:, 2]
|
|
267
|
+
z = gs['rot'][:, 3]
|
|
268
|
+
R = np.array([
|
|
269
|
+
[1.0 - 2*(y**2 + z**2), 2*(x*y - z*w), 2*(x * z + y * w)],
|
|
270
|
+
[2*(x*y + z*w), 1.0 - 2*(x**2 + z**2), 2*(y*z - x*w)],
|
|
271
|
+
[2*(x*z - y*w), 2*(y*z + x*w), 1.0 - 2*(x**2 + y**2)]
|
|
272
|
+
]).transpose(2, 0, 1)
|
|
273
|
+
R_new = T @ R
|
|
274
|
+
rots = matrix_to_quaternion_wxyz(R_new)
|
|
275
|
+
gs['pw'] = pws
|
|
276
|
+
gs['rot'] = rots
|
|
277
|
+
return gs
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def load_gs(fn):
|
|
281
|
+
if fn.endswith('.ply'):
|
|
282
|
+
return load_gs_ply(fn)
|
|
283
|
+
elif fn.endswith('.npy'):
|
|
284
|
+
return np.load(fn)
|
|
285
|
+
else:
|
|
286
|
+
print("%s is not a supported file." % fn)
|
|
287
|
+
exit(0)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def save_gs(fn, gs):
|
|
291
|
+
np.save(fn, gs)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def get_example_gs():
|
|
295
|
+
gs_data = np.array([[0., 0., 0., # xyz
|
|
296
|
+
1., 0., 0., 0., # rot
|
|
297
|
+
0.05, 0.05, 0.05, # size
|
|
298
|
+
1.,
|
|
299
|
+
1.772484, -1.772484, 1.772484],
|
|
300
|
+
[1., 0., 0.,
|
|
301
|
+
1., 0., 0., 0.,
|
|
302
|
+
0.2, 0.05, 0.05,
|
|
303
|
+
1.,
|
|
304
|
+
1.772484, -1.772484, -1.772484],
|
|
305
|
+
[0., 1., 0.,
|
|
306
|
+
1., 0., 0., 0.,
|
|
307
|
+
0.05, 0.2, 0.05,
|
|
308
|
+
1.,
|
|
309
|
+
-1.772484, 1.772484, -1.772484],
|
|
310
|
+
[0., 0., 1.,
|
|
311
|
+
1., 0., 0., 0.,
|
|
312
|
+
0.05, 0.05, 0.2,
|
|
313
|
+
1.,
|
|
314
|
+
-1.772484, -1.772484, 1.772484]
|
|
315
|
+
], dtype=np.float32)
|
|
316
|
+
dtypes = gsdata_type(3)
|
|
317
|
+
gs = np.frombuffer(gs_data.tobytes(), dtype=dtypes)
|
|
318
|
+
return gs
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
gs = load_gs("/home/liu/tmp.ply")
|
|
323
|
+
print(gs.shape)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
|
|
3
|
+
Distributed under MIT license. See LICENSE for more information.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pypcd4 import PointCloud
|
|
8
|
+
from q3dviewer.utils.maths import make_transform
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def convert_pointcloud2_msg(msg):
|
|
12
|
+
pc = PointCloud.from_msg(msg).pc_data
|
|
13
|
+
data_type = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
14
|
+
rgb = np.zeros([pc.shape[0]], dtype=np.uint32)
|
|
15
|
+
intensity = np.zeros([pc.shape[0]], dtype=np.uint32)
|
|
16
|
+
fields = ['xyz']
|
|
17
|
+
if 'intensity' in pc.dtype.names:
|
|
18
|
+
intensity = pc['intensity'].astype(np.uint32)
|
|
19
|
+
fields.append('intensity')
|
|
20
|
+
if 'rgb' in pc.dtype.names:
|
|
21
|
+
rgb = pc['rgb'].view(np.uint32)
|
|
22
|
+
fields.append('rgb')
|
|
23
|
+
irgb = (intensity << 24) | rgb
|
|
24
|
+
xyz = np.stack([pc['x'], pc['y'], pc['z']], axis=1)
|
|
25
|
+
cloud = np.rec.fromarrays([xyz, irgb], dtype=data_type)
|
|
26
|
+
stamp = msg.header.stamp.to_sec()
|
|
27
|
+
return cloud, fields, stamp
|
|
28
|
+
|
|
29
|
+
def convert_odometry_msg(msg):
|
|
30
|
+
pose = np.array(
|
|
31
|
+
[msg.pose.pose.position.x,
|
|
32
|
+
msg.pose.pose.position.y,
|
|
33
|
+
msg.pose.pose.position.z])
|
|
34
|
+
rotation = np.array([
|
|
35
|
+
msg.pose.pose.orientation.x,
|
|
36
|
+
msg.pose.pose.orientation.y,
|
|
37
|
+
msg.pose.pose.orientation.z,
|
|
38
|
+
msg.pose.pose.orientation.w])
|
|
39
|
+
transform = make_transform(pose, rotation)
|
|
40
|
+
stamp = msg.header.stamp.to_sec()
|
|
41
|
+
return transform, stamp
|
|
42
|
+
|
|
43
|
+
def convert_image_msg(msg):
|
|
44
|
+
image = np.frombuffer(msg.data, dtype=np.uint8).reshape(
|
|
45
|
+
msg.height, msg.width, -1)
|
|
46
|
+
if (msg.encoding == 'bgr8'):
|
|
47
|
+
image = image[:, :, ::-1] # convert bgr to rgb
|
|
48
|
+
stamp = msg.header.stamp.to_sec()
|
|
49
|
+
return image, stamp
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
|
|
3
|
+
Distributed under MIT license. See LICENSE for more information.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from OpenGL.GL import *
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def set_uniform(shader, content, name):
|
|
11
|
+
location = glGetUniformLocation(shader, name)
|
|
12
|
+
if location == -1:
|
|
13
|
+
raise ValueError(
|
|
14
|
+
f"Uniform '{name}' not found in shader program {shader}.")
|
|
15
|
+
|
|
16
|
+
if isinstance(content, int):
|
|
17
|
+
glUniform1i(location, content)
|
|
18
|
+
elif isinstance(content, float):
|
|
19
|
+
glUniform1f(location, content)
|
|
20
|
+
elif isinstance(content, np.ndarray):
|
|
21
|
+
if content.ndim == 1:
|
|
22
|
+
if content.shape[0] == 2:
|
|
23
|
+
glUniform2f(location, *content)
|
|
24
|
+
elif content.shape[0] == 3:
|
|
25
|
+
glUniform3f(location, *content)
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"Unsupported 1D array size: {content.shape}.")
|
|
29
|
+
elif content.ndim == 2:
|
|
30
|
+
if content.shape == (4, 4):
|
|
31
|
+
glUniformMatrix4fv(location, 1, GL_FALSE,
|
|
32
|
+
content.T.astype(np.float32))
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Unsupported 2D array size: {content.shape}.")
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(f"Unsupported array dimension: {content.ndim}.")
|
|
38
|
+
else:
|
|
39
|
+
raise TypeError(
|
|
40
|
+
f"Unsupported type for uniform '{name}': {type(content)}.")
|
q3dviewer/utils/maths.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
|
|
3
|
+
Distributed under MIT license. See LICENSE for more information.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def frustum(left, right, bottom, top, near, far):
|
|
10
|
+
# see https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
|
|
11
|
+
if near <= 0 or far <= 0 or near >= far or left == right or bottom == top:
|
|
12
|
+
print("Invalid frustum parameters.")
|
|
13
|
+
return None
|
|
14
|
+
matrix = np.zeros((4, 4), dtype=np.float32)
|
|
15
|
+
matrix[0, 0] = 2.0 * near / (right - left)
|
|
16
|
+
matrix[0, 2] = (right + left) / (right - left)
|
|
17
|
+
matrix[1, 1] = 2.0 * near / (top - bottom)
|
|
18
|
+
matrix[1, 2] = (top + bottom) / (top - bottom)
|
|
19
|
+
matrix[2, 2] = -(far + near) / (far - near)
|
|
20
|
+
matrix[2, 3] = -2.0 * far * near / (far - near)
|
|
21
|
+
matrix[3, 2] = -1.0
|
|
22
|
+
return matrix
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rainbow(scalars, scalar_min=0, scalar_max=255):
|
|
26
|
+
range = scalar_max - scalar_min
|
|
27
|
+
values = 1.0 - (scalars - scalar_min) / range
|
|
28
|
+
# values = (scalars - scalar_min) / range # using inverted color
|
|
29
|
+
colors = np.zeros([scalars.shape[0], 3], dtype=np.float32)
|
|
30
|
+
values = np.clip(values, 0, 1)
|
|
31
|
+
|
|
32
|
+
h = values * 5.0 + 1.0
|
|
33
|
+
i = np.floor(h).astype(int)
|
|
34
|
+
f = h - i
|
|
35
|
+
f[np.logical_not(i % 2)] = 1 - f[np.logical_not(i % 2)]
|
|
36
|
+
n = 1 - f
|
|
37
|
+
|
|
38
|
+
# idx = i <= 1
|
|
39
|
+
colors[i <= 1, 0] = n[i <= 1] * 255
|
|
40
|
+
colors[i <= 1, 1] = 0
|
|
41
|
+
colors[i <= 1, 2] = 255
|
|
42
|
+
|
|
43
|
+
colors[i == 2, 0] = 0
|
|
44
|
+
colors[i == 2, 1] = n[i == 2] * 255
|
|
45
|
+
colors[i == 2, 2] = 255
|
|
46
|
+
|
|
47
|
+
colors[i == 3, 0] = 0
|
|
48
|
+
colors[i == 3, 1] = 255
|
|
49
|
+
colors[i == 3, 2] = n[i == 3] * 255
|
|
50
|
+
|
|
51
|
+
colors[i == 4, 0] = n[i == 4] * 255
|
|
52
|
+
colors[i == 4, 1] = 255
|
|
53
|
+
colors[i == 4, 2] = 0
|
|
54
|
+
|
|
55
|
+
colors[i >= 5, 0] = 255
|
|
56
|
+
colors[i >= 5, 1] = n[i >= 5] * 255
|
|
57
|
+
colors[i >= 5, 2] = 0
|
|
58
|
+
return colors
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def euler_to_matrix(rpy):
|
|
62
|
+
roll, pitch, yaw = rpy
|
|
63
|
+
Rx = np.array([[1, 0, 0],
|
|
64
|
+
[0, np.cos(roll), -np.sin(roll)],
|
|
65
|
+
[0, np.sin(roll), np.cos(roll)]])
|
|
66
|
+
Ry = np.array([[np.cos(pitch), 0, np.sin(pitch)],
|
|
67
|
+
[0, 1, 0],
|
|
68
|
+
[-np.sin(pitch), 0, np.cos(pitch)]])
|
|
69
|
+
Rz = np.array([[np.cos(yaw), -np.sin(yaw), 0],
|
|
70
|
+
[np.sin(yaw), np.cos(yaw), 0],
|
|
71
|
+
[0, 0, 1]])
|
|
72
|
+
R = Rz @ Ry @ Rx
|
|
73
|
+
return R
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def matrix_to_euler(R):
|
|
77
|
+
sy = np.sqrt(R[0, 0]**2 + R[1, 0]**2)
|
|
78
|
+
singular = sy < 1e-6 # Check for gimbal lock
|
|
79
|
+
if not singular:
|
|
80
|
+
roll = np.arctan2(R[2, 1], R[2, 2]) # X-axis rotation
|
|
81
|
+
pitch = np.arctan2(-R[2, 0], sy) # Y-axis rotation
|
|
82
|
+
yaw = np.arctan2(R[1, 0], R[0, 0]) # Z-axis rotation
|
|
83
|
+
else:
|
|
84
|
+
# Gimbal lock case
|
|
85
|
+
roll = np.arctan2(-R[1, 2], R[1, 1])
|
|
86
|
+
pitch = np.arctan2(-R[2, 0], sy)
|
|
87
|
+
yaw = 0 # Arbitrarily set yaw to 0
|
|
88
|
+
|
|
89
|
+
return np.array([roll, pitch, yaw])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def matrix_to_quaternion(matrix):
|
|
93
|
+
trace = matrix[0, 0] + matrix[1, 1] + matrix[2, 2]
|
|
94
|
+
if trace > 0:
|
|
95
|
+
s = 0.5 / np.sqrt(trace + 1.0)
|
|
96
|
+
w = 0.25 / s
|
|
97
|
+
x = (matrix[2, 1] - matrix[1, 2]) * s
|
|
98
|
+
y = (matrix[0, 2] - matrix[2, 0]) * s
|
|
99
|
+
z = (matrix[1, 0] - matrix[0, 1]) * s
|
|
100
|
+
else:
|
|
101
|
+
if matrix[0, 0] > matrix[1, 1] and matrix[0, 0] > matrix[2, 2]:
|
|
102
|
+
s = 2.0 * np.sqrt(1.0 + matrix[0, 0] - matrix[1, 1] - matrix[2, 2])
|
|
103
|
+
w = (matrix[2, 1] - matrix[1, 2]) / s
|
|
104
|
+
x = 0.25 * s
|
|
105
|
+
y = (matrix[0, 1] + matrix[1, 0]) / s
|
|
106
|
+
z = (matrix[0, 2] + matrix[2, 0]) / s
|
|
107
|
+
elif matrix[1, 1] > matrix[2, 2]:
|
|
108
|
+
s = 2.0 * np.sqrt(1.0 + matrix[1, 1] - matrix[0, 0] - matrix[2, 2])
|
|
109
|
+
w = (matrix[0, 2] - matrix[2, 0]) / s
|
|
110
|
+
x = (matrix[0, 1] + matrix[1, 0]) / s
|
|
111
|
+
y = 0.25 * s
|
|
112
|
+
z = (matrix[1, 2] + matrix[2, 1]) / s
|
|
113
|
+
else:
|
|
114
|
+
s = 2.0 * np.sqrt(1.0 + matrix[2, 2] - matrix[0, 0] - matrix[1, 1])
|
|
115
|
+
w = (matrix[1, 0] - matrix[0, 1]) / s
|
|
116
|
+
x = (matrix[0, 2] + matrix[2, 0]) / s
|
|
117
|
+
y = (matrix[1, 2] + matrix[2, 1]) / s
|
|
118
|
+
z = 0.25 * s
|
|
119
|
+
return np.array([x, y, z, w])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def quaternion_to_matrix(quaternion):
|
|
123
|
+
x, y, z, w = quaternion
|
|
124
|
+
q = np.array(quaternion[:4], dtype=np.float64, copy=True)
|
|
125
|
+
n = np.linalg.norm(q)
|
|
126
|
+
if np.any(n == 0.0):
|
|
127
|
+
raise ZeroDivisionError("bad quaternion input")
|
|
128
|
+
else:
|
|
129
|
+
m = np.empty((3, 3))
|
|
130
|
+
m[0, 0] = 1.0 - 2*(y**2 + z**2)/n
|
|
131
|
+
m[0, 1] = 2*(x*y - z*w)/n
|
|
132
|
+
m[0, 2] = 2*(x*z + y*w)/n
|
|
133
|
+
m[1, 0] = 2*(x*y + z*w)/n
|
|
134
|
+
m[1, 1] = 1.0 - 2*(x**2 + z**2)/n
|
|
135
|
+
m[1, 2] = 2*(y*z - x*w)/n
|
|
136
|
+
m[2, 0] = 2*(x*z - y*w)/n
|
|
137
|
+
m[2, 1] = 2*(y*z + x*w)/n
|
|
138
|
+
m[2, 2] = 1.0 - 2*(x**2 + y**2)/n
|
|
139
|
+
return m
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def make_transform(pose, rotation):
|
|
143
|
+
transform = np.eye(4)
|
|
144
|
+
transform[0:3, 0:3] = quaternion_to_matrix(rotation)
|
|
145
|
+
transform[0:3, 3] = pose
|
|
146
|
+
return transform
|
|
147
|
+
|
|
148
|
+
def makeT(R, t):
|
|
149
|
+
T = np.eye(4)
|
|
150
|
+
T[0:3, 0:3] = R
|
|
151
|
+
T[0:3, 3] = t
|
|
152
|
+
return T
|
|
153
|
+
|
|
154
|
+
def makeRt(T):
|
|
155
|
+
R = T[0:3, 0:3]
|
|
156
|
+
t = T[0:3, 3]
|
|
157
|
+
return R, t
|
|
158
|
+
|
|
159
|
+
def hex_to_rgba(hex_color):
|
|
160
|
+
color_flat = int(hex_color[1:], 16)
|
|
161
|
+
red = (color_flat >> 16) & 0xFF
|
|
162
|
+
green = (color_flat >> 8) & 0xFF
|
|
163
|
+
blue = color_flat & 0xFF
|
|
164
|
+
return (red / 255.0, green / 255.0, blue / 255.0, 1.0)
|
|
165
|
+
|
|
166
|
+
# euler = np.array([1, 0.1, 0.1])
|
|
167
|
+
# euler_angles = matrix_to_euler(euler_to_matrix(euler))
|
|
168
|
+
# print("Euler Angles:", euler_angles)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 Panasonic Advanced Technology Development Co.,Ltd. (Liu Yang)
|
|
3
|
+
Distributed under MIT license. See LICENSE for more information.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import Qt, Signal
|
|
7
|
+
from PySide6.QtGui import QPainter, QColor
|
|
8
|
+
from PySide6.QtWidgets import QSlider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RangeSlider(QSlider):
|
|
12
|
+
# Signal emitted when the range changes
|
|
13
|
+
rangeChanged = Signal(int, int)
|
|
14
|
+
|
|
15
|
+
def __init__(self, orientation=Qt.Horizontal,
|
|
16
|
+
parent=None, vmin=0, vmax=255):
|
|
17
|
+
super().__init__(orientation, parent)
|
|
18
|
+
self.setMinimum(vmin)
|
|
19
|
+
self.setMaximum(vmax)
|
|
20
|
+
self.lower_value = vmin
|
|
21
|
+
self.upper_value = vmax
|
|
22
|
+
self.active_handle = None
|
|
23
|
+
self.setTickPosition(QSlider.NoTicks) # Hide original ticks
|
|
24
|
+
# Hide slider handle
|
|
25
|
+
self.setStyleSheet("QSlider::handle { background: transparent; }")
|
|
26
|
+
|
|
27
|
+
def mousePressEvent(self, event):
|
|
28
|
+
"""Override to handle which handle is selected."""
|
|
29
|
+
pos = self.pixelPosToValue(event.pos())
|
|
30
|
+
if abs(pos - self.lower_value) < abs(pos - self.upper_value):
|
|
31
|
+
self.active_handle = "lower"
|
|
32
|
+
else:
|
|
33
|
+
self.active_handle = "upper"
|
|
34
|
+
|
|
35
|
+
def mouseMoveEvent(self, event):
|
|
36
|
+
"""Override to update handle positions."""
|
|
37
|
+
if event.buttons() != Qt.LeftButton:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
pos = self.pixelPosToValue(event.pos())
|
|
41
|
+
if self.active_handle == "lower":
|
|
42
|
+
self.lower_value = max(
|
|
43
|
+
self.minimum(), min(pos, self.upper_value - 1))
|
|
44
|
+
elif self.active_handle == "upper":
|
|
45
|
+
self.upper_value = min(
|
|
46
|
+
self.maximum(), max(pos, self.lower_value + 1))
|
|
47
|
+
self.rangeChanged.emit(self.lower_value, self.upper_value)
|
|
48
|
+
self.update()
|
|
49
|
+
|
|
50
|
+
def paintEvent(self, event):
|
|
51
|
+
"""Override to paint custom range handles."""
|
|
52
|
+
painter = QPainter(self)
|
|
53
|
+
|
|
54
|
+
# Draw the range bar
|
|
55
|
+
bar_color = QColor(200, 200, 200) # Gray bar
|
|
56
|
+
highlight_color = QColor(100, 100, 255) # Blue for selected range
|
|
57
|
+
painter.setPen(Qt.NoPen)
|
|
58
|
+
|
|
59
|
+
bar_height = 6
|
|
60
|
+
bar_y = self.height() // 2 - bar_height // 2
|
|
61
|
+
painter.setBrush(bar_color)
|
|
62
|
+
painter.drawRect(0, bar_y, self.width(), bar_height)
|
|
63
|
+
|
|
64
|
+
# Draw the selected range
|
|
65
|
+
lower_x = int(self.valueToPixelPos(self.lower_value))
|
|
66
|
+
upper_x = int(self.valueToPixelPos(self.upper_value))
|
|
67
|
+
painter.setBrush(highlight_color)
|
|
68
|
+
painter.drawRect(lower_x, bar_y, upper_x - lower_x, bar_height)
|
|
69
|
+
|
|
70
|
+
# Draw the range handles
|
|
71
|
+
handle_color = QColor(50, 50, 255) # Blue handles
|
|
72
|
+
painter.setBrush(handle_color)
|
|
73
|
+
painter.drawEllipse(lower_x - 5, bar_y - 4, 12, 12)
|
|
74
|
+
painter.drawEllipse(upper_x - 5, bar_y - 4, 12, 12)
|
|
75
|
+
|
|
76
|
+
painter.end()
|
|
77
|
+
|
|
78
|
+
def pixelPosToValue(self, pos):
|
|
79
|
+
"""Convert pixel position to slider value."""
|
|
80
|
+
return self.minimum() + (self.maximum() - self.minimum()) \
|
|
81
|
+
* pos.x() / self.width()
|
|
82
|
+
|
|
83
|
+
def valueToPixelPos(self, value):
|
|
84
|
+
"""Convert slider value to pixel position."""
|
|
85
|
+
return self.width() * (value - self.minimum()) /\
|
|
86
|
+
(self.maximum() - self.minimum())
|