You may have seen sylnsfar's "Artistic QR Code" library. It produces non-standard QR codes that incorporate arbitrary pictures, but are nonetheless machine-readable [5].
Of course, I had to understand how it worked ☺
It doesn't take much to find that the magic happens in the combine function in myqr.py:
if ver > 1: aloc = alig_location[ver-2] for a in range(len(aloc)): for b in range(len(aloc)): if not ((a==b==0) or (a==len(aloc)-1 and b==0) or (a==0 and b==len(aloc)-1)): for i in range(3*(aloc[a]-2), 3*(aloc[a]+3)): for j in range(3*(aloc[b]-2), 3*(aloc[b]+3)): aligs.append((i,j)) for i in range(qr.size[0]-24): for j in range(qr.size[1]-24): if not ((i in (18,19,20)) or (j in (18,19,20)) or (i<24 and j<24) or (i<24 and j>qr.size[1]-49) or (i>qr.size[0]-49 and j<24) or ((i,j) in aligs) or (i%3==1 and j%3==1) or (bg0.getpixel((i,j))[3]==0)): qr.putpixel((i+12,j+12), bg.getpixel((i,j)))
That code is full of magic numbers, and the multiple nested loops don't really help legibility much. What's going on in there?
To understand where those numbers came from, I studied a QR code tutorial. The most useful page for our purposes is the one that explains "function patterns".
Everything in that function assumes a 3-pixel QR module, so:
- the first block collects the placements of the alignment
patterns
- but only if the code is large enough to need them (ver > 1)
- and ignoring those that would collide with finder patterns ((a==b==0) or (a==len(aloc)-1 and b==0) or (a==0 and b==len(aloc)-1) means "top-left, top-right, bottom-left corners")
- the second block replaces pixels in the QR code (qr.putpixel)
with pixels from the fancy picture (bg.getpixel) if:
- they're not in timing patterns ((i in (18,19,20)) or (j in (18,19,20)))
- they're not in finder patterns ((i<24 and j<24) or (i<24 and j>qr.size[1]-49) or (i>qr.size[0]-49 and j<24))
- they're not in alignment patterns ((i,j) in aligs)
- and two more conditions I'll explain later
So all those magic numbers define the placements of finder / timing / alignment patterns: those really have to be there, otherwise the scanning / recognition code won't even realise it's looking at a QR code.
The last two conditions are:
(i%3==1 and j%3==1) or (bg0.getpixel((i,j))[3]==0)
The second one skips transparent pixels in the picture (bg0 is a ARGB-version of the input picture bg, and the pixel value at position 3 is the alpha value).
The first one makes sure to leave the central pixel of each 3×3 QR module untouched: we want the QR data to remain there!
So, another way to get the same result would be:
- paint a normal black & white QR code
- layer the fancy picture on top of it, honouring the alpha channel
- paint all non-data modules on top
- paint the data modules on top, but only their central pixels
Of course, it's not trivial to know whether a module is part of the data or of a function pattern, but we're in luck: the libqrencode C library provides us with that information! It generates the QR code as a matrix of bytes, and each byte is a bitfield:
MSB 76543210 LSB |||||||`- 1=black/0=white ||||||`-- data and ecc code area |||||`--- format information ||||`---- version information |||`----- timing pattern ||`------ alignment pattern |`------- finder pattern and separator `-------- non-data modules (format, timing, etc.)
The Perl modules Text::QRCode and Imager::QRCode wrap that library, but do not expose the internal byte matrix. So of course I wrapped it again myself ☺
Alien::QREncode uses Alien::Base to install libqrencode, then Data::QRCode binds it via Inline::Module and provides a Perlish API (see the test file for an example).
Now, we only need to deal with the actual images: let's use Imager, which seems simple and flexible enough. The actual code is maybe too flexible, but the core of the logic is:
my $back_image = qr_image($qr_code); return $back_image unless $fancy_picture; my $front_image = qr_dots_image($qr_code); my $target = Imager->new(xsize=>$size,ysize=>$size); $target->paste(src => $back_image); $target->compose(src => $fancy_picture); $target->compose(src => $front_image); return $target;
which is a direct translation of the procedure outlined before.
Once we've installed Alien::QREncode, Data::QRCode, and Imager::QRCode::Fancy, we can say:
perl overlay-qr.pl 'https://pokketmowse.deviantart.com/art/In-the-Kawaii-of-the-Beholder-177076230' kawaii-beholder.png kawaii-beholder-qr.png
and get this:
Or maybe:
perl overlay-qr.pl 'Squirrel Girl is the best' squirrel-girl.png squirrel-girl-qr.png
and get this:
Notice how the size of the "dots" adapts to the resolution of the picture (although it could be smarter), and how transparency is maintained.
more or less: I think you need a high-enough-resolution camera and some pretty forgiving recognition code