Import the required libraries
import SimpleCV as scv
import PIL
from PIL import Image, ImageDraw, ImageOps
Change the source path to your video, and then setup a display class.
I am using the notebook display type to get the images inline.
video = scv.VirtualCamera('source/lecture.MOV', 'video')
display = scv.Display(displaytype='notebook')
The loop below uses the display class to iterate through the video and saves the frames to disk.
x=1
while display.isNotDone():
img = video.getImage()
img.save('input/frame{0:03d}.png'.format(x))
x += 1
if display.mouseRight: display.done = True
display.quit()
testimg = scv.Image('input/frame001.png')
filename = testimg.filename
print_img = testimg.copy()
print_img.save(display)
The strategy for building the Kaleidoscope is to ultimately create equilateral triangle, and paste them around the canvas. This is accomplished using 5 functions and a functional inspired convenience function that chains the operations together. First the image is cropped to an equilateral triangle at the bottom center of the image. Next we use that image to build larger geometrical shapes that are easier to manipulate around the canvas. Theoretically the simplest approach would take these initial triangles and simply use a loop to rotate 60 degrees, mirror and paste across the canvas. Unfortunately any part of the image that extends outside the canvas when rotated is clipped. To circumvent this I use an alternative algorithm for rotating the triangles, which results in some minor distortion. The transposed triangle doesn't align with the initial state and a brute force methodology is required. If anyone has a more robust algorithm to apply the rotation, I am happy to update the notebook.
Below I have broken down each of the steps applied to the image and their resulting outputs.
def make_triangle(start_img, save=False):
w, h = start_img.size()
#crop square
inset = (max(w,h) - min(w,h)) / 2
w, h = start_img.size()
sqrimg = start_img.crop(inset, inset, h-inset, w-inset)
#solve equilateral triangle
w, h = sqrimg.size()
mask = Image.new('1', (w,h))
draw = ImageDraw.Draw(mask)
t_height = w/2 * np.tan(60)
triangle_negative = [(0,h), (w/2, t_height), (w,h), (0,h)]
draw.polygon(triangle_negative, fill=255)
output = ImageOps.fit(sqrimg.getPIL(), mask.size, centering=(0.5, 0.5))
output.putalpha(mask)
output = output.crop((0,int(t_height)-15, w, h))
if save:
path = 'output/triangle_{}'.format(filename.split('/')[1])
output.save(path)
return output, path
return output
import IPython.core.display
img, path = make_triangle(testimg, True)
IPython.core.display.Image(path)
# ImageRotate.py
# by Kevin Cazabon, 2000
# kevin@cazabon.com
# for rotating images, automatically expanding canvas to eliminate cropping
sin, cos, pi = np.sin, np.cos, np.pi
def rotateIm(im, rotation, backgroundColor = None):
if backgroundColor == None:
# FIXME this is still ugly... it can leave black corners even with the proper background color
if im.mode == 'RGB':
backgroundColor = (0,0,0)
elif im.mode == 'CMYK':
backgroundColor = (0,0,0,0)
# do the math to figure out how big the image box will be after rotation
rotationRadians = ((2.0*pi)/360.0) * float(rotation)
rotatedWidth = int(float(im.size[0]) * abs(cos(rotationRadians)) + float(im.size[1]) * abs(sin(rotationRadians)) + 0.5)
rotatedHeight = int(float(im.size[0]) * abs(sin(rotationRadians)) + float(im.size[1]) * abs(cos(rotationRadians)) + 0.5)
rotatedSize = (rotatedWidth, rotatedHeight)
# make the image background big enough for the original image in any rotation
if im.size[0] > im.size[1]:
largeImSize = (rotatedSize[0], rotatedSize[0])
else:
largeImSize = (rotatedSize[1], rotatedSize[1])
xOffset = int((float(largeImSize[0] - im.size[0])/2.0)+0.5)
yOffset = int((float(largeImSize[1] - im.size[1])/2.0)+0.5)
bgIm = Image.new(im.mode, largeImSize, backgroundColor)
# paste the im in the center of the bgIm
bgIm.paste(im, (xOffset, yOffset))
# rotate the bgIm
bgIm = bgIm.rotate(rotation)
# crop it down to the real used area
xOffset = int((float(bgIm.size[0] - rotatedSize[0])/2.0) + 0.5)
yOffset = int((float(bgIm.size[1] - rotatedSize[1])/2.0) + 0.5)
bgIm = bgIm.crop((xOffset, yOffset, xOffset + rotatedSize[0], yOffset + rotatedSize[1]))
return bgIm
def make_trapezoid(triangle, save=False):
w, h = triangle.size
can_w, can_h = w*3, h
output = Image.new('RGBA', (can_w, can_h), color=255)
def mirror_paste(last_img, coords):
mirror = rotateIm(ImageOps.mirror(last_img), 60)
output.paste(mirror, (coords), mirror)
return mirror, coords
#paste in bottom left corner
output.paste(triangle,(0, can_h-h), triangle)
last_img, coords = mirror_paste(triangle, (int(w/4.4), -int(h/2.125)))
last_img, coords = mirror_paste(rotateIm(last_img, 120), (int(can_w/7.3), -228))
output = output.crop((0,15, w*2-22, h))
if save:
path = 'output/trapezoid_{}'.format(filename.split('/')[1])
output.save(path)
return output, path
return output
img, path = make_trapezoid(img, True)
IPython.core.display.Image(path)
def make_hexagon(trapezoid, save=False):
w, h = trapezoid.size
output = Image.new('RGBA', (w, h*2), color=255)
output.paste(trapezoid, (0,0), trapezoid)
flip = ImageOps.flip(trapezoid)
output.paste(flip, (0,h), flip)
if save:
path = 'output/hexagon_{}'.format(filename.split('/')[1])
output.save(path)
return output, path
return output
img, path = make_hexagon(img, True)
IPython.core.display.Image(path)
def apply_map(hexagon, save=False):
w, h = hexagon.size
output = Image.new('RGBA', (w*4, h*2), color='GREY')
output.paste(hexagon, (0,-2), hexagon)
output.paste(hexagon, (0,h-2), hexagon)
output.paste(hexagon, (int(-w*.74),h/2), hexagon)
output.paste(hexagon, (int(-w*.74), int(h*1.5)), hexagon)
output.paste(hexagon, (int(-w*.74), int(-h/2)), hexagon)
output.paste(hexagon, (int(w*.75-2),h/2-2), hexagon)
output.paste(hexagon, (int(w*.75-2), int(h*1.5-4)), hexagon)
output.paste(hexagon, (int(w*.75-2), int(-h/2+2)), hexagon)
output.paste(hexagon, (int(w*1.5-7), h-2), hexagon)
output.paste(hexagon, (int(w*1.5-7), -1), hexagon)
output.paste(hexagon, (int(w*2.25-8), int(h*1.5-6)), hexagon)
output.paste(hexagon, (int(w*2.25-10), int(-h/2)), hexagon)
output.paste(hexagon, (int(w*2.25-10),h/2-3), hexagon)
output.paste(hexagon, (int(w*3-15), h), hexagon)
output.paste(hexagon, (int(w*3-16), 3), hexagon)
output.paste(hexagon, (int(w*3.75-20), int(h*1.5)), hexagon)
output.paste(hexagon, (int(w*3.75-22), int(-h/2+10)), hexagon)
output.paste(hexagon, (int(w*3.75-20),h/2+5), hexagon)
output = output.crop((10, 10, output.size[0]-10, output.size[1]-10))
output = output.resize(np.array(output.size)/4)
if save:
path = 'output/kaleidoscope_{}'.format(filename.split('/')[1])
output.save(path)
return output, path
return output
kscope, path = apply_map(img, True)
IPython.core.display.Image(path)
def make_kaleidoscope(img, save=False):
output = apply_map(make_hexagon(make_trapezoid(make_triangle(img))))
if save:
path = 'output/kaleidoscope_{}'.format(filename.split('/')[1])
output.save(path)
return output, path
return output
testimg = scv.Image('input/frame001.png')
filename = testimg.filename
testimg.save(display)
img, path = make_kaleidoscope(testimg, True)
IPython.core.display.Image(path)
Here is the main loop! If you are satisfied with the componenets and the sample kaledioscope output, run this to process your frames. Upon completion I turn to Quicktime 7 Pro, open the directory as an Image sequence and save as a video format. I find 15-20 fps to be a pretty good frame rate.
import os
for frame_path in os.listdir('input/')[1:]:
frame = scv.Image('input/{}'.format(frame_path))
filename = frame.filename
make_kaleidoscope(frame, True)