q3dviewer 1.1.8__py3-none-any.whl → 1.2.0__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/Qt/__init__.py +1 -0
- q3dviewer/base_glwidget.py +31 -7
- q3dviewer/custom_items/cloud_item.py +4 -14
- q3dviewer/custom_items/text3d_item.py +49 -17
- q3dviewer/utils/range_slider.py +7 -5
- {q3dviewer-1.1.8.dist-info → q3dviewer-1.2.0.dist-info}/METADATA +11 -9
- {q3dviewer-1.1.8.dist-info → q3dviewer-1.2.0.dist-info}/RECORD +11 -17
- {q3dviewer-1.1.8.dist-info → q3dviewer-1.2.0.dist-info}/WHEEL +1 -1
- {q3dviewer-1.1.8.dist-info → q3dviewer-1.2.0.dist-info}/entry_points.txt +1 -0
- q3dviewer/.vscode/c_cpp_properties.json +0 -30
- q3dviewer/.vscode/settings.json +0 -10
- q3dviewer/gau_io.py +0 -0
- q3dviewer/test/test_interpolation.py +0 -58
- q3dviewer/test/test_rendering.py +0 -73
- q3dviewer/tools/cinematographer.py +0 -367
- {q3dviewer-1.1.8.dist-info → q3dviewer-1.2.0.dist-info}/LICENSE +0 -0
- {q3dviewer-1.1.8.dist-info → q3dviewer-1.2.0.dist-info}/top_level.txt +0 -0
q3dviewer/Qt/__init__.py
CHANGED
q3dviewer/base_glwidget.py
CHANGED
|
@@ -379,24 +379,48 @@ class BaseGLWidget(QOpenGLWidget):
|
|
|
379
379
|
return linear_depth
|
|
380
380
|
|
|
381
381
|
|
|
382
|
-
def get_point(self,
|
|
382
|
+
def get_point(self, x0, y0, radius=5):
|
|
383
|
+
"""
|
|
384
|
+
Get the 3D point in world coordinates corresponding to the given
|
|
385
|
+
screen coordinates (x0, y0). It searches within a radius around the
|
|
386
|
+
given pixel to find a valid depth value.
|
|
387
|
+
"""
|
|
383
388
|
self.makeCurrent() # Ensure the OpenGL context is current
|
|
384
389
|
width = self.current_width()
|
|
385
390
|
height = self.current_height()
|
|
386
391
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
392
|
+
# Scale mouse coordinates by device pixel ratio for PySide6 compatibility
|
|
393
|
+
pixel_ratio = self.devicePixelRatioF()
|
|
394
|
+
|
|
395
|
+
points = []
|
|
396
|
+
for dx in range(-radius, radius + 1):
|
|
397
|
+
for dy in range(-radius, radius + 1):
|
|
398
|
+
if dx * dx + dy * dy <= radius * radius:
|
|
399
|
+
points.append((x0 + dx, y0 + dy))
|
|
400
|
+
points = sorted(points, key=lambda p: (p[0]-x0)**2 + (p[1]-y0)**2)
|
|
401
|
+
|
|
402
|
+
gl_y0 = height - y0 - 1
|
|
403
|
+
z = 1.0
|
|
404
|
+
for x, y in points:
|
|
405
|
+
x = int(x * pixel_ratio)
|
|
406
|
+
y = int(y * pixel_ratio)
|
|
407
|
+
|
|
408
|
+
gl_y = height - y - 1
|
|
409
|
+
z = glReadPixels(x, gl_y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
|
|
410
|
+
z = np.frombuffer(z, dtype=np.float32)[0]
|
|
411
|
+
if z != 1.0 and z != 0.0:
|
|
412
|
+
break
|
|
413
|
+
|
|
390
414
|
if z == 1.0 or z == 0.0:
|
|
391
|
-
return None
|
|
415
|
+
return None
|
|
392
416
|
|
|
393
417
|
# Retrieve OpenGL matrices (column-major), convert to numpy arrays and transpose
|
|
394
418
|
view = np.array(glGetFloatv(GL_MODELVIEW_MATRIX), dtype=np.float32).reshape((4,4)).T
|
|
395
419
|
proj = np.array(glGetFloatv(GL_PROJECTION_MATRIX), dtype=np.float32).reshape((4,4)).T
|
|
396
420
|
|
|
397
421
|
# Convert screen (x, y, z) to normalized device coordinates (NDC)
|
|
398
|
-
ndc_x = (
|
|
399
|
-
ndc_y = (
|
|
422
|
+
ndc_x = (x0 / width) * 2.0 - 1.0
|
|
423
|
+
ndc_y = (gl_y0 / height) * 2.0 - 1.0
|
|
400
424
|
ndc_z = 2.0 * z - 1.0
|
|
401
425
|
ndc = np.array([ndc_x, ndc_y, ndc_z, 1.0], dtype=np.float32)
|
|
402
426
|
|
|
@@ -70,7 +70,6 @@ class CloudItem(BaseItem):
|
|
|
70
70
|
self.need_update_setting = True
|
|
71
71
|
self.max_cloud_size = 300000000
|
|
72
72
|
# Enable depth test when full opaque
|
|
73
|
-
self.depth_test = depth_test
|
|
74
73
|
self.path = os.path.dirname(__file__)
|
|
75
74
|
|
|
76
75
|
def add_setting(self, layout):
|
|
@@ -126,13 +125,6 @@ class CloudItem(BaseItem):
|
|
|
126
125
|
self.slider_v.rangeChanged.connect(self._on_range)
|
|
127
126
|
layout.addWidget(self.slider_v)
|
|
128
127
|
|
|
129
|
-
self.checkbox_depth_test = QCheckBox(
|
|
130
|
-
"Show front points first (Depth Test)")
|
|
131
|
-
self.checkbox_depth_test.setChecked(self.depth_test)
|
|
132
|
-
self.checkbox_depth_test.stateChanged.connect(self.set_depthtest)
|
|
133
|
-
self._on_color_mode(self.color_mode)
|
|
134
|
-
layout.addWidget(self.checkbox_depth_test)
|
|
135
|
-
|
|
136
128
|
def _on_range(self, lower, upper):
|
|
137
129
|
self.vmin = lower
|
|
138
130
|
self.vmax = upper
|
|
@@ -192,9 +184,6 @@ class CloudItem(BaseItem):
|
|
|
192
184
|
self.size = size
|
|
193
185
|
self.need_update_setting = True
|
|
194
186
|
|
|
195
|
-
def set_depthtest(self, state):
|
|
196
|
-
self.depth_test = state
|
|
197
|
-
|
|
198
187
|
def clear(self):
|
|
199
188
|
data = np.empty((0), self.data_type)
|
|
200
189
|
self.set_data(data)
|
|
@@ -304,15 +293,16 @@ class CloudItem(BaseItem):
|
|
|
304
293
|
def paint(self):
|
|
305
294
|
self.update_render_buffer()
|
|
306
295
|
self.update_setting()
|
|
296
|
+
|
|
307
297
|
glEnable(GL_BLEND)
|
|
308
298
|
glEnable(GL_PROGRAM_POINT_SIZE)
|
|
309
299
|
glEnable(GL_POINT_SPRITE)
|
|
310
300
|
glEnable(GL_DEPTH_TEST)
|
|
311
301
|
|
|
312
|
-
if
|
|
313
|
-
glDepthFunc(GL_ALWAYS)
|
|
302
|
+
if self.alpha < 0.9:
|
|
303
|
+
glDepthFunc(GL_ALWAYS)
|
|
314
304
|
else:
|
|
315
|
-
glDepthFunc(GL_LESS)
|
|
305
|
+
glDepthFunc(GL_LESS)
|
|
316
306
|
|
|
317
307
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
318
308
|
glUseProgram(self.program)
|
|
@@ -4,19 +4,28 @@ Distributed under MIT license. See LICENSE for more information.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
from turtle import position
|
|
8
7
|
from q3dviewer.base_item import BaseItem
|
|
9
8
|
from OpenGL.GL import *
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from OpenGL.GLUT import glutBitmapCharacter, glutInit
|
|
13
|
+
from OpenGL.GLUT import (
|
|
14
|
+
GLUT_BITMAP_HELVETICA_10,
|
|
15
|
+
GLUT_BITMAP_HELVETICA_12,
|
|
16
|
+
GLUT_BITMAP_HELVETICA_18,
|
|
17
|
+
GLUT_BITMAP_TIMES_ROMAN_24,
|
|
18
|
+
)
|
|
19
|
+
GLUT_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
GLUT_AVAILABLE = False
|
|
22
|
+
print("Warning: GLUT not available. Text will not be rendered.")
|
|
18
23
|
|
|
19
24
|
def get_glut_font(font_size):
|
|
25
|
+
"""Get GLUT font based on size, only if GLUT is available"""
|
|
26
|
+
if not GLUT_AVAILABLE:
|
|
27
|
+
return None
|
|
28
|
+
|
|
20
29
|
# Map requested font_size to a GLUT font object
|
|
21
30
|
if font_size <= 10:
|
|
22
31
|
return GLUT_BITMAP_HELVETICA_10
|
|
@@ -63,7 +72,19 @@ class Text3DItem(BaseItem):
|
|
|
63
72
|
|
|
64
73
|
|
|
65
74
|
def initialize_gl(self):
|
|
66
|
-
|
|
75
|
+
"""Initialize OpenGL resources, with GLUT fallback handling"""
|
|
76
|
+
global GLUT_AVAILABLE
|
|
77
|
+
if GLUT_AVAILABLE:
|
|
78
|
+
try:
|
|
79
|
+
# Check if glutInit is actually callable before calling it
|
|
80
|
+
if hasattr(glutInit, '__call__') and bool(glutInit):
|
|
81
|
+
glutInit()
|
|
82
|
+
else:
|
|
83
|
+
print("Warning: glutInit not callable, using fallback text rendering")
|
|
84
|
+
GLUT_AVAILABLE = False
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Warning: GLUT initialization failed: {e}. Using fallback text rendering.")
|
|
87
|
+
GLUT_AVAILABLE = False
|
|
67
88
|
# super().initialize_gl()
|
|
68
89
|
|
|
69
90
|
|
|
@@ -87,20 +108,31 @@ class Text3DItem(BaseItem):
|
|
|
87
108
|
print(f"Warning: Unsupported item type: {type(item)}")
|
|
88
109
|
continue
|
|
89
110
|
|
|
111
|
+
# Convert numpy array to tuple if needed
|
|
112
|
+
if isinstance(pos, np.ndarray):
|
|
113
|
+
pos = tuple(pos.astype(float))
|
|
114
|
+
|
|
90
115
|
glColor4f(*color)
|
|
91
|
-
|
|
92
|
-
pos_text = (pos[0] + offset, pos[1]+ offset, pos[2]+ offset)
|
|
93
|
-
glRasterPos3f(*pos_text)
|
|
94
|
-
|
|
116
|
+
|
|
95
117
|
if point_size > 0.0:
|
|
96
118
|
# draw a point at the position
|
|
97
119
|
glPointSize(point_size)
|
|
98
120
|
glBegin(GL_POINTS)
|
|
99
121
|
glVertex3f(*pos)
|
|
100
122
|
glEnd()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
|
|
124
|
+
# Text rendering with GLUT fallback
|
|
125
|
+
if GLUT_AVAILABLE:
|
|
126
|
+
offset = 0.02
|
|
127
|
+
pos_text = (pos[0] + offset, pos[1] + offset, pos[2] + offset)
|
|
128
|
+
glRasterPos3f(*pos_text)
|
|
129
|
+
font = get_glut_font(font_size)
|
|
130
|
+
if font is not None:
|
|
131
|
+
try:
|
|
132
|
+
for ch in text:
|
|
133
|
+
glutBitmapCharacter(font, ord(ch))
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"Error rendering text '{text}': {e}")
|
|
104
136
|
|
|
105
137
|
# draw lines between points
|
|
106
138
|
for i in range(len(self.data_list) - 1):
|
q3dviewer/utils/range_slider.py
CHANGED
|
@@ -37,19 +37,21 @@ class RangeSlider(QSlider):
|
|
|
37
37
|
self.active_handle = "upper"
|
|
38
38
|
|
|
39
39
|
def mouseMoveEvent(self, event):
|
|
40
|
-
"""Override to update handle positions."""
|
|
40
|
+
"""Override to update handle positions, always clamp and int for cross-platform safety."""
|
|
41
41
|
if event.buttons() != Qt.LeftButton:
|
|
42
42
|
return
|
|
43
43
|
|
|
44
44
|
pos = self.pixelPosToValue(event.pos())
|
|
45
|
+
minv, maxv = self.minimum(), self.maximum()
|
|
45
46
|
if self.active_handle == "lower":
|
|
46
|
-
self.lower_value = max(
|
|
47
|
-
|
|
47
|
+
self.lower_value = max(minv, min(pos, self.upper_value - 1))
|
|
48
|
+
self.lower_value = int(round(self.lower_value))
|
|
48
49
|
QToolTip.showText(event.globalPos(), f"Lower: {self.lower_value:.1f}")
|
|
49
50
|
elif self.active_handle == "upper":
|
|
50
|
-
self.upper_value = min(
|
|
51
|
-
|
|
51
|
+
self.upper_value = min(maxv, max(pos, self.lower_value + 1))
|
|
52
|
+
self.upper_value = int(round(self.upper_value))
|
|
52
53
|
QToolTip.showText(event.globalPos(), f"Upper: {self.upper_value:.1f}")
|
|
54
|
+
# Always emit clamped, int values
|
|
53
55
|
self.rangeChanged.emit(self.lower_value, self.upper_value)
|
|
54
56
|
self.update()
|
|
55
57
|
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: q3dviewer
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A library designed for quickly deploying a 3D viewer.
|
|
5
5
|
Home-page: https://github.com/scomup/q3dviewer
|
|
6
6
|
Author: Liu Yang
|
|
7
|
+
License: UNKNOWN
|
|
8
|
+
Platform: UNKNOWN
|
|
7
9
|
Classifier: Programming Language :: Python :: 3
|
|
8
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
11
|
Classifier: Operating System :: OS Independent
|
|
10
12
|
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: numpy
|
|
13
|
-
Requires-Dist: pyside6
|
|
14
13
|
Requires-Dist: PyOpenGL
|
|
15
|
-
Requires-Dist: meshio
|
|
16
|
-
Requires-Dist: pypcd4
|
|
17
|
-
Requires-Dist: pye57
|
|
18
|
-
Requires-Dist: laspy
|
|
19
14
|
Requires-Dist: imageio
|
|
20
15
|
Requires-Dist: imageio[ffmpeg]
|
|
16
|
+
Requires-Dist: laspy
|
|
21
17
|
Requires-Dist: matplotlib
|
|
18
|
+
Requires-Dist: meshio
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: pye57
|
|
21
|
+
Requires-Dist: pypcd4
|
|
22
|
+
Requires-Dist: pyside6
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|

|
|
@@ -171,7 +172,7 @@ def main():
|
|
|
171
172
|
|
|
172
173
|
# Create a viewer
|
|
173
174
|
viewer = q3d.Viewer(name='example')
|
|
174
|
-
|
|
175
|
+
|
|
175
176
|
# Add items to the viewer
|
|
176
177
|
viewer.add_items({
|
|
177
178
|
'grid': grid_item,
|
|
@@ -234,3 +235,4 @@ class YourItem(q3d.BaseItem):
|
|
|
234
235
|
```
|
|
235
236
|
|
|
236
237
|
Enjoy using `q3dviewer`!
|
|
238
|
+
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
q3dviewer/__init__.py,sha256=cjyfUE5zK6xohDGDQIWfb0DKkWChVznBd7CrVLg7whQ,376
|
|
2
|
-
q3dviewer/base_glwidget.py,sha256=
|
|
2
|
+
q3dviewer/base_glwidget.py,sha256=QxAuZzQSBbzTwpHHqYpiM-Jqv41E4YJmFG4KRF-HruY,15274
|
|
3
3
|
q3dviewer/base_item.py,sha256=63MarHyoWszPL40ox-vPoOAQ1N4ypekOjoRARdPik-E,1755
|
|
4
|
-
q3dviewer/gau_io.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
4
|
q3dviewer/glwidget.py,sha256=EmrxPtVQ8RdPK5INKMlpKpVfX0KCfjSKRdGf4cSB1f0,5405
|
|
6
5
|
q3dviewer/viewer.py,sha256=Vq3ucDlBcBBoiVVGmqG1sRjhLePl50heblx6wJpsc1A,2603
|
|
7
|
-
q3dviewer
|
|
8
|
-
q3dviewer/.vscode/settings.json,sha256=3G4gE-fW3vfObgaZrlJzAG7vym2pIfr0fNnL8Jo3RJo,322
|
|
9
|
-
q3dviewer/Qt/__init__.py,sha256=CcwS6oSXBXTMr58JNbRNYcPMVubDD2jiPtJ55DoLm8o,2199
|
|
6
|
+
q3dviewer/Qt/__init__.py,sha256=VJj7Ge6N_81__T9eHFl_YQpa1HyQrlLhMqC_9pUOYtc,2233
|
|
10
7
|
q3dviewer/custom_items/__init__.py,sha256=cwcNn40MzYXVO9eSzpRknJdzZNKPf2kaEMZy5DCNYcE,567
|
|
11
8
|
q3dviewer/custom_items/axis_item.py,sha256=-WM2urosqV847zpTpOtxdLjb7y9NJqFCH13qqodcCTg,2572
|
|
12
9
|
q3dviewer/custom_items/cloud_io_item.py,sha256=Haz-SOUUCPDSHgmKyyyFfP7LXBSEiN4r8xmchQwCm-k,4721
|
|
13
|
-
q3dviewer/custom_items/cloud_item.py,sha256=
|
|
10
|
+
q3dviewer/custom_items/cloud_item.py,sha256=McZmTKFllQn0ZiVJDzJJnpcZ-jpgzfO3tLPp03rop7E,13520
|
|
14
11
|
q3dviewer/custom_items/frame_item.py,sha256=bUzww3tSDah0JZeqtU6_cYHhhTVWzXhJVMcAa5pCXHI,7458
|
|
15
12
|
q3dviewer/custom_items/gaussian_item.py,sha256=JMubpahkTPh0E8ShL3FLTahv0e35ODzjgK5K1i0YXSU,9884
|
|
16
13
|
q3dviewer/custom_items/grid_item.py,sha256=LDB_MYACoxld-xvz01_MfAf12vLcRkH7R_WtGHHdSgk,4945
|
|
17
14
|
q3dviewer/custom_items/image_item.py,sha256=k7HNTqdL2ckTbxMx7A7eKaP4aksZ85-pBjNdbpm6PXM,5355
|
|
18
15
|
q3dviewer/custom_items/line_item.py,sha256=rel-lx8AgjDY7qyIecHxHQZzaswRn2ZTiOIjB_0Mrqo,4444
|
|
19
|
-
q3dviewer/custom_items/text3d_item.py,sha256=
|
|
16
|
+
q3dviewer/custom_items/text3d_item.py,sha256=DYBPXnCmMEzWDE1y523YsWSl91taXAdu0kdnhUcwE4A,5524
|
|
20
17
|
q3dviewer/custom_items/text_item.py,sha256=toeGjBu7RtT8CMUuaDWnmXPnA1UKHhnCzUNeonGczSo,2703
|
|
21
18
|
q3dviewer/shaders/cloud_frag.glsl,sha256=psKVt9qI6BW0bCqOk4lcKqUd6XgYGtdFigyN9OdYSNI,609
|
|
22
19
|
q3dviewer/shaders/cloud_vert.glsl,sha256=gKI6EJrzX5ga2W2yjU6x7Wjz7Cu2Y-wrPl4g10RfTLM,2376
|
|
@@ -24,10 +21,7 @@ q3dviewer/shaders/gau_frag.glsl,sha256=vWt5I3Ojrc2PCxRlBJGyJhujbveSicMA54T01Fk29
|
|
|
24
21
|
q3dviewer/shaders/gau_prep.glsl,sha256=0BiWhYCQGeX2iN-e7m3dy1xWXqWrErErRAzHlcmWHF0,7218
|
|
25
22
|
q3dviewer/shaders/gau_vert.glsl,sha256=_rkm51zaWgPDJ-otJL-WX12fDvnPBOTooVfqo21Rexs,1666
|
|
26
23
|
q3dviewer/shaders/sort_by_key.glsl,sha256=M5RK6uRDp40vVH6XtBIrdJTcYatqXyZwd6kCzEa2DZg,1097
|
|
27
|
-
q3dviewer/test/test_interpolation.py,sha256=rR_CXsYFLpn0zO0mHf_jL-naluDBMSky--FviOQga0Q,1657
|
|
28
|
-
q3dviewer/test/test_rendering.py,sha256=CrJkJjxkcizZxC4MVyDuJjY_41-eeiD5u0vD_8VFHgU,2206
|
|
29
24
|
q3dviewer/tools/__init__.py,sha256=01wG7BGM6VX0QyFBKsqPmyf2e-vrmV_N3-mo-VQ1VBg,20
|
|
30
|
-
q3dviewer/tools/cinematographer.py,sha256=o_24SSQ4mF062QQ7Gv3i90v7fA79PcHLB03UHXufuEA,13950
|
|
31
25
|
q3dviewer/tools/cloud_viewer.py,sha256=UsKALGiFR2Ck___LSGNPIZ3PPUlECO20jVkcP5g6yyc,6807
|
|
32
26
|
q3dviewer/tools/example_viewer.py,sha256=C867mLnCBjawS6LGgRsJ_c6-6wztfL9vOBQt85KbbdU,572
|
|
33
27
|
q3dviewer/tools/film_maker.py,sha256=xLFgRhFWoMQ37qlvcu1lXWaTWXMNRYlRcZFfHW5JtmQ,16676
|
|
@@ -41,10 +35,10 @@ q3dviewer/utils/convert_ros_msg.py,sha256=lNbLIawJfwp3VzygdW3dUXkfSG8atg_CoZbQFm
|
|
|
41
35
|
q3dviewer/utils/gl_helper.py,sha256=dRY_kUqyPMr7NTcupUr6_VTvgnj53iE2C0Lk0-oFYsI,1435
|
|
42
36
|
q3dviewer/utils/helpers.py,sha256=SqR4YTQZi13FKbkVUYgodXce1JJ_YmrHEIRkUmnIUas,3085
|
|
43
37
|
q3dviewer/utils/maths.py,sha256=zHaPtvVZIuo8xepIXCMeSL9tpx8FahUrq0l4K1oXrBk,8834
|
|
44
|
-
q3dviewer/utils/range_slider.py,sha256=
|
|
45
|
-
q3dviewer-1.
|
|
46
|
-
q3dviewer-1.
|
|
47
|
-
q3dviewer-1.
|
|
48
|
-
q3dviewer-1.
|
|
49
|
-
q3dviewer-1.
|
|
50
|
-
q3dviewer-1.
|
|
38
|
+
q3dviewer/utils/range_slider.py,sha256=Cs_xrwt6FCDVxGxan7r-ARd5ySwQ50xnCzcmz0dB_X0,4215
|
|
39
|
+
q3dviewer-1.2.0.dist-info/LICENSE,sha256=81cMOyNfw8KLb1JnPYngGHJ5W83gSbZEBU9MEP3tl-E,1124
|
|
40
|
+
q3dviewer-1.2.0.dist-info/METADATA,sha256=RFXc1PDLrmk1xqidvKx4RoU0_aBfK1aYyQK_TZs3bp0,8024
|
|
41
|
+
q3dviewer-1.2.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
42
|
+
q3dviewer-1.2.0.dist-info/entry_points.txt,sha256=EOjker7XYaBk70ffvNB_knPcfA33Bnlg21ZjEeM1EyI,362
|
|
43
|
+
q3dviewer-1.2.0.dist-info/top_level.txt,sha256=HFFDCbGu28txcGe2HPc46A7EPaguBa_b5oH7bufmxHM,10
|
|
44
|
+
q3dviewer-1.2.0.dist-info/RECORD,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"configurations": [
|
|
3
|
-
{
|
|
4
|
-
"browse": {
|
|
5
|
-
"databaseFilename": "${default}",
|
|
6
|
-
"limitSymbolsToIncludedHeaders": false
|
|
7
|
-
},
|
|
8
|
-
"includePath": [
|
|
9
|
-
"/home/liu/catkin_ws/devel/include/**",
|
|
10
|
-
"/opt/ros/noetic/include/**",
|
|
11
|
-
"/home/liu/catkin_ws/src/marker_test/apriltag_ros/apriltag_ros/include/**",
|
|
12
|
-
"/home/liu/catkin_ws/src/BALM/include/**",
|
|
13
|
-
"/home/liu/catkin_ws/src/FAST-LIVO/include/**",
|
|
14
|
-
"/home/liu/catkin_ws/src/grid2dlib/include/**",
|
|
15
|
-
"/home/liu/catkin_ws/src/lidar_cam_calib/include/**",
|
|
16
|
-
"/home/liu/catkin_ws/src/livox_laser_simulation/include/**",
|
|
17
|
-
"/home/liu/catkin_ws/src/usb_cam/include/**",
|
|
18
|
-
"/home/liu/catkin_ws/src/rpg_vikit/vikit_common/include/**",
|
|
19
|
-
"/home/liu/catkin_ws/src/rpg_vikit/vikit_ros/include/**",
|
|
20
|
-
"/usr/include/**"
|
|
21
|
-
],
|
|
22
|
-
"name": "ROS",
|
|
23
|
-
"intelliSenseMode": "gcc-x64",
|
|
24
|
-
"compilerPath": "/usr/bin/gcc",
|
|
25
|
-
"cStandard": "gnu11",
|
|
26
|
-
"cppStandard": "c++14"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"version": 4
|
|
30
|
-
}
|
q3dviewer/.vscode/settings.json
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"python.autoComplete.extraPaths": [
|
|
3
|
-
"/home/liu/catkin_ws/devel/lib/python3/dist-packages",
|
|
4
|
-
"/opt/ros/noetic/lib/python3/dist-packages"
|
|
5
|
-
],
|
|
6
|
-
"python.analysis.extraPaths": [
|
|
7
|
-
"/home/liu/catkin_ws/devel/lib/python3/dist-packages",
|
|
8
|
-
"/opt/ros/noetic/lib/python3/dist-packages"
|
|
9
|
-
]
|
|
10
|
-
}
|
q3dviewer/gau_io.py
DELETED
|
File without changes
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
"""
|
|
9
|
-
this script tests interpolation of 3D poses.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import numpy as np
|
|
14
|
-
from q3dviewer.utils.maths import expSO3, logSO3, makeT, makeRt
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def interpolate_pose(T1, T2, v_max, omega_max, dt=0.1):
|
|
18
|
-
R1, t1 = makeRt(T1)
|
|
19
|
-
R2, t2 = makeRt(T2)
|
|
20
|
-
|
|
21
|
-
# Get transform time based on linear velocity
|
|
22
|
-
d = np.linalg.norm(t2 - t1)
|
|
23
|
-
t_lin = d / v_max
|
|
24
|
-
|
|
25
|
-
# Get transform time based on angular velocity
|
|
26
|
-
omega = logSO3(R2 @ R1.T)
|
|
27
|
-
theta = np.linalg.norm(omega)
|
|
28
|
-
t_ang = theta / omega_max
|
|
29
|
-
|
|
30
|
-
# Get total time based on the linear and angular time
|
|
31
|
-
t_total = max(t_lin, t_ang)
|
|
32
|
-
num_steps = int(np.ceil(t_total / dt))
|
|
33
|
-
|
|
34
|
-
# Generate interpolated transforms
|
|
35
|
-
interpolated_Ts = []
|
|
36
|
-
for i in range(num_steps + 1):
|
|
37
|
-
s = i / num_steps
|
|
38
|
-
t_interp = (1 - s) * t1 + s * t2
|
|
39
|
-
# Interpolate rotation using SO3
|
|
40
|
-
R_interp = expSO3(s * omega) @ R1
|
|
41
|
-
T_interp = makeT(R_interp, t_interp)
|
|
42
|
-
interpolated_Ts.append(T_interp)
|
|
43
|
-
|
|
44
|
-
return interpolated_Ts
|
|
45
|
-
|
|
46
|
-
if __name__ == "__main__":
|
|
47
|
-
T1 = np.eye(4) # Identity transformation
|
|
48
|
-
T2 = np.array([[0, -1, 0, 1], [1, 0, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]]) # Target transformation
|
|
49
|
-
|
|
50
|
-
v_max = 1.0 # Maximum linear velocity (m/s)
|
|
51
|
-
omega_max = np.pi / 4 # Maximum angular velocity (rad/s)
|
|
52
|
-
|
|
53
|
-
# Perform interpolation
|
|
54
|
-
interpolated_poses = interpolate_pose(T1, T2, v_max, omega_max)
|
|
55
|
-
for i, T in enumerate(interpolated_poses):
|
|
56
|
-
print(f"Step {i}:\n{T}\n")
|
|
57
|
-
|
|
58
|
-
|
q3dviewer/test/test_rendering.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
"""
|
|
9
|
-
this script tests the rendering of a cloud in a camera frame based on the
|
|
10
|
-
camera pose and intrinsic matrix
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import numpy as np
|
|
14
|
-
import q3dviewer as q3d
|
|
15
|
-
import cv2
|
|
16
|
-
from q3dviewer.utils.cloud_io import load_pcd
|
|
17
|
-
|
|
18
|
-
cloud, _ = load_pcd('/home/liu/lab.pcd')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Tcw = np.array([[7.07106781e-01, 7.07106781e-01, 0.00000000e+00,
|
|
22
|
-
0.00000000e+00],
|
|
23
|
-
[-3.53553391e-01, 3.53553391e-01, 8.66025404e-01,
|
|
24
|
-
3.55271368e-15],
|
|
25
|
-
[6.12372436e-01, -6.12372436e-01, 5.00000000e-01,
|
|
26
|
-
-4.00000000e+01],
|
|
27
|
-
[0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
|
|
28
|
-
1.00000000e+00]])
|
|
29
|
-
# convert the opengl camera coordinate to the opencv camera coordinate
|
|
30
|
-
Tconv = np.array([[1, 0, 0, 0],
|
|
31
|
-
[0, -1, 0, 0],
|
|
32
|
-
[0, 0, -1, 0],
|
|
33
|
-
[0, 0, 0, 1]])
|
|
34
|
-
|
|
35
|
-
Tcw = Tconv @ Tcw
|
|
36
|
-
|
|
37
|
-
K = np.array([[1.64718029e+03, 0.00000000e+00, 9.51000000e+02],
|
|
38
|
-
[0.00000000e+00, 1.64718036e+03, 5.31000000e+02],
|
|
39
|
-
[0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def render_frame(cloud, Tcw, K, width, height):
|
|
43
|
-
image = np.zeros((height, width, 3), dtype=np.uint8)
|
|
44
|
-
Rcw, tcw = Tcw[:3, :3], Tcw[:3, 3]
|
|
45
|
-
pc = (Rcw @ cloud['xyz'].T).T + tcw
|
|
46
|
-
uv = (K @ pc.T).T
|
|
47
|
-
uv = uv[:, :2] / uv[:, 2][:, np.newaxis]
|
|
48
|
-
mask = (pc[:, 2] > 0) & (uv[:, 0] > 0) & (
|
|
49
|
-
uv[:, 0] < width) & (uv[:, 1] > 0) & (uv[:, 1] < height)
|
|
50
|
-
uv = uv[mask]
|
|
51
|
-
u = uv[:, 0].astype(int)
|
|
52
|
-
v = uv[:, 1].astype(int)
|
|
53
|
-
rgb = cloud['irgb'][mask]
|
|
54
|
-
r = rgb >> 16 & 0xff
|
|
55
|
-
g = rgb >> 8 & 0xff
|
|
56
|
-
b = rgb & 0xff
|
|
57
|
-
|
|
58
|
-
# Sort by depth to ensure front points are drawn first
|
|
59
|
-
depth = pc[mask, 2]
|
|
60
|
-
sorted_indices = np.argsort(depth)
|
|
61
|
-
u = u[sorted_indices]
|
|
62
|
-
v = v[sorted_indices]
|
|
63
|
-
r = r[sorted_indices]
|
|
64
|
-
g = g[sorted_indices]
|
|
65
|
-
b = b[sorted_indices]
|
|
66
|
-
|
|
67
|
-
image[v, u] = np.stack([b, g, r], axis=1)
|
|
68
|
-
return image
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
image = render_frame(cloud, Tcw, K, 1902, 1062)
|
|
72
|
-
cv2.imshow('image', image)
|
|
73
|
-
cv2.waitKey(0)
|
|
@@ -1,367 +0,0 @@
|
|
|
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, QListWidget, QListWidgetItem, QPushButton, QDoubleSpinBox, QCheckBox, QLineEdit, QMessageBox, QLabel, QHBoxLayout
|
|
11
|
-
from PySide6.QtCore import QTimer
|
|
12
|
-
from cloud_viewer import ProgressDialog, FileLoaderThread
|
|
13
|
-
from PySide6 import QtCore
|
|
14
|
-
from PySide6.QtGui import QKeyEvent
|
|
15
|
-
from q3dviewer import GLWidget
|
|
16
|
-
import imageio.v2 as imageio
|
|
17
|
-
import os
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class KeyFrame:
|
|
21
|
-
def __init__(self, Twc):
|
|
22
|
-
self.Twc = Twc
|
|
23
|
-
self.linear_velocity = 10
|
|
24
|
-
self.angular_velocity = 1
|
|
25
|
-
self.stop_time = 0
|
|
26
|
-
self.item = q3d.FrameItem(Twc, width=3, color='#0000FF')
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class CustomGLWidget(GLWidget):
|
|
30
|
-
def __init__(self, viewer):
|
|
31
|
-
super().__init__()
|
|
32
|
-
self.viewer = viewer # Add a viewer handle
|
|
33
|
-
|
|
34
|
-
def keyPressEvent(self, ev: QKeyEvent):
|
|
35
|
-
if ev.key() == QtCore.Qt.Key_Space:
|
|
36
|
-
self.viewer.add_key_frame()
|
|
37
|
-
elif ev.key() == QtCore.Qt.Key_Delete:
|
|
38
|
-
self.viewer.del_key_frame()
|
|
39
|
-
super().keyPressEvent(ev)
|
|
40
|
-
|
|
41
|
-
class CMMViewer(q3d.Viewer):
|
|
42
|
-
"""
|
|
43
|
-
This class is a subclass of Viewer, which is used to create a cloud movie maker.
|
|
44
|
-
"""
|
|
45
|
-
def __init__(self, **kwargs):
|
|
46
|
-
self.key_frames = []
|
|
47
|
-
self.video_path = os.path.join(os.path.expanduser("~"), "output.mp4")
|
|
48
|
-
super().__init__(**kwargs, gl_widget_class=lambda: CustomGLWidget(self))
|
|
49
|
-
# for drop cloud file
|
|
50
|
-
self.setAcceptDrops(True)
|
|
51
|
-
|
|
52
|
-
def add_control_panel(self, main_layout):
|
|
53
|
-
"""
|
|
54
|
-
Add a control panel to the viewer.
|
|
55
|
-
"""
|
|
56
|
-
# Create a vertical layout for the settings
|
|
57
|
-
setting_layout = QVBoxLayout()
|
|
58
|
-
|
|
59
|
-
# Buttons to add and delete key frames
|
|
60
|
-
add_button = QPushButton("Add Key Frame")
|
|
61
|
-
add_button.clicked.connect(self.add_key_frame)
|
|
62
|
-
setting_layout.addWidget(add_button)
|
|
63
|
-
del_button = QPushButton("Delete Key Frame")
|
|
64
|
-
del_button.clicked.connect(self.del_key_frame)
|
|
65
|
-
setting_layout.addWidget(del_button)
|
|
66
|
-
|
|
67
|
-
# Add play/stop button
|
|
68
|
-
self.play_button = QPushButton("Play")
|
|
69
|
-
self.play_button.clicked.connect(self.toggle_playback)
|
|
70
|
-
setting_layout.addWidget(self.play_button)
|
|
71
|
-
|
|
72
|
-
# add a timer to play the frames
|
|
73
|
-
self.timer = QTimer()
|
|
74
|
-
self.timer.timeout.connect(self.play_frames)
|
|
75
|
-
self.current_frame_index = 0
|
|
76
|
-
self.is_playing = False
|
|
77
|
-
self.is_recording = False
|
|
78
|
-
|
|
79
|
-
# Add record checkbox
|
|
80
|
-
self.record_checkbox = QCheckBox("Record")
|
|
81
|
-
self.record_checkbox.stateChanged.connect(self.toggle_recording)
|
|
82
|
-
setting_layout.addWidget(self.record_checkbox)
|
|
83
|
-
|
|
84
|
-
# Add video path setting
|
|
85
|
-
video_path_layout = QHBoxLayout()
|
|
86
|
-
label_video_path = QLabel("Video Path:")
|
|
87
|
-
video_path_layout.addWidget(label_video_path)
|
|
88
|
-
self.video_path_edit = QLineEdit()
|
|
89
|
-
self.video_path_edit.setText(self.video_path)
|
|
90
|
-
self.video_path_edit.textChanged.connect(self.update_video_path)
|
|
91
|
-
video_path_layout.addWidget(self.video_path_edit)
|
|
92
|
-
setting_layout.addLayout(video_path_layout)
|
|
93
|
-
|
|
94
|
-
# Add a list of key frames
|
|
95
|
-
self.frame_list = QListWidget()
|
|
96
|
-
setting_layout.addWidget(self.frame_list)
|
|
97
|
-
self.frame_list.itemSelectionChanged.connect(self.on_select_frame)
|
|
98
|
-
self.installEventFilter(self)
|
|
99
|
-
|
|
100
|
-
# Add spin boxes for linear / angular velocity and stop time
|
|
101
|
-
self.lin_vel_spinbox = QDoubleSpinBox()
|
|
102
|
-
self.lin_vel_spinbox.setPrefix("Linear Velocity (m/s): ")
|
|
103
|
-
self.lin_vel_spinbox.setRange(0, 100)
|
|
104
|
-
self.lin_vel_spinbox.valueChanged.connect(self.set_frame_lin_vel)
|
|
105
|
-
setting_layout.addWidget(self.lin_vel_spinbox)
|
|
106
|
-
|
|
107
|
-
self.lin_ang_spinbox = QDoubleSpinBox()
|
|
108
|
-
self.lin_ang_spinbox.setPrefix("Angular Velocity (rad/s): ")
|
|
109
|
-
self.lin_ang_spinbox.setRange(0, 100)
|
|
110
|
-
self.lin_ang_spinbox.valueChanged.connect(self.set_frame_ang_vel)
|
|
111
|
-
setting_layout.addWidget(self.lin_ang_spinbox)
|
|
112
|
-
|
|
113
|
-
self.stop_time_spinbox = QDoubleSpinBox()
|
|
114
|
-
self.stop_time_spinbox.setPrefix("Stop Time: ")
|
|
115
|
-
self.stop_time_spinbox.setRange(0, 100)
|
|
116
|
-
self.stop_time_spinbox.valueChanged.connect(self.set_frame_stop_time)
|
|
117
|
-
setting_layout.addWidget(self.stop_time_spinbox)
|
|
118
|
-
|
|
119
|
-
setting_layout.setAlignment(QtCore.Qt.AlignTop)
|
|
120
|
-
main_layout.addLayout(setting_layout)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def update_video_path(self, path):
|
|
124
|
-
self.video_path = path
|
|
125
|
-
|
|
126
|
-
def add_key_frame(self):
|
|
127
|
-
view_matrix = self.glwidget.view_matrix
|
|
128
|
-
# Get camera pose in world frame
|
|
129
|
-
Twc = np.linalg.inv(view_matrix)
|
|
130
|
-
# Add the key frame to the list
|
|
131
|
-
key_frame = KeyFrame(Twc)
|
|
132
|
-
current_index = self.frame_list.currentRow()
|
|
133
|
-
self.key_frames.insert(current_index + 1, key_frame)
|
|
134
|
-
# visualize this key frame using FrameItem
|
|
135
|
-
self.glwidget.add_item(key_frame.item)
|
|
136
|
-
# move the camera back to 0.5 meter, let the user see the frame
|
|
137
|
-
self.glwidget.update_dist(0.5)
|
|
138
|
-
# Add the key frame to the Qt ListWidget
|
|
139
|
-
item = QListWidgetItem(f"Frame {current_index + 2}")
|
|
140
|
-
self.frame_list.insertItem(current_index + 1, item)
|
|
141
|
-
self.frame_list.setCurrentRow(current_index + 1)
|
|
142
|
-
# Update frame labels
|
|
143
|
-
for i in range(len(self.key_frames)):
|
|
144
|
-
self.frame_list.item(i).setText(f"Frame {i + 1}")
|
|
145
|
-
|
|
146
|
-
def del_key_frame(self):
|
|
147
|
-
current_index = self.frame_list.currentRow()
|
|
148
|
-
if current_index >= 0:
|
|
149
|
-
self.glwidget.remove_item(self.key_frames[current_index].item)
|
|
150
|
-
self.key_frames.pop(current_index)
|
|
151
|
-
self.frame_list.itemSelectionChanged.disconnect(self.on_select_frame)
|
|
152
|
-
self.frame_list.takeItem(current_index)
|
|
153
|
-
self.frame_list.itemSelectionChanged.connect(self.on_select_frame)
|
|
154
|
-
self.on_select_frame()
|
|
155
|
-
# Update frame labels
|
|
156
|
-
for i in range(len(self.key_frames)):
|
|
157
|
-
|
|
158
|
-
self.frame_list.item(i).setText(f"Frame {i + 1}")
|
|
159
|
-
|
|
160
|
-
def on_select_frame(self):
|
|
161
|
-
current = self.frame_list.currentRow()
|
|
162
|
-
for i, frame in enumerate(self.key_frames):
|
|
163
|
-
if i == current:
|
|
164
|
-
# Highlight the selected frame
|
|
165
|
-
frame.item.set_color('#FF0000')
|
|
166
|
-
frame.item.set_line_width(5)
|
|
167
|
-
# show current frame's parameters in the spinboxes
|
|
168
|
-
self.lin_vel_spinbox.setValue(frame.linear_velocity)
|
|
169
|
-
self.lin_ang_spinbox.setValue(frame.angular_velocity)
|
|
170
|
-
self.stop_time_spinbox.setValue(frame.stop_time)
|
|
171
|
-
else:
|
|
172
|
-
frame.item.set_color('#0000FF')
|
|
173
|
-
frame.item.set_line_width(3)
|
|
174
|
-
|
|
175
|
-
def set_frame_lin_vel(self, value):
|
|
176
|
-
current_index = self.frame_list.currentRow()
|
|
177
|
-
if current_index >= 0:
|
|
178
|
-
self.key_frames[current_index].linear_velocity = value
|
|
179
|
-
|
|
180
|
-
def set_frame_ang_vel(self, value):
|
|
181
|
-
current_index = self.frame_list.currentRow()
|
|
182
|
-
if current_index >= 0:
|
|
183
|
-
self.key_frames[current_index].angular_velocity = value
|
|
184
|
-
|
|
185
|
-
def set_frame_stop_time(self, value):
|
|
186
|
-
current_index = self.frame_list.currentRow()
|
|
187
|
-
if current_index >= 0:
|
|
188
|
-
self.key_frames[current_index].stop_time = value
|
|
189
|
-
|
|
190
|
-
def create_frames(self):
|
|
191
|
-
"""
|
|
192
|
-
Create the frames for playback by interpolating between key frames.
|
|
193
|
-
"""
|
|
194
|
-
self.frames = []
|
|
195
|
-
dt = 1 / float(self.update_interval)
|
|
196
|
-
for i in range(len(self.key_frames) - 1):
|
|
197
|
-
current_frame = self.key_frames[i]
|
|
198
|
-
if current_frame.stop_time > 0:
|
|
199
|
-
num_steps = int(current_frame.stop_time / dt)
|
|
200
|
-
for j in range(num_steps):
|
|
201
|
-
self.frames.append(current_frame.Twc)
|
|
202
|
-
next_frame = self.key_frames[i + 1]
|
|
203
|
-
Ts = q3d.interpolate_pose(current_frame.Twc, next_frame.Twc,
|
|
204
|
-
current_frame.linear_velocity,
|
|
205
|
-
current_frame.angular_velocity,
|
|
206
|
-
dt)
|
|
207
|
-
self.frames.extend(Ts)
|
|
208
|
-
|
|
209
|
-
print(f"Total frames: {len(self.frames)}")
|
|
210
|
-
print(f"Total time: {len(self.frames) * dt:.2f} seconds")
|
|
211
|
-
|
|
212
|
-
def toggle_playback(self):
|
|
213
|
-
if self.is_playing:
|
|
214
|
-
self.stop_playback()
|
|
215
|
-
else:
|
|
216
|
-
self.start_playback()
|
|
217
|
-
|
|
218
|
-
def start_playback(self):
|
|
219
|
-
if self.key_frames:
|
|
220
|
-
self.create_frames()
|
|
221
|
-
self.current_frame_index = 0
|
|
222
|
-
self.timer.start(self.update_interval) # Adjust the interval as needed
|
|
223
|
-
self.is_playing = True
|
|
224
|
-
self.play_button.setStyleSheet("")
|
|
225
|
-
self.play_button.setText("Stop")
|
|
226
|
-
self.record_checkbox.setEnabled(False)
|
|
227
|
-
if self.is_recording is True:
|
|
228
|
-
self.start_recording()
|
|
229
|
-
|
|
230
|
-
def stop_playback(self):
|
|
231
|
-
self.timer.stop()
|
|
232
|
-
self.is_playing = False
|
|
233
|
-
self.play_button.setStyleSheet("")
|
|
234
|
-
self.play_button.setText("Play")
|
|
235
|
-
self.record_checkbox.setEnabled(True)
|
|
236
|
-
if self.is_recording:
|
|
237
|
-
self.stop_recording()
|
|
238
|
-
|
|
239
|
-
def play_frames(self):
|
|
240
|
-
"""
|
|
241
|
-
callback function for the timer to play the frames
|
|
242
|
-
"""
|
|
243
|
-
# play the frames
|
|
244
|
-
if self.current_frame_index < len(self.frames):
|
|
245
|
-
self.glwidget.set_view_matrix(np.linalg.inv(self.frames[self.current_frame_index]))
|
|
246
|
-
self.current_frame_index += 1
|
|
247
|
-
if self.is_recording:
|
|
248
|
-
self.record_frame()
|
|
249
|
-
else:
|
|
250
|
-
self.stop_playback()
|
|
251
|
-
|
|
252
|
-
def toggle_recording(self, state):
|
|
253
|
-
if state == 2:
|
|
254
|
-
self.is_recording = True
|
|
255
|
-
else:
|
|
256
|
-
self.is_recording = False
|
|
257
|
-
|
|
258
|
-
def start_recording(self):
|
|
259
|
-
self.is_recording = True
|
|
260
|
-
self.frames_to_record = []
|
|
261
|
-
video_path = self.video_path_edit.text()
|
|
262
|
-
self.play_button.setStyleSheet("background-color: red")
|
|
263
|
-
self.play_button.setText("Recording")
|
|
264
|
-
self.writer = imageio.get_writer(video_path, fps=self.update_interval,
|
|
265
|
-
codec="libx264", bitrate="5M", quality=10)
|
|
266
|
-
# disable the all the frame_item while recording
|
|
267
|
-
for frame in self.key_frames:
|
|
268
|
-
frame.item.hide()
|
|
269
|
-
|
|
270
|
-
def stop_recording(self, save_movie=True):
|
|
271
|
-
self.is_recording = False
|
|
272
|
-
self.record_checkbox.setChecked(False)
|
|
273
|
-
# enable the all the frame_item after recording
|
|
274
|
-
for frame in self.key_frames:
|
|
275
|
-
frame.item.show()
|
|
276
|
-
if hasattr(self, 'writer') and save_movie:
|
|
277
|
-
self.writer.close()
|
|
278
|
-
self.show_save_message()
|
|
279
|
-
|
|
280
|
-
def show_save_message(self):
|
|
281
|
-
msg_box = QMessageBox()
|
|
282
|
-
msg_box.setIcon(QMessageBox.Information)
|
|
283
|
-
msg_box.setWindowTitle("Video Saved")
|
|
284
|
-
msg_box.setText(f"Video saved to {self.video_path_edit.text()}")
|
|
285
|
-
msg_box.setStandardButtons(QMessageBox.Ok)
|
|
286
|
-
msg_box.exec()
|
|
287
|
-
|
|
288
|
-
def record_frame(self):
|
|
289
|
-
frame = self.glwidget.capture_frame()
|
|
290
|
-
# make sure the frame size is multiple of 16
|
|
291
|
-
height, width, _ = frame.shape
|
|
292
|
-
if height % 16 != 0 or width % 16 != 0:
|
|
293
|
-
frame = frame[:-(height % 16), :-(width % 16), :]
|
|
294
|
-
frame = np.ascontiguousarray(frame)
|
|
295
|
-
self.frames_to_record.append(frame)
|
|
296
|
-
try:
|
|
297
|
-
self.writer.append_data(frame)
|
|
298
|
-
except Exception as e:
|
|
299
|
-
print("Don't change the window size during recording.")
|
|
300
|
-
self.stop_recording(False) # Stop recording without saving
|
|
301
|
-
self.stop_playback()
|
|
302
|
-
|
|
303
|
-
def eventFilter(self, obj, event):
|
|
304
|
-
if event.type() == QtCore.QEvent.KeyPress:
|
|
305
|
-
if event.key() == QtCore.Qt.Key_Delete:
|
|
306
|
-
self.del_key_frame()
|
|
307
|
-
return True
|
|
308
|
-
return super().eventFilter(obj, event)
|
|
309
|
-
|
|
310
|
-
def dragEnterEvent(self, event):
|
|
311
|
-
if event.mimeData().hasUrls():
|
|
312
|
-
event.accept()
|
|
313
|
-
else:
|
|
314
|
-
event.ignore()
|
|
315
|
-
|
|
316
|
-
def dropEvent(self, event):
|
|
317
|
-
"""
|
|
318
|
-
Overwrite the drop event to open the cloud file.
|
|
319
|
-
"""
|
|
320
|
-
self.progress_dialog = ProgressDialog(self)
|
|
321
|
-
self.progress_dialog.show()
|
|
322
|
-
files = event.mimeData().urls()
|
|
323
|
-
self.progress_thread = FileLoaderThread(self, files)
|
|
324
|
-
self['cloud'].load(files[0].toLocalFile(), append=False)
|
|
325
|
-
self.progress_thread.progress.connect(self.file_loading_progress)
|
|
326
|
-
self.progress_thread.finished.connect(self.file_loading_finished)
|
|
327
|
-
self.progress_thread.start()
|
|
328
|
-
|
|
329
|
-
def file_loading_progress(self, value):
|
|
330
|
-
self.progress_dialog.set_value(value)
|
|
331
|
-
|
|
332
|
-
def file_loading_finished(self):
|
|
333
|
-
self.progress_dialog.close()
|
|
334
|
-
|
|
335
|
-
def open_cloud_file(self, file, append=False):
|
|
336
|
-
cloud_item = self['cloud']
|
|
337
|
-
if cloud_item is None:
|
|
338
|
-
print("Can't find clouditem.")
|
|
339
|
-
return
|
|
340
|
-
cloud = cloud_item.load(file, append=append)
|
|
341
|
-
center = np.nanmean(cloud['xyz'].astype(np.float64), axis=0)
|
|
342
|
-
self.glwidget.set_cam_position(pos=center)
|
|
343
|
-
|
|
344
|
-
def main():
|
|
345
|
-
import argparse
|
|
346
|
-
parser = argparse.ArgumentParser()
|
|
347
|
-
parser.add_argument("--path", help="the cloud file path")
|
|
348
|
-
args = parser.parse_args()
|
|
349
|
-
app = q3d.QApplication(['Cloud Movie Maker'])
|
|
350
|
-
viewer = CMMViewer(name='Cloud Movie Maker', update_interval=30)
|
|
351
|
-
cloud_item = q3d.CloudIOItem(size=1, alpha=0.1)
|
|
352
|
-
axis_item = q3d.AxisItem(size=0.5, width=5)
|
|
353
|
-
grid_item = q3d.GridItem(size=1000, spacing=20)
|
|
354
|
-
|
|
355
|
-
viewer.add_items(
|
|
356
|
-
{'cloud': cloud_item, 'grid': grid_item, 'axis': axis_item})
|
|
357
|
-
|
|
358
|
-
if args.path:
|
|
359
|
-
pcd_fn = args.path
|
|
360
|
-
viewer.open_cloud_file(pcd_fn)
|
|
361
|
-
|
|
362
|
-
viewer.show()
|
|
363
|
-
app.exec()
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if __name__ == '__main__':
|
|
367
|
-
main()
|
|
File without changes
|
|
File without changes
|