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 CHANGED
@@ -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
 
@@ -379,24 +379,48 @@ class BaseGLWidget(QOpenGLWidget):
379
379
  return linear_depth
380
380
 
381
381
 
382
- def get_point(self, x, y):
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
- gl_y = height - y - 1
388
- z = glReadPixels(x, gl_y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT)
389
- z = np.frombuffer(z, dtype=np.float32)[0]
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 = (x / width) * 2.0 - 1.0
399
- ndc_y = (gl_y / height) * 2.0 - 1.0
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 not self.depth_test:
313
- glDepthFunc(GL_ALWAYS) # Always pass depth test but still write depth
302
+ if self.alpha < 0.9:
303
+ glDepthFunc(GL_ALWAYS)
314
304
  else:
315
- glDepthFunc(GL_LESS) # Normal depth testing
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
- from OpenGL.GLUT import glutBitmapCharacter, glutInit
11
- from OpenGL.GLUT import (
12
- GLUT_BITMAP_HELVETICA_10,
13
- GLUT_BITMAP_HELVETICA_12,
14
- GLUT_BITMAP_HELVETICA_18,
15
- # GLUT_BITMAP_TIMES_ROMAN_10,
16
- GLUT_BITMAP_TIMES_ROMAN_24,
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
- glutInit()
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
- offset = 0.02
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
- font = get_glut_font(font_size)
102
- for ch in text:
103
- glutBitmapCharacter(font, ord(ch))
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):
@@ -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
- self.minimum(), min(pos, self.upper_value - 1))
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
- self.maximum(), max(pos, self.lower_value + 1))
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.1.8
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
  ![q3dviewer Logo](imgs/logo.png)
@@ -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=0AWYwOntyCSterxOebTg_VwStPYV6k1H_BDRGHl41bo,14382
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/.vscode/c_cpp_properties.json,sha256=rcEXwMKO_Fve86bpEeEhoWRKzdLSVuG5dbNCtE5U3Fw,1037
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=NkZ5rFM_hVxBPMZYml3oX2CI5d0ApJY1wet8I1eIssE,14024
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=mQX-8ldUsPcM3ef77fevsXqVJY0_lnNsxCGrtpF4b-A,4167
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=9djTxuzmzH54DgSwAljRpLGjsrIJ0hTxhaxFjPxsk8g,4007
45
- q3dviewer-1.1.8.dist-info/LICENSE,sha256=81cMOyNfw8KLb1JnPYngGHJ5W83gSbZEBU9MEP3tl-E,1124
46
- q3dviewer-1.1.8.dist-info/METADATA,sha256=oAsXecjioM4FjtdSDR-8nOjte6sAMxyA0nAklwItxsQ,8014
47
- q3dviewer-1.1.8.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
48
- q3dviewer-1.1.8.dist-info/entry_points.txt,sha256=aeUdGH7UIgMZEMFUc-0xPZWspY95GoPdZcZuLceq85g,361
49
- q3dviewer-1.1.8.dist-info/top_level.txt,sha256=HFFDCbGu28txcGe2HPc46A7EPaguBa_b5oH7bufmxHM,10
50
- q3dviewer-1.1.8.dist-info/RECORD,,
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.34.2)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -6,3 +6,4 @@ lidar_calib = q3dviewer.tools.lidar_calib:main
6
6
  lidar_cam_calib = q3dviewer.tools.lidar_cam_calib:main
7
7
  mesh_viewer = q3dviewer.tools.mesh_viewer:main
8
8
  ros_viewer = q3dviewer.tools.ros_viewer:main
9
+
@@ -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
- }
@@ -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
-
@@ -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()