#!/usr/bin/env python # -*- coding: utf-8 -*- """ This module can be used to scan QR-Codes (C) 2022 Louis Heredero louis.heredero@edu.vs.ch """ import cv2 import numpy as np from math import sqrt, degrees, atan2, radians, cos, sin import imutils import matplotlib.pyplot as plt from PIL import Image DB_WIN = False TOL_CNT_DIST = 10 # Tolerance distance between finders' centers MASKS = [ lambda x,y: (x+y)%2 == 0, lambda x,y: y%2 == 0, lambda x,y: (x)%3 == 0, lambda x,y: (x+y)%3 == 0, lambda x,y: (y//2+x//3)%2 == 0, lambda x,y: ((x*y)%2 + (x*y)%3) == 0, lambda x,y: ((x*y)%2 + (x*y)%3)%2 == 0, lambda x,y: ((x+y)%2 + (x*y)%3)%2 == 0 ] ALIGNMENT_PATTERN_LOCATIONS = [ [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 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], [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] ] VERSIONS = [] with open("qr_versions.txt", "r") as f: versions = f.read().split("\n\n") for v in versions: lvls = [list(map(int, lvl.split("\t"))) for lvl in v.split("\n")] VERSIONS.append(lvls) VERSIONS = np.array(VERSIONS) ERROR_CORRECTION = [] with open("error_correction.txt", "r") as f: ecs = f.read().split("\n\n") for ec in ecs: lvls = [list(map(int, lvl.split("\t"))) for lvl in ec.split("\n")] lvls = [lvl + [0]*(6-len(lvl)) for lvl in lvls] ERROR_CORRECTION.append(lvls) ERROR_CORRECTION = np.array(ERROR_CORRECTION) EC_PARAMS = [] with open("ec_params.txt", "r") as f: ecs = f.read().split("\n\n") for ec in ecs: lvls = [list(map(int, lvl.split("\t"))) for lvl in ec.split("\n") if lvl] EC_PARAMS.append(lvls) EC_PARAMS = np.array(EC_PARAMS) ALPHANUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" class GF: def __init__(self, val): self.val = val def copy(self): return GF(self.val) def __add__(self, n): return GF(self.val ^ n.val) def __sub__(self, n): return GF(self.val ^ n.val) def __mul__(self, n): if self.val == 0 or n.val == 0: return GF(0) return GF.EXP[GF.LOG[self.val].val + GF.LOG[n.val].val].copy() def __truediv__(self, n): if n.val == 0: raise ZeroDivisionError if self.val == 0: return GF(0) return GF.EXP[(GF.LOG[self.val].val + 255 - GF.LOG[n.val].val)%255].copy() def __pow__(self, n): return GF.EXP[(GF.LOG[self.val].val * n.val)%255].copy() def __repr__(self): return self.val.__repr__() def log(self): return GF.LOG[self.val] GF.EXP = [GF(0)]*512 GF.LOG = [GF(0)]*256 value = 1 for exponent in range(255): GF.LOG[value] = GF(exponent) GF.EXP[exponent] = GF(value) value = ((value << 1) ^ 285) if value > 127 else value << 1 for i in range(255, 512): GF.EXP[i] = GF.EXP[i-255].copy() class Poly: def __init__(self, coefs): self.coefs = coefs.copy() @property def deg(self): return len(self.coefs) def copy(self): return Poly(self.coefs) def __add__(self, p): d1, d2 = self.deg, p.deg deg = max(d1,d2) result = [GF(0) for i in range(deg)] for i in range(d1): result[i + deg - d1] = self.coefs[i] for i in range(d2): result[i + deg - d2] += p.coefs[i] return Poly(result) def __mul__(self, p): result = [GF(0) for i in range(self.deg+p.deg-1)] for i in range(p.deg): for j in range(self.deg): result[i+j] += self.coefs[j] * p.coefs[i] return Poly(result) def __truediv__(self, p): dividend = self.coefs.copy() dividend += [GF(0) for i in range(p.deg-1)] quotient = [] for i in range(self.deg): coef = dividend[i] / p.coefs[0] quotient.append(coef) for j in range(p.deg): dividend[i+j] -= p.coefs[j] * coef while dividend[0].val == 0: dividend.pop(0) return [Poly(quotient), Poly(dividend)] def __repr__(self): return f"" def eval(self, x): y = GF(0) for i in range(self.deg): y += self.coefs[i] * x**GF(self.deg-i-1) return y def del_lead_zeros(self): while len(self.coefs) > 1 and self.coefs[0].val == 0: self.coefs.pop(0) if len(self.coefs) == 0: self.coefs = [GF(0)] return self def center(c): M = cv2.moments(c) if M["m00"] != 0: cX = int(M["m10"] / M["m00"]) cY = int(M["m01"] / M["m00"]) return (cX, cY) return (None, None) def dist(p1, p2): return sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2) def rotate(o, p, a): ox, oy = o px, py = p a = radians(a) c, s = cos(a), sin(a) return [ int(ox + c*(px-ox) - s * (py-oy)), int(oy + s*(px-ox) + c * (py-oy)) ] def rotate_cnt(cnt, o, a): c = cnt pts = c[:, 0, :] pts = np.array([rotate(o, p, a) for p in pts]) c[:, 0, :] = pts return c.astype(np.int32) def is_finder(i, cnts, hrcy): c1 = cnts[i] h1 = hrcy[0][i] cX1, cY1 = center(c1) if len(c1) != 4: return False if cX1 is None: return False if h1[2] == -1: return False i2 = h1[2] c2 = cnts[i2] h2 = hrcy[0][i2] cX2, cY2 = center(c2) if cX2 is None: return False if len(c2) != 4: return False if abs(dist((cX1, cY1), (cX2, cY2))) > TOL_CNT_DIST: return False if h2[2] == -1: return False i3 = h2[2] c3 = cnts[i3] h3 = hrcy[0][i3] cX3, cY3 = center(c3) if len(c3) != 4: return False if cX3 is None: return False if abs(dist((cX1, cY1), (cX3, cY3))) > TOL_CNT_DIST: return False return True def decode(img): grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) grey = cv2.GaussianBlur(grey, (5,5), 0) #if DB_WIN: cv2.imshow("grey", grey) bw = cv2.threshold(grey, np.mean(grey), 255, cv2.THRESH_BINARY)[1] if DB_WIN: cv2.imshow("bw", bw) #laplacian = cv2.Laplacian(bw, cv2.CV_8U, 15) #cv2.imshow("laplacian", laplacian) contours, hierarchy = cv2.findContours(bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) if DB_WIN: img2 = img.copy() cv2.drawContours(img2, contours, -1, (0,255,0), 1) candidates = [] contours = list(contours) for i, cnt in enumerate(contours): peri = cv2.arcLength(cnt, True) contours[i] = cv2.approxPolyDP(cnt, 0.04 * peri, True) for i in range(len(contours)): if is_finder(i, contours, hierarchy): candidates.append(i) if DB_WIN: for i in candidates: cv2.drawContours(img2, contours, i, (0,0,255), 1) cv2.imshow("contours", img2) if DB_WIN: img3 = img.copy() corners = [] corners_cnts = [] for i1 in candidates: i2 = hierarchy[0][i1][2] i3 = hierarchy[0][i2][2] c1 = contours[i1] c2 = contours[i2] c3 = contours[i3] x1, y1 = center(c1) x2, y2 = center(c2) x3, y3 = center(c3) x, y = (x1+x2+x3)/3, (y1+y2+y3)/3 x, y = int(x), int(y) corners.append((x,y)) corners_cnts.append([c1,c2,c3]) if DB_WIN: cv2.line(img3, [x, y-10], [x, y+10], (0,255,0), 1) cv2.line(img3, [x-10, y], [x+10, y], (0,255,0), 1) #cv2.drawContours(img3, [c1], 0, (255,0,0), 1) #cv2.drawContours(img3, [c2], 0, (0,255,0), 1) #cv2.drawContours(img3, [c3], 0, (0,0,255), 1) if DB_WIN: cv2.imshow("lines", img3) if len(corners) != 3: return d01 = dist(corners[0], corners[1]) d02 = dist(corners[0], corners[2]) d12 = dist(corners[1], corners[2]) diffs = [abs(d01-d02), abs(d01-d12), abs(d02-d12)] mdiff = min(diffs) i = diffs.index(mdiff) a = corners.pop(i) b, c = corners V = [img.shape[1], img.shape[0]] d = [(b[0]+c[0])/2, (b[1]+c[1])/2] v = [d[0]-a[0], d[1]-a[1]] C = [img.shape[1]/2, img.shape[0]/2] angle = degrees(atan2(v[1], v[0])) angle_diff = angle-45 if DB_WIN: img4 = img.copy() cv2.line(img4, a, [int(d[0]), int(d[1])], (0,255,0), 1) cv2.imshow("vecs", img4) cA = corners_cnts[i][0] cA = rotate_cnt(cA, C, -angle_diff) rA = cv2.boundingRect(cA) cA2 = corners_cnts[i][1] cA2 = rotate_cnt(cA2, C, -angle_diff) rA2 = cv2.boundingRect(cA2) cB = corners_cnts[i-1][0] cB = rotate_cnt(cB, C, -angle_diff) rB = cv2.boundingRect(cB) cC = corners_cnts[i-2][0] cC = rotate_cnt(cC, C, -angle_diff) rC = cv2.boundingRect(cC) a, b, c = rotate(C, a, -angle_diff), rotate(C, b, -angle_diff), rotate(C, c, -angle_diff) if DB_WIN: img5 = img.copy() img5 = imutils.rotate(img5, angle_diff) cv2.rectangle(img5, rA, (255,0,0), 1) cv2.rectangle(img5, rB, (0,255,0), 1) cv2.rectangle(img5, rC, (0,0,255), 1) cv2.line(img5, a, b, (255,255,255), 1) cv2.line(img5, a, c, (255,255,255), 1) cv2.imshow("rot", img5) wul = rA[2] if rB[1] < rC[1]: wur = rB[2] else: wur = rC[2] if rB[1] < rC[1]: D = dist(a, b) else: D = dist(a, c) X = (wul + wur)/14 V = (D/X - 10)/4 V = round(V) size = V*4+17 grid = np.zeros([size, size]) bw_rot = imutils.rotate(bw, angle_diff) if DB_WIN: img6 = img.copy() img6 = imutils.rotate(img6, angle_diff) OX, OY = (rA[0]+rA2[0])/2, (rA[1]+rA2[1])/2 #Not fully visible if (OX + size*X+1 >= bw_rot.shape[1]) or (OY + size*X+1 >= bw_rot.shape[0]): return None for y in range(size): for x in range(size): zone = bw_rot[ int(OY + y*X-1): int(OY + y*X+2), int(OX + x*X-1): int(OX + x*X+2) ] grid[y, x] = 1-round(np.mean(zone)/255) #cv2.circle(img6, [int(OX+x*X), int(OY+y*X)], 3, (0,0,255), 1) #print(size) #cv2.rectangle(img6, [int(OX-X/2), int(OY-X/2), int(X), int(X)], (0,255,0), 1) #cv2.imshow("grid", img6) if DB_WIN: cv2.namedWindow("bw_rot", cv2.WINDOW_NORMAL) cv2.imshow("bw_rot", bw_rot) #cv2.imshow("code", cv2.resize(grid, [size*10,size*10])) value = _decode(grid, V) if value is None: value = _decode(1-grid, V) return value def _decode(grid, V, flipped=None, f2=False): if not flipped is None: grid = flipped lvl, mask_i = get_fmt(grid, f2) mask = MASKS[mask_i] unmasked = grid.copy() mask_area = np.ones(grid.shape) mask_area[:9, :9] = 0 mask_area[:9, -8:] = 0 mask_area[-8:, :9] = 0 #Add alignment patterns locations = ALIGNMENT_PATTERN_LOCATIONS[V-1] if V > 1: for y in locations: for x in locations: #Check if not overlapping with finders if np.all(mask_area[y-2:y+3, x-2:x+3] == 1): mask_area[y-2:y+3, x-2:x+3] = 0 mask_area[y-1:y+2, x-1:x+2] = 0 mask_area[y, x] = 0 #Add timing patterns timing_length = grid.shape[0]-2*8 mask_area[6, 8:-8] = np.zeros([timing_length]) mask_area[8:-8, 6] = np.zeros([timing_length]) if V >= 7: mask_area[-11:-8, :6] = 0 mask_area[:6, -11:-8] = 0 for y in range(grid.shape[0]): for x in range(grid.shape[1]): if mask_area[y,x] == 1 and mask(x,y): unmasked[y,x] = 1-unmasked[y,x] if DB_WIN: cv2.namedWindow("grid", cv2.WINDOW_NORMAL) cv2.namedWindow("unmasked", cv2.WINDOW_NORMAL) #cv2.namedWindow("mask_area", cv2.WINDOW_NORMAL) cv2.imshow("grid", 1-grid) cv2.imshow("unmasked", 1-unmasked) #cv2.imshow("mask_area", mask_area) #Un-place data dir_ = -1 #-1 = up | 1 = down x, y = grid.shape[1]-1, grid.shape[0]-1 i = 0 zigzag = 0 final_data_bits = "" while x >= 0: if mask_area[y,x] == 1: final_data_bits += str(int(unmasked[y, x])) if ((dir_+1)/2 + zigzag)%2 == 0: x -= 1 else: y += dir_ x += 1 if y == -1 or y == grid.shape[0]: dir_ = -dir_ y += dir_ x -= 2 else: zigzag = 1-zigzag #Vertical timing pattern if x == 6: x -= 1 #Remove remainder bits if 2 <= V < 7: final_data_bits = final_data_bits[:-7] elif 14 <= V < 21 or 28 <= V < 35: final_data_bits = final_data_bits[:-3] elif 21 <= V < 28: final_data_bits = final_data_bits[:-4] ec = ERROR_CORRECTION[V-1, lvl] #print(ec) codewords = [final_data_bits[i:i+8] for i in range(0,len(final_data_bits),8)] #Only one block if ec[2]+ec[4] == 1: data_codewords = codewords[:ec[3]] ec_codewords = codewords[ec[3]:] else: group1, group2 = [], [] group_ec = [] for b in range(ec[2]): block = [] for i in range(ec[3]): block.append(codewords[b+i*(ec[2]+ec[4])]) group1.append(block) if ec[4] > 0: for b in range(ec[2], ec[2]+ec[4]): block = [] for i in range(ec[5]): off = 0 if i >= ec[3]: off = (i-ec[3]+1)*ec[2] block.append(codewords[b+i*(ec[2]+ec[4])-off]) group2.append(block) codewords = codewords[ec[2]*ec[3]+ec[4]*ec[5]:] for b in range(ec[2]+ec[4]): block = [] for i in range(ec[1]): block.append(codewords[b+i*(ec[2]+ec[4])]) group_ec.append(block) data_codewords = sum(group1, []) + sum(group2, []) ec_codewords = sum(group_ec, []) #print(data_codewords) #print(ec_codewords) try: decoded = correct(data_codewords, ec_codewords) except ReedSolomonException as e: #print(e) if not f2: return _decode(grid, V, flipped, True) elif flipped is None: f = grid.copy() f = np.rot90(np.fliplr(f)) return _decode(grid, V, f, False) else: #raise ReedSolomonException("Cannot decode") return None decoded = "".join(list(map(lambda c: f"{c.val:08b}", decoded.coefs))) mode, decoded = decoded[:4], decoded[4:] MODES = ["0001", "0010", "0100", "1000"] mode = MODES.index(mode) if 1 <= V < 10: char_count_len = [10,9,8,8][mode] elif 10 <= V < 27: char_count_len = [12,11,16,10][mode] elif 27 <= V < 41: char_count_len = [14,13,16,12][mode] length, decoded = decoded[:char_count_len], decoded[char_count_len:] length = int(length, 2) value = None if mode == 0: value = "" _ = length//3 l = _ * 10 if 10 - _ == 2: l += 7 else: l += 4 data = decoded[:l] groups = [data[i:i+10] for i in range(0, l, 10)] for group in groups: value += str(int(group,2)) value = int(value) elif mode == 1: value = "" data = decoded[:length//2 * 11 + (length%2)*6] for i in range(0, len(data), 11): s = data[i:i+11] val = int(s, 2) if len(s) == 6: value += ALPHANUM[val] else: value += ALPHANUM[val//45] value += ALPHANUM[val%45] elif mode == 2: data = decoded[:length*8] data = [data[i:i+8] for i in range(0, length*8, 8)] data = list(map(lambda b: int(b, 2), data)) value = bytes(data).decode("ISO-8859-1") elif mode == 3: value = [] data = decoded[:length*13] for i in range(0, len(data), 13): val = int(data[i:i+13], 2) msb = val // 0xc0 lsb = val % 0xc0 dbyte = (msb << 8) + lsb if 0 <= dbyte <= 0x9ffc - 0x8140: dbyte += 0x8140 elif 0xe040 - 0xc140 <= dbyte <= 0xebbf - 0xc140: dbyte += 0xc140 value.append(dbyte >> 8) value.append(dbyte & 0xff) value = bytes(value).decode("shift_jis") #print("value:", value) return value # If unreadable # -> _decode(f2=True) # -> mirror image class ReedSolomonException(Exception): pass def correct(data, ec): n = len(ec) data = Poly([GF(int(cw, 2)) for cw in data+ec]) ##print("data", list(map(lambda c:c.val, data.coefs))) syndrome = [0]*n corrupted = False for i in range(n): syndrome[i] = data.eval(GF.EXP[i]) if syndrome[i].val != 0: corrupted = True if not corrupted: print("No errors") return data syndrome = Poly(syndrome[::-1]) #print("syndrome", syndrome) #Find locator poly sigma, omega = euclidean_algorithm(Poly([GF(1)]+[GF(0) for i in range(n)]), syndrome, n) #print("sigma", sigma) #print("omega", omega) error_loc = find_error_loc(sigma) error_mag = find_error_mag(omega, error_loc) for i in range(len(error_loc)): pos = GF(error_loc[i]).log() pos = data.deg - pos.val - 1 if pos < 0: raise ReedSolomonException("Bad error location") data.coefs[pos] += GF(error_mag[i]) return data def euclidean_algorithm(a, b, R): if a.deg < b.deg: a, b = b, a r_last = a r = b t_last = Poly([GF(0)]) t = Poly([GF(1)]) while r.deg-1 >= int(R/2): r_last_last = r_last t_last_last = t_last r_last = r t_last = t if r_last.coefs[0] == 0: raise ReedSolomonException("r_{i-1} was zero") r = r_last_last q = Poly([GF(0)]) denom_lead_term = r_last.coefs[0] dlt_inv = denom_lead_term ** GF(-1) I = 0 while r.deg >= r_last.deg and r.coefs[0] != 0: I += 1 deg_diff = r.deg - r_last.deg scale = r.coefs[0] * dlt_inv q += Poly([scale]+[GF(0) for i in range(deg_diff)]) r += r_last * Poly([scale]+[GF(0) for i in range(deg_diff)]) q.del_lead_zeros() r.del_lead_zeros() if I > 100: raise ReedSolomonException("Too long") t = (q * t_last).del_lead_zeros() + t_last_last t.del_lead_zeros() if r.deg >= r_last.deg: raise ReedSolomonException("Division algorithm failed to reduce polynomial") sigma_tilde_at_zero = t.coefs[-1] if sigma_tilde_at_zero.val == 0: raise ReedSolomonException("sigma_tilde(0) was zero") inv = Poly([sigma_tilde_at_zero ** GF(-1)]) sigma = t * inv omega = r * inv return [sigma, omega] def find_error_loc(error_loc): num_errors = error_loc.deg-1 if num_errors == 1: return [error_loc.coefs[-2].val] result = [0]*num_errors e = 0 i = 1 while i < 256 and e < num_errors: if error_loc.eval(GF(i)).val == 0: result[e] = (GF(i) ** GF(-1)).val e += 1 i += 1 if e != num_errors: raise ReedSolomonException("Error locator degree does not match number of roots") return result def find_error_mag(error_eval, error_loc): s = len(error_loc) result = [0]*s for i in range(s): xi_inv = GF(error_loc[i]) ** GF(-1) denom = GF(1) for j in range(s): if i != j: denom *= GF(1) + GF(error_loc[j]) * xi_inv result[i] = ( error_eval.eval(xi_inv) * (denom ** GF(-1)) ).val return result def get_fmt(grid ,f2=False): fmt1 = list(grid[0:6, 8]) + [grid[7,8], grid[8,8], grid[8,7]] + list(grid[8, 0:6][::-1]) fmt2 = list(grid[8, -8:][::-1]) + list(grid[-7:, 8]) fmt1 = "".join([str(int(b)) for b in fmt1])[::-1] fmt2 = "".join([str(int(b)) for b in fmt2])[::-1] if f2: return decode_fmt(fmt2) return decode_fmt(fmt1) def decode_fmt(fmt): format_data = int(fmt, 2) format_data ^= 0b101010000010010 format_data = f"{format_data:015b}" closest = None with open("./valid_format_str.txt", "r") as f: for i, format_str in enumerate(f): diff = sum(1 for a, b in zip(format_data, format_str) if a != b) if closest is None or diff < closest[1]: closest = (i, diff) lvl = closest[0] >> 3 lvl = (5-lvl)%4 mask = closest[0]&0b111 return [lvl, mask] if __name__ == "__main__": cam = cv2.VideoCapture(0) while True: ret_val, img = cam.read() if not ret_val: continue cv2.imshow("src", img) try: value = decode(img) if not value is None: print(value) except Exception as e: #pass #raise print(e) cv2.waitKey(1) cv2.destroyAllWindows()