Add QR decode logic

This commit is contained in:
Dag Heyman 2017-02-15 11:17:26 +01:00
parent 9d6e248cae
commit e8d2bbcf69
No known key found for this signature in database
GPG Key ID: 06FC004369E7D338
3 changed files with 327 additions and 18 deletions

312
py/qr/qrdecode.py Normal file
View File

@ -0,0 +1,312 @@
"""
Given a 2D matrix of pixel data from a QR code, this module will decode and
return the data contained within. Note that error correction is not
implemented, and the input will thus have to be without any errors. Only
supports numeric, alphanumeric and byte encodings.
"""
from __future__ import division
def decode_qr_data(qr_data):
"""Given a 2D matrix of QR data, returns the encoded string"""
size = len(qr_data)
version = (size - 17) // 4
level = bits_to_int(qr_data[8][:2])
mask = bits_to_int(qr_data[8][2:5]) ^ 0b101
read_mask = [x[:] for x in [[1]*size]*size]
# Verify/Remove alignment patterns
remove_locator_patterns(qr_data, read_mask)
remove_alignment_patterns(read_mask, version)
remove_timing_patterns(read_mask)
if version >= 7: # QR Codes version 7 or larger have version info.
remove_version_info(read_mask)
# Read and deinterleave
buf = bits_to_bytes(read_bits(qr_data, read_mask, mask))
buf = deinterleave(buf, INTERLEAVE_PARAMS[version][level])
bits = bytes_to_bits(buf)
# Decode data
buf = ''
while bits:
data, bits = parse_bits(bits, version)
buf += data
return buf
LOCATOR_BOX = [
[1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1]
]
MASKS = [
lambda x, y: (y+x) % 2 == 0,
lambda x, y: y % 2 == 0,
lambda x, y: x % 3 == 0,
lambda x, y: (y+x) % 3 == 0,
lambda x, y: (y//2 + x//3) % 2 == 0,
lambda x, y: (y*x) % 2 + (y*x) % 3 == 0,
lambda x, y: ((y*x) % 2 + (y*x) % 3) % 2 == 0,
lambda x, y: ((y+x) % 2 + (y*x) % 3) % 2 == 0
]
ALPHANUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
EC_LEVELS = ['H', 'Q', 'M', 'L']
INTERLEAVE_PARAMS = [ # From ISO/IEC 18004:2006
[], # H, Q, M, L
[1 * (9,), 1 * (13,), 1 * (16,), 1 * (19,)], # Version 1
[1 * (16,), 1 * (22,), 1 * (28,), 1 * (34,)],
[2 * (13,), 2 * (17,), 1 * (44,), 1 * (55,)],
[4 * (9,), 2 * (24,), 2 * (32,), 1 * (80,)],
[2 * (11,) + 2 * (12,), 2 * (15,) + 2 * (16,), 2 * (43,), 1 * (108,)],
[4 * (15,), 4 * (19,), 4 * (27,), 2 * (68,)],
[4 * (13,) + 1 * (14,), 2 * (14,) + 4 * (15,), 4 * (31,), 2 * (78,)],
[4 * (14,) + 2 * (15,), 4 * (18,) + 2 * (19,), 2 * (38,) + 2 * (39,), 2 * (97,)], # noqa: E501
[4 * (12,) + 4 * (13,), 4 * (16,) + 4 * (17,), 3 * (36,) + 2 * (37,), 2 * (116,)], # noqa: E501
[6 * (15,) + 2 * (16,), 6 * (19,) + 2 * (20,), 4 * (43,) + 1 * (44,), 2 * (68,) + 2 * (69,)], # noqa: E501
[3 * (12,) + 8 * (13,), 4 * (22,) + 4 * (23,), 1 * (50,) + 4 * (51,), 4 * (81,)], # noqa: E501
[7 * (14,) + 4 * (15,), 4 * (20,) + 6 * (21,), 6 * (36,) + 2 * (37,), 2 * (92,) + 2 * (93,)], # noqa: E501
[12 * (11,) + 4 * (12,), 8 * (20,) + 4 * (21,), 8 * (37,) + 1 * (38,), 4 * (107,)], # noqa: E501
[11 * (12,) + 5 * (13,), 11 * (16,) + 5 * (17,), 4 * (40,) + 5 * (41,), 3 * (115,) + 1 * (116,)], # noqa: E501
[11 * (12,) + 7 * (13,), 5 * (24,) + 7 * (25,), 5 * (41,) + 5 * (42,), 5 * (87,) + 1 * (88,)], # noqa: E501
[3 * (15,) + 13 * (16,), 15 * (19,) + 2 * (20,), 7 * (45,) + 3 * (46,), 5 * (98,) + 1 * (99,)], # noqa: E501
[2 * (14,) + 17 * (15,), 1 * (22,) + 15 * (23,), 10 * (46,) + 1 * (47,), 1 * (107,) + 5 * (108,)], # noqa: E501
[2 * (14,) + 19 * (15,), 17 * (22,) + 1 * (23,), 9 * (43,) + 4 * (44,), 5 * (120,) + 1 * (121,)], # noqa: E501
[9 * (13,) + 16 * (14,), 17 * (21,) + 4 * (22,), 3 * (44,) + 11 * (45,), 3 * (113,) + 4 * (114,)], # noqa: E501
[15 * (15,) + 10 * (16,), 15 * (24,) + 5 * (25,), 3 * (41,) + 13 * (42,), 3 * (107,) + 5 * (108,)], # noqa: E501
[19 * (16,) + 6 * (17,), 17 * (22,) + 6 * (23,), 17 * (42,), 4 * (116,) + 4 * (117,)], # noqa: E501
[34 * (13,), 7 * (24,) + 16 * (25,), 17 * (46,), 2 * (111,) + 7 * (112,)],
[16 * (15,) + 14 * (16,), 11 * (24,) + 14 * (25,), 4 * (47,) + 14 * (48,), 4 * (121,) + 5 * (122,)], # noqa: E501
[30 * (16,) + 2 * (17,), 11 * (24,) + 16 * (25,), 6 * (45,) + 14 * (46,), 6 * (117,) + 4 * (118,)], # noqa: E501
[22 * (15,) + 13 * (16,), 7 * (24,) + 22 * (25,), 8 * (47,) + 13 * (48,), 8 * (106,) + 4 * (107,)], # noqa: E501
[33 * (16,) + 4 * (17,), 28 * (22,) + 6 * (23,), 19 * (46,) + 4 * (47,), 10 * (114,) + 2 * (115,)], # noqa: E501
[12 * (15,) + 28 * (16,), 8 * (23,) + 26 * (24,), 22 * (45,) + 3 * (46,), 8 * (122,) + 4 * (123,)], # noqa: E501
[11 * (15,) + 31 * (16,), 4 * (24,) + 31 * (25,), 3 * (45,) + 23 * (46,), 3 * (117,) + 10 * (118,)], # noqa: E501
[19 * (15,) + 26 * (16,), 1 * (23,) + 37 * (24,), 21 * (45,) + 7 * (46,), 7 * (116,) + 7 * (117,)], # noqa: E501
[23 * (15,) + 25 * (16,), 15 * (24,) + 25 * (25,), 19 * (47,) + 10 * (48,), 5 * (115,) + 10 * (116,)], # noqa: E501
[23 * (15,) + 28 * (16,), 42 * (24,) + 1 * (25,), 2 * (46,) + 29 * (47,), 13 * (115,) + 3 * (116,)], # noqa: E501
[19 * (15,) + 35 * (16,), 10 * (24,) + 35 * (25,), 10 * (46,) + 23 * (47,), 17 * (115,)], # noqa: E501
[11 * (15,) + 46 * (16,), 29 * (24,) + 19 * (25,), 14 * (46,) + 21 * (47,), 17 * (115,) + 1 * (116,)], # noqa: E501
[59 * (16,) + 1 * (17,), 44 * (24,) + 7 * (25,), 14 * (46,) + 23 * (47,), 13 * (115,) + 6 * (116,)], # noqa: E501
[22 * (15,) + 41 * (16,), 39 * (24,) + 14 * (25,), 12 * (47,) + 26 * (48,), 12 * (121,) + 7 * (122,)], # noqa: E501
[2 * (15,) + 64 * (16,), 46 * (24,) + 10 * (25,), 6 * (47,) + 34 * (48,), 6 * (121,) + 14 * (122,)], # noqa: E501
[24 * (15,) + 46 * (16,), 49 * (24,) + 10 * (25,), 29 * (46,) + 14 * (47,), 17 * (122,) + 4 * (123,)], # noqa: E501
[42 * (15,) + 32 * (16,), 48 * (24,) + 14 * (25,), 13 * (46,) + 32 * (47,), 4 * (122,) + 18 * (123,)], # noqa: E501
[10 * (15,) + 67 * (16,), 43 * (24,) + 22 * (25,), 40 * (47,) + 7 * (48,), 20 * (117,) + 4 * (118,)], # noqa: E501
[20 * (15,) + 61 * (16,), 34 * (24,) + 34 * (25,), 18 * (47,) + 31 * (48,), 19 * (118,) + 6 * (119,)], # noqa: E501
]
ALIGNMENT_POSITIONS = [ # From ISO/IEC 18004:2006
[],
[],
[18], # Version 2
[22],
[26],
[30],
[34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106], # Version 24
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170]
]
def check_region(data, x, y, match):
"""Compares a region to the given """
w = len(match[0])
for cy in range(len(match)):
if match[cy] != data[y+cy][x:x+w]:
return False
return True
def zero_region(data, x, y, w, h):
"""Fills a region with zeroes."""
for by in range(y, y+h):
line = data[by]
data[by] = line[:x] + [0]*w + line[x+w:]
def bits_to_int(bits):
"""Convers a list of bits into an integer"""
val = 0
for bit in bits:
val = (val << 1) | bit
return val
def bits_to_bytes(bits):
"""Converts a list of bits into a string of bytes"""
return ''.join([chr(bits_to_int(bits[i:i+8]))
for i in range(0, len(bits), 8)])
def bytes_to_bits(buf):
"""Converts a string of bytes to a list of bits"""
return [b >> i & 1 for b in map(ord, buf) for i in range(7, -1, -1)]
def deinterleave(data, b_cap):
"""De-interleaves the bytes from a QR code"""
n_bufs = len(b_cap)
bufs = []
for _ in range(n_bufs):
bufs.append([])
b_i = 0
for i in range(sum(b_cap)):
b = data[i]
while b_cap[b_i] <= len(bufs[b_i]):
b_i = (b_i + 1) % n_bufs
bufs[b_i].append(b)
b_i = (b_i + 1) % n_bufs
buf = ''
for b in bufs:
buf += ''.join(b)
return buf
def parse_bits(bits, version):
"""
Parses and decodes a TLV value from the given list of bits.
Returns the parsed data and the remaining bits, if any.
"""
enc, bits = bits_to_int(bits[:4]), bits[4:]
if enc == 0: # End of data.
return '', []
elif enc == 1: # Number
n_l = 10 if version < 10 else 12 if version < 27 else 14
l, bits = bits_to_int(bits[:n_l]), bits[n_l:]
buf = ''
while l > 0:
if l >= 3:
num, bits = bits_to_int(bits[:10]), bits[10:]
elif l >= 2:
num, bits = bits_to_int(bits[:7]), bits[7:]
else:
num, bits = bits_to_int(bits[:3]), bits[3:]
buf += str(num)
elif enc == 2: # Alphanumeric
n_l = 9 if version < 10 else 11 if version < 27 else 13
l, bits = bits_to_int(bits[:n_l]), bits[n_l:]
buf = ''
while l > 0:
if l >= 2:
num, bits = bits_to_int(bits[:11]), bits[11:]
buf += ALPHANUM[num // 45]
buf += ALPHANUM[num % 45]
l -= 2
else:
num, bits = bits_to_int(bits[:6]), bits[6:]
buf += ALPHANUM[num]
l -= 1
return buf, bits
elif enc == 4: # Bytes
n_l = 8 if version < 10 else 16
l, bits = bits_to_int(bits[:n_l]), bits[n_l:]
return bits_to_bytes(bits[:l*8]), bits[l*8:]
else:
raise ValueError('Unsupported encoding: %d' % enc)
def remove_locator_patterns(data, mask):
"""
Verifies and blanks out the three large locator patterns and dedicated
whitespace surrounding them.
"""
width = len(data)
if not check_region(data, 0, 0, LOCATOR_BOX):
raise ValueError('Top-left square missing')
zero_region(mask, 0, 0, 9, 9)
if not check_region(data, width-7, 0, LOCATOR_BOX):
raise ValueError('Top-right square missing')
zero_region(mask, width-8, 0, 8, 9)
if not check_region(data, 0, width-7, LOCATOR_BOX):
raise ValueError('Bottom-left square missing')
zero_region(mask, 0, width-8, 9, 8)
def remove_alignment_patterns(mask, version):
"""Blanks out alignment patterns."""
positions = ALIGNMENT_POSITIONS[version]
for y in positions:
for x in positions:
# Do not try to remove patterns in locator pattern positions.
if (x, y) not in [(6, 6), (6, positions[-1]), (positions[-1], 6)]:
zero_region(mask, x-2, y-2, 5, 5)
def remove_timing_patterns(mask):
"""Blanks out tracking patterns."""
width = len(mask)
mask[6] = [0] * width
for y in range(width):
mask[y][6] = 0
def remove_version_info(mask):
"""Removes version data. Only for version 7 and greater."""
width = len(mask)
zero_region(mask, width-11, 0, 3, 6)
zero_region(mask, 0, width-11, 5, 6)
def read_bits(qr_data, read_mask, mask):
"""Reads the data contained in a QR code as bits."""
size = len(qr_data)
mask_f = MASKS[mask]
bits = []
# Skip over vertical timing pattern
for x in reversed(list(range(0, 6, 2)) + list(range(7, size, 2))):
y_range = range(0, size)
if (size - x) % 4 != 0:
y_range = reversed(y_range)
for y in y_range:
for i in reversed(range(2)):
if read_mask[y][x+i]:
bits.append(qr_data[y][x+i] ^ mask_f(x+i, y))
return bits

View File

@ -48,20 +48,20 @@ def check_line(pixels):
yield i - width, width
def check_row(line, bpp, x_offs, x_width):
def check_row(line, x_offs, x_width):
return check_line(line[x_offs:x_offs+x_width])
def check_col(image, bpp, x, y_offs, y_height):
def check_col(image, x, y_offs, y_height):
return check_line(bytes([image.get_line(i)[x]
for i in range(y_offs, y_offs + y_height)]))
def read_line(line, bpp, x_offs, x_width):
matching_dark = not is_dark(line[x_offs*bpp:(x_offs+1)*bpp])
def read_line(line, x_offs, x_width):
matching_dark = not is_dark(line[x_offs])
matched = []
for x in range(x_offs, x_offs + x_width):
pixel = line[x*bpp:(x+1)*bpp]
pixel = line[x]
if is_dark(pixel): # Dark pixel
if matching_dark:
matched[-1] += 1
@ -77,7 +77,7 @@ def read_line(line, bpp, x_offs, x_width):
return matching_dark, matched
def read_bits(image, bpp, img_x, img_y, img_w, img_h, size):
def read_bits(image, img_x, img_y, img_w, img_h, size):
qr_x_w = img_w / size
qr_y_h = img_h / size
qr_data = []
@ -104,9 +104,6 @@ FINDER = [
def parse_qr_codes(image, min_res=2):
size = image.size()
bpp = image.bytesPerLine() // size.width()
finders = locate_finders(image, min_res)
# Arrange finders into QR codes and extract data
@ -117,27 +114,26 @@ def parse_qr_codes(image, min_res=2):
height = bl.y + bl.h - min_y
# Determine resolution by reading timing pattern
line = image.scanLine(min_y + int(6.5 / 7 * max(tl.h, tr.h)))
_, line_data = read_line(line, bpp, min_x, width)
line = image.get_line(min_y + int(6.5 / 7 * max(tl.h, tr.h)))
_, line_data = read_line(line, min_x, width)
size = len(line_data) + 12
# Read QR code data
yield read_bits(image, bpp, min_x, min_y, width, height, size)
yield read_bits(image, min_x, min_y, width, height, size)
def locate_finders(image, min_res):
bpp = 1
finders = set()
for y in range(0, image.height, min_res * 3):
for (x, w) in check_row(image.get_line(y), bpp, 0, image.width):
for (x, w) in check_row(image.get_line(y), 0, image.width):
x_offs = x + w // 2
y_offs = max(0, y - w)
y_height = min(image.height - y_offs, 2 * w)
match = next(check_col(image, bpp, x_offs, y_offs, y_height), None)
match = next(check_col(image, x_offs, y_offs, y_height), None)
if match:
(pos, h) = match
y2 = y_offs + pos
if read_bits(image, bpp, x, y2, w, h, 7) == FINDER:
if read_bits(image, x, y2, w, h, 7) == FINDER:
finders.add(Box(x, y2, w, h))
return list(finders)

View File

@ -145,8 +145,9 @@ class Controller(object):
def parse_qr(self, image):
data = b64decode(image['data'])
image = PixelImage(data, image['width'], image['height'])
x = qrparse.locate_finders(image, 2)
return x
for qr in qrparse.parse_qr_codes(image, 2):
return qrdecode.decode_qr_data(qr)
return ""
class PixelImage(object):