Source code for pyetsimul.visualization.interactive

"""Interactive plotting functions for dynamic visualizations.

Provides interactive eye tracking visualization with keyboard controls for real-time exploration.
Enables dynamic visualization of eye tracking with target and eye movement controls.
"""

import copy

import matplotlib.pyplot as plt
import numpy as np

from pyetsimul.log import info

from ..core import Camera, Eye, Light
from ..types import Position3D
from .coordinate_utils import prepare_eye_data_for_plots
from .integrated_plots import plot_setup_and_camera_view
from .interactive_controls import InteractiveControls
from .plot_config import create_plot_config
from .setup_plots import plot_setup


[docs] def plot_interactive_setup(eye_base: Eye, lights: list[Light], camera: Camera, target_point: Position3D) -> None: """Create and run interactive setup and camera view with keyboard controls. Args: eye_base: Base eye object lights: List of light objects camera: Camera object target_point: Initial target point """ InteractiveControls.print_controls() config = create_plot_config() controls = InteractiveControls([eye_base], target_point) fig = plt.figure(figsize=config.layout.extra_wide, constrained_layout=True) ax1 = fig.add_subplot(1, 2, 1, projection="3d") ax2 = fig.add_subplot(1, 2, 2) e_ref = copy.deepcopy(eye_base) e_ref.look_at(target_point) plot_setup_and_camera_view(e_ref, target_point, camera, lights=lights, ax1=ax1, ax2=ax2, fig=fig) ref_bounds = {"x": ax1.get_xlim(), "y": ax1.get_ylim(), "z": ax1.get_zlim()} def update_plot() -> None: """Update the visualization with current eye and target positions.""" e = copy.deepcopy(eye_base) e.look_at(controls.target_point) plot_setup_and_camera_view( e, controls.target_point, camera, lights=lights, ax1=ax1, ax2=ax2, fig=fig, ref_bounds=ref_bounds ) eye_pos = eye_base.position fig.suptitle( f"Target X={controls.target_point.x:.1f} mm, Y={controls.target_point.y:.1f} mm, Z={controls.target_point.z:.1f} mm\n" f"Eye X={eye_pos.x:.1f} mm, Y={eye_pos.y:.1f} mm, Z={eye_pos.z:.1f} mm", fontsize=config.fonts.title, ) fig.canvas.draw_idle() controls.set_update_callback(update_plot) fig.canvas.mpl_connect("key_press_event", controls.handle_key_press) update_plot() plt.show()
[docs] def plot_interactive_cameras(cameras: list[Camera], eye: Eye, target_point: Position3D) -> None: """Create and run interactive camera comparison with keyboard controls. Args: cameras: List of Camera objects eye: Eye object target_point: Initial target point """ camera_names = [cam.name or f"Camera {i + 1}" for i, cam in enumerate(cameras)] info(f"Camera Comparison: {' vs '.join(camera_names)}") info("=" * 50) InteractiveControls.print_controls() info("=" * 50) config = create_plot_config() controls = InteractiveControls([eye], target_point, step_size=1) # Create figure with 3D view and camera comparison subplot fig = plt.figure(figsize=config.layout.wide_comparison) ax1 = fig.add_subplot(1, 2, 1, projection="3d") ax2 = fig.add_subplot(1, 2, 2) # Reference bounds for consistent 3D view (show all cameras) eye_ref = copy.deepcopy(eye) eye_ref.look_at(target_point) prepared_data_ref = prepare_eye_data_for_plots([eye_ref], [target_point], None, cameras) plot_setup( ax1, prepared_data_ref["eyes_data"], [target_point], None, cameras, prepared_data_ref["cr_3d_lists"], None, None, ) xlim = ax1.get_xlim() ylim = ax1.get_ylim() zlim = ax1.get_zlim() ref_bounds = {"x": xlim, "y": ylim, "z": zlim} # Use centralized colors and markers for camera comparison colors = config.colors.camera_comparison markers = config.markers.camera_comparison def update() -> None: """Update the visualization with current eye position.""" eye.look_at(controls.target_point) prepared_data = prepare_eye_data_for_plots([eye], [controls.target_point], None, cameras) # Clear axes ax1.cla() ax2.cla() # Plot 3D setup (show all cameras) plot_setup( ax1, prepared_data["eyes_data"], [controls.target_point], None, cameras, prepared_data["cr_3d_lists"], ref_bounds, None, ) # Plot camera comparison for all cameras for i, camera in enumerate(cameras): camera_image = prepared_data["camera_images"][i] color = colors[i % len(colors)] marker = markers[i % len(markers)] camera_name = camera.name or f"Camera {i + 1}" # Plot pupil boundary - closed loop if camera_image.pupil_boundary is not None: boundary = camera_image.pupil_boundary pupil_x = [p.x for p in boundary] + [boundary[0].x] pupil_y = [p.y for p in boundary] + [boundary[0].y] linestyle = ( config.lines.solid if i == 0 else config.lines.dashed ) # Solid line for first camera, dashed for others ax2.plot( pupil_x, pupil_y, color=color, linewidth=config.lines.thick_lines, linestyle=linestyle, label=f"Pupil ({camera_name})", ) # Plot pupil center if camera_image.pupil_center is not None: center = camera_image.pupil_center.to_array() ax2.scatter( center[0], center[1], color=color, s=config.markers.small_details, marker=marker, linewidth=config.lines.thick_lines, label=f"Center ({camera_name})", ) # Set up axes resolution = cameras[0].camera_matrix.resolution ax2.set_xlim(-resolution.x / 2, resolution.x / 2) ax2.set_ylim(-resolution.y / 2, resolution.y / 2) ax2.set_xlabel("X (pixels)") ax2.set_ylabel("Y (pixels)") ax2.set_title(f"Camera View Comparison: {' vs '.join(camera_names)}") ax2.grid(config.elements.grid_enabled, alpha=config.lines.grid_alpha) if config.elements.equal_aspect: ax2.set_aspect("equal") ax2.legend() # Calculate and display distortion info if len(cameras) >= 2: # Compare first camera with others first_center = prepared_data["camera_images"][0].pupil_center if first_center is not None: first_center_array = first_center.to_array() # Calculate differences from first camera differences = [] for i in range(1, len(cameras)): camera_center = prepared_data["camera_images"][i].pupil_center if camera_center is not None: camera_center_array = camera_center.to_array() center_diff = np.linalg.norm(camera_center_array - first_center_array) camera_name = cameras[i].name or f"Camera {i + 1}" differences.append(f"{camera_name}={center_diff:.3f}px") # Eye position info eye_pos = eye.position cam_pos = cameras[0].position distance = np.linalg.norm( np.array([eye_pos.x, eye_pos.y, eye_pos.z]) - np.array([cam_pos.x, cam_pos.y, cam_pos.z]) ) fig.suptitle( f"Target: ({controls.target_point.x:.0f}, {controls.target_point.y:.0f}, {controls.target_point.z:.0f})mm, " f"Eye: ({eye_pos.x:.0f}, {eye_pos.y:.0f}, {eye_pos.z:.0f})mm, " f"Distance: {distance:.0f}mm\n" f"Pupil center differences: {', '.join(differences)}", fontsize=config.fonts.subtitle, ) fig.canvas.draw_idle() controls.set_update_callback(update) fig.canvas.mpl_connect("key_press_event", controls.handle_key_press) update() plt.show()
[docs] def plot_interactive_pupil_comparison( eye_elliptical: Eye, eye_realistic: Eye, camera: Camera, target_point: Position3D ) -> None: """Create and run interactive pupil comparison with keyboard controls. Args: eye_elliptical: Elliptical pupil eye object eye_realistic: Realistic pupil eye object camera: Camera object target_point: Initial target point """ initial_pupil_radii = eye_elliptical.get_pupil_radii() info("Pupil Shape Comparison") info("=" * 50) InteractiveControls.print_controls(additional_controls={"Pupil Size ([/])": "[: smaller, ]: bigger"}) config = create_plot_config() def handle_pupil_size_decrease(_event: object, _controls: InteractiveControls) -> None: """Handle [ key - make pupil smaller.""" current_radii = eye_elliptical.get_pupil_radii() new_radius = max(0.5, current_radii[0] - 0.5) eye_elliptical.set_pupil_radii(new_radius, new_radius) eye_realistic.set_pupil_radii(new_radius, new_radius) def handle_pupil_size_increase(_event: object, _controls: InteractiveControls) -> None: """Handle ] key - make pupil bigger.""" current_radii = eye_elliptical.get_pupil_radii() new_radius = min(5, current_radii[0] + 0.5) eye_elliptical.set_pupil_radii(new_radius, new_radius) eye_realistic.set_pupil_radii(new_radius, new_radius) def handle_reset_with_pupil(_event: object, controls: InteractiveControls) -> None: """Reset positions and pupil size.""" # Let controls handle standard reset controls.reset_positions() # Sync realistic eye and reset pupil sizes eye_realistic.trans = eye_elliptical.trans.copy() eye_realistic.position = eye_elliptical.position eye_elliptical.set_pupil_radii(initial_pupil_radii[0], initial_pupil_radii[1]) eye_realistic.set_pupil_radii(initial_pupil_radii[0], initial_pupil_radii[1]) custom_handlers = { "[": handle_pupil_size_decrease, "]": handle_pupil_size_increase, " ": handle_reset_with_pupil, } controls = InteractiveControls([eye_elliptical], target_point, step_size=2.5, custom_handlers=custom_handlers) # Create figure with 3D view and camera comparison subplot fig = plt.figure(figsize=config.layout.wide_comparison) ax1 = fig.add_subplot(1, 2, 1, projection="3d") ax2 = fig.add_subplot(1, 2, 2) eye_ref = copy.deepcopy(eye_elliptical) eye_ref.look_at(target_point) prepared_data_ref = prepare_eye_data_for_plots([eye_ref], [target_point], None, [camera]) plot_setup( ax1, prepared_data_ref["eyes_data"], [target_point], None, [camera], prepared_data_ref["cr_3d_lists"], None, None, ) ref_bounds = {"x": ax1.get_xlim(), "y": ax1.get_ylim(), "z": ax1.get_zlim()} def update() -> None: """Update the visualization with current eye positions.""" # Sync realistic eye to match elliptical eye position eye_realistic.trans = eye_elliptical.trans.copy() eye_realistic.position = eye_elliptical.position # Both eyes look at the same target eye_elliptical.look_at(controls.target_point) eye_realistic.look_at(controls.target_point) # Get pupil images in camera view elliptical_pupil_img, elliptical_center = eye_elliptical.get_pupil_in_camera_image(camera) realistic_pupil_img, realistic_center = eye_realistic.get_pupil_in_camera_image(camera) # Clear axes ax1.cla() ax2.cla() prepared_data = prepare_eye_data_for_plots([eye_elliptical], [controls.target_point], None, [camera]) plot_setup( ax1, prepared_data["eyes_data"], [controls.target_point], None, [camera], prepared_data["cr_3d_lists"], ref_bounds, None, ) # Plot camera comparison ax2.set_title("Pupil Shape Comparison") ax2.set_xlabel("X (pixels)") ax2.set_ylabel("Y (pixels)") ax2.grid(config.elements.grid_enabled, alpha=config.lines.grid_alpha) # Plot elliptical pupil with dashed line - closed loop if elliptical_pupil_img is not None: boundary = elliptical_pupil_img elliptical_x = [p.x for p in boundary] + [boundary[0].x] elliptical_y = [p.y for p in boundary] + [boundary[0].y] ax2.plot( elliptical_x, elliptical_y, color=config.colors.eyes[0], linestyle=config.lines.dashed, linewidth=config.lines.standard_lines, label="Elliptical Pupil", ) if elliptical_center is not None: ax2.plot( elliptical_center.x, elliptical_center.y, color=config.colors.eyes[0], marker="x", markersize=6, label="Elliptical Center", ) # Plot realistic pupil with solid line - closed loop if realistic_pupil_img is not None: boundary = realistic_pupil_img realistic_x = [p.x for p in boundary] + [boundary[0].x] realistic_y = [p.y for p in boundary] + [boundary[0].y] ax2.plot( realistic_x, realistic_y, color=config.colors.eyes[1], linestyle=config.lines.solid, linewidth=config.lines.standard_lines, label="Realistic Pupil", ) if realistic_center is not None: ax2.plot( realistic_center.x, realistic_center.y, color=config.colors.eyes[1], marker="+", markersize=8, label="Realistic Center", ) # Set up axes with camera resolution (same as camera comparison) resolution = camera.camera_matrix.resolution ax2.set_xlim(-resolution.x / 2, resolution.x / 2) ax2.set_ylim(-resolution.y / 2, resolution.y / 2) ax2.set_xlabel("X (pixels)") ax2.set_ylabel("Y (pixels)") ax2.set_title("Pupil Shape Comparison") ax2.grid(config.elements.grid_enabled, alpha=config.lines.grid_alpha) if config.elements.equal_aspect: ax2.set_aspect("equal") ax2.legend() # Update title with current positions and pupil size eye_pos = eye_elliptical.position pupil_radii = eye_elliptical.get_pupil_radii() pupil_diameter = pupil_radii[0] * 2 # diameter in mm fig.suptitle( f"Target X={controls.target_point.x:.1f} mm, Y={controls.target_point.y:.1f} mm, Z={controls.target_point.z:.1f} mm\n" f"Eye X={eye_pos.x:.1f} mm, Y={eye_pos.y:.1f} mm, Z={eye_pos.z:.1f} mm, Pupil diameter: {pupil_diameter:.1f}mm", fontsize=config.fonts.title, ) fig.canvas.draw_idle() controls.set_update_callback(update) fig.canvas.mpl_connect("key_press_event", controls.handle_key_press) update() plt.show()