I’m experiencing a bug where the camera angle is resetting to a default value inbetween object modifications (I’m taking a slice), and image rendering. has anyone experienced a similar issue/ has a fix?
Please share ParaView version, OS and steps to reproduce the issue.
I am using Paraview version 5.10.0, the OS details can be seen below:
$ cat /etc/os-release
NAME=“Red Hat Enterprise Linux”
VERSION=“8.10 (Ootpa)”
ID=“rhel”
ID_LIKE=“fedora”
VERSION_ID=“8.10”
PLATFORM_ID=“platform:el8”
PRETTY_NAME=“Red Hat Enterprise Linux 8.10 (Ootpa)”
ANSI_COLOR=“0;31”
CPE_NAME=“cpe:/o:redhat:enterprise_linux:8::baseos”
HOME_URL=https://www.redhat.com/
DOCUMENTATION_URL=https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8
BUG_REPORT_URL=https://bugzilla.redhat.com/
REDHAT_BUGZILLA_PRODUCT=“Red Hat Enterprise Linux 8”
REDHAT_BUGZILLA_PRODUCT_VERSION=8.10
REDHAT_SUPPORT_PRODUCT=“Red Hat Enterprise Linux”
REDHAT_SUPPORT_PRODUCT_VERSION=“8.10”
A brief summary of the steps i took can be seen below:
- Before any operations are applied to objects i first set a plane as “vertical” for the camera using camera.SetViewUp() and passing it the required parameters for X,Y and Z.
- I am then applying certain functions/ filters to Objects (taking a slice, changing the colour ect.), and rendering the object to the output window using Render(render_view) followed by render_view.Update().
- Once all operations/functions have been applied to an object the camera angle and position is set based on the object’s location. A snippet of the code can be seen below:
-
`camera_position = [ view.position.x + center_x, view.position.y + center_y, view.position.z + center_z ] render_view.CameraPosition = camera_position render_view.CameraFocalPoint = [center_x, center_y, center_z] render_view.CameraViewAngle = view.fov.horizontal render_view.CameraParallelProjection = True render_view.CameraParallelScale = diagonal * distance_factor / 2 render_view.OrientationAxesVisibility = True `
- Render(render_view) and render_view.Update() are then called again to render the final view and it is saved to a png.
I am unsure why but the camera seems to be reset to a default value after the angle and position has been set. Based on printing values to the terminal the camera should be in the right position but each output image shows the same camera angle no matter what I set it to. I’m aware that Render can reset the camera but I was under the impression that this can only occur the first time it is called, please correct me if I’m wrong. Should I be using a different function for rendering or is the error due to something else?
Is this a pvpython only issue ?
Please update to the last release of ParaView, 5.13.1
Yes, this issue is only present when using pvpython. I have updated Paraview to the latest version and the issue is still present.
Could you share a pure pvpython reproducer script ?
Hi, Here’s the script, I’ve removed functions that dont have anything to do with the camera angle, thanks!
def begin_simulation(cases: List[Case], image_sets: List[ImageSet], scenes: List[Scene],
views: List[View], colour_palettes: List[ColourPalette] = None) -> None:
"""
Begins the simulation process, iterating over cases and image sets to render images.
Args:
cases (List[Case]): List of cases to process.
image_sets (List[ImageSet]): List of image sets to render.
scenes (List[Scene]): Available scenes to match with image sets.
views (List[View]): Available views to match with image sets.
Raises:
CaseNotFoundError: If a case directory is not found.
CrankAngleFileNotFoundError: If no crank angle file matches the specified angle.
SceneViewError: If a scene or view is not defined.
ConfigurationError: For errors in rendering or saving.
"""
# Create custom colour palettes before starting the simulation
if colour_palettes:
create_custom_colour_palettes(colour_palettes)
render_view = pv.CreateView('RenderView')
pv.SetActiveView(render_view)
scenes_dict = {scene.name: scene for scene in scenes}
views_dict = {view.name: view for view in views}
for case in cases:
try:
case_file_path = get_case_path(ROOT_DIR, case)
except CaseNotFoundError as e:
raise CaseNotFoundError(f"Error in case {case.case}: {e}")
for image_set in image_sets:
cycles = image_set.time_steps.cycles
try:
crank_angles = parse_crank_angles(
image_set.time_steps.crank_angles, cycles)
except CrankAngleFileNotFoundError as e:
raise CrankAngleFileNotFoundError(
f"Error in image set '{image_set.name}': {e}")
for image in image_set.images:
scene = scenes_dict.get(image.scene)
view = views_dict.get(image.view)
try:
validate_scene_view(scene, view)
except SceneViewError as e:
raise SceneViewError(
f"Error in scene '{image.scene}' or view '{image.view}': {e}")
adjust_plane_definitions(view)
for crank_angle in crank_angles:
crank_angle_path = get_crank_angle_path(case_file_path, crank_angle)
try:
objects = process_scene_objects(
crank_angle_path, scene, render_view, crank_angle)
save_path = render_and_save_view(
objects, scene, view, crank_angle, image_set.name, render_view, case)
notify_image_saved(save_path)
except ConfigurationError as e:
raise ConfigurationError(
f"Error processing crank angle {crank_angle}: {e}")
def adjust_plane_definitions(view: View) -> None:
"""
Adjusts the camera's plane definitions based on the view orientation.
Args:
view (View): The view containing orientation details.
Raises:
ConfigurationError: If an unsupported plane orientation is specified.
"""
render_view = pv.GetActiveView()
active_camera = render_view.GetActiveCamera()
try:
if view.orientation.plane == "-Y":
active_camera.SetViewUp(0, 0, 1)
print("Adjusted plane definitions for '-Y': Z is set as vertical.")
elif view.orientation.plane == "-Z":
active_camera.SetViewUp(0, 1, 0)
print("Adjusted plane definitions for '-Z': Y is set as vertical.")
elif view.orientation.plane == "-X":
active_camera.SetViewUp(0, 0, 1)
print("Adjusted plane definitions for '-X': Z is set as vertical.")
else:
raise ConfigurationError(
f"Unsupported plane orientation: {view.orientation.plane}")
except Exception as e:
raise ConfigurationError(f"Error adjusting plane definitions: {e}")
print(f"Plane definitions adjusted for view: {view.name}.")
def process_scene_objects(file: Path, scene: Scene, render_view: Any, crank_angle: float) -> List[Any]:
"""
Processes all objects in a scene, creating and configuring ParaView objects.
Args:
file (Path): Path to the crank angle file.
scene (Scene): Scene configuration.
render_view (Any): ParaView render view.
crank_angle (float): Current crank angle.
Returns:
List[Any]: List of generated ParaView objects.
Raises:
ConfigurationError: If an object kind is not supported or processing fails.
"""
function_map = {
"slice": slice_object,
"isosurface": isosurface_object,
"surface": surface_object
}
colour_map = scene.colour_maps[0] if scene.colour_maps else None
try:
paraview_obj = pv.CONVERGECFDReader(FileName=str(file))
pv.SetActiveSource(paraview_obj)
set_time_step(crank_angle, paraview_obj)
objects = []
for obj in scene.objects:
function = function_map.get(obj.kind)
if function:
generated_obj = function(
paraview_obj, obj, render_view, colour_map=colour_map)
if generated_obj:
objects.append(generated_obj)
else:
raise ConfigurationError(f"Unsupported object kind: {obj.kind}")
except Exception as e:
raise ConfigurationError(f"Error processing scene objects: {e}")
return objects
def surface_object(paraview_obj: Any, data: Any, render_view: Any, colour_map: Optional[ColourMap] = None) -> Any:
"""
Configures the visibility, color, and transparency of boundary surfaces.
Args:
paraview_obj (Any): The ParaView object.
data (Any): Configuration data for the surface object.
render_view (Any): The active render view.
Returns:
Any: The display object representing the boundary surfaces.
Raises:
ConfigurationError: If boundaries or color settings are invalid.
"""
try:
# Extract configurations from the input data
boundaries = data.boundaries
colour_by = data.colour_by
if not boundaries or not colour_by:
raise ConfigurationError(
"Boundaries and colour_by must be specified for surface objects."
)
# Show the ParaView object
boundary_surface_display = pv.Show(paraview_obj, render_view)
# Apply color and transparency
if colour_by.kind == "rgb":
rgb = [c / 255.0 for c in colour_by.rgb]
boundary_surface_display.AmbientColor = rgb
boundary_surface_display.DiffuseColor = rgb
if colour_by.opacity is not None:
boundary_surface_display.Opacity = colour_by.opacity
else:
raise ConfigurationError(f"Unsupported colour kind: {colour_by.kind}")
# Apply updates to ensure changes are rendered
apply_updates(paraview_obj, render_view)
return boundary_surface_display
except Exception as e:
raise ConfigurationError(f"Unexpected error in surface_object: {e}")
def isosurface_object(paraview_obj: Any, data: Any, render_view: Any, colour_map: Optional[Any] = None) -> Any:
"""
Generates an isosurface for the specified metric and value.
Args:
paraview_obj (Any): ParaView object to process.
data (Any): Isosurface object data from the scene configuration.
render_view (Any): ParaView render view.
colour_map (Optional[Any]): Colour map configuration.
Returns:
Any: Generated isosurface object.
Raises:
ConfigurationError: If the metric is missing or invalid.
"""
try:
metric = data.metric
if not metric:
raise ConfigurationError(
"Metric must be specified for isosurface creation.")
# Debug: Print metric and isosurface value
print(f"Generating isosurface with metric: {metric}")
print(f"Isosurface value: {data.value}")
if metric not in paraview_obj.PointData.keys():
if metric in paraview_obj.CellData.keys():
paraview_obj = pv.CellDatatoPointData(Input=paraview_obj)
apply_updates(paraview_obj, render_view)
else:
raise ConfigurationError(
f"Metric '{metric}' not found in the data arrays.")
isosurface = pv.Contour(Input=paraview_obj)
isosurface.ContourBy = ['POINTS', metric]
isosurface.Isosurfaces = [data.value]
isosurface.PointMergeMethod = 'Uniform Binning'
apply_updates(isosurface, render_view)
return isosurface
except Exception as e:
raise ConfigurationError(f"Error generating isosurface object: {e}")
def slice_object(paraview_obj: Any, data: Any, render_view: Any, colour_map: Optional[Any] = None) -> Any:
"""
Generates a slice object for the specified plane and orientation.
Args:
paraview_obj (Any): ParaView object to process.
data (Any): Slice object data from the scene configuration.
render_view (Any): ParaView render view.
colour_map (Optional[Any]): Colour map configuration.
Returns:
Any: Generated slice object.
Raises:
ConfigurationError: If the plane is invalid or other parameters are missing.
"""
try:
orientation = data.orientation
plane = orientation.plane
bounds = paraview_obj.GetDataInformation().GetBounds()
if any(b is None or abs(b) == float("inf") for b in bounds):
raise ConfigurationError("Invalid bounds detected in the data.")
mid_cylinder = (bounds[4] + bounds[5]) / 2
origin = [0, 0, 0]
normal_vector = [0, 0, 0]
if plane == "X":
origin[0] = mid_cylinder
normal_vector = [1, 0, 0]
elif plane == "Y":
origin[1] = mid_cylinder
normal_vector = [0, 1, 0]
elif plane == "Z":
origin[2] = mid_cylinder
normal_vector = [0, 0, 1]
else:
raise ConfigurationError(f"Invalid plane specified: {plane}")
slice_obj = pv.Slice(Input=paraview_obj)
slice_obj.SliceType = orientation.kind.capitalize()
slice_obj.SliceType.Normal = normal_vector
slice_obj.SliceType.Origin = origin
apply_updates(slice_obj, render_view)
return slice_obj
except Exception as e:
raise ConfigurationError(f"Error generating slice object: {e}")
def apply_updates(paraview_obj: Any, render_view: Any, update: bool = True,
render: bool = True) -> None:
"""
Applies updates to a ParaView object and optionally renders the view.
Args:
paraview_obj (Any): ParaView object to update.
render_view (Any): ParaView render view.
update (bool): Whether to update the pipeline. Defaults to True.
render (bool): Whether to render the view. Defaults to True.
"""
try:
pv.SetActiveSource(paraview_obj)
if update:
pv.UpdatePipeline()
if render:
pv.Render(render_view)
render_view.Update()
except Exception as e:
raise ConfigurationError(f"Error applying updates: {e}")
def render_and_save_view(objects: List[Any], scene: Scene, view: View, crank_angle: float,
image_name: str, render_view: Any, case: Case) -> str:
"""
Configures and renders all objects in the view, then saves the output image.
Args:
objects (List[Any]): List of ParaView objects to render.
scene (Scene): Scene configuration.
view (View): View configuration.
crank_angle (float): Current crank angle.
image_name (str): Name of the image set.
render_view (Any): ParaView render view.
case (Case): Simulation case details.
Returns:
str: Path to the saved image.
Raises:
IOErrorCustom: If an error occurs while saving the image.
"""
try:
# Construct the save path for the output image
case_save_path = construct_save_path(case.job, case.user, case.case)
save_path = f"{case_save_path}/results/images/{image_name}_{scene.name}_{view.name}_{crank_angle}.png"
# Debugging objects before calling set_camera_position
print("Objects passed to set_camera_position:")
for i, obj in enumerate(objects):
print(f"Object {i}: {obj}")
try:
bounds = obj.GetDataInformation().GetBounds()
print(f"Object {i} bounds: {bounds}")
except AttributeError:
print(f"Object {i} does not have GetBounds.")
# Set camera position for all objects in the view
set_camera_position(view, objects, render_view)
# Render each object in the scene
for obj, obj_config in zip(objects, scene.objects):
if obj_config.colour_by:
result = colour_object(obj, obj_config.colour_by, render_view,
scene.colour_maps[0] if scene.colour_maps else None)
if result is None:
print(f"Skipping object due to colour mapping failure: {obj}")
continue # Skip this object if colour_object failed
pv.Show(obj, render_view)
apply_updates(obj, render_view, update=True, render=True)
# Add annotations if defined
if scene.annotations:
for annotation in scene.annotations:
if isinstance(annotation.root, StringAnnotation):
add_annotation(render_view, annotation.root.string,
annotation.root.location, annotation.root.size)
elif isinstance(annotation.root, CrankAngleAnnotation):
add_crank_angle_annotation(render_view, crank_angle, annotation.root.location,
annotation.root.size)
# Use view.size for resolution
resolution = view.size if hasattr(view, "size") and view.size else [1920, 1080]
# Save the image
make_image(objects[0], save_path, resolution)
# Delete ParaView objects to clean up memory
for obj in objects:
pv.Delete(obj)
return save_path
except Exception as e:
raise IOErrorCustom(f"Error saving rendered view: {e}")
def set_camera_position(view: View, objects: List[Any], render_view: Any) -> None:
"""
Sets the camera position based on the largest object in the list.
Args:
view (View): View configuration.
objects (List[Any]): List of ParaView objects to consider.
render_view (Any): ParaView render view.
Raises:
ConfigurationError: If the camera position cannot be set.
"""
try:
# Print details of each object
for i, obj in enumerate(objects):
print(f"Object {i}: {obj}")
print(f"Object {i} type: {type(obj)}")
try:
bounds = obj.GetDataInformation().GetBounds()
print(f"Object {i} bounds: {bounds}")
except AttributeError as e:
print(f"Object {i} does not have bounds: {e}")
# Find the largest object based on bounds
largest_bounds = None
largest_object = None
for obj in objects:
try:
bounds = obj.GetDataInformation().GetBounds()
if bounds and (largest_bounds is None or
(bounds[1] - bounds[0]) * (bounds[3] - bounds[2]) * (bounds[5] - bounds[4]) >
(largest_bounds[1] - largest_bounds[0]) * (largest_bounds[3] - largest_bounds[2]) * (largest_bounds[5] - largest_bounds[4])):
largest_bounds = bounds
largest_object = obj
except AttributeError:
if hasattr(obj, 'bounds'):
bounds = obj.bounds
else:
print(
f"Object {obj} does not have GetBounds or a custom 'bounds' attribute. Skipping")
continue
if largest_object is None:
raise ConfigurationError("No valid object with bounds found.")
# Use the largest object's bounds to set the camera
center_x = (largest_bounds[0] + largest_bounds[1]) / 2
center_y = (largest_bounds[2] + largest_bounds[3]) / 2
center_z = (largest_bounds[4] + largest_bounds[5]) / 2
diagonal = ((largest_bounds[1] - largest_bounds[0]) ** 2 +
(largest_bounds[3] - largest_bounds[2]) ** 2 +
(largest_bounds[5] - largest_bounds[4]) ** 2) ** 0.5
distance_factor = 1.5
camera_position = [
view.position.x + center_x,
view.position.y + center_y,
view.position.z + center_z
]
render_view.CameraPosition = camera_position
render_view.CameraFocalPoint = [center_x, center_y, center_z]
render_view.CameraViewAngle = view.fov.horizontal
render_view.CameraParallelProjection = True
render_view.CameraParallelScale = diagonal * distance_factor / 2
render_view.OrientationAxesVisibility = True
apply_updates(largest_object, render_view)
except Exception as e:
raise ConfigurationError(f"Error setting camera position: {e}")
def colour_object(paraview_obj: Any, colour_by: ColourBy, render_view: Any,
colour_map: Optional[ColourMap] = None) -> Any:
"""
Applies color mapping to the ParaView object using a specified metric or solid RGB color.
Args:
paraview_obj (Any): ParaView object to color.
colour_by (ColourBy): Colour configuration.
render_view (Any): ParaView render view.
colour_map (Optional[ColourMap]): Optional colour map configuration.
Returns:
Any: Updated ParaView object with applied color.
Raises:
ConfigurationError: If invalid color settings are provided or metric is missing.
"""
try:
display = pv.GetDisplayProperties(paraview_obj, view=render_view)
# Validate the display object
if display is None:
print(f"Failed to get display properties for object: {paraview_obj}")
return None # Gracefully skip this object
if colour_by.kind == "rgb":
rgb = colour_by.rgb if colour_by.rgb else [255, 255, 255]
display.AmbientColor = [c / 255.0 for c in rgb]
display.DiffuseColor = [c / 255.0 for c in rgb]
if colour_by.opacity is not None:
display.Opacity = colour_by.opacity
apply_updates(paraview_obj, render_view)
elif colour_by.kind == "metric":
metric = colour_by.metric
if not metric:
raise ConfigurationError(
"Metric must be specified for metric-based coloring.")
palette = colour_map.palette if colour_map else "viridis"
min_value = colour_map.min if colour_map else 0.0
max_value = colour_map.max if colour_map else 1.0
# Set scalar coloring
pv.ColorBy(display, ('POINTS', metric))
# Get the color transfer function and apply settings
color_transfer_func = pv.GetColorTransferFunction(metric)
color_transfer_func.ApplyPreset(palette, True)
color_transfer_func.RescaleTransferFunction(min_value, max_value)
scalar_bar = pv.GetScalarBar(color_transfer_func, render_view)
scalar_bar.Title = metric
scalar_bar.ComponentTitle = ""
scalar_bar.Visibility = 1
scalar_bar.UseCustomLabels = True
scalar_bar.CustomLabels = [min_value,
(min_value + max_value) / 2, max_value]
# Associate data with metric
data_association = (
'POINTS' if metric in paraview_obj.PointData.keys() else
'CELLS' if metric in paraview_obj.CellData.keys() else None
)
if data_association:
pv.ColorBy(display, (data_association, metric))
display.RescaleTransferFunctionToDataRange(True, False)
display.SetScalarBarVisibility(render_view, True)
# Apply opacity settings
if colour_by.opacity:
opacity_func = pv.GetOpacityTransferFunction(metric)
opacity_func.RescaleTransferFunction(min_value, max_value)
opacity_func.Points = [
min_value, colour_by.opacity, 0.5, 0.0,
max_value, colour_by.opacity, 0.5, 0.0
]
apply_updates(paraview_obj, render_view)
else:
raise ConfigurationError(
f"Metric '{metric}' not found in the data arrays.")
else:
raise ConfigurationError(f"Unsupported colouring kind: {colour_by.kind}")
return paraview_obj
except ConfigurationError as e:
print(f"Configuration error in colour_object: {e}")
return None # Gracefully skip this object
except Exception as e:
print(f"Unexpected error in colour_object: {e}")
return None # Gracefully skip this object
Its quite a big script, some investigation is needed.