297 lines
7.6 KiB
Python
297 lines
7.6 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
This module can be used to scan Lycacodes
|
|
|
|
(C) 2022 Louis Heredero louis.heredero@edu.vs.ch
|
|
"""
|
|
|
|
import cv2
|
|
import numpy as np
|
|
from math import sqrt
|
|
import hamming
|
|
|
|
DB_WIN = True
|
|
TOL_CNT_DIST = 20
|
|
R = 3
|
|
|
|
MASKS = [
|
|
lambda x, y: x%3 == 0,
|
|
lambda x, y: y%3 == 0,
|
|
lambda x, y: (x+y)%3 == 0,
|
|
lambda x, y: (x%3)*(y%3)==0,
|
|
lambda x, y: (y//3+x//3)%2==0,
|
|
lambda x, y: (y%3-1)*(x%3-y%3-2)*(y%3-x%3-2)==0,
|
|
lambda x, y: (abs(13-x)+abs(13-y))%3==1,
|
|
lambda x, y: (1-x%2 + max(0, abs(13-y)-abs(13-x))) * (1-y%2 + max(0,abs(13-x)-abs(13-y))) == 0
|
|
]
|
|
|
|
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 is_symbol(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) != 8:
|
|
return False
|
|
|
|
if abs(dist((cX1, cY1), (cX2, cY2))) > 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]
|
|
#bw = cv2.adaptiveThreshold(grey, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 5, 0)
|
|
bw = cv2.threshold(grey, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[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_symbol(i, contours, hierarchy):
|
|
candidates.append(i)
|
|
|
|
if DB_WIN:
|
|
for i in candidates:
|
|
cv2.drawContours(img2, contours, i, (0,0,255), 1)
|
|
cv2.drawContours(img2, contours, hierarchy[0][i][2], (0,0,255), 1)
|
|
|
|
cv2.imshow("contours", img2)
|
|
|
|
if DB_WIN:
|
|
img3 = img.copy()
|
|
cv2.drawContours(img3, contours, -1, (0,0,255), 1)
|
|
cv2.imshow("contours-all", img3)
|
|
|
|
if len(candidates) == 0:
|
|
return
|
|
|
|
for i in candidates:
|
|
i = candidates[0]
|
|
j = hierarchy[0][i][2]
|
|
cnt1, cnt2 = contours[i][::-1], contours[j]
|
|
|
|
from_ = [ cnt1[0], cnt1[1], cnt1[2], cnt1[3] ]
|
|
to = [ (0,0), (320,0), (320,320), (0,320) ]
|
|
|
|
M = cv2.getPerspectiveTransform(np.array(from_, dtype="float32"), np.array(to, dtype="float32"))
|
|
#_ = cv2.dilate(bw, (11,11))
|
|
#warped = cv2.warpPerspective(_, M, (320,320))
|
|
warped = cv2.warpPerspective(bw, M, (320,320))
|
|
|
|
|
|
if DB_WIN:
|
|
cv2.imshow("warped", warped)
|
|
|
|
s = 320/10/R
|
|
matrix = np.zeros([R*9, R*9])-1
|
|
matrix[R*4:R*5, 0:] = 0
|
|
matrix[0:, R*4:R*5] = 0
|
|
matrix[R:R*2, R*3:R*6] = 0
|
|
matrix[R*3:R*6, R:R*2] = 0
|
|
matrix[-R*2:-R, -R*6:-R*3] = 0
|
|
matrix[-R*6:-R*3, -R*2:-R] = 0
|
|
|
|
dots = warped.copy()
|
|
dots = cv2.cvtColor(dots, cv2.COLOR_GRAY2BGR)
|
|
for y in range(R*9):
|
|
cv2.line(dots, (0, int(s*R/2+(y+1)*s)), (320, int(s*R/2+(y+1)*s)), (0,255,0), 1)
|
|
cv2.line(dots, (int(s*R/2+(y+1)*s), 0), (int(s*R/2+(y+1)*s), 320), (0,255,0), 1)
|
|
for x in range(R*9):
|
|
if matrix[y, x] == 0:
|
|
X, Y = (x+R/2)*s, (y+R/2)*s
|
|
val = np.mean(warped[int(Y+s/2)-1:int(Y+s/2)+2, int(X+s/2)-1:int(X+s/2)+2])
|
|
matrix[y, x] = int(round(val/255))
|
|
cv2.circle(dots, (int(Y+s/2), int(X+s/2)), 2, (0,0,255), 1)
|
|
|
|
if DB_WIN:
|
|
cv2.imshow("dots", dots)
|
|
|
|
v = _decode(matrix)
|
|
if not v is None:
|
|
return v
|
|
|
|
return None
|
|
#return _decode(matrix)
|
|
|
|
def _decode(matrix):
|
|
OFFSETS = [[(1,0), (R-1,1)], [(R-1,1), (R,R-1)], [(1,R-1), (R-1,R)], [(0,1), (1,R-1)]]
|
|
I = None
|
|
for i in range(4):
|
|
s, e = OFFSETS[i]
|
|
dx1, dy1 = s
|
|
dx2, dy2 = e
|
|
# Find top
|
|
if (matrix[R*4+dy1:R*4+dy2, R*4+dx1:R*4+dx2] == 1).all():
|
|
I = i
|
|
|
|
|
|
if I is None:
|
|
return
|
|
|
|
# Put top on top
|
|
matrix = np.rot90(matrix, I)
|
|
|
|
# If left on right, flip
|
|
if matrix[R*5-1, R*5-1] == 1:
|
|
matrix = np.fliplr(matrix)
|
|
|
|
# If not left on left -> problem
|
|
elif matrix[R*5-1, R*4] != 1:
|
|
return
|
|
|
|
matrix[R*4:R*5, R*4:R*5] = -1
|
|
|
|
mask_i = "".join([str(int(b)) for b in matrix[0, R*4:R*5]])
|
|
mask_i = int(mask_i, 2)
|
|
matrix[0, R*4:R*5] = -1
|
|
matrix[-1, R*4:R*5] = -1
|
|
|
|
for y in range(R*9):
|
|
for x in range(R*9):
|
|
if MASKS[mask_i](x,y) and matrix[y][x] != -1:
|
|
matrix[y][x] = 1-matrix[y][x]
|
|
|
|
if DB_WIN:
|
|
img = ((matrix+2)%3)/2*255
|
|
cv2.namedWindow("matrix", cv2.WINDOW_NORMAL)
|
|
cv2.imshow("matrix", np.array(img, dtype="uint8"))
|
|
|
|
bits = []
|
|
for y in range(R*9):
|
|
for x in range(R*9):
|
|
if matrix[y, x] != -1:
|
|
bits.append(int(matrix[y,x]))
|
|
|
|
bits = np.reshape(bits, [-1, int(len(bits)/7)]).T
|
|
bits = np.reshape(np.array(bits), [-1])
|
|
bits = "".join(list(map(str, bits)))
|
|
|
|
data, errors = hamming.decode(bits, 7)
|
|
if errors > 6:
|
|
return
|
|
|
|
mode, data = int(data[:2],2), data[2:]
|
|
|
|
if mode == 0:
|
|
person = {}
|
|
type_, data = int(data[:2],2), data[2:]
|
|
id_, data = int(data[:20],2), data[20:]
|
|
|
|
person["type"] = type_
|
|
person["id"] = id_
|
|
|
|
# Student
|
|
if type_ == 0:
|
|
year, data = int(data[:3],2), data[3:]
|
|
class_, data = int(data[:4],2), data[4:]
|
|
person["year"] = year
|
|
person["class"] = class_
|
|
|
|
in1, in2, data = int(data[:5],2), int(data[5:10],2), data[10:]
|
|
in1, in2 = chr(in1+ord("A")), chr(in2+ord("A"))
|
|
|
|
person["initials"] = in1+in2
|
|
|
|
# Teacher
|
|
elif type_ == 1:
|
|
pass
|
|
|
|
# Other
|
|
elif type_ == 2:
|
|
pass
|
|
|
|
else:
|
|
print(f"Invalid person type {type_}")
|
|
|
|
data = person
|
|
|
|
elif mode == 1:
|
|
loc = {}
|
|
section, data = int(data[:3],2), data[3:]
|
|
room, data = int(data[:9],2), data[9:]
|
|
|
|
loc["section"] = section
|
|
loc["room"] = room
|
|
data = loc
|
|
|
|
elif mode == 2:
|
|
data = int(data[:32],2)
|
|
|
|
elif mode == 3:
|
|
length, data = int(data[:4],2), data[4:]
|
|
if length*8 > len(data): return
|
|
|
|
data = bytes([int(data[i*8:i*8+8],2) for i in range(length)])
|
|
try:
|
|
data = data.decode("utf-8")
|
|
|
|
except UnicodeDecodeError:
|
|
return
|
|
|
|
else:
|
|
#raise LycacodeError(f"Invalid mode {self.mode}")
|
|
print(f"Invalid mode {self.mode}")
|
|
return
|
|
|
|
return data
|
|
|
|
if __name__ == "__main__":
|
|
np.set_printoptions(linewidth=200)
|
|
cam = cv2.VideoCapture(0)
|
|
while True:
|
|
ret, img = cam.read()
|
|
|
|
data = decode(img)
|
|
|
|
if not data is None:
|
|
print(data)
|
|
|
|
cv2.imshow("src", img)
|
|
cv2.waitKey(1)
|