Message Boards Message Boards

GROUPS:

Embedding images in QR code

Posted 5 months ago
1476 Views
|
6 Replies
|
23 Total Likes
|

manually edited

I guess a story-telling type post would attract more upvotes and probably give some insight about how to 'solve problems' using Mathematica, so I would go into details and try to explain not only the code but also how I figured out how to write them.

To begin with, here's three QR code generated with the code, check it by yourself, they are actually scan-able~ It's also amazing that even very fine details of the image can be shown in the QR code (Note that it would better if you view these QR with your glass off XD)

mma1 poa

mma2

How this works?

In fact, this form of QR code is not my original idea, I came across such type of QR code on internet but failed to find its origin. So I tried to figure out the principle by myself.

Carefully observe the image, one can find out that there's something odd about this QR:

wierd behavior

The marker on the corner are three times coarser than the majority of the QR. So I initially hypothesize that the QR recognition algorithm would first average the brightness of a segment, turning it into a normal QR and then recognize it. The code I used is as follows:

Block[{img = Import["http://community.wolfram.com//c/portal/getImageAttachment?filename=mathematica1.png&userId=1340903"], dat, partitioned},
 dat = ImageData@ImagePad[Binarize[ImageResize[img, Scaled[1/3]]], -4];
 partitioned = Partition[dat, {3, 3}];
 Grid[{ImageResize[#, Dimensions@dat], BarcodeRecognize@#} & /@ 
    {Image@dat,Binarize@Image@Map[Mean@*Flatten, partitioned, {2}]}]
 ]

The result proved me wrong as the averaged version cannot be properly recognized. Then further observer the QR code, I found that there are mysterious dots even in the places which should be purely white, also the dots are a bit too structural. So I suspect that normal QR code recognition algorithm only takes the color of the center dot, so I added this to the previous code:

Map[#[[2,2]]&,partitioned,{2}]

then it worked out properly!

theory

As we've already cracked the theory, we can now generate some of our own.

How to generate?

QR code generation

First we can use BarcodeImage to generate a QR code, for example here I would use: "This is a sample QR generated by Mathematica!" as the content of the QR code:

text = "This is a sample QR generated by Mathematica!";
qrraw = BarcodeImage[text, {"QR", "H"}, 1]
BarcodeRecognize@qrraw

Image processing

Then we create a black and white image to use as background, for example here we use the wolfram wolf icon:

wolfram wolf

Import, convert to grayscale and adjust the grayscale a bit:

img=ColorConvert[Rasterize[Graphics[{
  Inset[Import["http://community.wolfram.com//c/portal/getImageAttachment?filename=wolframwolf.png&userId=1340903"],{.6,.4},Automatic,.8],
  Text[Style["WOLFRAM",Bold,14],{.5,.92}]
},PlotRange->{{0,1},{0,1}},ImageSize->3ImageDimensions@qrraw]],"Grayscale"]^.45

which returns:

B&W image

Note that in order to get enough resolution while keeping the QR code easy to scan, the dimension of the QR code is best in the range of [25,50], one can test that using ImageDimensions@img and adjust it by changing the error correction level by setting {"QR",lev} where lev can be "L", "M", "Q", or "H".

Merging

Then we should merge this two image together. Here we use the technique of dither to display grayscale image using only white and black pixels. In the process of dithering, we should notice that at center of each 9*9 pixel the value should correspond to the value in the QR image, or the QR code would be invalid. The code could be easily written out as follows:

dithering[imgdat_, qrdat_] := 
 Block[{imgdat1 = imgdat, dimx, dimy, tmp1, tmp2, f = UnitStep[# - .5] &},
  {dimx, dimy} = Dimensions@imgdat;
  Quiet@Do[
    (*Rounding*)
    tmp1 = If[Mod[{i, j}, 3] == {2, 2}, qrdat[[(i + 1)/3, (j + 1)/3]], f[imgdat1[[i, j]]]];
    tmp2 = Clip[imgdat1[[i, j]] - tmp1, {-.5, .5}];
    (*Diffuse Error*)
    imgdat1[[i, j]] = tmp1;
    imgdat1[[i, j + 1]] += 0.4375 tmp2;
    If[j != 1, imgdat1[[i + 1, j - 1]] += 0.1875 tmp2];
    imgdat1[[i + 1, j]] += 0.3125 tmp2;
    imgdat1[[i + 1, j + 1]] += 0.0625 tmp2
    , {i, dimx}, {j, dimy}];
  imgdat1
  ]

Special attention should be paid to the handling key pixels of the QR code, the error created by introducing it should not be ignored, but its influence should be limited in a range, so here a Clip in error is required, while in a traditional dithering process it would be redundant.

Apply dithering to the image and we have:

Image[ditherdat=dithering[ImageData@img, ImageData@qrraw]]

output 1

Refinement

One can see that the shape of the original image is quite well preserved and key points of the QR code are properly dealt with. Then the final step is to process the key features on the corner and edge of the QR code, which is quite trivial:

replicate = (Flatten[ConstantArray[#, {3, 3}], {{3, 1}, {4, 2}}] &);

refineqr[qrdat_] := 
 Block[{qrd = qrdat, d = Length[qrdat]},
  (*Corner*)
  (qrd[[#1 ;; 24 #1 ;; #1, #2 ;; 24 #2 ;; #2]] = replicate[qrd[[2 #1 ;; 23 #1 ;; 3 #1, 2 #2 ;; 23 #2 ;; 3 #2]]]) & @@@ {{1, 1}, {1, -1}, {-1, 1}};
  (*Edge*)
  qrd[[22 ;; d - 21, 19 ;; 21]] = Transpose[qrd[[19 ;; 21, 22 ;; d - 21]] = replicate[{Mod[Range[(d + 1)/3 - 14], 2]}]];
  qrd]

Then apply this to previously get result, we get the final result, which is scan-able:

Image[final = refineqr@ditherdat]
BarcodeRecognize@%

It's usually favourable to have a 3x zoom to the image:

Image@replicate@final

final result

A fully packed version is shown in the attachment notebook file, where:

createqr[text,img]

would generate the same result.

Further optimizations could include using machine learning to further refine the display effect. Sharper lines, Less interfering key points and more could be expected.

ENJOY~


Update

@Henrik Schachner kindly remind me that the previous QR is not that easy to scan with average QR scanning software. So I made some tiny updates to make the QR more standardized and much more easier to scan:

refineqr[qrdat_] := 
 Block[{qrd = qrdat, d = Length[qrdat], temp = Fold[ArrayPad[#1, 1, #2] &, {{{0}}, 1, 0}], p},
  p = Position[Round@ListCorrelate[temp, qrdat[[2 ;; ;; 3, 2 ;; ;; 3]], {{1, 1}, {-1, -1}}, 0, Abs@*Subtract], 0, 2];
  (*Corner*)
  (qrd[[#1 ;; 24 #1 ;; #1, #2 ;; 24 #2 ;; #2]] = replicate[qrd[[2 #1 ;; 23 #1 ;; 3 #1, 2 #2 ;; 23 #2 ;; 3 #2]]]) & @@@ {{1, 1}, {1, -1}, {-1, 1}};
  (*Edge*)
  qrd[[22 ;; d - 21, 19 ;; 21]] = Transpose[ qrd[[19 ;; 21, 22 ;; d - 21]] = replicate[{Mod[Range[(d + 1)/3 - 14], 2]}]];
  (*Special*)
  (qrd[[3 #1 - 2 ;; 3 #1 + 12, 3 #2 - 2 ;; 3 #2 + 12]] = replicate@temp) & @@@ p;
  qrd]

after this update, the QR code would look like this:

edited

After minor manual edition, it could be like:

manually edited

Maybe this would be easier to scan due to the newly add correction block on the right-down corner.

Also, I think I found a better realization using same basic design principle here.

Attachments:
6 Replies

Hi Jingxian Wang,

thank you for sharing this nice idea together with the code! Meanwhile I have been playing around with it quite a bit. As far as I can say yet your idea is working well under "laboratory conditions", i.e. creating those QR images and applying BarcodeRecognize - all within the Mathematica environment. But when printed on paper and trying the recognition using a QR scanner of e.g. some Android device it obviously does not work. Any idea?

Regards -- Henrik

That's weird... I've actually tested all these QR using my phone before uploading and just now and all of them worked, and one of these codes, the one with "POA" on, is used in a real event in my school and most people can scan them properly...

I suspect that you're using a not-that-strong QR scanner, in China we mainly use the QR code scanner in Wechat, which is quite powerful. I usually get a correct result when I scan the QR with Wechat, but my phone's built-in scanner and Facebook app's scanner would fail to recognize it sometimes and also numerous online recognizer failed to recognize it at all... A small trick is to vibrate your phone a bit when scanning, sometimes it would help the phone scanner to locate the QR. (Have a try~ maybe it would work this time~)

I'm aware that this form of QR is more fragile than normal QR so I suggested that "the dimension of the QR code is best in the range of [25,50]", but it seems that it is just a bit too hard for average QR scanners even if the size is in this range :(

Hi~ I've updated my post a bit to improve success rate in scanning, now Wechat/Facebook/my phone's built-in scanner can all easily recognize the QR code~ Can you successfully scan the QR now? ;)

Dear Jingxian Wang,

many thanks for your effort! I am glad to tell you that now the QRs can be recognized successfully! The fourth little square obviously made the difference.

Best regards from Germany -- Henrik

Yeah, I guess the forth tiny block can help in the process of alignment. It's reasonable that QR code scanners needs some 'extra help' when trying to make the QR 'flat' before recognizing.

Also, I'm glad that you enjoy it~ :)

Best regards -- Jingxian Wang

enter image description here - Congratulations! This post is now a Staff Pick as distinguished by a badge on your profile! Thank you, keep it coming!

Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract