0.1.15
Loading...
Searching...
No Matches
LiDARCloud.py
Go to the documentation of this file.
1"""
2LiDARCloud - High-level interface for LiDAR simulation and point cloud processing
3
4Provides Python interface to Helios LiDAR plugin for:
5- Synthetic LiDAR scanning
6- Point cloud management and filtering
7- Triangulation and mesh generation
8- Leaf area density calculations
9"""
10
11from typing import List, Tuple, Optional, Union
12from .wrappers import ULiDARWrapper as lidar_wrapper
13from .Context import Context
14from .plugins.registry import get_plugin_registry
15from .exceptions import HeliosError
16from .wrappers.DataTypes import vec3, RGBcolor, SphericalCoord
17from .validation.datatypes import validate_vec3
18from .validation.core import validate_positive_value
19
20
22 """Exception raised for LiDAR-specific errors"""
23 pass
24
25
26class LiDARCloud:
27 """
28 High-level interface for LiDAR point cloud operations.
29
30 Supports synthetic scanning, point cloud filtering, triangulation,
31 and leaf area density calculations.
32
33 Example:
34 >>> from pyhelios import LiDARCloud
35 >>> from pyhelios.types import vec3
36 >>>
37 >>> with LiDARCloud() as lidar:
38 ... # Add a scan
39 ... scan_id = lidar.addScan(
40 ... origin=vec3(0, 0, 1),
41 ... Ntheta=100, theta_range=(0, 1.57),
42 ... Nphi=100, phi_range=(-3.14, 3.14),
43 ... exit_diameter=0.01, beam_divergence=0.001
44 ... )
45 ...
46 ... # Add hit points
47 ... lidar.addHitPoint(scan_id, vec3(1, 0, 0), vec3(1, 0, 0))
48 ...
49 ... # Export point cloud
50 ... lidar.exportPointCloud("output.xyz")
51 """
52
53 def __init__(self):
54 """
55 Initialize LiDARCloud.
56
57 Raises:
58 LiDARError: If plugin not available in current build
59 RuntimeError: If cloud initialization fails
60 """
61 # Check plugin availability
62 registry = get_plugin_registry()
63 if not registry.is_plugin_available('lidar'):
64 raise LiDARError(
65 "LiDAR plugin not available. Rebuild PyHelios with LiDAR:\n"
66 " build_scripts/build_helios --plugins lidar\n"
67 "\n"
68 "System requirements:\n"
69 " - Platforms: Windows, Linux, macOS\n"
70 " - GPU: Optional (enables GPU acceleration)"
71 )
72
73 self._cloud_ptr = lidar_wrapper.createLiDARcloud()
74 if not self._cloud_ptr:
75 raise LiDARError("Failed to create LiDAR cloud")
76
77 def __enter__(self):
78 """Context manager entry"""
79 return self
80
81 def __exit__(self, exc_type, exc_val, exc_tb):
82 """Context manager exit - cleanup resources"""
83 if hasattr(self, '_cloud_ptr') and self._cloud_ptr:
84 lidar_wrapper.destroyLiDARcloud(self._cloud_ptr)
85 self._cloud_ptr = None
86
87 def __del__(self):
88 """Fallback destructor for cleanup without context manager"""
89 if hasattr(self, '_cloud_ptr') and self._cloud_ptr is not None:
90 try:
91 lidar_wrapper.destroyLiDARcloud(self._cloud_ptr)
92 self._cloud_ptr = None
93 except Exception as e:
94 import warnings
95 warnings.warn(f"Error in LiDARCloud.__del__: {e}")
96
97 def addScan(self, origin: Union[vec3, List[float], Tuple[float, float, float]],
98 Ntheta: int, theta_range: Tuple[float, float],
99 Nphi: int, phi_range: Tuple[float, float],
100 exit_diameter: float, beam_divergence: float) -> int:
101 """
102 Add a LiDAR scan to the point cloud.
103
104 Args:
105 origin: Scanner position (vec3 or 3-element list/tuple)
106 Ntheta: Number of scan points in zenith direction
107 theta_range: Zenith angle range (min, max) in radians
108 Nphi: Number of scan points in azimuthal direction
109 phi_range: Azimuthal angle range (min, max) in radians
110 exit_diameter: Laser beam exit diameter (meters)
111 beam_divergence: Beam divergence angle (radians)
112
113 Returns:
114 Scan ID for referencing this scan
115
116 Example:
117 >>> scan_id = lidar.addScan(
118 ... origin=vec3(0, 0, 1),
119 ... Ntheta=100, theta_range=(0, 1.57),
120 ... Nphi=100, phi_range=(-3.14, 3.14),
121 ... exit_diameter=0.01, beam_divergence=0.001
122 ... )
123 """
124 # Convert origin to vec3 if needed
125 if isinstance(origin, (list, tuple)):
126 if len(origin) != 3:
127 raise ValueError("Origin must have 3 elements [x, y, z]")
128 origin = vec3(*origin)
129 elif not hasattr(origin, 'x'):
130 raise ValueError("Origin must be vec3 or 3-element list/tuple")
131
132 origin_list = [origin.x, origin.y, origin.z]
133
134 # Validate scan parameters
135 validate_positive_value(Ntheta, 'Ntheta', 'addScan')
136 validate_positive_value(Nphi, 'Nphi', 'addScan')
137
138 if not isinstance(theta_range, (list, tuple)) or len(theta_range) != 2:
139 raise ValueError("theta_range must be a tuple (min, max)")
140 if not isinstance(phi_range, (list, tuple)) or len(phi_range) != 2:
141 raise ValueError("phi_range must be a tuple (min, max)")
142
143 return lidar_wrapper.addLiDARScan(
144 self._cloud_ptr, origin_list, Ntheta, theta_range,
145 Nphi, phi_range, exit_diameter, beam_divergence
146 )
147
148 def getScanCount(self) -> int:
149 """Get total number of scans in the cloud"""
150 return lidar_wrapper.getLiDARScanCount(self._cloud_ptr)
151
152 def getScanOrigin(self, scanID: int) -> vec3:
153 """Get origin of a specific scan"""
154 if scanID < 0:
155 raise ValueError("Scan ID must be non-negative")
156 origin_list = lidar_wrapper.getLiDARScanOrigin(self._cloud_ptr, scanID)
157 return vec3(*origin_list)
158
159 def getScanSizeTheta(self, scanID: int) -> int:
160 """Get number of zenith scan points for a scan"""
161 if scanID < 0:
162 raise ValueError("Scan ID must be non-negative")
163 return lidar_wrapper.getLiDARScanSizeTheta(self._cloud_ptr, scanID)
164
165 def getScanSizePhi(self, scanID: int) -> int:
166 """Get number of azimuthal scan points for a scan"""
167 if scanID < 0:
168 raise ValueError("Scan ID must be non-negative")
169 return lidar_wrapper.getLiDARScanSizePhi(self._cloud_ptr, scanID)
170
171 def addHitPoint(self, scanID: int,
172 xyz: Union[vec3, List[float], Tuple[float, float, float]],
173 direction: Union[vec3, SphericalCoord, List[float], Tuple[float, float]],
174 color: Optional[Union[RGBcolor, List[float], Tuple[float, float, float]]] = None):
175 """
176 Add a hit point to the point cloud.
177
178 Args:
179 scanID: Scan ID this hit belongs to
180 xyz: Hit point coordinates (vec3 or 3-element list)
181 direction: Ray direction (vec3/SphericalCoord or 2-3 element list)
182 color: Optional RGB color (RGBcolor or 3-element list)
183 """
184 # Convert xyz to list
185 if isinstance(xyz, (list, tuple)):
186 if len(xyz) != 3:
187 raise ValueError("XYZ must have 3 elements")
188 xyz_list = list(xyz)
189 elif hasattr(xyz, 'x'):
190 xyz_list = [xyz.x, xyz.y, xyz.z]
191 else:
192 raise ValueError("XYZ must be vec3 or 3-element list/tuple")
193
194 # Convert direction to list
195 if isinstance(direction, (list, tuple)):
196 if len(direction) < 2:
197 raise ValueError("Direction must have at least 2 elements [radius, elevation]")
198 direction_list = list(direction)
199 elif hasattr(direction, 'radius'): # SphericalCoord
200 direction_list = [direction.radius, direction.elevation, direction.azimuth]
201 elif hasattr(direction, 'x'): # vec3
202 direction_list = [direction.x, direction.y, direction.z]
203 else:
204 raise ValueError("Direction must be vec3/SphericalCoord or 2-3 element list")
205
206 # Add with or without color
207 if color is not None:
208 if isinstance(color, (list, tuple)):
209 if len(color) != 3:
210 raise ValueError("Color must have 3 elements [r, g, b]")
211 color_list = list(color)
212 elif hasattr(color, 'r'):
213 color_list = [color.r, color.g, color.b]
214 else:
215 raise ValueError("Color must be RGBcolor or 3-element list")
216
217 lidar_wrapper.addLiDARHitPointRGB(self._cloud_ptr, scanID, xyz_list, direction_list, color_list)
218 else:
219 lidar_wrapper.addLiDARHitPoint(self._cloud_ptr, scanID, xyz_list, direction_list)
220
221 def getHitCount(self) -> int:
222 """Get total number of hit points in cloud"""
223 return lidar_wrapper.getLiDARHitCount(self._cloud_ptr)
224
225 def getHitXYZ(self, index: int) -> vec3:
226 """Get coordinates of a hit point"""
227 if index < 0:
228 raise ValueError("Index must be non-negative")
229 xyz_list = lidar_wrapper.getLiDARHitXYZ(self._cloud_ptr, index)
230 return vec3(*xyz_list)
231
232 def getHitRaydir(self, index: int) -> SphericalCoord:
233 """Get ray direction of a hit point"""
234 if index < 0:
235 raise ValueError("Index must be non-negative")
236 direction_list = lidar_wrapper.getLiDARHitRaydir(self._cloud_ptr, index)
237 return SphericalCoord(direction_list[0], direction_list[1])
238
239 def getHitColor(self, index: int) -> RGBcolor:
240 """Get color of a hit point"""
241 if index < 0:
242 raise ValueError("Index must be non-negative")
243 color_list = lidar_wrapper.getLiDARHitColor(self._cloud_ptr, index)
244 return RGBcolor(*color_list)
245
246 def deleteHitPoint(self, index: int):
247 """Delete a hit point from the cloud"""
248 if index < 0:
249 raise ValueError("Index must be non-negative")
250 lidar_wrapper.deleteLiDARHitPoint(self._cloud_ptr, index)
251
252 def coordinateShift(self, shift: Union[vec3, List[float], Tuple[float, float, float]]):
253 """
254 Translate all hit points by a shift vector.
255
256 Args:
257 shift: Translation vector (vec3 or 3-element list)
258 """
259 if isinstance(shift, (list, tuple)):
260 if len(shift) != 3:
261 raise ValueError("Shift must have 3 elements [x, y, z]")
262 shift_list = list(shift)
263 elif hasattr(shift, 'x'):
264 shift_list = [shift.x, shift.y, shift.z]
265 else:
266 raise ValueError("Shift must be vec3 or 3-element list/tuple")
267
268 lidar_wrapper.lidarCoordinateShift(self._cloud_ptr, shift_list)
269
270 def coordinateRotation(self, rotation: Union[SphericalCoord, List[float], Tuple[float, float]]):
271 """
272 Rotate all hit points by spherical rotation angles.
273
274 Args:
275 rotation: Rotation angles (SphericalCoord or 2-3 element list)
276 """
277 if isinstance(rotation, (list, tuple)):
278 if len(rotation) < 2:
279 raise ValueError("Rotation must have at least 2 elements [radius, elevation]")
280 rotation_list = list(rotation)
281 elif hasattr(rotation, 'radius'):
282 rotation_list = [rotation.radius, rotation.elevation, rotation.azimuth]
283 else:
284 raise ValueError("Rotation must be SphericalCoord or 2-3 element list")
285
286 lidar_wrapper.lidarCoordinateRotation(self._cloud_ptr, rotation_list)
287
288 def triangulateHitPoints(self, Lmax: float, max_aspect_ratio: float = 4.0):
289 """
290 Generate triangle mesh from hit points using Delaunay triangulation.
291
292 Args:
293 Lmax: Maximum triangle edge length
294 max_aspect_ratio: Maximum triangle aspect ratio (default 4.0)
295 """
296 validate_positive_value(Lmax, 'Lmax', 'triangulateHitPoints')
297 validate_positive_value(max_aspect_ratio, 'max_aspect_ratio', 'triangulateHitPoints')
298 lidar_wrapper.lidarTriangulateHitPoints(self._cloud_ptr, Lmax, max_aspect_ratio)
299
300 def getTriangleCount(self) -> int:
301 """Get number of triangles in the mesh"""
302 return lidar_wrapper.getLiDARTriangleCount(self._cloud_ptr)
303
304 def distanceFilter(self, maxdistance: float):
305 """Filter hit points by maximum distance from scanner"""
306 validate_positive_value(maxdistance, 'maxdistance', 'distanceFilter')
307 lidar_wrapper.lidarDistanceFilter(self._cloud_ptr, maxdistance)
308
309 def reflectanceFilter(self, minreflectance: float):
310 """Filter hit points by minimum reflectance value"""
311 lidar_wrapper.lidarReflectanceFilter(self._cloud_ptr, minreflectance)
312
313 def firstHitFilter(self):
314 """Keep only first return hit points"""
315 lidar_wrapper.lidarFirstHitFilter(self._cloud_ptr)
316
317 def lastHitFilter(self):
318 """Keep only last return hit points"""
319 lidar_wrapper.lidarLastHitFilter(self._cloud_ptr)
320
321 def exportPointCloud(self, filename: str):
322 """Export point cloud to ASCII file"""
323 if not filename:
324 raise ValueError("Filename cannot be empty")
325 lidar_wrapper.exportLiDARPointCloud(self._cloud_ptr, filename)
326
327 def loadXML(self, filename: str):
328 """Load scan metadata from XML file"""
329 if not filename:
330 raise ValueError("Filename cannot be empty")
331 lidar_wrapper.loadLiDARXML(self._cloud_ptr, filename)
332
333 def disableMessages(self):
334 """Disable console output messages"""
335 lidar_wrapper.lidarDisableMessages(self._cloud_ptr)
336
337 def enableMessages(self):
338 """Enable console output messages"""
339 lidar_wrapper.lidarEnableMessages(self._cloud_ptr)
340
341 def addGrid(self, center: Union[vec3, List[float], Tuple[float, float, float]],
342 size: Union[vec3, List[float], Tuple[float, float, float]],
343 ndiv: Union[List[int], Tuple[int, int, int]],
344 rotation: float = 0.0):
345 """
346 Add a rectangular grid of voxel cells.
347
348 Args:
349 center: Grid center position (vec3 or 3-element list)
350 size: Grid dimensions [x, y, z] (vec3 or 3-element list)
351 ndiv: Number of divisions [nx, ny, nz] (3-element list)
352 rotation: Azimuthal rotation angle (radians, default 0.0)
353
354 Example:
355 >>> lidar.addGrid(
356 ... center=vec3(0, 0, 0.5),
357 ... size=vec3(10, 10, 1),
358 ... ndiv=[10, 10, 5],
359 ... rotation=0.0
360 ... )
361 """
362 # Convert center to list
363 if isinstance(center, (list, tuple)):
364 if len(center) != 3:
365 raise ValueError("Center must have 3 elements [x, y, z]")
366 center_list = list(center)
367 elif hasattr(center, 'x'):
368 center_list = [center.x, center.y, center.z]
369 else:
370 raise ValueError("Center must be vec3 or 3-element list/tuple")
371
372 # Convert size to list
373 if isinstance(size, (list, tuple)):
374 if len(size) != 3:
375 raise ValueError("Size must have 3 elements [x, y, z]")
376 size_list = list(size)
377 elif hasattr(size, 'x'):
378 size_list = [size.x, size.y, size.z]
379 else:
380 raise ValueError("Size must be vec3 or 3-element list/tuple")
381
382 # Validate ndiv
383 if not isinstance(ndiv, (list, tuple)) or len(ndiv) != 3:
384 raise ValueError("Ndiv must be a 3-element list [nx, ny, nz]")
385
386 lidar_wrapper.addLiDARGrid(self._cloud_ptr, center_list, size_list, list(ndiv), rotation)
387
388 def addGridCell(self, center: Union[vec3, List[float], Tuple[float, float, float]],
389 size: Union[vec3, List[float], Tuple[float, float, float]],
390 rotation: float = 0.0):
391 """
392 Add a single grid cell.
393
394 Args:
395 center: Cell center position (vec3 or 3-element list)
396 size: Cell dimensions [x, y, z] (vec3 or 3-element list)
397 rotation: Azimuthal rotation angle (radians, default 0.0)
398 """
399 # Convert center to list
400 if isinstance(center, (list, tuple)):
401 if len(center) != 3:
402 raise ValueError("Center must have 3 elements [x, y, z]")
403 center_list = list(center)
404 elif hasattr(center, 'x'):
405 center_list = [center.x, center.y, center.z]
406 else:
407 raise ValueError("Center must be vec3 or 3-element list/tuple")
408
409 # Convert size to list
410 if isinstance(size, (list, tuple)):
411 if len(size) != 3:
412 raise ValueError("Size must have 3 elements [x, y, z]")
413 size_list = list(size)
414 elif hasattr(size, 'x'):
415 size_list = [size.x, size.y, size.z]
416 else:
417 raise ValueError("Size must be vec3 or 3-element list/tuple")
418
419 lidar_wrapper.addLiDARGridCell(self._cloud_ptr, center_list, size_list, rotation)
420
421 def getGridCellCount(self) -> int:
422 """Get total number of grid cells"""
423 return lidar_wrapper.getLiDARGridCellCount(self._cloud_ptr)
424
425 def getCellCenter(self, index: int) -> vec3:
426 """Get center position of a grid cell"""
427 if index < 0:
428 raise ValueError("Index must be non-negative")
429 center_list = lidar_wrapper.getLiDARCellCenter(self._cloud_ptr, index)
430 return vec3(*center_list)
431
432 def getCellSize(self, index: int) -> vec3:
433 """Get size of a grid cell"""
434 if index < 0:
435 raise ValueError("Index must be non-negative")
436 size_list = lidar_wrapper.getLiDARCellSize(self._cloud_ptr, index)
437 return vec3(*size_list)
438
439 def getCellLeafArea(self, index: int) -> float:
440 """Get leaf area of a grid cell (m²)"""
441 if index < 0:
442 raise ValueError("Index must be non-negative")
443 return lidar_wrapper.getLiDARCellLeafArea(self._cloud_ptr, index)
444
445 def getCellLeafAreaDensity(self, index: int) -> float:
446 """Get leaf area density of a grid cell (m²/m³)"""
447 if index < 0:
448 raise ValueError("Index must be non-negative")
449 return lidar_wrapper.getLiDARCellLeafAreaDensity(self._cloud_ptr, index)
450
451 def getCellGtheta(self, index: int) -> float:
452 """Get G(theta) value for a grid cell"""
453 if index < 0:
454 raise ValueError("Index must be non-negative")
455 return lidar_wrapper.getLiDARCellGtheta(self._cloud_ptr, index)
456
457 def setCellGtheta(self, Gtheta: float, index: int):
458 """Set G(theta) value for a grid cell"""
459 if index < 0:
460 raise ValueError("Index must be non-negative")
461 lidar_wrapper.setLiDARCellGtheta(self._cloud_ptr, Gtheta, index)
462
463 def calculateHitGridCell(self):
464 """Calculate hit point grid cell assignments"""
465 lidar_wrapper.calculateLiDARHitGridCell(self._cloud_ptr)
466
467 def gapfillMisses(self):
468 """
469 Gapfill sky/miss points where rays didn't hit geometry.
470
471 Important for accurate leaf area calculations with real LiDAR data.
472 Should be called before triangulation when processing real data.
473 """
474 lidar_wrapper.gapfillLiDARMisses(self._cloud_ptr)
475
476 def syntheticScan(self, context: Context,
477 rays_per_pulse: Optional[int] = None,
478 pulse_distance_threshold: Optional[float] = None,
479 scan_grid_only: bool = False,
480 record_misses: bool = True,
481 append: bool = False):
482 """
483 Perform synthetic LiDAR scan of geometry in Context.
484
485 Requires scan metadata to be defined first via addScan() or loadXML().
486 Uses ray tracing to simulate LiDAR instrument measurements.
487
488 Args:
489 context: Helios Context containing geometry to scan
490 rays_per_pulse: Number of rays per pulse (None=discrete-return, typical: 100)
491 pulse_distance_threshold: Distance threshold for aggregating hits (meters, required for waveform)
492 scan_grid_only: If True, only scan within defined grid cells
493 record_misses: If True, record miss/sky points where rays don't hit geometry
494 append: If True, append to existing hits; if False, clear existing hits
495
496 Example (Discrete-return):
497 >>> from pyhelios import Context, LiDARCloud
498 >>> from pyhelios.types import vec3
499 >>> with Context() as context:
500 ... # Add geometry
501 ... context.addPatch(center=vec3(0, 0, 0.5), size=vec2(1, 1))
502 ...
503 ... with LiDARCloud() as lidar:
504 ... # Define scan parameters
505 ... scan_id = lidar.addScan(
506 ... origin=vec3(0, 0, 2),
507 ... Ntheta=100, theta_range=(0, 1.57),
508 ... Nphi=100, phi_range=(0, 6.28),
509 ... exit_diameter=0, beam_divergence=0
510 ... )
511 ...
512 ... # Perform discrete-return scan
513 ... lidar.syntheticScan(context)
514
515 Example (Full-waveform):
516 >>> lidar.syntheticScan(
517 ... context,
518 ... rays_per_pulse=100,
519 ... pulse_distance_threshold=0.02,
520 ... record_misses=True
521 ... )
522 """
523 if not isinstance(context, Context):
524 raise TypeError("context must be a Context instance")
525
526 context_ptr = context.getNativePtr()
527
528 # Discrete-return mode (single ray per pulse)
529 if rays_per_pulse is None:
530 # Use append-aware version to ensure explicit control
531 lidar_wrapper.syntheticLiDARScanAppend(self._cloud_ptr, context_ptr, append)
532 else:
533 # Full-waveform mode (multiple rays per pulse)
534 if pulse_distance_threshold is None:
535 raise ValueError("pulse_distance_threshold required for full-waveform scanning")
536
537 validate_positive_value(rays_per_pulse, 'rays_per_pulse', 'syntheticScan')
538 validate_positive_value(pulse_distance_threshold, 'pulse_distance_threshold', 'syntheticScan')
539
540 lidar_wrapper.syntheticLiDARScanFull(
541 self._cloud_ptr, context_ptr,
542 rays_per_pulse, pulse_distance_threshold,
543 scan_grid_only, record_misses, append
544 )
545
546 def calculateLeafArea(self, context: Context, min_voxel_hits: Optional[int] = None):
547 """
548 Calculate leaf area for each grid cell.
549
550 Requires triangulation to have been performed first.
551
552 Args:
553 context: Helios Context instance
554 min_voxel_hits: Optional minimum number of hits required per voxel
555
556 Example:
557 >>> from pyhelios import Context, LiDARCloud
558 >>> with Context() as context:
559 ... with LiDARCloud() as lidar:
560 ... # ... load data, add grid, triangulate ...
561 ... lidar.calculateLeafArea(context)
562 """
563 if not isinstance(context, Context):
564 raise TypeError("context must be a Context instance")
565
566 context_ptr = context.getNativePtr()
567 if min_voxel_hits is None:
568 lidar_wrapper.calculateLiDARLeafArea(self._cloud_ptr, context_ptr)
569 else:
570 lidar_wrapper.calculateLiDARLeafAreaMinHits(self._cloud_ptr, context_ptr, min_voxel_hits)
571
572 def calculateSyntheticLeafArea(self, context: Context):
573 """
574 Calculate synthetic leaf area (for validation of synthetic scans).
575
576 Uses exact primitive geometry to calculate leaf area, useful for
577 validating synthetic scan accuracy.
578
579 Args:
580 context: Helios Context instance containing primitive geometry
581 """
582 if not isinstance(context, Context):
583 raise TypeError("context must be a Context instance")
584 context_ptr = context.getNativePtr()
585 lidar_wrapper.calculateSyntheticLiDARLeafArea(self._cloud_ptr, context_ptr)
586
587 def calculateSyntheticGtheta(self, context: Context):
588 """
589 Calculate synthetic G(theta) (for validation of synthetic scans).
590
591 Uses exact primitive geometry to calculate G(theta), useful for
592 validating synthetic scan accuracy.
593
594 Args:
595 context: Helios Context instance containing primitive geometry
596 """
597 if not isinstance(context, Context):
598 raise TypeError("context must be a Context instance")
599 context_ptr = context.getNativePtr()
600 lidar_wrapper.calculateSyntheticLiDARGtheta(self._cloud_ptr, context_ptr)
601
602 def exportTriangleNormals(self, filename: str):
603 """Export triangle normal vectors to file"""
604 if not filename:
605 raise ValueError("Filename cannot be empty")
606 lidar_wrapper.exportLiDARTriangleNormals(self._cloud_ptr, filename)
607
608 def exportTriangleAreas(self, filename: str):
609 """Export triangle areas to file"""
610 if not filename:
611 raise ValueError("Filename cannot be empty")
612 lidar_wrapper.exportLiDARTriangleAreas(self._cloud_ptr, filename)
613
614 def exportLeafAreas(self, filename: str):
615 """Export leaf areas for each grid cell to file"""
616 if not filename:
617 raise ValueError("Filename cannot be empty")
618 lidar_wrapper.exportLiDARLeafAreas(self._cloud_ptr, filename)
619
620 def exportLeafAreaDensities(self, filename: str):
621 """Export leaf area densities for each grid cell to file"""
622 if not filename:
623 raise ValueError("Filename cannot be empty")
624 lidar_wrapper.exportLiDARLeafAreaDensities(self._cloud_ptr, filename)
625
626 def exportGtheta(self, filename: str):
627 """Export G(theta) values for each grid cell to file"""
628 if not filename:
629 raise ValueError("Filename cannot be empty")
630 lidar_wrapper.exportLiDARGtheta(self._cloud_ptr, filename)
631
632 def addTrianglesToContext(self, context: Context):
633 """
634 Add triangulated mesh to Context as triangle primitives.
635
636 Converts the triangulated point cloud mesh into Context triangle
637 primitives that can be used for further analysis or visualization.
638
639 Args:
640 context: Helios Context instance
641
642 Example:
643 >>> with Context() as context:
644 ... with LiDARCloud() as lidar:
645 ... lidar.loadXML("scan.xml")
646 ... lidar.triangulateHitPoints(Lmax=0.5, max_aspect_ratio=5)
647 ... lidar.addTrianglesToContext(context)
648 ... print(f"Added {context.getPrimitiveCount()} triangles to context")
649 """
650 if not isinstance(context, Context):
651 raise TypeError("context must be a Context instance")
652 lidar_wrapper.addLiDARTrianglesToContext(self._cloud_ptr, context.getNativePtr())
653
654 def initializeCollisionDetection(self, context: Context):
655 """
656 Initialize CollisionDetection plugin for ray tracing.
657
658 Required before performing synthetic scans.
659
660 Args:
661 context: Helios Context instance containing geometry
662 """
663 if not isinstance(context, Context):
664 raise TypeError("context must be a Context instance")
665 lidar_wrapper.initializeLiDARCollisionDetection(self._cloud_ptr, context.getNativePtr())
666
667 def enableCDGPUAcceleration(self):
668 """Enable GPU acceleration for collision detection ray tracing"""
669 lidar_wrapper.enableLiDARCDGPUAcceleration(self._cloud_ptr)
670
671 def disableCDGPUAcceleration(self):
672 """Disable GPU acceleration (use CPU ray tracing)"""
673 lidar_wrapper.disableLiDARCDGPUAcceleration(self._cloud_ptr)
674
675 def is_available(self) -> bool:
676 """
677 Check if LiDAR is available in current build.
678
679 Returns:
680 True if plugin is available, False otherwise
681 """
682 registry = get_plugin_registry()
683 return registry.is_plugin_available('lidar')
684
685
686# Convenience function
687def create_lidar_cloud() -> LiDARCloud:
688 """
689 Create LiDARCloud instance.
690
691 Returns:
692 LiDARCloud instance
693 """
694 return LiDARCloud()
High-level interface for LiDAR point cloud operations.
Definition LiDARCloud.py:51
__init__(self)
Initialize LiDARCloud.
Definition LiDARCloud.py:60
__enter__(self)
Context manager entry.
Definition LiDARCloud.py:78
__exit__(self, exc_type, exc_val, exc_tb)
Context manager exit - cleanup resources.
Definition LiDARCloud.py:82
__del__(self)
Fallback destructor for cleanup without context manage.
Definition LiDARCloud.py:88
Exception raised for LiDAR-specific errors.
Definition LiDARCloud.py:22
Exception classes for PyHelios library.
Definition exceptions.py:10