import numpy as np
def rotate_coords(x, y, theta, ox, oy):
"""Rotate arrays of coordinates x and y by theta radians about the
point (ox, oy).
"""
s, c = np.sin(theta), np.cos(theta)
x, y = np.asarray(x) - ox, np.asarray(y) - oy
return x * c - y * s + ox, x * s + y * c + oy
def rotate_image(src, theta, ox, oy, fill=255):
"""Rotate the image src by theta radians about (ox, oy).
Pixels in the result that don't correspond to pixels in src are
replaced by the value fill.
"""
# Images have origin at the top left, so negate the angle.
theta = -theta
# Dimensions of source image. Note that scipy.misc.imread loads
# images in row-major order, so src.shape gives (height, width).
sh, sw = src.shape
# Rotated positions of the corners of the source image.
cx, cy = rotate_coords([0, sw, sw, 0], [0, 0, sh, sh], theta, ox, oy)
# Determine dimensions of destination image.
dw, dh = (int(np.ceil(c.max() - c.min())) for c in (cx, cy))
# Coordinates of pixels in destination image.
dx, dy = np.meshgrid(np.arange(dw), np.arange(dh))
# Corresponding coordinates in source image. Since we are
# transforming dest-to-src here, the rotation is negated.
sx, sy = rotate_coords(dx + cx.min(), dy + cy.min(), -theta, ox, oy)
# Select nearest neighbour.
sx, sy = sx.round().astype(int), sy.round().astype(int)
# Mask for valid coordinates.
mask = (0 <= sx) & (sx < sw) & (0 <= sy) & (sy < sh)
# Create destination image.
dest = np.empty(shape=(dh, dw), dtype=src.dtype)
# Copy valid coordinates from source image.
dest[dy[mask], dx[mask]] = src[sy[mask], sx[mask]]
# Fill invalid coordinates.
dest[dy[~mask], dx[~mask]] = fill
return dest
aW1wb3J0IG51bXB5IGFzIG5wCgpkZWYgcm90YXRlX2Nvb3Jkcyh4LCB5LCB0aGV0YSwgb3gsIG95KToKICAgICIiIlJvdGF0ZSBhcnJheXMgb2YgY29vcmRpbmF0ZXMgeCBhbmQgeSBieSB0aGV0YSByYWRpYW5zIGFib3V0IHRoZQogICAgcG9pbnQgKG94LCBveSkuCgogICAgIiIiCiAgICBzLCBjID0gbnAuc2luKHRoZXRhKSwgbnAuY29zKHRoZXRhKQogICAgeCwgeSA9IG5wLmFzYXJyYXkoeCkgLSBveCwgbnAuYXNhcnJheSh5KSAtIG95CiAgICByZXR1cm4geCAqIGMgLSB5ICogcyArIG94LCB4ICogcyArIHkgKiBjICsgb3kKCmRlZiByb3RhdGVfaW1hZ2Uoc3JjLCB0aGV0YSwgb3gsIG95LCBmaWxsPTI1NSk6CiAgICAiIiJSb3RhdGUgdGhlIGltYWdlIHNyYyBieSB0aGV0YSByYWRpYW5zIGFib3V0IChveCwgb3kpLgogICAgUGl4ZWxzIGluIHRoZSByZXN1bHQgdGhhdCBkb24ndCBjb3JyZXNwb25kIHRvIHBpeGVscyBpbiBzcmMgYXJlCiAgICByZXBsYWNlZCBieSB0aGUgdmFsdWUgZmlsbC4KCiAgICAiIiIKICAgICMgSW1hZ2VzIGhhdmUgb3JpZ2luIGF0IHRoZSB0b3AgbGVmdCwgc28gbmVnYXRlIHRoZSBhbmdsZS4KICAgIHRoZXRhID0gLXRoZXRhCgogICAgIyBEaW1lbnNpb25zIG9mIHNvdXJjZSBpbWFnZS4gTm90ZSB0aGF0IHNjaXB5Lm1pc2MuaW1yZWFkIGxvYWRzCiAgICAjIGltYWdlcyBpbiByb3ctbWFqb3Igb3JkZXIsIHNvIHNyYy5zaGFwZSBnaXZlcyAoaGVpZ2h0LCB3aWR0aCkuCiAgICBzaCwgc3cgPSBzcmMuc2hhcGUKCiAgICAjIFJvdGF0ZWQgcG9zaXRpb25zIG9mIHRoZSBjb3JuZXJzIG9mIHRoZSBzb3VyY2UgaW1hZ2UuCiAgICBjeCwgY3kgPSByb3RhdGVfY29vcmRzKFswLCBzdywgc3csIDBdLCBbMCwgMCwgc2gsIHNoXSwgdGhldGEsIG94LCBveSkKCiAgICAjIERldGVybWluZSBkaW1lbnNpb25zIG9mIGRlc3RpbmF0aW9uIGltYWdlLgogICAgZHcsIGRoID0gKGludChucC5jZWlsKGMubWF4KCkgLSBjLm1pbigpKSkgZm9yIGMgaW4gKGN4LCBjeSkpCgogICAgIyBDb29yZGluYXRlcyBvZiBwaXhlbHMgaW4gZGVzdGluYXRpb24gaW1hZ2UuCiAgICBkeCwgZHkgPSBucC5tZXNoZ3JpZChucC5hcmFuZ2UoZHcpLCBucC5hcmFuZ2UoZGgpKQoKICAgICMgQ29ycmVzcG9uZGluZyBjb29yZGluYXRlcyBpbiBzb3VyY2UgaW1hZ2UuIFNpbmNlIHdlIGFyZQogICAgIyB0cmFuc2Zvcm1pbmcgZGVzdC10by1zcmMgaGVyZSwgdGhlIHJvdGF0aW9uIGlzIG5lZ2F0ZWQuCiAgICBzeCwgc3kgPSByb3RhdGVfY29vcmRzKGR4ICsgY3gubWluKCksIGR5ICsgY3kubWluKCksIC10aGV0YSwgb3gsIG95KQoKICAgICMgU2VsZWN0IG5lYXJlc3QgbmVpZ2hib3VyLgogICAgc3gsIHN5ID0gc3gucm91bmQoKS5hc3R5cGUoaW50KSwgc3kucm91bmQoKS5hc3R5cGUoaW50KQoKICAgICMgTWFzayBmb3IgdmFsaWQgY29vcmRpbmF0ZXMuCiAgICBtYXNrID0gKDAgPD0gc3gpICYgKHN4IDwgc3cpICYgKDAgPD0gc3kpICYgKHN5IDwgc2gpCgogICAgIyBDcmVhdGUgZGVzdGluYXRpb24gaW1hZ2UuCiAgICBkZXN0ID0gbnAuZW1wdHkoc2hhcGU9KGRoLCBkdyksIGR0eXBlPXNyYy5kdHlwZSkKCiAgICAjIENvcHkgdmFsaWQgY29vcmRpbmF0ZXMgZnJvbSBzb3VyY2UgaW1hZ2UuCiAgICBkZXN0W2R5W21hc2tdLCBkeFttYXNrXV0gPSBzcmNbc3lbbWFza10sIHN4W21hc2tdXQoKICAgICMgRmlsbCBpbnZhbGlkIGNvb3JkaW5hdGVzLgogICAgZGVzdFtkeVt+bWFza10sIGR4W35tYXNrXV0gPSBmaWxsCgogICAgcmV0dXJuIGRlc3Q=