Skip to content

Instantly share code, notes, and snippets.

@jamestwebber
Last active July 15, 2019 13:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamestwebber/b6dcafdfa35d5c4784fa3b4fc130b03c to your computer and use it in GitHub Desktop.
Save jamestwebber/b6dcafdfa35d5c4784fa3b4fc130b03c to your computer and use it in GitHub Desktop.
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.
Display the source blob
Display the rendered blob
Raw
{
"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
Copy link
Author

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