I am trying to write a script to remove the vertical perspective from a camera relative to the center of the view frame (e.g. I want all vertical lines to be parallel when viewed from the camera). In order to achieve this, I set the camera's local x rotation to 90, and then need to compensate using the y-shift is order to keep what was exactly at the center of the frame in the same position.
I am struggling with figuring out how to determine the proper y-shift to achieve this. This is my current attempt, which unfortunately does not work:
def auto_remove_camera_vertical_perspective(cam_obj):
# ==Add plane and align to cam frustum==
bpy.ops.mesh.primitive_plane_add(
enter_editmode=False,
align="WORLD",
location=(0, 0, 0),
scale=(1, 1, 1),
)
view_plane = bpy.context.object
# Align rotation
bpy.ops.object.constraint_add(type="COPY_ROTATION")
view_plane.constraints["Copy Rotation"].target = cam_obj
bpy.ops.constraint.apply(constraint="Copy Rotation", owner="OBJECT")
# Align location using the cam view frame plane coords
cam_data = cam_obj.data
matrix = cam_obj.matrix_world.normalized()
frame_points = [matrix @ v for v in cam_data.view_frame(scene=bpy.context.scene)]
x = sum(p.x for p in frame_points) / 4
y = sum(p.y for p in frame_points) / 4
z = sum(p.z for p in frame_points) / 4
center_point = Vector((x, y, z))
view_plane.location = center_point
# Scale plane
scale_factor = 10_000
view_plane.scale = Vector([scale_factor] * 3)
# Apply transforms
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# ==Ray cast to plane==
# Get the camera's location and rotation
ray_origin = cam_obj.location
cam_rot = cam_obj.rotation_euler.to_matrix()
# Calculate the ray direction from camera's facing
ray_direction = cam_rot @ Vector((0, 0, -1))
# Cast the ray from the camera origin onto the object
ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction)
ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction)
loc_1 = ray_cast_result[1]
bpy.ops.object.empty_add(location=loc_1)
bpy.context.object.name = "ray_hit"
# Local x rotation set to 90 vector
cam_rot_corrected = Euler(cam_obj.rotation_euler)
cam_rot_corrected.x = math.radians(90.0)
cam_rot_corrected = cam_rot_corrected.to_matrix()
ray_direction = cam_rot_corrected @ Vector((0, 0, -1))
# Cast the ray from the camera origin onto the object
ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction)
ray_cast_result = view_plane.ray_cast(ray_origin, ray_direction)
loc_2 = ray_cast_result[1]
bpy.ops.object.empty_add(location=loc_2)
bpy.context.object.name = "ray_hit"
# ==Calculate y-shift==
shift_length = abs(loc_2.z - loc_1.z)
shift_sign = -1.0 if loc_2.z > loc_1.z else 1.0
# NOTE: Since the sensor is square and the aspect ratio is 16:9 (1920x1080), the width of the
# view frame should correspond to the full length of the sensor.
# TODO: I think this is probably the broken area.
# the sensor and the view frame.
sensor_length = (frame_points[1] - frame_points[3]).length
shift_amount = shift_sign * (shift_length / sensor_length)
# ==Apply y-shift and remove cam local x rotation
cam_obj.rotation_euler.x = math.radians(90.0)
cam_data.shift_y = shift_amount
# ==Cleanup==
bpy.ops.object.select_all(action="DESELECT")
view_plane.select_set(True)
bpy.ops.object.delete(
use_global=False,
confirm=False,
)
```