This was fun: A "Where Is Waldo" game for QR codes.
Can you find it? Your phone can!
The basic idea
QR scanners don't see color. The first thing a decoder does is convert the image to grayscale — collapsing each pixel to a single brightness value via roughly 0.299·R + 0.587·G + 0.114·B — then binarize and hunt for the finder patterns (the three corner squares) and module grid. Everything downstream operates on luminance alone.
That gap is the whole trick: luminance carries the code; hue and saturation are free to do whatever you want. You have two perceptual channels the scanner is blind to, and humans hunting for a pattern rely heavily on exactly those channels.
So the code is rendered as a luminance difference — "dark" modules low-luma, "light" modules high-luma — but each module is painted a vivid random color at its assigned brightness. The key subtlety is that a fully saturated color's true luminance depends on its hue: pure blue sits around 0.11, pure red around 0.30, pure yellow near 0.89. So "dark" modules don't have to be dark and muddy — you can use electric blues, reds, and violets that are naturally low-luminance at full saturation, and reserve the bright hues (yellow, cyan, green) for "light" modules.
To the hyman eye it's a saturated confetti, but converted to grayscale it's a scannable QR.
The code
#! /usr/bin/env python
import cv2
import numpy as np
PAYLOAD = "https://blog.vrypan.net/2026/07/07/where-is-qrdo/"
MOD = 8
GRID_W, GRID_H = 192, 96
N_DECOYS = 180
enc = cv2.QRCodeEncoder.create()
qr = enc.encode(PAYLOAD)[2:-2, 2:-2]
n = qr.shape[0]
det = cv2.QRCodeDetector()
LUMA = np.array([0.114, 0.587, 0.299]) # BGR
# --- curated DARK palette (BGR), all low-luma, balanced across families ----
# reds need V<255 to stay low-luma; blues/violets are low-luma at full V.
DARK = np.array([
[0, 0, 220], # strong red
[0, 0, 180], # deep red
[30, 0, 200], # red-magenta
[255, 0, 0], # pure blue
[200, 0, 0], # deep blue
[255, 40, 0], # blue-cyan lean
[200, 0, 120], # indigo
[180, 0, 180], # violet
[140, 0, 200], # purple-red
], np.float32)
# --- LIGHT palette (BGR), all high-luma, vivid + bright ---
LIGHT = np.array([
[0, 255, 255], # yellow
[0, 255, 150], # yellow-green
[0, 255, 0], # green
[255, 255, 0], # cyan
[255, 255, 150], # pale cyan
[150, 255, 255], # pale yellow
[200, 255, 200], # mint
], np.float32)
print("dark palette lumas:", np.round(DARK @ LUMA / 255, 2))
print("light palette lumas:", np.round(LIGHT @ LUMA / 255, 2))
def build(seed):
r = np.random.default_rng(seed)
# --- random QR placement (keep a 1-module margin for the finder halos) ---
qx = int(r.integers(1, GRID_W - n))
qy = int(r.integers(1, GRID_H - n))
is_dark = r.random((GRID_H, GRID_W)) < 0.40
for cx, cy in [(qx, qy), (qx+n-7, qy), (qx, qy+n-7)]:
is_dark[cy-1:cy+8, cx-1:cx+8] = False
for y in range(n):
for x in range(n):
is_dark[qy+y, qx+x] = qr[y, x] < 128
for _ in range(N_DECOYS):
dx = int(r.integers(1, GRID_W-9)); dy = int(r.integers(1, GRID_H-9))
if (qx-10 < dx < qx+n+1) and (qy-10 < dy < qy+n+1):
continue
s = int(r.integers(6, 9))
is_dark[dy:dy+s, dx:dx+s] = True
is_dark[dy+1:dy+s-1, dx+1:dx+s-1] = False
inner = int(r.integers(1, 3))
is_dark[dy+2:dy+s-2-(inner-1), dx+2:dx+s-2-(inner-1)] = True
canvas = np.zeros((GRID_H, GRID_W, 3), np.float32)
di = r.integers(0, len(DARK), (GRID_H, GRID_W))
li = r.integers(0, len(LIGHT), (GRID_H, GRID_W))
for y in range(GRID_H):
for x in range(GRID_W):
canvas[y, x] = DARK[di[y, x]] if is_dark[y, x] else LIGHT[li[y, x]]
img = cv2.resize(canvas.astype(np.uint8), (GRID_W*MOD, GRID_H*MOD),
interpolation=cv2.INTER_NEAREST)
return img, (qx, qy)
import secrets
# random search order (fresh OS entropy each run) so the QR lands in a
# different spot every time, rather than always picking the first seed that works
seeds = list(range(1, 4000))
np.random.default_rng(secrets.randbits(64)).shuffle(seeds)
for seed in seeds:
img, (qx, qy) = build(seed)
if det.detectAndDecode(img)[0] == PAYLOAD:
cv2.imwrite("qr_redblue.png", img)
print(f"seed {seed}: decodes OK -> qr_redblue.png (QR at module {qx},{qy})")
break
else:
raise SystemExit("no working seed found")
Update 08-07-2026
Variation: Instead of scattering decoy finder-lookalike squares at random, place them on a regular lattice and align the QR so its three real finder patterns land on lattice nodes. Now the real corners are just three nodes in a field of near-identical squares and the easy to find "three squares in an L" tell disappears.

#! /usr/bin/env python
import cv2
import numpy as np
import secrets
PAYLOAD = "https://blog.vrypan.net"
MOD = 8
GRID_W, GRID_H = 288, 144
PITCH = None # set to n-7 after QR is encoded; = finder spacing
JITTER = 2 # decoys wander +/- this many modules off their node
CLEARANCE = 6 # lattice leaves this margin around the QR body
enc = cv2.QRCodeEncoder.create()
qr = enc.encode(PAYLOAD)[2:-2, 2:-2]
n = qr.shape[0]
PITCH = n - 7 # so QR finders at (0,0),(n-7,0),(0,n-7) fall on nodes
det = cv2.QRCodeDetector()
LUMA = np.array([0.114, 0.587, 0.299]) # BGR
DARK = np.array([
[0, 0, 220], # strong red
[0, 0, 180], # deep red
[30, 0, 200], # red-magenta
[255, 0, 0], # pure blue
[200, 0, 0], # deep blue
[255, 40, 0], # blue-cyan lean
[200, 0, 120], # indigo
[180, 0, 180], # violet
[140, 0, 200], # purple-red
], np.float32)
LIGHT = np.array([
[0, 255, 255], # yellow
[0, 255, 150], # yellow-green
[0, 255, 0], # green
[255, 255, 0], # cyan
[255, 255, 150], # pale cyan
[150, 255, 255], # pale yellow
[200, 255, 200], # mint
], np.float32)
print("dark palette lumas:", np.round(DARK @ LUMA / 255, 2))
print("light palette lumas:", np.round(LIGHT @ LUMA / 255, 2))
def draw_decoy(is_dark, x, y, size, center):
"""Concentric square with a deliberately wrong ratio (scanner rejects it)."""
if x < 1 or y < 1 or x+size >= GRID_W or y+size >= GRID_H:
return
is_dark[y:y+size, x:x+size] = True
is_dark[y+1:y+size-1, x+1:x+size-1] = False
c0 = (size - center) // 2
is_dark[y+c0:y+c0+center, x+c0:x+c0+center] = True
def build(seed):
r = np.random.default_rng(seed)
is_dark = r.random((GRID_H, GRID_W)) < 0.40
# place QR origin on a lattice node (random node -> random placement)
nx = (GRID_W - n) // PITCH
ny = (GRID_H - n) // PITCH
qx = int(r.integers(0, nx)) * PITCH + 2
qy = int(r.integers(0, ny)) * PITCH + 2
# jittered-lattice decoys, skipping the QR's own footprint
x0, y0, x1, y1 = qx-CLEARANCE, qy-CLEARANCE, qx+n+CLEARANCE, qy+n+CLEARANCE
for j in range(0, GRID_H, PITCH):
for i in range(0, GRID_W, PITCH):
if x0 <= i <= x1 and y0 <= j <= y1:
continue # inside QR area: leave for real finders
jx = i + int(r.integers(-JITTER, JITTER+1))
jy = j + int(r.integers(-JITTER, JITTER+1))
size = int(r.choice([6, 8])) # wrong sizes -> wrong ratio
center = int(r.choice([2, 4]))
draw_decoy(is_dark, jx, jy, size, center)
# real finder halos + stamp the QR (overwrites lattice at its 3 nodes)
for cx, cy in [(qx, qy), (qx+n-7, qy), (qx, qy+n-7)]:
is_dark[cy-1:cy+8, cx-1:cx+8] = False
for y in range(n):
for x in range(n):
is_dark[qy+y, qx+x] = qr[y, x] < 128
# paint colours
canvas = np.zeros((GRID_H, GRID_W, 3), np.float32)
di = r.integers(0, len(DARK), (GRID_H, GRID_W))
li = r.integers(0, len(LIGHT), (GRID_H, GRID_W))
for y in range(GRID_H):
for x in range(GRID_W):
canvas[y, x] = DARK[di[y, x]] if is_dark[y, x] else LIGHT[li[y, x]]
img = cv2.resize(canvas.astype(np.uint8), (GRID_W*MOD, GRID_H*MOD),
interpolation=cv2.INTER_NEAREST)
return img, (qx, qy)
# randomized search order -> different working seed (and placement) each run
seeds = list(range(1, 8000))
np.random.default_rng(secrets.randbits(64)).shuffle(seeds)
for seed in seeds:
img, (qx, qy) = build(seed)
if det.detectAndDecode(img)[0] == PAYLOAD:
cv2.imwrite("qr_lattice.png", img)
print(f"seed {seed}: decodes OK -> qr_lattice.png (QR at module {qx},{qy})")
break
else:
raise SystemExit("no working seed found -- raise the seed ceiling or lower density")