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.
{ | |
"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
This comment has been minimized.
Updated this to Python 3 (e.g. changed to
//
for integer division) andimageio
instead of the deprecatedscipy
version.