Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
}
@jamestwebber

This comment has been minimized.

Copy link
Owner Author

@jamestwebber jamestwebber commented Oct 27, 2018

Updated this to Python 3 (e.g. changed to // for integer division) and imageio instead of the deprecated scipy version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.