Add QR parse logic

This commit is contained in:
Dag Heyman 2017-02-15 10:13:57 +01:00
parent 2777c0c9d3
commit 9d6e248cae
No known key found for this signature in database
GPG Key ID: 06FC004369E7D338
3 changed files with 182 additions and 8 deletions

159
py/qr/qrparse.py Normal file
View File

@ -0,0 +1,159 @@
"""
Given an image, locates and parses the pixel data in QR codes.
"""
from __future__ import division
from collections import namedtuple
__all__ = ['parse_qr_codes']
Box = namedtuple('Box', ['x', 'y', 'w', 'h'])
def is_dark(pixel):
return pixel == 1
def buffer_matches(matched):
return len(matched) == 5 \
and max(matched[:2] + matched[3:]) <= matched[2] // 2 \
and min(matched[:2] + matched[3:]) >= matched[2] // 6
def check_line(pixels):
matching_dark = False
matched = [0, 0, 0, 0, 0]
for (i, pixel) in enumerate(pixels):
if is_dark(pixel): # Dark pixel
if matching_dark:
matched[-1] += 1
else:
matched = matched[1:] + [1]
matching_dark = True
else: # Light pixel
if not matching_dark:
matched[-1] += 1
else:
if buffer_matches(matched):
width = sum(matched)
yield i - width, width
matched = matched[1:] + [1]
matching_dark = False
# Check final state of buffer
if matching_dark and buffer_matches(matched):
width = sum(matched)
yield i - width, width
def check_row(line, bpp, x_offs, x_width):
return check_line(line[x_offs:x_offs+x_width])
def check_col(image, bpp, 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])
matched = []
for x in range(x_offs, x_offs + x_width):
pixel = line[x*bpp:(x+1)*bpp]
if is_dark(pixel): # Dark pixel
if matching_dark:
matched[-1] += 1
else:
matched.append(1)
matching_dark = True
else: # Light pixel
if not matching_dark:
matched[-1] += 1
else:
matched.append(1)
matching_dark = False
return matching_dark, matched
def read_bits(image, bpp, img_x, img_y, img_w, img_h, size):
qr_x_w = img_w / size
qr_y_h = img_h / size
qr_data = []
for qr_y in range(size):
y = img_y + int(qr_y_h / 2 + qr_y * qr_y_h)
img_line = image.get_line(y)
qr_line = []
for qr_x in range(size):
x = img_x + int(qr_x_w / 2 + qr_x * qr_x_w)
qr_line.append(is_dark(img_line[x]))
qr_data.append(qr_line)
return qr_data
FINDER = [
[True, True, True, True, True, True, True],
[True, False, False, False, False, False, True],
[True, False, True, True, True, False, True],
[True, False, True, True, True, False, True],
[True, False, True, True, True, False, True],
[True, False, False, False, False, False, True],
[True, True, True, True, True, True, True]
]
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
for (tl, tr, bl) in identify_groups(finders):
min_x = min(tl.x, bl.x)
min_y = min(tl.y, tr.y)
width = tr.x + tr.w - min_x
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)
size = len(line_data) + 12
# Read QR code data
yield read_bits(image, bpp, 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):
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)
if match:
(pos, h) = match
y2 = y_offs + pos
if read_bits(image, bpp, x, y2, w, h, 7) == FINDER:
finders.add(Box(x, y2, w, h))
return list(finders)
def identify_groups(locators):
# Find top left
for tl in locators:
x_tol = tl.w / 14
y_tol = tl.h / 14
# Find top right
for tr in locators:
if tr.x > tl.x and abs(tl.y - tr.y) <= y_tol:
# Find bottom left
for bl in locators:
if bl.y > tl.y and abs(tl.x - bl.x) <= x_tol:
yield tl, tr, bl

View File

@ -1,18 +1,17 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import json
import types
import re
from base64 import b32decode
from base64 import b32decode, b64decode
from binascii import a2b_hex, b2a_hex
from ykman.descriptor import get_descriptors
from ykman.driver import ModeSwitchError
from ykman.util import CAPABILITY, TRANSPORT, Mode, derive_key
from ykman.util import CAPABILITY, TRANSPORT, derive_key
from ykman.oath import OathController, Credential
from py.qr import qrparse
from py.qr import qrdecode
NON_FEATURE_CAPABILITIES = [CAPABILITY.CCID, CAPABILITY.NFC]
@ -64,7 +63,7 @@ class Controller(object):
return self._dev_info
def refresh_credentials(self, timestamp, password_key=None):
if password_key != None:
if password_key is not None:
password_key = a2b_hex(password_key)
return [c.to_dict() for c in self._calculate_all(timestamp, password_key)]
@ -97,7 +96,6 @@ class Controller(object):
else:
controller.clear_password()
def add_credential(self, name, key, oath_type, digits, algo, touch, password_key):
dev = self._descriptor.open_device(TRANSPORT.CCID)
controller = OathController(dev.driver)
@ -144,4 +142,21 @@ class Controller(object):
return b32decode(key)
def parse_qr(self, image):
data = b64decode(image['data'])
image = PixelImage(data, image['width'], image['height'])
x = qrparse.locate_finders(image, 2)
return x
class PixelImage(object):
def __init__(self, data, width, height):
self.data = data
self.width = width
self.height = height
def get_line(self, line_number):
return self.data[self.width * line_number:self.width * (line_number + 1)]
controller = Controller()

View File

@ -19,7 +19,7 @@ public:
image = image.convertToFormat(QImage::Format_Mono, Qt::ThresholdDither);
// Iterate over all pixels
QByteArray imageArray(4 + 4 + (image.width() * image.height()), 0);
QByteArray imageArray;
for (int row = 0; row < image.height(); ++row) {
for (int col = 0; col < image.width(); ++col) {
QRgb px = image.pixel(col, row);