A Jupyter notebook for making qrcodes out of existing images and adding a URL. Fixed to 41x41 (version 6) images. Depends on numpy, scipy, matplotlib, Pillow, and python-qrcode.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"%matplotlib inline\n", | |
"import itertools\n", | |
"import re\n", | |
"\n", | |
"import qrcode\n", | |
"import qrcode.util\n", | |
"\n", | |
"import numpy as np\n", | |
"\n", | |
"import imageio\n", | |
"\n", | |
"import matplotlib.pyplot as plt\n", | |
"import matplotlib.colors\n" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"metadata": { | |
"collapsed": true | |
}, | |
"outputs": [], | |
"source": [ | |
"# dictionary of characters to numeric codes\n", | |
"anc_d = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,\n", | |
" '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,\n", | |
" 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14,\n", | |
" 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19,\n", | |
" 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24,\n", | |
" 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29,\n", | |
" 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34,\n", | |
" 'Z': 35, ' ': 36, '$': 37, '%': 38, '*': 39,\n", | |
" '+': 40, '-': 41, '.': 42, '/': 43, ':': 44}\n", | |
"\n", | |
"# reverse dictionary\n", | |
"anc_d2 = {anc_d[k]:k for k in anc_d}\n", | |
"\n", | |
"# the 8 different mask functions\n", | |
"mask_funcs = [lambda i, j: (i + j) % 2 == 0,\n", | |
" lambda i, j: i % 2 == 0,\n", | |
" lambda i, j: j % 3 == 0,\n", | |
" lambda i, j: (i + j) % 3 == 0,\n", | |
" lambda i, j: (i // 2 + j // 3) % 2 == 0,\n", | |
" lambda i, j: (i * j) % 2 + (i * j) % 3 == 0,\n", | |
" lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0,\n", | |
" lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0]\n", | |
"\n", | |
"# apply a given mask to a pixel\n", | |
"app_mask = lambda mf,bs,ijs: [(mf(*ij) ^ b) for b,ij in zip(bs,ijs)]\n", | |
"\n", | |
"# encode an alphanumeric character string into bits\n", | |
"def encode(chrstr):\n", | |
" bits = []\n", | |
" for i in range(0, len(chrstr), 2):\n", | |
" c1 = chrstr[i]\n", | |
" if i + 1 < len(chrstr):\n", | |
" c2 = chrstr[i+1]\n", | |
" bits.extend(bin(anc_d[c1] * 45 + anc_d[c2])[2:].zfill(11))\n", | |
" else:\n", | |
" bits.extend(bin(anc_d[c1])[2:].zfill(6))\n", | |
" return ''.join(bits)\n", | |
"\n", | |
"# decode a bit string into alphanumeric characters\n", | |
"def decode(bitstr):\n", | |
" chrs = []\n", | |
" for i in range(0, len(bitstr), 11):\n", | |
" b = bitstr[i:i+11]\n", | |
" j = int(b, base=2)\n", | |
" if len(b) == 11:\n", | |
" c1 = anc_d2.get(j // 45, '_')\n", | |
" c2 = anc_d2.get(j % 45, '_')\n", | |
" chrs.extend((c1,c2))\n", | |
" else:\n", | |
" chrs.append(anc_d2.get(j, '_'))\n", | |
" return ''.join(chrs)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [], | |
"source": [ | |
"# all pairs of alphanumeric characters\n", | |
"legal_pairs = list(itertools.product([anc_d2[i] for i in range(36)], repeat=2))\n", | |
"legal_codes = [(anc_d[c1] * 45 + anc_d[c2]) for c1,c2 in legal_pairs]\n", | |
"\n", | |
"# pairs with characters I don't want (punctuation)\n", | |
"illegal_pairs = [(i,j) for i,j in itertools.product(anc_d.keys() + ['_'], repeat=2)\n", | |
" if (anc_d.get(i, 45) > 35) or (anc_d.get(j, 45) > 35)]\n", | |
"illegal_codes = [(anc_d.get(c1, 45) * 45 + anc_d.get(c2, 45)) for c1,c2 in illegal_pairs]\n", | |
"\n", | |
"# hamming distance matrix between illegal pairs and legal pairs\n", | |
"hamming_matrix = np.zeros((len(illegal_pairs), len(legal_pairs)))\n", | |
"for i,ic in enumerate(illegal_codes):\n", | |
" hamming_matrix[i,:] = [bin(ic ^ lc)[2:].count('1') for lc in legal_codes]\n", | |
"\n", | |
"# closest legal pair (lowest hamming distance) to each illegal pair\n", | |
"min_c = {ij:legal_pairs[hamming_matrix[i,:].argmin()] for i,ij in enumerate(illegal_pairs)}\n", | |
"\n", | |
"# closest legal character for each illegal character, for the last one\n", | |
"for i in range(36, 46):\n", | |
" min_c[anc_d2.get(i, '_')] = anc_d2[min(range(36), key=lambda j: bin(j ^ i)[2:].count('1'))]" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 4, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [], | |
"source": [ | |
"# get the EC blocks given the data blocks\n", | |
"def get_ec(dc_block0, dc_block1):\n", | |
" ec_blocks = []\n", | |
" for dcb in (dc_block0, dc_block1):\n", | |
" ec_blocks.append([])\n", | |
"\n", | |
" # Get error correction polynomial.\n", | |
" rsPoly = qrcode.base.Polynomial([1], 0)\n", | |
" for i in range(18):\n", | |
" rsPoly = rsPoly * qrcode.base.Polynomial([1, qrcode.base.gexp(i)], 0)\n", | |
"\n", | |
" rawPoly = qrcode.base.Polynomial(dcb, len(rsPoly) - 1)\n", | |
" modPoly = rawPoly % rsPoly\n", | |
"\n", | |
" for i in range(18):\n", | |
" modIndex = i + len(modPoly) - 18\n", | |
" if (modIndex >= 0):\n", | |
" ec_blocks[-1].append(modPoly[modIndex])\n", | |
" else:\n", | |
" ec_blocks[-1].append(0)\n", | |
" \n", | |
" return ec_blocks" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 5, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [], | |
"source": [ | |
"# code adopted from python-qrcode, just used to set up the array\n", | |
"# with timing patterns, format, etc\n", | |
"def initialize_array(mask_pattern=0):\n", | |
" def setup_position_probe_pattern(row, col):\n", | |
" for r in range(-1, 8):\n", | |
" if row + r <= -1 or modules_count <= row + r:\n", | |
" continue\n", | |
"\n", | |
" for c in range(-1, 8):\n", | |
" if col + c <= -1 or modules_count <= col + c:\n", | |
" continue\n", | |
"\n", | |
" if (0 <= r and r <= 6 and (c == 0 or c == 6)\n", | |
" or (0 <= c and c <= 6 and (r == 0 or r == 6))\n", | |
" or (2 <= r and r <= 4 and 2 <= c and c <= 4)):\n", | |
" modules[row + r][col + c] = True\n", | |
" modules_b[row + r][col + c] = True\n", | |
" else:\n", | |
" modules[row + r][col + c] = False\n", | |
" modules_b[row + r][col + c] = True\n", | |
"\n", | |
" def setup_timing_pattern():\n", | |
" for r in range(8, modules_count - 8):\n", | |
" if modules_b[r][6]:\n", | |
" continue\n", | |
" modules[r][6] = int(r % 2 == 0)\n", | |
" modules_b[r][6] = True\n", | |
"\n", | |
" for c in range(8, modules_count - 8):\n", | |
" if modules_b[6][c]:\n", | |
" continue\n", | |
" modules[6][c] = int(c % 2 == 0)\n", | |
" modules_b[6][c] = True\n", | |
"\n", | |
" def setup_position_adjust_pattern():\n", | |
" pos = [6, 34]\n", | |
"\n", | |
" for i in range(len(pos)):\n", | |
" for j in range(len(pos)):\n", | |
" row = pos[i]\n", | |
" col = pos[j]\n", | |
"\n", | |
" if modules_b[row][col]:\n", | |
" continue\n", | |
"\n", | |
" for r in range(-2, 3):\n", | |
" for c in range(-2, 3):\n", | |
" if (r == -2 or r == 2 or c == -2 or c == 2 or\n", | |
" (r == 0 and c == 0)):\n", | |
" modules[row + r][col + c] = 1\n", | |
" modules_b[row + r][col + c] = True\n", | |
" else:\n", | |
" modules[row + r][col + c] = 0\n", | |
" modules_b[row + r][col + c] = True\n", | |
" \n", | |
" def setup_type_info(mask_pattern):\n", | |
" data = (error_correction << 3) | mask_pattern\n", | |
" bits = qrcode.util.BCH_type_info(data)\n", | |
"\n", | |
" # vertical\n", | |
" for i in range(15):\n", | |
" mod = ((bits >> i) & 1) == 1\n", | |
"\n", | |
" if i < 6:\n", | |
" modules[i][8] = mod\n", | |
" modules_b[i][8] = True\n", | |
" elif i < 8:\n", | |
" modules[i + 1][8] = mod\n", | |
" modules_b[i + 1][8] = True\n", | |
" else:\n", | |
" modules[modules_count - 15 + i][8] = mod\n", | |
" modules_b[modules_count - 15 + i][8] = True\n", | |
"\n", | |
" # horizontal\n", | |
" for i in range(15):\n", | |
" mod = ((bits >> i) & 1) == 1\n", | |
"\n", | |
" if i < 8:\n", | |
" modules[8][modules_count - i - 1] = mod\n", | |
" modules_b[8][modules_count - i - 1] = True\n", | |
" elif i < 9:\n", | |
" modules[8][15 - i - 1 + 1] = mod\n", | |
" modules_b[8][15 - i - 1 + 1] = True\n", | |
" else:\n", | |
" modules[8][15 - i - 1] = mod\n", | |
" modules_b[8][15 - i - 1] = True\n", | |
"\n", | |
" # fixed module\n", | |
" modules[modules_count - 8][8] = 1\n", | |
" modules_b[modules_count - 8][8] = True\n", | |
"\n", | |
"\n", | |
" def data_coords():\n", | |
" inc = -1\n", | |
" row = modules_count - 1\n", | |
" bitIndex = 7\n", | |
" byteIndex = 0\n", | |
"\n", | |
" coords = []\n", | |
"\n", | |
" for col in range(modules_count - 1, 0, -2):\n", | |
" if col <= 6:\n", | |
" col -= 1\n", | |
"\n", | |
" col_range = (col, col-1)\n", | |
"\n", | |
" while True:\n", | |
" for c in col_range:\n", | |
" if not modules_b[row][c]:\n", | |
" coords.append((row,c))\n", | |
" bitIndex -= 1\n", | |
"\n", | |
" if bitIndex == -1:\n", | |
" byteIndex += 1\n", | |
" bitIndex = 7\n", | |
"\n", | |
" row += inc\n", | |
"\n", | |
" if row < 0 or modules_count <= row:\n", | |
" row -= inc\n", | |
" inc = -inc\n", | |
" break\n", | |
"\n", | |
" return coords\n", | |
" \n", | |
" modules_count = 41\n", | |
" modules = np.zeros((modules_count, modules_count), dtype=int)\n", | |
" modules_b = np.zeros_like(modules, dtype=bool)\n", | |
"\n", | |
" error_correction = qrcode.base.ERROR_CORRECT_L\n", | |
"\n", | |
" setup_position_probe_pattern(0, 0)\n", | |
" setup_position_probe_pattern(modules_count - 7, 0)\n", | |
" setup_position_probe_pattern(0, modules_count - 7)\n", | |
"\n", | |
" setup_position_adjust_pattern()\n", | |
"\n", | |
" setup_timing_pattern()\n", | |
"\n", | |
" setup_type_info(mask_pattern)\n", | |
"\n", | |
" dc = data_coords()\n", | |
" \n", | |
" return modules,modules_b,dc\n", | |
"\n", | |
"# easiest way to get the data layout is to just initialize it\n", | |
"modules,modules_b,dc = initialize_array()\n", | |
"\n", | |
"# data coordinates for block 1, as pair of lists\n", | |
"b1 = [zip(*dc[i:i+8]) for i in range(0, 136 * 8, 16)]\n", | |
"# also storing the coordinates as a list of tuples, for masking\n", | |
"mfs1 = [dc[i:i+8] for i in range(0, 136 * 8, 16)]\n", | |
"# data coordinates for block 2\n", | |
"b2 = [zip(*dc[i:i+8]) for i in range(8, 136 * 8, 16)]\n", | |
"mfs2 = [dc[i:i+8] for i in range(8, 136 * 8, 16)]\n", | |
"\n", | |
"# errorcode coordinates for block 1\n", | |
"e1 = [zip(*dc[i:i+8]) for i in range(136 * 8, 172 * 8, 16)]\n", | |
"e_mfs1 = [dc[i:i+8] for i in range(136 * 8, 172 * 8, 16)]\n", | |
"# errorcode coordinates for block 2\n", | |
"e2 = [zip(*dc[i:i+8]) for i in range(137 * 8, 172 * 8, 16)]\n", | |
"e_mfs2 = [dc[i:i+8] for i in range(137 * 8, 172 * 8, 16)]" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"collapsed": false | |
}, | |
"outputs": [], | |
"source": [ | |
"# the image to be turned into a QR code\n", | |
"image_file = 'target_img.png'\n", | |
"\n", | |
"img = imageio.imread(image_file)\n", | |
"\n", | |
"t_data = ((img > 0).sum(2) // 4).astype(np.uint8)\n", | |
"\n", | |
"print(t_data.shape, t_data.max())\n", | |
"\n", | |
"plt.matshow(t_data, norm=matplotlib.colors.NoNorm(),\n", | |
" cmap=matplotlib.colors.ListedColormap(['k', 'w']))\n", | |
"plt.show()" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"collapsed": false, | |
"scrolled": false | |
}, | |
"outputs": [], | |
"source": [ | |
"# convert image data to 0,1 based (0 = white, 1 = black)\n", | |
"t_data_a = 1 - t_data\n", | |
"\n", | |
"# base URL for QR code. The rest of the data is based on the image\n", | |
"url_str = 'HTTP://MESWEBBER.COM/'\n", | |
"\n", | |
"# file name for saving output\n", | |
"output_file = 'output_mask_{}.png'\n", | |
"\n", | |
"# going to try each available mask, decode,\n", | |
"for ii,mf in enumerate(mask_funcs):\n", | |
" # get format bits for this mask\n", | |
" m,mb_,dc_ = initialize_array(ii)\n", | |
" \n", | |
" # retrieve and unmask data\n", | |
" td_b1 = [app_mask(mf, t_data_a[b], mfs) for b,mfs in zip(b1, mfs1)]\n", | |
" td_b2 = [app_mask(mf, t_data_a[b], mfs) for b,mfs in zip(b2, mfs2)]\n", | |
"\n", | |
" # convert to bitstring\n", | |
" td_b1s = [j for b in td_b1 for j in b]\n", | |
" td_b2s = [j for b in td_b2 for j in b]\n", | |
"\n", | |
" # decode text\n", | |
" ds = ''.join(decode(''.join(map(str, td_b1s[13:] + td_b2s[:542]))))\n", | |
" # replace non-alphanumeric characters with bitwise nearest neighbor\n", | |
" ds2 = [c for i in range(0, 194, 2)\n", | |
" for c in min_c.get((ds[i], ds[i+1]), ds[i:i+2])]\n", | |
" ds2.append(min_c.get(ds[194], ds[194]))\n", | |
"\n", | |
" # add url\n", | |
" ds2[:len(url_str)] = list(url_str)\n", | |
" ds2 = ''.join(ds2)\n", | |
"\n", | |
" td_b1s_2 = [0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1] + map(int, encode(ds2))[:531]\n", | |
" td_b2s_2 = map(int, encode(ds2))[531:] + [0, 0]\n", | |
" \n", | |
" # convert back to bytes\n", | |
" td_b1_2 = [td_b1s_2[i:i+8] for i in range(0, 544, 8)]\n", | |
" td_b2_2 = [td_b2s_2[i:i+8] for i in range(0, 544, 8)]\n", | |
" \n", | |
" # mask new data bytes\n", | |
" td_b1_3 = [app_mask(mf, b, mfs) for b,mfs in zip(td_b1_2, mfs1)]\n", | |
" td_b2_3 = [app_mask(mf, b, mfs) for b,mfs in zip(td_b2_2, mfs2)]\n", | |
" \n", | |
" # compute EC for new data\n", | |
" ec_block1, ec_block2 = get_ec([int(''.join(map(str, b)), base=2) for b in td_b1_2], \n", | |
" [int(''.join(map(str, b)), base=2) for b in td_b2_2])\n", | |
"\n", | |
" # mask EC bytes\n", | |
" ec_b1_2 = [app_mask(mf, [int(i) for i in bin(b)[2:].zfill(8)], mfs)\n", | |
" for b,mfs in zip(ec_block1, e_mfs1)]\n", | |
" ec_b2_2 = [app_mask(mf, [int(i) for i in bin(b)[2:].zfill(8)], mfs)\n", | |
" for b,mfs in zip(ec_block2, e_mfs2)]\n", | |
"\n", | |
" # make a copy of data\n", | |
" t_data_a2 = t_data_a.copy()\n", | |
" # add format info\n", | |
" t_data_a2[mb_ != 0] = m[mb_ != 0]\n", | |
" \n", | |
" # show the original image\n", | |
" fig,ax = plt.subplots(1, 2, figsize=(12,6))\n", | |
" ax[0].matshow(t_data_a, norm=matplotlib.colors.NoNorm(),\n", | |
" cmap=matplotlib.colors.ListedColormap(['w', 'k']))\n", | |
" \n", | |
" # rewrite the data\n", | |
" for i,b in enumerate(b1):\n", | |
" t_data_a2[b] = td_b1_3[i]\n", | |
"\n", | |
" for i,b in enumerate(b2):\n", | |
" t_data_a2[b] = td_b2_3[i]\n", | |
" \n", | |
" # put back the EC bits\n", | |
" for i,b in enumerate(e1):\n", | |
" t_data_a2[b] = ec_b1_2[i]\n", | |
"\n", | |
" for i,b in enumerate(e2):\n", | |
" t_data_a2[b] = ec_b2_2[i]\n", | |
"\n", | |
" # show the new image\n", | |
" ax[1].matshow(t_data_a2, norm=matplotlib.colors.NoNorm(),\n", | |
" cmap=matplotlib.colors.ListedColormap(['w', 'k']))\n", | |
"\n", | |
" imageio.imwrite(output_file.format(ii),\n", | |
" np.repeat(np.repeat(1 - t_data_a2, 1, 0), 1, 1))\n", | |
" \n", | |
" plt.show()\n", | |
" \n", | |
" print ii, ds2" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 2", | |
"language": "python", | |
"name": "python2" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 2.0 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython2", | |
"version": "2.7.11" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 0 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated this to Python 3 (e.g. changed to
//
for integer division) andimageio
instead of the deprecatedscipy
version.