144 def __init__(self, width: int, height: int, antialiasing_samples: int = 4, headless: bool =
False):
146 Initialize Visualizer with graceful plugin handling.
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)
157 VisualizerError: If visualizer plugin is not available
158 ValueError: If parameters are invalid
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__}")
171 if width <= 0
or height <= 0:
172 raise ValueError(
"Width and height must be positive integers")
173 if antialiasing_samples < 0:
175 f
"Antialiasing samples must be non-negative, got {antialiasing_samples}. "
176 "Pass 0 to disable antialiasing.")
185 registry = get_plugin_registry()
187 if not registry.is_plugin_available(
'visualizer'):
189 available_plugins = registry.get_available_plugins()
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}"
207 system = platform.system().lower()
208 if 'linux' in system:
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"
214 elif 'darwin' in system:
216 "\n\nmacOS installation hints:\n"
217 "- Install XQuartz: brew install --cask xquartz\n"
218 "- OpenGL should be available by default"
220 elif 'windows' in system:
222 "\n\nWindows installation hints:\n"
223 "- OpenGL drivers should be provided by graphics card drivers\n"
224 "- Visual Studio runtime may be required"
236 self.
visualizer = visualizer_wrapper.create_visualizer_with_antialiasing(
237 width, height, antialiasing_samples, headless
243 "Failed to create Visualizer instance. "
244 "This may indicate a problem with graphics drivers or OpenGL initialization."
246 logger.info(f
"Visualizer created successfully ({width}x{height}, AA:{antialiasing_samples}, headless:{headless})")
248 except Exception
as e:
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")
257 """Context manager entry."""
260 def __exit__(self, exc_type, exc_value, traceback):
261 """Context manager exit with proper cleanup."""
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}")
272 @validate_build_geometry_params
275 Build Context geometry in the visualizer.
277 This method loads geometry from a Helios Context into the visualizer
278 for rendering. If no UUIDs are specified, all geometry is loaded.
281 context: Helios Context instance containing geometry
282 uuids: Optional list of primitive UUIDs to visualize (default: all)
285 VisualizerError: If geometry building fails
286 ValueError: If parameters are invalid
290 if not isinstance(context, Context):
291 raise ValueError(
"context must be a Context instance")
303 visualizer_wrapper.build_context_geometry(self.
visualizer, context.getNativePtr())
304 logger.debug(
"Built all Context geometry in visualizer")
308 raise ValueError(
"UUIDs list cannot be empty")
309 visualizer_wrapper.build_context_geometry_uuids(
310 self.
visualizer, context.getNativePtr(), uuids
312 logger.debug(f
"Built {len(uuids)} primitives in visualizer")
314 except Exception
as e:
319 Open interactive visualization window.
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.
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
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)
343 visualizer_wrapper.plot_interactive(self.
visualizer)
344 logger.debug(
"Interactive visualization completed")
345 except Exception
as e:
350 Update visualization (non-interactive).
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.
356 In headless mode, automatically hides the window to prevent graphics driver crashes on some platforms.
359 VisualizerError: If visualization update fails
369 logger.debug(
"Visualization updated")
370 except Exception
as e:
373 @validate_print_window_params
374 def printWindow(self, filename: str, image_format: Optional[str] =
None) ->
None:
376 Save current visualization to image file.
378 This method exports the current visualization to an image file.
379 Starting from v1.3.53, supports both JPEG and PNG formats.
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.
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.
396 VisualizerError: If image saving fails
397 ValueError: If filename or format is invalid
400 PNG format is required to preserve transparent backgrounds when using
401 setBackgroundTransparent(). JPEG format will render transparent areas as black.
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
412 raise ValueError(
"Filename cannot be empty")
418 if image_format
is None:
419 if resolved_filename.lower().endswith(
'.png'):
421 elif resolved_filename.lower().endswith((
'.jpg',
'.jpeg')):
422 image_format =
'jpeg'
425 image_format =
'jpeg'
426 logger.debug(f
"No format specified and extension not recognized, defaulting to JPEG")
429 if image_format.lower()
not in [
'jpeg',
'png']:
430 raise ValueError(f
"Image format must be 'jpeg' or 'png', got '{image_format}'")
436 visualizer_wrapper.print_window_with_format(
441 logger.debug(f
"Visualization saved to {resolved_filename} ({image_format.upper()} format)")
442 except (AttributeError, NotImplementedError):
444 if image_format.lower() !=
'jpeg':
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."
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:
456 Close visualization window.
458 This method closes any open visualization window. It's safe to call
459 even if no window is open.
462 VisualizerError: If window closing fails
468 visualizer_wrapper.close_window(self.
visualizer)
469 logger.debug(
"Visualization window closed")
470 except Exception
as e:
475 Set camera position using Cartesian coordinates.
478 position: Camera position as vec3 in world coordinates
479 lookAt: Camera look-at point as vec3 in world coordinates
482 VisualizerError: If camera positioning fails
483 ValueError: If parameters are invalid
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__}")
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:
502 Set camera position using spherical coordinates.
505 angle: Camera position as SphericalCoord (radius, elevation, azimuth)
506 lookAt: Camera look-at point as vec3 in world coordinates
509 VisualizerError: If camera positioning fails
510 ValueError: If parameters are invalid
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__}")
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}")
529 Set background color.
532 color: Background color as RGBcolor with values in range [0, 1]
535 VisualizerError: If color setting fails
536 ValueError: If color values are invalid
542 if not isinstance(color, RGBcolor):
543 raise ValueError(f
"Color must be an RGBcolor, got {type(color).__name__}")
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]")
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:
557 Enable transparent background mode (v1.3.53+).
559 Sets the background to transparent with checkerboard pattern display.
560 Requires PNG output format to preserve transparency.
562 Note: When using transparent background, use printWindow() with PNG
563 format to save transparent images.
566 VisualizerError: If transparent background setting fails
573 visualizer_wrapper.set_background_transparent(self.
visualizer)
574 logger.debug(
"Background set to transparent mode")
575 except Exception
as e:
580 Set custom background image texture (v1.3.53+).
583 texture_file: Path to background image file
584 Can be absolute or relative to working directory
587 VisualizerError: If background image setting fails
588 ValueError: If texture file path is invalid
593 if not texture_file
or not isinstance(texture_file, str):
594 raise ValueError(
"Texture file path must be a non-empty string")
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:
607 Set sky sphere texture background with automatic scaling (v1.3.53+).
609 Creates a sky sphere that automatically scales with the scene.
610 Replaces the deprecated addSkyDomeByCenter() method.
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
619 VisualizerError: If sky texture setting fails
620 ValueError: If parameters are invalid
623 >>> visualizer.setBackgroundSkyTexture() # Default gradient sky
624 >>> visualizer.setBackgroundSkyTexture("sky_hdri.jpg", divisions=100)
629 if not isinstance(divisions, _INT_TYPE)
or divisions <= 0:
630 raise ValueError(
"Divisions must be a positive integer")
635 if not isinstance(texture_file, str):
636 raise ValueError(
"Texture file must be a string")
644 visualizer_wrapper.set_background_sky_texture(
650 logger.debug(f
"Sky texture background set: {resolved_path}, divisions={divisions}")
652 logger.debug(f
"Default sky texture background set with divisions={divisions}")
653 except Exception
as e:
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
666 VisualizerError: If light direction setting fails
667 ValueError: If direction is invalid
673 if not isinstance(direction, vec3):
674 raise ValueError(f
"Direction must be a vec3, got {type(direction).__name__}")
677 if direction.x == 0
and direction.y == 0
and direction.z == 0:
678 raise ValueError(
"Light direction cannot be zero vector")
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:
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
697 VisualizerError: If lighting model setting fails
698 ValueError: If lighting model is invalid
704 if isinstance(lighting_model, str):
705 lighting_model_lower = lighting_model.lower()
706 if lighting_model_lower
in [
'none',
'no',
'off']:
708 elif lighting_model_lower
in [
'phong',
'phong_lighting']:
710 elif lighting_model_lower
in [
'phong_shadowed',
'phong_shadows',
'shadowed']:
713 raise ValueError(f
"Unknown lighting model string: {lighting_model}")
717 raise ValueError(f
"Lighting model must be 0 (NONE), 1 (PHONG), or 2 (PHONG_SHADOWED), got {lighting_model}")
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:
728 Color context primitives based on primitive data values.
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.
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.
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.
743 VisualizerError: If visualizer is not initialized or operation fails
744 ValueError: If data_name is invalid or UUIDs are malformed
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)
751 >>> # Build geometry and color by data
752 >>> visualizer.buildContextGeometry(context)
753 >>> visualizer.colorContextPrimitivesByData("radiation_flux_SW")
754 >>> visualizer.plotInteractive()
756 >>> # Color only specific primitives
757 >>> visualizer.colorContextPrimitivesByData("temperature", [uuid1, uuid2, uuid3])
762 if not data_name
or not isinstance(data_name, str):
763 raise ValueError(
"Data name must be a non-empty string")
768 visualizer_wrapper.color_context_primitives_by_data(self.
visualizer, data_name)
769 logger.debug(f
"Colored all primitives by data: {data_name}")
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")
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}")
783 except Exception
as e:
784 raise VisualizerError(f
"Failed to color primitives by data '{data_name}': {e}")
790 Set camera field of view angle.
793 angle_FOV: Field of view angle in degrees
796 ValueError: If angle is invalid
797 VisualizerError: If operation fails
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")
810 helios_lib.setCameraFieldOfView(self.
visualizer, ctypes.c_float(angle_FOV))
811 except Exception
as e:
816 Get current camera position and look-at point.
819 Tuple of (camera_position, look_at_point) as vec3 objects
822 VisualizerError: If operation fails
829 camera_pos = (ctypes.c_float * 3)()
830 look_at = (ctypes.c_float * 3)()
832 helios_lib.getCameraPosition(self.
visualizer, camera_pos, look_at)
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:
841 Get current background color.
844 Background color as RGBcolor object
847 VisualizerError: If operation fails
854 color = (ctypes.c_float * 3)()
856 helios_lib.getBackgroundColor(self.
visualizer, color)
858 return RGBcolor(color[0], color[1], color[2])
859 except Exception
as e:
866 Set light intensity scaling factor.
869 intensity_factor: Light intensity scaling factor (typically 0.1 to 10.0)
872 ValueError: If intensity factor is invalid
873 VisualizerError: If operation fails
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")
884 helios_lib.setLightIntensityFactor(self.
visualizer, ctypes.c_float(intensity_factor))
885 except Exception
as e:
892 Get window size in pixels.
895 Tuple of (width, height) in pixels
898 VisualizerError: If operation fails
904 width = ctypes.c_uint()
905 height = ctypes.c_uint()
907 helios_lib.getWindowSize(self.
visualizer, ctypes.byref(width), ctypes.byref(height))
909 return (width.value, height.value)
910 except Exception
as e:
915 Get framebuffer size in pixels.
918 Tuple of (width, height) in pixels
921 VisualizerError: If operation fails
927 width = ctypes.c_uint()
928 height = ctypes.c_uint()
930 helios_lib.getFramebufferSize(self.
visualizer, ctypes.byref(width), ctypes.byref(height))
932 return (width.value, height.value)
933 except Exception
as e:
938 Print window with default filename.
941 VisualizerError: If operation fails
948 helios_lib.printWindowDefault(self.
visualizer)
949 except Exception
as e:
954 Display image from RGBA pixel data.
957 pixel_data: RGBA pixel data as list of integers (0-255)
958 width: Image width in pixels
959 height: Image height in pixels
962 ValueError: If parameters are invalid
963 VisualizerError: If operation fails
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")
975 expected_size = width * height * 4
976 if len(pixel_data) != expected_size:
977 raise ValueError(f
"Pixel data size mismatch: expected {expected_size}, got {len(pixel_data)}")
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:
988 Display image from file.
991 filename: Path to image file
994 ValueError: If filename is invalid
995 VisualizerError: If operation fails
1000 if not isinstance(filename, str)
or not filename.strip():
1001 raise ValueError(
"Filename must be a non-empty string")
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}")
1009 classes_file: str =
"", line_width: float = 2.0,
1010 fontsize: int = 12) ->
None:
1012 Display an image with YOLO bounding boxes overlaid.
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.
1018 This reads the annotation format written by
1019 :meth:`pyhelios.RadiationModel.writeImageBoundingBoxes`.
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
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
1035 Like :meth:`displayImageFromFile`, this clears any existing geometry and does
1036 not return until the window is closed.
1039 >>> vis.displayImageWithBoundingBoxes("scene.jpeg", "scene.txt")
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):
1050 f
"Classes file must be a string, got {type(classes_file).__name__}")
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}")
1057 visualizer_wrapper.display_image_with_bounding_boxes(
1062 float(line_width), int(fontsize))
1063 except Exception
as e:
1065 f
"Failed to display image '{image_file}' with bounding boxes: {e}")
1068 fill_opacity: float = 0.4, line_width: float = 2.0,
1069 fontsize: int = 12, show_labels: bool =
True) ->
None:
1071 Display an image with COCO segmentation masks overlaid.
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
1078 This reads the annotation format written by
1079 :meth:`pyhelios.RadiationModel.writeImageSegmentationMasks`.
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
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
1097 Like :meth:`displayImageFromFile`, this clears any existing geometry and does
1098 not return until the window is closed.
1101 >>> vis.displayImageWithSegmentationMasks("scene.jpeg", "annotations.json")
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}")
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):
1118 f
"Show labels must be a boolean, got {type(show_labels).__name__}")
1121 visualizer_wrapper.display_image_with_segmentation_masks(
1125 float(fill_opacity), float(line_width), int(fontsize), show_labels)
1126 except Exception
as e:
1128 f
"Failed to display image '{image_file}' with segmentation masks: {e}")
1132 Render primitive colors exactly as they are set in the Context.
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.
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
1146 This also disables the linear-light pipeline (see :meth:`disableLinearPipeline`),
1147 since tone mapping would likewise alter the values read back.
1150 VisualizerError: If the operation fails
1153 >>> vis = Visualizer(800, 600, antialiasing_samples=0)
1154 >>> vis.enableExactColorMode()
1159 visualizer_wrapper.enable_exact_color_mode(self.
visualizer)
1160 except Exception
as e:
1165 Restore the default brightening of primitive colors.
1167 This also restores the linear-light pipeline (see :meth:`enableLinearPipeline`).
1170 VisualizerError: If the operation fails
1173 >>> vis.disableExactColorMode()
1178 visualizer_wrapper.disable_exact_color_mode(self.
visualizer)
1179 except Exception
as e:
1184 Enable the physically-based linear-light rendering pipeline.
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.
1193 VisualizerError: If the operation fails
1196 >>> vis.enableLinearPipeline()
1201 visualizer_wrapper.enable_linear_pipeline(self.
visualizer)
1202 except Exception
as e:
1207 Disable the linear-light pipeline, shading directly in sRGB space.
1209 This reproduces the rendering behavior of helios-core versions before 1.3.83.
1212 VisualizerError: If the operation fails
1215 >>> vis.disableLinearPipeline()
1220 visualizer_wrapper.disable_linear_pipeline(self.
visualizer)
1221 except Exception
as e:
1226 Check whether the linear-light rendering pipeline is enabled.
1229 True if the linear pipeline is enabled
1232 VisualizerError: If the operation fails
1235 >>> vis.isLinearPipelineEnabled()
1241 return visualizer_wrapper.is_linear_pipeline_enabled(self.
visualizer)
1242 except Exception
as e:
1247 Set the linear exposure multiplier applied before tone mapping.
1249 Only has an effect while the linear pipeline is enabled.
1252 exposure: Exposure multiplier; must be positive
1255 ValueError: If exposure is not positive
1256 VisualizerError: If the operation fails
1259 >>> vis.setExposure(1.5)
1263 if not isinstance(exposure, (int, float))
or isinstance(exposure, bool):
1264 raise ValueError(f
"Exposure must be a number, got {type(exposure).__name__}")
1266 raise ValueError(f
"Exposure must be positive, got {exposure}")
1268 visualizer_wrapper.set_exposure(self.
visualizer, float(exposure))
1269 except Exception
as e:
1274 Get the linear exposure multiplier applied before tone mapping.
1277 Current exposure multiplier
1280 VisualizerError: If the operation fails
1283 >>> vis.getExposure()
1289 return visualizer_wrapper.get_exposure(self.
visualizer)
1290 except Exception
as e:
1293 def setPhongMaterial(self, ambient: float, diffuse: float, specular: float,
1294 shininess: float) ->
None:
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``.
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.
1310 ambient: Ambient reflectance weight
1311 diffuse: Diffuse reflectance weight
1312 specular: Specular highlight strength
1313 shininess: Specular exponent controlling highlight tightness
1316 VisualizerError: If the operation fails
1319 >>> vis.setPhongMaterial(1.0, 0.8, 0.0, 32.0) # no specular highlight
1324 visualizer_wrapper.set_phong_material(self.
visualizer, float(ambient),
1325 float(diffuse), float(specular),
1327 except Exception
as e:
1332 Get the Phong material parameters used to shade Context primitives.
1335 Tuple of (ambient, diffuse, specular, shininess)
1338 VisualizerError: If the operation fails
1341 >>> ambient, diffuse, specular, shininess = vis.getPhongMaterial()
1346 return visualizer_wrapper.get_phong_material(self.
visualizer)
1347 except Exception
as e:
1350 def setAmbientColors(self, sky_color: RGBcolor, ground_color: RGBcolor) ->
None:
1352 Set the hemispheric ambient sky and ground-bounce colors.
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.
1360 sky_color: Color of light arriving from above
1361 ground_color: Color of light bouncing from below
1364 ValueError: If either argument is not an RGBcolor
1365 VisualizerError: If the operation fails
1368 >>> vis.setAmbientColors(RGBcolor(0.5, 0.6, 0.75), RGBcolor(0.35, 0.3, 0.22))
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__}")
1377 visualizer_wrapper.set_ambient_colors(
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:
1387 Get the hemispheric ambient sky color.
1390 Color of ambient light arriving from above
1393 VisualizerError: If the operation fails
1396 >>> vis.getAmbientSkyColor()
1401 r, g, b = visualizer_wrapper.get_ambient_sky_color(self.
visualizer)
1403 except Exception
as e:
1408 Get the hemispheric ambient ground-bounce color.
1411 Color of ambient light bouncing from below
1414 VisualizerError: If the operation fails
1417 >>> vis.getAmbientGroundColor()
1422 r, g, b = visualizer_wrapper.get_ambient_ground_color(self.
visualizer)
1424 except Exception
as e:
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.
1438 VisualizerError: If the operation fails
1441 >>> vis.enableSmoothShading()
1446 visualizer_wrapper.enable_smooth_shading(self.
visualizer)
1447 except Exception
as e:
1452 Select flat (per-face) shading.
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.
1458 VisualizerError: If the operation fails
1461 >>> vis.disableSmoothShading()
1466 visualizer_wrapper.disable_smooth_shading(self.
visualizer)
1467 except Exception
as e:
1472 Check whether smooth per-vertex-normal shading is enabled.
1475 True if smooth shading is enabled
1478 VisualizerError: If the operation fails
1481 >>> vis.isSmoothShadingEnabled()
1487 return visualizer_wrapper.is_smooth_shading_enabled(self.
visualizer)
1488 except Exception
as e:
1493 Check whether headless rendering obtained multisampled framebuffer attachments.
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
1502 True if multisampled attachments were obtained
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.
1510 VisualizerError: If the operation fails
1513 >>> vis.isHeadlessMultisamplingActive()
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}")
1523 def getTextboxSize(self, textstring: str, fontsize: int, fontname: str) -> vec2:
1525 Measure the rendered size of a text string without adding it to the visualizer.
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.
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.
1542 Width and height of the text in window-normalized units.
1545 ValueError: If an argument is invalid
1546 VisualizerError: If the operation fails
1549 The result depends on the current framebuffer dimensions and DPI scale, and
1550 therefore changes when the window is resized.
1553 >>> size = vis.getTextboxSize("Leaf area", 14, "OpenSans-Regular")
1554 >>> print(f"{size.x:.3f} x {size.y:.3f}")
1559 if not isinstance(textstring, str):
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")
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:
1582 Get RGB pixel data from the current window.
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.
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::
1590 pixels, width, height = visualizer.getWindowPixelsRGB()
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.
1601 If ``buffer`` is None, a tuple of ``(pixel_data, width, height)``. Otherwise ``None``;
1602 ``buffer`` is filled in place.
1605 ValueError: If ``buffer`` is not a list, or is not sized for the current framebuffer
1606 VisualizerError: If operation fails
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:
1623 "getWindowPixelsRGB() returned no pixel data; the framebuffer reported "
1624 f
"{width.value}x{height.value}"
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:
1630 except Exception
as e:
1633 if not isinstance(buffer, list):
1634 raise ValueError(
"Buffer must be a list")
1638 required = 3 * fb_width * fb_height
1639 if len(buffer) != required:
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."
1650 buffer_array = (ctypes.c_uint * len(buffer))(*buffer)
1651 helios_lib.getWindowPixelsRGB(self.
visualizer, buffer_array)
1652 visualizer_wrapper._check_for_helios_error()
1655 for i
in range(len(buffer)):
1656 buffer[i] = buffer_array[i]
1657 except VisualizerError:
1659 except Exception
as e:
1662 def getDepthMap(self) -> Tuple[List[float], int, int]:
1664 Get the normalized depth map of the current view.
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.
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.
1679 VisualizerError: If operation fails
1686 depth_ptr = ctypes.POINTER(ctypes.c_float)()
1687 width = ctypes.c_uint()
1688 height = ctypes.c_uint()
1689 buffer_size = ctypes.c_uint()
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()
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)
1702 except Exception
as e:
1707 Plot depth map visualization.
1710 VisualizerError: If operation fails
1718 except Exception
as e:
1725 Get the distance from the camera to the nearest surface in each pixel of the current view.
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`).
1733 The frame is rendered as part of the call if needed, as for :meth:`printWindow`.
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`).
1742 VisualizerError: If the operation fails
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.
1752 >>> depth, width, height = visualizer.getDepthImage()
1753 >>> center = depth[(height // 2) * width + width // 2]
1760 return visualizer_wrapper.get_depth_image(self.
visualizer)
1761 except NotImplementedError:
1763 except Exception
as e:
1768 Get the Context primitive visible in each pixel of the current view.
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`.
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.
1784 VisualizerError: If the operation fails
1787 - A primitive whose texture has a transparency channel is visible only where its
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.
1794 >>> pixels, width, height = visualizer.getPixelUUIDs()
1795 >>> visible = {p - 1 for p in pixels if p != 0}
1802 return visualizer_wrapper.get_pixel_uuids(self.
visualizer)
1803 except NotImplementedError:
1805 except Exception
as e:
1810 """Validate the label arguments shared by the annotation writers.
1813 Tuple of (labels list, class ID list, use_object_data flag)
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")
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"
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__}")
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")
1837 raise ValueError(f
"{name} cannot be empty")
1838 class_ids = object_class_ids
1840 raise TypeError(f
"{name} must be a string or list of strings, got {type(labels).__name__}")
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")
1849 return list(labels), [int(cid)
for cid
in class_ids], use_object_data
1852 object_class_ids=None, image_file: str =
"",
1853 classes_txt_file: str =
"classes.txt",
1854 image_path: str =
"./") ->
None:
1856 Write bounding boxes for the current view in Ultralytics YOLO format.
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.
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`.
1871 Supply exactly one of ``primitive_data_labels`` or ``object_data_labels``.
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
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
1886 ValueError: If both or neither label arguments are given, lengths differ, or a class
1888 TypeError: If an argument has the wrong type
1889 VisualizerError: If writing fails (e.g. the output directory cannot be written)
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.
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")
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")
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:
1921 except Exception
as e:
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:
1930 Write segmentation masks for the current view in COCO JSON format.
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`.
1940 Supply exactly one of ``primitive_data_labels`` or ``object_data_labels``. The output
1941 can be checked with :meth:`displayImageWithSegmentationMasks`.
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.
1960 ValueError: If both or neither label arguments are given, lengths differ, or a class
1962 TypeError: If an argument has the wrong type
1963 VisualizerError: If writing fails (e.g. the image file does not exist)
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")
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__}")
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:
1998 except Exception
as e:
1999 raise VisualizerError(f
"Failed to write image segmentation masks: {e}")
2002 """Validate a depth-output filename and call the given wrapper writer."""
2006 if not isinstance(filename, str)
or not filename.strip():
2007 raise ValueError(
"Filename must be a non-empty string")
2012 except NotImplementedError:
2014 except Exception
as e:
2019 Write the depth image of the current view to a text file.
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.
2026 filename: Text file to write, which may include a path
2029 ValueError: If filename is empty
2030 VisualizerError: If writing fails
2032 self.
_write_depth_output(visualizer_wrapper.write_depth_image_data, filename,
"depth image data")
2036 Write the depth image of the current view to an EXR file with lossless float compression.
2038 See :meth:`getDepthImage` for the definition of depth. Pixels in which no geometry is
2039 visible are written as -1.
2042 filename: EXR file to write, which may include a path
2045 ValueError: If filename is empty
2046 VisualizerError: If writing fails
2048 self.
_write_depth_output(visualizer_wrapper.write_depth_image_data_exr, filename,
"EXR depth image")
2052 Write the depth image of the current view as a grayscale JPEG, normalized between the
2053 nearest depth and a maximum depth.
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``.
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.
2064 ValueError: If filename is empty or max_depth is not positive
2065 VisualizerError: If writing fails
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}")
2072 "normalized depth image", float(max_depth))
2078 Clear all geometry from visualizer.
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.
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.
2093 VisualizerError: If operation fails
2100 except Exception
as e:
2105 Clear context geometry from visualizer.
2108 VisualizerError: If operation fails
2114 helios_lib.clearContextGeometry(self.
visualizer)
2115 except Exception
as e:
2120 Delete specific geometry by ID.
2123 geometry_id: ID of geometry to delete
2126 ValueError: If geometry ID is invalid
2127 VisualizerError: If operation fails
2132 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
2133 raise ValueError(
"Geometry ID must be a non-negative integer")
2136 helios_lib.deleteGeometry(self.
visualizer, geometry_id)
2137 except Exception
as e:
2138 raise VisualizerError(f
"Failed to delete geometry {geometry_id}: {e}")
2142 Update context primitive colors.
2145 VisualizerError: If operation fails
2151 helios_lib.updateContextPrimitiveColors(self.
visualizer)
2152 except Exception
as e:
2153 raise VisualizerError(f
"Failed to update context primitive colors: {e}")
2159 Get vertices of a geometry primitive.
2162 geometry_id: Unique identifier of the geometry primitive
2165 List of vertices as vec3 objects
2168 ValueError: If geometry ID is invalid
2169 VisualizerError: If operation fails
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})")
2180 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
2181 raise ValueError(
"Geometry ID must be a non-negative integer")
2184 vertices_list = visualizer_wrapper.get_geometry_vertices(self.
visualizer, geometry_id)
2186 return [
vec3(v[0], v[1], v[2])
for v
in vertices_list]
2187 except Exception
as e:
2192 Set vertices of a geometry primitive.
2194 This allows dynamic modification of geometry shapes during visualization.
2195 Useful for animating geometry or adjusting shapes based on simulation results.
2198 geometry_id: Unique identifier of the geometry primitive
2199 vertices: List of new vertices as vec3 objects
2202 ValueError: If parameters are invalid
2203 VisualizerError: If operation fails
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)
2215 if not isinstance(geometry_id, _INT_TYPE)
or geometry_id < 0:
2216 raise ValueError(
"Geometry ID must be a non-negative integer")
2218 if not vertices
or not isinstance(vertices, (list, tuple)):
2219 raise ValueError(
"Vertices must be a non-empty list")
2221 if not all(isinstance(v, vec3)
for v
in vertices):
2222 raise ValueError(
"All vertices must be vec3 objects")
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:
2234 Add coordinate axes at origin with unit length.
2237 VisualizerError: If operation fails
2243 helios_lib.addCoordinateAxes(self.
visualizer)
2244 except Exception
as e:
2249 Add coordinate axes with custom properties.
2252 origin: Axes origin position
2253 length: Axes length in each direction
2254 sign: Axis direction ("both" or "positive")
2257 ValueError: If parameters are invalid
2258 VisualizerError: If operation fails
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'")
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:
2279 Remove coordinate axes.
2282 VisualizerError: If operation fails
2288 helios_lib.disableCoordinateAxes(self.
visualizer)
2289 except Exception
as e:
2292 def addGridWireFrame(self, center: vec3, size: vec3, subdivisions: List[int]) ->
None:
2297 center: Grid center position
2298 size: Grid size in each direction
2299 subdivisions: Grid subdivisions [x, y, z]
2302 ValueError: If parameters are invalid
2303 VisualizerError: If operation fails
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")
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:
2332 VisualizerError: If operation fails
2339 except Exception
as e:
2347 VisualizerError: If operation fails
2354 except Exception
as e:
2359 Set colorbar position.
2362 position: Colorbar position
2365 ValueError: If position is invalid
2366 VisualizerError: If operation fails
2371 if not isinstance(position, vec3):
2372 raise ValueError(
"Position must be a vec3")
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:
2385 width: Colorbar width
2386 height: Colorbar height
2389 ValueError: If size is invalid
2390 VisualizerError: If operation fails
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")
2401 size_array = (ctypes.c_float * 2)(float(width), float(height))
2402 helios_lib.setColorbarSize(self.
visualizer, size_array)
2403 except Exception
as e:
2410 Setting a range explicitly stops the colorbar from auto-ranging over the data, including
2411 for the degenerate range ``setColorbarRange(0, 0)``.
2414 min_val: Minimum value
2415 max_val: Maximum value. Must be greater than or equal to ``min_val``; helios ignores an
2419 ValueError: If range is invalid
2420 VisualizerError: If operation fails
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")
2433 helios_lib.setColorbarRange(self.
visualizer, float(min_val), float(max_val))
2434 except Exception
as e:
2439 Set colorbar tick marks.
2442 ticks: List of tick values
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`.
2451 ValueError: If ticks are invalid
2452 VisualizerError: If operation fails
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")
2464 ticks_array = (ctypes.c_float * len(ticks))(*ticks)
2465 helios_lib.setColorbarTicks(self.
visualizer, ticks_array, len(ticks))
2467 helios_lib.setColorbarTicks(self.
visualizer,
None, 0)
2468 except Exception
as e:
2476 title: Colorbar title
2479 ValueError: If title is invalid
2480 VisualizerError: If operation fails
2485 if not isinstance(title, str):
2486 raise ValueError(
"Title must be a string")
2489 helios_lib.setColorbarTitle(self.
visualizer, title.encode(
'utf-8'))
2490 except Exception
as e:
2495 Set colorbar font color.
2501 ValueError: If color is invalid
2502 VisualizerError: If operation fails
2507 if not isinstance(color, RGBcolor):
2508 raise ValueError(
"Color must be an RGBcolor")
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:
2518 Set colorbar font size.
2521 font_size: Font size
2524 ValueError: If font size is invalid
2525 VisualizerError: If operation fails
2530 if not isinstance(font_size, _INT_TYPE)
or font_size <= 0:
2531 raise ValueError(
"Font size must be a positive integer")
2534 helios_lib.setColorbarFontSize(self.
visualizer, font_size)
2535 except Exception
as e:
2540 def setColormap(self, colormap: Union[int, str]) ->
None:
2542 Set predefined colormap.
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`).
2553 ValueError: If colormap is invalid
2554 VisualizerError: If operation fails
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
2575 raise ValueError(
"Colormap must be integer ID or string name")
2578 helios_lib.setColormap(self.
visualizer, colormap_id)
2579 except Exception
as e:
2582 def setCustomColormap(self, colors: List[RGBcolor], divisions: List[float]) ->
None:
2584 Set custom colormap.
2587 colors: List of RGB colors
2588 divisions: List of division points (same length as colors)
2591 ValueError: If parameters are invalid
2592 VisualizerError: If operation fails
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")
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")
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
2617 divisions_array = (ctypes.c_float * len(divisions))(*divisions)
2619 helios_lib.setCustomColormap(self.
visualizer, color_array, divisions_array, len(colors))
2620 except Exception
as e:
2627 Color context primitives by object data.
2630 data_name: Name of object data to use for coloring
2631 obj_ids: Optional list of object IDs to color (None for all)
2634 ValueError: If parameters are invalid
2635 VisualizerError: If operation fails
2640 if not isinstance(data_name, str)
or not data_name.strip():
2641 raise ValueError(
"Data name must be a non-empty string")
2645 helios_lib.colorContextPrimitivesByObjectData(self.
visualizer, data_name.encode(
'utf-8'))
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")
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))
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}")
2662 Color context primitives randomly.
2665 uuids: Optional list of primitive UUIDs to color (None for all)
2668 ValueError: If UUIDs are invalid
2669 VisualizerError: If operation fails
2676 helios_lib.colorContextPrimitivesRandomly(self.
visualizer,
None, 0)
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")
2684 uuid_array = (ctypes.c_uint * len(uuids))(*uuids)
2685 helios_lib.colorContextPrimitivesRandomly(self.
visualizer, uuid_array, len(uuids))
2687 helios_lib.colorContextPrimitivesRandomly(self.
visualizer,
None, 0)
2688 except Exception
as e:
2693 Color context objects randomly.
2696 obj_ids: Optional list of object IDs to color (None for all)
2699 ValueError: If object IDs are invalid
2700 VisualizerError: If operation fails
2707 helios_lib.colorContextObjectsRandomly(self.
visualizer,
None, 0)
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")
2715 obj_ids_array = (ctypes.c_uint * len(obj_ids))(*obj_ids)
2716 helios_lib.colorContextObjectsRandomly(self.
visualizer, obj_ids_array, len(obj_ids))
2718 helios_lib.colorContextObjectsRandomly(self.
visualizer,
None, 0)
2719 except Exception
as e:
2724 Clear primitive colors from previous coloring operations.
2727 VisualizerError: If operation fails
2734 except Exception
as e:
2741 Hide Helios logo watermark.
2744 VisualizerError: If operation fails
2751 except Exception
as e:
2756 Show Helios logo watermark.
2759 VisualizerError: If operation fails
2766 except Exception
as e:
2771 Update watermark geometry to match current window size.
2774 VisualizerError: If operation fails
2781 except Exception
as e:
2788 Hide navigation gizmo (coordinate axes indicator in corner).
2790 The navigation gizmo shows XYZ axes orientation and can be clicked
2791 to snap the camera to standard views (top, front, side, etc.).
2794 VisualizerError: If operation fails
2801 visualizer_wrapper.hide_navigation_gizmo(self.
visualizer)
2802 logger.debug(
"Navigation gizmo hidden")
2803 except Exception
as e:
2808 Show navigation gizmo (coordinate axes indicator in corner).
2810 The navigation gizmo shows XYZ axes orientation and can be clicked
2811 to snap the camera to standard views (top, front, side, etc.).
2813 Note: Navigation gizmo is shown by default in v1.3.53+.
2816 VisualizerError: If operation fails
2823 visualizer_wrapper.show_navigation_gizmo(self.
visualizer)
2824 logger.debug(
"Navigation gizmo shown")
2825 except Exception
as e:
2832 Enable standard output from visualizer plugin.
2835 VisualizerError: If operation fails
2842 except Exception
as e:
2847 Disable standard output from visualizer plugin.
2850 VisualizerError: If operation fails
2857 except Exception
as e:
2860 def plotOnce(self, get_keystrokes: bool =
True) ->
None:
2862 Run one rendering loop.
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
2870 get_keystrokes: Whether to process keystrokes
2873 VisualizerError: If operation fails
2879 helios_lib.plotOnce(self.
visualizer, get_keystrokes)
2880 except Exception
as e:
2885 Update visualization with window visibility control.
2888 hide_window: Whether to hide the window during update
2891 VisualizerError: If operation fails
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}")
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.
2914 enabled: True to enable culling, False to disable (default: True)
2917 ValueError: If enabled is not a boolean
2918 VisualizerError: If operation fails
2921 >>> with Visualizer(800, 600) as vis:
2922 ... vis.setPointCullingEnabled(False) # Disable for highest quality
2923 ... vis.setPointCullingEnabled(True) # Enable for better performance
2927 if not isinstance(enabled, bool):
2928 raise ValueError(f
"Enabled must be a boolean, got {type(enabled).__name__}")
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:
2938 Set the minimum number of points required to trigger culling.
2940 Culling is only activated when the total point count exceeds this threshold.
2941 This prevents unnecessary culling overhead for small point clouds.
2944 threshold: Point count threshold (default: 10000). Set to 0 to always enable.
2947 ValueError: If threshold is not a non-negative integer
2948 VisualizerError: If operation fails
2951 >>> vis.setPointCullingThreshold(50000) # Only cull for >50k points
2952 >>> vis.setPointCullingThreshold(0) # Always enable culling
2956 if not isinstance(threshold, int):
2957 raise ValueError(f
"Threshold must be an integer, got {type(threshold).__name__}")
2959 raise ValueError(
"Point culling threshold must be non-negative")
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:
2969 Set the maximum rendering distance for points.
2971 Points beyond this distance from the camera are not rendered, improving
2972 performance for large scenes. The distance is measured in world units.
2975 distance: Maximum distance in world units. Use 0 for auto mode (scene_size * 5.0)
2978 ValueError: If distance is negative
2979 VisualizerError: If operation fails
2982 >>> vis.setPointMaxRenderDistance(0.0) # Auto mode
2983 >>> vis.setPointMaxRenderDistance(100.0) # Fixed distance
2986 Setting distance to 0 enables automatic mode, which calculates the
2987 render distance based on the scene bounding box dimensions.
2991 if not isinstance(distance, (int, float)):
2992 raise ValueError(f
"Distance must be numeric, got {type(distance).__name__}")
2994 raise ValueError(
"Point max render distance cannot be negative")
2997 visualizer_wrapper.set_point_max_render_distance(self.
visualizer, float(distance))
2999 logger.debug(
"Point max render distance set to auto mode")
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}")
3007 Set the level-of-detail factor for distance-based culling.
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).
3014 factor: LOD factor (default: 10.0, typical range: 1.0-50.0). Must be positive.
3017 ValueError: If factor is not positive
3018 VisualizerError: If operation fails
3021 >>> vis.setPointLODFactor(5.0) # Conservative culling
3022 >>> vis.setPointLODFactor(10.0) # Default culling
3023 >>> vis.setPointLODFactor(25.0) # Aggressive culling
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.
3032 if not isinstance(factor, (int, float)):
3033 raise ValueError(f
"LOD factor must be numeric, got {type(factor).__name__}")
3035 raise ValueError(
"Point LOD factor must be positive")
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")
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:
3051 Get point cloud rendering performance metrics.
3053 Provides detailed statistics about point cloud culling and rendering
3054 performance, useful for optimizing visualization settings.
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
3063 VisualizerError: If operation fails
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}%")
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.
3081 metrics = visualizer_wrapper.get_point_rendering_metrics(self.
visualizer)
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"
3088 except Exception
as e:
3092 """Destructor to ensure proper cleanup."""
3093 if hasattr(self,
'visualizer')
and self.
visualizer is not None:
3096 visualizer_wrapper.destroy_visualizer(self.
visualizer)