I've stumbled across the quite cool Youtube video below that is about an optical illusion that is very easy to reproduce just printing a copuple of images.
from IPython.lib.display import YouTubeVideo YouTubeVideo('RBgeKdVHWCs')
The idea seems to be quite simple.
The first image can be obtained by taking every frame of an animation, cutting it into vertical stripes, interleaving the stripes and sticthing them together.
The second image is essentialy a mask obained using the same approach, starting from an animation with the same number of frames of the original one, but made with a white frame, followed by black frames.
By interleaving I mean that once you have sliced the $N$ frames, you create a first block of stripes taking stripe $0$ from the first frame, then stripe $1$ from the second frame and so on so forth up to stripe $N-1$ from the last frame; then you start the second block of stripes by taking stripe $N$ from the first frame, stripe $N+1$ from the second frame and so on. Finally you stitch the blocks together.
Let's try to make it more clear with an example. Consider an animation with three frames, containing the first capital letters of the alphabet suitably colored.
import sys from PIL import Image, ImageFont, ImageDraw import numpy as np font_path = r'C:\Windows\Fonts\Arial.ttf' if sys.platform == 'win32' else 'Arial.ttf' font = ImageFont.truetype(font_path, size = 180) size = font.getsize('A') def rasterize(letter, rgb): img=Image.new('RGB', size, (0, 0, 0)) draw = ImageDraw.Draw(img) draw.text((0, -10), letter, rgb, font) draw = ImageDraw.Draw(img) return np.array(img) def show(images): images = list(Image.fromarray(i) for i in images) widths = np.cumsum([0,] + list(map(lambda _: 10 + _.size, images))) height = images.size result = Image.new('RGB', (widths[-1], height), (255, 255, 255)) for idx in range(len(images)): result.paste(im = images[idx], box = (widths[idx], 0)) return result frames = [ rasterize('A', (255, 0, 0)), rasterize('B', (0, 255, 0)), rasterize('C', (0, 0, 255)) ] show(frames)
To define the function performing the interleaving we'll use quite a few tricks.
First of all, given the
width of the stripes, we generate the indexes where we want to slice the frames.
width = 21 splits = range(width, frames.shape, width) list(splits)
[21, 42, 63, 84, 105]
numpy.hsplit we cut every frame in a list of stripes. For example, for the first frame we get
For the $k$-th frame, we keep just one stripe every
len(frames), that is we use
[k::len(frames)] to index the list of stripes of frame $k$. We collect all such list of stripes in a list.
stripes = [np.hsplit(f, splits)[k::len(frames)] for k, f in enumerate(frames)]
Again, for the first frame, we keep just the first and fourth stripes
Once we have such a list we
zip it and use
numpy.hstack to stitch together one stripe per frame obaining blocks of $N$ stripes each, finally we again use
numpy.hstack to stitch every block together.
result = np.hstack(np.hstack(s) for s in zip(*stripes)) show([result])
We are ready to put together our tricks.
def interleave(frames, width): splits = range(width, frames.shape, width) return np.hstack(np.hstack(s) for s in zip(*(np.hsplit(f, splits)[k::len(frames)] for k, f in enumerate(frames))))
Let's test it with a smaller width
result = interleave(frames, 7) show([result])
It's easy to note that there is no overlap among frames; since we have choosen red, green and blue, we can watch at a channel at a time to check this
show([result[:,:,c] for c in range(3)])
To produce a more interesting result, let's start from an animated GIF; we need first of all to extract the frames.
def extract_frames(path): result =  frame = Image.open(path) n = 0 while frame: result.append(np.array(frame.convert("RGB"))) n += 1 try: frame.seek(n) except EOFError: break return result
As an example, I choose a waling duck.
from IPython.display import HTML HTML('<img src="duck.gif" style="width: 200px">')
Since such GIF contains too many frames and every frame has a lot of white space around the main subject, we pick a frame every 3 and crop it.
frames = [f[100:650,300:800,:] for f in extract_frames('duck.gif')[::3]] show(frames)
width = 4 img = Image.fromarray(interleave(frames, width)) img.save('duck-i.png') img
white = (255 * np.ones(frames.shape)).astype(np.uint8) black = np.zeros(frames.shape).astype(np.uint8)
mask = Image.fromarray(interleave([white, ] + [black for _ in range(len(frames) - 1)], width)) mask.save('mask.png') mask
You can now print the two images and try if it works!