q3dviewer 1.1.2__tar.gz → 1.1.4__tar.gz
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-1.1.2/q3dviewer.egg-info → q3dviewer-1.1.4}/PKG-INFO +27 -7
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/README.md +26 -6
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/pyproject.toml +1 -1
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/base_glwidget.py +59 -31
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/cloud_io_item.py +15 -5
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/cloud_item.py +13 -12
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/frame_item.py +3 -7
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/grid_item.py +7 -9
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/image_item.py +2 -7
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/line_item.py +14 -10
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/text_item.py +7 -5
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/glwidget.py +8 -14
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/film_maker.py +0 -2
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/utils/cloud_io.py +41 -26
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/utils/maths.py +21 -19
- {q3dviewer-1.1.2 → q3dviewer-1.1.4/q3dviewer.egg-info}/PKG-INFO +27 -7
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer.egg-info/requires.txt +1 -1
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/setup.py +2 -2
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/LICENSE +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/__init__.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/base_item.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/__init__.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/axis_item.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/custom_items/gaussian_item.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/shaders/cloud_frag.glsl +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/shaders/cloud_vert.glsl +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/shaders/gau_frag.glsl +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/shaders/gau_prep.glsl +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/shaders/gau_vert.glsl +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/shaders/sort_by_key.glsl +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/__init__.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/cloud_viewer.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/example_viewer.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/gaussian_viewer.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/lidar_calib.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/lidar_cam_calib.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/tools/ros_viewer.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/utils/__init__.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/utils/convert_ros_msg.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/utils/gl_helper.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/utils/range_slider.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer/viewer.py +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer.egg-info/SOURCES.txt +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer.egg-info/dependency_links.txt +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer.egg-info/entry_points.txt +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/q3dviewer.egg-info/top_level.txt +0 -0
- {q3dviewer-1.1.2 → q3dviewer-1.1.4}/setup.cfg +0 -0
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: q3dviewer
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
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
7
|
License: UNKNOWN
|
|
8
|
-
Description:
|
|
8
|
+
Description:
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://badge.fury.io/py/q3dviewer)
|
|
9
13
|
|
|
10
14
|
`q3dviewer` is a library designed for quickly deploying a 3D viewer. It is based on Qt (PySide6) and provides efficient OpenGL items for displaying 3D objects (e.g., point clouds, cameras, and 3D Gaussians). You can use it to visualize your 3D data or set up an efficient viewer application. It is inspired by PyQtGraph but focuses more on efficient 3D rendering.
|
|
11
15
|
|
|
16
|
+
|
|
17
|
+
To show how to use `q3dviewer` as a library, we also provide some [very useful tools](#tools).
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
## Installation
|
|
13
21
|
|
|
14
22
|
To install `q3dviewer`, execute the following command in your terminal on either Linux or Windows:
|
|
@@ -37,10 +45,10 @@ Description: ## q3dviewer
|
|
|
37
45
|
|
|
38
46
|
### 1. Cloud Viewer
|
|
39
47
|
|
|
40
|
-
A tool for visualizing point cloud files. Launch it by executing the following command in your terminal:
|
|
48
|
+
A tool for visualizing point cloud files (LAS, PCD, PLY, E57). Launch it by executing the following command in your terminal:
|
|
41
49
|
|
|
42
50
|
```sh
|
|
43
|
-
cloud_viewer
|
|
51
|
+
cloud_viewer
|
|
44
52
|
```
|
|
45
53
|
|
|
46
54
|
*Alternatively*, if the path is not set (though it's not recommended):
|
|
@@ -49,7 +57,13 @@ Description: ## q3dviewer
|
|
|
49
57
|
python3 -m q3dviewer.tools.cloud_viewer
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
**Basic Operations**
|
|
61
|
+
* Load files: Drag and drop point cloud files onto the window (multiple files are OK).
|
|
62
|
+
* `M` key: Display the visualization settings screen for point clouds, background color, etc.
|
|
63
|
+
* `Left mouse button` & `W, A, S, D` keys: Move the viewpoint on the horizontal plane.
|
|
64
|
+
* `Z, X` keys: Move in the direction the screen is facing.
|
|
65
|
+
* `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the screen center unchanged.
|
|
66
|
+
* `Shift` + `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the camera position unchanged.
|
|
53
67
|
|
|
54
68
|
For example, you can download and view point clouds of Tokyo in LAS format from the following link:
|
|
55
69
|
|
|
@@ -75,15 +89,21 @@ Description: ## q3dviewer
|
|
|
75
89
|
Would you like to create a video from point cloud data? With Film Maker, you can easily create videos with simple operations. Just edit keyframes using the user-friendly GUI, and the software will automatically interpolate the keyframes to generate the video.
|
|
76
90
|
|
|
77
91
|
```sh
|
|
78
|
-
film_maker
|
|
92
|
+
film_maker
|
|
79
93
|
```
|
|
80
94
|
|
|
95
|
+
**Basic Operations**
|
|
96
|
+
* File loading & viewpoint movement: Same as Cloud_Viewer
|
|
81
97
|
* Space key to add a keyframe.
|
|
82
98
|
* Delete key to remove a keyframe.
|
|
99
|
+
* Play button: Automatically play the video (pressing again will stop playback)
|
|
100
|
+
* Record checkbox: When checked, actions will be automatically recorded during playback
|
|
83
101
|
|
|
84
102
|
Film Maker GUI:
|
|
85
103
|
|
|
86
|
-

|
|
105
|
+
|
|
106
|
+
The demo video demonstrating how to use Film Maker utilizes the [cloud data of Kyobashi Station Area](https://www.geospatial.jp/ckan/dataset/kyoubasiekisyuuhen_las) located in Osaka, Japan.
|
|
87
107
|
|
|
88
108
|
### 4. Gaussian Viewer
|
|
89
109
|
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://badge.fury.io/py/q3dviewer)
|
|
2
6
|
|
|
3
7
|
`q3dviewer` is a library designed for quickly deploying a 3D viewer. It is based on Qt (PySide6) and provides efficient OpenGL items for displaying 3D objects (e.g., point clouds, cameras, and 3D Gaussians). You can use it to visualize your 3D data or set up an efficient viewer application. It is inspired by PyQtGraph but focuses more on efficient 3D rendering.
|
|
4
8
|
|
|
9
|
+
|
|
10
|
+
To show how to use `q3dviewer` as a library, we also provide some [very useful tools](#tools).
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## Installation
|
|
6
14
|
|
|
7
15
|
To install `q3dviewer`, execute the following command in your terminal on either Linux or Windows:
|
|
@@ -30,10 +38,10 @@ Once installed, you can directly use the following tools:
|
|
|
30
38
|
|
|
31
39
|
### 1. Cloud Viewer
|
|
32
40
|
|
|
33
|
-
A tool for visualizing point cloud files. Launch it by executing the following command in your terminal:
|
|
41
|
+
A tool for visualizing point cloud files (LAS, PCD, PLY, E57). Launch it by executing the following command in your terminal:
|
|
34
42
|
|
|
35
43
|
```sh
|
|
36
|
-
cloud_viewer
|
|
44
|
+
cloud_viewer
|
|
37
45
|
```
|
|
38
46
|
|
|
39
47
|
*Alternatively*, if the path is not set (though it's not recommended):
|
|
@@ -42,7 +50,13 @@ cloud_viewer # The viewer will be displayed
|
|
|
42
50
|
python3 -m q3dviewer.tools.cloud_viewer
|
|
43
51
|
```
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
**Basic Operations**
|
|
54
|
+
* Load files: Drag and drop point cloud files onto the window (multiple files are OK).
|
|
55
|
+
* `M` key: Display the visualization settings screen for point clouds, background color, etc.
|
|
56
|
+
* `Left mouse button` & `W, A, S, D` keys: Move the viewpoint on the horizontal plane.
|
|
57
|
+
* `Z, X` keys: Move in the direction the screen is facing.
|
|
58
|
+
* `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the screen center unchanged.
|
|
59
|
+
* `Shift` + `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the camera position unchanged.
|
|
46
60
|
|
|
47
61
|
For example, you can download and view point clouds of Tokyo in LAS format from the following link:
|
|
48
62
|
|
|
@@ -68,15 +82,21 @@ ros_viewer
|
|
|
68
82
|
Would you like to create a video from point cloud data? With Film Maker, you can easily create videos with simple operations. Just edit keyframes using the user-friendly GUI, and the software will automatically interpolate the keyframes to generate the video.
|
|
69
83
|
|
|
70
84
|
```sh
|
|
71
|
-
film_maker
|
|
85
|
+
film_maker
|
|
72
86
|
```
|
|
73
87
|
|
|
88
|
+
**Basic Operations**
|
|
89
|
+
* File loading & viewpoint movement: Same as Cloud_Viewer
|
|
74
90
|
* Space key to add a keyframe.
|
|
75
91
|
* Delete key to remove a keyframe.
|
|
92
|
+
* Play button: Automatically play the video (pressing again will stop playback)
|
|
93
|
+
* Record checkbox: When checked, actions will be automatically recorded during playback
|
|
76
94
|
|
|
77
95
|
Film Maker GUI:
|
|
78
96
|
|
|
79
|
-

|
|
98
|
+
|
|
99
|
+
The demo video demonstrating how to use Film Maker utilizes the [cloud data of Kyobashi Station Area](https://www.geospatial.jp/ckan/dataset/kyoubasiekisyuuhen_las) located in Osaka, Japan.
|
|
80
100
|
|
|
81
101
|
### 4. Gaussian Viewer
|
|
82
102
|
|
|
@@ -9,12 +9,12 @@ python = "^3.8"
|
|
|
9
9
|
numpy = "^1.21"
|
|
10
10
|
pyside6 = "^6.2"
|
|
11
11
|
PyOpenGL = "^3.1"
|
|
12
|
-
pillow = "^8.3"
|
|
13
12
|
meshio = "^4.4"
|
|
14
13
|
pypcd4 = "^0.1"
|
|
15
14
|
pye57 = "^0.1"
|
|
16
15
|
laspy = "^2.0"
|
|
17
16
|
imageio = "^2.9"
|
|
17
|
+
matplotlib = "^3.4"
|
|
18
18
|
|
|
19
19
|
[tool.poetry.scripts]
|
|
20
20
|
cloud_viewer = "q3dviewer.tools.cloud_viewer:main"
|
|
@@ -25,7 +25,7 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
25
25
|
self.active_keys = set()
|
|
26
26
|
self.show_center = False
|
|
27
27
|
self.enable_show_center = True
|
|
28
|
-
self.
|
|
28
|
+
self.need_recalc_view = True
|
|
29
29
|
self.view_matrix = self.get_view_matrix()
|
|
30
30
|
self.projection_matrix = self.get_projection_matrix()
|
|
31
31
|
|
|
@@ -98,7 +98,7 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
98
98
|
|
|
99
99
|
def set_view_matrix(self, view_matrix):
|
|
100
100
|
self.view_matrix = view_matrix
|
|
101
|
-
self.
|
|
101
|
+
self.need_recalc_view = False
|
|
102
102
|
|
|
103
103
|
def mouseReleaseEvent(self, ev):
|
|
104
104
|
if hasattr(self, 'mousePos'):
|
|
@@ -106,13 +106,13 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
106
106
|
|
|
107
107
|
def set_dist(self, dist):
|
|
108
108
|
self.dist = dist
|
|
109
|
-
self.
|
|
109
|
+
self.need_recalc_view = True
|
|
110
110
|
|
|
111
111
|
def update_dist(self, delta):
|
|
112
112
|
self.dist += delta
|
|
113
113
|
if self.dist < 0.1:
|
|
114
114
|
self.dist = 0.1
|
|
115
|
-
self.
|
|
115
|
+
self.need_recalc_view = True
|
|
116
116
|
|
|
117
117
|
def wheelEvent(self, ev):
|
|
118
118
|
delta = ev.angleDelta().x()
|
|
@@ -121,6 +121,24 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
121
121
|
self.update_dist(-delta * self.dist * 0.001)
|
|
122
122
|
self.show_center = True
|
|
123
123
|
|
|
124
|
+
|
|
125
|
+
def rotate_keep_cam_pos(self, rx=0, ry=0, rz=0):
|
|
126
|
+
"""
|
|
127
|
+
Rotate the camera while keeping the current camera position.
|
|
128
|
+
This updates both the Euler angles and the center point.
|
|
129
|
+
"""
|
|
130
|
+
new_euler = self.euler + np.array([rx, ry, rz])
|
|
131
|
+
new_euler = (new_euler + np.pi) % (2 * np.pi) - np.pi
|
|
132
|
+
|
|
133
|
+
Rwc_old = euler_to_matrix(self.euler)
|
|
134
|
+
tco = np.array([0, 0, self.dist])
|
|
135
|
+
twc = self.center + Rwc_old @ tco
|
|
136
|
+
|
|
137
|
+
Rwc_new = euler_to_matrix(new_euler)
|
|
138
|
+
self.center = twc - Rwc_new @ tco
|
|
139
|
+
self.euler = new_euler
|
|
140
|
+
self.need_recalc_view = True
|
|
141
|
+
|
|
124
142
|
def mouseMoveEvent(self, ev):
|
|
125
143
|
lpos = ev.localPos()
|
|
126
144
|
if not hasattr(self, 'mousePos'):
|
|
@@ -131,24 +149,26 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
131
149
|
rot_speed = 0.2
|
|
132
150
|
dyaw = radians(-diff.x() * rot_speed)
|
|
133
151
|
droll = radians(-diff.y() * rot_speed)
|
|
134
|
-
|
|
152
|
+
if ev.modifiers() & QtCore.Qt.ShiftModifier:
|
|
153
|
+
self.rotate_keep_cam_pos(droll, 0, dyaw)
|
|
154
|
+
else:
|
|
155
|
+
self.rotate(droll, 0, dyaw)
|
|
135
156
|
elif ev.buttons() == QtCore.Qt.MouseButton.LeftButton:
|
|
136
157
|
Rwc = euler_to_matrix(self.euler)
|
|
137
158
|
Kinv = np.linalg.inv(self.get_K())
|
|
138
159
|
dist = max(self.dist, 0.5)
|
|
139
|
-
self.
|
|
160
|
+
self.translate(Rwc @ Kinv @ np.array([-diff.x(), diff.y(), 0]) * dist)
|
|
140
161
|
self.show_center = True
|
|
141
|
-
self.view_need_update = True
|
|
142
162
|
|
|
143
163
|
def set_center(self, center):
|
|
144
164
|
self.center = center
|
|
145
|
-
self.
|
|
165
|
+
self.need_recalc_view = True
|
|
146
166
|
|
|
147
167
|
def paintGL(self):
|
|
148
168
|
# if the camera is moved, update the model view matrix.
|
|
149
|
-
if self.
|
|
169
|
+
if self.need_recalc_view:
|
|
150
170
|
self.view_matrix = self.get_view_matrix()
|
|
151
|
-
self.
|
|
171
|
+
self.need_recalc_view = False
|
|
152
172
|
self.update_model_view()
|
|
153
173
|
|
|
154
174
|
# set the background color
|
|
@@ -192,45 +212,48 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
192
212
|
return
|
|
193
213
|
rot_speed = 0.5
|
|
194
214
|
trans_speed = max(self.dist * 0.005, 0.1)
|
|
215
|
+
shift_pressed = QtCore.Qt.Key_Shift in self.active_keys
|
|
195
216
|
# Handle rotation keys
|
|
196
217
|
if QtCore.Qt.Key_Up in self.active_keys:
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
if shift_pressed:
|
|
219
|
+
self.rotate_keep_cam_pos(radians(rot_speed), 0, 0)
|
|
220
|
+
else:
|
|
221
|
+
self.rotate(radians(rot_speed), 0, 0)
|
|
199
222
|
if QtCore.Qt.Key_Down in self.active_keys:
|
|
200
|
-
|
|
201
|
-
|
|
223
|
+
if shift_pressed:
|
|
224
|
+
self.rotate_keep_cam_pos(radians(-rot_speed), 0, 0)
|
|
225
|
+
else:
|
|
226
|
+
self.rotate(radians(-rot_speed), 0, 0)
|
|
202
227
|
if QtCore.Qt.Key_Left in self.active_keys:
|
|
203
|
-
|
|
204
|
-
|
|
228
|
+
if shift_pressed:
|
|
229
|
+
self.rotate_keep_cam_pos(0, 0, radians(rot_speed))
|
|
230
|
+
else:
|
|
231
|
+
self.rotate(0, 0, radians(rot_speed))
|
|
205
232
|
if QtCore.Qt.Key_Right in self.active_keys:
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
if shift_pressed:
|
|
234
|
+
self.rotate_keep_cam_pos(0, 0, radians(-rot_speed))
|
|
235
|
+
else:
|
|
236
|
+
self.rotate(0, 0, radians(-rot_speed))
|
|
208
237
|
# Handle zoom keys
|
|
209
238
|
xz_keys = {QtCore.Qt.Key_Z, QtCore.Qt.Key_X}
|
|
210
239
|
if self.active_keys & xz_keys:
|
|
211
240
|
Rwc = euler_to_matrix(self.euler)
|
|
212
241
|
if QtCore.Qt.Key_Z in self.active_keys:
|
|
213
|
-
self.
|
|
214
|
-
self.view_need_update = True
|
|
242
|
+
self.translate(Rwc @ np.array([0, 0, -trans_speed]))
|
|
215
243
|
if QtCore.Qt.Key_X in self.active_keys:
|
|
216
|
-
self.
|
|
217
|
-
self.view_need_update = True
|
|
244
|
+
self.translate(Rwc @ np.array([0, 0, trans_speed]))
|
|
218
245
|
# Handle translation keys on the z plane
|
|
219
246
|
dir_keys = {QtCore.Qt.Key_W, QtCore.Qt.Key_S, QtCore.Qt.Key_A, QtCore.Qt.Key_D}
|
|
220
247
|
if self.active_keys & dir_keys:
|
|
221
248
|
Rz = euler_to_matrix([0, 0, self.euler[2]])
|
|
222
249
|
if QtCore.Qt.Key_W in self.active_keys:
|
|
223
|
-
self.
|
|
224
|
-
self.view_need_update = True
|
|
250
|
+
self.translate(Rz @ np.array([0, trans_speed, 0]))
|
|
225
251
|
if QtCore.Qt.Key_S in self.active_keys:
|
|
226
|
-
self.
|
|
227
|
-
self.view_need_update = True
|
|
252
|
+
self.translate(Rz @ np.array([0, -trans_speed, 0]))
|
|
228
253
|
if QtCore.Qt.Key_A in self.active_keys:
|
|
229
|
-
self.
|
|
230
|
-
self.view_need_update = True
|
|
254
|
+
self.translate(Rz @ np.array([-trans_speed, 0, 0]))
|
|
231
255
|
if QtCore.Qt.Key_D in self.active_keys:
|
|
232
|
-
self.
|
|
233
|
-
self.view_need_update = True
|
|
256
|
+
self.translate(Rz @ np.array([trans_speed, 0, 0]))
|
|
234
257
|
|
|
235
258
|
def update_model_view(self):
|
|
236
259
|
glMatrixMode(GL_MODELVIEW)
|
|
@@ -259,7 +282,7 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
259
282
|
|
|
260
283
|
def set_euler(self, euler):
|
|
261
284
|
self.euler = euler
|
|
262
|
-
self.
|
|
285
|
+
self.need_recalc_view = True
|
|
263
286
|
|
|
264
287
|
def set_color(self, color):
|
|
265
288
|
self.color = color
|
|
@@ -303,6 +326,11 @@ class BaseGLWidget(QtOpenGLWidgets.QOpenGLWidget):
|
|
|
303
326
|
self.euler[2] = (self.euler[2] + np.pi) % (2 * np.pi) - np.pi
|
|
304
327
|
self.euler[1] = (self.euler[1] + np.pi) % (2 * np.pi) - np.pi
|
|
305
328
|
self.euler[0] = np.clip(self.euler[0], 0, np.pi)
|
|
329
|
+
self.need_recalc_view = True
|
|
330
|
+
|
|
331
|
+
def translate(self, trans):
|
|
332
|
+
self.center += trans
|
|
333
|
+
self.need_recalc_view = True
|
|
306
334
|
|
|
307
335
|
def change_show_center(self, state):
|
|
308
336
|
self.enable_show_center = state
|
|
@@ -61,18 +61,28 @@ class CloudIOItem(CloudItem):
|
|
|
61
61
|
def load(self, file, append=False):
|
|
62
62
|
# print("Try to load %s ..." % file)
|
|
63
63
|
if file.endswith(".pcd"):
|
|
64
|
-
cloud
|
|
64
|
+
cloud = load_pcd(file)
|
|
65
65
|
elif file.endswith(".ply"):
|
|
66
|
-
cloud
|
|
66
|
+
cloud = load_ply(file)
|
|
67
67
|
elif file.endswith(".e57"):
|
|
68
|
-
cloud
|
|
68
|
+
cloud = load_e57(file)
|
|
69
69
|
elif file.endswith(".las"):
|
|
70
|
-
cloud
|
|
70
|
+
cloud = load_las(file)
|
|
71
71
|
else:
|
|
72
72
|
print("Not supported file type.")
|
|
73
73
|
return
|
|
74
74
|
self.set_data(data=cloud, append=append)
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
has_intensity = cloud['irgb'][0] & 0xff000000 > 0
|
|
77
|
+
has_rgb = cloud['irgb'][0] & 0x00ffffff > 0
|
|
78
|
+
|
|
79
|
+
if has_rgb:
|
|
80
|
+
self.set_color_mode('RGB')
|
|
81
|
+
elif has_intensity:
|
|
82
|
+
self.set_color_mode('I')
|
|
83
|
+
else:
|
|
84
|
+
self.set_color_mode('FLAT')
|
|
85
|
+
|
|
76
86
|
return cloud
|
|
77
87
|
|
|
78
88
|
def set_path(self, path):
|
|
@@ -14,15 +14,13 @@ from PySide6.QtWidgets import QLabel, QLineEdit, QDoubleSpinBox, \
|
|
|
14
14
|
from OpenGL.GL import shaders
|
|
15
15
|
from q3dviewer.utils import *
|
|
16
16
|
from q3dviewer.utils.range_slider import RangeSlider
|
|
17
|
-
from PySide6.QtCore import QRegularExpression
|
|
18
|
-
from PySide6.QtGui import QRegularExpressionValidator
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
# draw points with color (x, y, z, color)
|
|
22
20
|
class CloudItem(BaseItem):
|
|
23
21
|
def __init__(self, size, alpha,
|
|
24
22
|
color_mode='I',
|
|
25
|
-
color='
|
|
23
|
+
color='white',
|
|
26
24
|
point_type='PIXEL',
|
|
27
25
|
depth_test=False):
|
|
28
26
|
super().__init__()
|
|
@@ -34,7 +32,12 @@ class CloudItem(BaseItem):
|
|
|
34
32
|
self.point_type = point_type
|
|
35
33
|
self.mutex = threading.Lock()
|
|
36
34
|
self.data_type = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
37
|
-
self.
|
|
35
|
+
self.color = color
|
|
36
|
+
try:
|
|
37
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
38
|
+
except ValueError:
|
|
39
|
+
print(f"Invalid color: {color}, please use matplotlib color format")
|
|
40
|
+
exit(1)
|
|
38
41
|
self.mode_table = {'FLAT': 0, 'I': 1, 'RGB': 2}
|
|
39
42
|
self.point_type_table = {'PIXEL': 0, 'SQUARE': 1, 'SPHERE': 2}
|
|
40
43
|
self.color_mode = self.mode_table[color_mode]
|
|
@@ -89,14 +92,12 @@ class CloudItem(BaseItem):
|
|
|
89
92
|
layout.addWidget(self.combo_color)
|
|
90
93
|
|
|
91
94
|
label_rgb = QLabel("Color:")
|
|
95
|
+
label_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
92
96
|
layout.addWidget(label_rgb)
|
|
93
97
|
self.edit_rgb = QLineEdit()
|
|
94
|
-
self.edit_rgb.setToolTip("
|
|
95
|
-
self.edit_rgb.setText(
|
|
98
|
+
self.edit_rgb.setToolTip("Use hex color, i.e. #FF4500, or named color, i.e. 'red'")
|
|
99
|
+
self.edit_rgb.setText(self.color)
|
|
96
100
|
self.edit_rgb.textChanged.connect(self._on_color)
|
|
97
|
-
regex = QRegularExpression(r"^#[0-9A-Fa-f]{6}$")
|
|
98
|
-
validator = QRegularExpressionValidator(regex)
|
|
99
|
-
self.edit_rgb.setValidator(validator)
|
|
100
101
|
layout.addWidget(self.edit_rgb)
|
|
101
102
|
|
|
102
103
|
self.slider_v = RangeSlider()
|
|
@@ -108,6 +109,7 @@ class CloudItem(BaseItem):
|
|
|
108
109
|
"Show front points first (Depth Test)")
|
|
109
110
|
self.checkbox_depth_test.setChecked(self.depth_test)
|
|
110
111
|
self.checkbox_depth_test.stateChanged.connect(self.set_depthtest)
|
|
112
|
+
self._on_color_mode(self.color_mode)
|
|
111
113
|
layout.addWidget(self.checkbox_depth_test)
|
|
112
114
|
|
|
113
115
|
def _on_range(self, lower, upper):
|
|
@@ -164,11 +166,10 @@ class CloudItem(BaseItem):
|
|
|
164
166
|
|
|
165
167
|
def _on_color(self, color):
|
|
166
168
|
try:
|
|
167
|
-
flat_rgb =
|
|
168
|
-
self.flat_rgb = flat_rgb
|
|
169
|
+
self.flat_rgb = text_to_rgba(color, flat=True)
|
|
169
170
|
self.need_update_setting = True
|
|
170
171
|
except ValueError:
|
|
171
|
-
|
|
172
|
+
print(f"Invalid color: {color}, please use matplotlib color format")
|
|
172
173
|
|
|
173
174
|
def set_size(self, size):
|
|
174
175
|
self.size = size
|
|
@@ -162,13 +162,9 @@ class FrameItem(BaseItem):
|
|
|
162
162
|
self.need_updating = False
|
|
163
163
|
|
|
164
164
|
def set_color(self, color):
|
|
165
|
-
|
|
166
|
-
self.rgba =
|
|
167
|
-
|
|
168
|
-
self.rgba = color
|
|
169
|
-
elif isinstance(color, tuple):
|
|
170
|
-
self.rgba = list(color)
|
|
171
|
-
else:
|
|
165
|
+
try:
|
|
166
|
+
self.rgba = text_to_rgba(color)
|
|
167
|
+
except ValueError:
|
|
172
168
|
raise ValueError("Invalid color format")
|
|
173
169
|
|
|
174
170
|
def set_line_width(self, width):
|
|
@@ -6,10 +6,8 @@ Distributed under MIT license. See LICENSE for more information.
|
|
|
6
6
|
from q3dviewer.base_item import BaseItem
|
|
7
7
|
from OpenGL.GL import *
|
|
8
8
|
from PySide6.QtWidgets import QLabel, QDoubleSpinBox, QLineEdit
|
|
9
|
-
from PySide6.QtCore import QRegularExpression
|
|
10
|
-
from PySide6.QtGui import QRegularExpressionValidator
|
|
11
9
|
import numpy as np
|
|
12
|
-
from q3dviewer.utils.maths import
|
|
10
|
+
from q3dviewer.utils.maths import text_to_rgba
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
class GridItem(BaseItem):
|
|
@@ -24,9 +22,9 @@ class GridItem(BaseItem):
|
|
|
24
22
|
|
|
25
23
|
def set_color(self, color):
|
|
26
24
|
try:
|
|
27
|
-
self.rgba =
|
|
25
|
+
self.rgba = text_to_rgba(color)
|
|
28
26
|
except ValueError:
|
|
29
|
-
|
|
27
|
+
raise ValueError("Invalid color format. Use hex format like '#RRGGBB' or '#RRGGBBAA'.")
|
|
30
28
|
|
|
31
29
|
def generate_grid_vertices(self):
|
|
32
30
|
vertices = []
|
|
@@ -41,16 +39,13 @@ class GridItem(BaseItem):
|
|
|
41
39
|
def initialize_gl(self):
|
|
42
40
|
self.vao = glGenVertexArrays(1)
|
|
43
41
|
vbo = glGenBuffers(1)
|
|
44
|
-
|
|
45
42
|
glBindVertexArray(self.vao)
|
|
46
|
-
|
|
47
43
|
glBindBuffer(GL_ARRAY_BUFFER, vbo)
|
|
48
44
|
glBufferData(GL_ARRAY_BUFFER, self.vertices.nbytes, self.vertices, GL_STATIC_DRAW)
|
|
49
|
-
|
|
50
45
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, ctypes.c_void_p(0))
|
|
51
46
|
glEnableVertexAttribArray(0)
|
|
52
|
-
|
|
53
47
|
glBindVertexArray(0)
|
|
48
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
54
49
|
|
|
55
50
|
def add_setting(self, layout):
|
|
56
51
|
spinbox_size = QDoubleSpinBox()
|
|
@@ -73,6 +68,7 @@ class GridItem(BaseItem):
|
|
|
73
68
|
spinbox_offset_x.setPrefix("Offset X: ")
|
|
74
69
|
spinbox_offset_x.setSingleStep(0.1)
|
|
75
70
|
spinbox_offset_x.setValue(self.offset[0])
|
|
71
|
+
spinbox_offset_x.setRange(-1000, 1000)
|
|
76
72
|
spinbox_offset_x.valueChanged.connect(self._on_offset_x)
|
|
77
73
|
layout.addWidget(spinbox_offset_x)
|
|
78
74
|
|
|
@@ -80,6 +76,7 @@ class GridItem(BaseItem):
|
|
|
80
76
|
spinbox_offset_y.setPrefix("Offset Y: ")
|
|
81
77
|
spinbox_offset_y.setSingleStep(0.1)
|
|
82
78
|
spinbox_offset_y.setValue(self.offset[1])
|
|
79
|
+
spinbox_offset_y.setRange(-1000, 1000)
|
|
83
80
|
spinbox_offset_y.valueChanged.connect(self._on_offset_y)
|
|
84
81
|
layout.addWidget(spinbox_offset_y)
|
|
85
82
|
|
|
@@ -87,6 +84,7 @@ class GridItem(BaseItem):
|
|
|
87
84
|
spinbox_offset_z.setPrefix("Offset Z: ")
|
|
88
85
|
spinbox_offset_z.setSingleStep(0.1)
|
|
89
86
|
spinbox_offset_z.setValue(self.offset[2])
|
|
87
|
+
spinbox_offset_z.setRange(-1000, 1000)
|
|
90
88
|
spinbox_offset_z.valueChanged.connect(self._on_offset_z)
|
|
91
89
|
layout.addWidget(spinbox_offset_z)
|
|
92
90
|
|
|
@@ -7,7 +7,6 @@ from q3dviewer.base_item import BaseItem
|
|
|
7
7
|
from OpenGL.GL import *
|
|
8
8
|
import numpy as np
|
|
9
9
|
from OpenGL.GL import shaders
|
|
10
|
-
from PIL import Image as PIL_Image
|
|
11
10
|
from PySide6.QtWidgets import QLabel, QSpinBox, QCheckBox
|
|
12
11
|
|
|
13
12
|
|
|
@@ -108,12 +107,8 @@ class ImageItem(BaseItem):
|
|
|
108
107
|
glBindVertexArray(0)
|
|
109
108
|
|
|
110
109
|
def set_data(self, data):
|
|
111
|
-
if isinstance(data, np.ndarray):
|
|
112
|
-
|
|
113
|
-
elif isinstance(data, PIL_Image.Image):
|
|
114
|
-
data = np.array(data)
|
|
115
|
-
else:
|
|
116
|
-
print("not support image type")
|
|
110
|
+
if not isinstance(data, np.ndarray):
|
|
111
|
+
print("The image type is not supported.")
|
|
117
112
|
raise NotImplementedError
|
|
118
113
|
|
|
119
114
|
if data.ndim == 2: # Grayscale image
|
|
@@ -8,13 +8,16 @@ from OpenGL.GL import *
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import threading
|
|
10
10
|
from PySide6.QtWidgets import QLabel, QLineEdit, QDoubleSpinBox
|
|
11
|
-
from
|
|
12
|
-
from PySide6.QtGui import QRegularExpressionValidator
|
|
13
|
-
from q3dviewer.utils.maths import hex_to_rgba
|
|
11
|
+
from q3dviewer.utils.maths import text_to_rgba
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class LineItem(BaseItem):
|
|
17
15
|
def __init__(self, width=1, color='#00ff00', line_type='LINE_STRIP'):
|
|
16
|
+
"""
|
|
17
|
+
line_type: 'LINE_STRIP' or 'LINES'
|
|
18
|
+
LINE_STRIP: draw a connected line strip
|
|
19
|
+
LINES: draw a series of unconnected lines
|
|
20
|
+
"""
|
|
18
21
|
super(LineItem, self).__init__()
|
|
19
22
|
self.width = width
|
|
20
23
|
self.buff = np.empty((0, 3), np.float32)
|
|
@@ -23,19 +26,20 @@ class LineItem(BaseItem):
|
|
|
23
26
|
self.capacity = 100000
|
|
24
27
|
self.valid_buff_top = 0
|
|
25
28
|
self.color = color
|
|
26
|
-
|
|
29
|
+
try:
|
|
30
|
+
self.rgb = text_to_rgba(color)
|
|
31
|
+
except ValueError:
|
|
32
|
+
raise ValueError("Invalid color format. Use mathplotlib color format.")
|
|
33
|
+
|
|
27
34
|
self.line_type = GL_LINE_STRIP if line_type == 'LINE_STRIP' else GL_LINES
|
|
28
35
|
|
|
29
36
|
def add_setting(self, layout):
|
|
30
37
|
label_color = QLabel("Color:")
|
|
31
38
|
layout.addWidget(label_color)
|
|
32
39
|
self.color_edit = QLineEdit()
|
|
33
|
-
self.color_edit.setToolTip("
|
|
40
|
+
self.color_edit.setToolTip("mathplotlib color format")
|
|
34
41
|
self.color_edit.setText(self.color)
|
|
35
42
|
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
43
|
layout.addWidget(self.color_edit)
|
|
40
44
|
|
|
41
45
|
spinbox_width = QDoubleSpinBox()
|
|
@@ -48,10 +52,10 @@ class LineItem(BaseItem):
|
|
|
48
52
|
|
|
49
53
|
def _on_color(self, color):
|
|
50
54
|
try:
|
|
51
|
-
self.rgb =
|
|
55
|
+
self.rgb = text_to_rgba(color)
|
|
52
56
|
self.color = color
|
|
53
57
|
except ValueError:
|
|
54
|
-
|
|
58
|
+
print("Invalid color format. Use mathplotlib color format.")
|
|
55
59
|
|
|
56
60
|
def set_color(self, color):
|
|
57
61
|
self.color_edit.setText(color)
|
|
@@ -6,7 +6,7 @@ Distributed under MIT license. See LICENSE for more information.
|
|
|
6
6
|
from PySide6 import QtCore, QtGui
|
|
7
7
|
from q3dviewer.base_item import BaseItem
|
|
8
8
|
from OpenGL.GL import *
|
|
9
|
-
from q3dviewer.utils.maths import
|
|
9
|
+
from q3dviewer.utils.maths import text_to_rgba
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Text2DItem(BaseItem):
|
|
@@ -16,8 +16,10 @@ class Text2DItem(BaseItem):
|
|
|
16
16
|
"""All keyword arguments are passed to set_data()"""
|
|
17
17
|
BaseItem.__init__(self)
|
|
18
18
|
self.pos = (20, 50)
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
try:
|
|
20
|
+
self.rgb = text_to_rgba(self.color)
|
|
21
|
+
except ValueError:
|
|
22
|
+
raise ValueError("Invalid color format. Use mathplotlib color format.")
|
|
21
23
|
self.text = ''
|
|
22
24
|
self.font = QtGui.QFont('Helvetica', 16)
|
|
23
25
|
self.set_data(**kwds)
|
|
@@ -44,10 +46,10 @@ class Text2DItem(BaseItem):
|
|
|
44
46
|
|
|
45
47
|
def set_color(self, color):
|
|
46
48
|
try:
|
|
47
|
-
self.rgb =
|
|
49
|
+
self.rgb = text_to_rgba(color)
|
|
48
50
|
self.color = color
|
|
49
51
|
except ValueError:
|
|
50
|
-
|
|
52
|
+
print("Invalid color format. Use mathplotlib color format.")
|
|
51
53
|
|
|
52
54
|
def paint(self):
|
|
53
55
|
if len(self.text) < 1:
|
|
@@ -5,11 +5,11 @@ Distributed under MIT license. See LICENSE for more information.
|
|
|
5
5
|
|
|
6
6
|
from PySide6 import QtCore
|
|
7
7
|
from PySide6.QtWidgets import QWidget, QComboBox, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QGroupBox
|
|
8
|
-
from PySide6.QtGui import QKeyEvent, QVector3D
|
|
9
|
-
from PySide6.QtCore import QRegularExpression
|
|
8
|
+
from PySide6.QtGui import QKeyEvent, QVector3D
|
|
10
9
|
from OpenGL.GL import *
|
|
11
10
|
import numpy as np
|
|
12
11
|
from q3dviewer.base_glwidget import BaseGLWidget
|
|
12
|
+
from q3dviewer.utils.maths import text_to_rgba
|
|
13
13
|
|
|
14
14
|
class SettingWindow(QWidget):
|
|
15
15
|
def __init__(self):
|
|
@@ -52,7 +52,7 @@ class GLWidget(BaseGLWidget):
|
|
|
52
52
|
def __init__(self):
|
|
53
53
|
self.followed_name = 'none'
|
|
54
54
|
self.named_items = {}
|
|
55
|
-
self.color_str = '
|
|
55
|
+
self.color_str = 'black'
|
|
56
56
|
self.followable_item_name = None
|
|
57
57
|
self.setting_window = SettingWindow()
|
|
58
58
|
self.enable_show_center = True
|
|
@@ -80,9 +80,6 @@ class GLWidget(BaseGLWidget):
|
|
|
80
80
|
color_edit.setToolTip("'using hex color, i.e. #FF4500")
|
|
81
81
|
color_edit.setText(self.color_str)
|
|
82
82
|
color_edit.textChanged.connect(self.set_bg_color)
|
|
83
|
-
regex = QRegularExpression(r"^#[0-9A-Fa-f]{6}$")
|
|
84
|
-
validator = QRegularExpressionValidator(regex)
|
|
85
|
-
color_edit.setValidator(validator)
|
|
86
83
|
layout.addWidget(color_edit)
|
|
87
84
|
|
|
88
85
|
label_focus = QLabel("Set Focus:")
|
|
@@ -108,16 +105,13 @@ class GLWidget(BaseGLWidget):
|
|
|
108
105
|
if item.__class__.__name__ == 'AxisItem' and not item._disable_setting:
|
|
109
106
|
self.followable_item_name.append(name)
|
|
110
107
|
|
|
111
|
-
def set_bg_color(self,
|
|
108
|
+
def set_bg_color(self, color):
|
|
112
109
|
try:
|
|
113
|
-
|
|
114
|
-
red = (
|
|
115
|
-
green
|
|
116
|
-
blue = color_flat & 0xFF
|
|
117
|
-
self.color_str = color_str
|
|
118
|
-
self.set_color([red, green, blue, 0])
|
|
110
|
+
self.color_str = color
|
|
111
|
+
red, green, blue, alpha = text_to_rgba(color)
|
|
112
|
+
self.set_color([red, green, blue, alpha])
|
|
119
113
|
except ValueError:
|
|
120
|
-
|
|
114
|
+
print("Invalid color format. Use mathplotlib color format.")
|
|
121
115
|
|
|
122
116
|
def add_item_with_name(self, name, item):
|
|
123
117
|
self.named_items.update({name: item})
|
|
@@ -151,7 +151,6 @@ class CMMViewer(q3d.Viewer):
|
|
|
151
151
|
main_layout.addWidget(dock_widget)
|
|
152
152
|
self.dock = dock_widget
|
|
153
153
|
|
|
154
|
-
|
|
155
154
|
def update_video_path(self, path):
|
|
156
155
|
self.video_path = path
|
|
157
156
|
|
|
@@ -235,7 +234,6 @@ class CMMViewer(q3d.Viewer):
|
|
|
235
234
|
self.glwidget.set_cam_position(center=center,
|
|
236
235
|
euler=euler)
|
|
237
236
|
|
|
238
|
-
|
|
239
237
|
def create_frames(self):
|
|
240
238
|
"""
|
|
241
239
|
Create the frames for playback by interpolating between key frames.
|
|
@@ -24,17 +24,14 @@ def load_ply(file):
|
|
|
24
24
|
xyz = mesh.points
|
|
25
25
|
rgb = np.zeros([xyz.shape[0]], dtype=np.uint32)
|
|
26
26
|
intensity = np.zeros([xyz.shape[0]], dtype=np.uint32)
|
|
27
|
-
color_mode = 'FLAT'
|
|
28
27
|
if "intensity" in mesh.point_data:
|
|
29
28
|
intensity = mesh.point_data["intensity"]
|
|
30
|
-
color_mode = 'I'
|
|
31
29
|
if "rgb" in mesh.point_data:
|
|
32
30
|
rgb = mesh.point_data["rgb"]
|
|
33
|
-
color_mode = 'RGB'
|
|
34
31
|
irgb = (intensity << 24) | rgb
|
|
35
32
|
dtype = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
36
33
|
cloud = np.rec.fromarrays([xyz, irgb], dtype=dtype)
|
|
37
|
-
return cloud
|
|
34
|
+
return cloud
|
|
38
35
|
|
|
39
36
|
|
|
40
37
|
def save_pcd(cloud, save_path):
|
|
@@ -51,8 +48,35 @@ def save_pcd(cloud, save_path):
|
|
|
51
48
|
i = (cloud['irgb'] & 0xFF000000) >> 24
|
|
52
49
|
rgb = cloud['irgb'] & 0x00FFFFFF
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
# check no rgb value
|
|
52
|
+
if np.max(rgb) == 0:
|
|
53
|
+
fields = ('x', 'y', 'z', 'intensity')
|
|
54
|
+
metadata = MetaData.model_validate(
|
|
55
|
+
{
|
|
56
|
+
"fields": fields,
|
|
57
|
+
"size": [4, 4, 4, 4],
|
|
58
|
+
"type": ['F', 'F', 'F', 'U'],
|
|
59
|
+
"count": [1, 1, 1, 1],
|
|
60
|
+
"width": cloud.shape[0],
|
|
61
|
+
"points": cloud.shape[0],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
dtype = [('xyz', '<f4', (3,)), ('intensity', '<u4')]
|
|
65
|
+
tmp = np.rec.fromarrays([cloud['xyz'], i], dtype=dtype)
|
|
66
|
+
PointCloud(metadata, tmp).save(save_path)
|
|
67
|
+
else:
|
|
68
|
+
fields = ('x', 'y', 'z', 'intensity', 'rgb')
|
|
69
|
+
metadata = MetaData.model_validate(
|
|
70
|
+
{
|
|
71
|
+
"fields": fields,
|
|
72
|
+
"size": [4, 4, 4, 4, 4],
|
|
73
|
+
"type": ['F', 'F', 'F', 'U', 'U'],
|
|
74
|
+
"count": [1, 1, 1, 1, 1],
|
|
75
|
+
"width": cloud.shape[0],
|
|
76
|
+
"points": cloud.shape[0],
|
|
77
|
+
})
|
|
78
|
+
dtype = [('xyz', '<f4', (3,)), ('intensity', '<u4'), ('rgb', '<u4')]
|
|
79
|
+
tmp = np.rec.fromarrays([cloud['xyz'], i, rgb], dtype=dtype)
|
|
56
80
|
|
|
57
81
|
PointCloud(metadata, tmp).save(save_path)
|
|
58
82
|
|
|
@@ -62,17 +86,14 @@ def load_pcd(file):
|
|
|
62
86
|
pc = PointCloud.from_path(file).pc_data
|
|
63
87
|
rgb = np.zeros([pc.shape[0]], dtype=np.uint32)
|
|
64
88
|
intensity = np.zeros([pc.shape[0]], dtype=np.uint32)
|
|
65
|
-
color_mode = 'FLAT'
|
|
66
89
|
if 'intensity' in pc.dtype.names:
|
|
67
90
|
intensity = pc['intensity'].astype(np.uint32)
|
|
68
|
-
color_mode = 'I'
|
|
69
91
|
if 'rgb' in pc.dtype.names:
|
|
70
92
|
rgb = pc['rgb'].astype(np.uint32)
|
|
71
|
-
color_mode = 'RGB'
|
|
72
93
|
irgb = (intensity << 24) | rgb
|
|
73
94
|
xyz = np.stack([pc['x'], pc['y'], pc['z']], axis=1)
|
|
74
95
|
cloud = np.rec.fromarrays([xyz, irgb], dtype=dtype)
|
|
75
|
-
return cloud
|
|
96
|
+
return cloud
|
|
76
97
|
|
|
77
98
|
|
|
78
99
|
def save_e57(cloud, save_path):
|
|
@@ -100,23 +121,20 @@ def load_e57(file_path):
|
|
|
100
121
|
z = scans["cartesianZ"]
|
|
101
122
|
rgb = np.zeros([x.shape[0]], dtype=np.uint32)
|
|
102
123
|
intensity = np.zeros([x.shape[0]], dtype=np.uint32)
|
|
103
|
-
color_mode = 'FLAT'
|
|
104
124
|
if "intensity" in scans:
|
|
105
125
|
intensity = scans["intensity"].astype(np.uint32)
|
|
106
|
-
color_mode = 'I'
|
|
107
126
|
if all([x in scans for x in ["colorRed", "colorGreen", "colorBlue"]]):
|
|
108
127
|
r = scans["colorRed"].astype(np.uint32)
|
|
109
128
|
g = scans["colorGreen"].astype(np.uint32)
|
|
110
129
|
b = scans["colorBlue"].astype(np.uint32)
|
|
111
130
|
rgb = (r << 16) | (g << 8) | b
|
|
112
|
-
color_mode = 'RGB'
|
|
113
131
|
irgb = (intensity << 24) | rgb
|
|
114
132
|
dtype = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
115
133
|
cloud = np.rec.fromarrays(
|
|
116
134
|
[np.stack([x, y, z], axis=1), irgb],
|
|
117
135
|
dtype=dtype)
|
|
118
136
|
e57.close()
|
|
119
|
-
return cloud
|
|
137
|
+
return cloud
|
|
120
138
|
|
|
121
139
|
|
|
122
140
|
def load_las(file):
|
|
@@ -124,27 +142,24 @@ def load_las(file):
|
|
|
124
142
|
las = f.read()
|
|
125
143
|
xyz = np.vstack((las.x, las.y, las.z)).transpose()
|
|
126
144
|
dimensions = list(las.point_format.dimension_names)
|
|
127
|
-
color_mode = 'FLAT'
|
|
128
145
|
rgb = np.zeros([las.x.shape[0]], dtype=np.uint32)
|
|
129
146
|
intensity = np.zeros([las.x.shape[0]], dtype=np.uint32)
|
|
130
147
|
if 'intensity' in dimensions:
|
|
131
148
|
intensity = las.intensity.astype(np.uint32)
|
|
132
|
-
color_mode = 'I'
|
|
133
149
|
if 'red' in dimensions and 'green' in dimensions and 'blue' in dimensions:
|
|
134
150
|
red = las.red
|
|
135
151
|
green = las.green
|
|
136
152
|
blue = las.blue
|
|
137
153
|
max_val = np.max([red, green, blue])
|
|
138
154
|
if red.dtype == np.dtype('uint16') and max_val > 255:
|
|
139
|
-
red = (red /
|
|
140
|
-
green = (green /
|
|
141
|
-
blue = (blue /
|
|
155
|
+
red = (red / 255).astype(np.uint32)
|
|
156
|
+
green = (green / 255).astype(np.uint32)
|
|
157
|
+
blue = (blue / 255).astype(np.uint32)
|
|
142
158
|
rgb = (red << 16) | (green << 8) | blue
|
|
143
|
-
|
|
144
|
-
color = (intensity << 24) | rgb
|
|
159
|
+
color = ((intensity / 255)<< 24) | rgb
|
|
145
160
|
dtype = [('xyz', '<f4', (3,)), ('irgb', '<u4')]
|
|
146
161
|
cloud = np.rec.fromarrays([xyz, color], dtype=dtype)
|
|
147
|
-
return cloud
|
|
162
|
+
return cloud
|
|
148
163
|
|
|
149
164
|
def save_las(cloud, save_path):
|
|
150
165
|
header = laspy.LasHeader(point_format=3, version="1.2")
|
|
@@ -152,10 +167,10 @@ def save_las(cloud, save_path):
|
|
|
152
167
|
las.x = cloud['xyz'][:, 0]
|
|
153
168
|
las.y = cloud['xyz'][:, 1]
|
|
154
169
|
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
|
|
170
|
+
las.red = ((cloud['irgb'] >> 16) & 0xFF) * 255
|
|
171
|
+
las.green = ((cloud['irgb'] >> 8) & 0xFF) * 255
|
|
172
|
+
las.blue = (cloud['irgb'] & 0xFF)
|
|
173
|
+
las.intensity = (cloud['irgb'] >> 24) * 255
|
|
159
174
|
las.write(save_path)
|
|
160
175
|
|
|
161
176
|
|
|
@@ -10,6 +10,7 @@ https://github.com/scomup/MathematicalRobotics.git
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
import numpy as np
|
|
13
|
+
from matplotlib.colors import to_rgba
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
_epsilon_ = 1e-5
|
|
@@ -292,26 +293,27 @@ def makeRt(T):
|
|
|
292
293
|
t = T[0:3, 3]
|
|
293
294
|
return R, t
|
|
294
295
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return (red / 255.0, green / 255.0, blue / 255.0, alpha / 255.0)
|
|
296
|
+
|
|
297
|
+
def text_to_rgba(color_text, flat=False):
|
|
298
|
+
"""
|
|
299
|
+
Convert a color text to an RGBA tuple.
|
|
300
|
+
|
|
301
|
+
:param color_text: e.g. '#FF0000', '#FF0000FF', 'red', 'green', 'blue', 'yellow',
|
|
302
|
+
'black', 'white', 'magenta', 'cyan', 'r', 'g', 'b', 'y', 'k', 'w', 'm', 'c'
|
|
303
|
+
:return: RGBA tuple, e.g. (1.0, 0.0, 0.0, 1.0)
|
|
304
|
+
"""
|
|
305
|
+
rgba = to_rgba(color_text)
|
|
306
|
+
if flat:
|
|
307
|
+
r, g, b, _ = (np.array(rgba)*255).astype(np.uint32)
|
|
308
|
+
falt_rgb = ((r << 16) & 0xFF0000) | \
|
|
309
|
+
((g << 8) & 0x00FF00) | \
|
|
310
|
+
((b << 0) & 0x0000FF)
|
|
311
|
+
return falt_rgb
|
|
312
312
|
else:
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
return rgba
|
|
314
|
+
# except ValueError as e:
|
|
315
|
+
# raise ValueError(f"Invalid color text '{color_text}': {e}")
|
|
316
|
+
|
|
315
317
|
|
|
316
318
|
# euler = np.array([1, 0.1, 0.1])
|
|
317
319
|
# euler_angles = matrix_to_euler(euler_to_matrix(euler))
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: q3dviewer
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
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
7
|
License: UNKNOWN
|
|
8
|
-
Description:
|
|
8
|
+
Description:
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://badge.fury.io/py/q3dviewer)
|
|
9
13
|
|
|
10
14
|
`q3dviewer` is a library designed for quickly deploying a 3D viewer. It is based on Qt (PySide6) and provides efficient OpenGL items for displaying 3D objects (e.g., point clouds, cameras, and 3D Gaussians). You can use it to visualize your 3D data or set up an efficient viewer application. It is inspired by PyQtGraph but focuses more on efficient 3D rendering.
|
|
11
15
|
|
|
16
|
+
|
|
17
|
+
To show how to use `q3dviewer` as a library, we also provide some [very useful tools](#tools).
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
## Installation
|
|
13
21
|
|
|
14
22
|
To install `q3dviewer`, execute the following command in your terminal on either Linux or Windows:
|
|
@@ -37,10 +45,10 @@ Description: ## q3dviewer
|
|
|
37
45
|
|
|
38
46
|
### 1. Cloud Viewer
|
|
39
47
|
|
|
40
|
-
A tool for visualizing point cloud files. Launch it by executing the following command in your terminal:
|
|
48
|
+
A tool for visualizing point cloud files (LAS, PCD, PLY, E57). Launch it by executing the following command in your terminal:
|
|
41
49
|
|
|
42
50
|
```sh
|
|
43
|
-
cloud_viewer
|
|
51
|
+
cloud_viewer
|
|
44
52
|
```
|
|
45
53
|
|
|
46
54
|
*Alternatively*, if the path is not set (though it's not recommended):
|
|
@@ -49,7 +57,13 @@ Description: ## q3dviewer
|
|
|
49
57
|
python3 -m q3dviewer.tools.cloud_viewer
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
**Basic Operations**
|
|
61
|
+
* Load files: Drag and drop point cloud files onto the window (multiple files are OK).
|
|
62
|
+
* `M` key: Display the visualization settings screen for point clouds, background color, etc.
|
|
63
|
+
* `Left mouse button` & `W, A, S, D` keys: Move the viewpoint on the horizontal plane.
|
|
64
|
+
* `Z, X` keys: Move in the direction the screen is facing.
|
|
65
|
+
* `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the screen center unchanged.
|
|
66
|
+
* `Shift` + `Right mouse button` & `Arrow` keys: Rotate the viewpoint while keeping the camera position unchanged.
|
|
53
67
|
|
|
54
68
|
For example, you can download and view point clouds of Tokyo in LAS format from the following link:
|
|
55
69
|
|
|
@@ -75,15 +89,21 @@ Description: ## q3dviewer
|
|
|
75
89
|
Would you like to create a video from point cloud data? With Film Maker, you can easily create videos with simple operations. Just edit keyframes using the user-friendly GUI, and the software will automatically interpolate the keyframes to generate the video.
|
|
76
90
|
|
|
77
91
|
```sh
|
|
78
|
-
film_maker
|
|
92
|
+
film_maker
|
|
79
93
|
```
|
|
80
94
|
|
|
95
|
+
**Basic Operations**
|
|
96
|
+
* File loading & viewpoint movement: Same as Cloud_Viewer
|
|
81
97
|
* Space key to add a keyframe.
|
|
82
98
|
* Delete key to remove a keyframe.
|
|
99
|
+
* Play button: Automatically play the video (pressing again will stop playback)
|
|
100
|
+
* Record checkbox: When checked, actions will be automatically recorded during playback
|
|
83
101
|
|
|
84
102
|
Film Maker GUI:
|
|
85
103
|
|
|
86
|
-

|
|
105
|
+
|
|
106
|
+
The demo video demonstrating how to use Film Maker utilizes the [cloud data of Kyobashi Station Area](https://www.geospatial.jp/ckan/dataset/kyoubasiekisyuuhen_las) located in Osaka, Japan.
|
|
87
107
|
|
|
88
108
|
### 4. Gaussian Viewer
|
|
89
109
|
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name='q3dviewer',
|
|
5
|
-
version='1.1.
|
|
5
|
+
version='1.1.4',
|
|
6
6
|
author="Liu Yang",
|
|
7
7
|
description="A library designed for quickly deploying a 3D viewer.",
|
|
8
8
|
long_description=open("README.md").read(),
|
|
@@ -22,13 +22,13 @@ setup(
|
|
|
22
22
|
'numpy',
|
|
23
23
|
'pyside6',
|
|
24
24
|
'PyOpenGL',
|
|
25
|
-
'pillow',
|
|
26
25
|
'meshio',
|
|
27
26
|
'pypcd4',
|
|
28
27
|
'pye57',
|
|
29
28
|
'laspy',
|
|
30
29
|
'imageio',
|
|
31
30
|
'imageio[ffmpeg]',
|
|
31
|
+
'matplotlib',
|
|
32
32
|
],
|
|
33
33
|
entry_points={
|
|
34
34
|
'console_scripts': [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|