q3dviewer 1.1.6__tar.gz → 1.1.9__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.
Files changed (53) hide show
  1. q3dviewer-1.1.9/PKG-INFO +226 -0
  2. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/README.md +3 -2
  3. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/Qt/__init__.py +1 -0
  4. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/base_glwidget.py +83 -1
  5. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/__init__.py +1 -0
  6. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/cloud_io_item.py +16 -1
  7. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/cloud_item.py +37 -22
  8. q3dviewer-1.1.9/q3dviewer/custom_items/text3d_item.py +152 -0
  9. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/text_item.py +19 -4
  10. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/glwidget.py +23 -3
  11. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/tools/cloud_viewer.py +79 -3
  12. q3dviewer-1.1.9/q3dviewer/tools/example_viewer.py +27 -0
  13. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/tools/film_maker.py +1 -1
  14. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/tools/lidar_cam_calib.py +10 -11
  15. q3dviewer-1.1.9/q3dviewer/utils/convert_ros_msg.py +94 -0
  16. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/viewer.py +4 -1
  17. q3dviewer-1.1.9/q3dviewer.egg-info/PKG-INFO +226 -0
  18. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer.egg-info/SOURCES.txt +1 -7
  19. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer.egg-info/entry_points.txt +1 -0
  20. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/setup.py +1 -1
  21. q3dviewer-1.1.6/LICENSE +0 -21
  22. q3dviewer-1.1.6/PKG-INFO +0 -235
  23. q3dviewer-1.1.6/q3dviewer/shaders/cloud_frag.glsl +0 -28
  24. q3dviewer-1.1.6/q3dviewer/shaders/cloud_vert.glsl +0 -72
  25. q3dviewer-1.1.6/q3dviewer/shaders/gau_frag.glsl +0 -42
  26. q3dviewer-1.1.6/q3dviewer/shaders/gau_prep.glsl +0 -249
  27. q3dviewer-1.1.6/q3dviewer/shaders/gau_vert.glsl +0 -77
  28. q3dviewer-1.1.6/q3dviewer/shaders/sort_by_key.glsl +0 -56
  29. q3dviewer-1.1.6/q3dviewer/tools/example_viewer.py +0 -47
  30. q3dviewer-1.1.6/q3dviewer/utils/convert_ros_msg.py +0 -51
  31. q3dviewer-1.1.6/q3dviewer.egg-info/PKG-INFO +0 -235
  32. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/__init__.py +0 -0
  33. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/base_item.py +0 -0
  34. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/axis_item.py +0 -0
  35. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/frame_item.py +0 -0
  36. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/gaussian_item.py +0 -0
  37. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/grid_item.py +0 -0
  38. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/image_item.py +0 -0
  39. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/custom_items/line_item.py +0 -0
  40. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/tools/__init__.py +0 -0
  41. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/tools/gaussian_viewer.py +0 -0
  42. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/tools/lidar_calib.py +0 -0
  43. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/tools/ros_viewer.py +0 -0
  44. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/utils/__init__.py +0 -0
  45. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/utils/cloud_io.py +0 -0
  46. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/utils/gl_helper.py +0 -0
  47. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/utils/helpers.py +0 -0
  48. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/utils/maths.py +0 -0
  49. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer/utils/range_slider.py +0 -0
  50. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer.egg-info/dependency_links.txt +0 -0
  51. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer.egg-info/requires.txt +6 -6
  52. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/q3dviewer.egg-info/top_level.txt +0 -0
  53. {q3dviewer-1.1.6 → q3dviewer-1.1.9}/setup.cfg +0 -0
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.1
2
+ Name: q3dviewer
3
+ Version: 1.1.9
4
+ Summary: A library designed for quickly deploying a 3D viewer.
5
+ Home-page: https://github.com/scomup/q3dviewer
6
+ Author: Liu Yang
7
+ License: UNKNOWN
8
+ Description:
9
+ ![q3dviewer Logo](imgs/logo.png)
10
+
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+ [![PyPI version](https://badge.fury.io/py/q3dviewer.svg)](https://badge.fury.io/py/q3dviewer)
13
+
14
+ `q3dviewer` is a library designed for quickly deploying a 3D viewer. It is based on Qt 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.
15
+
16
+
17
+ To show how to use `q3dviewer` as a library, we also provide some [very useful tools](#tools).
18
+
19
+
20
+ ## Installation
21
+
22
+ To install `q3dviewer`, execute the following command in your terminal on either Linux or Windows:
23
+
24
+ ```bash
25
+ pip install q3dviewer
26
+ ```
27
+
28
+ ### Note for Windows Users
29
+
30
+ - Ensure that you have a Python 3 environment set up:
31
+ - Download and install Python 3 from the [official Python website](https://www.python.org/downloads/).
32
+ - During installation, make sure to check the "Add Python to PATH" option.
33
+
34
+ ### Note for Linux Users
35
+
36
+ If you encounter an error related to loading the shared library `libxcb-cursor.so.0` on Ubuntu 20.04 or 22.04, please install `libxcb-cursor0`:
37
+
38
+ ```bash
39
+ sudo apt-get install libxcb-cursor0
40
+ ```
41
+
42
+ ## Tools
43
+
44
+ Once installed, you can directly use the following tools:
45
+
46
+ ### 1. Cloud Viewer
47
+
48
+ A tool for visualizing point cloud files (LAS, PCD, PLY, E57). Launch it by executing the following command in your terminal:
49
+
50
+ ```sh
51
+ cloud_viewer
52
+ ```
53
+
54
+ *Alternatively*, if the path is not set (though it's not recommended):
55
+
56
+ ```sh
57
+ python3 -m q3dviewer.tools.cloud_viewer
58
+ ```
59
+
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.
67
+
68
+ For example, you can download and view point clouds of Tokyo in LAS format from the following link:
69
+
70
+ [Tokyo Point Clouds](https://www.geospatial.jp/ckan/dataset/tokyopc-23ku-2024/resource/7807d6d1-29f3-4b36-b0c8-f7aa0ea2cff3)
71
+
72
+ ![Cloud Viewer Screenshot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/03c981c6-1aec-e5b9-4536-e07e1e56ff29.png)
73
+
74
+ Press `M` on your keyboard to display a menu on the screen, where you can modify visualization settings for each item. For example, you can adjust various settings such as shape, size, color, and transparency for `CloudItem`.
75
+
76
+ ![Cloud Viewer Settings](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/deeb996a-e419-58f4-6bc2-535099b1b73a.png)
77
+
78
+ ### 2. ROS Viewer
79
+
80
+ A high-performance SLAM viewer compatible with ROS, serving as an alternative to RVIZ.
81
+
82
+ ```sh
83
+ roscore &
84
+ ros_viewer
85
+ ```
86
+
87
+ ### 3. Film Maker
88
+
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.
90
+
91
+ ```sh
92
+ film_maker
93
+ ```
94
+
95
+ **Basic Operations**
96
+ * File loading & viewpoint movement: Same as Cloud_Viewer
97
+ * Space key to add a keyframe.
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
101
+
102
+ Film Maker GUI:
103
+
104
+ ![film_maker_demo.gif](imgs/film_maker_demo.gif)
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.
107
+
108
+ ### 4. Gaussian Viewer
109
+
110
+ A simple viewer for 3D Gaussians. See [EasyGaussianSplatting](https://github.com/scomup/EasyGaussianSplatting) for more information.
111
+
112
+ ```sh
113
+ gaussian_viewer # Drag and drop your Gaussian file onto the window
114
+ ```
115
+
116
+ ![Gaussian Viewer GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/441e6f5a-214d-f7c1-11bf-5fa79e63b38e.gif)
117
+
118
+ ### 5. LiDAR-LiDAR Calibration Tools
119
+
120
+ A tool to compute the relative pose between two LiDARs. It allows for both manual adjustment in the settings screen and automatic calibration.
121
+
122
+ ```sh
123
+ lidar_calib --lidar0=/YOUR_LIDAR0_TOPIC --lidar1=/YOUR_LIDAR1_TOPIC
124
+ ```
125
+
126
+ ![LiDAR Calibration](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/5a8a9903-a42a-8322-1d23-0cbecd3fa99a.png)
127
+
128
+ ### 6. LiDAR-Camera Calibration Tools
129
+
130
+ A tool for calculating the relative pose between a LiDAR and a camera. It allows for manual adjustment in the settings screen and real-time verification of LiDAR point projection onto images.
131
+
132
+ ```sh
133
+ lidar_cam_calib --lidar=/YOUR_LIDAR_TOPIC --camera=/YOUR_CAMERA_TOPIC --camera_info=/YOUR_CAMERA_INFO_TOPIC
134
+ ```
135
+
136
+ ![LiDAR-Camera Calibration](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149168/f8359820-2ae7-aa37-6577-0fa035f4dd95.png)
137
+
138
+ ## Using as a Library
139
+
140
+ Using the examples above, you can easily customize and develop your own 3D viewer with `q3dviewer`. Below is a coding example.
141
+
142
+ ### Custom 3D Viewer
143
+
144
+ ```python
145
+ #!/usr/bin/env python3
146
+
147
+ import q3dviewer as q3d # Import q3dviewer
148
+
149
+ def main():
150
+ # Create a Qt application
151
+ app = q3d.QApplication([])
152
+
153
+ # Create various 3D items
154
+ axis_item = q3d.AxisItem(size=0.5, width=5)
155
+ grid_item = q3d.GridItem(size=10, spacing=1)
156
+
157
+ # Create a viewer
158
+ viewer = q3d.Viewer(name='example')
159
+
160
+ # Add items to the viewer
161
+ viewer.add_items({
162
+ 'grid': grid_item,
163
+ 'axis': axis_item,
164
+ })
165
+
166
+ # Show the viewer & run the Qt application
167
+ viewer.show()
168
+ app.exec()
169
+
170
+ if __name__ == '__main__':
171
+ main()
172
+ ```
173
+
174
+ `q3dviewer` provides the following 3D items:
175
+
176
+ - **AxisItem**: Displays coordinate axes or the origin position.
177
+ - **CloudItem**: Displays point clouds.
178
+ - **CloudIOItem**: Displays point clouds with input/output capabilities.
179
+ - **GaussianItem**: Displays 3D Gaussians.
180
+ - **GridItem**: Displays grids.
181
+ - **ImageItem**: Displays 2D images.
182
+ - **Text2DItem**: Displays 2D text.
183
+ - **Text3DItem**: Displays 3D test and mark.
184
+ - **LineItem**: Displays lines or trajectories.
185
+
186
+ ### Developing Custom Items
187
+
188
+ In addition to the standard 3D items provided, you can visualize custom 3D items with simple coding. Below is a sample:
189
+
190
+ ```python
191
+ from OpenGL.GL import *
192
+ import numpy as np
193
+ import q3dviewer as q3d
194
+ from q3dviewer.Qt.QtWidgets import QLabel, QSpinBox
195
+
196
+ class YourItem(q3d.BaseItem):
197
+ def __init__(self):
198
+ super(YourItem, self).__init__()
199
+ # Necessary initialization
200
+
201
+ def add_setting(self, layout):
202
+ # Initialize the settings screen
203
+ label = QLabel("Add your setting:")
204
+ layout.addWidget(label)
205
+ box = QSpinBox()
206
+ layout.addWidget(box)
207
+
208
+ def set_data(self, data):
209
+ # Obtain the data you want to visualize
210
+ pass
211
+
212
+ def initialize_gl(self):
213
+ # OpenGL initialization settings (if needed)
214
+ pass
215
+
216
+ def paint(self):
217
+ # Visualize 3D objects using OpenGL
218
+ pass
219
+ ```
220
+
221
+ Enjoy using `q3dviewer`!
222
+ Platform: UNKNOWN
223
+ Classifier: Programming Language :: Python :: 3
224
+ Classifier: License :: OSI Approved :: MIT License
225
+ Classifier: Operating System :: OS Independent
226
+ Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![PyPI version](https://badge.fury.io/py/q3dviewer.svg)](https://badge.fury.io/py/q3dviewer)
6
6
 
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.
7
+ `q3dviewer` is a library designed for quickly deploying a 3D viewer. It is based on Qt 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.
8
8
 
9
9
 
10
10
  To show how to use `q3dviewer` as a library, we also provide some [very useful tools](#tools).
@@ -173,6 +173,7 @@ if __name__ == '__main__':
173
173
  - **GridItem**: Displays grids.
174
174
  - **ImageItem**: Displays 2D images.
175
175
  - **Text2DItem**: Displays 2D text.
176
+ - **Text3DItem**: Displays 3D test and mark.
176
177
  - **LineItem**: Displays lines or trajectories.
177
178
 
178
179
  ### Developing Custom Items
@@ -183,7 +184,7 @@ In addition to the standard 3D items provided, you can visualize custom 3D items
183
184
  from OpenGL.GL import *
184
185
  import numpy as np
185
186
  import q3dviewer as q3d
186
- from PySide6.QtWidgets import QLabel, QSpinBox
187
+ from q3dviewer.Qt.QtWidgets import QLabel, QSpinBox
187
188
 
188
189
  class YourItem(q3d.BaseItem):
189
190
  def __init__(self):
@@ -44,6 +44,7 @@ def import_module(name):
44
44
 
45
45
 
46
46
  def load_qt():
47
+ global Q3D_QT_IMPL, Q3D_DEBUG
47
48
  if Q3D_DEBUG is not None:
48
49
  print(f'Using {Q3D_QT_IMPL} as Qt binding.')
49
50
 
@@ -89,6 +89,9 @@ class BaseGLWidget(QOpenGLWidget):
89
89
  the method is herted from QOpenGLWidget,
90
90
  and it is called when the widget is first shown.
91
91
  """
92
+ glEnable(GL_DEPTH_TEST)
93
+ glDepthFunc(GL_LESS)
94
+
92
95
  for item in self.items:
93
96
  item.initialize()
94
97
  # initialize the projection matrix and model view matrix
@@ -302,7 +305,7 @@ class BaseGLWidget(QOpenGLWidget):
302
305
  near = dist * 0.001
303
306
  far = dist * 10000.
304
307
  r = near * tan(0.5 * radians(self._fov))
305
- t = r * h / w
308
+ t = r * h / max(w, 1)
306
309
  matrix = frustum(-r, r, -t, t, near, far)
307
310
  return matrix
308
311
 
@@ -349,3 +352,82 @@ class BaseGLWidget(QOpenGLWidget):
349
352
  frame = np.frombuffer(pixels, dtype=np.uint8).reshape(height, width, 3)
350
353
  frame = np.flip(frame, 0)
351
354
  return frame
355
+
356
+ def depth_to_meters(self, depth_buffer):
357
+ """
358
+ Convert normalized depth buffer values [0,1] to actual distances in meters.
359
+
360
+ Args:
361
+ depth_buffer: numpy array with depth values in range [0,1]
362
+
363
+ Returns:
364
+ numpy array with distances in meters
365
+ """
366
+ # Get near and far clipping planes
367
+ near = self.dist * 0.001
368
+ far = self.dist * 10000.
369
+
370
+ # Convert from normalized depth [0,1] to linear depth in meters
371
+ # OpenGL depth buffer formula: depth = (1/z - 1/near) / (1/far - 1/near)
372
+ # Solving for z: z = 1 / (depth * (1/far - 1/near) + 1/near)
373
+
374
+ # Avoid division by zero for depth = 1.0 (far plane)
375
+ depth_clamped = np.clip(depth_buffer, 0.0, 0.999999)
376
+
377
+ linear_depth = 1.0 / (depth_clamped * (1.0/far - 1.0/near) + 1.0/near)
378
+
379
+ return linear_depth
380
+
381
+
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
+ """
388
+ self.makeCurrent() # Ensure the OpenGL context is current
389
+ width = self.current_width()
390
+ height = self.current_height()
391
+
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
+ print("points to check:", len(points))
403
+
404
+ gl_y0 = height - y0 - 1
405
+ z = 1.0
406
+ for x, y in points:
407
+ x = int(x * pixel_ratio)
408
+ y = int(y * pixel_ratio)
409
+
410
+ gl_y = height - y - 1
411
+ z = glReadPixels(x, gl_y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
412
+ z = np.frombuffer(z, dtype=np.float32)[0]
413
+ if z != 1.0 and z != 0.0:
414
+ print("dist to p:", np.sqrt((x - x0)**2 + (y - y0)**2))
415
+ break
416
+
417
+ if z == 1.0 or z == 0.0:
418
+ return None
419
+
420
+ # Retrieve OpenGL matrices (column-major), convert to numpy arrays and transpose
421
+ view = np.array(glGetFloatv(GL_MODELVIEW_MATRIX), dtype=np.float32).reshape((4,4)).T
422
+ proj = np.array(glGetFloatv(GL_PROJECTION_MATRIX), dtype=np.float32).reshape((4,4)).T
423
+
424
+ # Convert screen (x, y, z) to normalized device coordinates (NDC)
425
+ ndc_x = (x0 / width) * 2.0 - 1.0
426
+ ndc_y = (gl_y0 / height) * 2.0 - 1.0
427
+ ndc_z = 2.0 * z - 1.0
428
+ ndc = np.array([ndc_x, ndc_y, ndc_z, 1.0], dtype=np.float32)
429
+
430
+ inv_projview = np.linalg.inv(proj @ view)
431
+ world_p = inv_projview @ ndc
432
+ world_p /= world_p[3]
433
+ return world_p[:3]
@@ -7,3 +7,4 @@ from q3dviewer.custom_items.grid_item import GridItem
7
7
  from q3dviewer.custom_items.text_item import Text2DItem
8
8
  from q3dviewer.custom_items.image_item import ImageItem
9
9
  from q3dviewer.custom_items.line_item import LineItem
10
+ from q3dviewer.custom_items.text3d_item import Text3DItem
@@ -11,7 +11,22 @@ from q3dviewer.utils.cloud_io import save_pcd, save_ply, save_e57, save_las, loa
11
11
 
12
12
  class CloudIOItem(CloudItem):
13
13
  """
14
- add save/load function to CloudItem
14
+ A OpenGL point cloud item with input/output capabilities.
15
+ Attributes:
16
+ size (float): The size of each point. When `point_type` is 'PIXEL', this is the size in screen pixels.
17
+ When `point_type` is 'SQUARE' or 'SPHERE', this is the size in centimeters in 3D space.
18
+ alpha (float): The transparency of the points, in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
19
+ color_mode (str): The coloring mode for the points.
20
+ - 'FLAT': Single flat color for all points (uses the `color` attribute).
21
+ - 'I': Color by intensity.
22
+ - 'RGB': Per-point RGB color.
23
+ - 'GRAY': Per-point grayscale color.
24
+ color (str or tuple): The flat color to use when `color_mode` is 'FLAT'. Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
25
+ point_type (str): The type/rendering style of each point:
26
+ - 'PIXEL': Draw each point as a square pixel on the screen.
27
+ - 'SQUARE': Draw each point as a square in 3D space.
28
+ - 'SPHERE': Draw each point as a sphere in 3D space.
29
+ depth_test (bool): Whether to enable depth testing. If True, points closer to the camera will appear in front of farther ones.
15
30
  """
16
31
  def __init__(self, **kwargs):
17
32
  super().__init__(**kwargs)
@@ -11,7 +11,7 @@ from OpenGL.GL import shaders
11
11
 
12
12
  import threading
13
13
  import os
14
- from q3dviewer.Qt.QtWidgets import QLabel, QLineEdit, QDoubleSpinBox, QComboBox, QCheckBox
14
+ from q3dviewer.Qt.QtWidgets import QLabel, QLineEdit, QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox
15
15
  from q3dviewer.utils.range_slider import RangeSlider
16
16
  from q3dviewer.utils import set_uniform
17
17
  from q3dviewer.utils import text_to_rgba
@@ -20,6 +20,25 @@ from q3dviewer.Qt import Q3D_DEBUG
20
20
 
21
21
  # draw points with color (x, y, z, color)
22
22
  class CloudItem(BaseItem):
23
+ """
24
+ A OpenGL point cloud item.
25
+
26
+ Attributes:
27
+ size (float): The size of each point. When `point_type` is 'PIXEL', this is the size in screen pixels.
28
+ When `point_type` is 'SQUARE' or 'SPHERE', this is the size in centimeters in 3D space.
29
+ alpha (float): The transparency of the points, in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
30
+ color_mode (str): The coloring mode for the points.
31
+ - 'FLAT': Single flat color for all points (uses the `color` attribute).
32
+ - 'I': Color by intensity.
33
+ - 'RGB': Per-point RGB color.
34
+ - 'GRAY': Per-point grayscale color.
35
+ color (str or tuple): The flat color to use when `color_mode` is 'FLAT'. Accepts any valid matplotlib color (e.g., 'red', '#FF4500', (1.0, 0.5, 0.0)).
36
+ point_type (str): The type/rendering style of each point:
37
+ - 'PIXEL': Draw each point as a square pixel on the screen.
38
+ - 'SQUARE': Draw each point as a square in 3D space.
39
+ - 'SPHERE': Draw each point as a sphere in 3D space.
40
+ depth_test (bool): Whether to enable depth testing. If True, points closer to the camera will appear in front of farther ones.
41
+ """
23
42
  def __init__(self, size, alpha,
24
43
  color_mode='I',
25
44
  color='white',
@@ -40,7 +59,7 @@ class CloudItem(BaseItem):
40
59
  except ValueError:
41
60
  print(f"Invalid color: {color}, please use matplotlib color format")
42
61
  exit(1)
43
- self.mode_table = {'FLAT': 0, 'I': 1, 'RGB': 2}
62
+ self.mode_table = {'FLAT': 0, 'I': 1, 'RGB': 2, 'GRAY': 3}
44
63
  self.point_type_table = {'PIXEL': 0, 'SQUARE': 1, 'SPHERE': 2}
45
64
  self.color_mode = self.mode_table[color_mode]
46
65
  self.CAPACITY = 10000000 # 10MB * 3 (x,y,z, color) * 4
@@ -65,11 +84,10 @@ class CloudItem(BaseItem):
65
84
  combo_ptype.currentIndexChanged.connect(self._on_point_type_selection)
66
85
  layout.addWidget(combo_ptype)
67
86
 
68
- self.box_size = QDoubleSpinBox()
87
+ self.box_size = QSpinBox()
69
88
  self.box_size.setPrefix("Size: ")
70
89
  self.box_size.setSingleStep(1)
71
- self.box_size.setDecimals(0)
72
- self.box_size.setValue(self.size)
90
+ self.box_size.setValue(int(self.size))
73
91
  self.box_size.setRange(0, 100)
74
92
  self.box_size.valueChanged.connect(self.set_size)
75
93
  self._on_point_type_selection(self.point_type_table[self.point_type])
@@ -89,6 +107,7 @@ class CloudItem(BaseItem):
89
107
  self.combo_color.addItem("flat color")
90
108
  self.combo_color.addItem("intensity")
91
109
  self.combo_color.addItem("RGB")
110
+ self.combo_color.addItem("gray")
92
111
  self.combo_color.setCurrentIndex(self.color_mode)
93
112
  self.combo_color.currentIndexChanged.connect(self._on_color_mode)
94
113
  layout.addWidget(self.combo_color)
@@ -127,10 +146,13 @@ class CloudItem(BaseItem):
127
146
  self.edit_rgb.show()
128
147
  elif (index == self.mode_table['I']): # flat color
129
148
  self.slider_v.show()
149
+ elif (index == self.mode_table['GRAY']): # flat color
150
+ self.slider_v.show()
151
+
130
152
  self.need_update_setting = True
131
153
 
132
154
  def set_color_mode(self, color_mode):
133
- if color_mode in {'FLAT', 'RGB', 'I'}:
155
+ if color_mode in {'FLAT', 'RGB', 'I', 'GRAY'}:
134
156
  try:
135
157
  self.combo_color.setCurrentIndex(self.mode_table[color_mode])
136
158
  except:
@@ -143,17 +165,10 @@ class CloudItem(BaseItem):
143
165
  self.point_type = list(self.point_type_table.keys())[index]
144
166
  if self.point_type == 'PIXEL':
145
167
  self.box_size.setPrefix("Set size (pixel): ")
146
- self.box_size.setDecimals(0)
147
- self.box_size.setSingleStep(1)
148
- self.size = np.ceil(self.size)
149
- self.box_size.setValue(self.size)
150
168
  else:
151
- self.box_size.setPrefix("Set size (meter): ")
152
- self.box_size.setDecimals(2)
153
- self.box_size.setSingleStep(0.01)
154
- if self.size >= 1:
155
- self.size = self.size * 0.01
156
- self.box_size.setValue(self.size)
169
+ self.box_size.setPrefix("Set size (cm): ")
170
+ # self.size = 1
171
+ # self.box_size.setValue(self.size)
157
172
  self.need_update_setting = True
158
173
 
159
174
  def set_alpha(self, alpha):
@@ -222,7 +237,7 @@ class CloudItem(BaseItem):
222
237
  set_uniform(self.program, float(self.vmax), 'vmax')
223
238
  set_uniform(self.program, float(self.vmin), 'vmin')
224
239
  set_uniform(self.program, float(self.alpha), 'alpha')
225
- set_uniform(self.program, float(self.size), 'point_size')
240
+ set_uniform(self.program, int(self.size), 'point_size')
226
241
  set_uniform(self.program, int(self.point_type_table[self.point_type]), 'point_type')
227
242
  glUseProgram(0)
228
243
  self.need_update_setting = False
@@ -292,10 +307,12 @@ class CloudItem(BaseItem):
292
307
  glEnable(GL_BLEND)
293
308
  glEnable(GL_PROGRAM_POINT_SIZE)
294
309
  glEnable(GL_POINT_SPRITE)
295
- if self.depth_test:
296
- glEnable(GL_DEPTH_TEST)
310
+ glEnable(GL_DEPTH_TEST)
311
+
312
+ if not self.depth_test:
313
+ glDepthFunc(GL_ALWAYS) # Always pass depth test but still write depth
297
314
  else:
298
- glDisable(GL_DEPTH_TEST)
315
+ glDepthFunc(GL_LESS) # Normal depth testing
299
316
 
300
317
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
301
318
  glUseProgram(self.program)
@@ -325,5 +342,3 @@ class CloudItem(BaseItem):
325
342
  glDisable(GL_POINT_SPRITE)
326
343
  glDisable(GL_PROGRAM_POINT_SIZE)
327
344
  glDisable(GL_BLEND)
328
- if self.depth_test:
329
- glDisable(GL_DEPTH_TEST) # Disable depth testing if it was enabled
@@ -0,0 +1,152 @@
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
+
7
+ from q3dviewer.base_item import BaseItem
8
+ from OpenGL.GL import *
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.")
23
+
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
+
29
+ # Map requested font_size to a GLUT font object
30
+ if font_size <= 10:
31
+ return GLUT_BITMAP_HELVETICA_10
32
+ elif font_size <= 12:
33
+ return GLUT_BITMAP_HELVETICA_12
34
+ elif font_size <= 18:
35
+ return GLUT_BITMAP_HELVETICA_18
36
+ elif font_size <= 24:
37
+ return GLUT_BITMAP_TIMES_ROMAN_24
38
+ else:
39
+ return GLUT_BITMAP_TIMES_ROMAN_24 # Largest available
40
+
41
+
42
+ # draw points with color (x, y, z, color)
43
+ class Text3DItem(BaseItem):
44
+ """
45
+ A OpenGL 3d text and mark item.
46
+
47
+ Attributes:
48
+ data: list storing text data. Each element is a dict with keys:
49
+ 'text': str, the text to display
50
+ 'position': (x, y, z), the 3D position of the text
51
+ 'color': (r, g, b, a), the color of the text
52
+ 'font_size': float, the font size of the text
53
+ 'point_size': float, size of point to draw at position (0 for no point)
54
+ 'line_width': float, width of line to draw between points (0 for no line)
55
+
56
+ """
57
+ def __init__(self, data=[]):
58
+ super().__init__()
59
+ self._disable_setting = True
60
+ self.data_list = data # map of {'text': str, 'position': (x,y,z), 'color': (r,g,b,a), 'size': float}
61
+
62
+ def add_setting(self, layout):
63
+ pass # No settings for Text3DItem
64
+
65
+ def set_data(self, data, append=False):
66
+ if not append:
67
+ self.data_list = []
68
+ self.data_list.extend(data)
69
+
70
+ def clear_data(self):
71
+ self.data_list = []
72
+
73
+
74
+ def initialize_gl(self):
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
88
+ # super().initialize_gl()
89
+
90
+
91
+ def paint(self):
92
+ for item in self.data_list:
93
+ # Handle both dictionary and string formats
94
+ if isinstance(item, dict):
95
+ text = item.get('text', '')
96
+ pos = item.get('position', (0.0, 0.0, 0.0))
97
+ font_size = item.get('font_size', 24)
98
+ color = item.get('color', (1.0, 1.0, 1.0, 1.0))
99
+ point_size = item.get('point_size', 0.0)
100
+ elif isinstance(item, str):
101
+ # If item is a string, treat it as text with default position and color
102
+ text = item
103
+ pos = (0.0, 0.0, 0.0)
104
+ color = (1.0, 1.0, 1.0, 1.0)
105
+ font_size = 24
106
+ point_size = 0.0
107
+ else:
108
+ print(f"Warning: Unsupported item type: {type(item)}")
109
+ continue
110
+
111
+ # Convert numpy array to tuple if needed
112
+ if isinstance(pos, np.ndarray):
113
+ pos = tuple(pos.astype(float))
114
+
115
+ glColor4f(*color)
116
+
117
+ if point_size > 0.0:
118
+ # draw a point at the position
119
+ glPointSize(point_size)
120
+ glBegin(GL_POINTS)
121
+ glVertex3f(*pos)
122
+ glEnd()
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}")
136
+
137
+ # draw lines between points
138
+ for i in range(len(self.data_list) - 1):
139
+ item1 = self.data_list[i]
140
+ item2 = self.data_list[i + 1]
141
+ if isinstance(item1, dict) and isinstance(item2, dict):
142
+ pos1 = item1.get('position', (0.0, 0.0, 0.0))
143
+ pos2 = item2.get('position', (0.0, 0.0, 0.0))
144
+ line_width = item1.get('line_width', 0.0)
145
+ color = item1.get('color', (1.0, 1.0, 1.0, 1.0))
146
+ if line_width > 0.0:
147
+ glColor4f(*color)
148
+ glLineWidth(line_width)
149
+ glBegin(GL_LINES)
150
+ glVertex3f(*pos1)
151
+ glVertex3f(*pos2)
152
+ glEnd()