A Kaggle notebook for making squint-to-see images: pictures that look like an ordinary rain-soaked British scene up close, and resolve into a hidden shape or word when you squint, blur them, or step back from the screen.
squint_to_see_kaggle.ipynb — upload to Kaggle
(Create → Notebook → File → Import Notebook) and run top to bottom.
| Setting | Value |
|---|---|
| Accelerator | GPU T4 x2 (or P100) |
| Internet | On — needed to pull models from Hugging Face |
Internet requires a phone-verified Kaggle account. Everything else is free-tier: about 3.5 GB of downloads, ~6 GB of VRAM, and roughly 20–30 s per 768x768 image. The ten-preset batch takes 4–5 minutes, well inside the 30 h/week GPU quota.
A black-and-white mask drives the QR Code Monster ControlNet, which steers the low-frequency luminance of a Stable Diffusion 1.5 render — bright mask areas become bright regions of the scene, dark areas become dark. Up close your eye reads the detail; squinting throws the detail away and leaves the light/dark layout, which is the hidden image.
SD 1.5 rather than SDXL on purpose: the SDXL illusion ControlNets are weaker, and a 1024px SDXL render plus a hi-res pass will not fit a free T4 alongside everything else.
Three ways to make one, all in section 6 of the notebook:
- from a prompt — describe a shape and get four candidates to choose from
- from text — words, auto-wrapped and stretched to fill the frame
- from a file — your own image, uploaded as a Kaggle dataset
Raw diffusion output is a poor control image, so every mask goes through the same cleanup: Otsu threshold, polarity normalisation, median despeckle, and a feathered edge. The feather matters more than people expect — hard edges make ControlNet carve visible seams across the scene, while a soft edge lets it hide the boundary inside real scene features.
The squint_preview() helper renders each result at progressively smaller sizes.
Judge your images by that strip, not by the full-size render — it is the only
honest test of whether the illusion actually works.
backstreet, greasy_spoon, bookies, estate, chippy, canal, terrace,
seaside, lock_in, launderette.
They are chosen to work as illusions, not just to sound atmospheric: each has
strong natural light/dark structure — wet tarmac under a sodium lamp, fog around
floodlights, a lit window on a dark facade — which gives the mask somewhere to
hide. Flat, evenly lit scenes have nowhere to put the shape and need a higher
cn_scale to compensate.
Three knobs decide everything:
| Parameter | Range | Effect |
|---|---|---|
cn_scale |
0.9–1.6 | Illusion strength. Start at 1.2. |
cn_start |
0.0–0.35 | When ControlNet joins. Higher = better scene, weaker illusion. |
cn_end |
0.6–1.0 | When it stops. The quality knob. |
Illusion invisible → raise cn_scale. Picture a smeared mess → lower cn_end to
~0.75 first, which hands the final steps back to SD to repair texture while the
illusion survives; only then lower cn_scale. Section 10 sweeps both axes into a
grid when a mask is being stubborn.
Every render is saved with a JSON sidecar containing all its parameters, so any result can be reproduced exactly.
The notebook is generated, so edit the source of truth and rebuild:
python tools/build_notebook.pyKeeping the cells as strings in tools/build_notebook.py means changes show up as
readable diffs instead of one reformatted line of JSON.