In the dark, dank, dismal, and pre-digital ages people would manually warp polaroid prints. I can see the attraction but it is one-off and rather unpredictable. We can do better now.
The algorithm is rather simple:
for each point in the image:
calculate the warped location of the point and put the value of the image from that point at the current location
Without much further ado
Starting from my headshot:
We can distort it to make me look like an alien
or a surrealist (I actually like this one a lot better):
(I can’t for the life of me remember the name of the work that inspired this – it’s a pen sketch of a famous critic)
Anyway here’s the python code – which is surprisingly short.
#!/usr/bin/python3
# (c) 2024 Treadco
# read an image and a "warpmap" then output a distorted image.
# meant to mimic the mechanical distortion of an SX70 polaroid image
#
# warpmap defines the geometry of the distortion
# height,cx,cy
# map the coordinate to a new position and put the color of that position in the coordinates.
#
import numpy as np
import sys,os
import math
from PIL import Image
from PIL import ImageChops
from PIL import ImageColor
#from pylab import * # not used
def warper( x,y, heights, centers):
xx = x
yy = y
for i in range(0, len(heights)): #yes I could use a tuple of heights,.. but this is OK
dx = xx - centers[i][0]
dy = yy - centers[i][1]
dr = dx*dx + dy*dy
dr = math.sqrt(dr)
# if dr < 0.001:
# continue
x += dx*heights[i]/(dr+abs(heights[i]))
y += dy*heights[i]/(dr+abs(heights[i]))
#was y += dy*heights[i]/(dr+1.)
return (x,y)
def main():
try:
image = Image.open(sys.argv[1])
except IndexError:
print("Could not open the input \nUsage warp inputfile warpmap outputname")
sys.exit()
try:
pushmap = open(sys.argv[2])
except IndexError:
print("Could not open the input \nUsage warp inputfile warpmap outputname")
sys.exit()
if len( sys.argv) < 4:
outputfilename = "output.jpg"
else:
outputfilename = sys.argv[3]
height = []
centers = []
for line in pushmap:
s = line.split()
print(s)
height.append( float(s[0]))
centers.append( (float(s[1]), float(s[2])) )
iarray = np.array(image.convert("RGB"))
nx = iarray.shape[0]
ny = iarray.shape[1]
#
# for i in range(0,int(centers[0][0])):
# for j in range(0,int(centers[0][1])):
# print( str(i)+' '+str(j)+' '+str(warper(i,j, height, centers) ) )
# sys.exit()
#
outarray = np.array(Image.new("RGB",(ny,nx),ImageColor.getcolor("white","RGB")))
print( outarray.shape, iarray.shape)
for i in range(0,nx):
for j in range(0,ny):
dc = warper(i,j,height,centers)
ii = int(dc[0])
jj = int(dc[1])
if( ii < 0 or ii >= nx):
continue
if( jj < 0 or jj >= ny):
continue
outarray[i,j] = iarray[ii,jj]
inew = Image.fromarray( outarray)
inew.save(outputfilename)
main()
There’s more boilerplate about opening files and syntactical sugar than lines of code to implement the algorithm. That’s rare in python.