Issue with camera angle

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:

  1. 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.
  2. 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().
  3. 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:
  4.     `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
          `
    
  5. 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.