def inf_zero_one_to_triple(p, q, r): """returns SL(2,C) matrix that sends the three points infinity, zero, one to given input points p,q,r""" ### infinity = [1,0], zero = [0,1], one = [1,1] in CP^1 p1, p2 = p q1, q2 = q r1, r2 = r M = [[p1, q1], [p2, q2]] Minv = matrix2_inv(M) [mu, lam] = matrix_mult_vector(matrix2_inv([[p1, q1], [p2, q2]]), [r1, r2]) return [[mu * p1, lam * q1], [mu * p2, lam * q2]]
def inf_zero_one_to_triple(p,q,r): """returns SL(2,C) matrix that sends the three points infinity, zero, one to given input points p,q,r""" ### infinity = [1,0], zero = [0,1], one = [1,1] in CP^1 p1,p2=p q1,q2=q r1,r2=r M = [[p1,q1],[p2,q2]] Minv = matrix2_inv(M) [mu,lam] = matrix_mult_vector(matrix2_inv([[p1,q1],[p2,q2]]), [r1,r2]) return [[mu*p1, lam*q1],[mu*p2, lam*q2]]
def apply_SL2C_elt_to_image(M, source_image_filename, out_x_size=None, save_filename="sphere_transforms_test.png"): """apply an element of SL(2,C) (i.e. a matrix) to an equirectangular image file""" Minv = matrix2_inv( M ) # to push an image forwards by M, we pull the pixel coordinates of the output backwards source_image = Image.open(source_image_filename) s_im = source_image.load() # faster pixel by pixel access in_x_size, in_y_size = source_image.size if out_x_size == None: out_x_size, out_y_size = source_image.size else: out_y_size = out_x_size / 2 out_image = Image.new("RGB", (out_x_size, out_y_size)) o_im = out_image.load() # faster pixel by pixel access for i in range(out_x_size): for j in range(out_y_size): pt = (i, j) pt = angles_from_pixel_coords(pt, x_size=out_x_size) pt = sphere_from_angles(pt) pt = CP1_from_sphere(pt) pt = matrix_mult_vector(Minv, pt) pt = sphere_from_CP1(pt) pt = angles_from_sphere(pt) pt = pixel_coords_from_angles(pt, x_size=in_x_size) o_im[i, j] = get_interpolated_pixel_colour(pt, s_im, x_size=in_x_size) out_image.save(save_filename)
def zoom_in_on_pixel_coords(p, zoom_factor, x_size=1000): """p is pixel coordinate point, return SL(2,C) matrix zooming in on p by a factor of scale""" # Note that the zoom factor is only accurate at the point p itself. As we move away from p, we zoom less and less. # We zoom with the inverse zoom_factor towards/away from the antipodal point to p. M_rot = rotate_pixel_coords_p_to_q(p, (0, 0), x_size=x_size) M_scl = [[zoom_factor, 0], [0, 1]] ### zoom in on zero in CP^1 return matrix_mult(matrix_mult(matrix2_inv(M_rot), M_scl), M_rot)
def apply_SL2C_elt_to_image(M, source_image_filename, out_x_size = None, save_filename = "sphere_transforms_test.png"): """apply an element of SL(2,C) (i.e. a matrix) to an equirectangular image file""" Minv = matrix2_inv(M) # to push an image forwards by M, we pull the pixel coordinates of the output backwards source_image = Image.open(source_image_filename) s_im = source_image.load() # faster pixel by pixel access in_x_size, in_y_size = source_image.size if out_x_size == None: out_x_size, out_y_size = source_image.size else: out_y_size = out_x_size/2 out_image = Image.new("RGB", (out_x_size, out_y_size)) o_im = out_image.load() # faster pixel by pixel access for i in range(out_x_size): for j in range(out_y_size): pt = (i,j) pt = angles_from_pixel_coords(pt, x_size = out_x_size) pt = sphere_from_angles(pt) pt = CP1_from_sphere(pt) pt = matrix_mult_vector(Minv, pt) pt = sphere_from_CP1(pt) pt = angles_from_sphere(pt) pt = pixel_coords_from_angles(pt, x_size = in_x_size) o_im[i,j] = get_interpolated_pixel_colour(pt, s_im, x_size = in_x_size) out_image.save(save_filename)
def zoom_in_on_pixel_coords(p, zoom_factor, x_size = 1000): """p is pixel coordinate point, return SL(2,C) matrix zooming in on p by a factor of scale""" # Note that the zoom factor is only accurate at the point p itself. As we move away from p, we zoom less and less. # We zoom with the inverse zoom_factor towards/away from the antipodal point to p. M_rot = rotate_pixel_coords_p_to_q( p, (0,0), x_size = x_size) M_scl = [[zoom_factor,0],[0,1]] ### zoom in on zero in CP^1 return matrix_mult( matrix_mult(matrix2_inv(M_rot), M_scl), M_rot )
def zoom_along_axis_sphere_points_p_q(p, q, zoom_factor): """p and q are points on sphere, return SL(2,C) matrix zooming along axis from p to q""" p, q = vector(p), vector(q) CP1p, CP1q = CP1_from_sphere(p), CP1_from_sphere(q) assert dot(p,q) < 0.9999 #points should not be in the same place r = get_vector_perp_to_p_and_q(p, q) CP1r = CP1_from_sphere(r) M_standardise = two_triples_to_SL(CP1p, CP1q, CP1r, [0,1], [1,0], [1,1]) M_theta = [[zoom_factor,0],[0,1]] return matrix_mult( matrix_mult(matrix2_inv(M_standardise), M_theta), M_standardise )
def rotate_around_axis_sphere_points_p_q(p,q,theta): """p and q are points on sphere, return SL(2,C) matrix rotating by angle theta around the axis from p to q""" p, q = vector(p), vector(q) CP1p, CP1q = CP1_from_sphere(p), CP1_from_sphere(q) assert dot(p,q) < 0.9999, "axis points should not be in the same place!" r = get_vector_perp_to_p_and_q(p, q) CP1r = CP1_from_sphere(r) M_standardise = two_triples_to_SL(CP1p, CP1q, CP1r, [0,1], [1,0], [1,1]) M_theta = [[complex(cos(theta),sin(theta)),0],[0,1]] #rotate on axis through 0, infty by theta return matrix_mult( matrix_mult(matrix2_inv(M_standardise), M_theta), M_standardise )
def zoom_along_axis_sphere_points_p_q(p, q, zoom_factor): """p and q are points on sphere, return SL(2,C) matrix zooming along axis from p to q""" p, q = vector(p), vector(q) CP1p, CP1q = CP1_from_sphere(p), CP1_from_sphere(q) assert dot(p, q) < 0.9999 #points should not be in the same place r = get_vector_perp_to_p_and_q(p, q) CP1r = CP1_from_sphere(r) M_standardise = two_triples_to_SL(CP1p, CP1q, CP1r, [0, 1], [1, 0], [1, 1]) M_theta = [[zoom_factor, 0], [0, 1]] return matrix_mult(matrix_mult(matrix2_inv(M_standardise), M_theta), M_standardise)
def rotate_around_axis_sphere_points_p_q(p, q, theta): """p and q are points on sphere, return SL(2,C) matrix rotating by angle theta around the axis from p to q""" p, q = vector(p), vector(q) CP1p, CP1q = CP1_from_sphere(p), CP1_from_sphere(q) assert dot(p, q) < 0.9999, "axis points should not be in the same place!" r = get_vector_perp_to_p_and_q(p, q) CP1r = CP1_from_sphere(r) M_standardise = two_triples_to_SL(CP1p, CP1q, CP1r, [0, 1], [1, 0], [1, 1]) M_theta = [[complex(cos(theta), sin(theta)), 0], [0, 1]] #rotate on axis through 0, infty by theta return matrix_mult(matrix_mult(matrix2_inv(M_standardise), M_theta), M_standardise)
def droste_effect(zoom_center_pixel_coords, zoom_factor, zoom_cutoff, source_image_filename, out_x_size = None, twist = False, zoom_loop_value = 0.0, save_filename = "sphere_transforms_test.png"): """produces a zooming Droste effect image from an equirectangular source image""" # The source image should be a composite of the original image together with a zoomed version, # fit inside a picture frame or similar in the original image source_image = Image.open(source_image_filename) s_im = source_image.load() in_x_size, in_y_size = source_image.size M_rot = rotate_pixel_coords_p_to_q(zoom_center_pixel_coords, (0,0), x_size = in_x_size) M_rot_inv = matrix2_inv(M_rot) if out_x_size == None: out_x_size, out_y_size = source_image.size else: out_y_size = out_x_size/2 out_image = Image.new("RGB", (out_x_size, out_y_size)) o_im = out_image.load() droste_factor = ( cmath.log(zoom_factor) + complex(0, 2*pi) ) / complex(0, 2*pi) # used if twisting for i in range(out_x_size): for j in range(out_y_size): pt = (i,j) pt = angles_from_pixel_coords(pt, x_size = out_x_size) pt = sphere_from_angles(pt) pt = CP1_from_sphere(pt) pt = matrix_mult_vector(M_rot, pt) # if ever you don't know how to do some operation in complex projective coordinates, it's almost certainly # safe to just switch back to ordinary complex numbers by pt = pt[0]/pt[1]. The only danger is if pt[1] == 0, # or is near enough to cause floating point errors. In this application, you are probably fine unless you # make some very specific choices of where to zoom to etc. pt = pt[0]/pt[1] pt = cmath.log(pt) if twist: # otherwise straight zoom pt = droste_factor * pt # zoom_loop_value is between 0 and 1, vary this from 0.0 to 1.0 to animate frames zooming into the droste image pt = complex(pt.real + log(zoom_factor) * zoom_loop_value, pt.imag) # zoom_cutoff alters the slice of the input image that we use, so that it covers mostly the original image, together with # some of the zoomed image that was composited with the original. The slice needs to cover the seam between the two # (i.e. the picture frame you are using, but should cover as little as possible of the zoomed version of the image. pt = complex((pt.real + zoom_cutoff) % log(zoom_factor) - zoom_cutoff, pt.imag) pt = cmath.exp(pt) pt = [pt, 1] #back to projective coordinates pt = matrix_mult_vector(M_rot_inv, pt) pt = sphere_from_CP1(pt) pt = angles_from_sphere(pt) pt = pixel_coords_from_angles(pt, x_size = in_x_size) o_im[i,j] = get_interpolated_pixel_colour(pt, s_im, in_x_size) out_image.save(save_filename)
def droste_effect(zoom_center_pixel_coords, zoom_factor, zoom_cutoff, source_image_filename, out_x_size=None, twist=False, zoom_loop_value=0.0, save_filename="sphere_transforms_test.png"): """produces a zooming Droste effect image from an equirectangular source image""" # The source image should be a composite of the original image together with a zoomed version, # fit inside a picture frame or similar in the original image source_image = Image.open(source_image_filename) s_im = source_image.load() in_x_size, in_y_size = source_image.size M_rot = rotate_pixel_coords_p_to_q(zoom_center_pixel_coords, (0, 0), x_size=in_x_size) M_rot_inv = matrix2_inv(M_rot) if out_x_size == None: out_x_size, out_y_size = source_image.size else: out_y_size = out_x_size / 2 out_image = Image.new("RGB", (out_x_size, out_y_size)) o_im = out_image.load() droste_factor = (cmath.log(zoom_factor) + complex(0, 2 * pi)) / complex( 0, 2 * pi) # used if twisting for i in range(out_x_size): for j in range(out_y_size): pt = (i, j) pt = angles_from_pixel_coords(pt, x_size=out_x_size) pt = sphere_from_angles(pt) pt = CP1_from_sphere(pt) pt = matrix_mult_vector(M_rot, pt) # if ever you don't know how to do some operation in complex projective coordinates, it's almost certainly # safe to just switch back to ordinary complex numbers by pt = pt[0]/pt[1]. The only danger is if pt[1] == 0, # or is near enough to cause floating point errors. In this application, you are probably fine unless you # make some very specific choices of where to zoom to etc. pt = pt[0] / pt[1] pt = cmath.log(pt) if twist: # otherwise straight zoom pt = droste_factor * pt # zoom_loop_value is between 0 and 1, vary this from 0.0 to 1.0 to animate frames zooming into the droste image pt = complex(pt.real + log(zoom_factor) * zoom_loop_value, pt.imag) # zoom_cutoff alters the slice of the input image that we use, so that it covers mostly the original image, together with # some of the zoomed image that was composited with the original. The slice needs to cover the seam between the two # (i.e. the picture frame you are using, but should cover as little as possible of the zoomed version of the image. pt = complex( (pt.real + zoom_cutoff) % log(zoom_factor) - zoom_cutoff, pt.imag) pt = cmath.exp(pt) pt = [pt, 1] #back to projective coordinates pt = matrix_mult_vector(M_rot_inv, pt) pt = sphere_from_CP1(pt) pt = angles_from_sphere(pt) pt = pixel_coords_from_angles(pt, x_size=in_x_size) o_im[i, j] = get_interpolated_pixel_colour(pt, s_im, in_x_size) out_image.save(save_filename)
def two_triples_to_SL(a1, b1, c1, a2, b2, c2): """returns SL(2,C) matrix that sends the three CP^1 points a1,b1,c1 to a2,b2,c2""" return matrix_mult(inf_zero_one_to_triple(a2, b2, c2), matrix2_inv(inf_zero_one_to_triple(a1, b1, c1)))
def two_triples_to_SL(a1,b1,c1,a2,b2,c2): """returns SL(2,C) matrix that sends the three CP^1 points a1,b1,c1 to a2,b2,c2""" return matrix_mult( inf_zero_one_to_triple(a2,b2,c2), matrix2_inv(inf_zero_one_to_triple(a1,b1,c1) ) )