0.1.39
Loading...
Searching...
No Matches
Visualizer.py
Go to the documentation of this file.
1"""
2High-level Visualizer interface for PyHelios.
3
4This module provides a user-friendly interface to the 3D visualization
5capabilities with graceful plugin handling and informative error messages.
6"""
7
8import logging
9import os
10import ctypes
11from pathlib import Path
12from contextlib import contextmanager
13from typing import List, Optional, Union, Tuple
14
15from .plugins.registry import get_plugin_registry
16from .plugins import helios_lib
17from .wrappers import UVisualizerWrapper as visualizer_wrapper
18from .wrappers.DataTypes import vec2, vec3, RGBcolor, SphericalCoord
19from .Context import Context, check_context_alive
20from .validation.plugin_decorators import validate_build_geometry_params, validate_print_window_params
21from .assets import get_asset_manager
22
23logger = logging.getLogger(__name__)
24
25# Type references for type checking (avoids doxygen parsing issues)
26_INT_TYPE = int
27_NUMERIC_TYPES = (int, float)
28
30def _resolve_user_path(path: str) -> str:
31 """
32 Resolve a user-provided path to an absolute path before working directory changes.
34 This ensures that user file paths are interpreted relative to their original
35 working directory, not the temporary working directory used for asset discovery.
36
37 Args:
38 path: User-provided file path (absolute or relative)
39
40 Returns:
41 Absolute path resolved from the user's original working directory
42 """
43 from pathlib import Path
44
45 path_obj = Path(path)
46 if path_obj.is_absolute():
47 return str(path_obj)
48 else:
49 # Resolve relative to the user's current working directory
50 return str(Path.cwd().resolve() / path_obj)
51
52@contextmanager
54 """
55 Context manager that temporarily changes working directory for visualizer operations.
56
57 The C++ visualizer code expects to find assets at 'plugins/visualizer/' relative
58 to the current working directory. This context manager ensures the working directory
59 is set correctly during visualizer initialization and operations.
60
61 Note: This is required because the Helios C++ core prioritizes current working
62 directory for asset resolution over environment variables.
63 """
64 # Find the build directory where assets are located
65 # Try asset manager first (works for both development and wheel installations)
66 asset_manager = get_asset_manager()
67 working_dir = asset_manager._get_helios_build_path()
68
69 if working_dir and working_dir.exists():
70 visualizer_assets = working_dir / 'plugins' / 'visualizer'
71 else:
72 # For wheel installations, check packaged assets
73 current_dir = Path(__file__).parent
74 packaged_build = current_dir / 'assets' / 'build'
75
76 if packaged_build.exists():
77 working_dir = packaged_build
78 visualizer_assets = working_dir / 'plugins' / 'visualizer'
79 else:
80 # Fallback to development paths
81 repo_root = current_dir.parent
82 build_lib_dir = repo_root / 'pyhelios_build' / 'build' / 'lib'
83 working_dir = build_lib_dir.parent
84 visualizer_assets = working_dir / 'plugins' / 'visualizer'
85
86 if not build_lib_dir.exists():
87 logger.warning(f"Build directory not found: {build_lib_dir}")
88 # Fallback to current directory - may not work but don't break
89 yield
90 return
91
92 if not (visualizer_assets / 'shaders').exists():
93 # Only warn in development environments, not wheel installations
94 asset_mgr = get_asset_manager()
95 if not asset_mgr._is_wheel_install():
96 logger.warning(f"Visualizer assets not found at: {visualizer_assets}")
97 # Continue anyway - may be using source assets or alternative setup
98
99 # Change working directory temporarily
100 original_cwd = Path.cwd()
101
102 try:
103 logger.debug(f"Changing working directory from {original_cwd} to {working_dir}")
104 os.chdir(working_dir)
105 yield
106 finally:
107 logger.debug(f"Restoring working directory to {original_cwd}")
108 os.chdir(original_cwd)
109
110
111class VisualizerError(Exception):
112 """Raised when Visualizer operations fail."""
113 pass
114
115
116class Visualizer:
117 """
118 High-level interface for 3D visualization and rendering.
119
120 This class provides a user-friendly wrapper around the native Helios
121 visualizer plugin with automatic plugin availability checking and
122 graceful error handling.
123
124 The visualizer provides OpenGL-based 3D rendering with interactive controls,
125 image export, and comprehensive scene configuration options.
126 """
127
128 # Lighting model constants
129 LIGHTING_NONE = 0
130 LIGHTING_PHONG = 1
131 LIGHTING_PHONG_SHADOWED = 2
132
133 # Colormap constants (matching C++ enum values)
134 COLORMAP_HOT = 0
135 COLORMAP_COOL = 1
136 COLORMAP_RAINBOW = 2
137 COLORMAP_LAVA = 3
138 COLORMAP_PARULA = 4
139 COLORMAP_GRAY = 5
140 COLORMAP_LINES = 7
141 COLORMAP_ALGAE = 8
142 COLORMAP_GREEN = 9
143
144 def __init__(self, width: int, height: int, antialiasing_samples: int = 4, headless: bool = False):
145 """
146 Initialize Visualizer with graceful plugin handling.
148 Args:
149 width: Window width in pixels
150 height: Window height in pixels
151 antialiasing_samples: Number of antialiasing samples (default: 4). Pass 0 to
152 disable antialiasing, which is required for exact color reproduction --
153 see :meth:`enableExactColorMode`.
154 headless: Enable headless mode for offscreen rendering (default: False)
155
156 Raises:
157 VisualizerError: If visualizer plugin is not available
158 ValueError: If parameters are invalid
159 """
160 # Validate parameter types first
161 if not isinstance(width, _INT_TYPE):
162 raise ValueError(f"Width must be an integer, got {type(width).__name__}")
163 if not isinstance(height, _INT_TYPE):
164 raise ValueError(f"Height must be an integer, got {type(height).__name__}")
165 if not isinstance(antialiasing_samples, _INT_TYPE):
166 raise ValueError(f"Antialiasing samples must be an integer, got {type(antialiasing_samples).__name__}")
167 if not isinstance(headless, bool):
168 raise ValueError(f"Headless must be a boolean, got {type(headless).__name__}")
170 # Validate parameter values
171 if width <= 0 or height <= 0:
172 raise ValueError("Width and height must be positive integers")
173 if antialiasing_samples < 0:
174 raise ValueError(
175 f"Antialiasing samples must be non-negative, got {antialiasing_samples}. "
176 "Pass 0 to disable antialiasing.")
177
178 self.width = width
179 self.height = height
180 self.antialiasing_samples = antialiasing_samples
181 self.headless = headless
182 self.visualizer = None
183
184 # Check plugin availability using registry
185 registry = get_plugin_registry()
186
187 if not registry.is_plugin_available('visualizer'):
188 # Get helpful information about the missing plugin
189 available_plugins = registry.get_available_plugins()
191 error_msg = (
192 "Visualizer requires the 'visualizer' plugin which is not available.\n\n"
193 "The visualizer plugin provides OpenGL-based 3D rendering and visualization.\n"
194 "System requirements:\n"
195 "- OpenGL 3.3 or higher\n"
196 "- GLFW library for window management\n"
197 "- FreeType library for text rendering\n"
198 "- Display/graphics drivers (X11 on Linux, native on Windows/macOS)\n\n"
199 "To enable visualization:\n"
200 "1. Build PyHelios with visualizer plugin:\n"
201 " build_scripts/build_helios --plugins visualizer\n"
202 f"\nCurrently available plugins: {available_plugins}"
203 )
204
205 # Add platform-specific installation hints
206 import platform
207 system = platform.system().lower()
208 if 'linux' in system:
209 error_msg += (
210 "\n\nLinux installation hints:\n"
211 "- Ubuntu/Debian: sudo apt-get install libx11-dev xorg-dev libgl1-mesa-dev libglu1-mesa-dev\n"
212 "- CentOS/RHEL: sudo yum install libX11-devel mesa-libGL-devel mesa-libGLU-devel"
213 )
214 elif 'darwin' in system:
215 error_msg += (
216 "\n\nmacOS installation hints:\n"
217 "- Install XQuartz: brew install --cask xquartz\n"
218 "- OpenGL should be available by default"
219 )
220 elif 'windows' in system:
221 error_msg += (
222 "\n\nWindows installation hints:\n"
223 "- OpenGL drivers should be provided by graphics card drivers\n"
224 "- Visual Studio runtime may be required"
225 )
226
227 raise VisualizerError(error_msg)
228
229 # Plugin is available - create visualizer with correct working directory
230 try:
232 # Always use the explicit-sample constructor: create_visualizer()
233 # hardcodes 4 samples on the C++ side, so routing through it would
234 # silently ignore the requested count (including a request to disable
235 # antialiasing, which exact color mode depends on).
236 self.visualizer = visualizer_wrapper.create_visualizer_with_antialiasing(
237 width, height, antialiasing_samples, headless
238 )
239
240
241 if self.visualizer is None:
242 raise VisualizerError(
243 "Failed to create Visualizer instance. "
244 "This may indicate a problem with graphics drivers or OpenGL initialization."
245 )
246 logger.info(f"Visualizer created successfully ({width}x{height}, AA:{antialiasing_samples}, headless:{headless})")
247
248 except Exception as e:
249 raise VisualizerError(f"Failed to initialize Visualizer: {e}")
250
251 def _check_context_alive(self):
252 """Raise if a Context was loaded and has since been destroyed."""
253 if getattr(self, "_context", None) is not None:
254 check_context_alive(self._context, "Visualizer")
255
256 def __enter__(self):
257 """Context manager entry."""
258 return self
259
260 def __exit__(self, exc_type, exc_value, traceback):
261 """Context manager exit with proper cleanup."""
262 if self.visualizer is not None:
263 try:
265 visualizer_wrapper.destroy_visualizer(self.visualizer)
266 logger.debug("Visualizer destroyed successfully")
267 except Exception as e:
268 logger.warning(f"Error destroying Visualizer: {e}")
269 finally:
270 self.visualizer = None
271
272 @validate_build_geometry_params
273 def buildContextGeometry(self, context: Context, uuids: Optional[List[int]] = None) -> None:
274 """
275 Build Context geometry in the visualizer.
276
277 This method loads geometry from a Helios Context into the visualizer
278 for rendering. If no UUIDs are specified, all geometry is loaded.
279
280 Args:
281 context: Helios Context instance containing geometry
282 uuids: Optional list of primitive UUIDs to visualize (default: all)
283
284 Raises:
285 VisualizerError: If geometry building fails
286 ValueError: If parameters are invalid
287 """
288 if self.visualizer is None:
289 raise VisualizerError("Visualizer has been destroyed")
290 if not isinstance(context, Context):
291 raise ValueError("context must be a Context instance")
292
293 # Retain a reference to the Context. The native visualizer stores the raw
294 # Context* and only dereferences it later, at render time, so without this
295 # a temporary Context (e.g. buildContextGeometry(make_scene())) would be
296 # garbage collected before the first plot call and crash the interpreter.
297 self._context = context
298
299 try:
301 if uuids is None:
302 # Load all geometry
303 visualizer_wrapper.build_context_geometry(self.visualizer, context.getNativePtr())
304 logger.debug("Built all Context geometry in visualizer")
305 else:
306 # Load specific UUIDs
307 if not uuids:
308 raise ValueError("UUIDs list cannot be empty")
309 visualizer_wrapper.build_context_geometry_uuids(
310 self.visualizer, context.getNativePtr(), uuids
311 )
312 logger.debug(f"Built {len(uuids)} primitives in visualizer")
313
314 except Exception as e:
315 raise VisualizerError(f"Failed to build Context geometry: {e}")
316
317 def plotInteractive(self) -> None:
318 """
319 Open interactive visualization window.
320
321 This method opens a window with the current scene and allows user
322 interaction (camera rotation, zooming, etc.). The program will pause
323 until the window is closed by the user.
324
325 Interactive controls:
326 - Mouse scroll: Zoom in/out
327 - Left mouse + drag: Rotate camera
328 - Right mouse + drag: Pan camera
329 - Arrow keys: Camera movement
330 - +/- keys: Zoom in/out
331
332 Raises:
333 VisualizerError: If visualization fails, or if the Visualizer was
334 constructed with ``headless=True`` (helios-core 1.3.85+ raises rather
335 than driving a window that was never shown or does not exist)
336 """
338 if self.visualizer is None:
339 raise VisualizerError("Visualizer has been destroyed")
340
341 try:
343 visualizer_wrapper.plot_interactive(self.visualizer)
344 logger.debug("Interactive visualization completed")
345 except Exception as e:
346 raise VisualizerError(f"Interactive visualization failed: {e}")
347
348 def plotUpdate(self) -> None:
349 """
350 Update visualization (non-interactive).
351
352 This method updates the visualization window without user interaction.
353 The program continues immediately after rendering. Useful for batch
354 processing or creating image sequences.
355
356 In headless mode, automatically hides the window to prevent graphics driver crashes on some platforms.
357
358 Raises:
359 VisualizerError: If visualization update fails
360 """
362 if self.visualizer is None:
363 raise VisualizerError("Visualizer has been destroyed")
364
365 try:
367 # In headless mode, hide the window to avoid OpenGL/Metal crashes on macOS
368 visualizer_wrapper.plot_update(self.visualizer, hide_window=self.headless)
369 logger.debug("Visualization updated")
370 except Exception as e:
371 raise VisualizerError(f"Visualization update failed: {e}")
373 @validate_print_window_params
374 def printWindow(self, filename: str, image_format: Optional[str] = None) -> None:
375 """
376 Save current visualization to image file.
377
378 This method exports the current visualization to an image file.
379 Starting from v1.3.53, supports both JPEG and PNG formats.
380
381 The frame is rendered as part of the capture (helios-core 1.3.85+), so it is
382 not necessary to call :meth:`plotUpdate` beforehand: handing the Visualizer a
383 Context with :meth:`buildContextGeometry` and calling this straight away
384 produces a correct image. Calling :meth:`plotUpdate` first is harmless but
385 redundant. The navigation gizmo is always absent from the captured image.
386
387 Args:
388 filename: Output filename for image
389 Can be absolute or relative to user's current working directory
390 Extension (.jpg, .png) is recommended but not required
391 image_format: Image format - "jpeg" or "png" (v1.3.53+).
392 If None, automatically detects from filename extension.
393 Defaults to "jpeg" if not detectable from extension.
394
395 Raises:
396 VisualizerError: If image saving fails
397 ValueError: If filename or format is invalid
398
399 Note:
400 PNG format is required to preserve transparent backgrounds when using
401 setBackgroundTransparent(). JPEG format will render transparent areas as black.
402
403 Example:
404 >>> visualizer.printWindow("output.jpg") # Auto-detects JPEG
405 >>> visualizer.printWindow("output.png") # Auto-detects PNG
406 >>> visualizer.printWindow("output.img", image_format="png") # Explicit PNG
407 """
409 if self.visualizer is None:
410 raise VisualizerError("Visualizer has been destroyed")
411 if not filename:
412 raise ValueError("Filename cannot be empty")
413
414 # Resolve filename relative to user's working directory before chdir
415 resolved_filename = _resolve_user_path(filename)
416
417 # Auto-detect format from extension if not specified
418 if image_format is None:
419 if resolved_filename.lower().endswith('.png'):
420 image_format = 'png'
421 elif resolved_filename.lower().endswith(('.jpg', '.jpeg')):
422 image_format = 'jpeg'
423 else:
424 # Default to jpeg for backward compatibility
425 image_format = 'jpeg'
426 logger.debug(f"No format specified and extension not recognized, defaulting to JPEG")
427
428 # Validate format
429 if image_format.lower() not in ['jpeg', 'png']:
430 raise ValueError(f"Image format must be 'jpeg' or 'png', got '{image_format}'")
431
432 try:
434 # Try using the new format-aware function (v1.3.53+)
435 try:
436 visualizer_wrapper.print_window_with_format(
437 self.visualizer,
438 resolved_filename,
439 image_format
440 )
441 logger.debug(f"Visualization saved to {resolved_filename} ({image_format.upper()} format)")
442 except (AttributeError, NotImplementedError):
443 # Fallback to old function for older Helios versions
444 if image_format.lower() != 'jpeg':
445 logger.warning(
446 "PNG format requested but not available in current Helios version. "
447 "Falling back to JPEG format. Update to Helios v1.3.53+ for PNG support."
448 )
449 visualizer_wrapper.print_window(self.visualizer, resolved_filename)
450 logger.debug(f"Visualization saved to {resolved_filename} (JPEG format - legacy mode)")
451 except Exception as e:
452 raise VisualizerError(f"Failed to save image: {e}")
453
454 def closeWindow(self) -> None:
455 """
456 Close visualization window.
457
458 This method closes any open visualization window. It's safe to call
459 even if no window is open.
460
461 Raises:
462 VisualizerError: If window closing fails
463 """
464 if self.visualizer is None:
465 raise VisualizerError("Visualizer has been destroyed")
466
467 try:
468 visualizer_wrapper.close_window(self.visualizer)
469 logger.debug("Visualization window closed")
470 except Exception as e:
471 raise VisualizerError(f"Failed to close window: {e}")
472
473 def setCameraPosition(self, position: vec3, lookAt: vec3) -> None:
474 """
475 Set camera position using Cartesian coordinates.
476
477 Args:
478 position: Camera position as vec3 in world coordinates
479 lookAt: Camera look-at point as vec3 in world coordinates
480
481 Raises:
482 VisualizerError: If camera positioning fails
483 ValueError: If parameters are invalid
484 """
485 if self.visualizer is None:
486 raise VisualizerError("Visualizer has been destroyed")
487
488 # Validate DataType parameters
489 if not isinstance(position, vec3):
490 raise ValueError(f"Position must be a vec3, got {type(position).__name__}")
491 if not isinstance(lookAt, vec3):
492 raise ValueError(f"LookAt must be a vec3, got {type(lookAt).__name__}")
493
494 try:
495 visualizer_wrapper.set_camera_position(self.visualizer, position, lookAt)
496 logger.debug(f"Camera position set to ({position.x}, {position.y}, {position.z}), looking at ({lookAt.x}, {lookAt.y}, {lookAt.z})")
497 except Exception as e:
498 raise VisualizerError(f"Failed to set camera position: {e}")
499
500 def setCameraPositionSpherical(self, angle: SphericalCoord, lookAt: vec3) -> None:
501 """
502 Set camera position using spherical coordinates.
503
504 Args:
505 angle: Camera position as SphericalCoord (radius, elevation, azimuth)
506 lookAt: Camera look-at point as vec3 in world coordinates
507
508 Raises:
509 VisualizerError: If camera positioning fails
510 ValueError: If parameters are invalid
511 """
512 if self.visualizer is None:
513 raise VisualizerError("Visualizer has been destroyed")
514
515 # Validate DataType parameters
516 if not isinstance(angle, SphericalCoord):
517 raise ValueError(f"Angle must be a SphericalCoord, got {type(angle).__name__}")
518 if not isinstance(lookAt, vec3):
519 raise ValueError(f"LookAt must be a vec3, got {type(lookAt).__name__}")
520
521 try:
522 visualizer_wrapper.set_camera_position_spherical(self.visualizer, angle, lookAt)
523 logger.debug(f"Camera position set to spherical (r={angle.radius}, el={angle.elevation}, az={angle.azimuth}), looking at ({lookAt.x}, {lookAt.y}, {lookAt.z})")
524 except Exception as e:
525 raise VisualizerError(f"Failed to set camera position (spherical): {e}")
526
527 def setBackgroundColor(self, color: RGBcolor) -> None:
528 """
529 Set background color.
530
531 Args:
532 color: Background color as RGBcolor with values in range [0, 1]
533
534 Raises:
535 VisualizerError: If color setting fails
536 ValueError: If color values are invalid
537 """
538 if self.visualizer is None:
539 raise VisualizerError("Visualizer has been destroyed")
540
541 # Validate DataType parameter
542 if not isinstance(color, RGBcolor):
543 raise ValueError(f"Color must be an RGBcolor, got {type(color).__name__}")
544
545 # Validate color range
546 if not (0 <= color.r <= 1 and 0 <= color.g <= 1 and 0 <= color.b <= 1):
547 raise ValueError(f"Color components ({color.r}, {color.g}, {color.b}) must be in range [0, 1]")
548
549 try:
550 visualizer_wrapper.set_background_color(self.visualizer, color)
551 logger.debug(f"Background color set to ({color.r}, {color.g}, {color.b})")
552 except Exception as e:
553 raise VisualizerError(f"Failed to set background color: {e}")
554
555 def setBackgroundTransparent(self) -> None:
556 """
557 Enable transparent background mode (v1.3.53+).
558
559 Sets the background to transparent with checkerboard pattern display.
560 Requires PNG output format to preserve transparency.
561
562 Note: When using transparent background, use printWindow() with PNG
563 format to save transparent images.
564
565 Raises:
566 VisualizerError: If transparent background setting fails
567 """
568 if self.visualizer is None:
569 raise VisualizerError("Visualizer has been destroyed")
570
571 try:
573 visualizer_wrapper.set_background_transparent(self.visualizer)
574 logger.debug("Background set to transparent mode")
575 except Exception as e:
576 raise VisualizerError(f"Failed to set transparent background: {e}")
577
578 def setBackgroundImage(self, texture_file: str) -> None:
579 """
580 Set custom background image texture (v1.3.53+).
581
582 Args:
583 texture_file: Path to background image file
584 Can be absolute or relative to working directory
585
586 Raises:
587 VisualizerError: If background image setting fails
588 ValueError: If texture file path is invalid
589 """
590 if self.visualizer is None:
591 raise VisualizerError("Visualizer has been destroyed")
592
593 if not texture_file or not isinstance(texture_file, str):
594 raise ValueError("Texture file path must be a non-empty string")
595
596 # Resolve texture file path relative to user's working directory
597 resolved_path = _resolve_user_path(texture_file)
598
599 try:
600 visualizer_wrapper.set_background_image(self.visualizer, resolved_path)
601 logger.debug(f"Background image set to {resolved_path}")
602 except Exception as e:
603 raise VisualizerError(f"Failed to set background image: {e}")
604
605 def setBackgroundSkyTexture(self, texture_file: Optional[str] = None, divisions: int = 50) -> None:
606 """
607 Set sky sphere texture background with automatic scaling (v1.3.53+).
608
609 Creates a sky sphere that automatically scales with the scene.
610 Replaces the deprecated addSkyDomeByCenter() method.
611
612 Args:
613 texture_file: Path to spherical/equirectangular texture image
614 If None, uses default gradient sky texture
615 divisions: Number of sphere tessellation divisions (default: 50)
616 Higher values create smoother sphere but use more GPU
617
618 Raises:
619 VisualizerError: If sky texture setting fails
620 ValueError: If parameters are invalid
621
622 Example:
623 >>> visualizer.setBackgroundSkyTexture() # Default gradient sky
624 >>> visualizer.setBackgroundSkyTexture("sky_hdri.jpg", divisions=100)
625 """
626 if self.visualizer is None:
627 raise VisualizerError("Visualizer has been destroyed")
628
629 if not isinstance(divisions, _INT_TYPE) or divisions <= 0:
630 raise ValueError("Divisions must be a positive integer")
631
632 # Resolve texture file path if provided
633 resolved_path = None
634 if texture_file:
635 if not isinstance(texture_file, str):
636 raise ValueError("Texture file must be a string")
637 resolved_path = _resolve_user_path(texture_file)
638
639 try:
640 # resolved_path is already absolute when the caller supplied one; the
641 # default sky texture is a packaged asset the native code resolves
642 # relative to the working directory.
644 visualizer_wrapper.set_background_sky_texture(
645 self.visualizer,
646 resolved_path,
647 divisions
648 )
649 if resolved_path:
650 logger.debug(f"Sky texture background set: {resolved_path}, divisions={divisions}")
651 else:
652 logger.debug(f"Default sky texture background set with divisions={divisions}")
653 except Exception as e:
654 raise VisualizerError(f"Failed to set sky texture background: {e}")
655
656 def setLightDirection(self, direction: vec3) -> None:
657 """
658 Set light direction.
659
660 Args:
661 direction: Vector pointing from the scene toward the light source; for example,
662 ``vec3(0, 0, 1)`` places the light directly overhead. It does not need to be a
663 unit vector.
664
665 Raises:
666 VisualizerError: If light direction setting fails
667 ValueError: If direction is invalid
668 """
669 if self.visualizer is None:
670 raise VisualizerError("Visualizer has been destroyed")
671
672 # Validate DataType parameter
673 if not isinstance(direction, vec3):
674 raise ValueError(f"Direction must be a vec3, got {type(direction).__name__}")
675
676 # Check for zero vector
677 if direction.x == 0 and direction.y == 0 and direction.z == 0:
678 raise ValueError("Light direction cannot be zero vector")
679
680 try:
681 visualizer_wrapper.set_light_direction(self.visualizer, direction)
682 logger.debug(f"Light direction set to ({direction.x}, {direction.y}, {direction.z})")
683 except Exception as e:
684 raise VisualizerError(f"Failed to set light direction: {e}")
685
686 def setLightingModel(self, lighting_model: Union[int, str]) -> None:
687 """
688 Set lighting model.
689
690 Args:
691 lighting_model: Lighting model, either:
692 - 0 or "none": No lighting
693 - 1 or "phong": Phong shading
694 - 2 or "phong_shadowed": Phong shading with shadows
695
696 Raises:
697 VisualizerError: If lighting model setting fails
698 ValueError: If lighting model is invalid
699 """
700 if self.visualizer is None:
701 raise VisualizerError("Visualizer has been destroyed")
702
703 # Convert string to integer if needed
704 if isinstance(lighting_model, str):
705 lighting_model_lower = lighting_model.lower()
706 if lighting_model_lower in ['none', 'no', 'off']:
707 lighting_model = self.LIGHTING_NONE
708 elif lighting_model_lower in ['phong', 'phong_lighting']:
709 lighting_model = self.LIGHTING_PHONG
710 elif lighting_model_lower in ['phong_shadowed', 'phong_shadows', 'shadowed']:
711 lighting_model = self.LIGHTING_PHONG_SHADOWED
712 else:
713 raise ValueError(f"Unknown lighting model string: {lighting_model}")
714
715 # Validate integer value
716 if lighting_model not in [self.LIGHTING_NONE, self.LIGHTING_PHONG, self.LIGHTING_PHONG_SHADOWED]:
717 raise ValueError(f"Lighting model must be 0 (NONE), 1 (PHONG), or 2 (PHONG_SHADOWED), got {lighting_model}")
718
719 try:
720 visualizer_wrapper.set_lighting_model(self.visualizer, lighting_model)
721 model_names = {0: "NONE", 1: "PHONG", 2: "PHONG_SHADOWED"}
722 logger.debug(f"Lighting model set to {model_names.get(lighting_model, lighting_model)}")
723 except Exception as e:
724 raise VisualizerError(f"Failed to set lighting model: {e}")
725
726 def colorContextPrimitivesByData(self, data_name: str, uuids: Optional[List[int]] = None) -> None:
727 """
728 Color context primitives based on primitive data values.
729
730 This method maps primitive data values to colors using the current colormap.
731 The visualization will be updated to show data variations across primitives.
732
733 The data must have been previously set on the primitives in the Context using
734 context.setPrimitiveDataFloat(UUID, data_name, value) before calling this method.
735
736 Args:
737 data_name: Name of the primitive data to use for coloring.
738 This should match the data label used with setPrimitiveDataFloat().
739 uuids: Optional list of specific primitive UUIDs to color.
740 If None, all primitives in context will be colored.
741
742 Raises:
743 VisualizerError: If visualizer is not initialized or operation fails
744 ValueError: If data_name is invalid or UUIDs are malformed
745
746 Example:
747 >>> # Set data on primitives in context
748 >>> context.setPrimitiveDataFloat(patch_uuid, "radiation_flux_SW", 450.2)
749 >>> context.setPrimitiveDataFloat(triangle_uuid, "radiation_flux_SW", 320.1)
750 >>>
751 >>> # Build geometry and color by data
752 >>> visualizer.buildContextGeometry(context)
753 >>> visualizer.colorContextPrimitivesByData("radiation_flux_SW")
754 >>> visualizer.plotInteractive()
755
756 >>> # Color only specific primitives
757 >>> visualizer.colorContextPrimitivesByData("temperature", [uuid1, uuid2, uuid3])
758 """
759 if not self.visualizer:
760 raise VisualizerError("Visualizer not initialized")
761
762 if not data_name or not isinstance(data_name, str):
763 raise ValueError("Data name must be a non-empty string")
764
765 try:
766 if uuids is None:
767 # Color all primitives
768 visualizer_wrapper.color_context_primitives_by_data(self.visualizer, data_name)
769 logger.debug(f"Colored all primitives by data: {data_name}")
770 else:
771 # Color specific primitives
772 if not isinstance(uuids, (list, tuple)) or not uuids:
773 raise ValueError("UUIDs must be a non-empty list or tuple")
774 if not all(isinstance(uuid, _INT_TYPE) and uuid >= 0 for uuid in uuids):
775 raise ValueError("All UUIDs must be non-negative integers")
776
777 visualizer_wrapper.color_context_primitives_by_data_uuids(self.visualizer, data_name, list(uuids))
778 logger.debug(f"Colored {len(uuids)} primitives by data: {data_name}")
779
780 except ValueError:
781 # Re-raise ValueError as is
782 raise
783 except Exception as e:
784 raise VisualizerError(f"Failed to color primitives by data '{data_name}': {e}")
785
786 # Camera Control Methods
787
788 def setCameraFieldOfView(self, angle_FOV: float) -> None:
789 """
790 Set camera field of view angle.
791
792 Args:
793 angle_FOV: Field of view angle in degrees
794
795 Raises:
796 ValueError: If angle is invalid
797 VisualizerError: If operation fails
798 """
799 if not self.visualizer:
800 raise VisualizerError("Visualizer not initialized")
801
802 try:
803 float(angle_FOV)
804 except (TypeError, ValueError):
805 raise ValueError("Field of view angle must be numeric")
806 if angle_FOV <= 0 or angle_FOV >= 180:
807 raise ValueError("Field of view angle must be between 0 and 180 degrees")
808
809 try:
810 helios_lib.setCameraFieldOfView(self.visualizer, ctypes.c_float(angle_FOV))
811 except Exception as e:
812 raise VisualizerError(f"Failed to set camera field of view: {e}")
813
814 def getCameraPosition(self) -> Tuple[vec3, vec3]:
815 """
816 Get current camera position and look-at point.
817
818 Returns:
819 Tuple of (camera_position, look_at_point) as vec3 objects
820
821 Raises:
822 VisualizerError: If operation fails
823 """
824 if not self.visualizer:
825 raise VisualizerError("Visualizer not initialized")
826
827 try:
828 # Prepare output arrays
829 camera_pos = (ctypes.c_float * 3)()
830 look_at = (ctypes.c_float * 3)()
831
832 helios_lib.getCameraPosition(self.visualizer, camera_pos, look_at)
833
834 return (vec3(camera_pos[0], camera_pos[1], camera_pos[2]),
835 vec3(look_at[0], look_at[1], look_at[2]))
836 except Exception as e:
837 raise VisualizerError(f"Failed to get camera position: {e}")
838
839 def getBackgroundColor(self) -> RGBcolor:
840 """
841 Get current background color.
842
843 Returns:
844 Background color as RGBcolor object
845
846 Raises:
847 VisualizerError: If operation fails
848 """
849 if not self.visualizer:
850 raise VisualizerError("Visualizer not initialized")
851
852 try:
853 # Prepare output array
854 color = (ctypes.c_float * 3)()
855
856 helios_lib.getBackgroundColor(self.visualizer, color)
857
858 return RGBcolor(color[0], color[1], color[2])
859 except Exception as e:
860 raise VisualizerError(f"Failed to get background color: {e}")
861
862 # Lighting Control Methods
863
864 def setLightIntensityFactor(self, intensity_factor: float) -> None:
865 """
866 Set light intensity scaling factor.
867
868 Args:
869 intensity_factor: Light intensity scaling factor (typically 0.1 to 10.0)
870
871 Raises:
872 ValueError: If intensity factor is invalid
873 VisualizerError: If operation fails
874 """
875 if not self.visualizer:
876 raise VisualizerError("Visualizer not initialized")
877
878 if not isinstance(intensity_factor, _NUMERIC_TYPES):
879 raise ValueError("Light intensity factor must be numeric")
880 if intensity_factor <= 0:
881 raise ValueError("Light intensity factor must be positive")
882
883 try:
884 helios_lib.setLightIntensityFactor(self.visualizer, ctypes.c_float(intensity_factor))
885 except Exception as e:
886 raise VisualizerError(f"Failed to set light intensity factor: {e}")
887
888 # Window and Display Methods
889
890 def getWindowSize(self) -> Tuple[int, int]:
891 """
892 Get window size in pixels.
893
894 Returns:
895 Tuple of (width, height) in pixels
896
897 Raises:
898 VisualizerError: If operation fails
899 """
900 if not self.visualizer:
901 raise VisualizerError("Visualizer not initialized")
902
903 try:
904 width = ctypes.c_uint()
905 height = ctypes.c_uint()
906
907 helios_lib.getWindowSize(self.visualizer, ctypes.byref(width), ctypes.byref(height))
908
909 return (width.value, height.value)
910 except Exception as e:
911 raise VisualizerError(f"Failed to get window size: {e}")
912
913 def getFramebufferSize(self) -> Tuple[int, int]:
914 """
915 Get framebuffer size in pixels.
916
917 Returns:
918 Tuple of (width, height) in pixels
919
920 Raises:
921 VisualizerError: If operation fails
922 """
923 if not self.visualizer:
924 raise VisualizerError("Visualizer not initialized")
925
926 try:
927 width = ctypes.c_uint()
928 height = ctypes.c_uint()
929
930 helios_lib.getFramebufferSize(self.visualizer, ctypes.byref(width), ctypes.byref(height))
931
932 return (width.value, height.value)
933 except Exception as e:
934 raise VisualizerError(f"Failed to get framebuffer size: {e}")
935
936 def printWindowDefault(self) -> None:
937 """
938 Print window with default filename.
939
940 Raises:
941 VisualizerError: If operation fails
942 """
944 if not self.visualizer:
945 raise VisualizerError("Visualizer not initialized")
946
947 try:
948 helios_lib.printWindowDefault(self.visualizer)
949 except Exception as e:
950 raise VisualizerError(f"Failed to print window: {e}")
951
952 def displayImageFromPixels(self, pixel_data: List[int], width: int, height: int) -> None:
953 """
954 Display image from RGBA pixel data.
955
956 Args:
957 pixel_data: RGBA pixel data as list of integers (0-255)
958 width: Image width in pixels
959 height: Image height in pixels
960
961 Raises:
962 ValueError: If parameters are invalid
963 VisualizerError: If operation fails
964 """
965 if not self.visualizer:
966 raise VisualizerError("Visualizer not initialized")
967
968 if not isinstance(pixel_data, (list, tuple)):
969 raise ValueError("Pixel data must be a list or tuple")
970 if not isinstance(width, _INT_TYPE) or width <= 0:
971 raise ValueError("Width must be a positive integer")
972 if not isinstance(height, _INT_TYPE) or height <= 0:
973 raise ValueError("Height must be a positive integer")
974
975 expected_size = width * height * 4 # RGBA format
976 if len(pixel_data) != expected_size:
977 raise ValueError(f"Pixel data size mismatch: expected {expected_size}, got {len(pixel_data)}")
978
979 try:
980 # Convert to ctypes array
981 pixel_array = (ctypes.c_ubyte * len(pixel_data))(*pixel_data)
982 helios_lib.displayImageFromPixels(self.visualizer, pixel_array, width, height)
983 except Exception as e:
984 raise VisualizerError(f"Failed to display image from pixels: {e}")
985
986 def displayImageFromFile(self, filename: str) -> None:
987 """
988 Display image from file.
989
990 Args:
991 filename: Path to image file
992
993 Raises:
994 ValueError: If filename is invalid
995 VisualizerError: If operation fails
996 """
997 if not self.visualizer:
998 raise VisualizerError("Visualizer not initialized")
999
1000 if not isinstance(filename, str) or not filename.strip():
1001 raise ValueError("Filename must be a non-empty string")
1002
1003 try:
1004 helios_lib.displayImageFromFile(self.visualizer, filename.encode('utf-8'))
1005 except Exception as e:
1006 raise VisualizerError(f"Failed to display image from file '{filename}': {e}")
1007
1008 def displayImageWithBoundingBoxes(self, image_file: str, bbox_file: str,
1009 classes_file: str = "", line_width: float = 2.0,
1010 fontsize: int = 12) -> None:
1011 """
1012 Display an image with YOLO bounding boxes overlaid.
1013
1014 Each box is drawn as a colored outline with its class name on a filled chip in
1015 the box's top-left corner. Boxes are colored by class ID from a fixed palette of
1016 seven colors, so classes whose IDs differ by a multiple of seven share a color.
1017
1018 This reads the annotation format written by
1019 :meth:`pyhelios.RadiationModel.writeImageBoundingBoxes`.
1020
1021 Args:
1022 image_file: Path to the image file (JPEG or PNG)
1023 bbox_file: Path to the YOLO-format bounding box annotation file
1024 classes_file: Path to the class name file. If empty (the default), a file
1025 named "classes.txt" beside ``bbox_file`` is used when one exists;
1026 otherwise boxes are labeled with their numeric class ID.
1027 line_width: Width of the box outlines in screen pixels
1028 fontsize: Size of the class label font in points
1029
1030 Raises:
1031 ValueError: If a path is not a non-empty string, or a numeric argument is out of range
1032 VisualizerError: If the operation fails
1033
1034 Note:
1035 Like :meth:`displayImageFromFile`, this clears any existing geometry and does
1036 not return until the window is closed.
1037
1038 Example:
1039 >>> vis.displayImageWithBoundingBoxes("scene.jpeg", "scene.txt")
1040 """
1041 if not self.visualizer:
1042 raise VisualizerError("Visualizer not initialized")
1043
1044 if not isinstance(image_file, str) or not image_file.strip():
1045 raise ValueError("Image file must be a non-empty string")
1046 if not isinstance(bbox_file, str) or not bbox_file.strip():
1047 raise ValueError("Bounding box file must be a non-empty string")
1048 if classes_file and not isinstance(classes_file, str):
1049 raise ValueError(
1050 f"Classes file must be a string, got {type(classes_file).__name__}")
1051 if line_width <= 0:
1052 raise ValueError(f"Line width must be positive, got {line_width}")
1053 if not isinstance(fontsize, _INT_TYPE) or fontsize <= 0:
1054 raise ValueError(f"Font size must be a positive integer, got {fontsize}")
1055
1056 try:
1057 visualizer_wrapper.display_image_with_bounding_boxes(
1058 self.visualizer,
1059 _resolve_user_path(image_file),
1060 _resolve_user_path(bbox_file),
1061 _resolve_user_path(classes_file) if classes_file else "",
1062 float(line_width), int(fontsize))
1063 except Exception as e:
1064 raise VisualizerError(
1065 f"Failed to display image '{image_file}' with bounding boxes: {e}")
1066
1067 def displayImageWithSegmentationMasks(self, image_file: str, mask_file: str,
1068 fill_opacity: float = 0.4, line_width: float = 2.0,
1069 fontsize: int = 12, show_labels: bool = True) -> None:
1070 """
1071 Display an image with COCO segmentation masks overlaid.
1072
1073 Each mask is drawn as a translucent filled polygon with a solid outline and its
1074 class name on a filled chip. Masks are colored by their position in the file
1075 rather than by class, so two touching objects of the same class stay
1076 distinguishable.
1077
1078 This reads the annotation format written by
1079 :meth:`pyhelios.RadiationModel.writeImageSegmentationMasks`.
1080
1081 Args:
1082 image_file: Path to the image file (JPEG or PNG)
1083 mask_file: Path to the COCO JSON segmentation mask file
1084 fill_opacity: Opacity of the translucent fill, between 0 and 1. A value of 0
1085 draws the outline without a fill.
1086 line_width: Width of the mask outlines in screen pixels
1087 fontsize: Size of the class label font in points
1088 show_labels: Whether to draw the class name chip on each mask. Pass False to
1089 see the masks alone, which helps when many overlapping chips would cover
1090 the image.
1091
1092 Raises:
1093 ValueError: If a path is not a non-empty string, or a numeric argument is out of range
1094 VisualizerError: If the operation fails
1095
1096 Note:
1097 Like :meth:`displayImageFromFile`, this clears any existing geometry and does
1098 not return until the window is closed.
1099
1100 Example:
1101 >>> vis.displayImageWithSegmentationMasks("scene.jpeg", "annotations.json")
1102 """
1103 if not self.visualizer:
1104 raise VisualizerError("Visualizer not initialized")
1105
1106 if not isinstance(image_file, str) or not image_file.strip():
1107 raise ValueError("Image file must be a non-empty string")
1108 if not isinstance(mask_file, str) or not mask_file.strip():
1109 raise ValueError("Mask file must be a non-empty string")
1110 if not 0.0 <= fill_opacity <= 1.0:
1111 raise ValueError(f"Fill opacity must be between 0 and 1, got {fill_opacity}")
1112 if line_width <= 0:
1113 raise ValueError(f"Line width must be positive, got {line_width}")
1114 if not isinstance(fontsize, _INT_TYPE) or fontsize <= 0:
1115 raise ValueError(f"Font size must be a positive integer, got {fontsize}")
1116 if not isinstance(show_labels, bool):
1117 raise ValueError(
1118 f"Show labels must be a boolean, got {type(show_labels).__name__}")
1119
1120 try:
1121 visualizer_wrapper.display_image_with_segmentation_masks(
1122 self.visualizer,
1123 _resolve_user_path(image_file),
1124 _resolve_user_path(mask_file),
1125 float(fill_opacity), float(line_width), int(fontsize), show_labels)
1126 except Exception as e:
1127 raise VisualizerError(
1128 f"Failed to display image '{image_file}' with segmentation masks: {e}")
1129
1130 def enableExactColorMode(self) -> None:
1131 """
1132 Render primitive colors exactly as they are set in the Context.
1133
1134 By default the fragment shader multiplies primitive colors by 1.5, which
1135 brightens ordinary renders but means a color read back out of the framebuffer is
1136 not the color that was set. This mode disables that multiplier, which is required
1137 when the framebuffer carries data rather than an image -- for example when object
1138 IDs are encoded as RGB values and decoded from the rendered pixels.
1139
1140 Exact reproduction additionally requires that no lighting be applied (see
1141 :meth:`setLightingModel` with ``LIGHTING_NONE``, the default) and that the
1142 Visualizer was constructed with ``antialiasing_samples=0``, since antialiasing
1143 blends colors at primitive edges and produces pixels that decode to meaningless
1144 IDs.
1145
1146 This also disables the linear-light pipeline (see :meth:`disableLinearPipeline`),
1147 since tone mapping would likewise alter the values read back.
1148
1149 Raises:
1150 VisualizerError: If the operation fails
1151
1152 Example:
1153 >>> vis = Visualizer(800, 600, antialiasing_samples=0)
1154 >>> vis.enableExactColorMode()
1155 """
1156 if not self.visualizer:
1157 raise VisualizerError("Visualizer not initialized")
1158 try:
1159 visualizer_wrapper.enable_exact_color_mode(self.visualizer)
1160 except Exception as e:
1161 raise VisualizerError(f"Failed to enable exact color mode: {e}")
1162
1163 def disableExactColorMode(self) -> None:
1164 """
1165 Restore the default brightening of primitive colors.
1166
1167 This also restores the linear-light pipeline (see :meth:`enableLinearPipeline`).
1168
1169 Raises:
1170 VisualizerError: If the operation fails
1171
1172 Example:
1173 >>> vis.disableExactColorMode()
1174 """
1175 if not self.visualizer:
1176 raise VisualizerError("Visualizer not initialized")
1177 try:
1178 visualizer_wrapper.disable_exact_color_mode(self.visualizer)
1179 except Exception as e:
1180 raise VisualizerError(f"Failed to disable exact color mode: {e}")
1181
1182 def enableLinearPipeline(self) -> None:
1183 """
1184 Enable the physically-based linear-light rendering pipeline.
1185
1186 This is the default. Albedo is decoded from sRGB to linear light before shading,
1187 and the shaded radiance is tone-mapped through an ACES filmic curve and
1188 re-encoded to sRGB. Compared with shading directly on non-linear sRGB values,
1189 mid-tones are brighter, shadow terminators are smoother, and bright surfaces roll
1190 off rather than clipping flat against the 8-bit framebuffer.
1191
1192 Raises:
1193 VisualizerError: If the operation fails
1194
1195 Example:
1196 >>> vis.enableLinearPipeline()
1197 """
1198 if not self.visualizer:
1199 raise VisualizerError("Visualizer not initialized")
1200 try:
1201 visualizer_wrapper.enable_linear_pipeline(self.visualizer)
1202 except Exception as e:
1203 raise VisualizerError(f"Failed to enable linear pipeline: {e}")
1204
1205 def disableLinearPipeline(self) -> None:
1206 """
1207 Disable the linear-light pipeline, shading directly in sRGB space.
1208
1209 This reproduces the rendering behavior of helios-core versions before 1.3.83.
1210
1211 Raises:
1212 VisualizerError: If the operation fails
1213
1214 Example:
1215 >>> vis.disableLinearPipeline()
1216 """
1217 if not self.visualizer:
1218 raise VisualizerError("Visualizer not initialized")
1219 try:
1220 visualizer_wrapper.disable_linear_pipeline(self.visualizer)
1221 except Exception as e:
1222 raise VisualizerError(f"Failed to disable linear pipeline: {e}")
1223
1224 def isLinearPipelineEnabled(self) -> bool:
1225 """
1226 Check whether the linear-light rendering pipeline is enabled.
1227
1228 Returns:
1229 True if the linear pipeline is enabled
1230
1231 Raises:
1232 VisualizerError: If the operation fails
1233
1234 Example:
1235 >>> vis.isLinearPipelineEnabled()
1236 True
1237 """
1238 if not self.visualizer:
1239 raise VisualizerError("Visualizer not initialized")
1240 try:
1241 return visualizer_wrapper.is_linear_pipeline_enabled(self.visualizer)
1242 except Exception as e:
1243 raise VisualizerError(f"Failed to query linear pipeline state: {e}")
1244
1245 def setExposure(self, exposure: float) -> None:
1246 """
1247 Set the linear exposure multiplier applied before tone mapping.
1248
1249 Only has an effect while the linear pipeline is enabled.
1250
1251 Args:
1252 exposure: Exposure multiplier; must be positive
1253
1254 Raises:
1255 ValueError: If exposure is not positive
1256 VisualizerError: If the operation fails
1257
1258 Example:
1259 >>> vis.setExposure(1.5)
1260 """
1261 if not self.visualizer:
1262 raise VisualizerError("Visualizer not initialized")
1263 if not isinstance(exposure, (int, float)) or isinstance(exposure, bool):
1264 raise ValueError(f"Exposure must be a number, got {type(exposure).__name__}")
1265 if exposure <= 0:
1266 raise ValueError(f"Exposure must be positive, got {exposure}")
1267 try:
1268 visualizer_wrapper.set_exposure(self.visualizer, float(exposure))
1269 except Exception as e:
1270 raise VisualizerError(f"Failed to set exposure: {e}")
1271
1272 def getExposure(self) -> float:
1273 """
1274 Get the linear exposure multiplier applied before tone mapping.
1275
1276 Returns:
1277 Current exposure multiplier
1278
1279 Raises:
1280 VisualizerError: If the operation fails
1281
1282 Example:
1283 >>> vis.getExposure()
1284 1.0
1285 """
1286 if not self.visualizer:
1287 raise VisualizerError("Visualizer not initialized")
1288 try:
1289 return visualizer_wrapper.get_exposure(self.visualizer)
1290 except Exception as e:
1291 raise VisualizerError(f"Failed to get exposure: {e}")
1292
1293 def setPhongMaterial(self, ambient: float, diffuse: float, specular: float,
1294 shininess: float) -> None:
1295 """
1296 Set the Phong material parameters used to shade Context primitives.
1298 Surfaces are shaded as ``ambient*A + diffuse*max(0, N.L) +
1299 specular*max(0, N.H)^shininess``, where ``A`` is the hemispheric ambient term set
1300 by :meth:`setAmbientColors`. Setting ``specular`` to zero removes the highlight,
1301 recovering the appearance of helios-core versions before 1.3.83. Has no effect
1302 under ``LIGHTING_NONE``.
1303
1304 Individual materials can override any of these parameters by attaching
1305 ``phong_ambient``, ``phong_diffuse``, ``phong_specular`` or ``phong_shininess``
1306 material data with :meth:`Context.setMaterialDataFloat`; each parameter falls
1307 back individually to the value set here.
1308
1309 Args:
1310 ambient: Ambient reflectance weight
1311 diffuse: Diffuse reflectance weight
1312 specular: Specular highlight strength
1313 shininess: Specular exponent controlling highlight tightness
1314
1315 Raises:
1316 VisualizerError: If the operation fails
1317
1318 Example:
1319 >>> vis.setPhongMaterial(1.0, 0.8, 0.0, 32.0) # no specular highlight
1320 """
1321 if not self.visualizer:
1322 raise VisualizerError("Visualizer not initialized")
1323 try:
1324 visualizer_wrapper.set_phong_material(self.visualizer, float(ambient),
1325 float(diffuse), float(specular),
1326 float(shininess))
1327 except Exception as e:
1328 raise VisualizerError(f"Failed to set Phong material: {e}")
1329
1330 def getPhongMaterial(self) -> Tuple[float, float, float, float]:
1331 """
1332 Get the Phong material parameters used to shade Context primitives.
1333
1334 Returns:
1335 Tuple of (ambient, diffuse, specular, shininess)
1336
1337 Raises:
1338 VisualizerError: If the operation fails
1339
1340 Example:
1341 >>> ambient, diffuse, specular, shininess = vis.getPhongMaterial()
1342 """
1343 if not self.visualizer:
1344 raise VisualizerError("Visualizer not initialized")
1345 try:
1346 return visualizer_wrapper.get_phong_material(self.visualizer)
1347 except Exception as e:
1348 raise VisualizerError(f"Failed to get Phong material: {e}")
1349
1350 def setAmbientColors(self, sky_color: RGBcolor, ground_color: RGBcolor) -> None:
1351 """
1352 Set the hemispheric ambient sky and ground-bounce colors.
1353
1354 Ambient light is blended between the two according to surface orientation:
1355 upward-facing surfaces pick up ``sky_color``, downward-facing surfaces
1356 ``ground_color``. Setting both to the same value recovers the single constant
1357 ambient term used before helios-core 1.3.83.
1358
1359 Args:
1360 sky_color: Color of light arriving from above
1361 ground_color: Color of light bouncing from below
1362
1363 Raises:
1364 ValueError: If either argument is not an RGBcolor
1365 VisualizerError: If the operation fails
1366
1367 Example:
1368 >>> vis.setAmbientColors(RGBcolor(0.5, 0.6, 0.75), RGBcolor(0.35, 0.3, 0.22))
1369 """
1370 if not self.visualizer:
1371 raise VisualizerError("Visualizer not initialized")
1372 if not isinstance(sky_color, RGBcolor):
1373 raise ValueError(f"sky_color must be an RGBcolor, got {type(sky_color).__name__}")
1374 if not isinstance(ground_color, RGBcolor):
1375 raise ValueError(f"ground_color must be an RGBcolor, got {type(ground_color).__name__}")
1376 try:
1377 visualizer_wrapper.set_ambient_colors(
1378 self.visualizer,
1379 (sky_color.r, sky_color.g, sky_color.b),
1380 (ground_color.r, ground_color.g, ground_color.b),
1382 except Exception as e:
1383 raise VisualizerError(f"Failed to set ambient colors: {e}")
1384
1385 def getAmbientSkyColor(self) -> RGBcolor:
1386 """
1387 Get the hemispheric ambient sky color.
1388
1389 Returns:
1390 Color of ambient light arriving from above
1391
1392 Raises:
1393 VisualizerError: If the operation fails
1394
1395 Example:
1396 >>> vis.getAmbientSkyColor()
1397 """
1398 if not self.visualizer:
1399 raise VisualizerError("Visualizer not initialized")
1400 try:
1401 r, g, b = visualizer_wrapper.get_ambient_sky_color(self.visualizer)
1402 return RGBcolor(r, g, b)
1403 except Exception as e:
1404 raise VisualizerError(f"Failed to get ambient sky color: {e}")
1405
1406 def getAmbientGroundColor(self) -> RGBcolor:
1407 """
1408 Get the hemispheric ambient ground-bounce color.
1410 Returns:
1411 Color of ambient light bouncing from below
1412
1413 Raises:
1414 VisualizerError: If the operation fails
1415
1416 Example:
1417 >>> vis.getAmbientGroundColor()
1418 """
1419 if not self.visualizer:
1420 raise VisualizerError("Visualizer not initialized")
1421 try:
1422 r, g, b = visualizer_wrapper.get_ambient_ground_color(self.visualizer)
1423 return RGBcolor(r, g, b)
1424 except Exception as e:
1425 raise VisualizerError(f"Failed to get ambient ground color: {e}")
1426
1427 def enableSmoothShading(self) -> None:
1428 """
1429 Enable smooth per-vertex-normal shading.
1431 This is the default. Only geometry that supplies distinct vertex normals is
1432 affected: Sphere, Tube and Cone objects, and Polymesh objects loaded from an OBJ
1433 or PLY file that carries them. Patches, triangles and voxels added directly to
1434 the Context carry the face normal replicated across their vertices and render
1435 identically either way.
1436
1437 Raises:
1438 VisualizerError: If the operation fails
1439
1440 Example:
1441 >>> vis.enableSmoothShading()
1442 """
1443 if not self.visualizer:
1444 raise VisualizerError("Visualizer not initialized")
1445 try:
1446 visualizer_wrapper.enable_smooth_shading(self.visualizer)
1447 except Exception as e:
1448 raise VisualizerError(f"Failed to enable smooth shading: {e}")
1449
1450 def disableSmoothShading(self) -> None:
1451 """
1452 Select flat (per-face) shading.
1453
1454 Useful for alpha-masked cutout geometry such as leaf textures, where interpolated
1455 normals can look worse than a single flat normal per face.
1456
1457 Raises:
1458 VisualizerError: If the operation fails
1459
1460 Example:
1461 >>> vis.disableSmoothShading()
1462 """
1463 if not self.visualizer:
1464 raise VisualizerError("Visualizer not initialized")
1465 try:
1466 visualizer_wrapper.disable_smooth_shading(self.visualizer)
1467 except Exception as e:
1468 raise VisualizerError(f"Failed to disable smooth shading: {e}")
1469
1470 def isSmoothShadingEnabled(self) -> bool:
1471 """
1472 Check whether smooth per-vertex-normal shading is enabled.
1473
1474 Returns:
1475 True if smooth shading is enabled
1476
1477 Raises:
1478 VisualizerError: If the operation fails
1479
1480 Example:
1481 >>> vis.isSmoothShadingEnabled()
1482 True
1483 """
1484 if not self.visualizer:
1485 raise VisualizerError("Visualizer not initialized")
1486 try:
1487 return visualizer_wrapper.is_smooth_shading_enabled(self.visualizer)
1488 except Exception as e:
1489 raise VisualizerError(f"Failed to query smooth shading state: {e}")
1490
1491 def isHeadlessMultisamplingActive(self) -> bool:
1492 """
1493 Check whether headless rendering obtained multisampled framebuffer attachments.
1494
1495 Headless rendering draws into a multisampled framebuffer using the sample count
1496 given to the constructor and resolves it before readback, so saved images are
1497 anti-aliased. The requested count is clamped to the driver maximum, and a driver
1498 that refuses the attachments falls back silently -- this query is how to detect
1499 that.
1500
1501 Returns:
1502 True if multisampled attachments were obtained
1503
1504 Note:
1505 macOS drives OpenGL through a translation layer that accepts the attachments
1506 but does not rasterize into them, so this can report True while the saved
1507 image is not actually anti-aliased.
1508
1509 Raises:
1510 VisualizerError: If the operation fails
1511
1512 Example:
1513 >>> vis.isHeadlessMultisamplingActive()
1514 True
1515 """
1516 if not self.visualizer:
1517 raise VisualizerError("Visualizer not initialized")
1518 try:
1519 return visualizer_wrapper.is_headless_multisampling_active(self.visualizer)
1520 except Exception as e:
1521 raise VisualizerError(f"Failed to query headless multisampling state: {e}")
1522
1523 def getTextboxSize(self, textstring: str, fontsize: int, fontname: str) -> vec2:
1524 """
1525 Measure the rendered size of a text string without adding it to the visualizer.
1526
1527 Returns the extent that :meth:`addTextboxByCenter` would occupy for the same
1528 string, font and font size. The width is the sum of the glyph advances, so it
1529 includes the side bearings; the height is that of the tallest glyph in the
1530 string, so it depends on which characters the string contains. The ``_`` and
1531 ``^`` subscript and superscript markers are handled as
1532 :meth:`addTextboxByCenter` handles them: they occupy no width themselves and
1533 halve the size of the character that follows.
1534
1535 Args:
1536 textstring: Text to be measured
1537 fontsize: Size of the text font in points
1538 fontname: Name of a font in the visualizer fonts directory, e.g.
1539 "OpenSans-Regular"
1540
1541 Returns:
1542 Width and height of the text in window-normalized units.
1543
1544 Raises:
1545 ValueError: If an argument is invalid
1546 VisualizerError: If the operation fails
1547
1548 Note:
1549 The result depends on the current framebuffer dimensions and DPI scale, and
1550 therefore changes when the window is resized.
1551
1552 Example:
1553 >>> size = vis.getTextboxSize("Leaf area", 14, "OpenSans-Regular")
1554 >>> print(f"{size.x:.3f} x {size.y:.3f}")
1555 """
1556 if not self.visualizer:
1557 raise VisualizerError("Visualizer not initialized")
1558
1559 if not isinstance(textstring, str):
1560 raise ValueError(
1561 f"Text string must be a string, got {type(textstring).__name__}")
1562 if not isinstance(fontsize, _INT_TYPE) or fontsize <= 0:
1563 raise ValueError(f"Font size must be a positive integer, got {fontsize}")
1564 if not isinstance(fontname, str) or not fontname.strip():
1565 raise ValueError("Font name must be a non-empty string")
1566
1567 try:
1568 # The font is read from the visualizer asset directory, which the native
1569 # code resolves relative to the current working directory. Without this the
1570 # lookup fails for any caller not already sitting in that directory.
1572 width, height = visualizer_wrapper.get_textbox_size(
1573 self.visualizer, textstring, int(fontsize), fontname)
1574 return vec2(width, height)
1575 except Exception as e:
1576 raise VisualizerError(f"Failed to measure text size: {e}")
1577
1578 # Window Data Access Methods
1579
1580 def getWindowPixelsRGB(self, buffer: Optional[List[int]] = None):
1581 """
1582 Get RGB pixel data from the current window.
1583
1584 Data is stored as r-g-b * column * row, so indices (0,1,2) are the RGB values for row 0
1585 column 0, indices (3,4,5) are row 0 column 1, and so on.
1586
1587 Call without arguments to have the buffer allocated for you at the correct size -- this is
1588 the recommended form and cannot be undersized::
1589
1590 pixels, width, height = visualizer.getWindowPixelsRGB()
1591
1592 Args:
1593 buffer: Optional pre-allocated list to fill in place. It must hold exactly
1594 ``3 * width * height`` elements, where width and height come from
1595 :meth:`getFramebufferSize` -- **not** :meth:`getWindowSize` and not the dimensions
1596 passed to the constructor. On a high-DPI (Retina) display the framebuffer is larger
1597 than the window, typically by a factor of two per axis, so a buffer sized from the
1598 window dimensions is four times too small.
1599
1600 Returns:
1601 If ``buffer`` is None, a tuple of ``(pixel_data, width, height)``. Otherwise ``None``;
1602 ``buffer`` is filled in place.
1603
1604 Raises:
1605 ValueError: If ``buffer`` is not a list, or is not sized for the current framebuffer
1606 VisualizerError: If operation fails
1607 """
1609 if not self.visualizer:
1610 raise VisualizerError("Visualizer not initialized")
1611
1612 if buffer is None:
1613 try:
1614 width = ctypes.c_uint()
1615 height = ctypes.c_uint()
1616 size = ctypes.c_uint()
1617 ptr = helios_lib.getWindowPixelsRGB_sized(
1618 self.visualizer, ctypes.byref(width), ctypes.byref(height), ctypes.byref(size)
1620 visualizer_wrapper._check_for_helios_error()
1621 if not ptr or size.value == 0:
1622 raise VisualizerError(
1623 "getWindowPixelsRGB() returned no pixel data; the framebuffer reported "
1624 f"{width.value}x{height.value}"
1625 )
1626 arr = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_uint * size.value)).contents
1627 return ([int(v) for v in arr], width.value, height.value)
1628 except VisualizerError:
1629 raise
1630 except Exception as e:
1631 raise VisualizerError(f"Failed to get window pixels: {e}")
1632
1633 if not isinstance(buffer, list):
1634 raise ValueError("Buffer must be a list")
1635
1636 # Reject an undersized buffer here rather than letting the native call write past its end.
1637 fb_width, fb_height = self.getFramebufferSize()
1638 required = 3 * fb_width * fb_height
1639 if len(buffer) != required:
1640 raise ValueError(
1641 f"Buffer must hold exactly {required} elements for the current framebuffer of "
1642 f"{fb_width}x{fb_height} (3*width*height), got {len(buffer)}. Note the framebuffer "
1643 f"may be larger than the window on a high-DPI display; use getFramebufferSize(), "
1644 f"not getWindowSize(). Call getWindowPixelsRGB() with no argument to have the "
1645 f"buffer allocated for you."
1646 )
1647
1648 try:
1649 # Convert buffer to ctypes array
1650 buffer_array = (ctypes.c_uint * len(buffer))(*buffer)
1651 helios_lib.getWindowPixelsRGB(self.visualizer, buffer_array)
1652 visualizer_wrapper._check_for_helios_error()
1653
1654 # Copy results back to Python list
1655 for i in range(len(buffer)):
1656 buffer[i] = buffer_array[i]
1657 except VisualizerError:
1658 raise
1659 except Exception as e:
1660 raise VisualizerError(f"Failed to get window pixels: {e}")
1661
1662 def getDepthMap(self) -> Tuple[List[float], int, int]:
1663 """
1664 Get the normalized depth map of the current view.
1665
1666 The values are the non-linear depth buffer values of the current frame, rescaled
1667 between the nearest and farthest of them and inverted, so that 255 is the nearest
1668 surface in view and 0 the farthest (including background). This is the image shown
1669 by :meth:`plotDepthMap`. It is **not** a distance: use :meth:`getDepthImage` for
1670 depth in world units.
1671
1672 Returns:
1673 Tuple of ``(depth_pixels, width, height)``. ``depth_pixels`` is row-major with
1674 row 0 at the BOTTOM of the image. ``width`` and ``height`` are the framebuffer
1675 dimensions (see :meth:`getFramebufferSize`), which are larger than the window
1676 on a high-DPI display.
1677
1678 Raises:
1679 VisualizerError: If operation fails
1680 """
1682 if not self.visualizer:
1683 raise VisualizerError("Visualizer not initialized")
1684
1685 try:
1686 depth_ptr = ctypes.POINTER(ctypes.c_float)()
1687 width = ctypes.c_uint()
1688 height = ctypes.c_uint()
1689 buffer_size = ctypes.c_uint()
1690
1691 helios_lib.getDepthMap(self.visualizer, ctypes.byref(depth_ptr),
1692 ctypes.byref(width), ctypes.byref(height),
1693 ctypes.byref(buffer_size))
1694 visualizer_wrapper._check_for_helios_error()
1695
1696 # Convert to Python list
1697 if depth_ptr and buffer_size.value > 0:
1698 depth_data = [depth_ptr[i] for i in range(buffer_size.value)]
1699 return (depth_data, width.value, height.value)
1700 else:
1701 return ([], 0, 0)
1702 except Exception as e:
1703 raise VisualizerError(f"Failed to get depth map: {e}")
1704
1705 def plotDepthMap(self) -> None:
1706 """
1707 Plot depth map visualization.
1708
1709 Raises:
1710 VisualizerError: If operation fails
1711 """
1713 if not self.visualizer:
1714 raise VisualizerError("Visualizer not initialized")
1715
1716 try:
1717 helios_lib.plotDepthMap(self.visualizer)
1718 except Exception as e:
1719 raise VisualizerError(f"Failed to plot depth map: {e}")
1720
1721 # Annotation and Depth Image Methods (helios-core 1.3.91)
1722
1723 def getDepthImage(self) -> Tuple[List[float], int, int]:
1724 """
1725 Get the distance from the camera to the nearest surface in each pixel of the current view.
1726
1727 The depth is the distance in world units from the camera to the visible surface,
1728 measured along the camera viewing direction, so geometry lying in a plane
1729 perpendicular to the viewing direction has one depth across the whole image. This is
1730 the same definition as the camera depth images of the radiation plug-in
1731 (:meth:`pyhelios.RadiationModel.writeDepthImageData`).
1732
1733 The frame is rendered as part of the call if needed, as for :meth:`printWindow`.
1734
1735 Returns:
1736 Tuple of ``(depth_pixels, width, height)``. ``depth_pixels`` is row-major with row 0
1737 at the TOP of the image (the orientation of an image saved by :meth:`printWindow`).
1738 Pixels in which no geometry is visible hold -1. ``width`` and ``height`` are the
1739 framebuffer dimensions (see :meth:`getFramebufferSize`).
1740
1741 Raises:
1742 VisualizerError: If the operation fails
1743
1744 Note:
1745 - Geometry nearer than the near clipping plane (5% of the camera-to-look-at distance,
1746 and no nearer than 0.1) is not visible and is absent here.
1747 - Semi-transparent primitives (color alpha < 1) are treated as opaque.
1748 - Lines, points, the sky dome and 2D overlays (colorbar, watermark, text) have no
1749 depth; a pixel showing only these holds -1.
1750
1751 Example:
1752 >>> depth, width, height = visualizer.getDepthImage()
1753 >>> center = depth[(height // 2) * width + width // 2]
1754 """
1756 if not self.visualizer:
1757 raise VisualizerError("Visualizer not initialized")
1758 try:
1760 return visualizer_wrapper.get_depth_image(self.visualizer)
1761 except NotImplementedError:
1762 raise
1763 except Exception as e:
1764 raise VisualizerError(f"Failed to get depth image: {e}")
1765
1766 def getPixelUUIDs(self) -> Tuple[List[int], int, int]:
1767 """
1768 Get the Context primitive visible in each pixel of the current view.
1769
1770 This is the pixel-to-primitive map from which bounding boxes and segmentation masks are
1771 computed. It is exact: it does not depend on primitive colors, lighting, or the
1772 anti-aliasing the Visualizer was created with, and producing it does not modify the
1773 Context. The frame is rendered as part of the call if needed, as for :meth:`printWindow`.
1774
1775 Returns:
1776 Tuple of ``(pixel_UUIDs, width, height)``. Each element of ``pixel_UUIDs`` is the
1777 **UUID of the visible primitive plus one**; 0 means no Context primitive is visible
1778 (background, or geometry added directly to the Visualizer). Subtract 1 from a
1779 non-zero value to get the UUID. The map is row-major with row 0 at the TOP of the
1780 image, and is in the form the :mod:`pyhelios.annotation` functions accept.
1781 ``width`` and ``height`` are the framebuffer dimensions.
1782
1783 Raises:
1784 VisualizerError: If the operation fails
1785
1786 Note:
1787 - A primitive whose texture has a transparency channel is visible only where its
1788 texture is opaque.
1789 - Semi-transparent primitives (color alpha < 1) are treated as opaque.
1790 - Lines, points, the sky dome and 2D overlays are not part of the map and do not
1791 hide primitives behind them.
1792
1793 Example:
1794 >>> pixels, width, height = visualizer.getPixelUUIDs()
1795 >>> visible = {p - 1 for p in pixels if p != 0}
1796 """
1798 if not self.visualizer:
1799 raise VisualizerError("Visualizer not initialized")
1800 try:
1802 return visualizer_wrapper.get_pixel_uuids(self.visualizer)
1803 except NotImplementedError:
1804 raise
1805 except Exception as e:
1806 raise VisualizerError(f"Failed to get pixel UUIDs: {e}")
1807
1808 @staticmethod
1809 def _normalize_annotation_labels(primitive_data_labels, object_data_labels, object_class_ids):
1810 """Validate the label arguments shared by the annotation writers.
1811
1812 Returns:
1813 Tuple of (labels list, class ID list, use_object_data flag)
1814 """
1815 if primitive_data_labels is not None and object_data_labels is not None:
1816 raise ValueError("Cannot specify both primitive_data_labels and object_data_labels")
1817 if primitive_data_labels is None and object_data_labels is None:
1818 raise ValueError("Must specify either primitive_data_labels or object_data_labels")
1819
1820 use_object_data = object_data_labels is not None
1821 labels = object_data_labels if use_object_data else primitive_data_labels
1822 name = "object_data_labels" if use_object_data else "primitive_data_labels"
1823
1824 if isinstance(labels, str):
1825 if isinstance(object_class_ids, bool) or not isinstance(object_class_ids, _INT_TYPE):
1826 raise TypeError(f"For a single data label, object_class_ids must be an integer, "
1827 f"got {type(object_class_ids).__name__}")
1828 labels = [labels]
1829 class_ids = [object_class_ids]
1830 elif isinstance(labels, list):
1831 if not isinstance(object_class_ids, list):
1832 raise TypeError(f"For multiple data labels, object_class_ids must be a list, "
1833 f"got {type(object_class_ids).__name__}")
1834 if len(labels) != len(object_class_ids):
1835 raise ValueError(f"{name} and object_class_ids must have the same length")
1836 if not labels:
1837 raise ValueError(f"{name} cannot be empty")
1838 class_ids = object_class_ids
1839 else:
1840 raise TypeError(f"{name} must be a string or list of strings, got {type(labels).__name__}")
1841
1842 if not all(isinstance(label, str) and label.strip() for label in labels):
1843 raise TypeError("All data labels must be non-empty strings")
1844 if not all(isinstance(cid, _INT_TYPE) and not isinstance(cid, bool) for cid in class_ids):
1845 raise TypeError("All object class IDs must be integers")
1846 if any(cid < 0 for cid in class_ids):
1847 raise ValueError("Object class IDs must be non-negative")
1848
1849 return list(labels), [int(cid) for cid in class_ids], use_object_data
1850
1851 def writeImageBoundingBoxes(self, primitive_data_labels=None, object_data_labels=None,
1852 object_class_ids=None, image_file: str = "",
1853 classes_txt_file: str = "classes.txt",
1854 image_path: str = "./") -> None:
1855 """
1856 Write bounding boxes for the current view in Ultralytics YOLO format.
1857
1858 Objects are identified by a primitive data label (or, with ``object_data_labels``, an
1859 object data label of each primitive's parent object) of type ``uint`` or ``int``: all
1860 visible primitives carrying the same value of the label form one object, and one box is
1861 written per distinct value. Primitives without the label are not annotated. This is the
1862 labeling convention of :meth:`pyhelios.RadiationModel.writeImageBoundingBoxes`, and the
1863 boxes are computed by the same native routine.
1864
1865 A box encloses whole every pixel in which the object is visible, so even an object
1866 visible in a single pixel has a box. The boxes are written to a text file named after
1867 the image with its extension replaced by ".txt", in ``image_path``, and describe the view
1868 at the time of the call. Save the image itself with :meth:`printWindow`, and check the
1869 output with :meth:`displayImageWithBoundingBoxes`.
1870
1871 Supply exactly one of ``primitive_data_labels`` or ``object_data_labels``.
1872
1873 Args:
1874 primitive_data_labels: Primitive data label (str) or list of labels
1875 object_data_labels: Object data label (str) or list of labels
1876 object_class_ids: Class ID (int) for a single label, or a list of class IDs in the
1877 same order as the labels
1878 image_file: Name of the image these boxes annotate. Only its stem is used, to name
1879 the output file.
1880 classes_txt_file: Name of the class file to write within ``image_path``. Each line
1881 holds a class ID and the data label it was given for.
1882 image_path: Directory in which the output files are written (default: current
1883 working directory)
1884
1885 Raises:
1886 ValueError: If both or neither label arguments are given, lengths differ, or a class
1887 ID is negative
1888 TypeError: If an argument has the wrong type
1889 VisualizerError: If writing fails (e.g. the output directory cannot be written)
1890
1891 Note:
1892 Only ``uint`` and ``int`` label data is read. A primitive whose label has another
1893 type is treated as unlabeled and gets no box; this is not an error.
1894
1895 Example:
1896 >>> context.setPrimitiveDataUInt(leaf_uuids, "leaf_id", 1)
1897 >>> visualizer.printWindow("scene.jpeg")
1898 >>> visualizer.writeImageBoundingBoxes(primitive_data_labels="leaf_id",
1899 ... object_class_ids=0, image_file="scene.jpeg")
1900 """
1902 if not self.visualizer:
1903 raise VisualizerError("Visualizer not initialized")
1904 labels, class_ids, use_object_data = self._normalize_annotation_labels(
1905 primitive_data_labels, object_data_labels, object_class_ids)
1906 if not isinstance(image_file, str) or not image_file.strip():
1907 raise TypeError("Image file must be a non-empty string")
1908 if not isinstance(classes_txt_file, str) or not classes_txt_file.strip():
1909 raise TypeError("Classes txt file must be a non-empty string")
1910 if not isinstance(image_path, str) or not image_path.strip():
1911 raise TypeError("Image path must be a non-empty string")
1912
1913 resolved_path = _resolve_user_path(image_path)
1914 try:
1916 visualizer_wrapper.write_image_bounding_boxes(
1917 self.visualizer, labels, class_ids, image_file, classes_txt_file,
1918 resolved_path, use_object_data)
1919 except NotImplementedError:
1920 raise
1921 except Exception as e:
1922 raise VisualizerError(f"Failed to write image bounding boxes: {e}")
1923
1924 def writeImageSegmentationMasks(self, primitive_data_labels=None, object_data_labels=None,
1925 object_class_ids=None, json_filename: str = "",
1926 image_file: str = "",
1927 data_attribute_labels: Optional[List[str]] = None,
1928 append_file: bool = False) -> None:
1929 """
1930 Write segmentation masks for the current view in COCO JSON format.
1931
1932 Objects are identified by a primitive data label (or, with ``object_data_labels``, an
1933 object data label) of type ``uint`` or ``int``, as for :meth:`writeImageBoundingBoxes`.
1934 Each connected region of pixels sharing one value of the label becomes one annotation
1935 holding the outline of that region, so an object split in two by something in front of
1936 it yields two annotations. Parts hidden behind other geometry are not included. A region
1937 of only one or two pixels is too small to outline and is not written. This is the
1938 convention of :meth:`pyhelios.RadiationModel.writeImageSegmentationMasks`.
1939
1940 Supply exactly one of ``primitive_data_labels`` or ``object_data_labels``. The output
1941 can be checked with :meth:`displayImageWithSegmentationMasks`.
1942
1943 Args:
1944 primitive_data_labels: Primitive data label (str) or list of labels
1945 object_data_labels: Object data label (str) or list of labels
1946 object_class_ids: Class ID (COCO category ID) for a single label, or a list in the
1947 same order as the labels
1948 json_filename: Output JSON file, which may include a path. ".json" is appended if
1949 it has no such extension.
1950 image_file: Image file these masks annotate, which must already exist (see
1951 :meth:`printWindow`)
1952 data_attribute_labels: Optional primitive or object data labels of type int, uint,
1953 float or double whose mean over the pixels of each mask is written to that
1954 annotation's "attributes". A label that exists as both is read from primitive
1955 data. Labels that do not exist are ignored.
1956 append_file: If True, annotations are added to an existing JSON file so one file can
1957 describe many images. If False, a new file is written.
1958
1959 Raises:
1960 ValueError: If both or neither label arguments are given, lengths differ, or a class
1961 ID is negative
1962 TypeError: If an argument has the wrong type
1963 VisualizerError: If writing fails (e.g. the image file does not exist)
1964
1965 Example:
1966 >>> visualizer.printWindow("scene.jpeg")
1967 >>> visualizer.writeImageSegmentationMasks(primitive_data_labels="leaf_id",
1968 ... object_class_ids=0,
1969 ... json_filename="annotations.json",
1970 ... image_file="scene.jpeg")
1971 """
1973 if not self.visualizer:
1974 raise VisualizerError("Visualizer not initialized")
1975 labels, class_ids, use_object_data = self._normalize_annotation_labels(
1976 primitive_data_labels, object_data_labels, object_class_ids)
1977 if not isinstance(json_filename, str) or not json_filename.strip():
1978 raise TypeError("JSON filename must be a non-empty string")
1979 if not isinstance(image_file, str) or not image_file.strip():
1980 raise TypeError("Image file must be a non-empty string")
1981 if data_attribute_labels is None:
1982 data_attribute_labels = []
1983 if not isinstance(data_attribute_labels, list) or not all(
1984 isinstance(label, str) and label.strip() for label in data_attribute_labels):
1985 raise TypeError("data_attribute_labels must be a list of non-empty strings")
1986 if not isinstance(append_file, bool):
1987 raise TypeError(f"append_file must be a bool, got {type(append_file).__name__}")
1988
1989 resolved_json = _resolve_user_path(json_filename)
1990 resolved_image = _resolve_user_path(image_file)
1991 try:
1993 visualizer_wrapper.write_image_segmentation_masks(
1994 self.visualizer, labels, class_ids, resolved_json, resolved_image,
1995 data_attribute_labels, append_file, use_object_data)
1996 except NotImplementedError:
1997 raise
1998 except Exception as e:
1999 raise VisualizerError(f"Failed to write image segmentation masks: {e}")
2000
2001 def _write_depth_output(self, writer, filename: str, description: str, *args) -> None:
2002 """Validate a depth-output filename and call the given wrapper writer."""
2004 if not self.visualizer:
2005 raise VisualizerError("Visualizer not initialized")
2006 if not isinstance(filename, str) or not filename.strip():
2007 raise ValueError("Filename must be a non-empty string")
2008 resolved = _resolve_user_path(filename)
2009 try:
2011 writer(self.visualizer, resolved, *args)
2012 except NotImplementedError:
2013 raise
2014 except Exception as e:
2015 raise VisualizerError(f"Failed to write {description}: {e}")
2016
2017 def writeDepthImageData(self, filename: str) -> None:
2018 """
2019 Write the depth image of the current view to a text file.
2020
2021 The file holds one line per image row, starting from the top of the image, with the
2022 depth of each pixel in that row separated by spaces. See :meth:`getDepthImage` for the
2023 definition of depth. Pixels in which no geometry is visible are written as -1.
2024
2025 Args:
2026 filename: Text file to write, which may include a path
2027
2028 Raises:
2029 ValueError: If filename is empty
2030 VisualizerError: If writing fails
2031 """
2032 self._write_depth_output(visualizer_wrapper.write_depth_image_data, filename, "depth image data")
2033
2034 def writeDepthImageDataEXR(self, filename: str) -> None:
2035 """
2036 Write the depth image of the current view to an EXR file with lossless float compression.
2037
2038 See :meth:`getDepthImage` for the definition of depth. Pixels in which no geometry is
2039 visible are written as -1.
2040
2041 Args:
2042 filename: EXR file to write, which may include a path
2043
2044 Raises:
2045 ValueError: If filename is empty
2046 VisualizerError: If writing fails
2047 """
2048 self._write_depth_output(visualizer_wrapper.write_depth_image_data_exr, filename, "EXR depth image")
2049
2050 def writeNormDepthImage(self, filename: str, max_depth: float) -> None:
2051 """
2052 Write the depth image of the current view as a grayscale JPEG, normalized between the
2053 nearest depth and a maximum depth.
2054
2055 The nearest visible surface is white and anything at or beyond ``max_depth`` is black.
2056 Pixels in which no geometry is visible are treated as lying at ``max_depth``.
2057
2058 Args:
2059 filename: JPEG file to write, which may include a path
2060 max_depth: Depth that maps to black, in world units (e.g. the far side of the scene).
2061 Must be greater than zero.
2062
2063 Raises:
2064 ValueError: If filename is empty or max_depth is not positive
2065 VisualizerError: If writing fails
2066 """
2067 if isinstance(max_depth, bool) or not isinstance(max_depth, _NUMERIC_TYPES):
2068 raise ValueError(f"max_depth must be a number, got {type(max_depth).__name__}")
2069 if not max_depth > 0:
2070 raise ValueError(f"max_depth must be greater than zero, got {max_depth}")
2071 self._write_depth_output(visualizer_wrapper.write_norm_depth_image, filename,
2072 "normalized depth image", float(max_depth))
2073
2074 # Geometry Management Methods
2075
2076 def clearGeometry(self) -> None:
2077 """
2078 Clear all geometry from visualizer.
2079
2080 Warning:
2081 Do NOT use this to refresh a scene between animation frames. The
2082 visualizer syncs from the Context incrementally using the Context's
2083 dirty flags. Clearing discards the visualizer's geometry while the
2084 Context primitives remain marked clean, so the next rebuild pulls in
2085 nothing and subsequent frames render empty -- printWindow() then
2086 silently writes no file.
2087
2088 To animate a changing Context, just call buildContextGeometry()
2089 and plotUpdate() each frame without clearing; additions, deletions
2090 and modifications are all picked up automatically.
2091
2092 Raises:
2093 VisualizerError: If operation fails
2094 """
2095 if not self.visualizer:
2096 raise VisualizerError("Visualizer not initialized")
2097
2098 try:
2099 helios_lib.clearGeometry(self.visualizer)
2100 except Exception as e:
2101 raise VisualizerError(f"Failed to clear geometry: {e}")
2102
2103 def clearContextGeometry(self) -> None:
2104 """
2105 Clear context geometry from visualizer.
2106
2107 Raises:
2108 VisualizerError: If operation fails
2109 """
2110 if not self.visualizer:
2111 raise VisualizerError("Visualizer not initialized")
2112
2113 try:
2114 helios_lib.clearContextGeometry(self.visualizer)
2115 except Exception as e:
2116 raise VisualizerError(f"Failed to clear context geometry: {e}")
2117
2118 def deleteGeometry(self, geometry_id: int) -> None:
2119 """
2120 Delete specific geometry by ID.
2121
2122 Args:
2123 geometry_id: ID of geometry to delete
2124
2125 Raises:
2126 ValueError: If geometry ID is invalid
2127 VisualizerError: If operation fails
2128 """
2129 if not self.visualizer:
2130 raise VisualizerError("Visualizer not initialized")
2131
2132 if not isinstance(geometry_id, _INT_TYPE) or geometry_id < 0:
2133 raise ValueError("Geometry ID must be a non-negative integer")
2134
2135 try:
2136 helios_lib.deleteGeometry(self.visualizer, geometry_id)
2137 except Exception as e:
2138 raise VisualizerError(f"Failed to delete geometry {geometry_id}: {e}")
2139
2140 def updateContextPrimitiveColors(self) -> None:
2141 """
2142 Update context primitive colors.
2143
2144 Raises:
2145 VisualizerError: If operation fails
2146 """
2147 if not self.visualizer:
2148 raise VisualizerError("Visualizer not initialized")
2149
2150 try:
2151 helios_lib.updateContextPrimitiveColors(self.visualizer)
2152 except Exception as e:
2153 raise VisualizerError(f"Failed to update context primitive colors: {e}")
2154
2155 # Geometry Vertex Manipulation Methods (v1.3.53+)
2156
2157 def getGeometryVertices(self, geometry_id: int) -> List[vec3]:
2158 """
2159 Get vertices of a geometry primitive.
2160
2161 Args:
2162 geometry_id: Unique identifier of the geometry primitive
2163
2164 Returns:
2165 List of vertices as vec3 objects
2166
2167 Raises:
2168 ValueError: If geometry ID is invalid
2169 VisualizerError: If operation fails
2170
2171 Example:
2172 >>> # Get vertices of a specific geometry
2173 >>> vertices = visualizer.getGeometryVertices(geometry_id)
2174 >>> for vertex in vertices:
2175 ... print(f"Vertex: ({vertex.x}, {vertex.y}, {vertex.z})")
2176 """
2177 if not self.visualizer:
2178 raise VisualizerError("Visualizer not initialized")
2179
2180 if not isinstance(geometry_id, _INT_TYPE) or geometry_id < 0:
2181 raise ValueError("Geometry ID must be a non-negative integer")
2182
2183 try:
2184 vertices_list = visualizer_wrapper.get_geometry_vertices(self.visualizer, geometry_id)
2185 # Convert [[x,y,z], ...] to [vec3(), ...]
2186 return [vec3(v[0], v[1], v[2]) for v in vertices_list]
2187 except Exception as e:
2188 raise VisualizerError(f"Failed to get geometry vertices: {e}")
2189
2190 def setGeometryVertices(self, geometry_id: int, vertices: List[vec3]) -> None:
2191 """
2192 Set vertices of a geometry primitive.
2193
2194 This allows dynamic modification of geometry shapes during visualization.
2195 Useful for animating geometry or adjusting shapes based on simulation results.
2196
2197 Args:
2198 geometry_id: Unique identifier of the geometry primitive
2199 vertices: List of new vertices as vec3 objects
2200
2201 Raises:
2202 ValueError: If parameters are invalid
2203 VisualizerError: If operation fails
2204
2205 Example:
2206 >>> # Modify vertices of an existing geometry
2207 >>> vertices = visualizer.getGeometryVertices(geometry_id)
2208 >>> # Scale all vertices by 2x
2209 >>> scaled_vertices = [vec3(v.x*2, v.y*2, v.z*2) for v in vertices]
2210 >>> visualizer.setGeometryVertices(geometry_id, scaled_vertices)
2211 """
2212 if not self.visualizer:
2213 raise VisualizerError("Visualizer not initialized")
2214
2215 if not isinstance(geometry_id, _INT_TYPE) or geometry_id < 0:
2216 raise ValueError("Geometry ID must be a non-negative integer")
2217
2218 if not vertices or not isinstance(vertices, (list, tuple)):
2219 raise ValueError("Vertices must be a non-empty list")
2220
2221 if not all(isinstance(v, vec3) for v in vertices):
2222 raise ValueError("All vertices must be vec3 objects")
2223
2224 try:
2225 visualizer_wrapper.set_geometry_vertices(self.visualizer, geometry_id, vertices)
2226 logger.debug(f"Set {len(vertices)} vertices for geometry {geometry_id}")
2227 except Exception as e:
2228 raise VisualizerError(f"Failed to set geometry vertices: {e}")
2229
2230 # Coordinate Axes and Grid Methods
2231
2232 def addCoordinateAxes(self) -> None:
2233 """
2234 Add coordinate axes at origin with unit length.
2235
2236 Raises:
2237 VisualizerError: If operation fails
2238 """
2239 if not self.visualizer:
2240 raise VisualizerError("Visualizer not initialized")
2241
2242 try:
2243 helios_lib.addCoordinateAxes(self.visualizer)
2244 except Exception as e:
2245 raise VisualizerError(f"Failed to add coordinate axes: {e}")
2246
2247 def addCoordinateAxesCustom(self, origin: vec3, length: vec3, sign: str = "both") -> None:
2248 """
2249 Add coordinate axes with custom properties.
2250
2251 Args:
2252 origin: Axes origin position
2253 length: Axes length in each direction
2254 sign: Axis direction ("both" or "positive")
2255
2256 Raises:
2257 ValueError: If parameters are invalid
2258 VisualizerError: If operation fails
2259 """
2260 if not self.visualizer:
2261 raise VisualizerError("Visualizer not initialized")
2262
2263 if not isinstance(origin, vec3):
2264 raise ValueError("Origin must be a vec3")
2265 if not isinstance(length, vec3):
2266 raise ValueError("Length must be a vec3")
2267 if not isinstance(sign, str) or sign not in ["both", "positive"]:
2268 raise ValueError("Sign must be 'both' or 'positive'")
2269
2270 try:
2271 origin_array = (ctypes.c_float * 3)(origin.x, origin.y, origin.z)
2272 length_array = (ctypes.c_float * 3)(length.x, length.y, length.z)
2273 helios_lib.addCoordinateAxesCustom(self.visualizer, origin_array, length_array, sign.encode('utf-8'))
2274 except Exception as e:
2275 raise VisualizerError(f"Failed to add custom coordinate axes: {e}")
2276
2277 def disableCoordinateAxes(self) -> None:
2278 """
2279 Remove coordinate axes.
2280
2281 Raises:
2282 VisualizerError: If operation fails
2283 """
2284 if not self.visualizer:
2285 raise VisualizerError("Visualizer not initialized")
2286
2287 try:
2288 helios_lib.disableCoordinateAxes(self.visualizer)
2289 except Exception as e:
2290 raise VisualizerError(f"Failed to disable coordinate axes: {e}")
2291
2292 def addGridWireFrame(self, center: vec3, size: vec3, subdivisions: List[int]) -> None:
2293 """
2294 Add grid wireframe.
2295
2296 Args:
2297 center: Grid center position
2298 size: Grid size in each direction
2299 subdivisions: Grid subdivisions [x, y, z]
2300
2301 Raises:
2302 ValueError: If parameters are invalid
2303 VisualizerError: If operation fails
2304 """
2305 if not self.visualizer:
2306 raise VisualizerError("Visualizer not initialized")
2307
2308 if not isinstance(center, vec3):
2309 raise ValueError("Center must be a vec3")
2310 if not isinstance(size, vec3):
2311 raise ValueError("Size must be a vec3")
2312 if not isinstance(subdivisions, (list, tuple)) or len(subdivisions) != 3:
2313 raise ValueError("Subdivisions must be a list of 3 integers")
2314 if not all(isinstance(s, _INT_TYPE) and s > 0 for s in subdivisions):
2315 raise ValueError("All subdivisions must be positive integers")
2316
2317 try:
2318 center_array = (ctypes.c_float * 3)(center.x, center.y, center.z)
2319 size_array = (ctypes.c_float * 3)(size.x, size.y, size.z)
2320 subdiv_array = (ctypes.c_int * 3)(*subdivisions)
2321 helios_lib.addGridWireFrame(self.visualizer, center_array, size_array, subdiv_array)
2322 except Exception as e:
2323 raise VisualizerError(f"Failed to add grid wireframe: {e}")
2324
2325 # Colorbar Control Methods
2326
2327 def enableColorbar(self) -> None:
2328 """
2329 Enable colorbar.
2330
2331 Raises:
2332 VisualizerError: If operation fails
2333 """
2334 if not self.visualizer:
2335 raise VisualizerError("Visualizer not initialized")
2336
2337 try:
2338 helios_lib.enableColorbar(self.visualizer)
2339 except Exception as e:
2340 raise VisualizerError(f"Failed to enable colorbar: {e}")
2341
2342 def disableColorbar(self) -> None:
2343 """
2344 Disable colorbar.
2345
2346 Raises:
2347 VisualizerError: If operation fails
2348 """
2349 if not self.visualizer:
2350 raise VisualizerError("Visualizer not initialized")
2351
2352 try:
2353 helios_lib.disableColorbar(self.visualizer)
2354 except Exception as e:
2355 raise VisualizerError(f"Failed to disable colorbar: {e}")
2356
2357 def setColorbarPosition(self, position: vec3) -> None:
2358 """
2359 Set colorbar position.
2360
2361 Args:
2362 position: Colorbar position
2363
2364 Raises:
2365 ValueError: If position is invalid
2366 VisualizerError: If operation fails
2367 """
2368 if not self.visualizer:
2369 raise VisualizerError("Visualizer not initialized")
2370
2371 if not isinstance(position, vec3):
2372 raise ValueError("Position must be a vec3")
2373
2374 try:
2375 pos_array = (ctypes.c_float * 3)(position.x, position.y, position.z)
2376 helios_lib.setColorbarPosition(self.visualizer, pos_array)
2377 except Exception as e:
2378 raise VisualizerError(f"Failed to set colorbar position: {e}")
2379
2380 def setColorbarSize(self, width: float, height: float) -> None:
2381 """
2382 Set colorbar size.
2384 Args:
2385 width: Colorbar width
2386 height: Colorbar height
2387
2388 Raises:
2389 ValueError: If size is invalid
2390 VisualizerError: If operation fails
2391 """
2392 if not self.visualizer:
2393 raise VisualizerError("Visualizer not initialized")
2394
2395 if not isinstance(width, _NUMERIC_TYPES) or width <= 0:
2396 raise ValueError("Width must be a positive number")
2397 if not isinstance(height, _NUMERIC_TYPES) or height <= 0:
2398 raise ValueError("Height must be a positive number")
2399
2400 try:
2401 size_array = (ctypes.c_float * 2)(float(width), float(height))
2402 helios_lib.setColorbarSize(self.visualizer, size_array)
2403 except Exception as e:
2404 raise VisualizerError(f"Failed to set colorbar size: {e}")
2405
2406 def setColorbarRange(self, min_val: float, max_val: float) -> None:
2407 """
2408 Set colorbar range.
2409
2410 Setting a range explicitly stops the colorbar from auto-ranging over the data, including
2411 for the degenerate range ``setColorbarRange(0, 0)``.
2412
2413 Args:
2414 min_val: Minimum value
2415 max_val: Maximum value. Must be greater than or equal to ``min_val``; helios ignores an
2416 inverted range.
2417
2418 Raises:
2419 ValueError: If range is invalid
2420 VisualizerError: If operation fails
2421 """
2422 if not self.visualizer:
2423 raise VisualizerError("Visualizer not initialized")
2424
2425 if not isinstance(min_val, _NUMERIC_TYPES):
2426 raise ValueError("Minimum value must be numeric")
2427 if not isinstance(max_val, _NUMERIC_TYPES):
2428 raise ValueError("Maximum value must be numeric")
2429 if min_val > max_val:
2430 raise ValueError("Minimum value must not be greater than maximum value")
2431
2432 try:
2433 helios_lib.setColorbarRange(self.visualizer, float(min_val), float(max_val))
2434 except Exception as e:
2435 raise VisualizerError(f"Failed to set colorbar range: {e}")
2436
2437 def setColorbarTicks(self, ticks: List[float]) -> None:
2438 """
2439 Set colorbar tick marks.
2440
2441 Args:
2442 ticks: List of tick values
2443
2444 Note:
2445 If a tick value falls outside the colorbar range, the range is automatically expanded
2446 to fit it. Because the colormap limits follow the colorbar range, this changes the
2447 colors shown as well as the labels. To keep an explicit range authoritative, call
2448 :meth:`setColorbarRange` after :meth:`setColorbarTicks`.
2449
2450 Raises:
2451 ValueError: If ticks are invalid
2452 VisualizerError: If operation fails
2453 """
2454 if not self.visualizer:
2455 raise VisualizerError("Visualizer not initialized")
2456
2457 if not isinstance(ticks, (list, tuple)):
2458 raise ValueError("Ticks must be a list or tuple")
2459 if not all(isinstance(t, _NUMERIC_TYPES) for t in ticks):
2460 raise ValueError("All tick values must be numeric")
2461
2462 try:
2463 if ticks:
2464 ticks_array = (ctypes.c_float * len(ticks))(*ticks)
2465 helios_lib.setColorbarTicks(self.visualizer, ticks_array, len(ticks))
2466 else:
2467 helios_lib.setColorbarTicks(self.visualizer, None, 0)
2468 except Exception as e:
2469 raise VisualizerError(f"Failed to set colorbar ticks: {e}")
2470
2471 def setColorbarTitle(self, title: str) -> None:
2472 """
2473 Set colorbar title.
2474
2475 Args:
2476 title: Colorbar title
2477
2478 Raises:
2479 ValueError: If title is invalid
2480 VisualizerError: If operation fails
2481 """
2482 if not self.visualizer:
2483 raise VisualizerError("Visualizer not initialized")
2484
2485 if not isinstance(title, str):
2486 raise ValueError("Title must be a string")
2487
2488 try:
2489 helios_lib.setColorbarTitle(self.visualizer, title.encode('utf-8'))
2490 except Exception as e:
2491 raise VisualizerError(f"Failed to set colorbar title: {e}")
2492
2493 def setColorbarFontColor(self, color: RGBcolor) -> None:
2494 """
2495 Set colorbar font color.
2496
2497 Args:
2498 color: Font color
2499
2500 Raises:
2501 ValueError: If color is invalid
2502 VisualizerError: If operation fails
2503 """
2504 if not self.visualizer:
2505 raise VisualizerError("Visualizer not initialized")
2506
2507 if not isinstance(color, RGBcolor):
2508 raise ValueError("Color must be an RGBcolor")
2509
2510 try:
2511 color_array = (ctypes.c_float * 3)(color.r, color.g, color.b)
2512 helios_lib.setColorbarFontColor(self.visualizer, color_array)
2513 except Exception as e:
2514 raise VisualizerError(f"Failed to set colorbar font color: {e}")
2515
2516 def setColorbarFontSize(self, font_size: int) -> None:
2517 """
2518 Set colorbar font size.
2520 Args:
2521 font_size: Font size
2522
2523 Raises:
2524 ValueError: If font size is invalid
2525 VisualizerError: If operation fails
2526 """
2527 if not self.visualizer:
2528 raise VisualizerError("Visualizer not initialized")
2529
2530 if not isinstance(font_size, _INT_TYPE) or font_size <= 0:
2531 raise ValueError("Font size must be a positive integer")
2532
2533 try:
2534 helios_lib.setColorbarFontSize(self.visualizer, font_size)
2535 except Exception as e:
2536 raise VisualizerError(f"Failed to set colorbar font size: {e}")
2537
2538 # Colormap Methods
2539
2540 def setColormap(self, colormap: Union[int, str]) -> None:
2541 """
2542 Set predefined colormap.
2543
2544 Args:
2545 colormap: Colormap ID (one of the ``COLORMAP_*`` class constants: 0-5 or 7-9) or
2546 name ("HOT", "COOL", "RAINBOW", "LAVA", "PARULA", "GRAY", "LINES", "ALGAE",
2547 "GREEN"). ID 6 is the custom colormap, which is set with
2548 :meth:`setCustomColormap`. Each predefined colormap is identical to the
2549 Context colormap of the same name (see
2550 :meth:`pyhelios.Context.Context.getColormapControlPoints`).
2551
2552 Raises:
2553 ValueError: If colormap is invalid
2554 VisualizerError: If operation fails
2555 """
2556 if not self.visualizer:
2557 raise VisualizerError("Visualizer not initialized")
2558
2559 colormap_map = {
2560 "HOT": self.COLORMAP_HOT, "COOL": self.COLORMAP_COOL, "RAINBOW": self.COLORMAP_RAINBOW,
2561 "LAVA": self.COLORMAP_LAVA, "PARULA": self.COLORMAP_PARULA, "GRAY": self.COLORMAP_GRAY,
2562 "LINES": self.COLORMAP_LINES, "ALGAE": self.COLORMAP_ALGAE, "GREEN": self.COLORMAP_GREEN,
2563 }
2564
2565 if isinstance(colormap, str):
2566 if colormap.upper() not in colormap_map:
2567 raise ValueError(f"Unknown colormap name: {colormap}. Valid names: {', '.join(colormap_map)}")
2568 colormap_id = colormap_map[colormap.upper()]
2569 elif isinstance(colormap, _INT_TYPE):
2570 if colormap not in colormap_map.values():
2571 raise ValueError(f"Colormap ID must be one of {sorted(colormap_map.values())} "
2572 "(6 is the custom colormap; use setCustomColormap)")
2573 colormap_id = colormap
2574 else:
2575 raise ValueError("Colormap must be integer ID or string name")
2576
2577 try:
2578 helios_lib.setColormap(self.visualizer, colormap_id)
2579 except Exception as e:
2580 raise VisualizerError(f"Failed to set colormap: {e}")
2581
2582 def setCustomColormap(self, colors: List[RGBcolor], divisions: List[float]) -> None:
2583 """
2584 Set custom colormap.
2585
2586 Args:
2587 colors: List of RGB colors
2588 divisions: List of division points (same length as colors)
2589
2590 Raises:
2591 ValueError: If parameters are invalid
2592 VisualizerError: If operation fails
2593 """
2594 if not self.visualizer:
2595 raise VisualizerError("Visualizer not initialized")
2596
2597 if not isinstance(colors, (list, tuple)) or not colors:
2598 raise ValueError("Colors must be a non-empty list")
2599 if not isinstance(divisions, (list, tuple)) or not divisions:
2600 raise ValueError("Divisions must be a non-empty list")
2601 if len(colors) != len(divisions):
2602 raise ValueError("Colors and divisions must have the same length")
2603
2604 if not all(isinstance(c, RGBcolor) for c in colors):
2605 raise ValueError("All colors must be RGBcolor objects")
2606 if not all(isinstance(d, _NUMERIC_TYPES) for d in divisions):
2607 raise ValueError("All divisions must be numeric")
2608
2609 try:
2610 # Flatten colors to RGB array
2611 color_array = (ctypes.c_float * (len(colors) * 3))()
2612 for i, color in enumerate(colors):
2613 color_array[i*3] = color.r
2614 color_array[i*3+1] = color.g
2615 color_array[i*3+2] = color.b
2616
2617 divisions_array = (ctypes.c_float * len(divisions))(*divisions)
2618
2619 helios_lib.setCustomColormap(self.visualizer, color_array, divisions_array, len(colors))
2620 except Exception as e:
2621 raise VisualizerError(f"Failed to set custom colormap: {e}")
2622
2623 # Advanced Coloring Methods
2624
2625 def colorContextPrimitivesByObjectData(self, data_name: str, obj_ids: Optional[List[int]] = None) -> None:
2626 """
2627 Color context primitives by object data.
2628
2629 Args:
2630 data_name: Name of object data to use for coloring
2631 obj_ids: Optional list of object IDs to color (None for all)
2632
2633 Raises:
2634 ValueError: If parameters are invalid
2635 VisualizerError: If operation fails
2636 """
2637 if not self.visualizer:
2638 raise VisualizerError("Visualizer not initialized")
2639
2640 if not isinstance(data_name, str) or not data_name.strip():
2641 raise ValueError("Data name must be a non-empty string")
2642
2643 try:
2644 if obj_ids is None:
2645 helios_lib.colorContextPrimitivesByObjectData(self.visualizer, data_name.encode('utf-8'))
2646 else:
2647 if not isinstance(obj_ids, (list, tuple)):
2648 raise ValueError("Object IDs must be a list or tuple")
2649 if not all(isinstance(oid, _INT_TYPE) and oid >= 0 for oid in obj_ids):
2650 raise ValueError("All object IDs must be non-negative integers")
2651
2652 if obj_ids:
2653 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
2654 helios_lib.colorContextPrimitivesByObjectDataIDs(self.visualizer, data_name.encode('utf-8'), obj_ids_array, len(obj_ids))
2655 else:
2656 helios_lib.colorContextPrimitivesByObjectDataIDs(self.visualizer, data_name.encode('utf-8'), None, 0)
2657 except Exception as e:
2658 raise VisualizerError(f"Failed to color primitives by object data '{data_name}': {e}")
2659
2660 def colorContextPrimitivesRandomly(self, uuids: Optional[List[int]] = None) -> None:
2661 """
2662 Color context primitives randomly.
2663
2664 Args:
2665 uuids: Optional list of primitive UUIDs to color (None for all)
2666
2667 Raises:
2668 ValueError: If UUIDs are invalid
2669 VisualizerError: If operation fails
2670 """
2671 if not self.visualizer:
2672 raise VisualizerError("Visualizer not initialized")
2673
2674 try:
2675 if uuids is None:
2676 helios_lib.colorContextPrimitivesRandomly(self.visualizer, None, 0)
2677 else:
2678 if not isinstance(uuids, (list, tuple)):
2679 raise ValueError("UUIDs must be a list or tuple")
2680 if not all(isinstance(uuid, _INT_TYPE) and uuid >= 0 for uuid in uuids):
2681 raise ValueError("All UUIDs must be non-negative integers")
2682
2683 if uuids:
2684 uuid_array = (ctypes.c_uint * len(uuids))(*uuids)
2685 helios_lib.colorContextPrimitivesRandomly(self.visualizer, uuid_array, len(uuids))
2686 else:
2687 helios_lib.colorContextPrimitivesRandomly(self.visualizer, None, 0)
2688 except Exception as e:
2689 raise VisualizerError(f"Failed to color primitives randomly: {e}")
2690
2691 def colorContextObjectsRandomly(self, obj_ids: Optional[List[int]] = None) -> None:
2692 """
2693 Color context objects randomly.
2694
2695 Args:
2696 obj_ids: Optional list of object IDs to color (None for all)
2697
2698 Raises:
2699 ValueError: If object IDs are invalid
2700 VisualizerError: If operation fails
2701 """
2702 if not self.visualizer:
2703 raise VisualizerError("Visualizer not initialized")
2704
2705 try:
2706 if obj_ids is None:
2707 helios_lib.colorContextObjectsRandomly(self.visualizer, None, 0)
2708 else:
2709 if not isinstance(obj_ids, (list, tuple)):
2710 raise ValueError("Object IDs must be a list or tuple")
2711 if not all(isinstance(oid, _INT_TYPE) and oid >= 0 for oid in obj_ids):
2712 raise ValueError("All object IDs must be non-negative integers")
2713
2714 if obj_ids:
2715 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
2716 helios_lib.colorContextObjectsRandomly(self.visualizer, obj_ids_array, len(obj_ids))
2717 else:
2718 helios_lib.colorContextObjectsRandomly(self.visualizer, None, 0)
2719 except Exception as e:
2720 raise VisualizerError(f"Failed to color objects randomly: {e}")
2721
2722 def clearColor(self) -> None:
2723 """
2724 Clear primitive colors from previous coloring operations.
2725
2726 Raises:
2727 VisualizerError: If operation fails
2728 """
2729 if not self.visualizer:
2730 raise VisualizerError("Visualizer not initialized")
2731
2732 try:
2733 helios_lib.clearColor(self.visualizer)
2734 except Exception as e:
2735 raise VisualizerError(f"Failed to clear colors: {e}")
2736
2737 # Watermark Control Methods
2738
2739 def hideWatermark(self) -> None:
2740 """
2741 Hide Helios logo watermark.
2742
2743 Raises:
2744 VisualizerError: If operation fails
2745 """
2746 if not self.visualizer:
2747 raise VisualizerError("Visualizer not initialized")
2748
2749 try:
2750 helios_lib.hideWatermark(self.visualizer)
2751 except Exception as e:
2752 raise VisualizerError(f"Failed to hide watermark: {e}")
2753
2754 def showWatermark(self) -> None:
2755 """
2756 Show Helios logo watermark.
2757
2758 Raises:
2759 VisualizerError: If operation fails
2760 """
2761 if not self.visualizer:
2762 raise VisualizerError("Visualizer not initialized")
2763
2764 try:
2765 helios_lib.showWatermark(self.visualizer)
2766 except Exception as e:
2767 raise VisualizerError(f"Failed to show watermark: {e}")
2768
2769 def updateWatermark(self) -> None:
2770 """
2771 Update watermark geometry to match current window size.
2772
2773 Raises:
2774 VisualizerError: If operation fails
2775 """
2776 if not self.visualizer:
2777 raise VisualizerError("Visualizer not initialized")
2778
2779 try:
2780 helios_lib.updateWatermark(self.visualizer)
2781 except Exception as e:
2782 raise VisualizerError(f"Failed to update watermark: {e}")
2783
2784 # Navigation Gizmo Methods (v1.3.53+)
2785
2786 def hideNavigationGizmo(self) -> None:
2787 """
2788 Hide navigation gizmo (coordinate axes indicator in corner).
2789
2790 The navigation gizmo shows XYZ axes orientation and can be clicked
2791 to snap the camera to standard views (top, front, side, etc.).
2792
2793 Raises:
2794 VisualizerError: If operation fails
2795 """
2796 if not self.visualizer:
2797 raise VisualizerError("Visualizer not initialized")
2798
2799 try:
2801 visualizer_wrapper.hide_navigation_gizmo(self.visualizer)
2802 logger.debug("Navigation gizmo hidden")
2803 except Exception as e:
2804 raise VisualizerError(f"Failed to hide navigation gizmo: {e}")
2805
2806 def showNavigationGizmo(self) -> None:
2807 """
2808 Show navigation gizmo (coordinate axes indicator in corner).
2809
2810 The navigation gizmo shows XYZ axes orientation and can be clicked
2811 to snap the camera to standard views (top, front, side, etc.).
2812
2813 Note: Navigation gizmo is shown by default in v1.3.53+.
2814
2815 Raises:
2816 VisualizerError: If operation fails
2817 """
2818 if not self.visualizer:
2819 raise VisualizerError("Visualizer not initialized")
2820
2821 try:
2823 visualizer_wrapper.show_navigation_gizmo(self.visualizer)
2824 logger.debug("Navigation gizmo shown")
2825 except Exception as e:
2826 raise VisualizerError(f"Failed to show navigation gizmo: {e}")
2827
2828 # Performance and Utility Methods
2829
2830 def enableMessages(self) -> None:
2831 """
2832 Enable standard output from visualizer plugin.
2834 Raises:
2835 VisualizerError: If operation fails
2836 """
2837 if not self.visualizer:
2838 raise VisualizerError("Visualizer not initialized")
2839
2840 try:
2841 helios_lib.enableMessages(self.visualizer)
2842 except Exception as e:
2843 raise VisualizerError(f"Failed to enable messages: {e}")
2844
2845 def disableMessages(self) -> None:
2846 """
2847 Disable standard output from visualizer plugin.
2848
2849 Raises:
2850 VisualizerError: If operation fails
2851 """
2852 if not self.visualizer:
2853 raise VisualizerError("Visualizer not initialized")
2854
2855 try:
2856 helios_lib.disableMessages(self.visualizer)
2857 except Exception as e:
2858 raise VisualizerError(f"Failed to disable messages: {e}")
2859
2860 def plotOnce(self, get_keystrokes: bool = True) -> None:
2861 """
2862 Run one rendering loop.
2863
2864 Any geometry pending upload is transferred to the GPU before rendering, but unlike
2865 :meth:`plotUpdate` the Context geometry is not rebuilt. Call :meth:`buildContextGeometry`
2866 or :meth:`plotUpdate` if primitives have been added to or changed in the Context since the
2867 last render.
2868
2869 Args:
2870 get_keystrokes: Whether to process keystrokes
2871
2872 Raises:
2873 VisualizerError: If operation fails
2874 """
2875 if not self.visualizer:
2876 raise VisualizerError("Visualizer not initialized")
2877
2878 try:
2879 helios_lib.plotOnce(self.visualizer, get_keystrokes)
2880 except Exception as e:
2881 raise VisualizerError(f"Failed to run plot once: {e}")
2882
2883 def plotUpdateWithVisibility(self, hide_window: bool = False) -> None:
2884 """
2885 Update visualization with window visibility control.
2886
2887 Args:
2888 hide_window: Whether to hide the window during update
2889
2890 Raises:
2891 VisualizerError: If operation fails
2892 """
2894 if not self.visualizer:
2895 raise VisualizerError("Visualizer not initialized")
2896
2897 try:
2899 helios_lib.plotUpdateWithVisibility(self.visualizer, hide_window)
2900 except Exception as e:
2901 raise VisualizerError(f"Failed to update plot with visibility control: {e}")
2902
2903 # Point Culling and LOD Methods (v1.3.54+)
2904
2905 def setPointCullingEnabled(self, enabled: bool) -> None:
2906 """
2907 Enable or disable point cloud culling optimization.
2909 Point culling improves rendering performance for large point clouds by
2910 selectively rendering only points that are visible based on distance
2911 and density criteria.
2912
2913 Args:
2914 enabled: True to enable culling, False to disable (default: True)
2915
2916 Raises:
2917 ValueError: If enabled is not a boolean
2918 VisualizerError: If operation fails
2919
2920 Example:
2921 >>> with Visualizer(800, 600) as vis:
2922 ... vis.setPointCullingEnabled(False) # Disable for highest quality
2923 ... vis.setPointCullingEnabled(True) # Enable for better performance
2924 """
2925 if not self.visualizer:
2926 raise VisualizerError("Visualizer not initialized")
2927 if not isinstance(enabled, bool):
2928 raise ValueError(f"Enabled must be a boolean, got {type(enabled).__name__}")
2929
2930 try:
2931 visualizer_wrapper.set_point_culling_enabled(self.visualizer, enabled)
2932 logger.debug(f"Point culling {'enabled' if enabled else 'disabled'}")
2933 except Exception as e:
2934 raise VisualizerError(f"Failed to set point culling enabled: {e}")
2935
2936 def setPointCullingThreshold(self, threshold: int) -> None:
2937 """
2938 Set the minimum number of points required to trigger culling.
2939
2940 Culling is only activated when the total point count exceeds this threshold.
2941 This prevents unnecessary culling overhead for small point clouds.
2942
2943 Args:
2944 threshold: Point count threshold (default: 10000). Set to 0 to always enable.
2945
2946 Raises:
2947 ValueError: If threshold is not a non-negative integer
2948 VisualizerError: If operation fails
2949
2950 Example:
2951 >>> vis.setPointCullingThreshold(50000) # Only cull for >50k points
2952 >>> vis.setPointCullingThreshold(0) # Always enable culling
2953 """
2954 if not self.visualizer:
2955 raise VisualizerError("Visualizer not initialized")
2956 if not isinstance(threshold, int):
2957 raise ValueError(f"Threshold must be an integer, got {type(threshold).__name__}")
2958 if threshold < 0:
2959 raise ValueError("Point culling threshold must be non-negative")
2960
2961 try:
2962 visualizer_wrapper.set_point_culling_threshold(self.visualizer, threshold)
2963 logger.debug(f"Point culling threshold set to {threshold}")
2964 except Exception as e:
2965 raise VisualizerError(f"Failed to set point culling threshold: {e}")
2966
2967 def setPointMaxRenderDistance(self, distance: float) -> None:
2968 """
2969 Set the maximum rendering distance for points.
2970
2971 Points beyond this distance from the camera are not rendered, improving
2972 performance for large scenes. The distance is measured in world units.
2973
2974 Args:
2975 distance: Maximum distance in world units. Use 0 for auto mode (scene_size * 5.0)
2976
2977 Raises:
2978 ValueError: If distance is negative
2979 VisualizerError: If operation fails
2980
2981 Example:
2982 >>> vis.setPointMaxRenderDistance(0.0) # Auto mode
2983 >>> vis.setPointMaxRenderDistance(100.0) # Fixed distance
2984
2985 Note:
2986 Setting distance to 0 enables automatic mode, which calculates the
2987 render distance based on the scene bounding box dimensions.
2988 """
2989 if not self.visualizer:
2990 raise VisualizerError("Visualizer not initialized")
2991 if not isinstance(distance, (int, float)):
2992 raise ValueError(f"Distance must be numeric, got {type(distance).__name__}")
2993 if distance < 0.0:
2994 raise ValueError("Point max render distance cannot be negative")
2995
2996 try:
2997 visualizer_wrapper.set_point_max_render_distance(self.visualizer, float(distance))
2998 if distance == 0.0:
2999 logger.debug("Point max render distance set to auto mode")
3000 else:
3001 logger.debug(f"Point max render distance set to {distance}")
3002 except Exception as e:
3003 raise VisualizerError(f"Failed to set point max render distance: {e}")
3005 def setPointLODFactor(self, factor: float) -> None:
3006 """
3007 Set the level-of-detail factor for distance-based culling.
3008
3009 Controls how aggressively points are culled based on distance from camera.
3010 Higher values result in more aggressive culling (better performance, lower quality).
3011 Lower values preserve more points (higher quality, lower performance).
3012
3013 Args:
3014 factor: LOD factor (default: 10.0, typical range: 1.0-50.0). Must be positive.
3015
3016 Raises:
3017 ValueError: If factor is not positive
3018 VisualizerError: If operation fails
3019
3020 Example:
3021 >>> vis.setPointLODFactor(5.0) # Conservative culling
3022 >>> vis.setPointLODFactor(10.0) # Default culling
3023 >>> vis.setPointLODFactor(25.0) # Aggressive culling
3024
3025 Note:
3026 The LOD factor determines the rate at which point density decreases
3027 with distance. Higher factors mean points are culled more quickly
3028 as distance increases.
3029 """
3030 if not self.visualizer:
3031 raise VisualizerError("Visualizer not initialized")
3032 if not isinstance(factor, (int, float)):
3033 raise ValueError(f"LOD factor must be numeric, got {type(factor).__name__}")
3034 if factor <= 0.0:
3035 raise ValueError("Point LOD factor must be positive")
3036
3037 # Warn about extreme values
3038 if factor < 1.0:
3039 logger.warning(f"Point LOD factor {factor} is very low (< 1.0), may cause performance issues")
3040 elif factor > 100.0:
3041 logger.warning(f"Point LOD factor {factor} is very high (> 100.0), may over-cull points")
3042
3043 try:
3044 visualizer_wrapper.set_point_lod_factor(self.visualizer, float(factor))
3045 logger.debug(f"Point LOD factor set to {factor}")
3046 except Exception as e:
3047 raise VisualizerError(f"Failed to set point LOD factor: {e}")
3048
3049 def getPointRenderingMetrics(self) -> dict:
3050 """
3051 Get point cloud rendering performance metrics.
3052
3053 Provides detailed statistics about point cloud culling and rendering
3054 performance, useful for optimizing visualization settings.
3055
3056 Returns:
3057 Dictionary with keys:
3058 - 'total_points' (int): Total number of points in the scene
3059 - 'rendered_points' (int): Number of points actually rendered after culling
3060 - 'culling_time_ms' (float): Time spent on culling in milliseconds
3061
3062 Raises:
3063 VisualizerError: If operation fails
3064
3065 Example:
3066 >>> metrics = vis.getPointRenderingMetrics()
3067 >>> print(f"Total: {metrics['total_points']}")
3068 >>> print(f"Rendered: {metrics['rendered_points']}")
3069 >>> cull_rate = (1 - metrics['rendered_points']/metrics['total_points']) * 100
3070 >>> print(f"Culling rate: {cull_rate:.1f}%")
3071
3072 Note:
3073 Metrics are only meaningful after calling plotUpdate() or plotInteractive().
3074 The culling_time_ms represents CPU time spent on culling calculations,
3075 not total frame time.
3076 """
3077 if not self.visualizer:
3078 raise VisualizerError("Visualizer not initialized")
3079
3080 try:
3081 metrics = visualizer_wrapper.get_point_rendering_metrics(self.visualizer)
3082 logger.debug(
3083 f"Point rendering metrics: {metrics['total_points']} total, "
3084 f"{metrics['rendered_points']} rendered, "
3085 f"{metrics['culling_time_ms']:.2f} ms culling time"
3086 )
3087 return metrics
3088 except Exception as e:
3089 raise VisualizerError(f"Failed to get point rendering metrics: {e}")
3090
3091 def __del__(self):
3092 """Destructor to ensure proper cleanup."""
3093 if hasattr(self, 'visualizer') and self.visualizer is not None:
3094 try:
3096 visualizer_wrapper.destroy_visualizer(self.visualizer)
3097 except Exception:
3098 pass # Ignore errors during destruction
Raised when Visualizer operations fail.
None colorContextObjectsRandomly(self, Optional[List[int]] obj_ids=None)
Color context objects randomly.
None plotInteractive(self)
Open interactive visualization window.
None setColorbarFontColor(self, RGBcolor color)
Set colorbar font color.
None disableExactColorMode(self)
Restore the default brightening of primitive colors.
None setPointCullingEnabled(self, bool enabled)
Enable or disable point cloud culling optimization.
None enableSmoothShading(self)
Enable smooth per-vertex-normal shading.
None writeDepthImageData(self, str filename)
Write the depth image of the current view to a text file.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
None hideNavigationGizmo(self)
Hide navigation gizmo (coordinate axes indicator in corner).
getWindowPixelsRGB(self, Optional[List[int]] buffer=None)
Get RGB pixel data from the current window.
None writeImageSegmentationMasks(self, primitive_data_labels=None, object_data_labels=None, object_class_ids=None, str json_filename="", str image_file="", Optional[List[str]] data_attribute_labels=None, bool append_file=False)
Write segmentation masks for the current view in COCO JSON format.
RGBcolor getAmbientSkyColor(self)
Get the hemispheric ambient sky color.
None setBackgroundImage(self, str texture_file)
Set custom background image texture (v1.3.53+).
None buildContextGeometry(self, Context context, Optional[List[int]] uuids=None)
Build Context geometry in the visualizer.
None disableMessages(self)
Disable standard output from visualizer plugin.
None hideWatermark(self)
Hide Helios logo watermark.
None setBackgroundTransparent(self)
Enable transparent background mode (v1.3.53+).
None colorContextPrimitivesByObjectData(self, str data_name, Optional[List[int]] obj_ids=None)
Color context primitives by object data.
None setPhongMaterial(self, float ambient, float diffuse, float specular, float shininess)
Set the Phong material parameters used to shade Context primitives.
__enter__(self)
Context manager entry.
None setExposure(self, float exposure)
Set the linear exposure multiplier applied before tone mapping.
None setPointMaxRenderDistance(self, float distance)
Set the maximum rendering distance for points.
__init__(self, int width, int height, int antialiasing_samples=4, bool headless=False)
Initialize Visualizer with graceful plugin handling.
None setGeometryVertices(self, int geometry_id, List[vec3] vertices)
Set vertices of a geometry primitive.
None clearColor(self)
Clear primitive colors from previous coloring operations.
None setColorbarPosition(self, vec3 position)
Set colorbar position.
None setCustomColormap(self, List[RGBcolor] colors, List[float] divisions)
Set custom colormap.
bool isSmoothShadingEnabled(self)
Check whether smooth per-vertex-normal shading is enabled.
None deleteGeometry(self, int geometry_id)
Delete specific geometry by ID.
None setPointLODFactor(self, float factor)
Set the level-of-detail factor for distance-based culling.
None setCameraPositionSpherical(self, SphericalCoord angle, vec3 lookAt)
Set camera position using spherical coordinates.
None disableLinearPipeline(self)
Disable the linear-light pipeline, shading directly in sRGB space.
None clearGeometry(self)
Clear all geometry from visualizer.
None setCameraFieldOfView(self, float angle_FOV)
Set camera field of view angle.
None displayImageWithBoundingBoxes(self, str image_file, str bbox_file, str classes_file="", float line_width=2.0, int fontsize=12)
Display an image with YOLO bounding boxes overlaid.
None displayImageFromFile(self, str filename)
Display image from file.
None writeNormDepthImage(self, str filename, float max_depth)
Write the depth image of the current view as a grayscale JPEG, normalized between the nearest depth a...
None setLightingModel(self, Union[int, str] lighting_model)
Set lighting model.
None showNavigationGizmo(self)
Show navigation gizmo (coordinate axes indicator in corner).
None setPointCullingThreshold(self, int threshold)
Set the minimum number of points required to trigger culling.
None updateContextPrimitiveColors(self)
Update context primitive colors.
_check_context_alive(self)
Raise if a Context was loaded and has since been destroyed.
None setColorbarRange(self, float min_val, float max_val)
Set colorbar range.
None plotDepthMap(self)
Plot depth map visualization.
Tuple[int, int] getWindowSize(self)
Get window size in pixels.
None enableLinearPipeline(self)
Enable the physically-based linear-light rendering pipeline.
List[vec3] getGeometryVertices(self, int geometry_id)
Get vertices of a geometry primitive.
None printWindowDefault(self)
Print window with default filename.
None disableColorbar(self)
Disable colorbar.
None plotUpdateWithVisibility(self, bool hide_window=False)
Update visualization with window visibility control.
Tuple[vec3, vec3] getCameraPosition(self)
Get current camera position and look-at point.
None enableExactColorMode(self)
Render primitive colors exactly as they are set in the Context.
None setLightDirection(self, vec3 direction)
Set light direction.
bool isLinearPipelineEnabled(self)
Check whether the linear-light rendering pipeline is enabled.
None writeDepthImageDataEXR(self, str filename)
Write the depth image of the current view to an EXR file with lossless float compression.
None displayImageWithSegmentationMasks(self, str image_file, str mask_file, float fill_opacity=0.4, float line_width=2.0, int fontsize=12, bool show_labels=True)
Display an image with COCO segmentation masks overlaid.
None closeWindow(self)
Close visualization window.
None _write_depth_output(self, writer, str filename, str description, *args)
Validate a depth-output filename and call the given wrapper writer.
bool isHeadlessMultisamplingActive(self)
Check whether headless rendering obtained multisampled framebuffer attachments.
None setColorbarTitle(self, str title)
Set colorbar title.
__del__(self)
Destructor to ensure proper cleanup.
Tuple[float, float, float, float] getPhongMaterial(self)
Get the Phong material parameters used to shade Context primitives.
_normalize_annotation_labels(primitive_data_labels, object_data_labels, object_class_ids)
Validate the label arguments shared by the annotation writers.
None displayImageFromPixels(self, List[int] pixel_data, int width, int height)
Display image from RGBA pixel data.
None addCoordinateAxesCustom(self, vec3 origin, vec3 length, str sign="both")
Add coordinate axes with custom properties.
float getExposure(self)
Get the linear exposure multiplier applied before tone mapping.
dict getPointRenderingMetrics(self)
Get point cloud rendering performance metrics.
None setAmbientColors(self, RGBcolor sky_color, RGBcolor ground_color)
Set the hemispheric ambient sky and ground-bounce colors.
None setColormap(self, Union[int, str] colormap)
Set predefined colormap.
None setBackgroundColor(self, RGBcolor color)
Set background color.
None colorContextPrimitivesByData(self, str data_name, Optional[List[int]] uuids=None)
Color context primitives based on primitive data values.
None printWindow(self, str filename, Optional[str] image_format=None)
Save current visualization to image file.
Tuple[List[float], int, int] getDepthImage(self)
Get the distance from the camera to the nearest surface in each pixel of the current view.
None disableCoordinateAxes(self)
Remove coordinate axes.
None setColorbarSize(self, float width, float height)
Set colorbar size.
None clearContextGeometry(self)
Clear context geometry from visualizer.
Tuple[List[float], int, int] getDepthMap(self)
Get the normalized depth map of the current view.
None setCameraPosition(self, vec3 position, vec3 lookAt)
Set camera position using Cartesian coordinates.
RGBcolor getBackgroundColor(self)
Get current background color.
None setLightIntensityFactor(self, float intensity_factor)
Set light intensity scaling factor.
None enableMessages(self)
Enable standard output from visualizer plugin.
None addCoordinateAxes(self)
Add coordinate axes at origin with unit length.
vec2 getTextboxSize(self, str textstring, int fontsize, str fontname)
Measure the rendered size of a text string without adding it to the visualizer.
None addGridWireFrame(self, vec3 center, vec3 size, List[int] subdivisions)
Add grid wireframe.
None plotUpdate(self)
Update visualization (non-interactive).
None disableSmoothShading(self)
Select flat (per-face) shading.
None setColorbarFontSize(self, int font_size)
Set colorbar font size.
None setColorbarTicks(self, List[float] ticks)
Set colorbar tick marks.
None colorContextPrimitivesRandomly(self, Optional[List[int]] uuids=None)
Color context primitives randomly.
None showWatermark(self)
Show Helios logo watermark.
Tuple[List[int], int, int] getPixelUUIDs(self)
Get the Context primitive visible in each pixel of the current view.
Tuple[int, int] getFramebufferSize(self)
Get framebuffer size in pixels.
None writeImageBoundingBoxes(self, primitive_data_labels=None, object_data_labels=None, object_class_ids=None, str image_file="", str classes_txt_file="classes.txt", str image_path="./")
Write bounding boxes for the current view in Ultralytics YOLO format.
None enableColorbar(self)
Enable colorbar.
RGBcolor getAmbientGroundColor(self)
Get the hemispheric ambient ground-bounce color.
None plotOnce(self, bool get_keystrokes=True)
Run one rendering loop.
None setBackgroundSkyTexture(self, Optional[str] texture_file=None, int divisions=50)
Set sky sphere texture background with automatic scaling (v1.3.53+).
None updateWatermark(self)
Update watermark geometry to match current window size.
str _resolve_user_path(str path)
Resolve a user-provided path to an absolute path before working directory changes.
Definition Visualizer.py:50
_visualizer_working_directory()
Context manager that temporarily changes working directory for visualizer operations.
Definition Visualizer.py:73