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,254 @@
|
|
|
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 q3dviewer.base_item import BaseItem
|
|
8
|
+
from OpenGL.GL import *
|
|
9
|
+
import numpy as np
|
|
10
|
+
import os
|
|
11
|
+
from PySide6.QtWidgets import QComboBox, QLabel
|
|
12
|
+
from OpenGL.GL import shaders
|
|
13
|
+
from q3dviewer.utils import *
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def div_round_up(x, y):
|
|
17
|
+
return int((x + y - 1) / y)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GaussianItem(BaseItem):
|
|
21
|
+
def __init__(self, **kwds):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.need_updateGS = False
|
|
24
|
+
self.sh_dim = 0
|
|
25
|
+
self.gs_data = np.empty([0])
|
|
26
|
+
self.prev_Rz = np.array([np.inf, np.inf, np.inf])
|
|
27
|
+
self.path = os.path.dirname(__file__)
|
|
28
|
+
try:
|
|
29
|
+
import torch
|
|
30
|
+
if not torch.cuda.is_available():
|
|
31
|
+
raise ImportError
|
|
32
|
+
self.cuda_pw = None
|
|
33
|
+
self.sort = self.torch_sort
|
|
34
|
+
except ImportError:
|
|
35
|
+
self.sort = self.openg_sort
|
|
36
|
+
|
|
37
|
+
def add_setting(self, layout):
|
|
38
|
+
label1 = QLabel("set render mode:")
|
|
39
|
+
layout.addWidget(label1)
|
|
40
|
+
combo = QComboBox()
|
|
41
|
+
combo.addItem("render normal guassian")
|
|
42
|
+
combo.addItem("render ball")
|
|
43
|
+
combo.addItem("render inverse guassian")
|
|
44
|
+
combo.currentIndexChanged.connect(self.onComboboxSelection)
|
|
45
|
+
layout.addWidget(combo)
|
|
46
|
+
|
|
47
|
+
def onComboboxSelection(self, index):
|
|
48
|
+
glUseProgram(self.program)
|
|
49
|
+
set_uniform(self.program, index, 'render_mod')
|
|
50
|
+
glUseProgram(0)
|
|
51
|
+
|
|
52
|
+
def initialize_gl(self):
|
|
53
|
+
fragment_shader = open(
|
|
54
|
+
self.path + '/../shaders/gau_frag.glsl', 'r').read()
|
|
55
|
+
vertex_shader = open(
|
|
56
|
+
self.path + '/../shaders/gau_vert.glsl', 'r').read()
|
|
57
|
+
sort_shader = open(
|
|
58
|
+
self.path + '/../shaders/sort_by_key.glsl', 'r').read()
|
|
59
|
+
prep_shader = open(self.path + '/../shaders/gau_prep.glsl', 'r').read()
|
|
60
|
+
|
|
61
|
+
self.sort_program = shaders.compileProgram(
|
|
62
|
+
shaders.compileShader(sort_shader, GL_COMPUTE_SHADER))
|
|
63
|
+
|
|
64
|
+
self.prep_program = shaders.compileProgram(
|
|
65
|
+
shaders.compileShader(prep_shader, GL_COMPUTE_SHADER))
|
|
66
|
+
|
|
67
|
+
self.program = shaders.compileProgram(
|
|
68
|
+
shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
|
|
69
|
+
shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER),
|
|
70
|
+
)
|
|
71
|
+
self.vao = glGenVertexArrays(1)
|
|
72
|
+
|
|
73
|
+
# trade a gaussian as a square (4 2d points)
|
|
74
|
+
square_vert = np.array([-1, 1, 1, 1, 1, -1, -1, -1], dtype=np.float32)
|
|
75
|
+
indices = np.array([0, 1, 2, 0, 2, 3], dtype=np.uint32)
|
|
76
|
+
|
|
77
|
+
# set the vertices for square
|
|
78
|
+
vbo = glGenBuffers(1)
|
|
79
|
+
glBindVertexArray(self.vao)
|
|
80
|
+
glBindBuffer(GL_ARRAY_BUFFER, vbo)
|
|
81
|
+
glBufferData(GL_ARRAY_BUFFER, square_vert.nbytes,
|
|
82
|
+
square_vert, GL_STATIC_DRAW)
|
|
83
|
+
pos = glGetAttribLocation(self.program, 'vert')
|
|
84
|
+
glVertexAttribPointer(pos, 2, GL_FLOAT, False, 0, None)
|
|
85
|
+
glEnableVertexAttribArray(pos)
|
|
86
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
87
|
+
|
|
88
|
+
# the vert's indices for drawing square
|
|
89
|
+
self.ebo = glGenBuffers(1)
|
|
90
|
+
glBindVertexArray(self.vao)
|
|
91
|
+
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.ebo)
|
|
92
|
+
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
|
93
|
+
indices.nbytes, indices, GL_STATIC_DRAW)
|
|
94
|
+
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
|
|
95
|
+
glBindVertexArray(0)
|
|
96
|
+
|
|
97
|
+
# add SSBO for gaussian data
|
|
98
|
+
self.ssbo_gs = glGenBuffers(1)
|
|
99
|
+
self.ssbo_gi = glGenBuffers(1)
|
|
100
|
+
self.ssbo_dp = glGenBuffers(1)
|
|
101
|
+
self.ssbo_pp = glGenBuffers(1)
|
|
102
|
+
|
|
103
|
+
width = self.glwidget().current_width()
|
|
104
|
+
height = self.glwidget().current_height()
|
|
105
|
+
|
|
106
|
+
# set constant parameter for gaussian shader
|
|
107
|
+
project_matrix = self.glwidget().get_projection_matrix()
|
|
108
|
+
focal_x = project_matrix[0, 0] * width / 2
|
|
109
|
+
focal_y = project_matrix[1, 1] * height / 2
|
|
110
|
+
glUseProgram(self.prep_program)
|
|
111
|
+
set_uniform(self.prep_program,
|
|
112
|
+
project_matrix, 'projection_matrix')
|
|
113
|
+
set_uniform(self.prep_program, np.array([focal_x, focal_y]), 'focal')
|
|
114
|
+
glUseProgram(0)
|
|
115
|
+
|
|
116
|
+
glUseProgram(self.program)
|
|
117
|
+
set_uniform(self.program, np.array([width, height]), 'win_size')
|
|
118
|
+
set_uniform(self.program, 0, 'render_mod')
|
|
119
|
+
glUseProgram(0)
|
|
120
|
+
|
|
121
|
+
# opengl settings
|
|
122
|
+
glDisable(GL_CULL_FACE)
|
|
123
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
124
|
+
|
|
125
|
+
def updateGS(self):
|
|
126
|
+
if (self.need_updateGS):
|
|
127
|
+
# compute sorting size
|
|
128
|
+
self.num_sort = int(2**np.ceil(np.log2(self.gs_data.shape[0])))
|
|
129
|
+
|
|
130
|
+
# set input gaussian data
|
|
131
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, self.ssbo_gs)
|
|
132
|
+
glBufferData(GL_SHADER_STORAGE_BUFFER, self.gs_data.nbytes,
|
|
133
|
+
self.gs_data.reshape(-1), GL_STATIC_DRAW)
|
|
134
|
+
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, self.ssbo_gs)
|
|
135
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0)
|
|
136
|
+
|
|
137
|
+
# set depth for sorting
|
|
138
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, self.ssbo_dp)
|
|
139
|
+
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
140
|
+
self.num_sort * 4, None, GL_STATIC_DRAW)
|
|
141
|
+
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, self.ssbo_dp)
|
|
142
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0)
|
|
143
|
+
|
|
144
|
+
# set index for sorting (the index need be initialized)
|
|
145
|
+
gi = np.arange(self.num_sort, dtype=np.uint32)
|
|
146
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, self.ssbo_gi)
|
|
147
|
+
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
148
|
+
self.num_sort * 4, gi, GL_STATIC_DRAW)
|
|
149
|
+
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, self.ssbo_gi)
|
|
150
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0)
|
|
151
|
+
|
|
152
|
+
# set preprocess buffer
|
|
153
|
+
# the dim of preprocess data is 12 u(3),
|
|
154
|
+
# covinv(3), color(3), area(2), alpha(1)
|
|
155
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, self.ssbo_pp)
|
|
156
|
+
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
157
|
+
self.gs_data.shape[0] * 4 * 12,
|
|
158
|
+
None, GL_STATIC_DRAW)
|
|
159
|
+
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, self.ssbo_pp)
|
|
160
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0)
|
|
161
|
+
|
|
162
|
+
glUseProgram(self.prep_program)
|
|
163
|
+
set_uniform(self.prep_program, self.sh_dim, 'sh_dim')
|
|
164
|
+
set_uniform(self.prep_program,
|
|
165
|
+
self.gs_data.shape[0], 'gs_num')
|
|
166
|
+
glUseProgram(0)
|
|
167
|
+
self.need_updateGS = False
|
|
168
|
+
|
|
169
|
+
def paint(self):
|
|
170
|
+
# get current view matrix
|
|
171
|
+
self.view_matrix = self.glwidget().get_view_matrix()
|
|
172
|
+
|
|
173
|
+
# if gaussian data is update, renew vao, ssbo, etc...
|
|
174
|
+
self.updateGS()
|
|
175
|
+
|
|
176
|
+
if (self.gs_data.shape[0] == 0):
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# preprocess and sort gaussian by compute shader.
|
|
180
|
+
self.preprocessGS()
|
|
181
|
+
self.try_sort()
|
|
182
|
+
glEnable(GL_BLEND)
|
|
183
|
+
# draw by vert shader
|
|
184
|
+
glUseProgram(self.program)
|
|
185
|
+
# bind vao and ebo
|
|
186
|
+
glBindVertexArray(self.vao)
|
|
187
|
+
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.ebo)
|
|
188
|
+
# draw instances
|
|
189
|
+
glDrawElementsInstanced(
|
|
190
|
+
GL_TRIANGLES, 6, GL_UNSIGNED_INT, None, self.gs_data.shape[0])
|
|
191
|
+
# upbind vao and ebo
|
|
192
|
+
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
|
|
193
|
+
glBindVertexArray(0)
|
|
194
|
+
glUseProgram(0)
|
|
195
|
+
glDisable(GL_BLEND)
|
|
196
|
+
|
|
197
|
+
def try_sort(self):
|
|
198
|
+
# don't sort if the depths are not change.
|
|
199
|
+
Rz = self.view_matrix[2, :3]
|
|
200
|
+
if (np.linalg.norm(self.prev_Rz - Rz) > 0.1):
|
|
201
|
+
# import torch
|
|
202
|
+
# torch.cuda.synchronize()
|
|
203
|
+
# start = time.time()
|
|
204
|
+
self.sort()
|
|
205
|
+
self.prev_Rz = Rz
|
|
206
|
+
# torch.cuda.synchronize()
|
|
207
|
+
# end = time.time()
|
|
208
|
+
# time_diff = end - start
|
|
209
|
+
# print(time_diff)
|
|
210
|
+
|
|
211
|
+
def openg_sort(self):
|
|
212
|
+
glUseProgram(self.sort_program)
|
|
213
|
+
# can we move this loop to gpu?
|
|
214
|
+
# level = level*2
|
|
215
|
+
for level in 2**np.arange(1, int(np.ceil(np.log2(self.num_sort))+1)):
|
|
216
|
+
# stage =stage / 2
|
|
217
|
+
for stage in level/2**np.arange(1, np.log2(level)+1):
|
|
218
|
+
set_uniform(self.sort_program, int(level), 'level')
|
|
219
|
+
set_uniform(self.sort_program, int(stage), 'stage')
|
|
220
|
+
glDispatchCompute(div_round_up(self.num_sort//2, 256), 1, 1)
|
|
221
|
+
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT)
|
|
222
|
+
# glFinish()
|
|
223
|
+
glUseProgram(0)
|
|
224
|
+
|
|
225
|
+
def torch_sort(self):
|
|
226
|
+
import torch
|
|
227
|
+
if self.cuda_pw is None:
|
|
228
|
+
self.cuda_pw = torch.tensor(self.gs_data[:, :3]).cuda()
|
|
229
|
+
Rz = torch.tensor(self.view_matrix[2, :3].astype(np.float32)).cuda()
|
|
230
|
+
depth = Rz @ self.cuda_pw.T
|
|
231
|
+
index = torch.argsort(depth).type(torch.int32).cpu().numpy()
|
|
232
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, self.ssbo_gi)
|
|
233
|
+
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
234
|
+
index.nbytes, index, GL_STATIC_DRAW)
|
|
235
|
+
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, self.ssbo_gi)
|
|
236
|
+
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0)
|
|
237
|
+
return index
|
|
238
|
+
|
|
239
|
+
def preprocessGS(self):
|
|
240
|
+
glUseProgram(self.prep_program)
|
|
241
|
+
set_uniform(self.prep_program, self.view_matrix, 'view_matrix')
|
|
242
|
+
glDispatchCompute(div_round_up(self.gs_data.shape[0], 256), 1, 1)
|
|
243
|
+
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT)
|
|
244
|
+
glUseProgram(0)
|
|
245
|
+
|
|
246
|
+
def set_data(self, **kwds):
|
|
247
|
+
if 'gs_data' in kwds:
|
|
248
|
+
self.need_updateGS = False
|
|
249
|
+
gs_data = kwds.pop('gs_data')
|
|
250
|
+
self.gs_data = np.ascontiguousarray(gs_data, dtype=np.float32)
|
|
251
|
+
self.sh_dim = self.gs_data.shape[-1] - (3 + 4 + 3 + 1)
|
|
252
|
+
self.prev_Rz = np.array([np.inf, np.inf, np.inf])
|
|
253
|
+
self.cuda_pw = None
|
|
254
|
+
self.need_updateGS = True
|
|
@@ -0,0 +1,88 @@
|
|
|
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 q3dviewer.base_item import BaseItem
|
|
7
|
+
from OpenGL.GL import *
|
|
8
|
+
from PySide6.QtWidgets import QLabel, QDoubleSpinBox, QLineEdit
|
|
9
|
+
from PySide6.QtCore import QRegularExpression
|
|
10
|
+
from PySide6.QtGui import QRegularExpressionValidator
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GridItem(BaseItem):
|
|
15
|
+
def __init__(self, size, spacing, color=np.array([255, 255, 255, 76.5]), offset=np.array([0, 0, 0])):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.size = size
|
|
18
|
+
self.spacing = spacing
|
|
19
|
+
self.color = color
|
|
20
|
+
self.offset = offset
|
|
21
|
+
|
|
22
|
+
def add_setting(self, layout):
|
|
23
|
+
label_size = QLabel("Set size:")
|
|
24
|
+
layout.addWidget(label_size)
|
|
25
|
+
spinbox_size = QDoubleSpinBox()
|
|
26
|
+
spinbox_size.setSingleStep(1.0)
|
|
27
|
+
layout.addWidget(spinbox_size)
|
|
28
|
+
spinbox_size.setValue(self.size)
|
|
29
|
+
spinbox_size.valueChanged.connect(self.set_size)
|
|
30
|
+
spinbox_size.setRange(0, 100000)
|
|
31
|
+
|
|
32
|
+
label_spacing = QLabel("Set spacing:")
|
|
33
|
+
layout.addWidget(label_spacing)
|
|
34
|
+
spinbox_spacing = QDoubleSpinBox()
|
|
35
|
+
layout.addWidget(spinbox_spacing)
|
|
36
|
+
spinbox_spacing.setSingleStep(0.1)
|
|
37
|
+
spinbox_spacing.setValue(self.spacing)
|
|
38
|
+
spinbox_spacing.valueChanged.connect(self._on_spacing)
|
|
39
|
+
spinbox_spacing.setRange(0, 1000)
|
|
40
|
+
|
|
41
|
+
label_offset = QLabel("Set offset (x;y;z):")
|
|
42
|
+
layout.addWidget(label_offset)
|
|
43
|
+
self.edit_offset = QLineEdit()
|
|
44
|
+
regex = QRegularExpression(r"^-?\d+(\.\d+)?;-?\d+(\.\d+)?;-?\d+(\.\d+)?$")
|
|
45
|
+
validator = QRegularExpressionValidator(regex)
|
|
46
|
+
self.edit_offset.setValidator(validator)
|
|
47
|
+
self.edit_offset.setText(f"{self.offset[0]};{self.offset[1]};{self.offset[2]}")
|
|
48
|
+
self.edit_offset.textChanged.connect(self._on_offset)
|
|
49
|
+
layout.addWidget(self.edit_offset)
|
|
50
|
+
|
|
51
|
+
def set_size(self, size):
|
|
52
|
+
self.size = size
|
|
53
|
+
|
|
54
|
+
def _on_spacing(self, spacing):
|
|
55
|
+
if spacing > 0:
|
|
56
|
+
self.spacing = spacing
|
|
57
|
+
|
|
58
|
+
def _on_offset(self, text):
|
|
59
|
+
try:
|
|
60
|
+
values = list(map(float, text.split(';')))
|
|
61
|
+
if len(values) == 3:
|
|
62
|
+
self.offset = np.array(values)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("Offset must have 3 values separated by ';'")
|
|
65
|
+
except ValueError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def set_offset(self, offset):
|
|
69
|
+
if isinstance(offset, np.ndarray) and offset.shape == (3,):
|
|
70
|
+
self.offset = offset
|
|
71
|
+
self.edit_offset.setText(f"{self.offset[0]};{self.offset[1]};{self.offset[2]}")
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError("Offset must be a numpy array with shape (3,)")
|
|
74
|
+
|
|
75
|
+
def paint(self):
|
|
76
|
+
glEnable(GL_BLEND)
|
|
77
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
78
|
+
glColor4f(self.color[0] / 255.0, self.color[1] / 255.0, self.color[2] / 255.0, self.color[3] / 255.0)
|
|
79
|
+
glBegin(GL_LINES)
|
|
80
|
+
for i in np.arange(-self.size, self.size + self.spacing, self.spacing):
|
|
81
|
+
glVertex3f(i + self.offset[0], -self.size + self.offset[1], self.offset[2])
|
|
82
|
+
glVertex3f(i + self.offset[0], self.size + self.offset[1], self.offset[2])
|
|
83
|
+
glVertex3f(-self.size + self.offset[0], i + self.offset[1], self.offset[2])
|
|
84
|
+
glVertex3f(self.size + self.offset[0], i + self.offset[1], self.offset[2])
|
|
85
|
+
glEnd()
|
|
86
|
+
glDisable(GL_BLEND)
|
|
87
|
+
|
|
88
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
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 q3dviewer.base_item import BaseItem
|
|
7
|
+
from OpenGL.GL import *
|
|
8
|
+
import numpy as np
|
|
9
|
+
from OpenGL.GL import shaders
|
|
10
|
+
from PIL import Image as PIL_Image
|
|
11
|
+
from PySide6.QtWidgets import QLabel, QSpinBox, QCheckBox
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Vertex and Fragment shader source code
|
|
15
|
+
vertex_shader_source = """
|
|
16
|
+
#version 330 core
|
|
17
|
+
layout(location = 0) in vec3 position;
|
|
18
|
+
layout(location = 1) in vec2 texCoord;
|
|
19
|
+
|
|
20
|
+
out vec2 TexCoord;
|
|
21
|
+
|
|
22
|
+
uniform mat4 view_matrix;
|
|
23
|
+
uniform mat4 project_matrix;
|
|
24
|
+
|
|
25
|
+
void main()
|
|
26
|
+
{
|
|
27
|
+
gl_Position = vec4(position, 1.0);
|
|
28
|
+
TexCoord = texCoord;
|
|
29
|
+
}
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
fragment_shader_source = """
|
|
33
|
+
#version 330 core
|
|
34
|
+
in vec2 TexCoord;
|
|
35
|
+
out vec4 color;
|
|
36
|
+
uniform sampler2D ourTexture;
|
|
37
|
+
void main()
|
|
38
|
+
{
|
|
39
|
+
color = texture(ourTexture, TexCoord);
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ImageItem(BaseItem):
|
|
45
|
+
def __init__(self, pos=np.array([0, 0]), size=np.array([1280/2, 720/2])):
|
|
46
|
+
BaseItem.__init__(self)
|
|
47
|
+
self.pos = pos # bottom-left
|
|
48
|
+
self.size = size
|
|
49
|
+
self.image = np.zeros((self.size[0], self.size[1], 4), dtype=np.uint8)
|
|
50
|
+
self.alpha = 255
|
|
51
|
+
|
|
52
|
+
def initialize_gl(self):
|
|
53
|
+
# Rectangle vertices and texture coordinates
|
|
54
|
+
width = self.glwidget().current_width()
|
|
55
|
+
height = self.glwidget().current_height()
|
|
56
|
+
x0, y0 = self.pos
|
|
57
|
+
x1, y1 = self.pos + self.size
|
|
58
|
+
x0 = x0 / width * 2 - 1
|
|
59
|
+
y0 = y0 / height * 2 - 1
|
|
60
|
+
x1 = x1 / width * 2 - 1
|
|
61
|
+
y1 = y1 / height * 2 - 1
|
|
62
|
+
|
|
63
|
+
self.vertices = np.array([
|
|
64
|
+
# positions # texture coords
|
|
65
|
+
[x0, y0, 0.0, 0.0, 0.0], # bottom-left
|
|
66
|
+
[x1, y0, 0.0, 1.0, 0.0], # bottom-right
|
|
67
|
+
[x1, y1, 0.0, 1.0, 1.0], # top-right
|
|
68
|
+
[x0, y1, 0.0, 0.0, 1.0], # top-left
|
|
69
|
+
], dtype=np.float32)
|
|
70
|
+
|
|
71
|
+
indices = np.array([
|
|
72
|
+
0, 1, 2, # first triangle
|
|
73
|
+
2, 3, 0 # second triangle
|
|
74
|
+
], dtype=np.uint32)
|
|
75
|
+
|
|
76
|
+
self.vao = glGenVertexArrays(1)
|
|
77
|
+
vbo = glGenBuffers(1)
|
|
78
|
+
ebo = glGenBuffers(1)
|
|
79
|
+
|
|
80
|
+
glBindVertexArray(self.vao)
|
|
81
|
+
|
|
82
|
+
glBindBuffer(GL_ARRAY_BUFFER, vbo)
|
|
83
|
+
glBufferData(GL_ARRAY_BUFFER, self.vertices.itemsize *
|
|
84
|
+
5 * 4, self.vertices, GL_STATIC_DRAW)
|
|
85
|
+
|
|
86
|
+
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
|
|
87
|
+
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
|
88
|
+
indices.nbytes, indices, GL_STATIC_DRAW)
|
|
89
|
+
|
|
90
|
+
# Vertex positions
|
|
91
|
+
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
|
|
92
|
+
20, ctypes.c_void_p(0))
|
|
93
|
+
glEnableVertexAttribArray(0)
|
|
94
|
+
|
|
95
|
+
# Texture coordinates
|
|
96
|
+
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
|
|
97
|
+
20, ctypes.c_void_p(12))
|
|
98
|
+
glEnableVertexAttribArray(1)
|
|
99
|
+
|
|
100
|
+
# Compile shaders and create shader program
|
|
101
|
+
self.program = shaders.compileProgram(
|
|
102
|
+
shaders.compileShader(vertex_shader_source, GL_VERTEX_SHADER),
|
|
103
|
+
shaders.compileShader(fragment_shader_source, GL_FRAGMENT_SHADER),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
self.texture = glGenTextures(1)
|
|
107
|
+
glBindTexture(GL_TEXTURE_2D, self.texture)
|
|
108
|
+
glBindVertexArray(0)
|
|
109
|
+
|
|
110
|
+
def set_data(self, data):
|
|
111
|
+
if isinstance(data, np.ndarray):
|
|
112
|
+
pass
|
|
113
|
+
elif isinstance(data, PIL_Image.Image):
|
|
114
|
+
data = np.array(data)
|
|
115
|
+
else:
|
|
116
|
+
print("not support image type")
|
|
117
|
+
raise NotImplementedError
|
|
118
|
+
|
|
119
|
+
if data.ndim == 2: # Grayscale image
|
|
120
|
+
data = np.stack((data,) * 3 + (np.ones_like(data) * 255,), axis=-1)
|
|
121
|
+
elif data.shape[-1] == 3: # RGB image
|
|
122
|
+
alpha_channel = np.ones(
|
|
123
|
+
(data.shape[0], data.shape[1], 1),
|
|
124
|
+
dtype=data.dtype) * self.alpha
|
|
125
|
+
data = np.concatenate((data, alpha_channel), axis=-1)
|
|
126
|
+
self.image = data
|
|
127
|
+
|
|
128
|
+
def paint(self):
|
|
129
|
+
if self.image is not None:
|
|
130
|
+
img_data = self.image
|
|
131
|
+
img_data = np.flipud(img_data) # Flip the image vertically
|
|
132
|
+
img_data = img_data.tobytes()
|
|
133
|
+
glBindTexture(GL_TEXTURE_2D, self.texture)
|
|
134
|
+
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.image.shape[1],
|
|
135
|
+
self.image.shape[0], 0,
|
|
136
|
+
GL_RGBA, GL_UNSIGNED_BYTE, img_data)
|
|
137
|
+
glGenerateMipmap(GL_TEXTURE_2D)
|
|
138
|
+
glBindTexture(GL_TEXTURE_2D, 0)
|
|
139
|
+
self.image = None
|
|
140
|
+
|
|
141
|
+
glEnable(GL_DEPTH_TEST)
|
|
142
|
+
glEnable(GL_BLEND)
|
|
143
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
144
|
+
|
|
145
|
+
glUseProgram(self.program)
|
|
146
|
+
glBindVertexArray(self.vao)
|
|
147
|
+
glBindTexture(GL_TEXTURE_2D, self.texture)
|
|
148
|
+
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)
|
|
149
|
+
glBindTexture(GL_TEXTURE_2D, 0)
|
|
150
|
+
glBindVertexArray(0)
|
|
151
|
+
glUseProgram(0)
|
|
152
|
+
|
|
153
|
+
glDisable(GL_DEPTH_TEST)
|
|
154
|
+
glDisable(GL_BLEND)
|
|
155
|
+
|
|
156
|
+
def add_setting(self, layout):
|
|
157
|
+
alpha_label = QLabel("Set Alpha:")
|
|
158
|
+
layout.addWidget(alpha_label)
|
|
159
|
+
spinbox_alpha = QSpinBox()
|
|
160
|
+
spinbox_alpha.setSingleStep(1)
|
|
161
|
+
spinbox_alpha.setRange(0, 255)
|
|
162
|
+
spinbox_alpha.setValue(self.alpha)
|
|
163
|
+
spinbox_alpha.valueChanged.connect(self.set_alpha)
|
|
164
|
+
layout.addWidget(spinbox_alpha)
|
|
165
|
+
|
|
166
|
+
checkbox_show = QCheckBox("Show Image")
|
|
167
|
+
checkbox_show.setChecked(True)
|
|
168
|
+
checkbox_show.stateChanged.connect(self.set_visible)
|
|
169
|
+
layout.addWidget(checkbox_show)
|
|
170
|
+
|
|
171
|
+
def set_alpha(self, alpha):
|
|
172
|
+
self.alpha = alpha
|
|
@@ -0,0 +1,120 @@
|
|
|
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 q3dviewer.base_item import BaseItem
|
|
7
|
+
from OpenGL.GL import *
|
|
8
|
+
import numpy as np
|
|
9
|
+
import threading
|
|
10
|
+
from PySide6.QtWidgets import QLabel, QLineEdit, QDoubleSpinBox
|
|
11
|
+
from PySide6.QtCore import QRegularExpression
|
|
12
|
+
from PySide6.QtGui import QRegularExpressionValidator
|
|
13
|
+
from q3dviewer.utils.maths import hex_to_rgba
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LineItem(BaseItem):
|
|
17
|
+
def __init__(self, width=1, color='#00ff00', line_type='LINE_STRIP'):
|
|
18
|
+
super(LineItem, self).__init__()
|
|
19
|
+
self.width = width
|
|
20
|
+
self.buff = np.empty((0, 3), np.float32)
|
|
21
|
+
self.wait_add_data = None
|
|
22
|
+
self.mutex = threading.Lock()
|
|
23
|
+
self.capacity = 100000
|
|
24
|
+
self.valid_buff_top = 0
|
|
25
|
+
self.color = color
|
|
26
|
+
self.rgb = hex_to_rgba(color)
|
|
27
|
+
self.line_type = GL_LINE_STRIP if line_type == 'LINE_STRIP' else GL_LINES
|
|
28
|
+
|
|
29
|
+
def add_setting(self, layout):
|
|
30
|
+
label_color = QLabel("Set trajectory color:")
|
|
31
|
+
layout.addWidget(label_color)
|
|
32
|
+
self.color_edit = QLineEdit()
|
|
33
|
+
self.color_edit.setToolTip("Hex number, i.e. #FF4500")
|
|
34
|
+
self.color_edit.setText(self.color)
|
|
35
|
+
self.color_edit.textChanged.connect(self._on_color)
|
|
36
|
+
regex = QRegularExpression(r"^#[0-9A-Fa-f]{6}$")
|
|
37
|
+
validator = QRegularExpressionValidator(regex)
|
|
38
|
+
self.color_edit.setValidator(validator)
|
|
39
|
+
layout.addWidget(self.color_edit)
|
|
40
|
+
|
|
41
|
+
label_width = QLabel("Set width:")
|
|
42
|
+
layout.addWidget(label_width)
|
|
43
|
+
spinbox_width = QDoubleSpinBox()
|
|
44
|
+
spinbox_width.setSingleStep(0.1)
|
|
45
|
+
layout.addWidget(spinbox_width)
|
|
46
|
+
spinbox_width.setValue(self.width)
|
|
47
|
+
spinbox_width.valueChanged.connect(self.set_width)
|
|
48
|
+
spinbox_width.setRange(0.1, 10.0)
|
|
49
|
+
|
|
50
|
+
def _on_color(self, color):
|
|
51
|
+
try:
|
|
52
|
+
self.rgb = hex_to_rgba(color)
|
|
53
|
+
self.color = color
|
|
54
|
+
except ValueError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def set_color(self, color):
|
|
58
|
+
self.color_edit.setText(color)
|
|
59
|
+
|
|
60
|
+
def set_width(self, width):
|
|
61
|
+
self.width = width
|
|
62
|
+
|
|
63
|
+
def set_data(self, data, append=False):
|
|
64
|
+
self.mutex.acquire()
|
|
65
|
+
data = data.astype(np.float32).reshape(-1, 3)
|
|
66
|
+
if (append is False):
|
|
67
|
+
self.wait_add_data = data
|
|
68
|
+
self.add_buff_loc = 0
|
|
69
|
+
else:
|
|
70
|
+
if (self.wait_add_data is None):
|
|
71
|
+
self.wait_add_data = data
|
|
72
|
+
else:
|
|
73
|
+
self.wait_add_data = np.concatenate([self.wait_add_data, data])
|
|
74
|
+
self.add_buff_loc = self.valid_buff_top
|
|
75
|
+
self.mutex.release()
|
|
76
|
+
|
|
77
|
+
def update_render_buffer(self):
|
|
78
|
+
if (self.wait_add_data is None):
|
|
79
|
+
return
|
|
80
|
+
self.mutex.acquire()
|
|
81
|
+
|
|
82
|
+
new_buff_top = self.add_buff_loc + self.wait_add_data.shape[0]
|
|
83
|
+
if new_buff_top > self.buff.shape[0]:
|
|
84
|
+
buff_capacity = self.buff.shape[0]
|
|
85
|
+
while (new_buff_top > buff_capacity):
|
|
86
|
+
buff_capacity += self.capacity
|
|
87
|
+
self.buff = np.empty((buff_capacity, 3), np.float32)
|
|
88
|
+
self.buff[self.add_buff_loc:new_buff_top] = self.wait_add_data
|
|
89
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
90
|
+
glBufferData(GL_ARRAY_BUFFER, self.buff.nbytes,
|
|
91
|
+
self.buff, GL_DYNAMIC_DRAW)
|
|
92
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
93
|
+
else:
|
|
94
|
+
self.buff[self.add_buff_loc:new_buff_top] = self.wait_add_data
|
|
95
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
96
|
+
glBufferSubData(GL_ARRAY_BUFFER, self.add_buff_loc * 12,
|
|
97
|
+
self.wait_add_data.shape[0] * 12,
|
|
98
|
+
self.wait_add_data)
|
|
99
|
+
self.valid_buff_top = new_buff_top
|
|
100
|
+
self.wait_add_data = None
|
|
101
|
+
self.mutex.release()
|
|
102
|
+
|
|
103
|
+
def initialize_gl(self):
|
|
104
|
+
self.vbo = glGenBuffers(1)
|
|
105
|
+
|
|
106
|
+
def paint(self):
|
|
107
|
+
self.update_render_buffer()
|
|
108
|
+
glEnable(GL_BLEND)
|
|
109
|
+
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
110
|
+
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
|
|
111
|
+
glEnableClientState(GL_VERTEX_ARRAY)
|
|
112
|
+
glVertexPointer(3, GL_FLOAT, 0, None)
|
|
113
|
+
glLineWidth(self.width)
|
|
114
|
+
glColor4f(*self.rgb)
|
|
115
|
+
|
|
116
|
+
glDrawArrays(self.line_type, 0, self.valid_buff_top)
|
|
117
|
+
glDisableClientState(GL_VERTEX_ARRAY)
|
|
118
|
+
|
|
119
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
120
|
+
glDisable(GL_BLEND)
|
|
@@ -0,0 +1,63 @@
|
|
|
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 import QtCore, QtGui
|
|
7
|
+
from q3dviewer.base_item import BaseItem
|
|
8
|
+
from OpenGL.GL import *
|
|
9
|
+
from q3dviewer.utils.maths import hex_to_rgba
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Text2DItem(BaseItem):
|
|
13
|
+
"""Draws text over opengl 3D."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwds):
|
|
16
|
+
"""All keyword arguments are passed to set_data()"""
|
|
17
|
+
BaseItem.__init__(self)
|
|
18
|
+
self.pos = (20, 50)
|
|
19
|
+
self.color = '#ffffff'
|
|
20
|
+
self.rgb = hex_to_rgba(self.color)
|
|
21
|
+
self.text = ''
|
|
22
|
+
self.font = QtGui.QFont('Helvetica', 16)
|
|
23
|
+
self.set_data(**kwds)
|
|
24
|
+
|
|
25
|
+
def set_data(self, **kwds):
|
|
26
|
+
args = ['pos', 'color', 'text', 'size', 'font']
|
|
27
|
+
for k in kwds.keys():
|
|
28
|
+
if k not in args:
|
|
29
|
+
raise ValueError('Invalid keyword argument: %s\
|
|
30
|
+
(allowed arguments are %s)' % (k, str(args)))
|
|
31
|
+
for arg in args:
|
|
32
|
+
if arg in kwds:
|
|
33
|
+
value = kwds[arg]
|
|
34
|
+
if arg == 'pos':
|
|
35
|
+
self.pos = value
|
|
36
|
+
elif arg == 'color':
|
|
37
|
+
self.set_color(value)
|
|
38
|
+
elif arg == 'font':
|
|
39
|
+
if isinstance(value, QtGui.QFont) is False:
|
|
40
|
+
raise TypeError('"font" must be QFont.')
|
|
41
|
+
elif arg == 'size':
|
|
42
|
+
self.font.setPointSize(value)
|
|
43
|
+
setattr(self, arg, value)
|
|
44
|
+
|
|
45
|
+
def set_color(self, color):
|
|
46
|
+
try:
|
|
47
|
+
self.rgb = hex_to_rgba(color)
|
|
48
|
+
self.color = color
|
|
49
|
+
except ValueError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def paint(self):
|
|
53
|
+
if len(self.text) < 1:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
text_pos = QtCore.QPointF(*self.pos)
|
|
57
|
+
painter = QtGui.QPainter(self.glwidget())
|
|
58
|
+
painter.setPen(QtGui.QColor(*[int(c * 255) for c in self.rgb[:3]]))
|
|
59
|
+
painter.setFont(self.font)
|
|
60
|
+
painter.setRenderHints(QtGui.QPainter.RenderHint.Antialiasing |
|
|
61
|
+
QtGui.QPainter.RenderHint.TextAntialiasing)
|
|
62
|
+
painter.drawText(text_pos, self.text)
|
|
63
|
+
painter.end()
|
q3dviewer/gau_io.py
ADDED
|
File without changes
|