I wrote a short python script to generate optical illusion buttons that can’t be read very well unless you tilt the screen or image to a very low angle, almost edge-on, yielding images like this:
If you’re doing it right, you should see the phrase, “NPR COOL DAD ROCK.”
The code is below, to generate an 18-character-or-less button. There are two things to be aware of: the only two dependencies are PIL and NumPy, and if you’re not on a Debian-based Linux system, you may need to adjust a line of code near the top to find the font you want. Try experimenting with parameters and fonts, some definitely work better than others. Narrow fonts tend to look pretty good. I’d love to optimize for speed, but it only takes about 3 or 4 seconds to run on my machine. This should work with either Python 2 or 3 (tested with 3.5). This repo is now hosted on GitHub here.
from PIL import ImageFont from PIL import Image from PIL import ImageDraw from PIL import ImageOps from numpy import sqrt illusion_text = "NPR COOL DAD ROCK" text_color = (0, 0, 0) background_color = (255, 255, 255) img_side = 1024 font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-ExtraLight.ttf" charmax = 18 font_size_guess = 7*(30-len(illusion_text)) crop_width_x = 12 crop_width_y = 3 num_rotations = 6 darkness_threshold = 108 if len(illusion_text) <= charmax: pass else: print('WARNING: Text too long. Exiting.') raise SystemExit(0) img_size = (img_side, img_side) img_size_text = (img_side, img_side) raw_img = Image.new("RGB", img_size_text, background_color) img = Image.new("RGBA", img_size, background_color) circle_img = Image.new("RGBA", img_size, background_color) full_image = Image.new("RGBA", img_size, background_color) draw = ImageDraw.Draw(raw_img) # step through font sizes to find optimal font for box for font_trial in range(font_size_guess-30, font_size_guess+30): possible_font = ImageFont.truetype(font_path, font_trial) raw_img = Image.new("RGB", img_size_text, background_color) draw = ImageDraw.Draw(raw_img) draw.text((crop_width_x, crop_width_y), illusion_text, text_color, font=possible_font) # find bounding box of text by inversion inverted = ImageOps.invert(raw_img) possible_boundingbox = (inverted.getbbox()[0] - crop_width_x, \ inverted.getbbox()[1] - crop_width_y, \ inverted.getbbox()[2] + crop_width_x, \ inverted.getbbox()[3] + crop_width_y) if possible_boundingbox[2] - possible_boundingbox[0] < img_side-2*crop_width_x: boundingbox = possible_boundingbox font_size = font_trial else: break raw_img = Image.new("RGB", img_size_text, background_color) draw = ImageDraw.Draw(raw_img) font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-ExtraLight.ttf", font_size) draw.text((crop_width_x, crop_width_y), illusion_text, text_color, font=font) inverted = ImageOps.invert(raw_img) raw_img = raw_img.crop(boundingbox) scaled_img = raw_img.resize((img_side, img_side), Image.BICUBIC) img.paste(scaled_img, (0, 0)) # map points in the square image to points in a circle # turn light grey and white to alpha channel. Blacken dark grays. pixdata = img.load() for y in range(img.size[1]): for x in range(img.size[0]): if pixdata[x, y] == (255, 255, 255, 255): pixdata[x, y] = (255, 255, 255, 0) elif pixdata[x, y][1] >= darkness_threshold: pixdata[x, y] = (255, 255, 255, 0) elif pixdata[x, y][1] <= darkness_threshold: pixdata[x, y] = (0, 0, 0, 255) circle_img.paste(img, (0, 0)) pixdata2 = circle_img.load() for x in range(img_side): Ysize = 2 * sqrt((img_side / 2) ** 2 - (x - (img_side / 2)) ** 2) for y in range(img_side): Yoffset = int((img_side-Ysize)/2.) Y = Yoffset + int((Ysize/img_side)*y) pixdata2[x, Y] = pixdata[x, y] if sqrt((x-img_side/2)**2 + (y-img_side/2)**2) >= img_side/2 - 2: pixdata2[x, y] = (255, 255, 255, 0) for i in range(num_rotations): this_circle = circle_img.rotate(i*180/num_rotations) full_image.paste(this_circle, (0, 0), this_circle) pixdata = full_image.load() for y in range(full_image.size[1]): for x in range(full_image.size[0]): if pixdata[x, y] == (255, 255, 255, 0): pixdata[x, y] = (255, 255, 255, 255) full_image.save(illusion_text+".png")