Better QR codes
Snake Race codes to replace QR codes. With NumPy, rotation matrix and animated images!
QR codes are quite interesting, but boring to look at. For this reason I introduce SR codes, which stands for Snake Race codes. It improves QR codes on several fronts:
- Animation!
- Color!
- Hardcore. QR codes have error correction. SR codes don't. If you make an error it's game over.
- Not quick. Savour the wait before you can scan the code.
Developing a scanner app is left as an exercise for the reader :)
QR codes are really interesting. Error correction and packing as much information as you can in a small amount of pixels while keeping it robust is a nice challenge.
Nice explanation in Dutch https://michielp1807.github.io/qr-codes/ or check out the Wikipedia page.
We represent the position of the snake as a point $(x, y)$ with an orientation $(h, v)$. Taking turns is done with a rotation matrix that defines 90 degree turns and going straight (=not changing direction).
actions = [
# Left
np.array([
[0, 1],
[-1, 0]
]),
# Straight
np.array([
[1, 0],
[0, 1]
]),
# Right
np.array([
[0, -1],
[1, 0]
])
]
For example, if the snake is going to the right it has orientation $(1,0)$. If it makes a 90 degree turn to the left it goes up and has orientation $(0, -1)$. The first coordinate is horizontal and the second is vertical.
actions[0] @ np.array([1, 0])
Going left four times we should end up in the same direction:
orientation = np.array([1, 0])
for i in range(4):
orientation = actions[0] @ orientation
orientation
Going places
We want to see where the snakes have been as well as their current position. Below we define:
- The different snakes in
snakes
, including start position, color and trail - For each snake we also keep track if they are active. The snake stops if there is no valid move to make.
- To make our lives easy we have per position the information if some snake has been there to limit possible moves
matrix
Define a method to generat the trails of the snakes
def generate_trails(size: int, bits):
# Define quarter distance
q = size // 4
# Define the start positions of all snakes
snakes = [
{
'c': 'Greens',
'position': np.array([q+1, q+1]),
'orientation': np.array([1, 0]),
'trail': np.full((2*(size+2), 2*(size+2)), 0),
'active': True
},
{
'c': 'Reds',
'position': np.array([3*q+1, 1+1]),
'orientation': np.array([0, 1]),
'trail': np.full((2*(size+2), 2*(size+2)), 0),
'active': True
},
{
'c': 'Blues',
'position': np.array([3*q+1, 3*q+1]),
'orientation': np.array([-1, 0]),
'trail': np.full((2*(size+2), 2*(size+2)), 0),
'active': True
},
{
'c': 'Purples',
'position': np.array([q+1, 3*q+1]),
'orientation': np.array([0, -1]),
'trail': np.full((2*(size+2), 2*(size+2)), 0),
'active': True
}
]
# Keep track of where the snakes have passed
matrix = np.full((size+2, size+2), False)
# Set the borders
matrix[0, :] = True
matrix[:, 0] = True
matrix[-1, :] = True
matrix[:, -1] = True
# Go over the bits
current_bit = 0
iteration = 0
# While there are active snakes try it out
while any([snake['active'] for snake in snakes]):
# For each active snake
active_snakes = [snake for snake in snakes if snake['active']]
for i, snake in enumerate(active_snakes) :
# Get the bit or exit
if current_bit >= len(bits):
return snakes
bit = bits[current_bit]
# Get the possible actions
possible_actions = [
action for action in actions
if not matrix[ tuple(snake['position'] + action @ snake['orientation']) ]
]
# Execute an action
if len(possible_actions) == 0:
snake['active'] = False
continue # See if other snakes can still play
elif len(possible_actions) == 1:
action = possible_actions[0]
elif len(possible_actions) == 2:
action = possible_actions[int(bit)]
elif len(possible_actions) == 3:
offset = iteration % 2
action = possible_actions[(int(bit) + offset)]
# Update
current_bit += 1
snake['orientation'] = action @ snake['orientation']
prev_position = snake['position']
snake['position'] = snake['position'] + snake['orientation']
# Update boundaries where snakes have been
matrix[ tuple(snake['position']) ] = True
# Update the twice larger pretty image (so that there is space and it looks pretty)
snake['trail'][ tuple(prev_position + snake['position']) ] = 2 * iteration
snake['trail'][tuple(2 * snake['position']) ] = 2 * iteration + 1
# Update iteration
iteration += 1
return snakes, current_bit, iteration
def generate_image(snakes, frame=np.inf, canvas_size = 44):
# Define a transparant layer
transparent = np.uint8(np.broadcast_to(
np.array([255, 255, 255, 0]),
(canvas_size, canvas_size, 4)
))
# Define the bottom layer (transparent white)
composite = Image.new(
'RGBA',
(canvas_size, canvas_size),
(255, 255, 255, 255)
)
# Draw each trail
for snake in snakes:
# Get the trail until this frame
trail = snake['trail'] * (snake['trail'] <= frame)
# Get the color
color = cm.get_cmap(snake['c'])
# Set the trail and rest transparent
df = np.where(
trail[:, :, np.newaxis] > 0,
np.uint8(color(trail / trail.max()) *255),
transparent
)
# Create an image and add the layer
im = Image.fromarray(df)
composite = Image.alpha_composite(composite, im)
d = ImageDraw.Draw(composite)
d.rectangle(
[(0, 0), (canvas_size-1, canvas_size-1)],
fill=None,
outline=(0,0,0,255),
width=1)
return composite.resize((5 * canvas_size, 5 * canvas_size), Image.NEAREST)
For this piece of code we'll generate 500 bits on a canvas of 22x22. The bits are not generated efficiently.
seed = 0
nr_bits = 500
size = 22
# Sizes of the canvas
canvas_size=2*(2+size)
canvas_size_xl = canvas_size * 5
# Generate some random bits
rng = default_rng(seed)
bits = rng.random(nr_bits) < 0.5
# Get the trails
snakes, nr_generated, max_iteration = generate_trails(
size,
bits
)
# Display
image = generate_image(snakes, frame=max_iteration, canvas_size=canvas_size)
display(image)
gif = Image.new('RGBA', (canvas_size_xl, canvas_size_xl))
gif.save('animation.gif',
save_all=True,
append_images=[generate_image(snakes, frame=frame, canvas_size=canvas_size)
for frame in range(2, max_iteration)],
duration=100,
loop=0)